@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/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(/^(?:import|from)\s+([a-zA-Z0-9_.]+)/gm);
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(/^def\s+([a-zA-Z0-9_]+)/gm);
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(/^class\s+([a-zA-Z0-9_]+)/gm);
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(/^func\s+([a-zA-Z0-9_]+)/gm);
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)) {
@@ -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
- // Simple retry if already saving
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].thoughtNumber -= (removedCount - 1);
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
- // If file doesn't exist, start with empty array
19
- this.notes = [];
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
- 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
- priority,
34
- expiresAt,
35
- createdAt: new Date().toISOString(),
36
- updatedAt: new Date().toISOString()
37
- };
38
- this.notes.push(note);
39
- await this.save();
40
- return note;
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
- await this.load();
44
- const now = new Date();
45
- let activeNotes = this.notes;
46
- if (!includeExpired) {
47
- activeNotes = this.notes.filter(n => !n.expiresAt || new Date(n.expiresAt) > now);
48
- }
49
- if (tag) {
50
- return activeNotes.filter(n => n.tags.includes(tag));
51
- }
52
- return activeNotes.sort((a, b) => {
53
- const priorityMap = { 'critical': 4, 'high': 3, 'medium': 2, 'low': 1 };
54
- return (priorityMap[b.priority] || 0) - (priorityMap[a.priority] || 0);
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
- await this.load();
59
- const lowerQuery = query.toLowerCase();
60
- return this.notes.filter(n => n.title.toLowerCase().includes(lowerQuery) ||
61
- n.content.toLowerCase().includes(lowerQuery) ||
62
- n.tags.some(t => t.toLowerCase().includes(lowerQuery)));
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
- await this.load();
66
- const initialLength = this.notes.length;
67
- this.notes = this.notes.filter(n => n.id !== id);
68
- if (this.notes.length !== initialLength) {
69
- await this.save();
70
- return true;
71
- }
72
- return false;
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
- await this.load();
76
- const index = this.notes.findIndex(n => n.id === id);
77
- if (index === -1)
78
- return null;
79
- this.notes[index] = {
80
- ...this.notes[index],
81
- ...updates,
82
- updatedAt: new Date().toISOString()
83
- };
84
- await this.save();
85
- return this.notes[index];
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();
@@ -0,0 +1,3 @@
1
+ import { createRequire as _createRequire } from "module";
2
+ const __require = _createRequire(import.meta.url);
3
+ const fs = __require("fs");