@gotza02/sequential-thinking 2026.2.30 → 2026.2.32

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/lib.js CHANGED
@@ -63,53 +63,104 @@ export class SequentialThinkingServer {
63
63
  }
64
64
  }
65
65
  attemptRecovery(data) {
66
- const lastBrace = data.lastIndexOf('}');
67
- if (lastBrace !== -1) {
66
+ // Try incremental approach: find the longest valid JSON prefix
67
+ let braceCount = 0;
68
+ let lastValidIndex = -1;
69
+ for (let i = 0; i < data.length; i++) {
70
+ const char = data[i];
71
+ if (char === '{') {
72
+ braceCount++;
73
+ }
74
+ else if (char === '}') {
75
+ braceCount--;
76
+ // When brace count returns to 0, we have a complete object
77
+ if (braceCount === 0) {
78
+ lastValidIndex = i;
79
+ }
80
+ }
81
+ }
82
+ if (lastValidIndex !== -1) {
68
83
  try {
69
- const recoveredData = data.substring(0, lastBrace + 1).trim();
70
- const attemptedJson = recoveredData.endsWith(']') ? recoveredData : recoveredData + ']';
71
- const history = JSON.parse(attemptedJson);
72
- if (Array.isArray(history)) {
73
- history.forEach(thought => this.addToMemory(thought));
84
+ const recoveredData = data.substring(0, lastValidIndex + 1).trim();
85
+ const parsed = JSON.parse(recoveredData);
86
+ // Handle new format with version
87
+ if (parsed.version && parsed.blocks) {
88
+ this.blocks = parsed.blocks;
89
+ this.thoughtHistory = parsed.thoughtHistory || [];
90
+ if (this.blocks.length > 0) {
91
+ const activeBlock = this.blocks.find(b => b.status === 'active');
92
+ this.currentBlockId = activeBlock?.id || this.blocks[this.blocks.length - 1].id;
93
+ }
94
+ this.rebuildBranches();
95
+ console.log(`Successfully recovered ${this.thoughtHistory.length} thoughts from new format.`);
96
+ return;
97
+ }
98
+ // Handle legacy array format
99
+ if (Array.isArray(parsed)) {
100
+ parsed.forEach(thought => this.addToMemory(thought));
74
101
  this.rebuildBlocks();
75
- console.log(`Successfully recovered ${history.length} thoughts.`);
102
+ console.log(`Successfully recovered ${parsed.length} thoughts from legacy format.`);
76
103
  }
77
104
  }
78
105
  catch (recoveryError) {
79
106
  console.error('Recovery failed, starting with empty history.');
80
107
  }
81
108
  }
109
+ else {
110
+ console.error('No valid JSON structure found for recovery.');
111
+ }
82
112
  }
83
113
  rebuildBlocks() {
114
+ // Map existing blocks to preserve their metadata
84
115
  const oldBlocksMap = new Map();
85
116
  for (const b of this.blocks) {
86
- oldBlocksMap.set(b.id, b);
117
+ oldBlocksMap.set(b.id, { ...b });
87
118
  }
88
- this.blocks = [];
89
- const blockMap = new Map();
119
+ // Map thoughts by blockId from thoughtHistory
120
+ const thoughtsByBlock = new Map();
90
121
  for (const t of this.thoughtHistory) {
91
122
  const bid = t.blockId || 'default';
92
- if (!blockMap.has(bid)) {
93
- const oldBlock = oldBlocksMap.get(bid);
94
- const newBlock = {
95
- id: bid,
96
- topic: oldBlock ? oldBlock.topic : t.thought.substring(0, 50),
97
- status: oldBlock ? oldBlock.status : 'active',
98
- thoughts: [],
99
- createdAt: oldBlock ? oldBlock.createdAt : new Date().toISOString(),
100
- updatedAt: new Date().toISOString()
101
- };
102
- blockMap.set(bid, newBlock);
103
- this.blocks.push(newBlock);
123
+ if (!thoughtsByBlock.has(bid)) {
124
+ thoughtsByBlock.set(bid, []);
104
125
  }
105
- blockMap.get(bid).thoughts.push(t);
126
+ thoughtsByBlock.get(bid).push(t);
106
127
  }
107
- if (this.blocks.length > 0) {
108
- // Ensure currentBlockId points to a valid block or the last one
109
- if (!this.blocks.find(b => b.id === this.currentBlockId)) {
110
- this.currentBlockId = this.blocks[this.blocks.length - 1].id;
128
+ // Merge old blocks with new thoughts
129
+ const mergedBlocks = [];
130
+ const mergedBlockIds = new Set();
131
+ // First, update existing blocks with their thoughts
132
+ for (const [bid, thoughts] of thoughtsByBlock) {
133
+ const oldBlock = oldBlocksMap.get(bid);
134
+ // Merge old thoughts with new thoughts (avoid duplicates by thoughtNumber)
135
+ const oldThoughts = oldBlock?.thoughts || [];
136
+ const oldThoughtNumbers = new Set(oldThoughts.map(t => t.thoughtNumber));
137
+ const newThoughts = thoughts.filter(t => !oldThoughtNumbers.has(t.thoughtNumber));
138
+ const mergedThoughts = [...oldThoughts, ...newThoughts];
139
+ const mergedBlock = {
140
+ id: bid,
141
+ topic: oldBlock?.topic || thoughts[0]?.thought.substring(0, 50) || 'Untitled',
142
+ status: oldBlock?.status || 'active',
143
+ thoughts: mergedThoughts,
144
+ createdAt: oldBlock?.createdAt || new Date().toISOString(),
145
+ updatedAt: new Date().toISOString()
146
+ };
147
+ mergedBlocks.push(mergedBlock);
148
+ mergedBlockIds.add(bid);
149
+ }
150
+ // Then, preserve blocks that have no thoughts in current thoughtHistory
151
+ for (const [bid, oldBlock] of oldBlocksMap) {
152
+ if (!mergedBlockIds.has(bid)) {
153
+ mergedBlocks.push(oldBlock);
111
154
  }
112
155
  }
156
+ this.blocks = mergedBlocks;
157
+ // Ensure currentBlockId points to a valid block
158
+ if (this.blocks.length > 0) {
159
+ const activeBlock = this.blocks.find(b => b.status === 'active');
160
+ this.currentBlockId = activeBlock?.id ||
161
+ (this.blocks.find(b => b.id === this.currentBlockId)?.id) ||
162
+ this.blocks[this.blocks.length - 1].id;
163
+ }
113
164
  else {
114
165
  this.currentBlockId = null;
115
166
  }
@@ -409,6 +460,11 @@ ${typeof wrappedThought === 'string' && wrappedThought.startsWith('│') ? wrapp
409
460
  recentInBlock.every(t => t.thoughtType === input.thoughtType)) {
410
461
  warnings.push(`⚠️ TYPE LOOP: You've used '${input.thoughtType}' for 4 consecutive steps. Consider using a different thought type to progress.`);
411
462
  }
463
+ // Rule 7: Struggle Detection (Rule of 3)
464
+ const executionCount = blockThoughts.filter(t => t.thoughtType === 'execution').length;
465
+ if (executionCount >= 3 && input.thoughtType === 'execution') {
466
+ warnings.push(`🛑 STRUGGLE DETECTED: You have executed 3+ commands in this block without reaching a solution. If you are fixing a bug and it's not working, STOP. Do not try a 4th time linearly. Use 'branchFromThought' to try a completely different approach.`);
467
+ }
412
468
  // C. Update State
413
469
  this.addToMemory(input);
414
470
  await this.saveHistory();
package/dist/notes.d.ts CHANGED
@@ -12,14 +12,51 @@ export interface Note {
12
12
  export declare class NotesManager {
13
13
  private filePath;
14
14
  private notes;
15
- private loaded;
15
+ private lastModifiedTime;
16
16
  private mutex;
17
17
  constructor(storagePath?: string);
18
+ /**
19
+ * Load notes from disk with automatic reload detection.
20
+ * Uses mtime to detect if file has been modified externally.
21
+ */
18
22
  private load;
23
+ /**
24
+ * Attempt to recover notes from corrupted JSON using incremental parsing.
25
+ * Similar to the approach in lib.ts but adapted for notes format.
26
+ */
27
+ private attemptRecovery;
28
+ /**
29
+ * Backup corrupted file with timestamp
30
+ */
31
+ private backupCorruptedFile;
32
+ /**
33
+ * Atomic save using write-to-temp + rename pattern.
34
+ * This ensures either the old file or new file exists, never a partial write.
35
+ */
19
36
  private save;
37
+ /**
38
+ * Reload notes from disk (useful for external changes)
39
+ */
40
+ reload(): Promise<void>;
20
41
  addNote(title: string, content: string, tags?: string[], priority?: Priority, expiresAt?: string): Promise<Note>;
21
42
  listNotes(tag?: string, includeExpired?: boolean): Promise<Note[]>;
22
43
  searchNotes(query: string): Promise<Note[]>;
23
44
  deleteNote(id: string): Promise<boolean>;
24
- updateNote(id: string, updates: Partial<Pick<Note, 'title' | 'content' | 'tags'>>): Promise<Note | null>;
45
+ updateNote(id: string, updates: Partial<Pick<Note, 'title' | 'content' | 'tags' | 'priority' | 'expiresAt'>>): Promise<Note | null>;
46
+ /**
47
+ * Get a single note by ID
48
+ */
49
+ getNote(id: string): Promise<Note | null>;
50
+ /**
51
+ * Clear all notes (use with caution!)
52
+ */
53
+ clearAll(): Promise<void>;
54
+ /**
55
+ * Export notes as JSON string
56
+ */
57
+ export(): Promise<string>;
58
+ /**
59
+ * Import notes from JSON string
60
+ */
61
+ import(jsonData: string, merge?: boolean): Promise<number>;
25
62
  }
package/dist/notes.js CHANGED
@@ -1,52 +1,185 @@
1
1
  import * as fs from 'fs/promises';
2
+ import { existsSync, statSync } from 'fs';
2
3
  import * as path from 'path';
3
4
  import { AsyncMutex } from './utils.js';
4
5
  export class NotesManager {
5
6
  filePath;
6
7
  notes = [];
7
- loaded = false;
8
+ lastModifiedTime = 0; // mtime for reload detection
8
9
  mutex = new AsyncMutex();
9
10
  constructor(storagePath = 'project_notes.json') {
10
11
  this.filePath = path.resolve(storagePath);
11
12
  }
12
- async load() {
13
- if (this.loaded)
14
- return;
13
+ /**
14
+ * Load notes from disk with automatic reload detection.
15
+ * Uses mtime to detect if file has been modified externally.
16
+ */
17
+ async load(forceReload = false) {
15
18
  try {
19
+ // Check if file exists and get its mtime
20
+ if (!existsSync(this.filePath)) {
21
+ this.notes = [];
22
+ this.lastModifiedTime = 0;
23
+ return;
24
+ }
25
+ const stats = statSync(this.filePath);
26
+ const currentMtime = stats.mtimeMs;
27
+ // Skip reload if file hasn't been modified (unless forced)
28
+ if (!forceReload && currentMtime === this.lastModifiedTime && this.notes.length > 0) {
29
+ return;
30
+ }
16
31
  const data = await fs.readFile(this.filePath, 'utf-8');
17
- this.notes = JSON.parse(data);
32
+ if (!data.trim()) {
33
+ this.notes = [];
34
+ this.lastModifiedTime = currentMtime;
35
+ return;
36
+ }
37
+ try {
38
+ const parsed = JSON.parse(data);
39
+ // New format with version
40
+ if (parsed.version && parsed.notes) {
41
+ this.notes = parsed.notes || [];
42
+ }
43
+ // Legacy format (direct array)
44
+ else if (Array.isArray(parsed)) {
45
+ this.notes = parsed;
46
+ }
47
+ // Invalid format
48
+ else {
49
+ throw new Error('Invalid storage format');
50
+ }
51
+ this.lastModifiedTime = currentMtime;
52
+ }
53
+ catch (parseError) {
54
+ console.error(`[NotesManager] Parse error, attempting recovery:`, parseError);
55
+ this.attemptRecovery(data);
56
+ this.lastModifiedTime = currentMtime;
57
+ }
18
58
  }
19
59
  catch (error) {
20
- // Case 1: File doesn't exist (Normal first run)
21
60
  if (error.code === 'ENOENT') {
22
61
  this.notes = [];
62
+ this.lastModifiedTime = 0;
23
63
  }
24
- // Case 2: Corrupted JSON or other read errors
25
64
  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}`);
65
+ console.error(`[NotesManager] Load error:`, error);
66
+ this.notes = [];
67
+ this.lastModifiedTime = 0;
68
+ }
69
+ }
70
+ }
71
+ /**
72
+ * Attempt to recover notes from corrupted JSON using incremental parsing.
73
+ * Similar to the approach in lib.ts but adapted for notes format.
74
+ */
75
+ attemptRecovery(data) {
76
+ let braceCount = 0;
77
+ let lastValidIndex = -1;
78
+ for (let i = 0; i < data.length; i++) {
79
+ const char = data[i];
80
+ if (char === '{') {
81
+ braceCount++;
82
+ }
83
+ else if (char === '}') {
84
+ braceCount--;
85
+ if (braceCount === 0) {
86
+ lastValidIndex = i;
32
87
  }
33
- catch (backupError) {
34
- console.error(`[NotesManager] Critical: Failed to backup corrupted notes file: ${backupError}`);
88
+ }
89
+ }
90
+ if (lastValidIndex !== -1) {
91
+ try {
92
+ const recoveredData = data.substring(0, lastValidIndex + 1).trim();
93
+ const parsed = JSON.parse(recoveredData);
94
+ // Try to extract notes from recovered data
95
+ if (parsed.version && parsed.notes) {
96
+ this.notes = parsed.notes;
97
+ console.log(`[NotesManager] Recovered ${this.notes.length} notes from new format.`);
98
+ }
99
+ else if (Array.isArray(parsed)) {
100
+ this.notes = parsed;
101
+ console.log(`[NotesManager] Recovered ${this.notes.length} notes from legacy format.`);
102
+ }
103
+ else if (parsed.notes && Array.isArray(parsed.notes)) {
104
+ this.notes = parsed.notes;
105
+ console.log(`[NotesManager] Recovered ${this.notes.length} notes.`);
35
106
  }
36
- // Initialize empty to allow system to recover
107
+ else {
108
+ this.notes = [];
109
+ }
110
+ // Backup the corrupted file
111
+ this.backupCorruptedFile(data);
112
+ }
113
+ catch (recoveryError) {
114
+ console.error('[NotesManager] Recovery failed:', recoveryError);
37
115
  this.notes = [];
116
+ this.backupCorruptedFile(data);
38
117
  }
39
118
  }
40
- this.loaded = true;
119
+ else {
120
+ console.error('[NotesManager] No valid JSON structure found.');
121
+ this.notes = [];
122
+ }
41
123
  }
124
+ /**
125
+ * Backup corrupted file with timestamp
126
+ */
127
+ async backupCorruptedFile(corruptedData) {
128
+ try {
129
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
130
+ const backupPath = `${this.filePath}.corrupted.${timestamp}`;
131
+ await fs.writeFile(backupPath, corruptedData, 'utf-8');
132
+ console.error(`[NotesManager] Corrupted data backed up to: ${backupPath}`);
133
+ }
134
+ catch (backupError) {
135
+ console.error('[NotesManager] Failed to backup corrupted file:', backupError);
136
+ }
137
+ }
138
+ /**
139
+ * Atomic save using write-to-temp + rename pattern.
140
+ * This ensures either the old file or new file exists, never a partial write.
141
+ */
42
142
  async save() {
43
- await fs.writeFile(this.filePath, JSON.stringify(this.notes, null, 2), 'utf-8');
143
+ const tmpPath = `${this.filePath}.tmp`;
144
+ // Prepare storage format with version
145
+ const storage = {
146
+ version: '2.0',
147
+ notes: this.notes,
148
+ metadata: {
149
+ lastModified: new Date().toISOString()
150
+ }
151
+ };
152
+ try {
153
+ // Step 1: Write to temporary file
154
+ await fs.writeFile(tmpPath, JSON.stringify(storage, null, 2), 'utf-8');
155
+ // Step 2: Atomic rename (overwrites target if exists)
156
+ await fs.rename(tmpPath, this.filePath);
157
+ // Step 3: Update our mtime cache
158
+ const stats = await fs.stat(this.filePath);
159
+ this.lastModifiedTime = stats.mtimeMs;
160
+ }
161
+ catch (error) {
162
+ // Clean up temp file if something went wrong
163
+ try {
164
+ await fs.unlink(tmpPath);
165
+ }
166
+ catch { }
167
+ throw error;
168
+ }
169
+ }
170
+ /**
171
+ * Reload notes from disk (useful for external changes)
172
+ */
173
+ async reload() {
174
+ return this.mutex.dispatch(async () => {
175
+ await this.load(true);
176
+ });
44
177
  }
45
178
  async addNote(title, content, tags = [], priority = 'medium', expiresAt) {
46
179
  return this.mutex.dispatch(async () => {
47
180
  await this.load();
48
181
  const note = {
49
- id: Date.now().toString(36) + Math.random().toString(36).substring(2, 7),
182
+ id: `${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 9)}`,
50
183
  title,
51
184
  content,
52
185
  tags,
@@ -64,15 +197,23 @@ export class NotesManager {
64
197
  return this.mutex.dispatch(async () => {
65
198
  await this.load();
66
199
  const now = new Date();
67
- let activeNotes = this.notes;
200
+ let filteredNotes = this.notes;
201
+ // Filter expired notes
68
202
  if (!includeExpired) {
69
- activeNotes = this.notes.filter(n => !n.expiresAt || new Date(n.expiresAt) > now);
203
+ filteredNotes = filteredNotes.filter(n => !n.expiresAt || new Date(n.expiresAt) > now);
70
204
  }
205
+ // Filter by tag
71
206
  if (tag) {
72
- return activeNotes.filter(n => n.tags.includes(tag));
207
+ filteredNotes = filteredNotes.filter(n => n.tags.includes(tag));
73
208
  }
74
- return activeNotes.sort((a, b) => {
75
- const priorityMap = { 'critical': 4, 'high': 3, 'medium': 2, 'low': 1 };
209
+ // Sort by priority (highest first)
210
+ return filteredNotes.sort((a, b) => {
211
+ const priorityMap = {
212
+ critical: 4,
213
+ high: 3,
214
+ medium: 2,
215
+ low: 1
216
+ };
76
217
  return (priorityMap[b.priority] || 0) - (priorityMap[a.priority] || 0);
77
218
  });
78
219
  });
@@ -113,4 +254,72 @@ export class NotesManager {
113
254
  return this.notes[index];
114
255
  });
115
256
  }
257
+ /**
258
+ * Get a single note by ID
259
+ */
260
+ async getNote(id) {
261
+ return this.mutex.dispatch(async () => {
262
+ await this.load();
263
+ return this.notes.find(n => n.id === id) || null;
264
+ });
265
+ }
266
+ /**
267
+ * Clear all notes (use with caution!)
268
+ */
269
+ async clearAll() {
270
+ return this.mutex.dispatch(async () => {
271
+ this.notes = [];
272
+ await this.save();
273
+ });
274
+ }
275
+ /**
276
+ * Export notes as JSON string
277
+ */
278
+ async export() {
279
+ return this.mutex.dispatch(async () => {
280
+ await this.load();
281
+ return JSON.stringify({
282
+ version: '2.0',
283
+ notes: this.notes,
284
+ exportedAt: new Date().toISOString()
285
+ }, null, 2);
286
+ });
287
+ }
288
+ /**
289
+ * Import notes from JSON string
290
+ */
291
+ async import(jsonData, merge = false) {
292
+ return this.mutex.dispatch(async () => {
293
+ await this.load();
294
+ try {
295
+ const parsed = JSON.parse(jsonData);
296
+ let importedNotes = [];
297
+ if (parsed.version && parsed.notes) {
298
+ importedNotes = parsed.notes;
299
+ }
300
+ else if (Array.isArray(parsed)) {
301
+ importedNotes = parsed;
302
+ }
303
+ if (merge) {
304
+ // Merge: avoid duplicates by ID
305
+ const existingIds = new Set(this.notes.map(n => n.id));
306
+ for (const note of importedNotes) {
307
+ if (!existingIds.has(note.id)) {
308
+ this.notes.push(note);
309
+ existingIds.add(note.id);
310
+ }
311
+ }
312
+ }
313
+ else {
314
+ // Replace all
315
+ this.notes = importedNotes;
316
+ }
317
+ await this.save();
318
+ return this.notes.length;
319
+ }
320
+ catch (error) {
321
+ throw new Error(`Failed to import notes: ${error}`);
322
+ }
323
+ });
324
+ }
116
325
  }
@@ -36,7 +36,7 @@ async function testSystem() {
36
36
  console.log("2. Searching for 'Deepest'...");
37
37
  const results = await codeDb.searchSnippets("Deepest");
38
38
  console.log(`Found ${results.length} result(s):`);
39
- results.forEach(r => console.log(`- ${r.title} (${r.language})`));
39
+ results.forEach(r => console.log(`- ${r.snippet.title} (${r.snippet.language})`));
40
40
  console.log("\n✅ Test Complete!");
41
41
  // Cleanup
42
42
  try {
@@ -32,7 +32,7 @@ export function registerCodeDbTools(server, db) {
32
32
  if (results.length > 0) {
33
33
  output += `SNIPPETS FOUND:\n`;
34
34
  results.forEach(s => {
35
- output += `ID: ${s.id} | ${s.title} (${s.language})\nDesc: ${s.description}\nCode:\n\`\`\`\n${s.code}\n\`\`\`\n\n`;
35
+ output += `ID: ${s.snippet.id} | ${s.snippet.title} (${s.snippet.language})\nDesc: ${s.snippet.description}\nCode:\n\`\`\`\n${s.snippet.code}\n\`\`\`\n\n`;
36
36
  });
37
37
  }
38
38
  const matchedPatterns = Object.entries(patterns).filter(([k, v]) => k.toLowerCase().includes(query.toLowerCase()) || v.toLowerCase().includes(query.toLowerCase()));