@cartisien/engram-mcp 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/LICENSE +21 -0
  2. package/dist/analysis/emotions.d.ts +16 -0
  3. package/dist/analysis/emotions.js +130 -0
  4. package/dist/analysis/emotions.js.map +1 -0
  5. package/dist/analysis/entities.d.ts +15 -0
  6. package/dist/analysis/entities.js +182 -0
  7. package/dist/analysis/entities.js.map +1 -0
  8. package/dist/analysis/themes.d.ts +11 -0
  9. package/dist/analysis/themes.js +151 -0
  10. package/dist/analysis/themes.js.map +1 -0
  11. package/dist/client.d.ts +3 -0
  12. package/dist/client.js +31 -0
  13. package/dist/client.js.map +1 -0
  14. package/dist/config.d.ts +13 -0
  15. package/dist/config.js +20 -0
  16. package/dist/config.js.map +1 -0
  17. package/dist/response.d.ts +17 -0
  18. package/dist/response.js +17 -0
  19. package/dist/response.js.map +1 -0
  20. package/dist/server.d.ts +7 -0
  21. package/dist/server.js +104 -0
  22. package/dist/server.js.map +1 -0
  23. package/dist/tools/consolidate.d.ts +41 -0
  24. package/dist/tools/consolidate.js +53 -0
  25. package/dist/tools/consolidate.js.map +1 -0
  26. package/dist/tools/context.d.ts +44 -0
  27. package/dist/tools/context.js +186 -0
  28. package/dist/tools/context.js.map +1 -0
  29. package/dist/tools/detect_changes.d.ts +35 -0
  30. package/dist/tools/detect_changes.js +178 -0
  31. package/dist/tools/detect_changes.js.map +1 -0
  32. package/dist/tools/forget.d.ts +38 -0
  33. package/dist/tools/forget.js +38 -0
  34. package/dist/tools/forget.js.map +1 -0
  35. package/dist/tools/graph.d.ts +40 -0
  36. package/dist/tools/graph.js +74 -0
  37. package/dist/tools/graph.js.map +1 -0
  38. package/dist/tools/impact.d.ts +39 -0
  39. package/dist/tools/impact.js +143 -0
  40. package/dist/tools/impact.js.map +1 -0
  41. package/dist/tools/process_detect.d.ts +49 -0
  42. package/dist/tools/process_detect.js +218 -0
  43. package/dist/tools/process_detect.js.map +1 -0
  44. package/dist/tools/recall.d.ts +81 -0
  45. package/dist/tools/recall.js +134 -0
  46. package/dist/tools/recall.js.map +1 -0
  47. package/dist/tools/remember.d.ts +47 -0
  48. package/dist/tools/remember.js +79 -0
  49. package/dist/tools/remember.js.map +1 -0
  50. package/dist/tools/search.d.ts +72 -0
  51. package/dist/tools/search.js +96 -0
  52. package/dist/tools/search.js.map +1 -0
  53. package/dist/tools/stats.d.ts +23 -0
  54. package/dist/tools/stats.js +47 -0
  55. package/dist/tools/stats.js.map +1 -0
  56. package/package.json +29 -14
  57. package/README.md +0 -114
  58. package/dist/engram.d.ts +0 -99
  59. package/dist/engram.js +0 -283
  60. package/dist/src/index.d.ts +0 -2
  61. package/dist/src/index.js +0 -322
  62. package/engram.ts +0 -385
  63. package/src/index.ts +0 -336
  64. package/tests/mcp.test.ts +0 -274
  65. package/tsconfig.json +0 -17
  66. package/tsconfig.test.json +0 -5
