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/README.md +127 -31
- package/bin/mem.js +246 -34
- package/bin/memshell.js +246 -34
- package/package.json +3 -2
- package/server.js +63 -13
- package/src/index.js +293 -47
- package/src/ingest.js +348 -0
package/src/index.js
CHANGED
|
@@ -64,75 +64,305 @@ class TfIdf {
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
// ──
|
|
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.
|
|
107
|
+
this.dbPath = path.join(this.dir, 'mem.db');
|
|
72
108
|
this.tfidf = new TfIdf();
|
|
73
|
-
this.
|
|
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
|
-
|
|
77
|
-
if (this.
|
|
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
|
-
|
|
132
|
+
const buf = fs.readFileSync(this.dbPath);
|
|
133
|
+
this._db = new this._SQL.Database(buf);
|
|
81
134
|
} catch {
|
|
82
|
-
this.
|
|
135
|
+
this._db = new this._SQL.Database();
|
|
83
136
|
}
|
|
84
|
-
|
|
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
|
-
|
|
155
|
+
const data = this._db.export();
|
|
156
|
+
const buf = Buffer.from(data);
|
|
157
|
+
fs.writeFileSync(this.dbPath, buf);
|
|
89
158
|
}
|
|
90
159
|
|
|
91
|
-
|
|
92
|
-
const
|
|
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
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
196
|
+
return { id, text, agent, tags };
|
|
104
197
|
}
|
|
105
198
|
|
|
106
|
-
recall(query, opts = {}) {
|
|
107
|
-
|
|
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
|
|
111
|
-
|
|
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
|
-
|
|
275
|
+
async list(opts = {}) {
|
|
276
|
+
await this.init();
|
|
116
277
|
const agent = opts.agent || 'default';
|
|
117
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
const
|
|
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
|
|
297
|
+
return { changes };
|
|
127
298
|
}
|
|
128
299
|
|
|
129
|
-
clear(opts = {}) {
|
|
130
|
-
|
|
300
|
+
async clear(opts = {}) {
|
|
301
|
+
await this.init();
|
|
131
302
|
const agent = opts.agent || 'default';
|
|
132
|
-
|
|
133
|
-
|
|
303
|
+
this._db.run('DELETE FROM memories WHERE agent = ?', [agent]);
|
|
304
|
+
const changes = this._db.getRowsModified();
|
|
134
305
|
this._save();
|
|
135
|
-
return { changes
|
|
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
|
-
|
|
168
|
-
|
|
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
|
|
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) {
|
|
188
|
-
recall(query, opts) {
|
|
189
|
-
list(opts) {
|
|
190
|
-
forget(id) {
|
|
191
|
-
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
|