memshell 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.
package/bin/mem.js CHANGED
@@ -1,89 +1,163 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
+ const fs = require('fs');
5
+ const path = require('path');
4
6
  const mem = require('../src/index');
5
7
 
6
8
  const args = process.argv.slice(2);
7
9
  const cmd = args[0];
8
- const rest = args.slice(1).join(' ');
9
10
 
10
11
  const HELP = `
11
- mem.sh — persistent memory for AI agents
12
+ \x1b[1mmem.sh\x1b[0m — persistent memory for AI agents
12
13
 
13
- Usage:
14
- mem set <text> Store a memory
15
- mem recall <query> Semantic recall
16
- mem list List all memories
17
- mem forget <id> Delete a memory by ID
18
- mem clear Wipe all memories
19
- mem serve [--port N] Start API server
14
+ \x1b[36mUsage:\x1b[0m
15
+ memshell set <text> Store a memory
16
+ memshell recall <query> Semantic recall
17
+ memshell list List all memories
18
+ memshell forget <id> Delete a memory by ID
19
+ memshell clear Wipe all memories
20
+ memshell important <id> Boost memory importance
21
+ memshell stats Show memory statistics
22
+ memshell export Export all memories as JSON
23
+ memshell import <file.json> Import memories from JSON
24
+ memshell serve [--port N] Start API server
20
25
 
21
- Options:
26
+ \x1b[36mOptions:\x1b[0m
22
27
  --agent <name> Agent namespace
23
28
  --api <url> Use remote API instead of local
24
29
  --key <key> API key for remote server
30
+ --tags <t1,t2> Tags (comma-separated)
31
+ --top <N> Return top N results only
32
+ --embeddings Enable OpenAI embeddings (needs OPENAI_API_KEY)
25
33
 
26
- Examples:
27
- mem set "user prefers dark mode"
28
- mem recall "what theme?"
29
- mem list
30
- mem forget 3
34
+ \x1b[36mExamples:\x1b[0m
35
+ memshell set "user prefers dark mode" --tags preferences,ui
36
+ memshell recall "what theme?" --tags preferences --top 3
37
+ memshell important 5
38
+ memshell stats
39
+ memshell export > backup.json
40
+ memshell import backup.json
31
41
  `;
32
42
 
33
43
  // Parse flags