package/engram.ts DELETED
@@ -1,385 +0,0 @@
1
- import { createHash } from 'crypto';
2
- import { open, Database as SQLiteDatabase } from 'sqlite';
3
-
4
- export interface MemoryEntry {
5
- id: string;
6
- sessionId: string;
7
- content: string;
8
- role: 'user' | 'assistant' | 'system';
9
- timestamp: Date;
10
- metadata?: Record<string, unknown>;
11
- similarity?: number;
12
- }
13
-
14
- export interface RecallOptions {
15
- limit?: number;
16
- before?: Date;
17
- after?: Date;
18
- role?: 'user' | 'assistant' | 'system';
19
- }
20
-
21
- export interface EngramConfig {
22
- dbPath?: string;
23
- maxContextLength?: number;
24
- embeddingUrl?: string;
25
- embeddingModel?: string;
26
- semanticSearch?: boolean;
27
- }
28
-
29
- /**
30
- * Engram - Persistent memory for AI assistants
31
- *
32
- * A lightweight, SQLite-backed memory system that gives your AI assistants
33
- * the ability to remember conversations across sessions.
34
- * v0.2 adds semantic search via Ollama embeddings (nomic-embed-text).
35
- *
36
- * @example
37
- * ```typescript
38
- * import { Engram } from '@cartisien/engram';
39
- *
40
- * const memory = new Engram({ dbPath: './memory.db' });
41
- *
42
- * // Store a memory
43
- * await memory.remember('user_123', 'User prefers dark mode and TypeScript', 'user');
44
- *
45
- * // Retrieve relevant memories semantically
46
- * const context = await memory.recall('user_123', 'What are this user\'s preferences?', 5);
47
- * ```
48
- */
49
- export class Engram {
50
- private db!: SQLiteDatabase;
51
- private maxContextLength: number;
52
- private dbPath: string;
53
- private initialized: boolean = false;
54
- private embeddingUrl: string;
55
- private embeddingModel: string;
56
- private semanticSearch: boolean;
57
-
58
- constructor(config: EngramConfig = {}) {
59
- this.dbPath = config.dbPath || ':memory:';
60
- this.maxContextLength = config.maxContextLength || 4000;
61
- this.embeddingUrl = config.embeddingUrl || 'http://192.168.68.73:11434';
62
- this.embeddingModel = config.embeddingModel || 'nomic-embed-text';
63
- this.semanticSearch = config.semanticSearch !== false;
64
- }
65
-
66
- private async init(): Promise<void> {
67
- if (this.initialized) return;
68
-
69
- const sqlite3 = require('sqlite3').verbose();
70
- const { open } = require('sqlite');
71
-
72
- this.db = await open({
73
- filename: this.dbPath,
74
- driver: sqlite3.Database
75
- });
76
-
77
- await this.db.exec(`
78
- CREATE TABLE IF NOT EXISTS memories (
79
- id TEXT PRIMARY KEY,
80
- session_id TEXT NOT NULL,
81
- content TEXT NOT NULL,
82
- role TEXT CHECK(role IN ('user', 'assistant', 'system')),
83
- timestamp INTEGER NOT NULL,
84
- metadata TEXT,
85
- content_hash TEXT NOT NULL,
86
- embedding TEXT
87
- );
88
- `);
89
-
90
- // Add embedding column if upgrading from v0.1
91
- try {
92
- await this.db.exec(`ALTER TABLE memories ADD COLUMN embedding TEXT`);
93
- } catch {
94
- // Column already exists, ignore
95
- }
96
-
97
- await this.db.exec(`
98
- CREATE INDEX IF NOT EXISTS idx_session_timestamp
99
- ON memories(session_id, timestamp DESC);
100
- `);
101
-
102
- await this.db.exec(`
103
- CREATE INDEX IF NOT EXISTS idx_content
104
- ON memories(content);
105
- `);
106
-
107
- this.initialized = true;
108
- }
109
-
110
- /**
111
- * Fetch embedding vector from Ollama
112
- */
113
- private async embed(text: string): Promise<number[] | null> {
114
- try {
115
- const response = await fetch(`${this.embeddingUrl}/api/embeddings`, {
116
- method: 'POST',
117
- headers: { 'Content-Type': 'application/json' },
118
- body: JSON.stringify({ model: this.embeddingModel, prompt: text }),
119
- signal: AbortSignal.timeout(5000)
120
- });
121
- if (!response.ok) return null;
122
- const data = await response.json() as { embedding: number[] };
123
- return data.embedding ?? null;
124
- } catch {
125
- return null;
126
- }
127
- }
128
-
129
- /**
130
- * Cosine similarity between two vectors
131
- */
132
- private cosineSimilarity(a: number[], b: number[]): number {
133
- let dot = 0, magA = 0, magB = 0;
134
- for (let i = 0; i < a.length; i++) {
135
- dot += a[i] * b[i];
136
- magA += a[i] * a[i];
137
- magB += b[i] * b[i];
138
- }
139
- const denom = Math.sqrt(magA) * Math.sqrt(magB);
140
- return denom === 0 ? 0 : dot / denom;
141
- }
142
-
143
- /**
144
- * Store a memory entry
145
- */
146
- async remember(
147
- sessionId: string,
148
- content: string,
149
- role: 'user' | 'assistant' | 'system' = 'user',
150
- metadata?: Record<string, unknown>
151
- ): Promise<MemoryEntry> {
152
- await this.init();
153
-
154
- const id = createHash('sha256')
155
- .update(`${sessionId}:${content}:${Date.now()}`)
156
- .digest('hex')
157
- .slice(0, 16);
158
-
159
- const contentHash = createHash('sha256')
160
- .update(content)
161
- .digest('hex')
162
- .slice(0, 16);
163
-
164
- const truncated = content.slice(0, this.maxContextLength);
165
-
166
- // Fetch embedding if semantic search enabled
167
- let embeddingJson: string | null = null;
168
- if (this.semanticSearch) {
169
- const vector = await this.embed(truncated);
170
- if (vector) embeddingJson = JSON.stringify(vector);
171
- }
172
-
173
- const entry: MemoryEntry = {
174
- id,
175
- sessionId,
176
- content: truncated,
177
- role,
178
- timestamp: new Date(),
179
- metadata
180
- };
181
-
182
- await this.db.run(
183
- `INSERT INTO memories (id, session_id, content, role, timestamp, metadata, content_hash, embedding)
184
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
185
- [
186
- entry.id,
187
- entry.sessionId,
188
- entry.content,
189
- entry.role,
190
- entry.timestamp.getTime(),
191
- metadata ? JSON.stringify(metadata) : null,
192
- contentHash,
193
- embeddingJson
194
- ]
195
- );
196
-
197
- return entry;
198
- }
199
-
200
- /**
201
- * Recall memories for a session.
202
- *
203
- * With semantic search enabled (default), embeds the query and ranks
204
- * results by cosine similarity. Falls back to keyword search if Ollama
205
- * is unreachable or no embeddings are stored.
206
- */
207
- async recall(
208
- sessionId: string,
209
- query?: string,
210
- limit: number = 10,
211
- options: RecallOptions = {}
212
- ): Promise<MemoryEntry[]> {
213
- await this.init();
214
-
215
- // Build base query
216
- let sql = `
217
- SELECT id, session_id, content, role, timestamp, metadata, embedding
218
- FROM memories
219
- WHERE session_id = ?
220
- `;
221
- const params: (string | number)[] = [sessionId];
222
-
223
- if (options.role) {
224
- sql += ` AND role = ?`;
225
- params.push(options.role);
226
- }
227
- if (options.after) {
228
- sql += ` AND timestamp >= ?`;
229
- params.push(options.after.getTime());
230
- }
231
- if (options.before) {
232
- sql += ` AND timestamp <= ?`;
233
- params.push(options.before.getTime());
234
- }
235
-
236
- // Try semantic search
237
- if (query && query.trim() && this.semanticSearch) {
238
- const queryVector = await this.embed(query);
239
- if (queryVector) {
240
- sql += ` ORDER BY timestamp DESC`;
241
- const rows = await this.db.all(sql, params);
242
-
243
- // Score each row by cosine similarity
244
- const scored = rows
245
- .map((row: any) => {
246
- let similarity = 0;
247
- if (row.embedding) {
248
- try {
249
- const vec: number[] = JSON.parse(row.embedding);
250
- similarity = this.cosineSimilarity(queryVector, vec);
251
- } catch { /* malformed, skip */ }
252
- }
253
- return { row, similarity };
254
- })
255
- .sort((a, b) => b.similarity - a.similarity)
256
- .slice(0, limit);
257
-
258
- return scored.map(({ row, similarity }) => ({
259
- id: row.id,
260
- sessionId: row.session_id,
261
- content: row.content,
262
- role: row.role,
263
- timestamp: new Date(row.timestamp),
264
- metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
265
- similarity
266
- }));
267
- }
268
- // Ollama unreachable — fall through to keyword search
269
- }
270
-
271
- // Keyword fallback
272
- if (query && query.trim()) {
273
- const keywords = query.toLowerCase().split(/\s+/).filter(k => k.length > 2);
274
- if (keywords.length > 0) {
275
- sql += ` AND (` + keywords.map(() => `LOWER(content) LIKE ?`).join(' OR ') + `)`;
276
- params.push(...keywords.map(k => `%${k}%`));
277
- }
278
- }
279
-
280
- sql += ` ORDER BY timestamp DESC LIMIT ?`;
281
- params.push(limit);
282
-
283
- const rows = await this.db.all(sql, params);
284
- return rows.map((row: any) => ({
285
- id: row.id,
286
- sessionId: row.session_id,
287
- content: row.content,
288
- role: row.role,
289
- timestamp: new Date(row.timestamp),
290
- metadata: row.metadata ? JSON.parse(row.metadata) : undefined
291
- }));
292
- }
293
-
294
- /**
295
- * Get recent conversation history for a session
296
- */
297
- async history(sessionId: string, limit: number = 20): Promise<MemoryEntry[]> {
298
- return this.recall(sessionId, undefined, limit, {});
299
- }
300
-
301
- /**
302
- * Forget (delete) memories
303
- */
304
- async forget(
305
- sessionId: string,
306
- options?: { before?: Date; id?: string }
307
- ): Promise<number> {
308
- await this.init();
309
-
310
- if (options?.id) {
311
- const result = await this.db.run(
312
- 'DELETE FROM memories WHERE session_id = ? AND id = ?',
313
- [sessionId, options.id]
314
- );
315
- return result.changes || 0;
316
- }
317
-
318
- let sql = 'DELETE FROM memories WHERE session_id = ?';
319
- const params: (string | number)[] = [sessionId];
320
-
321
- if (options?.before) {
322
- sql += ' AND timestamp < ?';
323
- params.push(options.before.getTime());
324
- }
325
-
326
- const result = await this.db.run(sql, params);
327
- return result.changes || 0;
328
- }
329
-
330
- /**
331
- * Get memory statistics for a session
332
- */
333
- async stats(sessionId: string): Promise<{
334
- total: number;
335
- byRole: Record<string, number>;
336
- oldest: Date | null;
337
- newest: Date | null;
338
- withEmbeddings: number;
339
- }> {
340
- await this.init();
341
-
342
- const totalRow = await this.db.get(
343
- 'SELECT COUNT(*) as count FROM memories WHERE session_id = ?',
344
- [sessionId]
345
- );
346
- const total = totalRow?.count || 0;
347
-
348
- const roleRows = await this.db.all(
349
- 'SELECT role, COUNT(*) as count FROM memories WHERE session_id = ? GROUP BY role',
350
- [sessionId]
351
- );
352
- const byRole: Record<string, number> = {};
353
- roleRows.forEach((row: any) => { byRole[row.role] = row.count; });
354
-
355
- const range = await this.db.get(
356
- 'SELECT MIN(timestamp) as oldest, MAX(timestamp) as newest FROM memories WHERE session_id = ?',
357
- [sessionId]
358
- );
359
-
360
- const embRow = await this.db.get(
361
- 'SELECT COUNT(*) as count FROM memories WHERE session_id = ? AND embedding IS NOT NULL',
362
- [sessionId]
363
- );
364
-
365
- return {
366
- total,
367
- byRole,
368
- oldest: range?.oldest ? new Date(range.oldest) : null,
369
- newest: range?.newest ? new Date(range.newest) : null,
370
- withEmbeddings: embRow?.count || 0
371
- };
372
- }
373
-
374
- /**
375
- * Close the database connection
376
- */
377
- async close(): Promise<void> {
378
- if (this.db) {
379
- await this.db.close();
380
- this.initialized = false;
381
- }
382
- }
383
- }
384
-
385
- export default Engram;