@gotza02/sequential-thinking 2026.1.27 → 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
@@ -52,6 +52,14 @@ The core engine for structured problem-solving. It forces a step-by-step analysi
52
52
  #### `clear_thought_history`
53
53
  Clears the stored thinking history. Use this to start fresh or free up context.
54
54
 
55
+ #### `summarize_history`
56
+ Compresses multiple thoughts into a single summary thought. This is essential for long reasoning chains to save token context while preserving the core insights.
57
+
58
+ **Inputs:**
59
+ - `startIndex` (integer): Start of the range to summarize.
60
+ - `endIndex` (integer): End of the range to summarize.
61
+ - `summary` (string): The summary text that replaces the range.
62
+
55
63
  ### 🌐 External Knowledge
56
64
 
57
65
  #### `web_search`
@@ -82,29 +90,16 @@ Reads a webpage and converts it to clean Markdown, removing ads and navigation.
82
90
  ### 🏗 Codebase Intelligence
83
91
 
84
92
  #### `build_project_graph`
85
- **RUN THIS FIRST** when entering a new project. It scans the directory and builds a map of file dependencies using TypeScript AST analysis.
93
+ **RUN THIS FIRST** when entering a new project. It scans the directory and builds a map of file dependencies using TypeScript AST analysis. Now also extracts **exported symbols** (Functions, Classes, Variables) to provide deeper structural insight.
86
94
 
87
95
  **Inputs:**
88
96
  - `path` (string, optional): Root directory (defaults to `.`).
89
97
 
90
98
  #### `get_project_graph_summary`
91
- Returns high-level stats: total files and the top 5 most-referenced files. Use this to identify the "core" modules of the application.
92
-
93
- #### `get_file_relationships`
94
- Zoom in on a specific file to see its context.
95
-
96
- **Inputs:**
97
- - `filePath` (string, required): Path to the file (e.g., `src/index.ts`).
98
- **Returns:**
99
- - `imports`: What this file needs.
100
- - `importedBy`: Who relies on this file.
99
+ Get overview of project structure and most referenced files.
101
100
 
102
- #### `search_code`
103
- Searches for a text pattern across all code files in the project. Useful for finding usage examples or specific logic.
104
-
105
- **Inputs:**
106
- - `pattern` (string, required): Text to search for.
107
- - `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.
108
103
 
109
104
  ### 🛠 System Operations
110
105
 
@@ -121,6 +116,26 @@ Creates or overwrites a file.
121
116
  - `path` (string, required): File path.
122
117
  - `content` (string, required): The full content to write.
123
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
+
124
139
  #### `shell_execute`
125
140
  Executes a shell command. Use for running tests (`npm test`), building (`npm run build`), or file operations (`ls`, `mkdir`).
126
141
 
@@ -214,6 +229,29 @@ npm run build
214
229
  npm test
215
230
  ```
216
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
+
244
+ ## Recent Updates (v2026.1.28)
245
+ - **Robustness**:
246
+ - Implemented **Atomic Writes** for `thoughts_history.json` to prevent file corruption.
247
+ - Added **Internal Locking** to handle concurrent save requests gracefully.
248
+ - Added **API Retry Logic** with exponential backoff for all search and web tools (handles HTTP 429/5xx).
249
+ - Improved HTTP requests with browser-like headers (User-Agent) to reduce blocking.
250
+ - **New Tools**:
251
+ - `summarize_history`: Archive and condense long reasoning chains.
252
+ - **Graph Enhancements**:
253
+ - Added **Symbol Extraction**: The project graph now tracks exported functions, classes, and variables.
254
+
217
255
  ## Recent Updates (v2026.1.27)
218
256
  - **New Tools**:
219
257
  - `read_webpage`: Convert webpages to Markdown for efficient reading.
package/dist/graph.js CHANGED
@@ -14,7 +14,8 @@ export class ProjectKnowledgeGraph {
14
14
  this.nodes.set(file, {
15
15
  path: file,
16
16
  imports: [],
17
- importedBy: []
17
+ importedBy: [],
18
+ symbols: []
18
19
  });
19
20
  }
20
21
  // Step 2: Parse imports and build edges
