@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.
- package/LICENSE +21 -0
- package/README.md +535 -0
- package/bin/engram.js +421 -0
- package/dashboard/dist/assets/index-BHkLa5w_.css +1 -0
- package/dashboard/dist/assets/index-D9QR_Cnu.js +45 -0
- package/dashboard/dist/index.html +14 -0
- package/dashboard/package.json +21 -0
- package/package.json +76 -0
- package/src/config/index.js +150 -0
- package/src/embed/index.js +249 -0
- package/src/export/static.js +396 -0
- package/src/extract/rules.js +233 -0
- package/src/extract/secrets.js +114 -0
- package/src/index.js +54 -0
- package/src/memory/consolidate.js +420 -0
- package/src/memory/context.js +346 -0
- package/src/memory/feedback.js +197 -0
- package/src/memory/recall.js +350 -0
- package/src/memory/store.js +626 -0
- package/src/server/mcp.js +668 -0
- package/src/server/rest.js +499 -0
- package/src/utils/id.js +9 -0
- package/src/utils/logger.js +79 -0
- package/src/utils/time.js +296 -0
|
@@ -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
|
+
}
|