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/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
<h1>mem.sh</h1>
|
|
4
4
|
|
|
5
|
-
<p><strong>Persistent memory for AI agents.</strong><br>One line to save. One line to recall.</p>
|
|
5
|
+
<p><strong>Persistent memory for AI agents.</strong><br>One line to save. One line to recall. Auto-ingest conversations.</p>
|
|
6
6
|
|
|
7
7
|
[](https://www.npmjs.com/package/memshell)
|
|
8
8
|
[](https://github.com/justedv/mem.sh/blob/main/LICENSE)
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
<br>
|
|
12
12
|
|
|
13
|
-
[Quick Start](#quick-start) · [SDK](#sdk) · [API Server](#api-server) · [Architecture](#how-it-works)
|
|
13
|
+
[Quick Start](#quick-start) · [Auto-Ingest](#auto-ingest) · [OpenClaw Integration](#openclaw-integration) · [SDK](#sdk) · [API Server](#api-server) · [Architecture](#how-it-works)
|
|
14
14
|
|
|
15
15
|
</div>
|
|
16
16
|
|
|
@@ -29,17 +29,20 @@ Agents forget everything between sessions. **mem.sh** gives them a brain.
|
|
|
29
29
|
| | mem.sh | LangChain Memory | Roll your own |
|
|
30
30
|
|---|---|---|---|
|
|
31
31
|
| **Setup** | `npx memshell set "..."` | 47 dependencies + config | Hours of boilerplate |
|
|
32
|
-
| **
|
|
32
|
+
| **Auto-ingest** | Built-in | No | You build it |
|
|
33
|
+
| **External APIs** | None (optional) | OpenAI key required | Depends |
|
|
33
34
|
| **Semantic search** | Built-in TF-IDF | Embedding models | You build it |
|
|
34
35
|
| **Storage** | SQLite (local) | Varies | You choose |
|
|
35
|
-
| **Lines of code** | ~1 | ~50+ | ~200+ |
|
|
36
36
|
|
|
37
37
|
## Features
|
|
38
38
|
|
|
39
|
-
- **Fast**
|
|
40
|
-
- **Local-first**
|
|
41
|
-
- **Semantic**
|
|
42
|
-
- **
|
|
39
|
+
- **Fast** -- TF-IDF vectorization with cosine similarity, instant results
|
|
40
|
+
- **Local-first** -- SQLite storage at `~/.mem/mem.db`, no data leaves your machine
|
|
41
|
+
- **Semantic** -- Recall by meaning, not exact match
|
|
42
|
+
- **Auto-ingest** -- Feed raw conversations, auto-extract key facts via LLM
|
|
43
|
+
- **OpenClaw integration** -- Watch session transcripts and auto-learn
|
|
44
|
+
- **Zero config** -- `npx` and go. No API keys needed for core features
|
|
45
|
+
- **Smart recall** -- Shows source, creation time, and recall frequency
|
|
43
46
|
|
|
44
47
|
## Quick Start
|
|
45
48
|
|
|
@@ -51,7 +54,7 @@ npx memshell set "user prefers dark mode"
|
|
|
51
54
|
|
|
52
55
|
# Recall semantically
|
|
53
56
|
npx memshell recall "what theme does the user like?"
|
|
54
|
-
#
|
|
57
|
+
# => user prefers dark mode (score: 0.87)
|
|
55
58
|
|
|
56
59
|
# List all memories
|
|
57
60
|
npx memshell list
|
|
@@ -63,7 +66,92 @@ npx memshell forget <id>
|
|
|
63
66
|
npx memshell clear
|
|
64
67
|
```
|
|
65
68
|
|
|
66
|
-
|
|
69
|
+
## Auto-Ingest
|
|
70
|
+
|
|
71
|
+
Feed raw conversations and let the LLM extract key facts automatically.
|
|
72
|
+
|
|
73
|
+
Requires `OPENAI_API_KEY` or `ANTHROPIC_API_KEY` (or configure via `memshell config set apiKey <key>`).
|
|
74
|
+
|
|
75
|
+
### From a file
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npx memshell ingest conversation.txt
|
|
79
|
+
npx memshell ingest chat.jsonl
|
|
80
|
+
npx memshell ingest notes.md
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### From stdin
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
echo "User said they prefer dark mode and use vim" | npx memshell ingest --stdin
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Watch a directory
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
npx memshell ingest --watch ./logs/
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Watches for new or changed `.txt`, `.md`, `.json`, and `.jsonl` files. Tracks what has been processed to avoid duplicates.
|
|
96
|
+
|
|
97
|
+
### Via API
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
curl -X POST http://localhost:3456/mem/ingest \
|
|
101
|
+
-H "Content-Type: application/json" \
|
|
102
|
+
-d '{"text": "User mentioned they love Rust and prefer dark themes"}'
|
|
103
|
+
# => {"extracted": 2, "stored": 2, "duplicates": 0}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### How it works
|
|
107
|
+
|
|
108
|
+
1. Text is split into ~2000-token chunks
|
|
109
|
+
2. Each chunk is sent to an LLM (gpt-4o-mini or claude-3-haiku) to extract standalone facts
|
|
110
|
+
3. Facts are deduplicated against existing memories (Jaccard similarity > 0.85 = skip)
|
|
111
|
+
4. New facts are stored with auto-generated tags and source tracking
|
|
112
|
+
|
|
113
|
+
## OpenClaw Integration
|
|
114
|
+
|
|
115
|
+
Automatically learn from your OpenClaw agent conversations:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
# Start watching OpenClaw session transcripts
|
|
119
|
+
npx memshell connect openclaw
|
|
120
|
+
|
|
121
|
+
# Or specify a custom path
|
|
122
|
+
npx memshell connect openclaw /path/to/sessions/
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
This watches the OpenClaw sessions directory (`~/.openclaw/agents/main/sessions/` by default), parses JSONL transcripts, and auto-ingests new conversations.
|
|
126
|
+
|
|
127
|
+
### Daemon mode
|
|
128
|
+
|
|
129
|
+
Run continuous ingestion in the background:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
# Configure watchers first
|
|
133
|
+
npx memshell config set watch.openclaw ~/.openclaw/agents/main/sessions/
|
|
134
|
+
|
|
135
|
+
# Start the daemon
|
|
136
|
+
npx memshell daemon
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Configuration
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
# Set LLM API key
|
|
143
|
+
npx memshell config set apiKey sk-...
|
|
144
|
+
|
|
145
|
+
# Set model
|
|
146
|
+
npx memshell config set model gpt-4o-mini
|
|
147
|
+
|
|
148
|
+
# View config
|
|
149
|
+
npx memshell config get
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Config is stored at `~/.mem/config.json`.
|
|
153
|
+
|
|
154
|
+
## SDK
|
|
67
155
|
|
|
68
156
|
```js
|
|
69
157
|
const mem = require('memshell');
|
|
@@ -72,9 +160,9 @@ const mem = require('memshell');
|
|
|
72
160
|
await mem.set('user prefers dark mode');
|
|
73
161
|
await mem.set('favorite language is rust', { agent: 'coder-bot' });
|
|
74
162
|
|
|
75
|
-
// Recall (semantic search)
|
|
163
|
+
// Recall (semantic search) -- now includes source and recall count
|
|
76
164
|
const results = await mem.recall('what does the user like?');
|
|
77
|
-
// [{ id, text, score, created_at }]
|
|
165
|
+
// [{ id, text, score, created_at, source, recall_count }]
|
|
78
166
|
|
|
79
167
|
// List all
|
|
80
168
|
const all = await mem.list();
|
|
@@ -92,7 +180,7 @@ mem.sh uses **TF-IDF vectorization** with **cosine similarity** for semantic sea
|
|
|
92
180
|
|
|
93
181
|
Memories are stored in `~/.mem/mem.db` (SQLite). Each memory is tokenized and vectorized on write. Queries are vectorized at recall time and ranked by cosine similarity against stored vectors.
|
|
94
182
|
|
|
95
|
-
|
|
183
|
+
Optional: Enable OpenAI embeddings with `--embeddings` flag for higher quality recall (requires `OPENAI_API_KEY`).
|
|
96
184
|
|
|
97
185
|
## API Server
|
|
98
186
|
|
|
@@ -107,29 +195,19 @@ npx memshell serve --port 3456 --key my-secret-key
|
|
|
107
195
|
| Method | Path | Description |
|
|
108
196
|
|--------|------|-------------|
|
|
109
197
|
| `POST` | `/mem` | Store a memory |
|
|
198
|
+
| `POST` | `/mem/ingest` | Auto-ingest raw text |
|
|
110
199
|
| `GET` | `/mem/recall?q=` | Semantic recall |
|
|
111
200
|
| `GET` | `/mem/list` | List all memories |
|
|
201
|
+
| `GET` | `/mem/stats` | Memory statistics |
|
|
202
|
+
| `GET` | `/mem/export` | Export all memories |
|
|
203
|
+
| `POST` | `/mem/import` | Import memories |
|
|
112
204
|
| `DELETE` | `/mem/:id` | Delete a memory |
|
|
113
205
|
| `DELETE` | `/mem` | Clear all memories |
|
|
114
206
|
|
|
115
207
|
### Headers
|
|
116
208
|
|
|
117
|
-
- `X-Mem-Key`
|
|
118
|
-
- `X-Mem-Agent`
|
|
119
|
-
|
|
120
|
-
### Example
|
|
121
|
-
|
|
122
|
-
```bash
|
|
123
|
-
# Store
|
|
124
|
-
curl -X POST http://localhost:3456/mem \
|
|
125
|
-
-H "Content-Type: application/json" \
|
|
126
|
-
-H "X-Mem-Key: my-secret-key" \
|
|
127
|
-
-d '{"text": "user prefers dark mode"}'
|
|
128
|
-
|
|
129
|
-
# Recall
|
|
130
|
-
curl "http://localhost:3456/mem/recall?q=theme+preference" \
|
|
131
|
-
-H "X-Mem-Key: my-secret-key"
|
|
132
|
-
```
|
|
209
|
+
- `X-Mem-Key` -- API key (required if `--key` is set)
|
|
210
|
+
- `X-Mem-Agent` -- Agent namespace (optional, isolates memories per agent)
|
|
133
211
|
|
|
134
212
|
### SDK with API Mode
|
|
135
213
|
|
|
@@ -146,9 +224,27 @@ await mem.set('user prefers dark mode');
|
|
|
146
224
|
const results = await mem.recall('theme preference');
|
|
147
225
|
```
|
|
148
226
|
|
|
149
|
-
##
|
|
227
|
+
## All CLI Commands
|
|
150
228
|
|
|
151
|
-
|
|
229
|
+
```
|
|
230
|
+
memshell set <text> Store a memory
|
|
231
|
+
memshell recall <query> Semantic recall
|
|
232
|
+
memshell list List all memories
|
|
233
|
+
memshell forget <id> Delete a memory by ID
|
|
234
|
+
memshell clear Wipe all memories
|
|
235
|
+
memshell important <id> Boost memory importance
|
|
236
|
+
memshell ingest <file> Extract facts from a file
|
|
237
|
+
memshell ingest --stdin Extract facts from piped text
|
|
238
|
+
memshell ingest --watch <dir> Watch directory for new files
|
|
239
|
+
memshell connect openclaw Watch OpenClaw transcripts
|
|
240
|
+
memshell daemon Run continuous ingestion
|
|
241
|
+
memshell config set <key> <val> Set config value
|
|
242
|
+
memshell config get [key] Show config
|
|
243
|
+
memshell stats Show memory statistics
|
|
244
|
+
memshell export Export all memories as JSON
|
|
245
|
+
memshell import <file.json> Import memories from JSON
|
|
246
|
+
memshell serve [--port N] Start API server
|
|
247
|
+
```
|
|
152
248
|
|
|
153
249
|
## License
|
|
154
250
|
|
package/bin/mem.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
|
-
|
|
13
|
+
\x1b[1mmem.sh\x1b[0m — persistent memory for AI agents
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
53
|
-
if (!text) return console.log('Usage:
|
|
54
|
-
const r = await mem.set(text, opts);
|
|
55
|
-
console.log(
|
|
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 =
|
|
60
|
-
if (!query) return console.log('Usage:
|
|
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('
|
|
122
|
+
if (!results.length) return console.log('\x1b[33mNo memories found.\x1b[0m');
|
|
63
123
|
for (const r of results) {
|
|
64
|
-
|
|
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('
|
|
133
|
+
if (!all.length) return console.log('\x1b[33mNo memories stored.\x1b[0m');
|
|
71
134
|
for (const r of all) {
|
|
72
|
-
|
|
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:
|
|
145
|
+
if (!id) return console.log('Usage: memshell forget <id>');
|
|
80
146
|
await mem.forget(id);
|
|
81
|
-
console.log(
|
|
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('
|
|
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('
|
|
314
|
+
main().catch(e => { console.error('\x1b[31mError:\x1b[0m', e.message); process.exit(1); });
|