memshell 0.2.1 → 0.4.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/src/index.js CHANGED
@@ -64,75 +64,305 @@ class TfIdf {
64
64
  }
65
65
  }
66
66
 
67
- // ── JSON File Store ────────────────────────────────────────────
67
+ // ── OpenAI Embeddings ──────────────────────────────────────────
68
+ class OpenAIEmbedder {
69
+ constructor(apiKey) {
70
+ this.apiKey = apiKey;
71
+ }
72
+
73
+ async embed(text) {
74
+ const body = JSON.stringify({
75
+ model: 'text-embedding-3-small',
76
+ input: text
77
+ });
78
+ const res = await fetch('https://api.openai.com/v1/embeddings', {
79
+ method: 'POST',
80
+ headers: {
81
+ 'Content-Type': 'application/json',
82
+ 'Authorization': `Bearer ${this.apiKey}`
83
+ },
84
+ body
85
+ });
86
+ if (!res.ok) throw new Error(`OpenAI API error: ${res.status}`);
87
+ const data = await res.json();
88
+ return data.data[0].embedding;
89
+ }
90
+
91
+ cosine(a, b) {
92
+ let dot = 0, magA = 0, magB = 0;
93
+ for (let i = 0; i < a.length; i++) {
94
+ dot += a[i] * b[i];
95
+ magA += a[i] * a[i];
96
+ magB += b[i] * b[i];
97
+ }
98
+ if (!magA || !magB) return 0;
99
+ return dot / (Math.sqrt(magA) * Math.sqrt(magB));
100
+ }
101
+ }
102
+
103
+ // ── SQLite Store (sql.js) ──────────────────────────────────────
68
104
  class LocalStore {
69
- constructor(dir) {
105
+ constructor(dir, opts = {}) {
70
106
  this.dir = dir || path.join(os.homedir(), '.mem');
71
- this.dbPath = path.join(this.dir, 'mem.json');
107
+ this.dbPath = path.join(this.dir, 'mem.db');
72
108
  this.tfidf = new TfIdf();
73
- this._data = null;
109
+ this._db = null;
110
+ this._SQL = null;
111
+ this._openaiKey = opts.openaiKey || process.env.OPENAI_API_KEY || null;
112
+ this._embedder = this._openaiKey ? new OpenAIEmbedder(this._openaiKey) : null;
74
113
  }
75
114
 
76
- _load() {
77
- if (this._data) return this._data;
115
+ _initDb() {
116
+ if (this._db) return this._db;
117
+ const initSqlJs = require('sql.js');
118
+ // sql.js returns a promise, but we need sync init for backward compat
119
+ // Use the sync factory if available, otherwise we cache
120
+ if (!this._SQL) {
121
+ throw new Error('Must call await store.init() before using the store');
122
+ }
123
+ return this._db;
124
+ }
125
+
126
+ async init() {
127
+ if (this._db) return;
128
+ const initSqlJs = require('sql.js');
129
+ this._SQL = await initSqlJs();
78
130
  fs.mkdirSync(this.dir, { recursive: true });
79
131
  try {
80
- this._data = JSON.parse(fs.readFileSync(this.dbPath, 'utf8'));
132
+ const buf = fs.readFileSync(this.dbPath);
133
+ this._db = new this._SQL.Database(buf);
81
134
  } catch {
82
- this._data = { nextId: 1, memories: [] };
135
+ this._db = new this._SQL.Database();
83
136
  }
84
- return this._data;
137
+ this._db.run(`CREATE TABLE IF NOT EXISTS memories (
138
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
139
+ text TEXT NOT NULL,
140
+ agent TEXT DEFAULT 'default',
141
+ embedding TEXT,
142
+ tags TEXT DEFAULT '',
143
+ created_at TEXT NOT NULL,
144
+ importance REAL DEFAULT 1.0,
145
+ source TEXT DEFAULT 'manual',
146
+ recall_count INTEGER DEFAULT 0
147
+ )`);
148
+ // Migration: add columns if they don't exist (for existing DBs)
149
+ try { this._db.run('ALTER TABLE memories ADD COLUMN source TEXT DEFAULT "manual"'); } catch {}
150
+ try { this._db.run('ALTER TABLE memories ADD COLUMN recall_count INTEGER DEFAULT 0'); } catch {}
151
+ this._save();
85
152
  }
86
153
 
87
154
  _save() {
88
- fs.writeFileSync(this.dbPath, JSON.stringify(this._data, null, 2));
155
+ const data = this._db.export();
156
+ const buf = Buffer.from(data);
157
+ fs.writeFileSync(this.dbPath, buf);
89
158
  }
90
159
 
91
- set(text, opts = {}) {
92
- const data = this._load();
160
+ _applyDecay(row) {
161
+ const created = new Date(row.created_at);
162
+ const now = new Date();
163
+ const days = (now - created) / (1000 * 60 * 60 * 24);
164
+ if (days > 30) {
165
+ const decay = (days - 30) * 0.01;
166
+ const decayed = Math.max(0.1, row.importance - decay);
167
+ return decayed;
168
+ }
169
+ return row.importance;
170
+ }
171
+
172
+ async set(text, opts = {}) {
173
+ await this.init();
93
174
  const agent = opts.agent || 'default';
94
- const entry = {
95
- id: data.nextId++,
96
- text,
97
- agent,
98
- created_at: new Date().toISOString(),
99
- metadata: opts.metadata || {}
100
- };
101
- data.memories.push(entry);
175
+ const tags = opts.tags || '';
176
+ const importance = opts.importance || 1.0;
177
+ const source = opts.source || 'manual';
178
+ const created_at = new Date().toISOString();
179
+
180
+ let embedding = null;
181
+ if (this._embedder) {
182
+ try {
183
+ const emb = await this._embedder.embed(text);
184
+ embedding = JSON.stringify(emb);
185
+ } catch (e) {
186
+ // fallback: no embedding
187
+ }
188
+ }
189
+
190
+ this._db.run(
191
+ 'INSERT INTO memories (text, agent, embedding, tags, created_at, importance, source) VALUES (?, ?, ?, ?, ?, ?, ?)',
192
+ [text, agent, embedding, tags, created_at, importance, source]
193
+ );
194
+ const id = this._db.exec('SELECT last_insert_rowid() as id')[0].values[0][0];
102
195
  this._save();
103
- return { id: entry.id, text, agent };
196
+ return { id, text, agent, tags };
104
197
  }
105
198
 
106
- recall(query, opts = {}) {
107
- const data = this._load();
199
+ async recall(query, opts = {}) {
200
+ await this.init();
108
201
  const agent = opts.agent || 'default';
109
202
  const limit = opts.limit || 10;
110
- const rows = data.memories.filter(m => m.agent === agent);
111
- return this.tfidf.rank(query, rows).slice(0, limit);
203
+ const top = opts.top || null;
204
+ const filterTags = opts.tags ? opts.tags.split(',').map(t => t.trim()) : null;
205
+
206
+ const stmt = this._db.exec(
207
+ 'SELECT id, text, agent, embedding, tags, created_at, importance, source, recall_count FROM memories WHERE agent = ?',
208
+ [agent]
209
+ );
210
+ if (!stmt.length) return [];
211
+
212
+ const cols = stmt[0].columns;
213
+ let rows = stmt[0].values.map(v => {
214
+ const obj = {};
215
+ cols.forEach((c, i) => obj[c] = v[i]);
216
+ return obj;
217
+ });
218
+
219
+ // Filter by tags if specified
220
+ if (filterTags) {
221
+ rows = rows.filter(r => {
222
+ const rTags = (r.tags || '').split(',').map(t => t.trim()).filter(Boolean);
223
+ return filterTags.some(ft => rTags.includes(ft));
224
+ });
225
+ }
226
+
227
+ let scored;
228
+
229
+ // Try OpenAI embeddings first
230
+ if (this._embedder) {
231
+ try {
232
+ const qEmb = await this._embedder.embed(query);
233
+ scored = rows.map(row => {
234
+ let similarity = 0;
235
+ if (row.embedding) {
236
+ const emb = JSON.parse(row.embedding);
237
+ similarity = this._embedder.cosine(qEmb, emb);
238
+ }
239
+ const effectiveImportance = this._applyDecay(row);
240
+ const maxImportance = Math.max(...rows.map(r => this._applyDecay(r)), 1);
241
+ const normImportance = effectiveImportance / maxImportance;
242
+ const finalScore = similarity * 0.7 + normImportance * 0.3;
243
+ return { id: row.id, text: row.text, agent: row.agent, tags: row.tags, created_at: row.created_at, importance: row.importance, score: Math.round(finalScore * 1000) / 1000 };
244
+ }).filter(d => d.score > 0.01).sort((a, b) => b.score - a.score);
245
+ } catch {
246
+ scored = null; // fall through to TF-IDF
247
+ }
248
+ }
249
+
250
+ if (!scored) {
251
+ // TF-IDF fallback
252
+ const tfidfResults = this.tfidf.rank(query, rows);
253
+ scored = tfidfResults.map(r => {
254
+ const effectiveImportance = this._applyDecay(r);
255
+ const maxImportance = Math.max(...rows.map(row => this._applyDecay(row)), 1);
256
+ const normImportance = effectiveImportance / maxImportance;
257
+ const similarity = r.score;
258
+ const finalScore = similarity * 0.7 + normImportance * 0.3;
259
+ return { ...r, score: Math.round(finalScore * 1000) / 1000 };
260
+ }).sort((a, b) => b.score - a.score);
261
+ }
262
+
263
+ const resultLimit = top || limit;
264
+ const results = scored.slice(0, resultLimit);
265
+
266
+ // Bump importance and recall_count for recalled memories
267
+ for (const r of results) {
268
+ this._db.run('UPDATE memories SET importance = importance + 0.1, recall_count = recall_count + 1 WHERE id = ?', [r.id]);
269
+ }
270
+ this._save();
271
+
272
+ return results;
112
273
  }
113
274
 
114
- list(opts = {}) {
115
- const data = this._load();
275
+ async list(opts = {}) {
276
+ await this.init();
116
277
  const agent = opts.agent || 'default';
117
- return data.memories.filter(m => m.agent === agent).sort((a, b) => b.id - a.id);
278
+ const stmt = this._db.exec(
279
+ 'SELECT id, text, agent, tags, created_at, importance, source, recall_count FROM memories WHERE agent = ? ORDER BY id DESC',
280
+ [agent]
281
+ );
282
+ if (!stmt.length) return [];
283
+ const cols = stmt[0].columns;
284
+ return stmt[0].values.map(v => {
285
+ const obj = {};
286
+ cols.forEach((c, i) => obj[c] = v[i]);
287
+ obj.importance = this._applyDecay(obj);
288
+ return obj;
289
+ });
118
290
  }
119
291
 
120
- forget(id) {
121
- const data = this._load();
122
- const numId = Number(id);
123
- const before = data.memories.length;
124
- data.memories = data.memories.filter(m => m.id !== numId);
292
+ async forget(id) {
293
+ await this.init();
294
+ this._db.run('DELETE FROM memories WHERE id = ?', [Number(id)]);
295
+ const changes = this._db.getRowsModified();
125
296
  this._save();
126
- return { changes: before - data.memories.length };
297
+ return { changes };
127
298
  }
128
299
 
129
- clear(opts = {}) {
130
- const data = this._load();
300
+ async clear(opts = {}) {
301
+ await this.init();
131
302
  const agent = opts.agent || 'default';
132
- const before = data.memories.length;
133
- data.memories = data.memories.filter(m => m.agent !== agent);
303
+ this._db.run('DELETE FROM memories WHERE agent = ?', [agent]);
304
+ const changes = this._db.getRowsModified();
134
305
  this._save();
135
- return { changes: before - data.memories.length };
306
+ return { changes };
307
+ }
308
+
309
+ async important(id, boost = 0.5) {
310
+ await this.init();
311
+ this._db.run('UPDATE memories SET importance = importance + ? WHERE id = ?', [boost, Number(id)]);
312
+ this._save();
313
+ const stmt = this._db.exec('SELECT id, text, importance FROM memories WHERE id = ?', [Number(id)]);
314
+ if (!stmt.length) return null;
315
+ const cols = stmt[0].columns;
316
+ const v = stmt[0].values[0];
317
+ const obj = {};
318
+ cols.forEach((c, i) => obj[c] = v[i]);
319
+ return obj;
320
+ }
321
+
322
+ async stats(opts = {}) {
323
+ await this.init();
324
+ const agent = opts.agent || 'default';
325
+ const stmt = this._db.exec(
326
+ `SELECT COUNT(*) as total, MIN(created_at) as oldest, MAX(created_at) as newest, AVG(importance) as avg_importance FROM memories WHERE agent = ?`,
327
+ [agent]
328
+ );
329
+ if (!stmt.length || !stmt[0].values[0][0]) return { total: 0, oldest: null, newest: null, avg_importance: 0 };
330
+ const [total, oldest, newest, avg_importance] = stmt[0].values[0];
331
+ return { total, oldest, newest, avg_importance: Math.round(avg_importance * 100) / 100 };
332
+ }
333
+
334
+ async exportAll(opts = {}) {
335
+ await this.init();
336
+ const agent = opts.agent;
337
+ let query = 'SELECT id, text, agent, tags, created_at, importance FROM memories';
338
+ const params = [];
339
+ if (agent) {
340
+ query += ' WHERE agent = ?';
341
+ params.push(agent);
342
+ }
343
+ query += ' ORDER BY id ASC';
344
+ const stmt = this._db.exec(query, params);
345
+ if (!stmt.length) return [];
346
+ const cols = stmt[0].columns;
347
+ return stmt[0].values.map(v => {
348
+ const obj = {};
349
+ cols.forEach((c, i) => obj[c] = v[i]);
350
+ return obj;
351
+ });
352
+ }
353
+
354
+ async importAll(memories) {
355
+ await this.init();
356
+ let count = 0;
357
+ for (const m of memories) {
358
+ this._db.run(
359
+ 'INSERT INTO memories (text, agent, tags, created_at, importance) VALUES (?, ?, ?, ?, ?)',
360
+ [m.text, m.agent || 'default', m.tags || '', m.created_at || new Date().toISOString(), m.importance || 1.0]
361
+ );
362
+ count++;
363
+ }
364
+ this._save();
365
+ return { imported: count };
136
366
  }
137
367
  }
138
368
 
@@ -164,11 +394,21 @@ class ApiClient {
164
394
  });
165
395
  }
166
396
 
167
- set(text, opts = {}) { return this._req('POST', '/mem', { text, metadata: opts.metadata }); }
168
- recall(query, opts = {}) { return this._req('GET', `/mem/recall?q=${encodeURIComponent(query)}&limit=${opts.limit || 10}`); }
397
+ async init() {} // no-op for API client
398
+ set(text, opts = {}) { return this._req('POST', '/mem', { text, tags: opts.tags, importance: opts.importance, metadata: opts.metadata }); }
399
+ recall(query, opts = {}) {
400
+ let url = `/mem/recall?q=${encodeURIComponent(query)}&limit=${opts.limit || 10}`;
401
+ if (opts.tags) url += `&tags=${encodeURIComponent(opts.tags)}`;
402
+ if (opts.top) url += `&top=${opts.top}`;
403
+ return this._req('GET', url);
404
+ }
169
405
  list() { return this._req('GET', '/mem/list'); }
170
406
  forget(id) { return this._req('DELETE', `/mem/${id}`); }
171
407
  clear() { return this._req('DELETE', '/mem?confirm=true'); }
408
+ important(id) { return this._req('POST', `/mem/${id}/important`); }
409
+ stats() { return this._req('GET', '/mem/stats'); }
410
+ exportAll() { return this._req('GET', '/mem/export'); }
411
+ importAll(memories) { return this._req('POST', '/mem/import', memories); }
172
412
  }
173
413
 
174
414
  // ── Exports ────────────────────────────────────────────────────
@@ -177,18 +417,24 @@ let _store = null;
177
417
 
178
418
  function getStore() {
179
419
  if (!_store) {
180
- _store = _config.api ? new ApiClient(_config) : new LocalStore(_config.dir);
420
+ _store = _config.api
421
+ ? new ApiClient(_config)
422
+ : new LocalStore(_config.dir, { openaiKey: _config.openaiKey });
181
423
  }
182
424
  return _store;
183
425
  }
184
426
 
185
427
  module.exports = {
186
428
  configure(opts) { _config = opts; _store = null; },
187
- set(text, opts) { return Promise.resolve(getStore().set(text, opts)); },
188
- recall(query, opts) { return Promise.resolve(getStore().recall(query, opts)); },
189
- list(opts) { return Promise.resolve(getStore().list(opts)); },
190
- forget(id) { return Promise.resolve(getStore().forget(id)); },
191
- clear(opts) { return Promise.resolve(getStore().clear(opts)); },
429
+ async set(text, opts) { const s = getStore(); await s.init(); return s.set(text, opts); },
430
+ async recall(query, opts) { const s = getStore(); await s.init(); return s.recall(query, opts); },
431
+ async list(opts) { const s = getStore(); await s.init(); return s.list(opts); },
432
+ async forget(id) { const s = getStore(); await s.init(); return s.forget(id); },
433
+ async clear(opts) { const s = getStore(); await s.init(); return s.clear(opts); },
434
+ async important(id, boost) { const s = getStore(); await s.init(); return s.important(id, boost); },
435
+ async stats(opts) { const s = getStore(); await s.init(); return s.stats(opts); },
436
+ async exportAll(opts) { const s = getStore(); await s.init(); return s.exportAll(opts); },
437
+ async importAll(memories) { const s = getStore(); await s.init(); return s.importAll(memories); },
192
438
  TfIdf,
193
439
  LocalStore,
194
440
  ApiClient