@@ -49,8 +50,29 @@ export class ProjectKnowledgeGraph {
49
50
  const content = await fs.readFile(filePath, 'utf-8');
50
51
  const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
51
52
  const imports = [];
53
+ const symbols = [];
52
54
  const visit = (node) => {
53
- // 1. Static imports: import ... from '...'
55
+ // --- Symbols (Exports) ---
56
+ if (ts.isFunctionDeclaration(node) && node.name) {
57
+ const isExported = node.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword);
58
+ if (isExported)
59
+ symbols.push(`function:${node.name.text}`);
60
+ }
61
+ else if (ts.isClassDeclaration(node) && node.name) {
62
+ const isExported = node.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword);
63
+ if (isExported)
64
+ symbols.push(`class:${node.name.text}`);
65
+ }
66
+ else if (ts.isVariableStatement(node)) {
67
+ const isExported = node.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword);
68
+ if (isExported) {
69
+ node.declarationList.declarations.forEach(d => {
70
+ if (ts.isIdentifier(d.name))
71
+ symbols.push(`var:${d.name.text}`);
72
+ });
73
+ }
74
+ }
75
+ // --- Imports ---
54
76
  if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {
55
77
  if (node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
56
78
  imports.push(node.moduleSpecifier.text);
@@ -78,6 +100,7 @@ export class ProjectKnowledgeGraph {
78
100
  const currentNode = this.nodes.get(filePath);
79
101
  if (!currentNode)
80
102
  return;
103
+ currentNode.symbols = symbols;
81
104
  for (const importPath of imports) {
82
105
  let resolvedPath = null;
83
106
  if (importPath.startsWith('.')) {
@@ -146,7 +169,8 @@ export class ProjectKnowledgeGraph {
146
169
  return {
147
170
  path: node.path,
148
171
  imports: node.imports.map(p => path.relative(this.rootDir, p)),
149
- importedBy: node.importedBy.map(p => path.relative(this.rootDir, p))
172
+ importedBy: node.importedBy.map(p => path.relative(this.rootDir, p)),
173
+ symbols: node.symbols
150
174
  };
151
175
  }
152
176
  getSummary() {
@@ -162,4 +186,29 @@ export class ProjectKnowledgeGraph {
162
186
  }))
163
187
  };
164
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
+ }
165
214
  }
File without changes
File without changes
package/dist/index.js CHANGED
@@ -7,17 +7,51 @@ 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';
13
14
  import TurndownService from 'turndown';
14
15
  const execAsync = promisify(exec);
16
+ const DEFAULT_HEADERS = {
17
+ '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',
18
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
19
+ 'Accept-Language': 'en-US,en;q=0.9',
20
+ };
21
+ async function fetchWithRetry(url, options = {}, retries = 3, backoff = 1000) {
22
+ const fetchOptions = {
23
+ ...options,
24
+ headers: { ...DEFAULT_HEADERS, ...options.headers }
25
+ };
26
+ try {
27
+ const response = await fetch(url, fetchOptions);
28
+ if (response.status === 429 && retries > 0) {
29
+ const retryAfter = response.headers.get('Retry-After');
30
+ const waitTime = retryAfter ? parseInt(retryAfter) * 1000 : backoff;
31
+ await new Promise(resolve => setTimeout(resolve, waitTime));
32
+ return fetchWithRetry(url, options, retries - 1, backoff * 2);
33
+ }
34
+ if (!response.ok && retries > 0 && response.status >= 500) {
35
+ await new Promise(resolve => setTimeout(resolve, backoff));
36
+ return fetchWithRetry(url, options, retries - 1, backoff * 2);
37
+ }
38
+ return response;
39
+ }
40
+ catch (error) {
41
+ if (retries > 0) {
42
+ await new Promise(resolve => setTimeout(resolve, backoff));
43
+ return fetchWithRetry(url, options, retries - 1, backoff * 2);
44
+ }
45
+ throw error;
46
+ }
47
+ }
15
48
  const server = new McpServer({
16
49
  name: "sequential-thinking-server",
17
50
  version: "2026.1.18",
18
51
  });
19
52
  const thinkingServer = new SequentialThinkingServer(process.env.THOUGHTS_STORAGE_PATH || 'thoughts_history.json', parseInt(process.env.THOUGHT_DELAY_MS || '0', 10));
20
53
  const knowledgeGraph = new ProjectKnowledgeGraph();
54
+ const notesManager = new NotesManager(process.env.NOTES_STORAGE_PATH || 'project_notes.json');
21
55
  // --- Sequential Thinking Tool ---
22
56
  server.tool("sequentialthinking", `A detailed tool for dynamic and reflective problem-solving through thoughts.
23
57
  This tool helps analyze problems through a flexible thinking process that can adapt and evolve.
@@ -105,7 +139,7 @@ You should:
105
139
  // --- New Tools ---
106
140
  // 1. web_search
107
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).", {
108
- query: z.string().describe("The search query"),
142
+ query: z.string().min(1).describe("The search query"),
109
143
  provider: z.enum(['brave', 'exa', 'google']).optional().describe("Preferred search provider")
110
144
  }, async ({ query, provider }) => {
111
145
  try {
@@ -124,7 +158,7 @@ server.tool("web_search", "Search the web using Brave or Exa APIs (requires API
124
158
  if (selectedProvider === 'brave') {
125
159
  if (!process.env.BRAVE_API_KEY)
126
160
  throw new Error("BRAVE_API_KEY not found");
127
- const response = await fetch(`https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=5`, {
161
+ const response = await fetchWithRetry(`https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=5`, {
128
162
  headers: { 'X-Subscription-Token': process.env.BRAVE_API_KEY }
129
163
  });
130
164
  if (!response.ok)
@@ -135,7 +169,7 @@ server.tool("web_search", "Search the web using Brave or Exa APIs (requires API
135
169
  if (selectedProvider === 'exa') {
136
170
  if (!process.env.EXA_API_KEY)
137
171
  throw new Error("EXA_API_KEY not found");
138
- const response = await fetch('https://api.exa.ai/search', {
172
+ const response = await fetchWithRetry('https://api.exa.ai/search', {
139
173
  method: 'POST',
140
174
  headers: {
141
175
  'x-api-key': process.env.EXA_API_KEY,
@@ -153,7 +187,7 @@ server.tool("web_search", "Search the web using Brave or Exa APIs (requires API
153
187
  throw new Error("GOOGLE_SEARCH_API_KEY not found");
154
188
  if (!process.env.GOOGLE_SEARCH_CX)
155
189
  throw new Error("GOOGLE_SEARCH_CX (Search Engine ID) not found");
156
- const response = await fetch(`https://www.googleapis.com/customsearch/v1?key=${process.env.GOOGLE_SEARCH_API_KEY}&cx=${process.env.GOOGLE_SEARCH_CX}&q=${encodeURIComponent(query)}&num=5`);
190
+ 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`);
157
191
  if (!response.ok)
158
192
  throw new Error(`Google API error: ${response.statusText}`);
159
193
  const data = await response.json();
@@ -182,7 +216,7 @@ server.tool("fetch", "Perform an HTTP request to a specific URL.", {
182
216
  body: z.string().optional().describe("Request body (for POST/PUT)")
183
217
  }, async ({ url, method, headers, body }) => {
184
218
  try {
185
- const response = await fetch(url, {
219
+ const response = await fetchWithRetry(url, {
186
220
  method,
187
221
  headers: headers || {},
188
222
  body: body
@@ -324,7 +358,7 @@ server.tool("read_webpage", "Read a webpage and convert it to clean Markdown (re
324
358
  url: z.string().url().describe("The URL to read")
325
359
  }, async ({ url }) => {
326
360
  try {
327
- const response = await fetch(url);
361
+ const response = await fetchWithRetry(url);
328
362
  const html = await response.text();
329
363
  const doc = new JSDOM(html, { url });
330
364
  const reader = new Readability(doc.window.document);
@@ -353,6 +387,18 @@ server.tool("search_code", "Search for a text pattern in project files (excludes
353
387
  path: z.string().optional().default('.').describe("Root directory to search")
354
388
  }, async ({ pattern, path: searchPath }) => {
355
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
+ }
356
402
  async function searchDir(dir) {
357
403
  const results = [];
358
404
  const entries = await fs.readdir(dir, { withFileTypes: true });
@@ -372,7 +418,7 @@ server.tool("search_code", "Search for a text pattern in project files (excludes
372
418
  }
373
419
  return results;
374
420
  }
375
- const matches = await searchDir(path.resolve(searchPath || '.'));
421
+ const matches = await searchDir(resolvedPath);
376
422
  return {
377
423
  content: [{
378
424
  type: "text",
@@ -394,6 +440,137 @@ server.tool("clear_thought_history", "Clear the sequential thinking history.", {
394
440
  content: [{ type: "text", text: "Thought history cleared." }]
395
441
  };
396
442
  });
443
+ // 12. summarize_history
444
+ server.tool("summarize_history", "Compress multiple thoughts into a single summary thought to save space/context.", {
445
+ startIndex: z.number().int().min(1).describe("The starting thought number to summarize"),
446
+ endIndex: z.number().int().min(1).describe("The ending thought number to summarize"),
447
+ summary: z.string().describe("The summary text that replaces the range")
448
+ }, async ({ startIndex, endIndex, summary }) => {
449
+ try {
450
+ const result = await thinkingServer.archiveHistory(startIndex, endIndex, summary);
451
+ return {
452
+ content: [{ type: "text", text: `Successfully summarized thoughts ${startIndex}-${endIndex}. New history length: ${result.newHistoryLength}` }]
453
+ };
454
+ }
455
+ catch (error) {
456
+ return {
457
+ content: [{ type: "text", text: `Archive Error: ${error instanceof Error ? error.message : String(error)}` }],
458
+ isError: true
459
+ };
460
+ }
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
+ });
397
574
  runServer().catch((error) => {
398
575
  console.error("Fatal error running server:", error);
399
576
  process.exit(1);
package/dist/lib.js CHANGED
@@ -8,6 +8,7 @@ export class SequentialThinkingServer {
8
8
  disableThoughtLogging;
9
9
  storagePath;
10
10
  delayMs;
11
+ isSaving = false;
11
12
  constructor(storagePath = 'thoughts_history.json', delayMs = 0) {
12
13
  this.disableThoughtLogging = (process.env.DISABLE_THOUGHT_LOGGING || "").toLowerCase() === "true";
13
14
  this.storagePath = path.resolve(storagePath);
@@ -31,18 +32,64 @@ export class SequentialThinkingServer {
31
32
  }
32
33
  }
33
34
  async saveHistory() {
35
+ if (this.isSaving) {
36
+ // Simple retry if already saving
37
+ setTimeout(() => this.saveHistory(), 100);
38
+ return;
39
+ }
40
+ this.isSaving = true;
34
41
  try {
35
- await fs.writeFile(this.storagePath, JSON.stringify(this.thoughtHistory, null, 2), 'utf-8');
42
+ // Atomic write: write to tmp then rename
43
+ const tmpPath = `${this.storagePath}.tmp`;
44
+ await fs.writeFile(tmpPath, JSON.stringify(this.thoughtHistory, null, 2), 'utf-8');
45
+ await fs.rename(tmpPath, this.storagePath);
36
46
  }
37
47
  catch (error) {
38
48
  console.error(`Error saving history to ${this.storagePath}:`, error);
39
49
  }
50
+ finally {
51
+ this.isSaving = false;
52
+ }
40
53
  }
41
54
  async clearHistory() {
42
55
  this.thoughtHistory = [];
43
56
  this.branches = {};
44
57
  await this.saveHistory();
45
58
  }
59
+ async archiveHistory(startIndex, endIndex, summary) {
60
+ if (startIndex < 1 || endIndex > this.thoughtHistory.length || startIndex > endIndex) {
61
+ throw new Error(`Invalid range: ${startIndex} to ${endIndex}. History length is ${this.thoughtHistory.length}.`);
62
+ }
63
+ const summaryThought = {
64
+ thought: `SUMMARY [${startIndex}-${endIndex}]: ${summary}`,
65
+ thoughtNumber: startIndex,
66
+ totalThoughts: this.thoughtHistory[this.thoughtHistory.length - 1].totalThoughts - (endIndex - startIndex),
67
+ nextThoughtNeeded: true,
68
+ thoughtType: 'analysis'
69
+ };
70
+ // Remove the range and insert summary
71
+ const removedCount = endIndex - startIndex + 1;
72
+ this.thoughtHistory.splice(startIndex - 1, removedCount, summaryThought);
73
+ // Renumber subsequent thoughts
74
+ for (let i = startIndex; i < this.thoughtHistory.length; i++) {
75
+ this.thoughtHistory[i].thoughtNumber -= (removedCount - 1);
76
+ }
77
+ // Rebuild branches (simplification: clear and let it rebuild if needed, or just clear)
78
+ this.branches = {};
79
+ this.thoughtHistory.forEach(t => {
80
+ if (t.branchFromThought && t.branchId) {
81
+ const branchKey = `${t.branchFromThought}-${t.branchId}`;
82
+ if (!this.branches[branchKey])
83
+ this.branches[branchKey] = [];
84
+ this.branches[branchKey].push(t);
85
+ }
86
+ });
87
+ await this.saveHistory();
88
+ return {
89
+ newHistoryLength: this.thoughtHistory.length,
90
+ summaryInsertedAt: startIndex
91
+ };
92
+ }
46
93
  addToMemory(input) {
47
94
  if (input.thoughtNumber > input.totalThoughts) {
48
95
  input.totalThoughts = input.thoughtNumber;
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.27",
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"