34
44
  function flag(name) {
35
45
  const i = args.indexOf('--' + name);
36
46
  if (i === -1) return null;
37
- return args[i + 1] || true;
47
+ if (i + 1 < args.length && !args[i + 1].startsWith('--')) return args[i + 1];
48
+ return true;
49
+ }
50
+
51
+ function hasFlag(name) {
52
+ return args.includes('--' + name);
53
+ }
54
+
55
+ function getTextArgs() {
56
+ return args.slice(1).filter(a => !a.startsWith('--') && (args.indexOf(a) === 0 || !['--agent', '--api', '--key', '--tags', '--top', '--port'].includes(args[args.indexOf(a) - 1]))).join(' ').replace(/^["']|["']$/g, '');
57
+ }
58
+
59
+ // Smarter text extraction: skip flag values
60
+ function getText() {
61
+ const skip = new Set(['--agent', '--api', '--key', '--tags', '--top', '--port']);
62
+ const parts = [];
63
+ let i = 1; // skip command
64
+ while (i < args.length) {
65
+ if (skip.has(args[i])) { i += 2; continue; }
66
+ if (args[i] === '--embeddings') { i++; continue; }
67
+ if (args[i].startsWith('--')) { i++; continue; }
68
+ parts.push(args[i]);
69
+ i++;
70
+ }
71
+ return parts.join(' ').replace(/^["']|["']$/g, '');
38
72
  }
39
73
 
40
74
  async function main() {
41
75
  const agent = flag('agent') || 'default';
42
76
  const api = flag('api');
43
77
  const key = flag('key');
78
+ const tags = flag('tags') || '';
79
+ const top = flag('top') ? parseInt(flag('top')) : null;
80
+ const useEmbeddings = hasFlag('embeddings');
44
81
 
45
- if (api) mem.configure({ api, key, agent });
46
- else mem.configure({ agent });
82
+ const configOpts = { agent };
83
+ if (api) { configOpts.api = api; configOpts.key = key; }
84
+ if (useEmbeddings) { configOpts.openaiKey = process.env.OPENAI_API_KEY; }
47
85
 
48
- const opts = { agent };
86
+ mem.configure(configOpts);
87
+
88
+ const opts = { agent, tags, top };
49
89
 
50
90
  switch (cmd) {
51
91
  case 'set': case 's': case 'save': case 'remember': {
52
- const text = args.slice(1).filter(a => !a.startsWith('--')).join(' ').replace(/^["']|["']$/g, '');
53
- if (!text) return console.log('Usage: mem set <text>');
54
- const r = await mem.set(text, opts);
55
- console.log(`✓ Stored (id: ${r.id})`);
92
+ const text = getText();
93
+ if (!text) return console.log('Usage: memshell set <text>');
94
+ const r = await mem.set(text, { ...opts, tags });
95
+ console.log(`\x1b[32m✓\x1b[0m Stored (id: \x1b[1m${r.id}\x1b[0m)${tags ? ` [tags: ${tags}]` : ''}`);
56
96
  break;
57
97
  }
58
98
  case 'recall': case 'r': case 'search': case 'q': {
59
- const query = args.slice(1).filter(a => !a.startsWith('--')).join(' ').replace(/^["']|["']$/g, '');
60
- if (!query) return console.log('Usage: mem recall <query>');
99
+ const query = getText();
100
+ if (!query) return console.log('Usage: memshell recall <query>');
61
101
  const results = await mem.recall(query, opts);
62
- if (!results.length) return console.log('No memories found.');
102
+ if (!results.length) return console.log('\x1b[33mNo memories found.\x1b[0m');
63
103
  for (const r of results) {
64
- console.log(` [${r.id}] ${r.text} (score: ${r.score})`);
104
+ const tagStr = r.tags ? ` \x1b[35m[${r.tags}]\x1b[0m` : '';
105
+ console.log(` \x1b[36m[${r.id}]\x1b[0m ${r.text} \x1b[33m(score: ${r.score})\x1b[0m${tagStr}`);
65
106
  }
66
107
  break;
67
108
  }
68
109
  case 'list': case 'ls': case 'l': {
69
110
  const all = await mem.list(opts);
70
- if (!all.length) return console.log('No memories stored.');
111
+ if (!all.length) return console.log('\x1b[33mNo memories stored.\x1b[0m');
71
112
  for (const r of all) {
72
- console.log(` [${r.id}] ${r.text} (${r.created_at})`);
113
+ const tagStr = r.tags ? ` \x1b[35m[${r.tags}]\x1b[0m` : '';
114
+ const imp = r.importance !== 1.0 ? ` \x1b[33m★${r.importance.toFixed(1)}\x1b[0m` : '';
115
+ console.log(` \x1b[36m[${r.id}]\x1b[0m ${r.text}${tagStr}${imp} \x1b[2m(${r.created_at})\x1b[0m`);
73
116
  }
74
- console.log(`\n ${all.length} memor${all.length === 1 ? 'y' : 'ies'}`);
117
+ console.log(`\n \x1b[1m${all.length}\x1b[0m memor${all.length === 1 ? 'y' : 'ies'}`);
75
118
  break;
76
119
  }
77
120
  case 'forget': case 'delete': case 'rm': {
78
121
  const id = args[1];
79
- if (!id) return console.log('Usage: mem forget <id>');
122
+ if (!id) return console.log('Usage: memshell forget <id>');
80
123
  await mem.forget(id);
81
- console.log(`✓ Forgotten (id: ${id})`);
124
+ console.log(`\x1b[32m✓\x1b[0m Forgotten (id: ${id})`);
82
125
  break;
83
126
  }
84
127
  case 'clear': case 'wipe': case 'reset': {
85
128
  await mem.clear(opts);
86
- console.log(' All memories cleared');
129
+ console.log('\x1b[32m✓\x1b[0m All memories cleared');
130
+ break;
131
+ }
132
+ case 'important': case 'boost': {
133
+ const id = args[1];
134
+ if (!id) return console.log('Usage: memshell important <id>');
135
+ const r = await mem.important(Number(id));
136
+ if (!r) return console.log('\x1b[31mMemory not found.\x1b[0m');
137
+ console.log(`\x1b[32m✓\x1b[0m Boosted memory ${r.id} → importance: \x1b[1m${r.importance.toFixed(1)}\x1b[0m`);
138
+ break;
139
+ }
140
+ case 'stats': {
141
+ const s = await mem.stats(opts);
142
+ console.log(`\n \x1b[1m🧠 Memory Stats\x1b[0m`);
143
+ console.log(` Total: \x1b[36m${s.total}\x1b[0m`);
144
+ console.log(` Oldest: ${s.oldest || 'n/a'}`);
145
+ console.log(` Newest: ${s.newest || 'n/a'}`);
146
+ console.log(` Avg importance: \x1b[33m${s.avg_importance}\x1b[0m\n`);
147
+ break;
148
+ }
149
+ case 'export': {
150
+ const data = await mem.exportAll(opts);
151
+ console.log(JSON.stringify(data, null, 2));
152
+ break;
153
+ }
154
+ case 'import': {
155
+ const file = args[1];
156
+ if (!file) return console.log('Usage: memshell import <file.json>');
157
+ const raw = fs.readFileSync(path.resolve(file), 'utf8');
158
+ const data = JSON.parse(raw);
159
+ const r = await mem.importAll(Array.isArray(data) ? data : data.memories || []);
160
+ console.log(`\x1b[32m✓\x1b[0m Imported ${r.imported} memories`);
87
161
  break;
88
162
  }
89
163
  case 'serve': case 'server': {
@@ -91,6 +165,7 @@ async function main() {
91
165
  const authKey = flag('key') || process.env.MEM_KEY || '';
92
166
  process.env.MEM_PORT = port;
93
167
  if (authKey) process.env.MEM_KEY = authKey;
168
+ if (useEmbeddings) process.env.MEM_USE_EMBEDDINGS = '1';
94
169
  require('../server');
95
170
  break;
96
171
  }
@@ -99,4 +174,4 @@ async function main() {
99
174
  }
100
175
  }
101
176
 
102
- main().catch(e => { console.error('Error:', e.message); process.exit(1); });
177
+ main().catch(e => { console.error('\x1b[31mError:\x1b[0m', e.message); process.exit(1); });
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const mem = require('../src/index');
7
+
8
+ const args = process.argv.slice(2);
9
+ const cmd = args[0];
10
+
11
+ const HELP = `
12
+ \x1b[1mmem.sh\x1b[0m — persistent memory for AI agents
13
+
14
+ \x1b[36mUsage:\x1b[0m
15
+ memshell set <text> Store a memory
16
+ memshell recall <query> Semantic recall
17
+ memshell list List all memories
18
+ memshell forget <id> Delete a memory by ID
19
+ memshell clear Wipe all memories
20
+ memshell important <id> Boost memory importance
21
+ memshell stats Show memory statistics
22
+ memshell export Export all memories as JSON
23
+ memshell import <file.json> Import memories from JSON
24
+ memshell serve [--port N] Start API server
25
+
26
+ \x1b[36mOptions:\x1b[0m
27
+ --agent <name> Agent namespace
28
+ --api <url> Use remote API instead of local
29
+ --key <key> API key for remote server
30
+ --tags <t1,t2> Tags (comma-separated)
31
+ --top <N> Return top N results only
32
+ --embeddings Enable OpenAI embeddings (needs OPENAI_API_KEY)
33
+
34
+ \x1b[36mExamples:\x1b[0m
35
+ memshell set "user prefers dark mode" --tags preferences,ui
36
+ memshell recall "what theme?" --tags preferences --top 3
37
+ memshell important 5
38
+ memshell stats
39
+ memshell export > backup.json
40
+ memshell import backup.json
41
+ `;
42
+
43
+ // Parse flags
44
+ function flag(name) {
45
+ const i = args.indexOf('--' + name);
46
+ if (i === -1) return null;
47
+ if (i + 1 < args.length && !args[i + 1].startsWith('--')) return args[i + 1];
48
+ return true;
49
+ }
50
+
51
+ function hasFlag(name) {
52
+ return args.includes('--' + name);
53
+ }
54
+
55
+ function getTextArgs() {
56
+ return args.slice(1).filter(a => !a.startsWith('--') && (args.indexOf(a) === 0 || !['--agent', '--api', '--key', '--tags', '--top', '--port'].includes(args[args.indexOf(a) - 1]))).join(' ').replace(/^["']|["']$/g, '');
57
+ }
58
+
59
+ // Smarter text extraction: skip flag values
60
+ function getText() {
61
+ const skip = new Set(['--agent', '--api', '--key', '--tags', '--top', '--port']);
62
+ const parts = [];
63
+ let i = 1; // skip command
64
+ while (i < args.length) {
65
+ if (skip.has(args[i])) { i += 2; continue; }
66
+ if (args[i] === '--embeddings') { i++; continue; }
67
+ if (args[i].startsWith('--')) { i++; continue; }
68
+ parts.push(args[i]);
69
+ i++;
70
+ }
71
+ return parts.join(' ').replace(/^["']|["']$/g, '');
72
+ }
73
+
74
+ async function main() {
75
+ const agent = flag('agent') || 'default';
76
+ const api = flag('api');
77
+ const key = flag('key');
78
+ const tags = flag('tags') || '';
79
+ const top = flag('top') ? parseInt(flag('top')) : null;
80
+ const useEmbeddings = hasFlag('embeddings');
81
+
82
+ const configOpts = { agent };
83
+ if (api) { configOpts.api = api; configOpts.key = key; }
84
+ if (useEmbeddings) { configOpts.openaiKey = process.env.OPENAI_API_KEY; }
85
+
86
+ mem.configure(configOpts);
87
+
88
+ const opts = { agent, tags, top };
89
+
90
+ switch (cmd) {
91
+ case 'set': case 's': case 'save': case 'remember': {
92
+ const text = getText();
93
+ if (!text) return console.log('Usage: memshell set <text>');
94
+ const r = await mem.set(text, { ...opts, tags });
95
+ console.log(`\x1b[32m✓\x1b[0m Stored (id: \x1b[1m${r.id}\x1b[0m)${tags ? ` [tags: ${tags}]` : ''}`);
96
+ break;
97
+ }
98
+ case 'recall': case 'r': case 'search': case 'q': {
99
+ const query = getText();
100
+ if (!query) return console.log('Usage: memshell recall <query>');
101
+ const results = await mem.recall(query, opts);
102
+ if (!results.length) return console.log('\x1b[33mNo memories found.\x1b[0m');
103
+ for (const r of results) {
104
+ const tagStr = r.tags ? ` \x1b[35m[${r.tags}]\x1b[0m` : '';
105
+ console.log(` \x1b[36m[${r.id}]\x1b[0m ${r.text} \x1b[33m(score: ${r.score})\x1b[0m${tagStr}`);
106
+ }
107
+ break;
108
+ }
109
+ case 'list': case 'ls': case 'l': {
110
+ const all = await mem.list(opts);
111
+ if (!all.length) return console.log('\x1b[33mNo memories stored.\x1b[0m');
112
+ for (const r of all) {
113
+ const tagStr = r.tags ? ` \x1b[35m[${r.tags}]\x1b[0m` : '';
114
+ const imp = r.importance !== 1.0 ? ` \x1b[33m★${r.importance.toFixed(1)}\x1b[0m` : '';
115
+ console.log(` \x1b[36m[${r.id}]\x1b[0m ${r.text}${tagStr}${imp} \x1b[2m(${r.created_at})\x1b[0m`);
116
+ }
117
+ console.log(`\n \x1b[1m${all.length}\x1b[0m memor${all.length === 1 ? 'y' : 'ies'}`);
118
+ break;
119
+ }
120
+ case 'forget': case 'delete': case 'rm': {
121
+ const id = args[1];
122
+ if (!id) return console.log('Usage: memshell forget <id>');
123
+ await mem.forget(id);
124
+ console.log(`\x1b[32m✓\x1b[0m Forgotten (id: ${id})`);
125
+ break;
126
+ }
127
+ case 'clear': case 'wipe': case 'reset': {
128
+ await mem.clear(opts);
129
+ console.log('\x1b[32m✓\x1b[0m All memories cleared');
130
+ break;
131
+ }
132
+ case 'important': case 'boost': {
133
+ const id = args[1];
134
+ if (!id) return console.log('Usage: memshell important <id>');
135
+ const r = await mem.important(Number(id));
136
+ if (!r) return console.log('\x1b[31mMemory not found.\x1b[0m');
137
+ console.log(`\x1b[32m✓\x1b[0m Boosted memory ${r.id} → importance: \x1b[1m${r.importance.toFixed(1)}\x1b[0m`);
138
+ break;
139
+ }
140
+ case 'stats': {
141
+ const s = await mem.stats(opts);
142
+ console.log(`\n \x1b[1m🧠 Memory Stats\x1b[0m`);
143
+ console.log(` Total: \x1b[36m${s.total}\x1b[0m`);
144
+ console.log(` Oldest: ${s.oldest || 'n/a'}`);
145
+ console.log(` Newest: ${s.newest || 'n/a'}`);
146
+ console.log(` Avg importance: \x1b[33m${s.avg_importance}\x1b[0m\n`);
147
+ break;
148
+ }
149
+ case 'export': {
150
+ const data = await mem.exportAll(opts);
151
+ console.log(JSON.stringify(data, null, 2));
152
+ break;
153
+ }
154
+ case 'import': {
155
+ const file = args[1];
156
+ if (!file) return console.log('Usage: memshell import <file.json>');
157
+ const raw = fs.readFileSync(path.resolve(file), 'utf8');
158
+ const data = JSON.parse(raw);
159
+ const r = await mem.importAll(Array.isArray(data) ? data : data.memories || []);
160
+ console.log(`\x1b[32m✓\x1b[0m Imported ${r.imported} memories`);
161
+ break;
162
+ }
163
+ case 'serve': case 'server': {
164
+ const port = flag('port') || 3456;
165
+ const authKey = flag('key') || process.env.MEM_KEY || '';
166
+ process.env.MEM_PORT = port;
167
+ if (authKey) process.env.MEM_KEY = authKey;
168
+ if (useEmbeddings) process.env.MEM_USE_EMBEDDINGS = '1';
169
+ require('../server');
170
+ break;
171
+ }
172
+ default:
173
+ console.log(HELP);
174
+ }
175
+ }
176
+
177
+ main().catch(e => { console.error('\x1b[31mError:\x1b[0m', e.message); process.exit(1); });
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "memshell",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Persistent memory for AI agents. Like localStorage but for LLMs.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
7
- "memshell": "./bin/mem.js"
7
+ "memshell": "bin/memshell.js"
8
8
  },
9
9
  "scripts": {
10
10
  "start": "node server.js",
@@ -25,7 +25,8 @@
25
25
  "author": "justedv",
26
26
  "license": "MIT",
27
27
  "dependencies": {
28
- "express": "^4.18.2"
28
+ "express": "^4.18.2",
29
+ "sql.js": "^1.11.0"
29
30
  },
30
31
  "repository": {
31
32
  "type": "git",
package/server.js CHANGED
@@ -6,12 +6,20 @@ const { LocalStore } = require('./src/index');
6
6
  const app = express();
7
7
  app.use(express.json());
8
8
 
9
- const store = new LocalStore();
9
+ const storeOpts = {};
10
+ if (process.env.MEM_USE_EMBEDDINGS && process.env.OPENAI_API_KEY) {
11
+ storeOpts.openaiKey = process.env.OPENAI_API_KEY;
12
+ }
13
+ const store = new LocalStore(undefined, storeOpts);
10
14
  const PORT = process.env.MEM_PORT || 3456;
11
15
  const AUTH_KEY = process.env.MEM_KEY || '';
12
16
 
17
+ // Ensure store is initialized
18
+ let initPromise = store.init();
19
+
13
20
  // Auth middleware
14
- app.use('/mem', (req, res, next) => {
21
+ app.use('/mem', async (req, res, next) => {
22
+ await initPromise;
15
23
  if (AUTH_KEY && req.headers['x-mem-key'] !== AUTH_KEY) {
16
24
  return res.status(401).json({ error: 'Invalid API key' });
17
25
  }
@@ -20,43 +28,72 @@ app.use('/mem', (req, res, next) => {
20
28
  });
21
29
 
22
30
  // Store a memory
23
- app.post('/mem', (req, res) => {
24
- const { text, metadata } = req.body;
31
+ app.post('/mem', async (req, res) => {
32
+ const { text, tags, importance, metadata } = req.body;
25
33
  if (!text) return res.status(400).json({ error: 'text is required' });
26
- const result = store.set(text, { agent: req.agent, metadata });
34
+ const result = await store.set(text, { agent: req.agent, tags: tags || '', importance, metadata });
27
35
  res.json(result);
28
36
  });
29
37
 
30
38
  // Semantic recall
31
- app.get('/mem/recall', (req, res) => {
39
+ app.get('/mem/recall', async (req, res) => {
32
40
  const q = req.query.q;
33
41
  if (!q) return res.status(400).json({ error: 'q parameter is required' });
34
42
  const limit = parseInt(req.query.limit) || 10;
35
- const results = store.recall(q, { agent: req.agent, limit });
43
+ const tags = req.query.tags || '';
44
+ const top = req.query.top ? parseInt(req.query.top) : null;
45
+ const results = await store.recall(q, { agent: req.agent, limit, tags, top });
36
46
  res.json(results);
37
47
  });
38
48
 
39
49
  // List all
40
- app.get('/mem/list', (req, res) => {
41
- const results = store.list({ agent: req.agent });
50
+ app.get('/mem/list', async (req, res) => {
51
+ const results = await store.list({ agent: req.agent });
42
52
  res.json(results);
43
53
  });
44
54
 
55
+ // Stats
56
+ app.get('/mem/stats', async (req, res) => {
57
+ const stats = await store.stats({ agent: req.agent });
58
+ res.json(stats);
59
+ });
60
+
61
+ // Export
62
+ app.get('/mem/export', async (req, res) => {
63
+ const data = await store.exportAll({ agent: req.query.agent });
64
+ res.json(data);
65
+ });
66
+
67
+ // Import
68
+ app.post('/mem/import', async (req, res) => {
69
+ const memories = Array.isArray(req.body) ? req.body : req.body.memories || [];
70
+ const result = await store.importAll(memories);
71
+ res.json(result);
72
+ });
73
+
74
+ // Boost importance
75
+ app.post('/mem/:id/important', async (req, res) => {
76
+ const result = await store.important(req.params.id);
77
+ if (!result) return res.status(404).json({ error: 'Memory not found' });
78
+ res.json(result);
79
+ });
80
+
45
81
  // Delete by id
46
- app.delete('/mem/:id', (req, res) => {
47
- store.forget(req.params.id);
82
+ app.delete('/mem/:id', async (req, res) => {
83
+ await store.forget(req.params.id);
48
84
  res.json({ ok: true, id: req.params.id });
49
85
  });
50
86
 
51
87
  // Clear all for agent
52
- app.delete('/mem', (req, res) => {
53
- store.clear({ agent: req.agent });
88
+ app.delete('/mem', async (req, res) => {
89
+ await store.clear({ agent: req.agent });
54
90
  res.json({ ok: true });
55
91
  });
56
92
 
57
93
  app.listen(PORT, () => {
58
94
  console.log(`\n 🧠 mem.sh server running on http://localhost:${PORT}`);
59
95
  console.log(` Auth: ${AUTH_KEY ? 'enabled' : 'disabled'}`);
96
+ console.log(` Embeddings: ${storeOpts.openaiKey ? 'OpenAI' : 'TF-IDF'}`);
60
97
  console.log();
61
98
  });
62
99
 
package/src/index.js CHANGED
@@ -64,75 +64,299 @@ 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
+ )`);
146
+ this._save();
85
147
  }
86
148
 
87
149
  _save() {
88
- fs.writeFileSync(this.dbPath, JSON.stringify(this._data, null, 2));
150
+ const data = this._db.export();
151
+ const buf = Buffer.from(data);
152
+ fs.writeFileSync(this.dbPath, buf);
89
153
  }
90
154
 
91
- set(text, opts = {}) {
92
- const data = this._load();
155
+ _applyDecay(row) {
156
+ const created = new Date(row.created_at);
157
+ const now = new Date();
158
+ const days = (now - created) / (1000 * 60 * 60 * 24);
159
+ if (days > 30) {
160
+ const decay = (days - 30) * 0.01;
161
+ const decayed = Math.max(0.1, row.importance - decay);
162
+ return decayed;
163
+ }
164
+ return row.importance;
165
+ }
166
+
167
+ async set(text, opts = {}) {
168
+ await this.init();
93
169
  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);
170
+ const tags = opts.tags || '';
171
+ const importance = opts.importance || 1.0;
172
+ const created_at = new Date().toISOString();
173
+
174
+ let embedding = null;
175
+ if (this._embedder) {
176
+ try {
177
+ const emb = await this._embedder.embed(text);
178
+ embedding = JSON.stringify(emb);
179
+ } catch (e) {
180
+ // fallback: no embedding
181
+ }
182
+ }
183
+
184
+ this._db.run(
185
+ 'INSERT INTO memories (text, agent, embedding, tags, created_at, importance) VALUES (?, ?, ?, ?, ?, ?)',
186
+ [text, agent, embedding, tags, created_at, importance]
187
+ );
188
+ const id = this._db.exec('SELECT last_insert_rowid() as id')[0].values[0][0];
102
189
  this._save();
103
- return { id: entry.id, text, agent };
190
+ return { id, text, agent, tags };
104
191
  }
105
192
 
106
- recall(query, opts = {}) {
107
- const data = this._load();
193
+ async recall(query, opts = {}) {
194
+ await this.init();
108
195
  const agent = opts.agent || 'default';
109
196
  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);
197
+ const top = opts.top || null;
198
+ const filterTags = opts.tags ? opts.tags.split(',').map(t => t.trim()) : null;
199
+
200
+ const stmt = this._db.exec(
201
+ 'SELECT id, text, agent, embedding, tags, created_at, importance FROM memories WHERE agent = ?',
202
+ [agent]
203
+ );
204
+ if (!stmt.length) return [];
205
+
206
+ const cols = stmt[0].columns;
207
+ let rows = stmt[0].values.map(v => {
208
+ const obj = {};
209
+ cols.forEach((c, i) => obj[c] = v[i]);
210
+ return obj;
211
+ });
212
+
213
+ // Filter by tags if specified
214
+ if (filterTags) {
215
+ rows = rows.filter(r => {
216
+ const rTags = (r.tags || '').split(',').map(t => t.trim()).filter(Boolean);
217
+ return filterTags.some(ft => rTags.includes(ft));
218
+ });
219
+ }
220
+
221
+ let scored;
222
+
223
+ // Try OpenAI embeddings first
224
+ if (this._embedder) {
225
+ try {
226
+ const qEmb = await this._embedder.embed(query);
227
+ scored = rows.map(row => {
228
+ let similarity = 0;
229
+ if (row.embedding) {
230
+ const emb = JSON.parse(row.embedding);
231
+ similarity = this._embedder.cosine(qEmb, emb);
232
+ }
233
+ const effectiveImportance = this._applyDecay(row);
234
+ const maxImportance = Math.max(...rows.map(r => this._applyDecay(r)), 1);
235
+ const normImportance = effectiveImportance / maxImportance;
236
+ const finalScore = similarity * 0.7 + normImportance * 0.3;
237
+ 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 };
238
+ }).filter(d => d.score > 0.01).sort((a, b) => b.score - a.score);
239
+ } catch {
240
+ scored = null; // fall through to TF-IDF
241
+ }
242
+ }
243
+
244
+ if (!scored) {
245
+ // TF-IDF fallback
246
+ const tfidfResults = this.tfidf.rank(query, rows);
247
+ scored = tfidfResults.map(r => {
248
+ const effectiveImportance = this._applyDecay(r);
249
+ const maxImportance = Math.max(...rows.map(row => this._applyDecay(row)), 1);
250
+ const normImportance = effectiveImportance / maxImportance;
251
+ const similarity = r.score;
252
+ const finalScore = similarity * 0.7 + normImportance * 0.3;
253
+ return { ...r, score: Math.round(finalScore * 1000) / 1000 };
254
+ }).sort((a, b) => b.score - a.score);
255
+ }
256
+
257
+ const resultLimit = top || limit;
258
+ const results = scored.slice(0, resultLimit);
259
+
260
+ // Bump importance for recalled memories
261
+ for (const r of results) {
262
+ this._db.run('UPDATE memories SET importance = importance + 0.1 WHERE id = ?', [r.id]);
263
+ }
264
+ this._save();
265
+
266
+ return results;
112
267
  }
113
268
 
114
- list(opts = {}) {
115
- const data = this._load();
269
+ async list(opts = {}) {
270
+ await this.init();
116
271
  const agent = opts.agent || 'default';
117
- return data.memories.filter(m => m.agent === agent).sort((a, b) => b.id - a.id);
272
+ const stmt = this._db.exec(
273
+ 'SELECT id, text, agent, tags, created_at, importance FROM memories WHERE agent = ? ORDER BY id DESC',
274
+ [agent]
275
+ );
276
+ if (!stmt.length) return [];
277
+ const cols = stmt[0].columns;
278
+ return stmt[0].values.map(v => {
279
+ const obj = {};
280
+ cols.forEach((c, i) => obj[c] = v[i]);
281
+ obj.importance = this._applyDecay(obj);
282
+ return obj;
283
+ });
118
284
  }
119
285
 
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);
286
+ async forget(id) {
287
+ await this.init();
288
+ this._db.run('DELETE FROM memories WHERE id = ?', [Number(id)]);
289
+ const changes = this._db.getRowsModified();
125
290
  this._save();
126
- return { changes: before - data.memories.length };
291
+ return { changes };
127
292
  }
128
293
 
129
- clear(opts = {}) {
130
- const data = this._load();
294
+ async clear(opts = {}) {
295
+ await this.init();
131
296
  const agent = opts.agent || 'default';
132
- const before = data.memories.length;
133
- data.memories = data.memories.filter(m => m.agent !== agent);
297
+ this._db.run('DELETE FROM memories WHERE agent = ?', [agent]);
298
+ const changes = this._db.getRowsModified();
134
299
  this._save();
135
- return { changes: before - data.memories.length };
300
+ return { changes };
301
+ }
302
+
303
+ async important(id, boost = 0.5) {
304
+ await this.init();
305
+ this._db.run('UPDATE memories SET importance = importance + ? WHERE id = ?', [boost, Number(id)]);
306
+ this._save();
307
+ const stmt = this._db.exec('SELECT id, text, importance FROM memories WHERE id = ?', [Number(id)]);
308
+ if (!stmt.length) return null;
309
+ const cols = stmt[0].columns;
310
+ const v = stmt[0].values[0];
311
+ const obj = {};
312
+ cols.forEach((c, i) => obj[c] = v[i]);
313
+ return obj;
314
+ }
315
+
316
+ async stats(opts = {}) {
317
+ await this.init();
318
+ const agent = opts.agent || 'default';
319
+ const stmt = this._db.exec(
320
+ `SELECT COUNT(*) as total, MIN(created_at) as oldest, MAX(created_at) as newest, AVG(importance) as avg_importance FROM memories WHERE agent = ?`,
321
+ [agent]
322
+ );
323
+ if (!stmt.length || !stmt[0].values[0][0]) return { total: 0, oldest: null, newest: null, avg_importance: 0 };
324
+ const [total, oldest, newest, avg_importance] = stmt[0].values[0];
325
+ return { total, oldest, newest, avg_importance: Math.round(avg_importance * 100) / 100 };
326
+ }
327
+
328
+ async exportAll(opts = {}) {
329
+ await this.init();
330
+ const agent = opts.agent;
331
+ let query = 'SELECT id, text, agent, tags, created_at, importance FROM memories';
332
+ const params = [];
333
+ if (agent) {
334
+ query += ' WHERE agent = ?';
335
+ params.push(agent);
336
+ }
337
+ query += ' ORDER BY id ASC';
338
+ const stmt = this._db.exec(query, params);
339
+ if (!stmt.length) return [];
340
+ const cols = stmt[0].columns;
341
+ return stmt[0].values.map(v => {
342
+ const obj = {};
343
+ cols.forEach((c, i) => obj[c] = v[i]);
344
+ return obj;
345
+ });
346
+ }
347
+
348
+ async importAll(memories) {
349
+ await this.init();
350
+ let count = 0;
351
+ for (const m of memories) {
352
+ this._db.run(
353
+ 'INSERT INTO memories (text, agent, tags, created_at, importance) VALUES (?, ?, ?, ?, ?)',
354
+ [m.text, m.agent || 'default', m.tags || '', m.created_at || new Date().toISOString(), m.importance || 1.0]
355
+ );
356
+ count++;
357
+ }
358
+ this._save();
359
+ return { imported: count };
136
360
  }
137
361
  }
138
362
 
@@ -164,11 +388,21 @@ class ApiClient {
164
388
  });
165
389
  }
166
390
 
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}`); }
391
+ async init() {} // no-op for API client
392
+ set(text, opts = {}) { return this._req('POST', '/mem', { text, tags: opts.tags, importance: opts.importance, metadata: opts.metadata }); }
393
+ recall(query, opts = {}) {
394
+ let url = `/mem/recall?q=${encodeURIComponent(query)}&limit=${opts.limit || 10}`;
395
+ if (opts.tags) url += `&tags=${encodeURIComponent(opts.tags)}`;
396
+ if (opts.top) url += `&top=${opts.top}`;
397
+ return this._req('GET', url);
398
+ }
169
399
  list() { return this._req('GET', '/mem/list'); }
170
400
  forget(id) { return this._req('DELETE', `/mem/${id}`); }
171
401
  clear() { return this._req('DELETE', '/mem?confirm=true'); }
402
+ important(id) { return this._req('POST', `/mem/${id}/important`); }
403
+ stats() { return this._req('GET', '/mem/stats'); }
404
+ exportAll() { return this._req('GET', '/mem/export'); }
405
+ importAll(memories) { return this._req('POST', '/mem/import', memories); }
172
406
  }
173
407
 
174
408
  // ── Exports ────────────────────────────────────────────────────
@@ -177,18 +411,24 @@ let _store = null;
177
411
 
178
412
  function getStore() {
179
413
  if (!_store) {
180
- _store = _config.api ? new ApiClient(_config) : new LocalStore(_config.dir);
414
+ _store = _config.api
415
+ ? new ApiClient(_config)
416
+ : new LocalStore(_config.dir, { openaiKey: _config.openaiKey });
181
417
  }
182
418
  return _store;
183
419
  }
184
420
 
185
421
  module.exports = {
186
422
  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)); },
423
+ async set(text, opts) { const s = getStore(); await s.init(); return s.set(text, opts); },
424
+ async recall(query, opts) { const s = getStore(); await s.init(); return s.recall(query, opts); },
425
+ async list(opts) { const s = getStore(); await s.init(); return s.list(opts); },
426
+ async forget(id) { const s = getStore(); await s.init(); return s.forget(id); },
427
+ async clear(opts) { const s = getStore(); await s.init(); return s.clear(opts); },
428
+ async important(id, boost) { const s = getStore(); await s.init(); return s.important(id, boost); },
429
+ async stats(opts) { const s = getStore(); await s.init(); return s.stats(opts); },
430
+ async exportAll(opts) { const s = getStore(); await s.init(); return s.exportAll(opts); },
431
+ async importAll(memories) { const s = getStore(); await s.init(); return s.importAll(memories); },
192
432
  TfIdf,
193
433
  LocalStore,
194
434
  ApiClient