@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.
- package/LICENSE +21 -0
- package/README.md +448 -0
- package/agents/backend-python-expert.md +453 -0
- package/agents/database-postgres-expert.md +538 -0
- package/agents/frontend-flutter-expert.md +487 -0
- package/agents/frontend-react-expert.md +424 -0
- package/agents/quality-reviewer.md +542 -0
- package/agents/specification-agent.md +505 -0
- package/bin/sutory +332 -0
- package/bin/vibe +338 -0
- package/mcp/dist/__tests__/complexity.test.js +126 -0
- package/mcp/dist/__tests__/memory.test.js +120 -0
- package/mcp/dist/__tests__/python-dart-complexity.test.js +146 -0
- package/mcp/dist/index.js +230 -0
- package/mcp/dist/lib/ContextCompressor.js +305 -0
- package/mcp/dist/lib/MemoryManager.js +334 -0
- package/mcp/dist/lib/ProjectCache.js +126 -0
- package/mcp/dist/lib/PythonParser.js +241 -0
- package/mcp/dist/tools/browser/browserPool.js +76 -0
- package/mcp/dist/tools/browser/browserUtils.js +135 -0
- package/mcp/dist/tools/browser/inspectNetworkRequests.js +140 -0
- package/mcp/dist/tools/browser/monitorConsoleLogs.js +97 -0
- package/mcp/dist/tools/convention/analyzeComplexity.js +248 -0
- package/mcp/dist/tools/convention/applyQualityRules.js +102 -0
- package/mcp/dist/tools/convention/checkCouplingCohesion.js +233 -0
- package/mcp/dist/tools/convention/complexityMetrics.js +133 -0
- package/mcp/dist/tools/convention/dartComplexity.js +117 -0
- package/mcp/dist/tools/convention/getCodingGuide.js +64 -0
- package/mcp/dist/tools/convention/languageDetector.js +50 -0
- package/mcp/dist/tools/convention/pythonComplexity.js +109 -0
- package/mcp/dist/tools/convention/suggestImprovements.js +257 -0
- package/mcp/dist/tools/convention/validateCodeQuality.js +177 -0
- package/mcp/dist/tools/memory/autoSaveContext.js +79 -0
- package/mcp/dist/tools/memory/database.js +123 -0
- package/mcp/dist/tools/memory/deleteMemory.js +39 -0
- package/mcp/dist/tools/memory/listMemories.js +38 -0
- package/mcp/dist/tools/memory/memoryConfig.js +27 -0
- package/mcp/dist/tools/memory/memorySQLite.js +138 -0
- package/mcp/dist/tools/memory/memoryUtils.js +34 -0
- package/mcp/dist/tools/memory/migrate.js +113 -0
- package/mcp/dist/tools/memory/prioritizeMemory.js +109 -0
- package/mcp/dist/tools/memory/recallMemory.js +40 -0
- package/mcp/dist/tools/memory/restoreSessionContext.js +69 -0
- package/mcp/dist/tools/memory/saveMemory.js +34 -0
- package/mcp/dist/tools/memory/searchMemories.js +37 -0
- package/mcp/dist/tools/memory/startSession.js +100 -0
- package/mcp/dist/tools/memory/updateMemory.js +46 -0
- package/mcp/dist/tools/planning/analyzeRequirements.js +166 -0
- package/mcp/dist/tools/planning/createUserStories.js +119 -0
- package/mcp/dist/tools/planning/featureRoadmap.js +202 -0
- package/mcp/dist/tools/planning/generatePrd.js +156 -0
- package/mcp/dist/tools/prompt/analyzePrompt.js +145 -0
- package/mcp/dist/tools/prompt/enhancePrompt.js +105 -0
- package/mcp/dist/tools/semantic/findReferences.js +195 -0
- package/mcp/dist/tools/semantic/findSymbol.js +200 -0
- package/mcp/dist/tools/thinking/analyzeProblem.js +50 -0
- package/mcp/dist/tools/thinking/breakDownProblem.js +140 -0
- package/mcp/dist/tools/thinking/createThinkingChain.js +39 -0
- package/mcp/dist/tools/thinking/formatAsPlan.js +73 -0
- package/mcp/dist/tools/thinking/stepByStepAnalysis.js +58 -0
- package/mcp/dist/tools/thinking/thinkAloudProcess.js +75 -0
- package/mcp/dist/tools/time/getCurrentTime.js +61 -0
- package/mcp/dist/tools/ui/previewUiAscii.js +232 -0
- package/mcp/dist/types/tool.js +2 -0
- package/mcp/package.json +53 -0
- package/package.json +49 -0
- package/scripts/install-mcp.js +48 -0
- package/scripts/install.sh +70 -0
- package/skills/core/communication-guide.md +104 -0
- package/skills/core/development-philosophy.md +53 -0
- package/skills/core/quick-start.md +121 -0
- package/skills/languages/dart-flutter.md +509 -0
- package/skills/languages/python-fastapi.md +386 -0
- package/skills/languages/typescript-nextjs.md +441 -0
- package/skills/languages/typescript-react-native.md +446 -0
- package/skills/languages/typescript-react.md +525 -0
- package/skills/quality/checklist.md +276 -0
- package/skills/quality/testing-strategy.md +437 -0
- package/skills/standards/anti-patterns.md +369 -0
- package/skills/standards/code-structure.md +291 -0
- package/skills/standards/complexity-metrics.md +312 -0
- package/skills/standards/naming-conventions.md +198 -0
- package/skills/tools/mcp-hi-ai-guide.md +665 -0
- package/skills/tools/mcp-workflow.md +51 -0
- package/templates/constitution-template.md +193 -0
- package/templates/plan-template.md +237 -0
- package/templates/spec-template.md +142 -0
- 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
|
+
}
|