@gotza02/sequential-thinking 2026.2.31 → 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.
@@ -7,22 +7,122 @@ export interface CodeSnippet {
7
7
  tags: string[];
8
8
  context?: string;
9
9
  updatedAt: string;
10
+ embedding?: number[];
10
11
  }
11
12
  export interface CodeKnowledge {
13
+ version: string;
12
14
  snippets: CodeSnippet[];
13
15
  patterns: Record<string, string>;
16
+ metadata?: {
17
+ lastModified: string;
18
+ snippetCount: number;
19
+ };
20
+ }
21
+ export interface SearchOptions {
22
+ fuzzy?: boolean;
23
+ threshold?: number;
24
+ limit?: number;
25
+ includeCode?: boolean;
26
+ }
27
+ export interface SearchResult {
28
+ snippet: CodeSnippet;
29
+ relevance: number;
30
+ matchHighlights?: {
31
+ title?: string[];
32
+ description?: string[];
33
+ tags?: string[];
34
+ };
14
35
  }
15
36
  export declare class CodeDatabase {
16
37
  private filePath;
17
38
  private db;
18
- private loaded;
39
+ private lastModifiedTime;
19
40
  private mutex;
20
41
  constructor(storagePath?: string);
42
+ /**
43
+ * Load database from disk with automatic reload detection.
44
+ * Uses mtime to detect if file has been modified externally.
45
+ */
21
46
  private load;
47
+ /**
48
+ * Attempt to recover database from corrupted JSON
49
+ */
50
+ private attemptRecovery;
51
+ /**
52
+ * Backup corrupted database file
53
+ */
54
+ private backupCorruptedFile;
55
+ /**
56
+ * Atomic save using write-to-temp + rename pattern.
57
+ */
22
58
  private save;
59
+ /**
60
+ * Reload database from disk
61
+ */
62
+ reload(): Promise<void>;
23
63
  addSnippet(snippet: Omit<CodeSnippet, 'id' | 'updatedAt'>): Promise<CodeSnippet>;
24
- searchSnippets(query: string): Promise<CodeSnippet[]>;
64
+ /**
65
+ * Enhanced search with relevance scoring and optional fuzzy matching
66
+ */
67
+ searchSnippets(query: string, options?: SearchOptions): Promise<SearchResult[]>;
68
+ /**
69
+ * Get snippet by ID
70
+ */
71
+ getSnippet(id: string): Promise<CodeSnippet | null>;
72
+ /**
73
+ * Update snippet
74
+ */
75
+ updateSnippet(id: string, updates: Partial<Omit<CodeSnippet, 'id' | 'updatedAt'>>): Promise<CodeSnippet | null>;
76
+ /**
77
+ * Delete snippet
78
+ */
79
+ deleteSnippet(id: string): Promise<boolean>;
80
+ /**
81
+ * List all snippets with optional filtering
82
+ */
83
+ listSnippets(filter?: {
84
+ language?: string;
85
+ tags?: string[];
86
+ limit?: number;
87
+ }): Promise<CodeSnippet[]>;
25
88
  learnPattern(name: string, description: string): Promise<void>;
26
89
  getPattern(name: string): Promise<string | null>;
27
90
  listAllPatterns(): Promise<Record<string, string>>;
91
+ deletePattern(name: string): Promise<boolean>;
92
+ /**
93
+ * Search patterns with fuzzy matching
94
+ */
95
+ searchPatterns(query: string, limit?: number): Promise<Array<{
96
+ name: string;
97
+ description: string;
98
+ relevance: number;
99
+ }>>;
100
+ /**
101
+ * Get database statistics
102
+ */
103
+ getStats(): Promise<{
104
+ snippetCount: number;
105
+ patternCount: number;
106
+ languages: Record<string, number>;
107
+ totalTags: string[];
108
+ lastModified: string | null;
109
+ }>;
110
+ /**
111
+ * Export database as JSON string
112
+ */
113
+ export(): Promise<string>;
114
+ /**
115
+ * Import snippets from JSON string
116
+ */
117
+ import(jsonData: string, merge?: boolean): Promise<number>;
118
+ /**
119
+ * Clear all data (use with caution!)
120
+ */
121
+ clearAll(): Promise<void>;
122
+ /**
123
+ * Prepare embeddings for semantic search
124
+ * This is a placeholder for future integration with embedding models
125
+ * Call this after adding/updating snippets to enable semantic search
126
+ */
127
+ updateEmbeddings(): Promise<void>;
28
128
  }
