@su-record/vibe 0.1.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.
Files changed (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +448 -0
  3. package/agents/backend-python-expert.md +453 -0
  4. package/agents/database-postgres-expert.md +538 -0
  5. package/agents/frontend-flutter-expert.md +487 -0
  6. package/agents/frontend-react-expert.md +424 -0
  7. package/agents/quality-reviewer.md +542 -0
  8. package/agents/specification-agent.md +505 -0
  9. package/bin/sutory +332 -0
  10. package/bin/vibe +338 -0
  11. package/mcp/dist/__tests__/complexity.test.js +126 -0
  12. package/mcp/dist/__tests__/memory.test.js +120 -0
  13. package/mcp/dist/__tests__/python-dart-complexity.test.js +146 -0
  14. package/mcp/dist/index.js +230 -0
  15. package/mcp/dist/lib/ContextCompressor.js +305 -0
  16. package/mcp/dist/lib/MemoryManager.js +334 -0
  17. package/mcp/dist/lib/ProjectCache.js +126 -0
  18. package/mcp/dist/lib/PythonParser.js +241 -0
  19. package/mcp/dist/tools/browser/browserPool.js +76 -0
  20. package/mcp/dist/tools/browser/browserUtils.js +135 -0
  21. package/mcp/dist/tools/browser/inspectNetworkRequests.js +140 -0
  22. package/mcp/dist/tools/browser/monitorConsoleLogs.js +97 -0
  23. package/mcp/dist/tools/convention/analyzeComplexity.js +248 -0
  24. package/mcp/dist/tools/convention/applyQualityRules.js +102 -0
  25. package/mcp/dist/tools/convention/checkCouplingCohesion.js +233 -0
  26. package/mcp/dist/tools/convention/complexityMetrics.js +133 -0
  27. package/mcp/dist/tools/convention/dartComplexity.js +117 -0
  28. package/mcp/dist/tools/convention/getCodingGuide.js +64 -0
  29. package/mcp/dist/tools/convention/languageDetector.js +50 -0
  30. package/mcp/dist/tools/convention/pythonComplexity.js +109 -0
  31. package/mcp/dist/tools/convention/suggestImprovements.js +257 -0
  32. package/mcp/dist/tools/convention/validateCodeQuality.js +177 -0
  33. package/mcp/dist/tools/memory/autoSaveContext.js +79 -0
  34. package/mcp/dist/tools/memory/database.js +123 -0
  35. package/mcp/dist/tools/memory/deleteMemory.js +39 -0
  36. package/mcp/dist/tools/memory/listMemories.js +38 -0
  37. package/mcp/dist/tools/memory/memoryConfig.js +27 -0
  38. package/mcp/dist/tools/memory/memorySQLite.js +138 -0
  39. package/mcp/dist/tools/memory/memoryUtils.js +34 -0
  40. package/mcp/dist/tools/memory/migrate.js +113 -0
  41. package/mcp/dist/tools/memory/prioritizeMemory.js +109 -0
  42. package/mcp/dist/tools/memory/recallMemory.js +40 -0
  43. package/mcp/dist/tools/memory/restoreSessionContext.js +69 -0
  44. package/mcp/dist/tools/memory/saveMemory.js +34 -0
  45. package/mcp/dist/tools/memory/searchMemories.js +37 -0
  46. package/mcp/dist/tools/memory/startSession.js +100 -0
  47. package/mcp/dist/tools/memory/updateMemory.js +46 -0
  48. package/mcp/dist/tools/planning/analyzeRequirements.js +166 -0
  49. package/mcp/dist/tools/planning/createUserStories.js +119 -0
  50. package/mcp/dist/tools/planning/featureRoadmap.js +202 -0
  51. package/mcp/dist/tools/planning/generatePrd.js +156 -0
  52. package/mcp/dist/tools/prompt/analyzePrompt.js +145 -0
  53. package/mcp/dist/tools/prompt/enhancePrompt.js +105 -0
  54. package/mcp/dist/tools/semantic/findReferences.js +195 -0
  55. package/mcp/dist/tools/semantic/findSymbol.js +200 -0
  56. package/mcp/dist/tools/thinking/analyzeProblem.js +50 -0
  57. package/mcp/dist/tools/thinking/breakDownProblem.js +140 -0
  58. package/mcp/dist/tools/thinking/createThinkingChain.js +39 -0
  59. package/mcp/dist/tools/thinking/formatAsPlan.js +73 -0
  60. package/mcp/dist/tools/thinking/stepByStepAnalysis.js +58 -0
  61. package/mcp/dist/tools/thinking/thinkAloudProcess.js +75 -0
  62. package/mcp/dist/tools/time/getCurrentTime.js +61 -0
  63. package/mcp/dist/tools/ui/previewUiAscii.js +232 -0
  64. package/mcp/dist/types/tool.js +2 -0
  65. package/mcp/package.json +53 -0
  66. package/package.json +49 -0
  67. package/scripts/install-mcp.js +48 -0
  68. package/scripts/install.sh +70 -0
  69. package/skills/core/communication-guide.md +104 -0
  70. package/skills/core/development-philosophy.md +53 -0
  71. package/skills/core/quick-start.md +121 -0
  72. package/skills/languages/dart-flutter.md +509 -0
  73. package/skills/languages/python-fastapi.md +386 -0
  74. package/skills/languages/typescript-nextjs.md +441 -0
  75. package/skills/languages/typescript-react-native.md +446 -0
  76. package/skills/languages/typescript-react.md +525 -0
  77. package/skills/quality/checklist.md +276 -0
  78. package/skills/quality/testing-strategy.md +437 -0
  79. package/skills/standards/anti-patterns.md +369 -0
  80. package/skills/standards/code-structure.md +291 -0
  81. package/skills/standards/complexity-metrics.md +312 -0
  82. package/skills/standards/naming-conventions.md +198 -0
  83. package/skills/tools/mcp-hi-ai-guide.md +665 -0
  84. package/skills/tools/mcp-workflow.md +51 -0
  85. package/templates/constitution-template.md +193 -0
  86. package/templates/plan-template.md +237 -0
  87. package/templates/spec-template.md +142 -0
  88. package/templates/tasks-template.md +132 -0
@@ -0,0 +1,305 @@
1
+ // Context compression utility (v1.3)
2
+ // Intelligently compress context when approaching token limits
3
+ export class ContextCompressor {
4
+ static MAX_CHUNK_SIZE = 500; // characters
5
+ static DEFAULT_TARGET_TOKENS = 4000;
6
+ static TOKENS_PER_CHAR_ESTIMATE = 0.25;
7
+ static MAX_SCORE = 100;
8
+ static MIN_SCORE = 0;
9
+ static CODE_KEYWORDS = [
10
+ 'function', 'class', 'const', 'let', 'var', 'import', 'export',
11
+ 'def', 'async', 'await', 'return', 'if', 'for', 'while'
12
+ ];
13
+ static IMPORTANT_KEYWORDS = [
14
+ 'error', 'bug', 'fix', 'issue', 'problem', 'solution',
15
+ '에러', '버그', '수정', '문제', '해결', 'TODO', 'FIXME'
16
+ ];
17
+ /**
18
+ * Compress context by selecting most important chunks
19
+ * @param context - Text content to compress
20
+ * @param targetTokens - Target token count (default: 4000)
21
+ * @returns Compression result with statistics
22
+ */
23
+ static compress(context, targetTokens = ContextCompressor.DEFAULT_TARGET_TOKENS) {
24
+ // Handle empty or very short context
25
+ if (!context || context.trim().length === 0) {
26
+ return {
27
+ compressed: '',
28
+ originalSize: 0,
29
+ compressedSize: 0,
30
+ compressionRatio: 0,
31
+ removedSections: [],
32
+ retainedSections: []
33
+ };
34
+ }
35
+ const chunks = this.splitIntoChunks(context);
36
+ const scoredChunks = chunks.map(chunk => this.scoreChunk(chunk));
37
+ // If content is already smaller than target, return as-is
38
+ // Only skip compression if content is very small (use 1.2x instead of 4x)
39
+ // This ensures compression activates more aggressively
40
+ if (context.length <= targetTokens * 1.2) {
41
+ return {
42
+ compressed: context,
43
+ originalSize: context.length,
44
+ compressedSize: context.length,
45
+ compressionRatio: 1,
46
+ removedSections: [],
47
+ retainedSections: scoredChunks.map(s => s.type),
48
+ retentionStats: {
49
+ codeRetentionPercent: 100,
50
+ answerRetentionPercent: 100,
51
+ questionRetentionPercent: 100
52
+ }
53
+ };
54
+ }
55
+ // Sort by score (highest first)
56
+ scoredChunks.sort((a, b) => b.score - a.score);
57
+ // Select chunks until target size
58
+ // TOKENS_PER_CHAR_ESTIMATE = 0.25 means 1 char ≈ 0.25 tokens, so 4 chars ≈ 1 token
59
+ // Reserve space for headers and formatting (5% overhead, min 50 chars, max 300 chars)
60
+ const HEADER_OVERHEAD = Math.max(50, Math.min(300, targetTokens * 4 * 0.05));
61
+ const targetChars = (targetTokens * 4) - HEADER_OVERHEAD;
62
+ const selected = [];
63
+ const removed = [];
64
+ let currentSize = 0;
65
+ for (const chunk of scoredChunks) {
66
+ if (currentSize + chunk.text.length <= targetChars) {
67
+ selected.push(chunk);
68
+ currentSize += chunk.text.length;
69
+ }
70
+ else {
71
+ removed.push(this.summarizeChunk(chunk));
72
+ }
73
+ }
74
+ // Reconstruct compressed context
75
+ const compressed = this.reconstructContext(selected, removed);
76
+ // Calculate retention statistics
77
+ const retentionStats = this.calculateRetentionStats(scoredChunks, selected);
78
+ return {
79
+ compressed,
80
+ originalSize: context.length,
81
+ compressedSize: compressed.length,
82
+ compressionRatio: compressed.length / context.length,
83
+ removedSections: removed,
84
+ retainedSections: selected.map(s => s.type),
85
+ retentionStats
86
+ };
87
+ }
88
+ /**
89
+ * Calculate retention percentages by type
90
+ */
91
+ static calculateRetentionStats(allChunks, selectedChunks) {
92
+ const countByType = (chunks, type) => {
93
+ return chunks.filter(c => c.type === type).length;
94
+ };
95
+ const totalCode = countByType(allChunks, 'code');
96
+ const totalAnswer = countByType(allChunks, 'answer');
97
+ const totalQuestion = countByType(allChunks, 'question');
98
+ const retainedCode = countByType(selectedChunks, 'code');
99
+ const retainedAnswer = countByType(selectedChunks, 'answer');
100
+ const retainedQuestion = countByType(selectedChunks, 'question');
101
+ return {
102
+ codeRetentionPercent: totalCode > 0 ? Math.round((retainedCode / totalCode) * 100) : 0,
103
+ answerRetentionPercent: totalAnswer > 0 ? Math.round((retainedAnswer / totalAnswer) * 100) : 0,
104
+ questionRetentionPercent: totalQuestion > 0 ? Math.round((retainedQuestion / totalQuestion) * 100) : 0
105
+ };
106
+ }
107
+ /**
108
+ * Split context into manageable chunks
109
+ */
110
+ static splitIntoChunks(context) {
111
+ const chunks = [];
112
+ const lines = context.split('\n');
113
+ let currentChunk = '';
114
+ for (const line of lines) {
115
+ if (currentChunk.length + line.length > this.MAX_CHUNK_SIZE) {
116
+ if (currentChunk.trim()) {
117
+ chunks.push(currentChunk.trim());
118
+ }
119
+ currentChunk = line;
120
+ }
121
+ else {
122
+ currentChunk += '\n' + line;
123
+ }
124
+ }
125
+ if (currentChunk.trim()) {
126
+ chunks.push(currentChunk.trim());
127
+ }
128
+ return chunks;
129
+ }
130
+ /**
131
+ * Score chunk importance (0-100)
132
+ * @param text - Text chunk to score
133
+ * @returns Scored chunk with type and keywords
134
+ */
135
+ static scoreChunk(text) {
136
+ const lowerText = text.toLowerCase();
137
+ const type = this.detectChunkType(lowerText, text);
138
+ const keywords = this.extractKeywords(lowerText);
139
+ const baseScore = this.calculateBaseScore(text, lowerText, type);
140
+ const finalScore = Math.max(ContextCompressor.MIN_SCORE, Math.min(ContextCompressor.MAX_SCORE, baseScore));
141
+ return { text, score: finalScore, type, keywords };
142
+ }
143
+ /**
144
+ * Detect chunk type based on content
145
+ */
146
+ static detectChunkType(lowerText, text) {
147
+ if (text.includes('```'))
148
+ return 'code';
149
+ if (lowerText.match(/^(answer|solution|결과|답변):/i))
150
+ return 'answer';
151
+ if (lowerText.match(/^(timestamp|date|author|file):/i))
152
+ return 'metadata';
153
+ if (lowerText.includes('?'))
154
+ return 'question';
155
+ if (this.CODE_KEYWORDS.some(kw => lowerText.includes(kw)))
156
+ return 'code';
157
+ return 'explanation';
158
+ }
159
+ /**
160
+ * Extract important keywords from text
161
+ */
162
+ static extractKeywords(lowerText) {
163
+ const keywords = [];
164
+ for (const keyword of this.IMPORTANT_KEYWORDS) {
165
+ if (lowerText.includes(keyword.toLowerCase())) {
166
+ keywords.push(keyword);
167
+ }
168
+ }
169
+ return keywords;
170
+ }
171
+ /**
172
+ * Calculate base score for chunk
173
+ */
174
+ static calculateBaseScore(text, lowerText, type) {
175
+ let score = 0;
176
+ // Type-based scoring
177
+ score += this.getTypeScore(type, lowerText);
178
+ // Keyword bonus
179
+ score += this.getKeywordScore(lowerText);
180
+ // Structure bonuses
181
+ score += this.getStructureScore(text);
182
+ return score;
183
+ }
184
+ /**
185
+ * Get score based on chunk type
186
+ */
187
+ static getTypeScore(type, lowerText) {
188
+ const typeScores = {
189
+ code: 30,
190
+ answer: 35,
191
+ question: 25,
192
+ explanation: 0,
193
+ metadata: -20
194
+ };
195
+ return typeScores[type];
196
+ }
197
+ /**
198
+ * Get score for important keywords
199
+ */
200
+ static getKeywordScore(lowerText) {
201
+ let score = 0;
202
+ for (const keyword of this.IMPORTANT_KEYWORDS) {
203
+ if (lowerText.includes(keyword.toLowerCase())) {
204
+ score += 15;
205
+ }
206
+ }
207
+ return score;
208
+ }
209
+ /**
210
+ * Get score based on text structure
211
+ */
212
+ static getStructureScore(text) {
213
+ let score = 0;
214
+ // Penalize very long chunks
215
+ if (text.length > 1000)
216
+ score -= 10;
217
+ // Boost short, concise chunks
218
+ if (text.length < 200 && text.split('\n').length <= 5)
219
+ score += 10;
220
+ // Boost structured content (lists)
221
+ if (text.match(/^[\d\-\*•]/m))
222
+ score += 15;
223
+ // Boost code blocks
224
+ if (text.includes('```'))
225
+ score += 20;
226
+ return score;
227
+ }
228
+ /**
229
+ * Summarize removed chunk (one-liner)
230
+ */
231
+ static summarizeChunk(chunk) {
232
+ const firstLine = chunk.text.split('\n')[0].trim();
233
+ const summary = firstLine.length > 80
234
+ ? firstLine.substring(0, 77) + '...'
235
+ : firstLine;
236
+ return `[${chunk.type}] ${summary}`;
237
+ }
238
+ /**
239
+ * Reconstruct compressed context
240
+ */
241
+ static reconstructContext(selected, removed) {
242
+ // Group by type for better organization
243
+ const byType = {
244
+ code: [],
245
+ answer: [],
246
+ question: [],
247
+ explanation: [],
248
+ metadata: []
249
+ };
250
+ selected.forEach(chunk => {
251
+ byType[chunk.type].push(chunk);
252
+ });
253
+ const sections = [];
254
+ // Add header
255
+ sections.push('[Compressed Context - High Priority Information]\n');
256
+ // Add answers first (most important)
257
+ if (byType.answer.length > 0) {
258
+ sections.push('## Key Answers & Solutions');
259
+ sections.push(byType.answer.map(c => c.text).join('\n\n'));
260
+ sections.push('');
261
+ }
262
+ // Add code blocks
263
+ if (byType.code.length > 0) {
264
+ sections.push('## Code Snippets');
265
+ sections.push(byType.code.map(c => c.text).join('\n\n'));
266
+ sections.push('');
267
+ }
268
+ // Add questions
269
+ if (byType.question.length > 0) {
270
+ sections.push('## Questions');
271
+ sections.push(byType.question.map(c => c.text).join('\n\n'));
272
+ sections.push('');
273
+ }
274
+ // Add explanations
275
+ if (byType.explanation.length > 0) {
276
+ sections.push('## Context');
277
+ sections.push(byType.explanation.map(c => c.text).join('\n\n'));
278
+ sections.push('');
279
+ }
280
+ // Add summary of removed sections
281
+ if (removed.length > 0) {
282
+ sections.push('## Removed Sections (Low Priority)');
283
+ sections.push(removed.join('\n'));
284
+ }
285
+ return sections.join('\n');
286
+ }
287
+ /**
288
+ * Extract key entities (names, numbers, dates) from context
289
+ */
290
+ static extractKeyEntities(context) {
291
+ const names = Array.from(new Set(context.match(/\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+)*\b/g) || []));
292
+ const numbers = Array.from(new Set(context.match(/\b\d+(?:\.\d+)?\b/g) || []));
293
+ const dates = Array.from(new Set(context.match(/\d{4}-\d{2}-\d{2}|\d{2}\/\d{2}\/\d{4}/g) || []));
294
+ const files = Array.from(new Set(context.match(/[\w\-]+\.[a-z]{2,4}\b/gi) || []));
295
+ return { names, numbers, dates, files };
296
+ }
297
+ /**
298
+ * Estimate token count (rough approximation)
299
+ */
300
+ static estimateTokens(text) {
301
+ // GPT-like tokenization: ~1 token per 4 characters
302
+ // More accurate would require actual tokenizer
303
+ return Math.ceil(text.length / 4);
304
+ }
305
+ }
@@ -0,0 +1,334 @@
1
+ // SQLite-based memory management system
2
+ // Replaces JSON file storage with proper database
3
+ import Database from 'better-sqlite3';
4
+ import path from 'path';
5
+ export class MemoryManager {
6
+ db;
7
+ static instance = null;
8
+ dbPath;
9
+ recallStmt = null;
10
+ saveStmt = null;
11
+ recallSelectStmt = null;
12
+ recallUpdateStmt = null;
13
+ constructor(customDbPath) {
14
+ if (customDbPath) {
15
+ this.dbPath = customDbPath;
16
+ }
17
+ else {
18
+ const memoryDir = path.join(process.cwd(), 'memories');
19
+ this.dbPath = path.join(memoryDir, 'memories.db');
20
+ // Ensure directory exists synchronously (needed for DB init)
21
+ try {
22
+ require('fs').mkdirSync(memoryDir, { recursive: true });
23
+ }
24
+ catch (error) {
25
+ const nodeError = error;
26
+ if (nodeError.code !== 'EEXIST') {
27
+ throw new Error(`Failed to create memory directory: ${nodeError.message}`);
28
+ }
29
+ }
30
+ }
31
+ this.db = new Database(this.dbPath);
32
+ this.initializeDatabase();
33
+ // Only migrate if using default path (not for tests)
34
+ if (!customDbPath) {
35
+ this.migrateFromJSON();
36
+ }
37
+ }
38
+ static cleanupRegistered = false;
39
+ static getInstance(customDbPath) {
40
+ if (!MemoryManager.instance) {
41
+ MemoryManager.instance = new MemoryManager(customDbPath);
42
+ // Register cleanup handlers only once
43
+ if (!MemoryManager.cleanupRegistered) {
44
+ MemoryManager.cleanupRegistered = true;
45
+ // Increase max listeners to avoid warnings in test environments
46
+ process.setMaxListeners(Math.max(process.getMaxListeners(), 15));
47
+ // Register cleanup on process exit to prevent memory leaks
48
+ const cleanup = () => {
49
+ if (MemoryManager.instance) {
50
+ MemoryManager.instance.close();
51
+ }
52
+ };
53
+ process.on('exit', cleanup);
54
+ process.on('SIGINT', () => {
55
+ cleanup();
56
+ process.exit(0);
57
+ });
58
+ process.on('SIGTERM', () => {
59
+ cleanup();
60
+ process.exit(0);
61
+ });
62
+ }
63
+ }
64
+ return MemoryManager.instance;
65
+ }
66
+ initializeDatabase() {
67
+ // Create memories table
68
+ this.db.exec(`
69
+ CREATE TABLE IF NOT EXISTS memories (
70
+ key TEXT PRIMARY KEY,
71
+ value TEXT NOT NULL,
72
+ category TEXT NOT NULL DEFAULT 'general',
73
+ timestamp TEXT NOT NULL,
74
+ lastAccessed TEXT NOT NULL,
75
+ priority INTEGER DEFAULT 0
76
+ );
77
+
78
+ CREATE INDEX IF NOT EXISTS idx_category ON memories(category);
79
+ CREATE INDEX IF NOT EXISTS idx_timestamp ON memories(timestamp);
80
+ CREATE INDEX IF NOT EXISTS idx_priority ON memories(priority);
81
+ CREATE INDEX IF NOT EXISTS idx_lastAccessed ON memories(lastAccessed);
82
+ `);
83
+ // Enable WAL mode for better concurrency
84
+ this.db.pragma('journal_mode = WAL');
85
+ // Pre-compile frequently used statements for performance
86
+ this.initializePreparedStatements();
87
+ }
88
+ initializePreparedStatements() {
89
+ // Pre-compile recall statement
90
+ try {
91
+ this.recallStmt = this.db.prepare(`
92
+ UPDATE memories SET lastAccessed = ?
93
+ WHERE key = ?
94
+ RETURNING *
95
+ `);
96
+ }
97
+ catch (error) {
98
+ // RETURNING not supported, pre-compile fallback statements
99
+ this.recallStmt = null;
100
+ this.recallSelectStmt = this.db.prepare(`SELECT * FROM memories WHERE key = ?`);
101
+ this.recallUpdateStmt = this.db.prepare(`UPDATE memories SET lastAccessed = ? WHERE key = ?`);
102
+ }
103
+ // Pre-compile save statement
104
+ this.saveStmt = this.db.prepare(`
105
+ INSERT OR REPLACE INTO memories (key, value, category, timestamp, lastAccessed, priority)
106
+ VALUES (?, ?, ?, ?, ?, ?)
107
+ `);
108
+ }
109
+ /**
110
+ * Auto-migrate from JSON to SQLite database
111
+ */
112
+ migrateFromJSON() {
113
+ const jsonPath = this.getJSONPath();
114
+ const memories = this.loadJSONMemories(jsonPath);
115
+ if (memories.length === 0)
116
+ return;
117
+ this.importMemories(memories);
118
+ this.backupAndCleanup(jsonPath, memories.length);
119
+ }
120
+ /**
121
+ * Get JSON file path
122
+ */
123
+ getJSONPath() {
124
+ return path.join(path.dirname(this.dbPath), 'memories.json');
125
+ }
126
+ /**
127
+ * Load memories from JSON file
128
+ */
129
+ loadJSONMemories(jsonPath) {
130
+ try {
131
+ const jsonData = require('fs').readFileSync(jsonPath, 'utf-8');
132
+ return JSON.parse(jsonData);
133
+ }
134
+ catch (error) {
135
+ return [];
136
+ }
137
+ }
138
+ /**
139
+ * Import memories into SQLite database
140
+ */
141
+ importMemories(memories) {
142
+ const insert = this.db.prepare(`
143
+ INSERT OR REPLACE INTO memories (key, value, category, timestamp, lastAccessed, priority)
144
+ VALUES (?, ?, ?, ?, ?, ?)
145
+ `);
146
+ const insertMany = this.db.transaction((items) => {
147
+ for (const item of items) {
148
+ insert.run(item.key, item.value, item.category || 'general', item.timestamp, item.lastAccessed, item.priority || 0);
149
+ }
150
+ });
151
+ insertMany(memories);
152
+ }
153
+ /**
154
+ * Backup JSON file and log migration
155
+ */
156
+ backupAndCleanup(jsonPath, count) {
157
+ try {
158
+ require('fs').renameSync(jsonPath, `${jsonPath}.backup`);
159
+ // Migration successful - could add logger here
160
+ }
161
+ catch (error) {
162
+ // Backup failed but migration completed
163
+ }
164
+ }
165
+ /**
166
+ * Save or update a memory item
167
+ * @param key - Unique identifier for the memory
168
+ * @param value - Content to store
169
+ * @param category - Category for organization (default: 'general')
170
+ * @param priority - Priority level (default: 0)
171
+ */
172
+ save(key, value, category = 'general', priority = 0) {
173
+ const timestamp = new Date().toISOString();
174
+ if (this.saveStmt) {
175
+ this.saveStmt.run(key, value, category, timestamp, timestamp, priority);
176
+ }
177
+ else {
178
+ // Fallback if prepared statement not available
179
+ const stmt = this.db.prepare(`
180
+ INSERT OR REPLACE INTO memories (key, value, category, timestamp, lastAccessed, priority)
181
+ VALUES (?, ?, ?, ?, ?, ?)
182
+ `);
183
+ stmt.run(key, value, category, timestamp, timestamp, priority);
184
+ }
185
+ }
186
+ /**
187
+ * Recall a memory item by key
188
+ * @param key - Memory key to recall
189
+ * @returns Memory item or null if not found
190
+ */
191
+ recall(key) {
192
+ const timestamp = new Date().toISOString();
193
+ // Use pre-compiled statement if available
194
+ if (this.recallStmt) {
195
+ const result = this.recallStmt.get(timestamp, key);
196
+ return result || null;
197
+ }
198
+ // Fallback for older SQLite versions (using pre-compiled statements)
199
+ if (!this.recallSelectStmt || !this.recallUpdateStmt) {
200
+ throw new Error('Fallback recall statements not initialized');
201
+ }
202
+ const result = this.recallSelectStmt.get(key);
203
+ if (result) {
204
+ this.recallUpdateStmt.run(timestamp, key);
205
+ }
206
+ return result || null;
207
+ }
208
+ /**
209
+ * Delete a memory item
210
+ * @param key - Memory key to delete
211
+ * @returns True if deleted successfully
212
+ */
213
+ delete(key) {
214
+ const stmt = this.db.prepare(`
215
+ DELETE FROM memories WHERE key = ?
216
+ `);
217
+ const result = stmt.run(key);
218
+ return result.changes > 0;
219
+ }
220
+ /**
221
+ * Update a memory item's value
222
+ * @param key - Memory key to update
223
+ * @param value - New value
224
+ * @returns True if updated successfully
225
+ */
226
+ update(key, value) {
227
+ const timestamp = new Date().toISOString();
228
+ const stmt = this.db.prepare(`
229
+ UPDATE memories
230
+ SET value = ?, timestamp = ?, lastAccessed = ?
231
+ WHERE key = ?
232
+ `);
233
+ const result = stmt.run(value, timestamp, timestamp, key);
234
+ return result.changes > 0;
235
+ }
236
+ /**
237
+ * List all memories or filter by category
238
+ * @param category - Optional category filter
239
+ * @returns Array of memory items
240
+ */
241
+ list(category) {
242
+ let stmt;
243
+ if (category) {
244
+ stmt = this.db.prepare(`
245
+ SELECT * FROM memories WHERE category = ?
246
+ ORDER BY priority DESC, timestamp DESC
247
+ `);
248
+ return stmt.all(category);
249
+ }
250
+ else {
251
+ stmt = this.db.prepare(`
252
+ SELECT * FROM memories
253
+ ORDER BY priority DESC, timestamp DESC
254
+ `);
255
+ return stmt.all();
256
+ }
257
+ }
258
+ /**
259
+ * Search memories by keyword
260
+ * @param query - Search query string
261
+ * @returns Array of matching memory items
262
+ */
263
+ search(query) {
264
+ const stmt = this.db.prepare(`
265
+ SELECT * FROM memories
266
+ WHERE key LIKE ? OR value LIKE ?
267
+ ORDER BY priority DESC, timestamp DESC
268
+ `);
269
+ const pattern = `%${query}%`;
270
+ return stmt.all(pattern, pattern);
271
+ }
272
+ /**
273
+ * Get memories by priority level
274
+ * @param priority - Priority level to filter
275
+ * @returns Array of memory items with specified priority
276
+ */
277
+ getByPriority(priority) {
278
+ const stmt = this.db.prepare(`
279
+ SELECT * FROM memories
280
+ WHERE priority = ?
281
+ ORDER BY timestamp DESC
282
+ `);
283
+ return stmt.all(priority);
284
+ }
285
+ /**
286
+ * Update priority of a memory item
287
+ * @param key - Memory key
288
+ * @param priority - New priority level
289
+ * @returns True if updated successfully
290
+ */
291
+ setPriority(key, priority) {
292
+ const stmt = this.db.prepare(`
293
+ UPDATE memories SET priority = ? WHERE key = ?
294
+ `);
295
+ const result = stmt.run(priority, key);
296
+ return result.changes > 0;
297
+ }
298
+ /**
299
+ * Get memory statistics (optimized to single query)
300
+ * @returns Total count and count by category
301
+ */
302
+ getStats() {
303
+ // Single query with ROLLUP or combined approach
304
+ const categories = this.db.prepare(`
305
+ SELECT category, COUNT(*) as count
306
+ FROM memories
307
+ GROUP BY category
308
+ `).all();
309
+ const byCategory = {};
310
+ let total = 0;
311
+ categories.forEach(cat => {
312
+ byCategory[cat.category] = cat.count;
313
+ total += cat.count;
314
+ });
315
+ return { total, byCategory };
316
+ }
317
+ /**
318
+ * Close database connection
319
+ */
320
+ close() {
321
+ if (this.db) {
322
+ this.db.close();
323
+ MemoryManager.instance = null;
324
+ }
325
+ }
326
+ /**
327
+ * Reset singleton instance (useful for testing and cleanup)
328
+ */
329
+ static resetInstance() {
330
+ if (MemoryManager.instance) {
331
+ MemoryManager.instance.close();
332
+ }
333
+ }
334
+ }