@gotza02/sequential-thinking 2026.2.9 → 2026.2.11
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 +4 -2
- package/dist/chaos.test.js +72 -0
- package/dist/codestore.js +34 -22
- package/dist/codestore.test.js +59 -0
- package/dist/coding.test.js +105 -8
- package/dist/e2e.test.js +122 -0
- package/dist/filesystem.test.js +174 -0
- package/dist/graph.js +13 -5
- package/dist/graph.test.js +53 -0
- package/dist/integration.test.js +58 -0
- package/dist/lib.js +29 -4
- package/dist/notes.js +81 -52
- package/dist/notes.test.js +74 -0
- package/dist/registration.test.js +39 -0
- package/dist/repro_dollar.js +30 -0
- package/dist/repro_dollar_simple.js +22 -0
- package/dist/repro_history.js +41 -0
- package/dist/repro_path.js +17 -0
- package/dist/repro_ts_req.js +3 -0
- package/dist/server.test.js +32 -0
- package/dist/stress.test.js +68 -0
- package/dist/test_ts_req.js +46 -0
- package/dist/tools/coding.js +15 -1
- package/dist/tools/filesystem.js +12 -9
- package/dist/tools/graph.js +3 -1
- package/dist/tools/web.js +7 -2
- package/dist/utils.js +103 -2
- package/dist/utils.test.js +40 -0
- package/dist/web_read.test.js +60 -0
- package/package.json +1 -1
package/dist/graph.js
CHANGED
|
@@ -103,6 +103,14 @@ export class ProjectKnowledgeGraph {
|
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
|
+
// 4. Import Equals: import x = require('...')
|
|
107
|
+
else if (ts.isImportEqualsDeclaration(node)) {
|
|
108
|
+
if (ts.isExternalModuleReference(node.moduleReference)) {
|
|
109
|
+
if (ts.isStringLiteral(node.moduleReference.expression)) {
|
|
110
|
+
imports.push(node.moduleReference.expression.text);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
106
114
|
ts.forEachChild(node, visit);
|
|
107
115
|
};
|
|
108
116
|
visit(sourceFile);
|
|
@@ -121,13 +129,13 @@ export class ProjectKnowledgeGraph {
|
|
|
121
129
|
// Basic Regex for generic symbols and imports
|
|
122
130
|
if (ext === '.py') {
|
|
123
131
|
// Python: import x, from x import y, def func, class Cls
|
|
124
|
-
const importMatches = content.matchAll(
|
|
132
|
+
const importMatches = content.matchAll(/^\s*(?:import|from)\s+([a-zA-Z0-9_.]+)/gm);
|
|
125
133
|
for (const match of importMatches)
|
|
126
134
|
imports.push(match[1]);
|
|
127
|
-
const funcMatches = content.matchAll(
|
|
135
|
+
const funcMatches = content.matchAll(/^\s*def\s+([a-zA-Z0-9_]+)/gm);
|
|
128
136
|
for (const match of funcMatches)
|
|
129
137
|
symbols.push(`def:${match[1]}`);
|
|
130
|
-
const classMatches = content.matchAll(
|
|
138
|
+
const classMatches = content.matchAll(/^\s*class\s+([a-zA-Z0-9_]+)/gm);
|
|
131
139
|
for (const match of classMatches)
|
|
132
140
|
symbols.push(`class:${match[1]}`);
|
|
133
141
|
}
|
|
@@ -136,7 +144,7 @@ export class ProjectKnowledgeGraph {
|
|
|
136
144
|
const importMatches = content.matchAll(/import\s+"([^"]+)"/g);
|
|
137
145
|
for (const match of importMatches)
|
|
138
146
|
imports.push(match[1]);
|
|
139
|
-
const funcMatches = content.matchAll(
|
|
147
|
+
const funcMatches = content.matchAll(/^\s*func\s+([a-zA-Z0-9_]+)/gm);
|
|
140
148
|
for (const match of funcMatches)
|
|
141
149
|
symbols.push(`func:${match[1]}`);
|
|
142
150
|
}
|
|
@@ -176,7 +184,7 @@ export class ProjectKnowledgeGraph {
|
|
|
176
184
|
return absolutePath;
|
|
177
185
|
}
|
|
178
186
|
// 2. Try appending extensions
|
|
179
|
-
const extensions = ['.ts', '.js', '.tsx', '.jsx', '.json', '/index.ts', '/index.js'];
|
|
187
|
+
const extensions = ['.ts', '.js', '.tsx', '.jsx', '.json', '.py', '.go', '.rs', '.java', '.c', '.cpp', '.h', '/index.ts', '/index.js'];
|
|
180
188
|
for (const ext of extensions) {
|
|
181
189
|
const p = absolutePath + ext;
|
|
182
190
|
if (this.nodes.has(p)) {
|
package/dist/graph.test.js
CHANGED
|
@@ -76,4 +76,57 @@ describe('ProjectKnowledgeGraph', () => {
|
|
|
76
76
|
const relationships = graph.getRelationships('/app/index.js');
|
|
77
77
|
expect(relationships?.imports).toContain('Button.jsx');
|
|
78
78
|
});
|
|
79
|
+
it('should handle circular dependencies', async () => {
|
|
80
|
+
const contentA = `import { b } from './b'; export const a = 1;`;
|
|
81
|
+
const contentB = `import { a } from './a'; export const b = 2;`;
|
|
82
|
+
fs.readdir.mockResolvedValue([
|
|
83
|
+
{ name: 'a.ts', isDirectory: () => false },
|
|
84
|
+
{ name: 'b.ts', isDirectory: () => false }
|
|
85
|
+
]);
|
|
86
|
+
fs.readFile.mockImplementation(async (filePath) => {
|
|
87
|
+
if (filePath.endsWith('a.ts'))
|
|
88
|
+
return contentA;
|
|
89
|
+
if (filePath.endsWith('b.ts'))
|
|
90
|
+
return contentB;
|
|
91
|
+
return '';
|
|
92
|
+
});
|
|
93
|
+
await graph.build('/app');
|
|
94
|
+
const relA = graph.getRelationships('/app/a.ts');
|
|
95
|
+
const relB = graph.getRelationships('/app/b.ts');
|
|
96
|
+
expect(relA?.imports).toContain('b.ts');
|
|
97
|
+
expect(relA?.importedBy).toContain('b.ts');
|
|
98
|
+
expect(relB?.imports).toContain('a.ts');
|
|
99
|
+
expect(relB?.importedBy).toContain('a.ts');
|
|
100
|
+
});
|
|
101
|
+
it('should gracefully handle missing imports', async () => {
|
|
102
|
+
const contentA = `import { ghost } from './ghost';`;
|
|
103
|
+
fs.readdir.mockResolvedValue([
|
|
104
|
+
{ name: 'a.ts', isDirectory: () => false }
|
|
105
|
+
]);
|
|
106
|
+
fs.readFile.mockImplementation(async (filePath) => {
|
|
107
|
+
if (filePath.endsWith('a.ts'))
|
|
108
|
+
return contentA;
|
|
109
|
+
return '';
|
|
110
|
+
});
|
|
111
|
+
await graph.build('/app');
|
|
112
|
+
const relA = graph.getRelationships('/app/a.ts');
|
|
113
|
+
// ghost.ts doesn't exist, so imports should be empty (filtered out)
|
|
114
|
+
expect(relA?.imports).toHaveLength(0);
|
|
115
|
+
});
|
|
116
|
+
it('should ignore directory traversal attempts outside root', async () => {
|
|
117
|
+
// If we pretend root is /app, and we try to import ../outside
|
|
118
|
+
const contentA = `import { secret } from '../secret';`;
|
|
119
|
+
fs.readdir.mockResolvedValue([
|
|
120
|
+
{ name: 'a.ts', isDirectory: () => false }
|
|
121
|
+
]);
|
|
122
|
+
fs.readFile.mockImplementation(async (filePath) => {
|
|
123
|
+
if (filePath.endsWith('a.ts'))
|
|
124
|
+
return contentA;
|
|
125
|
+
return '';
|
|
126
|
+
});
|
|
127
|
+
await graph.build('/app');
|
|
128
|
+
const relA = graph.getRelationships('/app/a.ts');
|
|
129
|
+
// Should be empty because '../secret' is not in the scanned file list (nodes)
|
|
130
|
+
expect(relA?.imports).toHaveLength(0);
|
|
131
|
+
});
|
|
79
132
|
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
// We need to import the register functions to get the callbacks,
|
|
3
|
+
// but we can just use the classes directly for this integration logic test.
|
|
4
|
+
// Actually, testing the *interaction* via the tool layer is better.
|
|
5
|
+
import { registerThinkingTools } from './tools/thinking.js';
|
|
6
|
+
import { registerGraphTools } from './tools/graph.js';
|
|
7
|
+
import { registerNoteTools } from './tools/notes.js';
|
|
8
|
+
describe('Integration Workflow', () => {
|
|
9
|
+
let toolCallbacks = {};
|
|
10
|
+
const mockServer = {
|
|
11
|
+
tool: (name, desc, schema, cb) => {
|
|
12
|
+
toolCallbacks[name] = cb;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
// Mocks
|
|
16
|
+
const mockThinking = { processThought: vi.fn(), clearHistory: vi.fn() };
|
|
17
|
+
const mockGraph = { build: vi.fn(), getRelationships: vi.fn() };
|
|
18
|
+
const mockNotes = { addNote: vi.fn() };
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
toolCallbacks = {};
|
|
21
|
+
registerThinkingTools(mockServer, mockThinking);
|
|
22
|
+
registerGraphTools(mockServer, mockGraph);
|
|
23
|
+
registerNoteTools(mockServer, mockNotes);
|
|
24
|
+
vi.clearAllMocks();
|
|
25
|
+
});
|
|
26
|
+
it('should support a full analysis workflow', async () => {
|
|
27
|
+
// Step 1: Start Thinking
|
|
28
|
+
mockThinking.processThought.mockResolvedValue({
|
|
29
|
+
content: [{ type: "text", text: "Thought processed" }]
|
|
30
|
+
});
|
|
31
|
+
await toolCallbacks['sequentialthinking']({
|
|
32
|
+
thought: "Analyze architecture",
|
|
33
|
+
thoughtNumber: 1,
|
|
34
|
+
totalThoughts: 5,
|
|
35
|
+
nextThoughtNeeded: true,
|
|
36
|
+
thoughtType: 'analysis'
|
|
37
|
+
});
|
|
38
|
+
expect(mockThinking.processThought).toHaveBeenCalledWith(expect.objectContaining({
|
|
39
|
+
thought: "Analyze architecture"
|
|
40
|
+
}));
|
|
41
|
+
// Step 2: Build Graph
|
|
42
|
+
mockGraph.build.mockResolvedValue({ nodeCount: 10, totalFiles: 20 });
|
|
43
|
+
await toolCallbacks['build_project_graph']({ path: '.' });
|
|
44
|
+
expect(mockGraph.build).toHaveBeenCalled();
|
|
45
|
+
// Step 3: Get Relationships
|
|
46
|
+
mockGraph.getRelationships.mockReturnValue({ imports: ['utils.ts'] });
|
|
47
|
+
const relResult = await toolCallbacks['get_file_relationships']({ filePath: 'src/index.ts' });
|
|
48
|
+
expect(JSON.parse(relResult.content[0].text).imports).toContain('utils.ts');
|
|
49
|
+
// Step 4: Add Note
|
|
50
|
+
mockNotes.addNote.mockResolvedValue({ id: '123', title: 'Arch Note' });
|
|
51
|
+
await toolCallbacks['manage_notes']({
|
|
52
|
+
action: 'add',
|
|
53
|
+
title: 'Architecture Review',
|
|
54
|
+
content: 'Found circular deps'
|
|
55
|
+
});
|
|
56
|
+
expect(mockNotes.addNote).toHaveBeenCalledWith('Architecture Review', 'Found circular deps', undefined, undefined, undefined);
|
|
57
|
+
});
|
|
58
|
+
});
|
package/dist/lib.js
CHANGED
|
@@ -9,6 +9,7 @@ export class SequentialThinkingServer {
|
|
|
9
9
|
storagePath;
|
|
10
10
|
delayMs;
|
|
11
11
|
isSaving = false;
|
|
12
|
+
hasPendingSave = false;
|
|
12
13
|
constructor(storagePath = 'thoughts_history.json', delayMs = 0) {
|
|
13
14
|
this.disableThoughtLogging = (process.env.DISABLE_THOUGHT_LOGGING || "").toLowerCase() === "true";
|
|
14
15
|
this.storagePath = path.resolve(storagePath);
|
|
@@ -33,8 +34,7 @@ export class SequentialThinkingServer {
|
|
|
33
34
|
}
|
|
34
35
|
async saveHistory() {
|
|
35
36
|
if (this.isSaving) {
|
|
36
|
-
|
|
37
|
-
setTimeout(() => this.saveHistory(), 100);
|
|
37
|
+
this.hasPendingSave = true;
|
|
38
38
|
return;
|
|
39
39
|
}
|
|
40
40
|
this.isSaving = true;
|
|
@@ -49,6 +49,10 @@ export class SequentialThinkingServer {
|
|
|
49
49
|
}
|
|
50
50
|
finally {
|
|
51
51
|
this.isSaving = false;
|
|
52
|
+
if (this.hasPendingSave) {
|
|
53
|
+
this.hasPendingSave = false;
|
|
54
|
+
this.saveHistory();
|
|
55
|
+
}
|
|
52
56
|
}
|
|
53
57
|
}
|
|
54
58
|
async clearHistory() {
|
|
@@ -70,9 +74,30 @@ export class SequentialThinkingServer {
|
|
|
70
74
|
// Remove the range and insert summary
|
|
71
75
|
const removedCount = endIndex - startIndex + 1;
|
|
72
76
|
this.thoughtHistory.splice(startIndex - 1, removedCount, summaryThought);
|
|
73
|
-
// Renumber subsequent thoughts
|
|
77
|
+
// Renumber subsequent thoughts and update references
|
|
78
|
+
const shiftAmount = removedCount - 1;
|
|
74
79
|
for (let i = startIndex; i < this.thoughtHistory.length; i++) {
|
|
75
|
-
this.thoughtHistory[i]
|
|
80
|
+
const t = this.thoughtHistory[i];
|
|
81
|
+
// Update own number
|
|
82
|
+
t.thoughtNumber -= shiftAmount;
|
|
83
|
+
// Update references (branchFromThought)
|
|
84
|
+
if (t.branchFromThought) {
|
|
85
|
+
if (t.branchFromThought > endIndex) {
|
|
86
|
+
t.branchFromThought -= shiftAmount;
|
|
87
|
+
}
|
|
88
|
+
else if (t.branchFromThought >= startIndex) {
|
|
89
|
+
t.branchFromThought = startIndex; // Point to summary
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Update references (revisesThought)
|
|
93
|
+
if (t.revisesThought) {
|
|
94
|
+
if (t.revisesThought > endIndex) {
|
|
95
|
+
t.revisesThought -= shiftAmount;
|
|
96
|
+
}
|
|
97
|
+
else if (t.revisesThought >= startIndex) {
|
|
98
|
+
t.revisesThought = startIndex; // Point to summary
|
|
99
|
+
}
|
|
100
|
+
}
|
|
76
101
|
}
|
|
77
102
|
// Rebuild branches (simplification: clear and let it rebuild if needed, or just clear)
|
|
78
103
|
this.branches = {};
|
package/dist/notes.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import * as fs from 'fs/promises';
|
|
2
2
|
import * as path from 'path';
|
|
3
|
+
import { AsyncMutex } from './utils.js';
|
|
3
4
|
export class NotesManager {
|
|
4
5
|
filePath;
|
|
5
6
|
notes = [];
|
|
6
7
|
loaded = false;
|
|
8
|
+
mutex = new AsyncMutex();
|
|
7
9
|
constructor(storagePath = 'project_notes.json') {
|
|
8
10
|
this.filePath = path.resolve(storagePath);
|
|
9
11
|
}
|
|
@@ -15,8 +17,25 @@ export class NotesManager {
|
|
|
15
17
|
this.notes = JSON.parse(data);
|
|
16
18
|
}
|
|
17
19
|
catch (error) {
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
+
// Case 1: File doesn't exist (Normal first run)
|
|
21
|
+
if (error.code === 'ENOENT') {
|
|
22
|
+
this.notes = [];
|
|
23
|
+
}
|
|
24
|
+
// Case 2: Corrupted JSON or other read errors
|
|
25
|
+
else {
|
|
26
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
27
|
+
const backupPath = `${this.filePath}.bak.${timestamp}`;
|
|
28
|
+
try {
|
|
29
|
+
// Try to backup the corrupted file
|
|
30
|
+
await fs.rename(this.filePath, backupPath);
|
|
31
|
+
console.error(`[NotesManager] Error reading notes file. Corrupted file backed up to: ${backupPath}`);
|
|
32
|
+
}
|
|
33
|
+
catch (backupError) {
|
|
34
|
+
console.error(`[NotesManager] Critical: Failed to backup corrupted notes file: ${backupError}`);
|
|
35
|
+
}
|
|
36
|
+
// Initialize empty to allow system to recover
|
|
37
|
+
this.notes = [];
|
|
38
|
+
}
|
|
20
39
|
}
|
|
21
40
|
this.loaded = true;
|
|
22
41
|
}
|
|
@@ -24,64 +43,74 @@ export class NotesManager {
|
|
|
24
43
|
await fs.writeFile(this.filePath, JSON.stringify(this.notes, null, 2), 'utf-8');
|
|
25
44
|
}
|
|
26
45
|
async addNote(title, content, tags = [], priority = 'medium', expiresAt) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
46
|
+
return this.mutex.dispatch(async () => {
|
|
47
|
+
await this.load();
|
|
48
|
+
const note = {
|
|
49
|
+
id: Date.now().toString(36) + Math.random().toString(36).substring(2, 7),
|
|
50
|
+
title,
|
|
51
|
+
content,
|
|
52
|
+
tags,
|
|
53
|
+
priority,
|
|
54
|
+
expiresAt,
|
|
55
|
+
createdAt: new Date().toISOString(),
|
|
56
|
+
updatedAt: new Date().toISOString()
|
|
57
|
+
};
|
|
58
|
+
this.notes.push(note);
|
|
59
|
+
await this.save();
|
|
60
|
+
return note;
|
|
61
|
+
});
|
|
41
62
|
}
|
|
42
63
|
async listNotes(tag, includeExpired = false) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
64
|
+
return this.mutex.dispatch(async () => {
|
|
65
|
+
await this.load();
|
|
66
|
+
const now = new Date();
|
|
67
|
+
let activeNotes = this.notes;
|
|
68
|
+
if (!includeExpired) {
|
|
69
|
+
activeNotes = this.notes.filter(n => !n.expiresAt || new Date(n.expiresAt) > now);
|
|
70
|
+
}
|
|
71
|
+
if (tag) {
|
|
72
|
+
return activeNotes.filter(n => n.tags.includes(tag));
|
|
73
|
+
}
|
|
74
|
+
return activeNotes.sort((a, b) => {
|
|
75
|
+
const priorityMap = { 'critical': 4, 'high': 3, 'medium': 2, 'low': 1 };
|
|
76
|
+
return (priorityMap[b.priority] || 0) - (priorityMap[a.priority] || 0);
|
|
77
|
+
});
|
|
55
78
|
});
|
|
56
79
|
}
|
|
57
80
|
async searchNotes(query) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
n.
|
|
62
|
-
|
|
81
|
+
return this.mutex.dispatch(async () => {
|
|
82
|
+
await this.load();
|
|
83
|
+
const lowerQuery = query.toLowerCase();
|
|
84
|
+
return this.notes.filter(n => n.title.toLowerCase().includes(lowerQuery) ||
|
|
85
|
+
n.content.toLowerCase().includes(lowerQuery) ||
|
|
86
|
+
n.tags.some(t => t.toLowerCase().includes(lowerQuery)));
|
|
87
|
+
});
|
|
63
88
|
}
|
|
64
89
|
async deleteNote(id) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
90
|
+
return this.mutex.dispatch(async () => {
|
|
91
|
+
await this.load();
|
|
92
|
+
const initialLength = this.notes.length;
|
|
93
|
+
this.notes = this.notes.filter(n => n.id !== id);
|
|
94
|
+
if (this.notes.length !== initialLength) {
|
|
95
|
+
await this.save();
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
});
|
|
73
100
|
}
|
|
74
101
|
async updateNote(id, updates) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
102
|
+
return this.mutex.dispatch(async () => {
|
|
103
|
+
await this.load();
|
|
104
|
+
const index = this.notes.findIndex(n => n.id === id);
|
|
105
|
+
if (index === -1)
|
|
106
|
+
return null;
|
|
107
|
+
this.notes[index] = {
|
|
108
|
+
...this.notes[index],
|
|
109
|
+
...updates,
|
|
110
|
+
updatedAt: new Date().toISOString()
|
|
111
|
+
};
|
|
112
|
+
await this.save();
|
|
113
|
+
return this.notes[index];
|
|
114
|
+
});
|
|
86
115
|
}
|
|
87
116
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
|
2
|
+
import { NotesManager } from './notes.js';
|
|
3
|
+
import * as fs from 'fs/promises';
|
|
4
|
+
vi.mock('fs/promises');
|
|
5
|
+
describe('NotesManager', () => {
|
|
6
|
+
let manager;
|
|
7
|
+
const testPath = 'test_notes.json';
|
|
8
|
+
let mockStore = '[]';
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
mockStore = '[]';
|
|
11
|
+
fs.readFile.mockImplementation(async () => mockStore);
|
|
12
|
+
fs.writeFile.mockImplementation(async (path, data) => {
|
|
13
|
+
mockStore = data;
|
|
14
|
+
});
|
|
15
|
+
manager = new NotesManager(testPath);
|
|
16
|
+
});
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
vi.clearAllMocks();
|
|
19
|
+
});
|
|
20
|
+
it('should add a note correctly', async () => {
|
|
21
|
+
const note = await manager.addNote("Test Title", "Test Content", ["tag1"]);
|
|
22
|
+
expect(note.title).toBe("Test Title");
|
|
23
|
+
expect(note.tags).toContain("tag1");
|
|
24
|
+
expect(JSON.parse(mockStore)).toHaveLength(1);
|
|
25
|
+
});
|
|
26
|
+
it('should list notes with sorting by priority', async () => {
|
|
27
|
+
await manager.addNote("Low Prio", "Content", [], "low");
|
|
28
|
+
await manager.addNote("High Prio", "Content", [], "high");
|
|
29
|
+
const notes = await manager.listNotes();
|
|
30
|
+
expect(notes[0].priority).toBe('high');
|
|
31
|
+
expect(notes[1].priority).toBe('low');
|
|
32
|
+
});
|
|
33
|
+
it('should filter notes by tag', async () => {
|
|
34
|
+
await manager.addNote("Note 1", "Content", ["react"]);
|
|
35
|
+
await manager.addNote("Note 2", "Content", ["vue"]);
|
|
36
|
+
const reactNotes = await manager.listNotes('react');
|
|
37
|
+
expect(reactNotes).toHaveLength(1);
|
|
38
|
+
expect(reactNotes[0].title).toBe("Note 1");
|
|
39
|
+
});
|
|
40
|
+
it('should search notes by query', async () => {
|
|
41
|
+
await manager.addNote("Deploy Script", "Run npm build", ["devops"]);
|
|
42
|
+
await manager.addNote("Meeting Notes", "Discuss API", ["meeting"]);
|
|
43
|
+
const results = await manager.searchNotes("build");
|
|
44
|
+
expect(results).toHaveLength(1);
|
|
45
|
+
expect(results[0].title).toBe("Deploy Script");
|
|
46
|
+
});
|
|
47
|
+
it('should update a note', async () => {
|
|
48
|
+
const note = await manager.addNote("Original", "Content");
|
|
49
|
+
const updated = await manager.updateNote(note.id, { title: "Updated" });
|
|
50
|
+
expect(updated?.title).toBe("Updated");
|
|
51
|
+
expect(updated?.content).toBe("Content"); // Should remain
|
|
52
|
+
const list = await manager.listNotes();
|
|
53
|
+
expect(list[0].title).toBe("Updated");
|
|
54
|
+
});
|
|
55
|
+
it('should delete a note', async () => {
|
|
56
|
+
const note = await manager.addNote("To Delete", "Content");
|
|
57
|
+
const success = await manager.deleteNote(note.id);
|
|
58
|
+
expect(success).toBe(true);
|
|
59
|
+
const list = await manager.listNotes();
|
|
60
|
+
expect(list).toHaveLength(0);
|
|
61
|
+
});
|
|
62
|
+
it('should hide expired notes by default', async () => {
|
|
63
|
+
// Expired yesterday
|
|
64
|
+
const yesterday = new Date();
|
|
65
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
66
|
+
await manager.addNote("Expired", "Content", [], "medium", yesterday.toISOString());
|
|
67
|
+
await manager.addNote("Active", "Content");
|
|
68
|
+
const list = await manager.listNotes();
|
|
69
|
+
expect(list).toHaveLength(1);
|
|
70
|
+
expect(list[0].title).toBe("Active");
|
|
71
|
+
const all = await manager.listNotes(undefined, true);
|
|
72
|
+
expect(all).toHaveLength(2);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { registerThinkingTools } from './tools/thinking.js';
|
|
3
|
+
import { registerGraphTools } from './tools/graph.js';
|
|
4
|
+
import { registerNoteTools } from './tools/notes.js';
|
|
5
|
+
import { registerWebTools } from './tools/web.js';
|
|
6
|
+
import { registerFileSystemTools } from './tools/filesystem.js';
|
|
7
|
+
import { registerCodingTools } from './tools/coding.js';
|
|
8
|
+
import { registerCodeDbTools } from './tools/codestore.js';
|
|
9
|
+
describe('Tool Registration', () => {
|
|
10
|
+
it('should register all tools without duplicates', () => {
|
|
11
|
+
const registeredTools = new Set();
|
|
12
|
+
const mockServer = {
|
|
13
|
+
tool: (name, desc, schema, cb) => {
|
|
14
|
+
if (registeredTools.has(name)) {
|
|
15
|
+
throw new Error(`Duplicate tool name: ${name}`);
|
|
16
|
+
}
|
|
17
|
+
registeredTools.add(name);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
// Mock dependencies
|
|
21
|
+
const mockThinking = { processThought: vi.fn(), clearHistory: vi.fn(), archiveHistory: vi.fn() };
|
|
22
|
+
const mockGraph = { build: vi.fn(), getRelationships: vi.fn(), getSummary: vi.fn(), toMermaid: vi.fn() };
|
|
23
|
+
const mockNotes = {};
|
|
24
|
+
const mockCodeDb = {};
|
|
25
|
+
registerThinkingTools(mockServer, mockThinking);
|
|
26
|
+
registerGraphTools(mockServer, mockGraph);
|
|
27
|
+
registerNoteTools(mockServer, mockNotes);
|
|
28
|
+
registerWebTools(mockServer);
|
|
29
|
+
registerFileSystemTools(mockServer);
|
|
30
|
+
registerCodingTools(mockServer, mockGraph);
|
|
31
|
+
registerCodeDbTools(mockServer, mockCodeDb);
|
|
32
|
+
expect(registeredTools.has('sequentialthinking')).toBe(true);
|
|
33
|
+
expect(registeredTools.has('build_project_graph')).toBe(true);
|
|
34
|
+
expect(registeredTools.has('read_file')).toBe(true);
|
|
35
|
+
expect(registeredTools.has('web_search')).toBe(true);
|
|
36
|
+
// ... and so on
|
|
37
|
+
console.log('Registered tools:', Array.from(registeredTools).join(', '));
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { registerFileSystemTools } from './tools/filesystem.js';
|
|
3
|
+
// Mock server
|
|
4
|
+
const server = new McpServer({ name: "test", version: "1" });
|
|
5
|
+
// Mock tool registration to capture the handler
|
|
6
|
+
let editHandler;
|
|
7
|
+
server.tool = (name, desc, schema, handler) => {
|
|
8
|
+
if (name === 'edit_file')
|
|
9
|
+
editHandler = handler;
|
|
10
|
+
return undefined;
|
|
11
|
+
};
|
|
12
|
+
registerFileSystemTools(server);
|
|
13
|
+
async function test() {
|
|
14
|
+
console.log("Testing edit_file with dollar signs...");
|
|
15
|
+
const result = await editHandler({
|
|
16
|
+
path: 'test_dollar.txt',
|
|
17
|
+
oldText: 'OLD',
|
|
18
|
+
newText: '$100' // This usually becomes empty or weird if interpreted as regex replacement
|
|
19
|
+
});
|
|
20
|
+
const fs = await import('fs/promises');
|
|
21
|
+
const content = await fs.readFile('test_dollar.txt', 'utf-8');
|
|
22
|
+
console.log("File content:", content.trim());
|
|
23
|
+
if (content.trim() === 'Price: $100') {
|
|
24
|
+
console.log("PASS: $ preserved");
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
console.log("FAIL: $ corrupted");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
test();
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
async function test() {
|
|
2
|
+
console.log("Testing edit_file logic...");
|
|
3
|
+
// Simulate the logic in filesystem.ts
|
|
4
|
+
const content = "Price: OLD";
|
|
5
|
+
const oldText = "OLD";
|
|
6
|
+
const newText = "$&"; // Should be "$&" literally, but replace will make it "OLD"
|
|
7
|
+
// Logic from tool
|
|
8
|
+
// If allowMultiple is false, it passes string directly
|
|
9
|
+
const buggedResult = content.replace(oldText, newText);
|
|
10
|
+
console.log(`Original: "${content}"`);
|
|
11
|
+
console.log(`Old: "${oldText}"`);
|
|
12
|
+
console.log(`New: "${newText}"`);
|
|
13
|
+
console.log(`Result: "${buggedResult}"`);
|
|
14
|
+
if (buggedResult === "Price: OLD") {
|
|
15
|
+
console.log("FAIL: $& was interpreted as the matched string");
|
|
16
|
+
}
|
|
17
|
+
else if (buggedResult === "Price: $&") {
|
|
18
|
+
console.log("PASS: $& was preserved literally");
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
test();
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { SequentialThinkingServer } from './lib.js';
|
|
2
|
+
import * as fs from 'fs/promises';
|
|
3
|
+
async function test() {
|
|
4
|
+
const server = new SequentialThinkingServer('test_history_bug.json');
|
|
5
|
+
await server.clearHistory();
|
|
6
|
+
// Create thoughts: 1, 2, 3, 4
|
|
7
|
+
await server.processThought({ thought: "1", thoughtNumber: 1, totalThoughts: 4, nextThoughtNeeded: true });
|
|
8
|
+
await server.processThought({ thought: "2", thoughtNumber: 2, totalThoughts: 4, nextThoughtNeeded: true });
|
|
9
|
+
await server.processThought({ thought: "3", thoughtNumber: 3, totalThoughts: 4, nextThoughtNeeded: true });
|
|
10
|
+
// Thought 4 branches from 3
|
|
11
|
+
await server.processThought({
|
|
12
|
+
thought: "4",
|
|
13
|
+
thoughtNumber: 4,
|
|
14
|
+
totalThoughts: 4,
|
|
15
|
+
nextThoughtNeeded: false,
|
|
16
|
+
branchFromThought: 3,
|
|
17
|
+
branchId: "test"
|
|
18
|
+
});
|
|
19
|
+
// Verify initial state
|
|
20
|
+
// History: [1, 2, 3, 4(from 3)]
|
|
21
|
+
// Summarize 2-3
|
|
22
|
+
await server.archiveHistory(2, 3, "Summary of 2 and 3");
|
|
23
|
+
// Expected History: [1, Summary(2), 4(3)]
|
|
24
|
+
// Thought 4 should now contain "branchFromThought: 2" (pointing to summary)
|
|
25
|
+
// Or if it broke, it might still say 3 (which is itself!) or not be updated.
|
|
26
|
+
// Read file manually to check JSON content
|
|
27
|
+
const data = JSON.parse(await fs.readFile('test_history_bug.json', 'utf-8'));
|
|
28
|
+
const lastThought = data[data.length - 1];
|
|
29
|
+
console.log("Last Thought:", lastThought);
|
|
30
|
+
if (lastThought.branchFromThought === 3) {
|
|
31
|
+
console.log("FAIL: branchFromThought was NOT updated. It points to 3 (which is now itself).");
|
|
32
|
+
}
|
|
33
|
+
else if (lastThought.branchFromThought === 2) {
|
|
34
|
+
console.log("PASS: branchFromThought was updated to point to Summary.");
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
console.log(`FAIL: branchFromThought is ${lastThought.branchFromThought} (Unknown state)`);
|
|
38
|
+
}
|
|
39
|
+
await fs.unlink('test_history_bug.json');
|
|
40
|
+
}
|
|
41
|
+
test();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
async function test() {
|
|
3
|
+
console.log("Testing path traversal vulnerability...");
|
|
4
|
+
const cwd = process.cwd();
|
|
5
|
+
const maliciousPath = '/etc/passwd'; // Or any file outside cwd
|
|
6
|
+
const resolved = path.resolve(maliciousPath);
|
|
7
|
+
console.log(`CWD: ${cwd}`);
|
|
8
|
+
console.log(`Input: ${maliciousPath}`);
|
|
9
|
+
console.log(`Resolved: ${resolved}`);
|
|
10
|
+
if (!resolved.startsWith(cwd)) {
|
|
11
|
+
console.log("FAIL: Path is outside CWD and would be allowed by current logic.");
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
console.log("PASS: Path is inside CWD.");
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
test();
|