package/dist/codestore.js CHANGED
@@ -1,35 +1,273 @@
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';
5
+ /**
6
+ * Simple fuzzy matching using character-by-character comparison
7
+ */
8
+ function fuzzyMatch(pattern, text) {
9
+ const p = pattern.toLowerCase();
10
+ const t = text.toLowerCase();
11
+ let pIdx = 0;
12
+ let tIdx = 0;
13
+ let matches = 0;
14
+ while (pIdx < p.length && tIdx < t.length) {
15
+ if (p[pIdx] === t[tIdx]) {
16
+ matches++;
17
+ pIdx++;
18
+ }
19
+ tIdx++;
20
+ }
21
+ if (pIdx === p.length) {
22
+ // All pattern characters found - calculate score
23
+ const accuracy = matches / t.length;
24
+ const completeness = matches / p.length;
25
+ return (accuracy + completeness) / 2;
26
+ }
27
+ return 0;
28
+ }
29
+ /**
30
+ * Calculate relevance score for a snippet against a query
31
+ */
32
+ function calculateRelevance(query, snippet, includeCode) {
33
+ const q = query.toLowerCase();
34
+ const qWords = q.split(/\s+/).filter(w => w.length > 0);
35
+ let score = 0;
36
+ const maxScore = qWords.length * 3; // Max possible score
37
+ for (const word of qWords) {
38
+ // Exact match in title = 3 points
39
+ if (snippet.title.toLowerCase().includes(word)) {
40
+ score += 3;
41
+ }
42
+ // Exact match in description = 2 points
43
+ else if (snippet.description.toLowerCase().includes(word)) {
44
+ score += 2;
45
+ }
46
+ // Match in tags = 2.5 points
47
+ else if (snippet.tags.some(t => t.toLowerCase().includes(word))) {
48
+ score += 2.5;
49
+ }
50
+ // Match in context = 1.5 points
51
+ else if (snippet.context?.toLowerCase().includes(word)) {
52
+ score += 1.5;
53
+ }
54
+ // Match in code (if enabled) = 1 point
55
+ else if (includeCode && snippet.code.toLowerCase().includes(word)) {
56
+ score += 1;
57
+ }
58
+ // Partial match (fuzzy) = 0.5 points
59
+ else {
60
+ let fuzzyFound = false;
61
+ const fields = [
62
+ snippet.title,
63
+ snippet.description,
64
+ ...snippet.tags,
65
+ snippet.context || '',
66
+ includeCode ? snippet.code : ''
67
+ ];
68
+ for (const field of fields) {
69
+ if (fuzzyMatch(word, field) > 0.3) {
70
+ score += 0.5;
71
+ fuzzyFound = true;
72
+ break;
73
+ }
74
+ }
75
+ if (!fuzzyFound) {
76
+ // Very loose match - 0.1 points
77
+ if (fields.some(f => f.toLowerCase().includes(word.substring(0, Math.min(3, word.length))))) {
78
+ score += 0.1;
79
+ }
80
+ }
81
+ }
82
+ }
83
+ return Math.min(score / maxScore, 1);
84
+ }
4
85
  export class CodeDatabase {
5
86
  filePath;
6
- db = { snippets: [], patterns: {} };
7
- loaded = false;
87
+ db = { version: '2.0', snippets: [], patterns: {} };
88
+ lastModifiedTime = 0;
8
89
  mutex = new AsyncMutex();
9
90
  constructor(storagePath = 'code_database.json') {
10
91
  this.filePath = path.resolve(storagePath);
11
92
  }
12
- async load() {
13
- if (this.loaded)
14
- return;
93
+ /**
94
+ * Load database from disk with automatic reload detection.
95
+ * Uses mtime to detect if file has been modified externally.
96
+ */
97
+ async load(forceReload = false) {
15
98
  try {
99
+ if (!existsSync(this.filePath)) {
100
+ this.db = { version: '2.0', snippets: [], patterns: {} };
101
+ this.lastModifiedTime = 0;
102
+ return;
103
+ }
104
+ const stats = statSync(this.filePath);
105
+ const currentMtime = stats.mtimeMs;
106
+ // Skip reload if file hasn't been modified (unless forced)
107
+ if (!forceReload && currentMtime === this.lastModifiedTime && this.db.snippets.length > 0) {
108
+ return;
109
+ }
16
110
  const data = await fs.readFile(this.filePath, 'utf-8');
17
- this.db = JSON.parse(data);
111
+ if (!data.trim()) {
112
+ this.db = { version: '2.0', snippets: [], patterns: {} };
113
+ this.lastModifiedTime = currentMtime;
114
+ return;
115
+ }
116
+ try {
117
+ const parsed = JSON.parse(data);
118
+ // New format with version
119
+ if (parsed.version && parsed.snippets) {
120
+ this.db = parsed;
121
+ }
122
+ // Legacy format
123
+ else if (parsed.snippets || parsed.patterns) {
124
+ this.db = {
125
+ version: '2.0',
126
+ snippets: parsed.snippets || [],
127
+ patterns: parsed.patterns || {}
128
+ };
129
+ }
130
+ // Very old format (just array)
131
+ else if (Array.isArray(parsed)) {
132
+ this.db = {
133
+ version: '2.0',
134
+ snippets: parsed,
135
+ patterns: {}
136
+ };
137
+ }
138
+ this.lastModifiedTime = currentMtime;
139
+ }
140
+ catch (parseError) {
141
+ console.error(`[CodeDatabase] Parse error, attempting recovery:`, parseError);
142
+ this.attemptRecovery(data);
143
+ this.lastModifiedTime = currentMtime;
144
+ }
18
145
  }
19
146
  catch (error) {
20
- this.db = { snippets: [], patterns: {} };
147
+ if (error.code === 'ENOENT') {
148
+ this.db = { version: '2.0', snippets: [], patterns: {} };
149
+ this.lastModifiedTime = 0;
150
+ }
151
+ else {
152
+ console.error(`[CodeDatabase] Load error:`, error);
153
+ this.db = { version: '2.0', snippets: [], patterns: {} };
154
+ this.lastModifiedTime = 0;
155
+ }
21
156
  }
22
- this.loaded = true;
23
157
  }
158
+ /**
159
+ * Attempt to recover database from corrupted JSON
160
+ */
161
+ attemptRecovery(data) {
162
+ let braceCount = 0;
163
+ let lastValidIndex = -1;
164
+ for (let i = 0; i < data.length; i++) {
165
+ const char = data[i];
166
+ if (char === '{') {
167
+ braceCount++;
168
+ }
169
+ else if (char === '}') {
170
+ braceCount--;
171
+ if (braceCount === 0) {
172
+ lastValidIndex = i;
173
+ }
174
+ }
175
+ }
176
+ if (lastValidIndex !== -1) {
177
+ try {
178
+ const recoveredData = data.substring(0, lastValidIndex + 1).trim();
179
+ const parsed = JSON.parse(recoveredData);
180
+ if (parsed.version && parsed.snippets) {
181
+ this.db = parsed;
182
+ }
183
+ else if (parsed.snippets || parsed.patterns) {
184
+ this.db = {
185
+ version: '2.0',
186
+ snippets: parsed.snippets || [],
187
+ patterns: parsed.patterns || {}
188
+ };
189
+ }
190
+ else if (Array.isArray(parsed)) {
191
+ this.db = { version: '2.0', snippets: parsed, patterns: {} };
192
+ }
193
+ else {
194
+ this.db = { version: '2.0', snippets: [], patterns: {} };
195
+ }
196
+ this.backupCorruptedFile(data);
197
+ console.log(`[CodeDatabase] Recovered ${this.db.snippets.length} snippets.`);
198
+ }
199
+ catch (recoveryError) {
200
+ console.error('[CodeDatabase] Recovery failed:', recoveryError);
201
+ this.db = { version: '2.0', snippets: [], patterns: {} };
202
+ this.backupCorruptedFile(data);
203
+ }
204
+ }
205
+ else {
206
+ console.error('[CodeDatabase] No valid JSON structure found.');
207
+ this.db = { version: '2.0', snippets: [], patterns: {} };
208
+ }
209
+ }
210
+ /**
211
+ * Backup corrupted database file
212
+ */
213
+ async backupCorruptedFile(corruptedData) {
214
+ try {
215
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
216
+ const backupPath = `${this.filePath}.corrupted.${timestamp}`;
217
+ await fs.writeFile(backupPath, corruptedData, 'utf-8');
218
+ console.error(`[CodeDatabase] Corrupted data backed up to: ${backupPath}`);
219
+ }
220
+ catch (backupError) {
221
+ console.error('[CodeDatabase] Failed to backup corrupted file:', backupError);
222
+ }
223
+ }
224
+ /**
225
+ * Atomic save using write-to-temp + rename pattern.
226
+ */
24
227
  async save() {
25
- await fs.writeFile(this.filePath, JSON.stringify(this.db, null, 2), 'utf-8');
228
+ const tmpPath = `${this.filePath}.tmp`;
229
+ // Prepare storage format
230
+ const storage = {
231
+ version: '2.0',
232
+ snippets: this.db.snippets,
233
+ patterns: this.db.patterns,
234
+ metadata: {
235
+ lastModified: new Date().toISOString(),
236
+ snippetCount: this.db.snippets.length
237
+ }
238
+ };
239
+ try {
240
+ // Step 1: Write to temporary file
241
+ await fs.writeFile(tmpPath, JSON.stringify(storage, null, 2), 'utf-8');
242
+ // Step 2: Atomic rename
243
+ await fs.rename(tmpPath, this.filePath);
244
+ // Step 3: Update mtime cache
245
+ const stats = await fs.stat(this.filePath);
246
+ this.lastModifiedTime = stats.mtimeMs;
247
+ }
248
+ catch (error) {
249
+ // Clean up temp file if something went wrong
250
+ try {
251
+ await fs.unlink(tmpPath);
252
+ }
253
+ catch { }
254
+ throw error;
255
+ }
256
+ }
257
+ /**
258
+ * Reload database from disk
259
+ */
260
+ async reload() {
261
+ return this.mutex.dispatch(async () => {
262
+ await this.load(true);
263
+ });
26
264
  }
27
265
  async addSnippet(snippet) {
28
266
  return this.mutex.dispatch(async () => {
29
267
  await this.load();
30
268
  const newSnippet = {
31
269
  ...snippet,
32
- id: Math.random().toString(36).substring(2, 9),
270
+ id: `${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 9)}`,
33
271
  updatedAt: new Date().toISOString()
34
272
  };
35
273
  this.db.snippets.push(newSnippet);
@@ -37,14 +275,106 @@ export class CodeDatabase {
37
275
  return newSnippet;
38
276
  });
39
277
  }
40
- async searchSnippets(query) {
278
+ /**
279
+ * Enhanced search with relevance scoring and optional fuzzy matching
280
+ */
281
+ async searchSnippets(query, options = {}) {
41
282
  return this.mutex.dispatch(async () => {
42
283
  await this.load();
43
- const q = query.toLowerCase();
44
- return this.db.snippets.filter(s => s.title.toLowerCase().includes(q) ||
45
- s.description.toLowerCase().includes(q) ||
46
- s.tags.some(t => t.toLowerCase().includes(q)) ||
47
- s.code.toLowerCase().includes(q));
284
+ const { fuzzy = true, threshold = 0.1, limit = 50, includeCode = true } = options;
285
+ if (!query.trim()) {
286
+ return this.db.snippets.slice(0, limit).map(snippet => ({
287
+ snippet,
288
+ relevance: 1
289
+ }));
290
+ }
291
+ const results = [];
292
+ for (const snippet of this.db.snippets) {
293
+ let relevance = 0;
294
+ if (fuzzy) {
295
+ relevance = calculateRelevance(query, snippet, includeCode);
296
+ }
297
+ else {
298
+ // Simple exact match only
299
+ const q = query.toLowerCase();
300
+ relevance =
301
+ (snippet.title.toLowerCase().includes(q) ? 1 : 0) +
302
+ (snippet.description.toLowerCase().includes(q) ? 0.8 : 0) +
303
+ (snippet.tags.some(t => t.toLowerCase().includes(q)) ? 0.9 : 0) +
304
+ (includeCode && snippet.code.toLowerCase().includes(q) ? 0.5 : 0);
305
+ relevance = Math.min(relevance, 1);
306
+ }
307
+ if (relevance >= threshold) {
308
+ results.push({
309
+ snippet,
310
+ relevance
311
+ });
312
+ }
313
+ }
314
+ // Sort by relevance (highest first)
315
+ results.sort((a, b) => b.relevance - a.relevance);
316
+ return results.slice(0, limit);
317
+ });
318
+ }
319
+ /**
320
+ * Get snippet by ID
321
+ */
322
+ async getSnippet(id) {
323
+ return this.mutex.dispatch(async () => {
324
+ await this.load();
325
+ return this.db.snippets.find(s => s.id === id) || null;
326
+ });
327
+ }
328
+ /**
329
+ * Update snippet
330
+ */
331
+ async updateSnippet(id, updates) {
332
+ return this.mutex.dispatch(async () => {
333
+ await this.load();
334
+ const index = this.db.snippets.findIndex(s => s.id === id);
335
+ if (index === -1)
336
+ return null;
337
+ this.db.snippets[index] = {
338
+ ...this.db.snippets[index],
339
+ ...updates,
340
+ updatedAt: new Date().toISOString()
341
+ };
342
+ await this.save();
343
+ return this.db.snippets[index];
344
+ });
345
+ }
346
+ /**
347
+ * Delete snippet
348
+ */
349
+ async deleteSnippet(id) {
350
+ return this.mutex.dispatch(async () => {
351
+ await this.load();
352
+ const initialLength = this.db.snippets.length;
353
+ this.db.snippets = this.db.snippets.filter(s => s.id !== id);
354
+ if (this.db.snippets.length !== initialLength) {
355
+ await this.save();
356
+ return true;
357
+ }
358
+ return false;
359
+ });
360
+ }
361
+ /**
362
+ * List all snippets with optional filtering
363
+ */
364
+ async listSnippets(filter) {
365
+ return this.mutex.dispatch(async () => {
366
+ await this.load();
367
+ let results = this.db.snippets;
368
+ if (filter?.language) {
369
+ results = results.filter(s => s.language.toLowerCase() === filter.language.toLowerCase());
370
+ }
371
+ if (filter?.tags && filter.tags.length > 0) {
372
+ results = results.filter(s => filter.tags.some(t => s.tags.includes(t)));
373
+ }
374
+ if (filter?.limit) {
375
+ results = results.slice(0, filter.limit);
376
+ }
377
+ return results;
48
378
  });
49
379
  }
50
380
  async learnPattern(name, description) {
@@ -63,7 +393,159 @@ export class CodeDatabase {
63
393
  async listAllPatterns() {
64
394
  return this.mutex.dispatch(async () => {
65
395
  await this.load();
66
- return this.db.patterns;
396
+ return { ...this.db.patterns };
397
+ });
398
+ }
399
+ async deletePattern(name) {
400
+ return this.mutex.dispatch(async () => {
401
+ await this.load();
402
+ if (name in this.db.patterns) {
403
+ delete this.db.patterns[name];
404
+ await this.save();
405
+ return true;
406
+ }
407
+ return false;
408
+ });
409
+ }
410
+ /**
411
+ * Search patterns with fuzzy matching
412
+ */
413
+ async searchPatterns(query, limit = 20) {
414
+ return this.mutex.dispatch(async () => {
415
+ await this.load();
416
+ if (!query.trim()) {
417
+ return Object.entries(this.db.patterns)
418
+ .slice(0, limit)
419
+ .map(([name, description]) => ({ name, description, relevance: 1 }));
420
+ }
421
+ const q = query.toLowerCase();
422
+ const results = [];
423
+ for (const [name, description] of Object.entries(this.db.patterns)) {
424
+ let relevance = 0;
425
+ // Exact name match = highest priority
426
+ if (name.toLowerCase() === q) {
427
+ relevance = 1;
428
+ }
429
+ // Name contains query
430
+ else if (name.toLowerCase().includes(q)) {
431
+ relevance = 0.8;
432
+ }
433
+ // Description contains query
434
+ else if (description.toLowerCase().includes(q)) {
435
+ relevance = 0.6;
436
+ }
437
+ // Fuzzy match
438
+ else {
439
+ const nameScore = fuzzyMatch(q, name);
440
+ const descScore = fuzzyMatch(q, description);
441
+ relevance = Math.max(nameScore, descScore) * 0.5;
442
+ }
443
+ if (relevance > 0.1) {
444
+ results.push({ name, description, relevance });
445
+ }
446
+ }
447
+ results.sort((a, b) => b.relevance - a.relevance);
448
+ return results.slice(0, limit);
67
449
  });
68
450
  }
451
+ /**
452
+ * Get database statistics
453
+ */
454
+ async getStats() {
455
+ return this.mutex.dispatch(async () => {
456
+ await this.load();
457
+ const languages = {};
458
+ const allTags = new Set();
459
+ for (const snippet of this.db.snippets) {
460
+ languages[snippet.language] = (languages[snippet.language] || 0) + 1;
461
+ snippet.tags.forEach(t => allTags.add(t));
462
+ }
463
+ return {
464
+ snippetCount: this.db.snippets.length,
465
+ patternCount: Object.keys(this.db.patterns).length,
466
+ languages,
467
+ totalTags: Array.from(allTags),
468
+ lastModified: this.db.metadata?.lastModified || null
469
+ };
470
+ });
471
+ }
472
+ /**
473
+ * Export database as JSON string
474
+ */
475
+ async export() {
476
+ return this.mutex.dispatch(async () => {
477
+ await this.load();
478
+ return JSON.stringify({
479
+ version: '2.0',
480
+ snippets: this.db.snippets,
481
+ patterns: this.db.patterns,
482
+ exportedAt: new Date().toISOString()
483
+ }, null, 2);
484
+ });
485
+ }
486
+ /**
487
+ * Import snippets from JSON string
488
+ */
489
+ async import(jsonData, merge = false) {
490
+ return this.mutex.dispatch(async () => {
491
+ await this.load();
492
+ try {
493
+ const parsed = JSON.parse(jsonData);
494
+ let importedSnippets = [];
495
+ if (parsed.version && parsed.snippets) {
496
+ importedSnippets = parsed.snippets;
497
+ }
498
+ else if (Array.isArray(parsed)) {
499
+ importedSnippets = parsed;
500
+ }
501
+ if (merge) {
502
+ // Merge: avoid duplicates by ID
503
+ const existingIds = new Set(this.db.snippets.map(s => s.id));
504
+ for (const snippet of importedSnippets) {
505
+ if (!existingIds.has(snippet.id)) {
506
+ this.db.snippets.push(snippet);
507
+ existingIds.add(snippet.id);
508
+ }
509
+ }
510
+ }
511
+ else {
512
+ // Replace all
513
+ this.db.snippets = importedSnippets;
514
+ }
515
+ // Import patterns too
516
+ if (parsed.patterns) {
517
+ if (merge) {
518
+ Object.assign(this.db.patterns, parsed.patterns);
519
+ }
520
+ else {
521
+ this.db.patterns = parsed.patterns;
522
+ }
523
+ }
524
+ await this.save();
525
+ return this.db.snippets.length;
526
+ }
527
+ catch (error) {
528
+ throw new Error(`Failed to import database: ${error}`);
529
+ }
530
+ });
531
+ }
532
+ /**
533
+ * Clear all data (use with caution!)
534
+ */
535
+ async clearAll() {
536
+ return this.mutex.dispatch(async () => {
537
+ this.db = { version: '2.0', snippets: [], patterns: {} };
538
+ await this.save();
539
+ });
540
+ }
541
+ /**
542
+ * Prepare embeddings for semantic search
543
+ * This is a placeholder for future integration with embedding models
544
+ * Call this after adding/updating snippets to enable semantic search
545
+ */
546
+ async updateEmbeddings() {
547
+ // TODO: Integrate with an embedding service (e.g., OpenAI embeddings, local transformers)
548
+ // For now, this is a no-op placeholder
549
+ console.log('[CodeDatabase] Embedding update not yet implemented. Requires external embedding service.');
550
+ }
69
551
  }