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/bin/memshell.js CHANGED
@@ -1,89 +1,300 @@
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');
7
+ const { LocalStore } = require('../src/index');
5
8
 
6
9
  const args = process.argv.slice(2);
7
10
  const cmd = args[0];
8
- const rest = args.slice(1).join(' ');
9
11
 
10
12
  const HELP = `
11
- mem.sh — persistent memory for AI agents
13
+ \x1b[1mmem.sh\x1b[0m — persistent memory for AI agents
12
14
 
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
15
+ \x1b[36mCore Commands:\x1b[0m
16
+ memshell set <text> Store a memory
17
+ memshell recall <query> Semantic recall
18
+ memshell list List all memories
19
+ memshell forget <id> Delete a memory by ID
20
+ memshell clear Wipe all memories
21
+ memshell important <id> Boost memory importance
20
22
 
21
- Options:
23
+ \x1b[36mAuto-Ingest:\x1b[0m
24
+ memshell ingest <file> Extract facts from a file
25
+ memshell ingest --stdin Extract facts from piped text
26
+ memshell ingest --watch <dir> Watch a directory for new files
27
+
28
+ \x1b[36mIntegrations:\x1b[0m
29
+ memshell connect openclaw Watch OpenClaw session transcripts
30
+ memshell daemon Run continuous ingestion daemon
31
+
32
+ \x1b[36mManagement:\x1b[0m
33
+ memshell config set <key> <val> Set config value
34
+ memshell config get [key] Show config
35
+ memshell stats Show memory statistics
36
+ memshell export Export all memories as JSON
37
+ memshell import <file.json> Import memories from JSON
38
+ memshell serve [--port N] Start API server
39
+
40
+ \x1b[36mOptions:\x1b[0m
22
41
  --agent <name> Agent namespace
23
42
  --api <url> Use remote API instead of local
24
43
  --key <key> API key for remote server
44
+ --tags <t1,t2> Tags (comma-separated)
45
+ --top <N> Return top N results only
46
+ --embeddings Enable OpenAI embeddings (needs OPENAI_API_KEY)
25
47
 
26
- Examples:
27
- mem set "user prefers dark mode"
28
- mem recall "what theme?"
29
- mem list
30
- mem forget 3
48
+ \x1b[36mExamples:\x1b[0m
49
+ memshell set "user prefers dark mode" --tags preferences,ui
50
+ memshell recall "what theme?" --tags preferences --top 3
51
+ echo "User likes vim and dark mode" | memshell ingest --stdin
52
+ memshell connect openclaw
53
+ memshell config set apiKey sk-...
31
54
  `;
32
55
 
33
56
  // Parse flags
