@hbarefoot/engram 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,396 @@
1
+ /**
2
+ * Export memories to static context files
3
+ * Generates files for .md, .txt, or .claude formats
4
+ */
5
+
6
+ import { listMemories } from '../memory/store.js';
7
+ import * as logger from '../utils/logger.js';
8
+
9
+ /**
10
+ * Export memories to a static context format
11
+ * @param {Database} db - SQLite database instance
12
+ * @param {Object} options - Export options
13
+ * @param {string} options.namespace - Namespace to export from
14
+ * @param {string} [options.format='markdown'] - Output format
15
+ * @param {string[]} [options.categories] - Filter by categories
16
+ * @param {number} [options.minConfidence=0.5] - Minimum confidence
17
+ * @param {number} [options.minAccess=0] - Minimum access count
18
+ * @param {boolean} [options.includeLowFeedback=false] - Include low feedback memories
19
+ * @param {string} [options.groupBy='category'] - Grouping method
20
+ * @param {string} [options.header] - Custom header text
21
+ * @param {string} [options.footer] - Custom footer text
22
+ * @returns {Object} Export result with content and metadata
23
+ */
24
+ export function exportToStatic(db, options = {}) {
25
+ const {
26
+ namespace,
27
+ format = 'markdown',
28
+ categories,
29
+ minConfidence = 0.5,
30
+ minAccess = 0,
31
+ includeLowFeedback = false,
32
+ groupBy = 'category',
33
+ header,
34
+ footer
35
+ } = options;
36
+
37
+ if (!namespace) {
38
+ throw new Error('Namespace is required for export');
39
+ }
40
+
41
+ logger.info('Exporting memories to static context', { namespace, format, minConfidence });
42
+
43
+ // Fetch all memories from namespace
44
+ let memories = listMemories(db, {
45
+ namespace,
46
+ limit: 1000,
47
+ sort: 'confidence DESC, access_count DESC'
48
+ });
49
+
50
+ // Apply filters
51
+ memories = memories.filter(m => m.confidence >= minConfidence);
52
+ memories = memories.filter(m => m.access_count >= minAccess);
53
+
54
+ if (!includeLowFeedback) {
55
+ memories = memories.filter(m => (m.feedback_score || 0) >= -0.3);
56
+ }
57
+
58
+ if (categories && categories.length > 0) {
59
+ memories = memories.filter(m => categories.includes(m.category));
60
+ }
61
+
62
+ // Deduplicate similar memories (keep highest confidence version)
63
+ memories = deduplicateMemories(memories);
64
+
65
+ // Sort by relevance within groups
66
+ memories.sort((a, b) => {
67
+ const scoreA = (a.confidence || 0.8) * (a.access_count + 1);
68
+ const scoreB = (b.confidence || 0.8) * (b.access_count + 1);
69
+ return scoreB - scoreA;
70
+ });
71
+
72
+ // Generate content
73
+ let content;
74
+ let filename;
75
+
76
+ switch (format) {
77
+ case 'markdown':
78
+ content = generateMarkdown(memories, namespace, { header, footer, groupBy });
79
+ filename = `${namespace}-context.md`;
80
+ break;
81
+ case 'claude':
82
+ content = generateClaudeFormat(memories, namespace, { header, footer });
83
+ filename = '.claude';
84
+ break;
85
+ case 'txt':
86
+ content = generatePlainText(memories, namespace, { header, footer, groupBy });
87
+ filename = `${namespace}-context.txt`;
88
+ break;
89
+ case 'json':
90
+ content = generateJSON(memories, namespace, { categories, minConfidence });
91
+ filename = `${namespace}-context.json`;
92
+ break;
93
+ default:
94
+ content = generateMarkdown(memories, namespace, { header, footer, groupBy });
95
+ filename = `${namespace}-context.md`;
96
+ }
97
+
98
+ // Warn if content is very large
99
+ const sizeKB = Buffer.byteLength(content, 'utf8') / 1024;
100
+ if (sizeKB > 50) {
101
+ logger.warn('Export content is large', { sizeKB: sizeKB.toFixed(1) });
102
+ }
103
+
104
+ const stats = {
105
+ totalExported: memories.length,
106
+ byCategory: groupMemoriesBy(memories, 'category'),
107
+ sizeBytes: Buffer.byteLength(content, 'utf8'),
108
+ sizeKB: sizeKB.toFixed(1)
109
+ };
110
+
111
+ logger.info('Export complete', stats);
112
+
113
+ return {
114
+ content,
115
+ filename,
116
+ stats
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Deduplicate similar memories (basic content comparison)
122
+ */
123
+ function deduplicateMemories(memories) {
124
+ const seen = new Map();
125
+
126
+ for (const memory of memories) {
127
+ // Simple dedup: normalize content and check for similar strings
128
+ const normalized = memory.content.toLowerCase().trim();
129
+ const key = normalized.substring(0, 50); // First 50 chars as key
130
+
131
+ if (!seen.has(key)) {
132
+ seen.set(key, memory);
133
+ } else {
134
+ // Keep the one with higher confidence
135
+ const existing = seen.get(key);
136
+ if (memory.confidence > existing.confidence) {
137
+ seen.set(key, memory);
138
+ }
139
+ }
140
+ }
141
+
142
+ return Array.from(seen.values());
143
+ }
144
+
145
+ /**
146
+ * Group memories by a field
147
+ */
148
+ function groupMemoriesBy(memories, field) {
149
+ const groups = {};
150
+ for (const memory of memories) {
151
+ const key = memory[field] || 'other';
152
+ if (!groups[key]) groups[key] = 0;
153
+ groups[key]++;
154
+ }
155
+ return groups;
156
+ }
157
+
158
+ /**
159
+ * Generate Markdown format
160
+ */
161
+ function generateMarkdown(memories, namespace, options = {}) {
162
+ const { header, footer, groupBy } = options;
163
+ const lines = [];
164
+
165
+ // Header
166
+ lines.push(header || '# Project Context');
167
+ lines.push('');
168
+ lines.push(`> Auto-generated from Engram memory on ${new Date().toISOString().split('T')[0]}`);
169
+ lines.push(`> Namespace: ${namespace} | Memories: ${memories.length}`);
170
+ lines.push('');
171
+
172
+ // Group and output memories
173
+ if (groupBy === 'category') {
174
+ const groups = {};
175
+ for (const m of memories) {
176
+ const cat = m.category || 'fact';
177
+ if (!groups[cat]) groups[cat] = [];
178
+ groups[cat].push(m);
179
+ }
180
+
181
+ const categoryOrder = ['fact', 'preference', 'pattern', 'decision', 'outcome'];
182
+ for (const cat of categoryOrder) {
183
+ if (groups[cat] && groups[cat].length > 0) {
184
+ lines.push(`## ${capitalizeFirst(cat)}s`);
185
+ lines.push('');
186
+
187
+ // Sub-group by entity if available
188
+ const byEntity = {};
189
+ for (const m of groups[cat]) {
190
+ const entity = m.entity || 'General';
191
+ if (!byEntity[entity]) byEntity[entity] = [];
192
+ byEntity[entity].push(m);
193
+ }
194
+
195
+ for (const [entity, entityMemories] of Object.entries(byEntity)) {
196
+ if (Object.keys(byEntity).length > 1) {
197
+ lines.push(`### ${capitalizeFirst(entity)}`);
198
+ }
199
+ for (const m of entityMemories) {
200
+ lines.push(`- ${m.content}`);
201
+ }
202
+ lines.push('');
203
+ }
204
+ }
205
+ }
206
+ } else if (groupBy === 'entity') {
207
+ const groups = {};
208
+ for (const m of memories) {
209
+ const entity = m.entity || 'General';
210
+ if (!groups[entity]) groups[entity] = [];
211
+ groups[entity].push(m);
212
+ }
213
+
214
+ for (const [entity, entityMemories] of Object.entries(groups)) {
215
+ lines.push(`## ${capitalizeFirst(entity)}`);
216
+ lines.push('');
217
+ for (const m of entityMemories) {
218
+ lines.push(`- ${m.content}`);
219
+ }
220
+ lines.push('');
221
+ }
222
+ } else {
223
+ // No grouping
224
+ for (const m of memories) {
225
+ lines.push(`- ${m.content}`);
226
+ }
227
+ lines.push('');
228
+ }
229
+
230
+ // Footer
231
+ lines.push('---');
232
+ lines.push(footer || `*Exported from Engram v1.0.0*`);
233
+
234
+ return lines.join('\n');
235
+ }
236
+
237
+ /**
238
+ * Generate Claude Code format (.claude file)
239
+ */
240
+ function generateClaudeFormat(memories, namespace, options = {}) {
241
+ const { header, footer } = options;
242
+ const lines = [];
243
+
244
+ lines.push(header || '# Project Memory');
245
+ lines.push('');
246
+ lines.push('This file contains accumulated knowledge about this project from Engram.');
247
+ lines.push('');
248
+
249
+ // Group by category
250
+ const groups = {};
251
+ for (const m of memories) {
252
+ const cat = m.category || 'fact';
253
+ if (!groups[cat]) groups[cat] = [];
254
+ groups[cat].push(m);
255
+ }
256
+
257
+ // Key Facts first
258
+ if (groups['fact'] && groups['fact'].length > 0) {
259
+ lines.push('## Key Facts');
260
+ lines.push('');
261
+ for (const m of groups['fact'].slice(0, 10)) {
262
+ lines.push(`- ${m.content}`);
263
+ }
264
+ lines.push('');
265
+ }
266
+
267
+ // Development Preferences
268
+ if (groups['preference'] && groups['preference'].length > 0) {
269
+ lines.push('## Development Preferences');
270
+ lines.push('');
271
+ for (const m of groups['preference'].slice(0, 10)) {
272
+ lines.push(`- ${m.content}`);
273
+ }
274
+ lines.push('');
275
+ }
276
+
277
+ // Common Patterns
278
+ if (groups['pattern'] && groups['pattern'].length > 0) {
279
+ lines.push('## Common Patterns');
280
+ lines.push('');
281
+ for (const m of groups['pattern'].slice(0, 10)) {
282
+ lines.push(`- ${m.content}`);
283
+ }
284
+ lines.push('');
285
+ }
286
+
287
+ // Decisions
288
+ if (groups['decision'] && groups['decision'].length > 0) {
289
+ lines.push('## Key Decisions');
290
+ lines.push('');
291
+ for (const m of groups['decision'].slice(0, 10)) {
292
+ lines.push(`- ${m.content}`);
293
+ }
294
+ lines.push('');
295
+ }
296
+
297
+ // Metadata comment
298
+ lines.push(`<!-- engram:exported:${new Date().toISOString()}:${namespace}:${memories.length} -->`);
299
+ if (footer) {
300
+ lines.push('');
301
+ lines.push(footer);
302
+ }
303
+
304
+ return lines.join('\n');
305
+ }
306
+
307
+ /**
308
+ * Generate plain text format
309
+ */
310
+ function generatePlainText(memories, namespace, options = {}) {
311
+ const { header, footer, groupBy } = options;
312
+ const lines = [];
313
+
314
+ if (header) {
315
+ lines.push(header);
316
+ lines.push('');
317
+ }
318
+
319
+ lines.push(`Engram Memory Export - ${namespace}`);
320
+ lines.push(`Generated: ${new Date().toISOString()}`);
321
+ lines.push(`Total Memories: ${memories.length}`);
322
+ lines.push('');
323
+ lines.push('---');
324
+ lines.push('');
325
+
326
+ if (groupBy === 'category') {
327
+ const groups = {};
328
+ for (const m of memories) {
329
+ const cat = m.category || 'fact';
330
+ if (!groups[cat]) groups[cat] = [];
331
+ groups[cat].push(m);
332
+ }
333
+
334
+ for (const [cat, catMemories] of Object.entries(groups)) {
335
+ lines.push(`[${cat.toUpperCase()}S]`);
336
+ for (const m of catMemories) {
337
+ lines.push(`* ${m.content}`);
338
+ }
339
+ lines.push('');
340
+ }
341
+ } else {
342
+ for (const m of memories) {
343
+ lines.push(`* ${m.content}`);
344
+ }
345
+ }
346
+
347
+ if (footer) {
348
+ lines.push('');
349
+ lines.push(footer);
350
+ }
351
+
352
+ return lines.join('\n');
353
+ }
354
+
355
+ /**
356
+ * Generate JSON format
357
+ */
358
+ function generateJSON(memories, namespace, options = {}) {
359
+ const { categories, minConfidence } = options;
360
+
361
+ const groups = {};
362
+ for (const m of memories) {
363
+ const cat = m.category || 'fact';
364
+ if (!groups[cat]) groups[cat] = [];
365
+ groups[cat].push({
366
+ id: m.id,
367
+ content: m.content,
368
+ entity: m.entity,
369
+ confidence: m.confidence,
370
+ access_count: m.access_count
371
+ });
372
+ }
373
+
374
+ return JSON.stringify({
375
+ exported_at: new Date().toISOString(),
376
+ namespace,
377
+ filters: {
378
+ min_confidence: minConfidence,
379
+ categories: categories || 'all'
380
+ },
381
+ memories: groups,
382
+ stats: {
383
+ total_exported: memories.length,
384
+ by_category: Object.fromEntries(
385
+ Object.entries(groups).map(([k, v]) => [k, v.length])
386
+ )
387
+ }
388
+ }, null, 2);
389
+ }
390
+
391
+ /**
392
+ * Capitalize first letter
393
+ */
394
+ function capitalizeFirst(str) {
395
+ return str.charAt(0).toUpperCase() + str.slice(1);
396
+ }
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Category detection patterns
3
+ */
4
+ const CATEGORY_SIGNALS = {
5
+ decision: [
6
+ /\b(decided|chose|picked|went with|switched to|migrated)\b/i,
7
+ /\b(because|reason|rationale|trade-?off)\b/i
8
+ ],
9
+ preference: [
10
+ /\b(prefer|like|love|hate|dislike|always use|never use|favorite|avoid)\b/i,
11
+ /\b(instead of|rather than|over|better than)\b/i
12
+ ],
13
+ pattern: [
14
+ /\b(usually|typically|always|every time|workflow|routine|habit)\b/i,
15
+ /\b(when .+ then|if .+ then|tends to)\b/i
16
+ ],
17
+ outcome: [
18
+ /\b(result|outcome|turned out|ended up|caused|fixed|broke|solved)\b/i,
19
+ /\b(worked|failed|succeeded|improved|degraded)\b/i
20
+ ],
21
+ fact: [] // Default — anything not matching above
22
+ };
23
+
24
+ /**
25
+ * Common technology/tool keywords for entity extraction
26
+ */
27
+ const TECH_KEYWORDS = [
28
+ 'nginx', 'apache', 'docker', 'kubernetes', 'k8s',
29
+ 'postgres', 'postgresql', 'mysql', 'mongodb', 'redis',
30
+ 'react', 'vue', 'angular', 'svelte', 'nextjs', 'next.js',
31
+ 'fastify', 'express', 'flask', 'django', 'rails',
32
+ 'node.js', 'nodejs', 'node', 'python', 'java', 'go', 'rust',
33
+ 'aws', 'azure', 'gcp', 'heroku', 'vercel', 'netlify',
34
+ 'github', 'gitlab', 'bitbucket',
35
+ 'tailwind', 'bootstrap', 'sass', 'css',
36
+ 'typescript', 'javascript', 'js', 'ts',
37
+ 'vite', 'webpack', 'rollup', 'parcel',
38
+ 'jest', 'vitest', 'mocha', 'cypress',
39
+ 'git', 'npm', 'yarn', 'pnpm'
40
+ ];
41
+
42
+ /**
43
+ * Detect the category of a memory based on its content
44
+ * @param {string} content - Memory content
45
+ * @returns {string} Detected category (preference, fact, pattern, decision, outcome)
46
+ */
47
+ export function detectCategory(content) {
48
+ for (const [category, patterns] of Object.entries(CATEGORY_SIGNALS)) {
49
+ if (category === 'fact') continue; // Skip fact (default)
50
+
51
+ for (const pattern of patterns) {
52
+ if (pattern.test(content)) {
53
+ return category;
54
+ }
55
+ }
56
+ }
57
+
58
+ return 'fact'; // Default category
59
+ }
60
+
61
+ /**
62
+ * Extract entity from content (what this memory is about)
63
+ * @param {string} content - Memory content
64
+ * @returns {string|null} Extracted entity or null
65
+ */
66
+ export function extractEntity(content) {
67
+ const contentLower = content.toLowerCase();
68
+
69
+ // Look for known tech keywords
70
+ for (const keyword of TECH_KEYWORDS) {
71
+ const regex = new RegExp(`\\b${keyword.replace(/\./g, '\\.')}\\b`, 'i');
72
+ if (regex.test(contentLower)) {
73
+ return keyword.toLowerCase().replace(/\./g, '');
74
+ }
75
+ }
76
+
77
+ // Try to extract from common patterns
78
+ // Pattern: "uses X", "with X", "via X", "on X"
79
+ const patterns = [
80
+ /\b(?:uses?|using|with|via|on|for)\s+([a-z][a-z0-9-]+(?:\s+[a-z][a-z0-9-]+)?)\b/i,
81
+ /\b([a-z][a-z0-9-]+(?:\s+[a-z][a-z0-9-]+)?)\s+(?:configuration|setup|deployment|server|database|framework|library)\b/i
82
+ ];
83
+
84
+ for (const pattern of patterns) {
85
+ const match = content.match(pattern);
86
+ if (match && match[1]) {
87
+ const entity = match[1].toLowerCase().trim();
88
+ // Filter out common words
89
+ const stopWords = ['the', 'a', 'an', 'this', 'that', 'their', 'user', 'users'];
90
+ if (!stopWords.includes(entity) && entity.length > 2) {
91
+ return entity.replace(/\s+/g, '-').replace(/\./g, '');
92
+ }
93
+ }
94
+ }
95
+
96
+ return null;
97
+ }
98
+
99
+ /**
100
+ * Determine confidence score based on content signals
101
+ * @param {string} content - Memory content
102
+ * @param {Object} [context] - Additional context
103
+ * @param {boolean} [context.userExplicit] - User explicitly stated this
104
+ * @param {boolean} [context.fromCode] - Extracted from code/config
105
+ * @param {boolean} [context.inferred] - Inferred from context
106
+ * @returns {number} Confidence score (0.0-1.0)
107
+ */
108
+ export function calculateConfidence(content, context = {}) {
109
+ if (context.userExplicit) {
110
+ return 1.0;
111
+ }
112
+
113
+ if (context.fromCode) {
114
+ return 0.9;
115
+ }
116
+
117
+ // Check for explicit statement indicators
118
+ const explicitIndicators = [
119
+ /\b(I use|I prefer|I always|I never|my setup|my workflow)\b/i
120
+ ];
121
+
122
+ for (const pattern of explicitIndicators) {
123
+ if (pattern.test(content)) {
124
+ return 0.9;
125
+ }
126
+ }
127
+
128
+ // Check for inference indicators
129
+ const inferenceIndicators = [
130
+ /\b(seems|appears|likely|probably|might|could)\b/i
131
+ ];
132
+
133
+ for (const pattern of inferenceIndicators) {
134
+ if (pattern.test(content)) {
135
+ return 0.6;
136
+ }
137
+ }
138
+
139
+ if (context.inferred) {
140
+ return 0.7;
141
+ }
142
+
143
+ // Default confidence for agent-submitted memories
144
+ return 0.8;
145
+ }
146
+
147
+ /**
148
+ * Extract structured memory from raw text
149
+ * @param {string} content - Raw content
150
+ * @param {Object} [options] - Extraction options
151
+ * @param {string} [options.source] - Source of the content
152
+ * @param {string} [options.namespace] - Namespace for the memory
153
+ * @returns {Object} Structured memory object
154
+ */
155
+ export function extractMemory(content, options = {}) {
156
+ const category = detectCategory(content);
157
+ const entity = extractEntity(content);
158
+ const confidence = calculateConfidence(content, options);
159
+
160
+ return {
161
+ content: content.trim(),
162
+ category,
163
+ entity,
164
+ confidence,
165
+ source: options.source || 'manual',
166
+ namespace: options.namespace || 'default',
167
+ tags: options.tags || []
168
+ };
169
+ }
170
+
171
+ /**
172
+ * Extract multiple memories from longer text
173
+ * @param {string} text - Long text content
174
+ * @param {Object} [options] - Extraction options
175
+ * @returns {Object[]} Array of extracted memories
176
+ */
177
+ export function extractMemories(text, options = {}) {
178
+ // Split by sentences or paragraphs
179
+ const sentences = text
180
+ .split(/[.!?]\s+/)
181
+ .map(s => s.trim())
182
+ .filter(s => s.length > 20); // Minimum length for a memory
183
+
184
+ const memories = [];
185
+
186
+ for (const sentence of sentences) {
187
+ // Skip if it's too generic or vague
188
+ if (isGeneric(sentence)) {
189
+ continue;
190
+ }
191
+
192
+ const memory = extractMemory(sentence, options);
193
+
194
+ // Only include if it has meaningful content
195
+ if (memory.entity || memory.category !== 'fact') {
196
+ memories.push(memory);
197
+ }
198
+ }
199
+
200
+ return memories;
201
+ }
202
+
203
+ /**
204
+ * Check if content is too generic to be a useful memory
205
+ * @param {string} content - Content to check
206
+ * @returns {boolean} True if too generic
207
+ */
208
+ function isGeneric(content) {
209
+ const genericPatterns = [
210
+ /^(ok|okay|yes|no|sure|thanks|thank you)$/i,
211
+ /^(good|great|nice|cool)$/i,
212
+ /^(I see|I understand|got it)$/i
213
+ ];
214
+
215
+ for (const pattern of genericPatterns) {
216
+ if (pattern.test(content.trim())) {
217
+ return true;
218
+ }
219
+ }
220
+
221
+ // Too short
222
+ if (content.length < 20) {
223
+ return true;
224
+ }
225
+
226
+ // No meaningful words
227
+ const words = content.split(/\s+/).filter(w => w.length > 3);
228
+ if (words.length < 3) {
229
+ return true;
230
+ }
231
+
232
+ return false;
233
+ }