@gotza02/sequential-thinking 1.0.0
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 +262 -0
- package/dist/graph.js +214 -0
- package/dist/graph.test.js +79 -0
- package/dist/graph_repro.test.js +63 -0
- package/dist/index.js +33 -0
- package/dist/lib.js +189 -0
- package/dist/notes.js +77 -0
- package/dist/repro_search.test.js +79 -0
- package/dist/server.test.js +95 -0
- package/dist/tools/filesystem.js +152 -0
- package/dist/tools/graph.js +79 -0
- package/dist/tools/notes.js +55 -0
- package/dist/tools/thinking.js +113 -0
- package/dist/tools/web.js +134 -0
- package/dist/utils.js +35 -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 +49 -0
package/dist/lib.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import * as fs from 'fs/promises';
|
|
3
|
+
import { existsSync, readFileSync } from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
export class SequentialThinkingServer {
|
|
6
|
+
thoughtHistory = [];
|
|
7
|
+
branches = {};
|
|
8
|
+
disableThoughtLogging;
|
|
9
|
+
storagePath;
|
|
10
|
+
delayMs;
|
|
11
|
+
isSaving = false;
|
|
12
|
+
constructor(storagePath = 'thoughts_history.json', delayMs = 0) {
|
|
13
|
+
this.disableThoughtLogging = (process.env.DISABLE_THOUGHT_LOGGING || "").toLowerCase() === "true";
|
|
14
|
+
this.storagePath = path.resolve(storagePath);
|
|
15
|
+
this.delayMs = delayMs;
|
|
16
|
+
this.loadHistory();
|
|
17
|
+
}
|
|
18
|
+
loadHistory() {
|
|
19
|
+
try {
|
|
20
|
+
if (existsSync(this.storagePath)) {
|
|
21
|
+
const data = readFileSync(this.storagePath, 'utf-8');
|
|
22
|
+
const history = JSON.parse(data);
|
|
23
|
+
if (Array.isArray(history)) {
|
|
24
|
+
this.thoughtHistory = []; // Reset to avoid duplicates
|
|
25
|
+
this.branches = {};
|
|
26
|
+
history.forEach(thought => this.addToMemory(thought));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
console.error(`Error loading history from ${this.storagePath}:`, error);
|
|
32
|
+
}
|
|
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;
|
|
41
|
+
try {
|
|
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);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
console.error(`Error saving history to ${this.storagePath}:`, error);
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
this.isSaving = false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async clearHistory() {
|
|
55
|
+
this.thoughtHistory = [];
|
|
56
|
+
this.branches = {};
|
|
57
|
+
await this.saveHistory();
|
|
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
|
+
}
|
|
93
|
+
addToMemory(input) {
|
|
94
|
+
if (input.thoughtNumber > input.totalThoughts) {
|
|
95
|
+
input.totalThoughts = input.thoughtNumber;
|
|
96
|
+
}
|
|
97
|
+
this.thoughtHistory.push(input);
|
|
98
|
+
if (input.branchFromThought && input.branchId) {
|
|
99
|
+
const branchKey = `${input.branchFromThought}-${input.branchId}`;
|
|
100
|
+
if (!this.branches[branchKey]) {
|
|
101
|
+
this.branches[branchKey] = [];
|
|
102
|
+
}
|
|
103
|
+
this.branches[branchKey].push(input);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
formatThought(thoughtData) {
|
|
107
|
+
const { thoughtNumber, totalThoughts, thought, isRevision, revisesThought, branchFromThought, branchId, thoughtType, score, options, selectedOption } = thoughtData;
|
|
108
|
+
let prefix = '';
|
|
109
|
+
let context = '';
|
|
110
|
+
if (thoughtType === 'reflexion' || isRevision) {
|
|
111
|
+
prefix = chalk.yellow('🔄 Reflexion');
|
|
112
|
+
if (revisesThought)
|
|
113
|
+
context += ` (revising thought ${revisesThought})`;
|
|
114
|
+
}
|
|
115
|
+
else if (thoughtType === 'generation') {
|
|
116
|
+
prefix = chalk.magenta('💡 Generation');
|
|
117
|
+
}
|
|
118
|
+
else if (thoughtType === 'evaluation') {
|
|
119
|
+
prefix = chalk.cyan('⚖️ Evaluation');
|
|
120
|
+
if (score)
|
|
121
|
+
context += ` (Score: ${score})`;
|
|
122
|
+
}
|
|
123
|
+
else if (thoughtType === 'selection') {
|
|
124
|
+
prefix = chalk.green('✅ Selection');
|
|
125
|
+
if (selectedOption)
|
|
126
|
+
context += ` (Selected: ${selectedOption})`;
|
|
127
|
+
}
|
|
128
|
+
else if (branchFromThought) {
|
|
129
|
+
prefix = chalk.green('🌿 Branch');
|
|
130
|
+
context = ` (from thought ${branchFromThought}, ID: ${branchId})`;
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
prefix = chalk.blue('💭 Thought');
|
|
134
|
+
context = '';
|
|
135
|
+
}
|
|
136
|
+
const header = `${prefix} ${thoughtNumber}/${totalThoughts}${context}`;
|
|
137
|
+
const borderLength = Math.max(header.length, thought.length) + 4;
|
|
138
|
+
const border = '─'.repeat(borderLength);
|
|
139
|
+
let extraContent = '';
|
|
140
|
+
if (options && options.length > 0) {
|
|
141
|
+
extraContent += `
|
|
142
|
+
│ Options:
|
|
143
|
+
` + options.map(o => `│ - ${o}`).join('\n');
|
|
144
|
+
}
|
|
145
|
+
return `
|
|
146
|
+
┌${border}┐
|
|
147
|
+
│ ${header} │
|
|
148
|
+
├${border}┤
|
|
149
|
+
│ ${thought.padEnd(borderLength - 2)} │${extraContent}
|
|
150
|
+
└${border}┘`;
|
|
151
|
+
}
|
|
152
|
+
async processThought(input) {
|
|
153
|
+
try {
|
|
154
|
+
if (this.delayMs > 0) {
|
|
155
|
+
await new Promise(resolve => setTimeout(resolve, this.delayMs));
|
|
156
|
+
}
|
|
157
|
+
this.addToMemory(input);
|
|
158
|
+
await this.saveHistory();
|
|
159
|
+
if (!this.disableThoughtLogging) {
|
|
160
|
+
const formattedThought = this.formatThought(input);
|
|
161
|
+
console.error(formattedThought);
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
content: [{
|
|
165
|
+
type: "text",
|
|
166
|
+
text: JSON.stringify({
|
|
167
|
+
thoughtNumber: input.thoughtNumber,
|
|
168
|
+
totalThoughts: input.totalThoughts,
|
|
169
|
+
nextThoughtNeeded: input.nextThoughtNeeded,
|
|
170
|
+
branches: Object.keys(this.branches),
|
|
171
|
+
thoughtHistoryLength: this.thoughtHistory.length
|
|
172
|
+
}, null, 2)
|
|
173
|
+
}]
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
return {
|
|
178
|
+
content: [{
|
|
179
|
+
type: "text",
|
|
180
|
+
text: JSON.stringify({
|
|
181
|
+
error: error instanceof Error ? error.message : String(error),
|
|
182
|
+
status: 'failed'
|
|
183
|
+
}, null, 2)
|
|
184
|
+
}],
|
|
185
|
+
isError: true
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
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
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterAll } from 'vitest';
|
|
2
|
+
import { SequentialThinkingServer } from './lib.js';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
describe('SequentialThinkingServer', () => {
|
|
5
|
+
let server;
|
|
6
|
+
const testStoragePath = 'test_thoughts.json';
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
if (fs.existsSync(testStoragePath)) {
|
|
9
|
+
fs.unlinkSync(testStoragePath);
|
|
10
|
+
}
|
|
11
|
+
server = new SequentialThinkingServer(testStoragePath);
|
|
12
|
+
});
|
|
13
|
+
afterAll(() => {
|
|
14
|
+
if (fs.existsSync(testStoragePath)) {
|
|
15
|
+
fs.unlinkSync(testStoragePath);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
it('should process a basic linear thought', async () => {
|
|
19
|
+
const input = {
|
|
20
|
+
thought: "First step",
|
|
21
|
+
thoughtNumber: 1,
|
|
22
|
+
totalThoughts: 3,
|
|
23
|
+
nextThoughtNeeded: true,
|
|
24
|
+
thoughtType: 'analysis'
|
|
25
|
+
};
|
|
26
|
+
const result = await server.processThought(input);
|
|
27
|
+
expect(result.isError).toBeUndefined();
|
|
28
|
+
const content = JSON.parse(result.content[0].text);
|
|
29
|
+
expect(content.thoughtNumber).toBe(1);
|
|
30
|
+
expect(content.thoughtHistoryLength).toBe(1);
|
|
31
|
+
});
|
|
32
|
+
it('should handle branching correctly', async () => {
|
|
33
|
+
// Initial thought
|
|
34
|
+
await server.processThought({
|
|
35
|
+
thought: "Root thought",
|
|
36
|
+
thoughtNumber: 1,
|
|
37
|
+
totalThoughts: 3,
|
|
38
|
+
nextThoughtNeeded: true
|
|
39
|
+
});
|
|
40
|
+
// Branch 1
|
|
41
|
+
const branch1Input = {
|
|
42
|
+
thought: "Alternative A",
|
|
43
|
+
thoughtNumber: 2,
|
|
44
|
+
totalThoughts: 3,
|
|
45
|
+
nextThoughtNeeded: true,
|
|
46
|
+
branchFromThought: 1,
|
|
47
|
+
branchId: "branch-A",
|
|
48
|
+
thoughtType: 'generation'
|
|
49
|
+
};
|
|
50
|
+
const result1 = await server.processThought(branch1Input);
|
|
51
|
+
const content1 = JSON.parse(result1.content[0].text);
|
|
52
|
+
expect(content1.branches).toContain("1-branch-A");
|
|
53
|
+
// Branch 2
|
|
54
|
+
const branch2Input = {
|
|
55
|
+
thought: "Alternative B",
|
|
56
|
+
thoughtNumber: 2,
|
|
57
|
+
totalThoughts: 3,
|
|
58
|
+
nextThoughtNeeded: true,
|
|
59
|
+
branchFromThought: 1,
|
|
60
|
+
branchId: "branch-B",
|
|
61
|
+
thoughtType: 'generation'
|
|
62
|
+
};
|
|
63
|
+
const result2 = await server.processThought(branch2Input);
|
|
64
|
+
const content2 = JSON.parse(result2.content[0].text);
|
|
65
|
+
expect(content2.branches).toContain("1-branch-B");
|
|
66
|
+
expect(content2.branches.length).toBe(2);
|
|
67
|
+
});
|
|
68
|
+
it('should handle evaluation with scores', async () => {
|
|
69
|
+
const input = {
|
|
70
|
+
thought: "Evaluating option X",
|
|
71
|
+
thoughtNumber: 3,
|
|
72
|
+
totalThoughts: 5,
|
|
73
|
+
nextThoughtNeeded: true,
|
|
74
|
+
thoughtType: 'evaluation',
|
|
75
|
+
score: 8,
|
|
76
|
+
options: ['Option X', 'Option Y']
|
|
77
|
+
};
|
|
78
|
+
const result = await server.processThought(input);
|
|
79
|
+
expect(result.isError).toBeUndefined();
|
|
80
|
+
// Since we don't return the score in the simple JSON response (only in logs or history),
|
|
81
|
+
// we mainly check that it doesn't crash and processes correctly.
|
|
82
|
+
// If we exposed history in the response, we could check that too.
|
|
83
|
+
});
|
|
84
|
+
it('should adjust totalThoughts if thoughtNumber exceeds it', async () => {
|
|
85
|
+
const input = {
|
|
86
|
+
thought: "Unexpected long process",
|
|
87
|
+
thoughtNumber: 6,
|
|
88
|
+
totalThoughts: 5,
|
|
89
|
+
nextThoughtNeeded: true
|
|
90
|
+
};
|
|
91
|
+
const result = await server.processThought(input);
|
|
92
|
+
const content = JSON.parse(result.content[0].text);
|
|
93
|
+
expect(content.totalThoughts).toBe(6);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import * as fs from 'fs/promises';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { execAsync } from "../utils.js";
|
|
5
|
+
export function registerFileSystemTools(server) {
|
|
6
|
+
// 3. shell_execute
|
|
7
|
+
server.tool("shell_execute", "Execute a shell command. Use with caution.", {
|
|
8
|
+
command: z.string().describe("The bash command to execute")
|
|
9
|
+
}, async ({ command }) => {
|
|
10
|
+
try {
|
|
11
|
+
const { stdout, stderr } = await execAsync(command);
|
|
12
|
+
return {
|
|
13
|
+
content: [{
|
|
14
|
+
type: "text",
|
|
15
|
+
text: `STDOUT:\n${stdout}\n\nSTDERR:\n${stderr}`
|
|
16
|
+
}]
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
return {
|
|
21
|
+
content: [{ type: "text", text: `Shell Error: ${error instanceof Error ? error.message : String(error)}` }],
|
|
22
|
+
isError: true
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
// 4. read_file
|
|
27
|
+
server.tool("read_file", "Read the contents of a file.", {
|
|
28
|
+
path: z.string().describe("Path to the file")
|
|
29
|
+
}, async ({ path }) => {
|
|
30
|
+
try {
|
|
31
|
+
const content = await fs.readFile(path, 'utf-8');
|
|
32
|
+
return {
|
|
33
|
+
content: [{ type: "text", text: content }]
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
return {
|
|
38
|
+
content: [{ type: "text", text: `Read Error: ${error instanceof Error ? error.message : String(error)}` }],
|
|
39
|
+
isError: true
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
// 5. write_file
|
|
44
|
+
server.tool("write_file", "Write content to a file (overwrites existing).", {
|
|
45
|
+
path: z.string().describe("Path to the file"),
|
|
46
|
+
content: z.string().describe("Content to write")
|
|
47
|
+
}, async ({ path, content }) => {
|
|
48
|
+
try {
|
|
49
|
+
await fs.writeFile(path, content, 'utf-8');
|
|
50
|
+
return {
|
|
51
|
+
content: [{ type: "text", text: `Successfully wrote to ${path}` }]
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
return {
|
|
56
|
+
content: [{ type: "text", text: `Write Error: ${error instanceof Error ? error.message : String(error)}` }],
|
|
57
|
+
isError: true
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
// 10. search_code
|
|
62
|
+
server.tool("search_code", "Search for a text pattern in project files (excludes node_modules, etc.).", {
|
|
63
|
+
pattern: z.string().describe("The text to search for"),
|
|
64
|
+
path: z.string().optional().default('.').describe("Root directory to search")
|
|
65
|
+
}, async ({ pattern, path: searchPath }) => {
|
|
66
|
+
try {
|
|
67
|
+
const resolvedPath = path.resolve(searchPath || '.');
|
|
68
|
+
const stats = await fs.stat(resolvedPath);
|
|
69
|
+
if (stats.isFile()) {
|
|
70
|
+
const content = await fs.readFile(resolvedPath, 'utf-8');
|
|
71
|
+
const matches = content.includes(pattern) ? [resolvedPath] : [];
|
|
72
|
+
return {
|
|
73
|
+
content: [{
|
|
74
|
+
type: "text",
|
|
75
|
+
text: matches.length > 0 ? `Found "${pattern}" in:\n${matches.join('\n')}` : `No matches found for "${pattern}"`
|
|
76
|
+
}]
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
async function searchDir(dir) {
|
|
80
|
+
const results = [];
|
|
81
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
82
|
+
for (const entry of entries) {
|
|
83
|
+
const fullPath = path.join(dir, entry.name);
|
|
84
|
+
if (entry.isDirectory()) {
|
|
85
|
+
if (['node_modules', '.git', 'dist', 'coverage', '.gemini'].includes(entry.name))
|
|
86
|
+
continue;
|
|
87
|
+
results.push(...await searchDir(fullPath));
|
|
88
|
+
}
|
|
89
|
+
else if (/\.(ts|js|json|md|txt|html|css|py|java|c|cpp|h|rs|go)$/.test(entry.name)) {
|
|
90
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
91
|
+
if (content.includes(pattern)) {
|
|
92
|
+
results.push(fullPath);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return results;
|
|
97
|
+
}
|
|
98
|
+
const matches = await searchDir(resolvedPath);
|
|
99
|
+
return {
|
|
100
|
+
content: [{
|
|
101
|
+
type: "text",
|
|
102
|
+
text: matches.length > 0 ? `Found "${pattern}" in:\n${matches.join('\n')}` : `No matches found for "${pattern}"`
|
|
103
|
+
}]
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
return {
|
|
108
|
+
content: [{ type: "text", text: `Search Error: ${error instanceof Error ? error.message : String(error)}` }],
|
|
109
|
+
isError: true
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
// 13. edit_file
|
|
114
|
+
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.", {
|
|
115
|
+
path: z.string().describe("Path to the file"),
|
|
116
|
+
oldText: z.string().describe("The exact text segment to replace"),
|
|
117
|
+
newText: z.string().describe("The new text to insert"),
|
|
118
|
+
allowMultiple: z.boolean().optional().default(false).describe("Allow replacing multiple occurrences (default: false)")
|
|
119
|
+
}, async ({ path, oldText, newText, allowMultiple }) => {
|
|
120
|
+
try {
|
|
121
|
+
const content = await fs.readFile(path, 'utf-8');
|
|
122
|
+
// Check occurrences
|
|
123
|
+
// Escape special regex characters in oldText to treat it as literal string
|
|
124
|
+
const escapeRegExp = (string) => string.replace(/[.*+?^${}()|[\\]/g, '\\$&');
|
|
125
|
+
const regex = new RegExp(escapeRegExp(oldText), 'g');
|
|
126
|
+
const matchCount = (content.match(regex) || []).length;
|
|
127
|
+
if (matchCount === 0) {
|
|
128
|
+
return {
|
|
129
|
+
content: [{ type: "text", text: "Error: 'oldText' not found in the file. Please ensure exact matching (including whitespace/indentation)." }],
|
|
130
|
+
isError: true
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
if (matchCount > 1 && !allowMultiple) {
|
|
134
|
+
return {
|
|
135
|
+
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'.` }],
|
|
136
|
+
isError: true
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
const newContent = content.replace(allowMultiple ? regex : oldText, newText);
|
|
140
|
+
await fs.writeFile(path, newContent, 'utf-8');
|
|
141
|
+
return {
|
|
142
|
+
content: [{ type: "text", text: `Successfully replaced ${allowMultiple ? matchCount : 1} occurrence(s) in ${path}` }]
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
return {
|
|
147
|
+
content: [{ type: "text", text: `Edit Error: ${error instanceof Error ? error.message : String(error)}` }],
|
|
148
|
+
isError: true
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export function registerGraphTools(server, knowledgeGraph) {
|
|
3
|
+
// 6. build_project_graph
|
|
4
|
+
server.tool("build_project_graph", "Scan the directory and build a dependency graph of the project (Analyzing imports/exports).", {
|
|
5
|
+
path: z.string().optional().default('.').describe("Root directory path to scan (default: current dir)")
|
|
6
|
+
}, async ({ path }) => {
|
|
7
|
+
try {
|
|
8
|
+
const result = await knowledgeGraph.build(path || '.');
|
|
9
|
+
return {
|
|
10
|
+
content: [{ type: "text", text: `Graph built successfully.\nNodes: ${result.nodeCount}\nTotal Scanned Files: ${result.totalFiles}` }]
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
return {
|
|
15
|
+
content: [{ type: "text", text: `Graph Build Error: ${error instanceof Error ? error.message : String(error)}` }],
|
|
16
|
+
isError: true
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
// 7. get_file_relationships
|
|
21
|
+
server.tool("get_file_relationships", "Get dependencies and references for a specific file from the built graph.", {
|
|
22
|
+
filePath: z.string().describe("Path to the file (e.g., 'src/index.ts')")
|
|
23
|
+
}, async ({ filePath }) => {
|
|
24
|
+
try {
|
|
25
|
+
const rel = knowledgeGraph.getRelationships(filePath);
|
|
26
|
+
if (!rel) {
|
|
27
|
+
return {
|
|
28
|
+
content: [{ type: "text", text: `File not found in graph: ${filePath}. (Did you run 'build_project_graph'?)` }],
|
|
29
|
+
isError: true
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
content: [{ type: "text", text: JSON.stringify(rel, null, 2) }]
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
return {
|
|
38
|
+
content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
|
|
39
|
+
isError: true
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
// 8. get_project_graph_summary
|
|
44
|
+
server.tool("get_project_graph_summary", "Get a summary of the project structure (most referenced files, total count).", {}, async () => {
|
|
45
|
+
try {
|
|
46
|
+
const summary = knowledgeGraph.getSummary();
|
|
47
|
+
return {
|
|
48
|
+
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
return {
|
|
53
|
+
content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
|
|
54
|
+
isError: true
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
// 14. get_project_graph_visualization
|
|
59
|
+
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 () => {
|
|
60
|
+
try {
|
|
61
|
+
const diagram = knowledgeGraph.toMermaid();
|
|
62
|
+
if (diagram.trim() === 'graph TD') {
|
|
63
|
+
return {
|
|
64
|
+
content: [{ type: "text", text: "Graph is empty. Please run 'build_project_graph' first." }],
|
|
65
|
+
isError: true
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
content: [{ type: "text", text: diagram }]
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
return {
|
|
74
|
+
content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
|
|
75
|
+
isError: true
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|