@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,114 @@
1
+ /**
2
+ * Patterns for detecting various types of secrets
3
+ */
4
+ const SECRET_PATTERNS = [
5
+ // API Keys
6
+ { pattern: /sk-[a-zA-Z0-9]{32,}/, name: 'OpenAI API Key' },
7
+ { pattern: /pk_[a-zA-Z0-9]{32,}/, name: 'Stripe Publishable Key' },
8
+ { pattern: /sk_live_[a-zA-Z0-9]{24,}/, name: 'Stripe Secret Key' },
9
+ { pattern: /AKIA[0-9A-Z]{16}/, name: 'AWS Access Key ID' },
10
+ { pattern: /ghp_[a-zA-Z0-9]{20,}/, name: 'GitHub Personal Access Token' },
11
+ { pattern: /gho_[a-zA-Z0-9]{36,}/, name: 'GitHub OAuth Token' },
12
+ { pattern: /github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59}/, name: 'GitHub Fine-grained PAT' },
13
+ { pattern: /xoxb-[0-9]{10,13}-[0-9]{10,13}-[a-zA-Z0-9]{24,}/, name: 'Slack Bot Token' },
14
+ { pattern: /xoxp-[0-9]{10,13}-[0-9]{10,13}-[a-zA-Z0-9]{24,}/, name: 'Slack User Token' },
15
+ { pattern: /AIza[0-9A-Za-z\\-_]{35}/, name: 'Google API Key' },
16
+ { pattern: /ya29\.[0-9A-Za-z\\-_]+/, name: 'Google OAuth Token' },
17
+
18
+ // Private Keys
19
+ { pattern: /-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----/, name: 'Private Key' },
20
+ { pattern: /-----BEGIN PGP PRIVATE KEY BLOCK-----/, name: 'PGP Private Key' },
21
+
22
+ // Connection Strings with Credentials
23
+ { pattern: /[a-zA-Z]+:\/\/[^:]+:[^@]+@[^\/]+/, name: 'Connection String with Credentials' },
24
+ { pattern: /mongodb(\+srv)?:\/\/[^:]+:[^@]+@/, name: 'MongoDB Connection String' },
25
+ { pattern: /postgres(ql)?:\/\/[^:]+:[^@]+@/, name: 'PostgreSQL Connection String' },
26
+ { pattern: /mysql:\/\/[^:]+:[^@]+@/, name: 'MySQL Connection String' },
27
+
28
+ // JWT Tokens
29
+ { pattern: /eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*/, name: 'JWT Token' },
30
+
31
+ // Generic High-Entropy Strings (common in secrets)
32
+ { pattern: /['"]([\da-f]{32,}|[A-Za-z0-9+/]{40,}={0,2})['"]/, name: 'High-Entropy String' },
33
+
34
+ // Common Secret Environment Variables
35
+ { pattern: /(password|passwd|pwd|secret|token|api_key|apikey|access_key|secret_key|private_key)\s*[:=]\s*['"]?[^\s'"]{8,}['"]?/i, name: 'Secret Environment Variable' }
36
+ ];
37
+
38
+ /**
39
+ * Check if content contains secrets
40
+ * @param {string} content - Content to check
41
+ * @returns {Object} Result object with detected secrets
42
+ */
43
+ export function detectSecrets(content) {
44
+ const detected = [];
45
+
46
+ for (const { pattern, name } of SECRET_PATTERNS) {
47
+ const matches = content.match(pattern);
48
+ if (matches) {
49
+ detected.push({
50
+ type: name,
51
+ match: matches[0],
52
+ position: matches.index
53
+ });
54
+ }
55
+ }
56
+
57
+ return {
58
+ hasSecrets: detected.length > 0,
59
+ secrets: detected
60
+ };
61
+ }
62
+
63
+ /**
64
+ * Redact secrets from content
65
+ * @param {string} content - Content to redact
66
+ * @returns {string} Content with secrets redacted
67
+ */
68
+ export function redactSecrets(content) {
69
+ let redacted = content;
70
+
71
+ for (const { pattern } of SECRET_PATTERNS) {
72
+ // Use global flag to replace all occurrences
73
+ const flags = pattern.flags || '';
74
+ const globalPattern = new RegExp(pattern.source, flags.includes('g') ? flags : flags + 'g');
75
+ redacted = redacted.replace(globalPattern, '[REDACTED]');
76
+ }
77
+
78
+ return redacted;
79
+ }
80
+
81
+ /**
82
+ * Validate that content is safe to store
83
+ * @param {string} content - Content to validate
84
+ * @param {Object} [options] - Validation options
85
+ * @param {boolean} [options.autoRedact=false] - Auto-redact instead of rejecting
86
+ * @returns {Object} Validation result
87
+ */
88
+ export function validateContent(content, options = {}) {
89
+ const { autoRedact = false } = options;
90
+ const detection = detectSecrets(content);
91
+
92
+ if (!detection.hasSecrets) {
93
+ return {
94
+ valid: true,
95
+ content,
96
+ warnings: []
97
+ };
98
+ }
99
+
100
+ if (autoRedact) {
101
+ return {
102
+ valid: true,
103
+ content: redactSecrets(content),
104
+ warnings: detection.secrets.map(s => `Redacted ${s.type}`)
105
+ };
106
+ }
107
+
108
+ return {
109
+ valid: false,
110
+ content,
111
+ errors: detection.secrets.map(s => `Detected ${s.type}: ${s.match.substring(0, 20)}...`),
112
+ warnings: ['Content contains sensitive information and was rejected']
113
+ };
114
+ }
package/src/index.js ADDED
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Engram - Persistent Memory for AI Agents
3
+ * Main entry point for programmatic usage
4
+ *
5
+ * @example
6
+ * import { createMemory, recallMemories, initDatabase } from 'engram';
7
+ */
8
+
9
+ // Configuration
10
+ export { loadConfig, getDatabasePath, getModelsPath } from './config/index.js';
11
+
12
+ // Memory operations
13
+ export {
14
+ initDatabase,
15
+ createMemory,
16
+ getMemory,
17
+ deleteMemory,
18
+ listMemories,
19
+ getStats
20
+ } from './memory/store.js';
21
+
22
+ // Recall & search
23
+ export {
24
+ recallMemories,
25
+ formatRecallResults
26
+ } from './memory/recall.js';
27
+
28
+ // Consolidation
29
+ export {
30
+ consolidate,
31
+ getConflicts
32
+ } from './memory/consolidate.js';
33
+
34
+ // Extraction
35
+ export {
36
+ extractMemory
37
+ } from './extract/rules.js';
38
+
39
+ export {
40
+ validateContent
41
+ } from './extract/secrets.js';
42
+
43
+ // Embedding
44
+ export {
45
+ generateEmbedding,
46
+ cosineSimilarity
47
+ } from './embed/index.js';
48
+
49
+ // Utilities
50
+ export {
51
+ generateId
52
+ } from './utils/id.js';
53
+
54
+ export * as logger from './utils/logger.js';
@@ -0,0 +1,420 @@
1
+ import { getMemoriesWithEmbeddings, listMemories, updateMemory, deleteMemory } from './store.js';
2
+ import { cosineSimilarity } from '../embed/index.js';
3
+ import * as logger from '../utils/logger.js';
4
+
5
+ /**
6
+ * Run full consolidation process
7
+ * @param {Database} db - SQLite database instance
8
+ * @param {Object} [options] - Consolidation options
9
+ * @param {boolean} [options.detectDuplicates=true] - Enable duplicate detection
10
+ * @param {boolean} [options.detectContradictions=true] - Enable contradiction detection
11
+ * @param {boolean} [options.applyDecay=true] - Enable confidence decay
12
+ * @param {boolean} [options.cleanupStale=false] - Enable stale cleanup
13
+ * @param {number} [options.duplicateThreshold=0.92] - Similarity threshold for duplicates
14
+ * @returns {Promise<Object>} Consolidation results
15
+ */
16
+ export async function consolidate(db, options = {}) {
17
+ const {
18
+ detectDuplicates = true,
19
+ detectContradictions = true,
20
+ applyDecay = true,
21
+ cleanupStale = false,
22
+ duplicateThreshold = 0.92
23
+ } = options;
24
+
25
+ logger.info('Starting memory consolidation', options);
26
+
27
+ const results = {
28
+ duplicatesRemoved: 0,
29
+ contradictionsDetected: 0,
30
+ memoriesDecayed: 0,
31
+ staleMemoriesCleaned: 0,
32
+ startTime: Date.now()
33
+ };
34
+
35
+ try {
36
+ // Step 1: Detect and merge duplicates
37
+ if (detectDuplicates) {
38
+ logger.debug('Detecting duplicates...');
39
+ const duplicates = await findDuplicates(db, duplicateThreshold);
40
+ results.duplicatesRemoved = await mergeDuplicates(db, duplicates);
41
+ logger.info('Duplicates removed', { count: results.duplicatesRemoved });
42
+ }
43
+
44
+ // Step 2: Detect contradictions
45
+ if (detectContradictions) {
46
+ logger.debug('Detecting contradictions...');
47
+ const contradictions = await findContradictions(db);
48
+ results.contradictionsDetected = contradictions.length;
49
+ // Flag contradictions (don't auto-resolve)
50
+ await flagContradictions(db, contradictions);
51
+ logger.info('Contradictions detected', { count: results.contradictionsDetected });
52
+ }
53
+
54
+ // Step 3: Apply confidence decay
55
+ if (applyDecay) {
56
+ logger.debug('Applying confidence decay...');
57
+ results.memoriesDecayed = await applyConfidenceDecay(db);
58
+ logger.info('Memories decayed', { count: results.memoriesDecayed });
59
+ }
60
+
61
+ // Step 4: Cleanup stale memories
62
+ if (cleanupStale) {
63
+ logger.debug('Cleaning up stale memories...');
64
+ results.staleMemoriesCleaned = await cleanupStaleMemories(db);
65
+ logger.info('Stale memories cleaned', { count: results.staleMemoriesCleaned });
66
+ }
67
+
68
+ results.duration = Date.now() - results.startTime;
69
+ logger.info('Consolidation complete', results);
70
+
71
+ return results;
72
+ } catch (error) {
73
+ logger.error('Consolidation failed', { error: error.message });
74
+ throw error;
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Find duplicate memories based on embedding similarity
80
+ * @param {Database} db - SQLite database instance
81
+ * @param {number} threshold - Similarity threshold (default 0.92)
82
+ * @returns {Promise<Array>} Array of duplicate pairs
83
+ */
84
+ async function findDuplicates(db, threshold = 0.92) {
85
+ const memories = getMemoriesWithEmbeddings(db);
86
+ const duplicates = [];
87
+
88
+ logger.debug('Checking for duplicates', { memories: memories.length, threshold });
89
+
90
+ // Compare each memory with every other memory
91
+ for (let i = 0; i < memories.length; i++) {
92
+ for (let j = i + 1; j < memories.length; j++) {
93
+ const memA = memories[i];
94
+ const memB = memories[j];
95
+
96
+ // Skip if in different namespaces
97
+ if (memA.namespace !== memB.namespace) {
98
+ continue;
99
+ }
100
+
101
+ // Calculate similarity
102
+ const similarity = cosineSimilarity(memA.embedding, memB.embedding);
103
+
104
+ if (similarity > threshold) {
105
+ duplicates.push({
106
+ memory1: memA,
107
+ memory2: memB,
108
+ similarity
109
+ });
110
+ }
111
+ }
112
+ }
113
+
114
+ logger.debug('Duplicates found', { count: duplicates.length });
115
+
116
+ return duplicates;
117
+ }
118
+
119
+ /**
120
+ * Merge duplicate memories
121
+ * @param {Database} db - SQLite database instance
122
+ * @param {Array} duplicates - Array of duplicate pairs
123
+ * @returns {Promise<number>} Number of duplicates removed
124
+ */
125
+ async function mergeDuplicates(db, duplicates) {
126
+ let removed = 0;
127
+
128
+ for (const { memory1, memory2, similarity } of duplicates) {
129
+ try {
130
+ // Determine which memory to keep (higher confidence, more accesses, or more recent)
131
+ let keepMemory, removeMemory;
132
+
133
+ if (memory1.confidence > memory2.confidence) {
134
+ keepMemory = memory1;
135
+ removeMemory = memory2;
136
+ } else if (memory1.confidence < memory2.confidence) {
137
+ keepMemory = memory2;
138
+ removeMemory = memory1;
139
+ } else if (memory1.access_count > memory2.access_count) {
140
+ keepMemory = memory1;
141
+ removeMemory = memory2;
142
+ } else if (memory1.access_count < memory2.access_count) {
143
+ keepMemory = memory2;
144
+ removeMemory = memory1;
145
+ } else if (memory1.updated_at > memory2.updated_at) {
146
+ keepMemory = memory1;
147
+ removeMemory = memory2;
148
+ } else {
149
+ keepMemory = memory2;
150
+ removeMemory = memory1;
151
+ }
152
+
153
+ // Merge access counts
154
+ const mergedAccessCount = keepMemory.access_count + removeMemory.access_count;
155
+
156
+ // Update the kept memory with merged stats
157
+ updateMemory(db, keepMemory.id, {
158
+ access_count: mergedAccessCount
159
+ });
160
+
161
+ // Delete the duplicate
162
+ deleteMemory(db, removeMemory.id);
163
+
164
+ logger.debug('Duplicate merged', {
165
+ kept: keepMemory.id.substring(0, 8),
166
+ removed: removeMemory.id.substring(0, 8),
167
+ similarity: similarity.toFixed(3)
168
+ });
169
+
170
+ removed++;
171
+ } catch (error) {
172
+ logger.warn('Failed to merge duplicate', { error: error.message });
173
+ }
174
+ }
175
+
176
+ return removed;
177
+ }
178
+
179
+ /**
180
+ * Find contradictory memories
181
+ * @param {Database} db - SQLite database instance
182
+ * @returns {Promise<Array>} Array of contradiction pairs
183
+ */
184
+ async function findContradictions(db) {
185
+ const memories = getMemoriesWithEmbeddings(db);
186
+ const contradictions = [];
187
+
188
+ // Group memories by entity
189
+ const byEntity = new Map();
190
+ for (const memory of memories) {
191
+ if (!memory.entity) continue;
192
+
193
+ if (!byEntity.has(memory.entity)) {
194
+ byEntity.set(memory.entity, []);
195
+ }
196
+ byEntity.get(memory.entity).push(memory);
197
+ }
198
+
199
+ // Look for contradictions within each entity group
200
+ for (const [entity, entityMemories] of byEntity.entries()) {
201
+ if (entityMemories.length < 2) continue;
202
+
203
+ for (let i = 0; i < entityMemories.length; i++) {
204
+ for (let j = i + 1; j < entityMemories.length; j++) {
205
+ const memA = entityMemories[i];
206
+ const memB = entityMemories[j];
207
+
208
+ // Skip if in different namespaces
209
+ if (memA.namespace !== memB.namespace) {
210
+ continue;
211
+ }
212
+
213
+ // Check if contents suggest contradiction
214
+ if (seemsContradictory(memA.content, memB.content)) {
215
+ contradictions.push({
216
+ memory1: memA,
217
+ memory2: memB,
218
+ entity
219
+ });
220
+ }
221
+ }
222
+ }
223
+ }
224
+
225
+ return contradictions;
226
+ }
227
+
228
+ /**
229
+ * Check if two memory contents seem contradictory
230
+ * @param {string} contentA - First memory content
231
+ * @param {string} contentB - Second memory content
232
+ * @returns {boolean} True if potentially contradictory
233
+ */
234
+ function seemsContradictory(contentA, contentB) {
235
+ const lowerA = contentA.toLowerCase();
236
+ const lowerB = contentB.toLowerCase();
237
+
238
+ // Check for explicit contradictions
239
+ const contradictionPatterns = [
240
+ // Preferences
241
+ { a: /prefer.*?(\w+)/, b: /prefer.*?(\w+)/ },
242
+ { a: /uses?\s+(\w+)/, b: /uses?\s+(\w+)/ },
243
+ { a: /instead of\s+(\w+)/, b: /instead of\s+(\w+)/ },
244
+
245
+ // Facts
246
+ { a: /is\s+(\w+)/, b: /is\s+(\w+)/ },
247
+ { a: /version\s+(\d+)/, b: /version\s+(\d+)/ },
248
+
249
+ // Negations
250
+ { a: /never|not|doesn't|don't/, b: /always|does|do/ }
251
+ ];
252
+
253
+ // Simple heuristic: if one contains "not"/"never" and the other doesn't
254
+ const hasNegationA = /\b(not|never|doesn't|don't|avoid|dislike)\b/i.test(lowerA);
255
+ const hasNegationB = /\b(not|never|doesn't|don't|avoid|dislike)\b/i.test(lowerB);
256
+
257
+ if (hasNegationA !== hasNegationB) {
258
+ // Remove negation words and check similarity
259
+ const normalizedA = lowerA.replace(/\b(not|never|doesn't|don't|avoid|dislike)\b/gi, '');
260
+ const normalizedB = lowerB.replace(/\b(not|never|doesn't|don't|avoid|dislike)\b/gi, '');
261
+
262
+ // If the rest is similar, it's likely a contradiction
263
+ const similarity = simpleSimilarity(normalizedA, normalizedB);
264
+ if (similarity > 0.6) {
265
+ return true;
266
+ }
267
+ }
268
+
269
+ return false;
270
+ }
271
+
272
+ /**
273
+ * Simple text similarity calculation
274
+ * @param {string} a - First text
275
+ * @param {string} b - Second text
276
+ * @returns {number} Similarity score (0-1)
277
+ */
278
+ function simpleSimilarity(a, b) {
279
+ const wordsA = new Set(a.toLowerCase().match(/\b\w+\b/g) || []);
280
+ const wordsB = new Set(b.toLowerCase().match(/\b\w+\b/g) || []);
281
+
282
+ const intersection = new Set([...wordsA].filter(x => wordsB.has(x)));
283
+ const union = new Set([...wordsA, ...wordsB]);
284
+
285
+ return union.size > 0 ? intersection.size / union.size : 0;
286
+ }
287
+
288
+ /**
289
+ * Flag contradictions for user review
290
+ * @param {Database} db - SQLite database instance
291
+ * @param {Array} contradictions - Array of contradiction pairs
292
+ * @returns {Promise<number>} Number of contradictions flagged
293
+ */
294
+ async function flagContradictions(db, contradictions) {
295
+ let flagged = 0;
296
+
297
+ for (const { memory1, memory2 } of contradictions) {
298
+ try {
299
+ // Generate a conflict ID for this pair
300
+ const conflictId = `conflict_${Date.now()}_${Math.random().toString(36).substring(7)}`;
301
+
302
+ // Add conflict tag to both memories
303
+ const tags1 = memory1.tags || [];
304
+ const tags2 = memory2.tags || [];
305
+
306
+ if (!tags1.includes(conflictId)) {
307
+ updateMemory(db, memory1.id, {
308
+ tags: [...tags1, conflictId, 'has-conflict']
309
+ });
310
+ }
311
+
312
+ if (!tags2.includes(conflictId)) {
313
+ updateMemory(db, memory2.id, {
314
+ tags: [...tags2, conflictId, 'has-conflict']
315
+ });
316
+ }
317
+
318
+ flagged++;
319
+ } catch (error) {
320
+ logger.warn('Failed to flag contradiction', { error: error.message });
321
+ }
322
+ }
323
+
324
+ return flagged;
325
+ }
326
+
327
+ /**
328
+ * Apply confidence decay to memories based on age and access
329
+ * @param {Database} db - SQLite database instance
330
+ * @returns {Promise<number>} Number of memories decayed
331
+ */
332
+ async function applyConfidenceDecay(db) {
333
+ const memories = listMemories(db, { limit: 10000 }); // Get all memories
334
+ const now = Date.now();
335
+ let decayed = 0;
336
+
337
+ for (const memory of memories) {
338
+ if (memory.decay_rate === 0) {
339
+ continue; // Skip memories that never decay
340
+ }
341
+
342
+ const lastAccessed = memory.last_accessed || memory.created_at;
343
+ const daysSinceAccess = (now - lastAccessed) / (1000 * 60 * 60 * 24);
344
+
345
+ // Calculate decay
346
+ const decay = memory.decay_rate * daysSinceAccess;
347
+ const newConfidence = Math.max(0.1, memory.confidence * (1 - decay));
348
+
349
+ // Only update if confidence changed significantly
350
+ if (Math.abs(newConfidence - memory.confidence) > 0.01) {
351
+ updateMemory(db, memory.id, { confidence: newConfidence });
352
+ decayed++;
353
+ }
354
+ }
355
+
356
+ return decayed;
357
+ }
358
+
359
+ /**
360
+ * Clean up stale memories
361
+ * @param {Database} db - SQLite database instance
362
+ * @returns {Promise<number>} Number of memories cleaned
363
+ */
364
+ async function cleanupStaleMemories(db) {
365
+ const memories = listMemories(db, { limit: 10000 });
366
+ const now = Date.now();
367
+ let cleaned = 0;
368
+
369
+ for (const memory of memories) {
370
+ // Criteria for stale: confidence < 0.15, no accesses, and > 90 days old
371
+ const age = (now - memory.created_at) / (1000 * 60 * 60 * 24);
372
+
373
+ if (memory.confidence < 0.15 && memory.access_count === 0 && age > 90) {
374
+ // Mark as stale (add tag) instead of deleting
375
+ const tags = memory.tags || [];
376
+ if (!tags.includes('stale')) {
377
+ updateMemory(db, memory.id, {
378
+ tags: [...tags, 'stale']
379
+ });
380
+ cleaned++;
381
+ }
382
+ }
383
+ }
384
+
385
+ return cleaned;
386
+ }
387
+
388
+ /**
389
+ * Get memories flagged with contradictions
390
+ * @param {Database} db - SQLite database instance
391
+ * @returns {Object[]} Array of memories with conflict tags
392
+ */
393
+ export function getConflicts(db) {
394
+ const memories = listMemories(db, { limit: 10000 });
395
+
396
+ // Find all memories with conflict tags
397
+ const conflicts = memories.filter(m =>
398
+ m.tags && m.tags.some(tag => tag.startsWith('conflict_') || tag === 'has-conflict')
399
+ );
400
+
401
+ // Group by conflict ID
402
+ const grouped = new Map();
403
+
404
+ for (const memory of conflicts) {
405
+ const conflictTags = memory.tags.filter(tag => tag.startsWith('conflict_'));
406
+
407
+ for (const conflictId of conflictTags) {
408
+ if (!grouped.has(conflictId)) {
409
+ grouped.set(conflictId, []);
410
+ }
411
+ grouped.get(conflictId).push(memory);
412
+ }
413
+ }
414
+
415
+ // Convert to array of conflict pairs
416
+ return Array.from(grouped.entries()).map(([conflictId, memories]) => ({
417
+ conflictId,
418
+ memories
419
+ }));
420
+ }