@gotza02/sequential-thinking 2026.2.7 → 2026.2.10
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 +34 -24
- package/dist/codestore.js +69 -0
- package/dist/codestore.test.js +59 -0
- package/dist/filesystem.test.js +48 -0
- package/dist/graph.js +13 -5
- package/dist/graph.test.js +53 -0
- package/dist/index.js +4 -0
- package/dist/integration.test.js +58 -0
- package/dist/lib.js +29 -4
- package/dist/notes.js +65 -43
- 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/system_test.js +51 -0
- package/dist/test_ts_req.js +46 -0
- package/dist/tools/codestore.js +51 -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/notes.js +9 -6
- package/dist/tools/thinking.js +7 -0
- package/dist/tools/web.js +7 -2
- package/dist/utils.js +103 -2
- package/dist/web_read.test.js +60 -0
- package/package.json +1 -1
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
|
}
|
|
@@ -23,55 +25,75 @@ export class NotesManager {
|
|
|
23
25
|
async save() {
|
|
24
26
|
await fs.writeFile(this.filePath, JSON.stringify(this.notes, null, 2), 'utf-8');
|
|
25
27
|
}
|
|
26
|
-
async addNote(title, content, tags = []) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
28
|
+
async addNote(title, content, tags = [], priority = 'medium', expiresAt) {
|
|
29
|
+
return this.mutex.dispatch(async () => {
|
|
30
|
+
await this.load();
|
|
31
|
+
const note = {
|
|
32
|
+
id: Date.now().toString(36) + Math.random().toString(36).substring(2, 7),
|
|
33
|
+
title,
|
|
34
|
+
content,
|
|
35
|
+
tags,
|
|
36
|
+
priority,
|
|
37
|
+
expiresAt,
|
|
38
|
+
createdAt: new Date().toISOString(),
|
|
39
|
+
updatedAt: new Date().toISOString()
|
|
40
|
+
};
|
|
41
|
+
this.notes.push(note);
|
|
42
|
+
await this.save();
|
|
43
|
+
return note;
|
|
44
|
+
});
|
|
39
45
|
}
|
|
40
|
-
async listNotes(tag) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
async listNotes(tag, includeExpired = false) {
|
|
47
|
+
return this.mutex.dispatch(async () => {
|
|
48
|
+
await this.load();
|
|
49
|
+
const now = new Date();
|
|
50
|
+
let activeNotes = this.notes;
|
|
51
|
+
if (!includeExpired) {
|
|
52
|
+
activeNotes = this.notes.filter(n => !n.expiresAt || new Date(n.expiresAt) > now);
|
|
53
|
+
}
|
|
54
|
+
if (tag) {
|
|
55
|
+
return activeNotes.filter(n => n.tags.includes(tag));
|
|
56
|
+
}
|
|
57
|
+
return activeNotes.sort((a, b) => {
|
|
58
|
+
const priorityMap = { 'critical': 4, 'high': 3, 'medium': 2, 'low': 1 };
|
|
59
|
+
return (priorityMap[b.priority] || 0) - (priorityMap[a.priority] || 0);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
46
62
|
}
|
|
47
63
|
async searchNotes(query) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
n.
|
|
52
|
-
|
|
64
|
+
return this.mutex.dispatch(async () => {
|
|
65
|
+
await this.load();
|
|
66
|
+
const lowerQuery = query.toLowerCase();
|
|
67
|
+
return this.notes.filter(n => n.title.toLowerCase().includes(lowerQuery) ||
|
|
68
|
+
n.content.toLowerCase().includes(lowerQuery) ||
|
|
69
|
+
n.tags.some(t => t.toLowerCase().includes(lowerQuery)));
|
|
70
|
+
});
|
|
53
71
|
}
|
|
54
72
|
async deleteNote(id) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
73
|
+
return this.mutex.dispatch(async () => {
|
|
74
|
+
await this.load();
|
|
75
|
+
const initialLength = this.notes.length;
|
|
76
|
+
this.notes = this.notes.filter(n => n.id !== id);
|
|
77
|
+
if (this.notes.length !== initialLength) {
|
|
78
|
+
await this.save();
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
});
|
|
63
83
|
}
|
|
64
84
|
async updateNote(id, updates) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
85
|
+
return this.mutex.dispatch(async () => {
|
|
86
|
+
await this.load();
|
|
87
|
+
const index = this.notes.findIndex(n => n.id === id);
|
|
88
|
+
if (index === -1)
|
|
89
|
+
return null;
|
|
90
|
+
this.notes[index] = {
|
|
91
|
+
...this.notes[index],
|
|
92
|
+
...updates,
|
|
93
|
+
updatedAt: new Date().toISOString()
|
|
94
|
+
};
|
|
95
|
+
await this.save();
|
|
96
|
+
return this.notes[index];
|
|
97
|
+
});
|
|
76
98
|
}
|
|
77
99
|
}
|
|
@@ -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();
|
package/dist/server.test.js
CHANGED
|
@@ -92,4 +92,36 @@ describe('SequentialThinkingServer', () => {
|
|
|
92
92
|
const content = JSON.parse(result.content[0].text);
|
|
93
93
|
expect(content.totalThoughts).toBe(6);
|
|
94
94
|
});
|
|
95
|
+
it('should clear thought history', async () => {
|
|
96
|
+
await server.processThought({
|
|
97
|
+
thought: "To be forgotten",
|
|
98
|
+
thoughtNumber: 1,
|
|
99
|
+
totalThoughts: 1,
|
|
100
|
+
nextThoughtNeeded: false
|
|
101
|
+
});
|
|
102
|
+
await server.clearHistory();
|
|
103
|
+
// Since we can't easily peek into private state, we'll process a new thought
|
|
104
|
+
// and check if thoughtHistoryLength is 1 (meaning it started over or is just this one)
|
|
105
|
+
const result = await server.processThought({
|
|
106
|
+
thought: "Fresh start",
|
|
107
|
+
thoughtNumber: 1,
|
|
108
|
+
totalThoughts: 1,
|
|
109
|
+
nextThoughtNeeded: false
|
|
110
|
+
});
|
|
111
|
+
const content = JSON.parse(result.content[0].text);
|
|
112
|
+
expect(content.thoughtHistoryLength).toBe(1);
|
|
113
|
+
});
|
|
114
|
+
it('should summarize history correctly', async () => {
|
|
115
|
+
// Add 3 thoughts
|
|
116
|
+
await server.processThought({ thought: "T1", thoughtNumber: 1, totalThoughts: 3, nextThoughtNeeded: true });
|
|
117
|
+
await server.processThought({ thought: "T2", thoughtNumber: 2, totalThoughts: 3, nextThoughtNeeded: true });
|
|
118
|
+
await server.processThought({ thought: "T3", thoughtNumber: 3, totalThoughts: 3, nextThoughtNeeded: false });
|
|
119
|
+
const result = await server.archiveHistory(1, 2, "Summary of T1 and T2");
|
|
120
|
+
expect(result.newHistoryLength).toBe(2); // Summary + T3
|
|
121
|
+
expect(result.summaryInsertedAt).toBe(1);
|
|
122
|
+
});
|
|
123
|
+
it('should throw error when summarizing invalid range', async () => {
|
|
124
|
+
await server.processThought({ thought: "T1", thoughtNumber: 1, totalThoughts: 1, nextThoughtNeeded: false });
|
|
125
|
+
await expect(server.archiveHistory(1, 5, "Invalid")).rejects.toThrow();
|
|
126
|
+
});
|
|
95
127
|
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { SequentialThinkingServer } from './lib.js';
|
|
3
|
+
import { ProjectKnowledgeGraph } from './graph.js';
|
|
4
|
+
import * as fs from 'fs/promises';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
// Mock fs for graph test
|
|
7
|
+
vi.mock('fs/promises', async (importOriginal) => {
|
|
8
|
+
const actual = await importOriginal();
|
|
9
|
+
return {
|
|
10
|
+
...actual,
|
|
11
|
+
readdir: vi.fn(),
|
|
12
|
+
readFile: vi.fn(),
|
|
13
|
+
writeFile: vi.fn(),
|
|
14
|
+
rename: vi.fn(),
|
|
15
|
+
unlink: vi.fn()
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
// Mock existsSync and readFileSync from 'fs' (non-promise) for SequentialThinkingServer
|
|
19
|
+
vi.mock('fs', async () => {
|
|
20
|
+
return {
|
|
21
|
+
existsSync: () => false,
|
|
22
|
+
readFileSync: () => '[]',
|
|
23
|
+
unlinkSync: () => { }
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
describe('Stress Testing', () => {
|
|
27
|
+
it('should handle 1000 sequential thoughts', async () => {
|
|
28
|
+
// We use a real instance but mocked fs
|
|
29
|
+
const server = new SequentialThinkingServer('stress_thoughts.json');
|
|
30
|
+
const startTime = Date.now();
|
|
31
|
+
for (let i = 1; i <= 1000; i++) {
|
|
32
|
+
await server.processThought({
|
|
33
|
+
thought: `Thought ${i}`,
|
|
34
|
+
thoughtNumber: i,
|
|
35
|
+
totalThoughts: 1000,
|
|
36
|
+
nextThoughtNeeded: i < 1000
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
const duration = Date.now() - startTime;
|
|
40
|
+
console.log(`Processed 1000 thoughts in ${duration}ms`);
|
|
41
|
+
expect(duration).toBeLessThan(10000); // Should be fast enough (< 10s)
|
|
42
|
+
});
|
|
43
|
+
it('should handle large graph construction', async () => {
|
|
44
|
+
const graph = new ProjectKnowledgeGraph();
|
|
45
|
+
// Mock 1000 files
|
|
46
|
+
const files = Array.from({ length: 1000 }, (_, i) => `file${i}.ts`);
|
|
47
|
+
// Mock readdir to return these files (recursively? no, just flat for stress test)
|
|
48
|
+
// logic in getAllFiles is recursive. We need to mock it to return all at once or handle recursion.
|
|
49
|
+
// Actually, ProjectKnowledgeGraph.getAllFiles calls readdir.
|
|
50
|
+
// Let's mock readdir to return files for root, and then nothing for subdirs.
|
|
51
|
+
fs.readdir.mockImplementation(async (dir) => {
|
|
52
|
+
if (dir === path.resolve('.')) {
|
|
53
|
+
return files.map(f => ({
|
|
54
|
+
name: f,
|
|
55
|
+
isDirectory: () => false
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
return [];
|
|
59
|
+
});
|
|
60
|
+
fs.readFile.mockResolvedValue("import { x } from './file0';");
|
|
61
|
+
const startTime = Date.now();
|
|
62
|
+
const result = await graph.build('.');
|
|
63
|
+
const duration = Date.now() - startTime;
|
|
64
|
+
console.log(`Built graph of ${result.totalFiles} files in ${duration}ms`);
|
|
65
|
+
expect(result.totalFiles).toBe(1000);
|
|
66
|
+
expect(duration).toBeLessThan(5000);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { NotesManager } from './notes.js';
|
|
2
|
+
import { CodeDatabase } from './codestore.js';
|
|
3
|
+
import * as fs from 'fs/promises';
|
|
4
|
+
async function testSystem() {
|
|
5
|
+
console.log("🚀 Starting System Logic Test...\n");
|
|
6
|
+
// --- 1. Testing Notes Manager (Priority & Expiration) ---
|
|
7
|
+
console.log("--- Testing Notes Manager ---");
|
|
8
|
+
const notes = new NotesManager('test_notes.json');
|
|
9
|
+
// Clean up previous test
|
|
10
|
+
try {
|
|
11
|
+
await fs.unlink('test_notes.json');
|
|
12
|
+
}
|
|
13
|
+
catch { }
|
|
14
|
+
console.log("1. Adding CRITICAL note (expires tomorrow)...");
|
|
15
|
+
await notes.addNote("Urgent Security Fix", "Fix the login bug", ["bug"], "critical", new Date(Date.now() + 86400000).toISOString());
|
|
16
|
+
console.log("2. Adding LOW priority note...");
|
|
17
|
+
await notes.addNote("Refactor UI", "Make it pretty eventually", ["ui"], "low");
|
|
18
|
+
console.log("3. Listing notes (Expect Critical first)...");
|
|
19
|
+
const list = await notes.listNotes();
|
|
20
|
+
list.forEach(n => console.log(`- [${n.priority.toUpperCase()}] ${n.title}`));
|
|
21
|
+
// --- 2. Testing Code Database ---
|
|
22
|
+
console.log("\n--- Testing Code Database ---");
|
|
23
|
+
const codeDb = new CodeDatabase('test_code_db.json');
|
|
24
|
+
try {
|
|
25
|
+
await fs.unlink('test_code_db.json');
|
|
26
|
+
}
|
|
27
|
+
catch { }
|
|
28
|
+
console.log("1. Adding Code Snippet...");
|
|
29
|
+
await codeDb.addSnippet({
|
|
30
|
+
title: "Deepest Thinking Prompt",
|
|
31
|
+
language: "markdown",
|
|
32
|
+
code: "You must use sequentialthinking...",
|
|
33
|
+
description: "Standard prompt for deep agents",
|
|
34
|
+
tags: ["prompt", "ai"]
|
|
35
|
+
});
|
|
36
|
+
console.log("2. Searching for 'Deepest'...");
|
|
37
|
+
const results = await codeDb.searchSnippets("Deepest");
|
|
38
|
+
console.log(`Found ${results.length} result(s):`);
|
|
39
|
+
results.forEach(r => console.log(`- ${r.title} (${r.language})`));
|
|
40
|
+
console.log("\n✅ Test Complete!");
|
|
41
|
+
// Cleanup
|
|
42
|
+
try {
|
|
43
|
+
await fs.unlink('test_notes.json');
|
|
44
|
+
}
|
|
45
|
+
catch { }
|
|
46
|
+
try {
|
|
47
|
+
await fs.unlink('test_code_db.json');
|
|
48
|
+
}
|
|
49
|
+
catch { }
|
|
50
|
+
}
|
|
51
|
+
testSystem();
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { ProjectKnowledgeGraph } from './graph.js';
|
|
2
|
+
import * as fs from 'fs/promises';
|
|
3
|
+
async function test() {
|
|
4
|
+
const tempFile = 'temp_req.ts';
|
|
5
|
+
await fs.writeFile(tempFile, 'import fs = require("fs");\nexport = fs;');
|
|
6
|
+
try {
|
|
7
|
+
const graph = new ProjectKnowledgeGraph();
|
|
8
|
+
await graph.build(process.cwd());
|
|
9
|
+
const rel = graph.getRelationships(tempFile);
|
|
10
|
+
console.log("Imports:", rel?.imports);
|
|
11
|
+
// Note: 'fs' is a built-in, so it might not be in 'imports' array unless resolved to a file.
|
|
12
|
+
// But ProjectKnowledgeGraph parser ADDS it to 'imports' list initially.
|
|
13
|
+
// finalizeFileNodes only keeps it if it resolves to a node OR if we assume internal logic keeps it?
|
|
14
|
+
// Wait, 'finalizeFileNodes' logic:
|
|
15
|
+
// for (const importPath of imports) {
|
|
16
|
+
// resolved = resolvePath(...)
|
|
17
|
+
// if (resolved && nodes.has(resolved)) { imports.push(resolved) }
|
|
18
|
+
// }
|
|
19
|
+
// It FILTERS out imports that don't resolve to files in the project!
|
|
20
|
+
// So 'fs' will be DROPPED.
|
|
21
|
+
// This makes verifying the PARSER hard via 'getRelationships'.
|
|
22
|
+
// However, 'symbols' are kept.
|
|
23
|
+
// 'import fs = require...' creates a symbol 'fs'?
|
|
24
|
+
// The parser only pushes to 'imports' or 'symbols'.
|
|
25
|
+
// Let's create a local file 'my_dep.ts' and import THAT.
|
|
26
|
+
await fs.writeFile('my_dep.ts', 'export const x = 1;');
|
|
27
|
+
await fs.writeFile(tempFile, 'import dep = require("./my_dep");');
|
|
28
|
+
await graph.build(process.cwd());
|
|
29
|
+
const rel2 = graph.getRelationships(tempFile);
|
|
30
|
+
console.log("Imports 2:", rel2?.imports);
|
|
31
|
+
if (rel2?.imports.some(i => i.includes('my_dep'))) {
|
|
32
|
+
console.log("PASS: Found 'my_dep' import");
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
console.log("FAIL: 'my_dep' import missing");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
await fs.unlink(tempFile);
|
|
40
|
+
try {
|
|
41
|
+
await fs.unlink('my_dep.ts');
|
|
42
|
+
}
|
|
43
|
+
catch { }
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
test();
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export function registerCodeDbTools(server, db) {
|
|
3
|
+
// 16. add_code_snippet
|
|
4
|
+
server.tool("add_code_snippet", "Add a useful code snippet or implementation pattern to the persistent Code Database.", {
|
|
5
|
+
title: z.string().describe("Title of the snippet"),
|
|
6
|
+
code: z.string().describe("The actual code"),
|
|
7
|
+
language: z.string().describe("Programming language"),
|
|
8
|
+
description: z.string().describe("What this code does and why it is useful"),
|
|
9
|
+
tags: z.array(z.string()).optional().default([]).describe("Tags for categorization")
|
|
10
|
+
}, async (args) => {
|
|
11
|
+
const snippet = await db.addSnippet(args);
|
|
12
|
+
return {
|
|
13
|
+
content: [{ type: "text", text: `Snippet saved with ID: ${snippet.id}` }]
|
|
14
|
+
};
|
|
15
|
+
});
|
|
16
|
+
// 17. search_code_db
|
|
17
|
+
server.tool("search_code_db", "Search the Code Database for existing snippets, patterns, or architectural knowledge.", {
|
|
18
|
+
query: z.string().describe("Search query")
|
|
19
|
+
}, async ({ query }) => {
|
|
20
|
+
const results = await db.searchSnippets(query);
|
|
21
|
+
const patterns = await db.listAllPatterns();
|
|
22
|
+
let output = `--- CODE DATABASE RESULTS ---\n\n`;
|
|
23
|
+
if (results.length > 0) {
|
|
24
|
+
output += `SNIPPETS FOUND:\n`;
|
|
25
|
+
results.forEach(s => {
|
|
26
|
+
output += `ID: ${s.id} | ${s.title} (${s.language})\nDesc: ${s.description}\nCode:\n\`\`\`\n${s.code}\n\`\`\`\n\n`;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
const matchedPatterns = Object.entries(patterns).filter(([k, v]) => k.toLowerCase().includes(query.toLowerCase()) || v.toLowerCase().includes(query.toLowerCase()));
|
|
30
|
+
if (matchedPatterns.length > 0) {
|
|
31
|
+
output += `PATTERNS FOUND:\n`;
|
|
32
|
+
matchedPatterns.forEach(([name, desc]) => {
|
|
33
|
+
output += `- ${name}: ${desc}\n`;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
if (results.length === 0 && matchedPatterns.length === 0) {
|
|
37
|
+
output += "No results found in the Code Database.";
|
|
38
|
+
}
|
|
39
|
+
return { content: [{ type: "text", text: output }] };
|
|
40
|
+
});
|
|
41
|
+
// 18. learn_architecture_pattern
|
|
42
|
+
server.tool("learn_architecture_pattern", "Store a high-level architectural or logic pattern in the Code DB (e.g., 'auth-flow', 'error-handling-strategy').", {
|
|
43
|
+
name: z.string().describe("Name of the pattern"),
|
|
44
|
+
description: z.string().describe("Detailed description of the pattern and how it applies to this project")
|
|
45
|
+
}, async ({ name, description }) => {
|
|
46
|
+
await db.learnPattern(name, description);
|
|
47
|
+
return {
|
|
48
|
+
content: [{ type: "text", text: `Pattern '${name}' learned and stored.` }]
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
}
|