@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 +55 -17
- package/dist/graph.js +52 -3
- package/dist/graph.test.js +0 -0
- package/dist/graph_repro.test.js +0 -0
- package/dist/index.js +184 -7
- package/dist/lib.js +48 -1
- 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/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
|
-
|
|
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
|
-
#### `
|
|
103
|
-
|
|
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
|
-
//
|
|
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
|
}
|
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,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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
+
});
|
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"
|