34
57
  function flag(name) {
35
58
  const i = args.indexOf('--' + name);
36
59
  if (i === -1) return null;
37
- return args[i + 1] || true;
60
+ if (i + 1 < args.length && !args[i + 1].startsWith('--')) return args[i + 1];
61
+ return true;
62
+ }
63
+
64
+ function hasFlag(name) {
65
+ return args.includes('--' + name);
66
+ }
67
+
68
+ // Smarter text extraction: skip flag values
69
+ function getText() {
70
+ const skip = new Set(['--agent', '--api', '--key', '--tags', '--top', '--port', '--watch']);
71
+ const parts = [];
72
+ let i = 1;
73
+ while (i < args.length) {
74
+ if (skip.has(args[i])) { i += 2; continue; }
75
+ if (args[i] === '--embeddings' || args[i] === '--stdin' || args[i] === '--force') { i++; continue; }
76
+ if (args[i].startsWith('--')) { i++; continue; }
77
+ parts.push(args[i]);
78
+ i++;
79
+ }
80
+ return parts.join(' ').replace(/^["']|["']$/g, '');
81
+ }
82
+
83
+ function readStdin() {
84
+ return new Promise((resolve) => {
85
+ let data = '';
86
+ process.stdin.setEncoding('utf8');
87
+ process.stdin.on('data', chunk => data += chunk);
88
+ process.stdin.on('end', () => resolve(data));
89
+ // If nothing after 100ms and stdin is a TTY, resolve empty
90
+ if (process.stdin.isTTY) resolve('');
91
+ });
38
92
  }
39
93
 
40
94
  async function main() {
41
95
  const agent = flag('agent') || 'default';
42
96
  const api = flag('api');
43
97
  const key = flag('key');
98
+ const tags = flag('tags') || '';
99
+ const top = flag('top') ? parseInt(flag('top')) : null;
100
+ const useEmbeddings = hasFlag('embeddings');
101
+
102
+ const configOpts = { agent };
103
+ if (api) { configOpts.api = api; configOpts.key = key; }
104
+ if (useEmbeddings) { configOpts.openaiKey = process.env.OPENAI_API_KEY; }
44
105
 
45
- if (api) mem.configure({ api, key, agent });
46
- else mem.configure({ agent });
106
+ mem.configure(configOpts);
47
107
 
48
- const opts = { agent };
108
+ const opts = { agent, tags, top };
49
109
 
50
110
  switch (cmd) {
51
111
  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})`);
112
+ const text = getText();
113
+ if (!text) return console.log('Usage: memshell set <text>');
114
+ const r = await mem.set(text, { ...opts, tags });
115
+ console.log(`\x1b[32m+\x1b[0m Stored (id: \x1b[1m${r.id}\x1b[0m)${tags ? ` [tags: ${tags}]` : ''}`);
56
116
  break;
57
117
  }
58
118
  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>');
119
+ const query = getText();
120
+ if (!query) return console.log('Usage: memshell recall <query>');
61
121
  const results = await mem.recall(query, opts);
62
- if (!results.length) return console.log('No memories found.');
122
+ if (!results.length) return console.log('\x1b[33mNo memories found.\x1b[0m');
63
123
  for (const r of results) {
64
- console.log(` [${r.id}] ${r.text} (score: ${r.score})`);
124
+ const tagStr = r.tags ? ` \x1b[35m[${r.tags}]\x1b[0m` : '';
125
+ const srcStr = r.source && r.source !== 'manual' ? ` \x1b[2m(src: ${r.source})\x1b[0m` : '';
126
+ const recallStr = r.recall_count ? ` \x1b[2m(recalled ${r.recall_count}x)\x1b[0m` : '';
127
+ console.log(` \x1b[36m[${r.id}]\x1b[0m ${r.text} \x1b[33m(score: ${r.score})\x1b[0m${tagStr}${srcStr}${recallStr}`);
65
128
  }
66
129
  break;
67
130
  }
68
131
  case 'list': case 'ls': case 'l': {
69
132
  const all = await mem.list(opts);
70
- if (!all.length) return console.log('No memories stored.');
133
+ if (!all.length) return console.log('\x1b[33mNo memories stored.\x1b[0m');
71
134
  for (const r of all) {
72
- console.log(` [${r.id}] ${r.text} (${r.created_at})`);
135
+ const tagStr = r.tags ? ` \x1b[35m[${r.tags}]\x1b[0m` : '';
136
+ const imp = r.importance !== 1.0 ? ` \x1b[33m*${r.importance.toFixed(1)}\x1b[0m` : '';
137
+ const srcStr = r.source && r.source !== 'manual' ? ` \x1b[2m[${r.source}]\x1b[0m` : '';
138
+ console.log(` \x1b[36m[${r.id}]\x1b[0m ${r.text}${tagStr}${imp}${srcStr} \x1b[2m(${r.created_at})\x1b[0m`);
73
139
  }
74
- console.log(`\n ${all.length} memor${all.length === 1 ? 'y' : 'ies'}`);
140
+ console.log(`\n \x1b[1m${all.length}\x1b[0m memor${all.length === 1 ? 'y' : 'ies'}`);
75
141
  break;
76
142
  }
77
143
  case 'forget': case 'delete': case 'rm': {
78
144
  const id = args[1];
79
- if (!id) return console.log('Usage: mem forget <id>');
145
+ if (!id) return console.log('Usage: memshell forget <id>');
80
146
  await mem.forget(id);
81
- console.log(`✓ Forgotten (id: ${id})`);
147
+ console.log(`\x1b[32m+\x1b[0m Forgotten (id: ${id})`);
82
148
  break;
83
149
  }
84
150
  case 'clear': case 'wipe': case 'reset': {
85
151
  await mem.clear(opts);
86
- console.log(' All memories cleared');
152
+ console.log('\x1b[32m+\x1b[0m All memories cleared');
153
+ break;
154
+ }
155
+ case 'important': case 'boost': {
156
+ const id = args[1];
157
+ if (!id) return console.log('Usage: memshell important <id>');
158
+ const r = await mem.important(Number(id));
159
+ if (!r) return console.log('\x1b[31mMemory not found.\x1b[0m');
160
+ console.log(`\x1b[32m+\x1b[0m Boosted memory ${r.id} -> importance: \x1b[1m${r.importance.toFixed(1)}\x1b[0m`);
161
+ break;
162
+ }
163
+ case 'stats': {
164
+ const s = await mem.stats(opts);
165
+ console.log(`\n \x1b[1mMemory Stats\x1b[0m`);
166
+ console.log(` Total: \x1b[36m${s.total}\x1b[0m`);
167
+ console.log(` Oldest: ${s.oldest || 'n/a'}`);
168
+ console.log(` Newest: ${s.newest || 'n/a'}`);
169
+ console.log(` Avg importance: \x1b[33m${s.avg_importance}\x1b[0m\n`);
170
+ break;
171
+ }
172
+ case 'export': {
173
+ const data = await mem.exportAll(opts);
174
+ console.log(JSON.stringify(data, null, 2));
175
+ break;
176
+ }
177
+ case 'import': {
178
+ const file = args[1];
179
+ if (!file) return console.log('Usage: memshell import <file.json>');
180
+ const raw = fs.readFileSync(path.resolve(file), 'utf8');
181
+ const data = JSON.parse(raw);
182
+ const r = await mem.importAll(Array.isArray(data) ? data : data.memories || []);
183
+ console.log(`\x1b[32m+\x1b[0m Imported ${r.imported} memories`);
184
+ break;
185
+ }
186
+ case 'ingest': {
187
+ const { ingestFile, ingest: ingestText } = require('../src/ingest');
188
+ const store = new LocalStore(undefined, useEmbeddings ? { openaiKey: process.env.OPENAI_API_KEY } : {});
189
+ await store.init();
190
+
191
+ if (hasFlag('stdin')) {
192
+ const text = await readStdin();
193
+ if (!text.trim()) return console.log('No input received via stdin.');
194
+ console.log(' Extracting facts from stdin...');
195
+ const result = await ingestText(text, store, { agent });
196
+ console.log(`\x1b[32m+\x1b[0m Extracted: ${result.extracted}, Stored: ${result.stored}, Duplicates: ${result.duplicates}`);
197
+ } else if (hasFlag('watch')) {
198
+ const dir = flag('watch');
199
+ if (!dir || dir === true) return console.log('Usage: memshell ingest --watch <directory>');
200
+ const { watchDirectory } = require('../src/ingest');
201
+ console.log(' Starting directory watcher (Ctrl+C to stop)...');
202
+ watchDirectory(dir, store, { agent });
203
+ // Keep process alive
204
+ process.on('SIGINT', () => { console.log('\n Stopped.'); process.exit(0); });
205
+ } else {
206
+ const file = getText();
207
+ if (!file) return console.log('Usage: memshell ingest <file> | --stdin | --watch <dir>');
208
+ console.log(` Ingesting: ${file}`);
209
+ const result = await ingestFile(file, store, { agent, force: hasFlag('force') });
210
+ if (result.skipped) {
211
+ console.log(` Skipped: ${result.file} (already processed, use --force to re-ingest)`);
212
+ } else {
213
+ console.log(`\x1b[32m+\x1b[0m Extracted: ${result.extracted}, Stored: ${result.stored}, Duplicates: ${result.duplicates}`);
214
+ }
215
+ }
216
+ break;
217
+ }
218
+ case 'connect': {
219
+ const target = args[1];
220
+ if (target !== 'openclaw') return console.log('Usage: memshell connect openclaw');
221
+
222
+ const { watchOpenClaw, defaultOpenClawPath, setConfigValue } = require('../src/ingest');
223
+ const store = new LocalStore(undefined, useEmbeddings ? { openaiKey: process.env.OPENAI_API_KEY } : {});
224
+ await store.init();
225
+
226
+ const sessionsPath = args[2] || defaultOpenClawPath();
227
+ setConfigValue('watch.openclaw', sessionsPath);
228
+ console.log(` OpenClaw integration configured.`);
229
+ console.log(` Sessions path: ${sessionsPath}`);
230
+ console.log(' Watching for new transcripts (Ctrl+C to stop)...\n');
231
+ watchOpenClaw(sessionsPath, store, { agent });
232
+ process.on('SIGINT', () => { console.log('\n Stopped.'); process.exit(0); });
233
+ break;
234
+ }
235
+ case 'daemon': {
236
+ const { loadConfig, watchDirectory, watchOpenClaw } = require('../src/ingest');
237
+ const store = new LocalStore(undefined, useEmbeddings ? { openaiKey: process.env.OPENAI_API_KEY } : {});
238
+ await store.init();
239
+
240
+ const config = loadConfig();
241
+ const watchers = config.watch || {};
242
+ let activeWatchers = 0;
243
+
244
+ console.log(' \x1b[1mmem.sh daemon\x1b[0m starting...\n');
245
+
246
+ if (watchers.openclaw) {
247
+ watchOpenClaw(watchers.openclaw, store, { agent });
248
+ activeWatchers++;
249
+ }
250
+
251
+ // Support array of dir watchers
252
+ if (Array.isArray(watchers.dirs)) {
253
+ for (const dir of watchers.dirs) {
254
+ watchDirectory(typeof dir === 'string' ? dir : dir.path, store, { agent });
255
+ activeWatchers++;
256
+ }
257
+ } else if (watchers.dir) {
258
+ watchDirectory(watchers.dir, store, { agent });
259
+ activeWatchers++;
260
+ }
261
+
262
+ if (activeWatchers === 0) {
263
+ console.log(' No watchers configured. Use:');
264
+ console.log(' memshell config set watch.openclaw ~/.openclaw/agents/main/sessions/');
265
+ console.log(' memshell config set watch.dir /path/to/watch');
266
+ process.exit(1);
267
+ }
268
+
269
+ console.log(`\n ${activeWatchers} watcher(s) active. Ctrl+C to stop.\n`);
270
+ process.on('SIGINT', () => { console.log('\n Daemon stopped.'); process.exit(0); });
271
+ break;
272
+ }
273
+ case 'config': {
274
+ const { loadConfig, setConfigValue } = require('../src/ingest');
275
+ const subCmd = args[1];
276
+
277
+ if (subCmd === 'set') {
278
+ const configKey = args[2];
279
+ const configVal = args.slice(3).join(' ');
280
+ if (!configKey || !configVal) return console.log('Usage: memshell config set <key> <value>');
281
+ const result = setConfigValue(configKey, configVal);
282
+ console.log(`\x1b[32m+\x1b[0m Set ${configKey} = ${configVal}`);
283
+ } else if (subCmd === 'get') {
284
+ const config = loadConfig();
285
+ const configKey = args[2];
286
+ if (configKey) {
287
+ const parts = configKey.split('.');
288
+ let val = config;
289
+ for (const p of parts) val = val?.[p];
290
+ console.log(val !== undefined ? JSON.stringify(val, null, 2) : 'Not set');
291
+ } else {
292
+ console.log(JSON.stringify(config, null, 2));
293
+ }
294
+ } else {
295
+ const config = loadConfig();
296
+ console.log(JSON.stringify(config, null, 2));
297
+ }
87
298
  break;
88
299
  }
89
300
  case 'serve': case 'server': {
@@ -91,6 +302,7 @@ async function main() {
91
302
  const authKey = flag('key') || process.env.MEM_KEY || '';
92
303
  process.env.MEM_PORT = port;
93
304
  if (authKey) process.env.MEM_KEY = authKey;
305
+ if (useEmbeddings) process.env.MEM_USE_EMBEDDINGS = '1';
94
306
  require('../server');
95
307
  break;
96
308
  }
@@ -99,4 +311,4 @@ async function main() {
99
311
  }
100
312
  }
101
313
 
102
- main().catch(e => { console.error('Error:', e.message); process.exit(1); });
314
+ main().catch(e => { console.error('\x1b[31mError:\x1b[0m', e.message); process.exit(1); });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memshell",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "Persistent memory for AI agents. Like localStorage but for LLMs.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -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
  }
@@ -19,44 +27,86 @@ app.use('/mem', (req, res, next) => {
19
27
  next();
20
28
  });
21
29
 
30
+ // Ingest raw text
31
+ app.post('/mem/ingest', async (req, res) => {
32
+ const { text, source } = req.body;
33
+ if (!text) return res.status(400).json({ error: 'text is required' });
34
+ try {
35
+ const { ingest } = require('./src/ingest');
36
+ const result = await ingest(text, store, { agent: req.agent, source: source || 'api' });
37
+ res.json(result);
38
+ } catch (e) {
39
+ res.status(500).json({ error: e.message });
40
+ }
41
+ });
42
+
22
43
  // Store a memory
23
- app.post('/mem', (req, res) => {
24
- const { text, metadata } = req.body;
44
+ app.post('/mem', async (req, res) => {
45
+ const { text, tags, importance, metadata } = req.body;
25
46
  if (!text) return res.status(400).json({ error: 'text is required' });
26
- const result = store.set(text, { agent: req.agent, metadata });
47
+ const result = await store.set(text, { agent: req.agent, tags: tags || '', importance, metadata });
27
48
  res.json(result);
28
49
  });
29
50
 
30
51
  // Semantic recall
31
- app.get('/mem/recall', (req, res) => {
52
+ app.get('/mem/recall', async (req, res) => {
32
53
  const q = req.query.q;
33
54
  if (!q) return res.status(400).json({ error: 'q parameter is required' });
34
55
  const limit = parseInt(req.query.limit) || 10;
35
- const results = store.recall(q, { agent: req.agent, limit });
56
+ const tags = req.query.tags || '';
57
+ const top = req.query.top ? parseInt(req.query.top) : null;
58
+ const results = await store.recall(q, { agent: req.agent, limit, tags, top });
36
59
  res.json(results);
37
60
  });
38
61
 
39
62
  // List all
40
- app.get('/mem/list', (req, res) => {
41
- const results = store.list({ agent: req.agent });
63
+ app.get('/mem/list', async (req, res) => {
64
+ const results = await store.list({ agent: req.agent });
42
65
  res.json(results);
43
66
  });
44
67
 
68
+ // Stats
69
+ app.get('/mem/stats', async (req, res) => {
70
+ const stats = await store.stats({ agent: req.agent });
71
+ res.json(stats);
72
+ });
73
+
74
+ // Export
75
+ app.get('/mem/export', async (req, res) => {
76
+ const data = await store.exportAll({ agent: req.query.agent });
77
+ res.json(data);
78
+ });
79
+
80
+ // Import
81
+ app.post('/mem/import', async (req, res) => {
82
+ const memories = Array.isArray(req.body) ? req.body : req.body.memories || [];
83
+ const result = await store.importAll(memories);
84
+ res.json(result);
85
+ });
86
+
87
+ // Boost importance
88
+ app.post('/mem/:id/important', async (req, res) => {
89
+ const result = await store.important(req.params.id);
90
+ if (!result) return res.status(404).json({ error: 'Memory not found' });
91
+ res.json(result);
92
+ });
93
+
45
94
  // Delete by id
46
- app.delete('/mem/:id', (req, res) => {
47
- store.forget(req.params.id);
95
+ app.delete('/mem/:id', async (req, res) => {
96
+ await store.forget(req.params.id);
48
97
  res.json({ ok: true, id: req.params.id });
49
98
  });
50
99
 
51
100
  // Clear all for agent
52
- app.delete('/mem', (req, res) => {
53
- store.clear({ agent: req.agent });
101
+ app.delete('/mem', async (req, res) => {
102
+ await store.clear({ agent: req.agent });
54
103
  res.json({ ok: true });
55
104
  });
56
105
 
57
106
  app.listen(PORT, () => {
58
107
  console.log(`\n 🧠 mem.sh server running on http://localhost:${PORT}`);
59
108
  console.log(` Auth: ${AUTH_KEY ? 'enabled' : 'disabled'}`);
109
+ console.log(` Embeddings: ${storeOpts.openaiKey ? 'OpenAI' : 'TF-IDF'}`);
60
110
  console.log();
61
111
  });
62
112