@tensakulabs/memory 0.1.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 +92 -0
- package/dist/cli.d.ts +24 -0
- package/dist/cli.js +521 -0
- package/dist/lib.d.ts +62 -0
- package/dist/lib.js +171 -0
- package/dist/rem-sleep.d.ts +13 -0
- package/dist/rem-sleep.js +337 -0
- package/dist/stage.d.ts +13 -0
- package/dist/stage.js +236 -0
- package/examples/atlas.config.json +19 -0
- package/examples/sage.config.json +18 -0
- package/memory-config.example.json +19 -0
- package/package.json +54 -0
package/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Memory Module
|
|
2
|
+
|
|
3
|
+
Three-tier memory system (hot/warm/cold) with REM Sleep batch consolidation.
|
|
4
|
+
|
|
5
|
+
## Tiers
|
|
6
|
+
|
|
7
|
+
| Tier | Storage | TTL | Purpose |
|
|
8
|
+
|------|---------|-----|---------|
|
|
9
|
+
| **Hot** | Local MEMORY.md | Permanent | Agent-specific facts |
|
|
10
|
+
| **Warm** | Shared JSONL | 7 days | Cross-agent working context |
|
|
11
|
+
| **Cold** | mem0 | Permanent | Long-tail semantic search |
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
git clone https://github.com/tensakulabs/memory-module ~/.pai/memory/rem
|
|
17
|
+
cd ~/.pai/memory/rem
|
|
18
|
+
cp memory-config.example.json memory-config.json
|
|
19
|
+
# Edit memory-config.json for your agent
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Update
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
cd ~/.pai/memory/rem && git pull
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Config and data are gitignored — updates never overwrite your settings.
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
### Stage facts during sessions ($0)
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
bun stage.ts "fact text" # auto-classify
|
|
36
|
+
bun stage.ts "fact text" --tier hot # explicit tier
|
|
37
|
+
bun stage.ts "fact text" --context "session info" # add context
|
|
38
|
+
bun stage.ts --list # show pending
|
|
39
|
+
bun stage.ts --count # count pending
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Batch process (REM Sleep)
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
bun rem-sleep.ts # full run
|
|
46
|
+
bun rem-sleep.ts --dry-run # preview decisions
|
|
47
|
+
bun rem-sleep.ts --stats # show staging stats
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Configuration
|
|
51
|
+
|
|
52
|
+
`memory-config.json` (create from `memory-config.example.json`):
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"agent": "sage",
|
|
57
|
+
"hot": { "path": "./MEMORY.md" },
|
|
58
|
+
"warm": { "path": "./warm.jsonl" },
|
|
59
|
+
"cold": {
|
|
60
|
+
"mode": "mcp",
|
|
61
|
+
"endpoint": "http://localhost:8080",
|
|
62
|
+
"userId": "justin"
|
|
63
|
+
},
|
|
64
|
+
"remSleep": {
|
|
65
|
+
"maxColdWrites": 5,
|
|
66
|
+
"dedupThreshold": 0.85,
|
|
67
|
+
"stagingPath": "./rem-staging.jsonl"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Cold mode
|
|
73
|
+
|
|
74
|
+
- `"mcp"` — For Claude Code agents. Uses `claude -p` to access mem0 MCP tools.
|
|
75
|
+
- `"http"` — For OpenClaw/standalone agents. Direct HTTP to mem0 REST API.
|
|
76
|
+
|
|
77
|
+
See `examples/` for pre-made configs.
|
|
78
|
+
|
|
79
|
+
## How REM Sleep works
|
|
80
|
+
|
|
81
|
+
1. During sessions: `bun stage.ts "fact"` → appends to `rem-staging.jsonl` ($0)
|
|
82
|
+
2. Batch runs periodically (cron, launchd, or manual)
|
|
83
|
+
3. Each fact is classified: hot (agent-specific) / warm (shared) / cold (long-tail)
|
|
84
|
+
4. Cold candidates are deduplicated against mem0 (skip if >85% match)
|
|
85
|
+
5. Max 5 cold writes per run (~$0.005 max cost)
|
|
86
|
+
6. Hot facts are printed as suggestions — never auto-written to MEMORY.md
|
|
87
|
+
|
|
88
|
+
## Requirements
|
|
89
|
+
|
|
90
|
+
- [Bun](https://bun.sh) runtime
|
|
91
|
+
- mem0 instance (for cold tier)
|
|
92
|
+
- Claude Code (for MCP mode) or direct mem0 API access (for HTTP mode)
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Memory Module CLI
|
|
4
|
+
*
|
|
5
|
+
* Three-tier memory system (hot/warm/cold) with REM Sleep batch consolidation.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* memory stage "fact text" [--tier hot|warm|cold] [--context "..."]
|
|
9
|
+
* memory stage --list | --count
|
|
10
|
+
* memory sleep [--dry-run] [--stats]
|
|
11
|
+
* memory init [--path ~/.pai/memory]
|
|
12
|
+
* memory config
|
|
13
|
+
* memory --help
|
|
14
|
+
*
|
|
15
|
+
* Install:
|
|
16
|
+
* bun add -g @tensakulabs/memory-module
|
|
17
|
+
* # or: bun build --compile cli.ts --outfile memory
|
|
18
|
+
*
|
|
19
|
+
* Config:
|
|
20
|
+
* memory init # creates config at ~/.pai/memory/
|
|
21
|
+
* MEMORY_CONFIG=./config.json # explicit config path
|
|
22
|
+
* memory stage --config ./c.json # per-command config
|
|
23
|
+
*/
|
|
24
|
+
export {};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __export = (target, all) => {
|
|
5
|
+
for (var name in all)
|
|
6
|
+
__defProp(target, name, {
|
|
7
|
+
get: all[name],
|
|
8
|
+
enumerable: true,
|
|
9
|
+
configurable: true,
|
|
10
|
+
set: (newValue) => all[name] = () => newValue
|
|
11
|
+
});
|
|
12
|
+
};
|
|
13
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
14
|
+
var __require = import.meta.require;
|
|
15
|
+
|
|
16
|
+
// lib.ts
|
|
17
|
+
var exports_lib = {};
|
|
18
|
+
__export(exports_lib, {
|
|
19
|
+
readJsonl: () => readJsonl,
|
|
20
|
+
loadConfig: () => loadConfig,
|
|
21
|
+
generateId: () => generateId,
|
|
22
|
+
coldSearch: () => coldSearch,
|
|
23
|
+
coldAdd: () => coldAdd
|
|
24
|
+
});
|
|
25
|
+
import { readFileSync, existsSync } from "fs";
|
|
26
|
+
import { join, resolve, dirname } from "path";
|
|
27
|
+
var {$ } = globalThis.Bun;
|
|
28
|
+
function loadConfig(explicitPath) {
|
|
29
|
+
const home = process.env.HOME || "~";
|
|
30
|
+
const candidates = [];
|
|
31
|
+
if (explicitPath) {
|
|
32
|
+
candidates.push(resolve(explicitPath));
|
|
33
|
+
}
|
|
34
|
+
if (process.env.MEMORY_CONFIG) {
|
|
35
|
+
candidates.push(resolve(process.env.MEMORY_CONFIG));
|
|
36
|
+
}
|
|
37
|
+
candidates.push(join(home, ".pai", "memory", CONFIG_FILENAME));
|
|
38
|
+
candidates.push(join(dirname(Bun.main), CONFIG_FILENAME));
|
|
39
|
+
candidates.push(resolve(CONFIG_FILENAME));
|
|
40
|
+
const configPath = candidates.find((p) => existsSync(p));
|
|
41
|
+
if (!configPath) {
|
|
42
|
+
console.error(`Config not found. Searched:`);
|
|
43
|
+
for (const c of candidates)
|
|
44
|
+
console.error(` - ${c}`);
|
|
45
|
+
console.error(`
|
|
46
|
+
Run: memory init`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
const raw = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
50
|
+
const configDir = dirname(configPath);
|
|
51
|
+
const config = {
|
|
52
|
+
agent: raw.agent || "unknown",
|
|
53
|
+
hot: {
|
|
54
|
+
path: resolvePath(configDir, raw.hot?.path || "./MEMORY.md")
|
|
55
|
+
},
|
|
56
|
+
warm: {
|
|
57
|
+
path: resolvePath(configDir, raw.warm?.path || "./warm.jsonl"),
|
|
58
|
+
ttlDays: raw.warm?.ttlDays ?? 7
|
|
59
|
+
},
|
|
60
|
+
cold: {
|
|
61
|
+
mode: raw.cold?.mode || "mcp",
|
|
62
|
+
endpoint: raw.cold?.endpoint,
|
|
63
|
+
userId: raw.cold?.userId || "justin"
|
|
64
|
+
},
|
|
65
|
+
remSleep: {
|
|
66
|
+
maxColdWrites: raw.remSleep?.maxColdWrites ?? 5,
|
|
67
|
+
dedupThreshold: raw.remSleep?.dedupThreshold ?? 0.85,
|
|
68
|
+
stagingPath: resolvePath(configDir, raw.remSleep?.stagingPath || "./rem-staging.jsonl")
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
if (config.cold.mode === "http" && !config.cold.endpoint) {
|
|
72
|
+
console.error("cold.mode is 'http' but no cold.endpoint specified.");
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
return { config, configDir };
|
|
76
|
+
}
|
|
77
|
+
function resolvePath(base, p) {
|
|
78
|
+
if (p.startsWith("/") || p.startsWith("~"))
|
|
79
|
+
return p.replace(/^~/, process.env.HOME || "~");
|
|
80
|
+
return resolve(base, p);
|
|
81
|
+
}
|
|
82
|
+
async function coldSearch(config, query) {
|
|
83
|
+
if (config.cold.mode === "mcp") {
|
|
84
|
+
return coldSearchMcp(query, config.cold.userId);
|
|
85
|
+
} else {
|
|
86
|
+
return coldSearchHttp(config.cold.endpoint, query, config.cold.userId);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async function coldAdd(config, fact) {
|
|
90
|
+
if (config.cold.mode === "mcp") {
|
|
91
|
+
return coldAddMcp(fact, config.cold.userId);
|
|
92
|
+
} else {
|
|
93
|
+
return coldAddHttp(config.cold.endpoint, fact, config.cold.userId);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async function coldSearchMcp(query, userId) {
|
|
97
|
+
try {
|
|
98
|
+
const escaped = query.replace(/"/g, "\\\"");
|
|
99
|
+
const prompt = `Search mem0 for this fact and report if it already exists. Use search_memories with query: "${escaped}" and user_id "${userId}". If results exist with high relevance, respond ONLY with: FOUND|<score>|<matching text>. If no close match, respond ONLY with: NOT_FOUND|0|none. Do not explain.`;
|
|
100
|
+
const result = await $`claude -p ${prompt} --allowedTools "mcp__mem0__search_memories" 2>/dev/null`.text();
|
|
101
|
+
const line = result.trim().split(`
|
|
102
|
+
`).pop() || "";
|
|
103
|
+
if (line.startsWith("FOUND|")) {
|
|
104
|
+
const parts = line.split("|");
|
|
105
|
+
return { score: parseFloat(parts[1]) || 0, text: parts[2] || "" };
|
|
106
|
+
}
|
|
107
|
+
return { score: 0, text: "" };
|
|
108
|
+
} catch {
|
|
109
|
+
return { score: 0, text: "" };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function coldAddMcp(fact, userId) {
|
|
113
|
+
try {
|
|
114
|
+
const escaped = fact.replace(/"/g, "\\\"");
|
|
115
|
+
const prompt = `Add this fact to mem0. Use add_memory with text: "${escaped}" and user_id "${userId}". Respond ONLY with: DONE or FAILED.`;
|
|
116
|
+
const result = await $`claude -p ${prompt} --allowedTools "mcp__mem0__add_memory" 2>/dev/null`.text();
|
|
117
|
+
return result.trim().includes("DONE");
|
|
118
|
+
} catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async function coldSearchHttp(endpoint, query, userId) {
|
|
123
|
+
try {
|
|
124
|
+
const res = await fetch(`${endpoint}/v1/memories/search/`, {
|
|
125
|
+
method: "POST",
|
|
126
|
+
headers: { "Content-Type": "application/json" },
|
|
127
|
+
body: JSON.stringify({ query, user_id: userId })
|
|
128
|
+
});
|
|
129
|
+
if (!res.ok)
|
|
130
|
+
return { score: 0, text: "" };
|
|
131
|
+
const data = await res.json();
|
|
132
|
+
const top = data.results?.[0];
|
|
133
|
+
if (top) {
|
|
134
|
+
return { score: top.score ?? 0, text: top.memory ?? "" };
|
|
135
|
+
}
|
|
136
|
+
return { score: 0, text: "" };
|
|
137
|
+
} catch {
|
|
138
|
+
return { score: 0, text: "" };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async function coldAddHttp(endpoint, fact, userId) {
|
|
142
|
+
try {
|
|
143
|
+
const res = await fetch(`${endpoint}/v1/memories/`, {
|
|
144
|
+
method: "POST",
|
|
145
|
+
headers: { "Content-Type": "application/json" },
|
|
146
|
+
body: JSON.stringify({ text: fact, user_id: userId })
|
|
147
|
+
});
|
|
148
|
+
return res.ok;
|
|
149
|
+
} catch {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function generateId() {
|
|
154
|
+
return `rem-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
155
|
+
}
|
|
156
|
+
function readJsonl(path) {
|
|
157
|
+
if (!existsSync(path))
|
|
158
|
+
return [];
|
|
159
|
+
return readFileSync(path, "utf-8").trim().split(`
|
|
160
|
+
`).filter(Boolean).map((line) => JSON.parse(line));
|
|
161
|
+
}
|
|
162
|
+
var CONFIG_FILENAME = "memory-config.json";
|
|
163
|
+
var init_lib = () => {};
|
|
164
|
+
|
|
165
|
+
// stage.ts
|
|
166
|
+
var exports_stage = {};
|
|
167
|
+
__export(exports_stage, {
|
|
168
|
+
run: () => run
|
|
169
|
+
});
|
|
170
|
+
import { appendFileSync } from "fs";
|
|
171
|
+
function parseArgs(args) {
|
|
172
|
+
if (args.includes("--list"))
|
|
173
|
+
return { command: "list" };
|
|
174
|
+
if (args.includes("--count"))
|
|
175
|
+
return { command: "count" };
|
|
176
|
+
const fact = args.find((a) => !a.startsWith("--"));
|
|
177
|
+
if (!fact) {
|
|
178
|
+
console.error('Usage: memory stage "fact text" [--context "..."] [--tier hot|warm|cold]');
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
let context = "";
|
|
182
|
+
const ctxIdx = args.indexOf("--context");
|
|
183
|
+
if (ctxIdx !== -1 && args[ctxIdx + 1])
|
|
184
|
+
context = args[ctxIdx + 1];
|
|
185
|
+
let tierHint = "auto";
|
|
186
|
+
const tierIdx = args.indexOf("--tier");
|
|
187
|
+
if (tierIdx !== -1 && args[tierIdx + 1]) {
|
|
188
|
+
const t = args[tierIdx + 1];
|
|
189
|
+
if (t === "hot" || t === "warm" || t === "cold")
|
|
190
|
+
tierHint = t;
|
|
191
|
+
}
|
|
192
|
+
return { command: "stage", fact, context, tierHint };
|
|
193
|
+
}
|
|
194
|
+
async function run(args) {
|
|
195
|
+
const { config } = loadConfig();
|
|
196
|
+
const STAGING = config.remSleep.stagingPath;
|
|
197
|
+
const parsed = parseArgs(args);
|
|
198
|
+
if (parsed.command === "list") {
|
|
199
|
+
const entries = readJsonl(STAGING).filter((e) => !e.processed);
|
|
200
|
+
if (entries.length === 0) {
|
|
201
|
+
console.log("No staged facts.");
|
|
202
|
+
} else {
|
|
203
|
+
for (const e of entries) {
|
|
204
|
+
const hint = e.tier_hint === "auto" ? "" : ` [${e.tier_hint}]`;
|
|
205
|
+
console.log(`${e.id}: ${e.fact}${hint}`);
|
|
206
|
+
}
|
|
207
|
+
console.log(`
|
|
208
|
+
${entries.length} fact(s) pending.`);
|
|
209
|
+
}
|
|
210
|
+
} else if (parsed.command === "count") {
|
|
211
|
+
console.log(readJsonl(STAGING).filter((e) => !e.processed).length);
|
|
212
|
+
} else {
|
|
213
|
+
const entry = {
|
|
214
|
+
id: generateId(),
|
|
215
|
+
fact: parsed.fact,
|
|
216
|
+
timestamp: new Date().toISOString(),
|
|
217
|
+
context: parsed.context || "",
|
|
218
|
+
tier_hint: parsed.tierHint || "auto",
|
|
219
|
+
processed: false
|
|
220
|
+
};
|
|
221
|
+
appendFileSync(STAGING, JSON.stringify(entry) + `
|
|
222
|
+
`);
|
|
223
|
+
console.log(`Staged: ${entry.id}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
var init_stage = __esm(() => {
|
|
227
|
+
init_lib();
|
|
228
|
+
if (import.meta.main) {
|
|
229
|
+
run(process.argv.slice(2));
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// rem-sleep.ts
|
|
234
|
+
var exports_rem_sleep = {};
|
|
235
|
+
__export(exports_rem_sleep, {
|
|
236
|
+
run: () => run2
|
|
237
|
+
});
|
|
238
|
+
import { writeFileSync, appendFileSync as appendFileSync2 } from "fs";
|
|
239
|
+
import { join as join2, dirname as dirname2 } from "path";
|
|
240
|
+
function classify(fact) {
|
|
241
|
+
if (fact.tier_hint !== "auto") {
|
|
242
|
+
return { ...fact, tier: fact.tier_hint, reason: `explicit: ${fact.tier_hint}` };
|
|
243
|
+
}
|
|
244
|
+
const text = `${fact.fact} ${fact.context}`;
|
|
245
|
+
const hot = HOT_SIGNALS.reduce((s, r) => s + (r.test(text) ? 1 : 0), 0);
|
|
246
|
+
const warm = WARM_SIGNALS.reduce((s, r) => s + (r.test(text) ? 1 : 0), 0);
|
|
247
|
+
const cold = COLD_SIGNALS.reduce((s, r) => s + (r.test(text) ? 1 : 0), 0);
|
|
248
|
+
if (hot > warm && hot > cold)
|
|
249
|
+
return { ...fact, tier: "hot", reason: `hot signals: ${hot}` };
|
|
250
|
+
if (warm > cold)
|
|
251
|
+
return { ...fact, tier: "warm", reason: `warm signals: ${warm}` };
|
|
252
|
+
if (cold > 0)
|
|
253
|
+
return { ...fact, tier: "cold", reason: `cold signals: ${cold}` };
|
|
254
|
+
return { ...fact, tier: "warm", reason: "default (no strong signals)" };
|
|
255
|
+
}
|
|
256
|
+
function appendToWarm(fact) {
|
|
257
|
+
const entry = {
|
|
258
|
+
id: fact.id,
|
|
259
|
+
fact: fact.fact,
|
|
260
|
+
origin: "new",
|
|
261
|
+
created: fact.timestamp,
|
|
262
|
+
last_access: new Date().toISOString(),
|
|
263
|
+
access_count: 1,
|
|
264
|
+
context: fact.context
|
|
265
|
+
};
|
|
266
|
+
appendFileSync2(WARM, JSON.stringify(entry) + `
|
|
267
|
+
`);
|
|
268
|
+
}
|
|
269
|
+
async function run2(args) {
|
|
270
|
+
const dryRun = args.includes("--dry-run");
|
|
271
|
+
const statsOnly = args.includes("--stats");
|
|
272
|
+
const allEntries = readJsonl(STAGING);
|
|
273
|
+
const pending = allEntries.filter((e) => !e.processed);
|
|
274
|
+
if (statsOnly) {
|
|
275
|
+
console.log(`Agent: ${config.agent}`);
|
|
276
|
+
console.log(`Cold mode: ${config.cold.mode}`);
|
|
277
|
+
console.log(`Total entries: ${allEntries.length}`);
|
|
278
|
+
console.log(`Pending: ${pending.length}`);
|
|
279
|
+
console.log(`Processed: ${allEntries.length - pending.length}`);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (pending.length === 0) {
|
|
283
|
+
console.log("No pending facts. Staging is clean.");
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
console.log(`
|
|
287
|
+
\uD83C\uDF19 REM Sleep [${config.agent}] \u2014 Processing ${pending.length} fact(s)${dryRun ? " [DRY RUN]" : ""}`);
|
|
288
|
+
console.log(` Cold mode: ${config.cold.mode} | Max writes: ${config.remSleep.maxColdWrites} | Dedup: ${config.remSleep.dedupThreshold}
|
|
289
|
+
`);
|
|
290
|
+
const classified = pending.map(classify);
|
|
291
|
+
const hot = classified.filter((f) => f.tier === "hot");
|
|
292
|
+
const warm = classified.filter((f) => f.tier === "warm");
|
|
293
|
+
const cold = classified.filter((f) => f.tier === "cold");
|
|
294
|
+
console.log(`\uD83D\uDCCA Classification: ${hot.length} hot, ${warm.length} warm, ${cold.length} cold
|
|
295
|
+
`);
|
|
296
|
+
if (hot.length > 0) {
|
|
297
|
+
console.log("\uD83D\uDD25 HOT \u2014 Suggested MEMORY.md additions (review manually):");
|
|
298
|
+
console.log(` Target: ${config.hot.path}`);
|
|
299
|
+
for (const f of hot)
|
|
300
|
+
console.log(` - ${f.fact} (${f.reason})`);
|
|
301
|
+
console.log();
|
|
302
|
+
}
|
|
303
|
+
if (warm.length > 0) {
|
|
304
|
+
console.log("\u2668\uFE0F WARM \u2014 Cross-agent shared facts:");
|
|
305
|
+
for (const f of warm) {
|
|
306
|
+
console.log(` - ${f.fact}`);
|
|
307
|
+
if (!dryRun)
|
|
308
|
+
appendToWarm(f);
|
|
309
|
+
}
|
|
310
|
+
console.log(dryRun ? ` \u2192 Would write to ${WARM}
|
|
311
|
+
` : ` \u2192 Written to ${WARM}
|
|
312
|
+
`);
|
|
313
|
+
}
|
|
314
|
+
let coldWritten = 0;
|
|
315
|
+
let coldSkipped = 0;
|
|
316
|
+
let coldCapped = 0;
|
|
317
|
+
if (cold.length > 0) {
|
|
318
|
+
console.log("\uD83E\uDDCA COLD \u2014 Long-tail facts (mem0 candidates):");
|
|
319
|
+
for (const f of cold) {
|
|
320
|
+
if (coldWritten >= config.remSleep.maxColdWrites) {
|
|
321
|
+
console.log(` \u23F8 ${f.fact} \u2192 DEFERRED (cap reached)`);
|
|
322
|
+
coldCapped++;
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
console.log(` - ${f.fact}`);
|
|
326
|
+
if (dryRun) {
|
|
327
|
+
console.log(` \u2192 Would search mem0 (${config.cold.mode}) then add if new`);
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
const match = await coldSearch(config, f.fact);
|
|
331
|
+
if (match.score >= config.remSleep.dedupThreshold) {
|
|
332
|
+
console.log(` \u2192 SKIP (dup: ${(match.score * 100).toFixed(0)}% \u2014 "${match.text.slice(0, 60)}...")`);
|
|
333
|
+
coldSkipped++;
|
|
334
|
+
} else {
|
|
335
|
+
const ok = await coldAdd(config, f.fact);
|
|
336
|
+
console.log(ok ? ` \u2192 ADDED to mem0` : ` \u2192 FAILED`);
|
|
337
|
+
if (ok)
|
|
338
|
+
coldWritten++;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
console.log();
|
|
342
|
+
}
|
|
343
|
+
if (!dryRun) {
|
|
344
|
+
const cappedIds = new Set(cold.slice(config.remSleep.maxColdWrites).map((f) => f.id));
|
|
345
|
+
const updated = allEntries.map((e) => !e.processed && !cappedIds.has(e.id) ? { ...e, processed: true } : e);
|
|
346
|
+
writeFileSync(STAGING, updated.map((e) => JSON.stringify(e)).join(`
|
|
347
|
+
`) + `
|
|
348
|
+
`);
|
|
349
|
+
}
|
|
350
|
+
const report = {
|
|
351
|
+
timestamp: new Date().toISOString(),
|
|
352
|
+
agent: config.agent,
|
|
353
|
+
coldMode: config.cold.mode,
|
|
354
|
+
dryRun,
|
|
355
|
+
staged: pending.length,
|
|
356
|
+
classified: { hot: hot.length, warm: warm.length, cold: cold.length },
|
|
357
|
+
written: { cold: coldWritten, warm: dryRun ? 0 : warm.length, hotSuggested: hot.length },
|
|
358
|
+
skipped: coldSkipped,
|
|
359
|
+
deferred: coldCapped,
|
|
360
|
+
cost: `~$${(coldWritten * 0.001).toFixed(4)}`
|
|
361
|
+
};
|
|
362
|
+
if (!dryRun)
|
|
363
|
+
writeFileSync(REPORT, JSON.stringify(report, null, 2));
|
|
364
|
+
console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
|
|
365
|
+
console.log(`\u2705 REM Sleep [${config.agent}] complete${dryRun ? " (dry run)" : ""}`);
|
|
366
|
+
console.log(` Hot: ${hot.length} suggested | Warm: ${dryRun ? 0 : warm.length} written | Cold: ${coldWritten} added, ${coldSkipped} dup, ${coldCapped} deferred`);
|
|
367
|
+
console.log(` Cost: ${report.cost}`);
|
|
368
|
+
}
|
|
369
|
+
var config, configDir, STAGING, WARM, REPORT, HOT_SIGNALS, WARM_SIGNALS, COLD_SIGNALS;
|
|
370
|
+
var init_rem_sleep = __esm(() => {
|
|
371
|
+
init_lib();
|
|
372
|
+
({ config, configDir } = loadConfig());
|
|
373
|
+
STAGING = config.remSleep.stagingPath;
|
|
374
|
+
WARM = config.warm.path;
|
|
375
|
+
REPORT = join2(dirname2(STAGING), "last-run.json");
|
|
376
|
+
HOT_SIGNALS = [
|
|
377
|
+
/\bmy (model|config|cron|path|key|token|port|setup)\b/i,
|
|
378
|
+
/\b(localhost|127\.0\.0\.1|~\/|\/home\/)/i,
|
|
379
|
+
/\b(launchd|plist|systemd|crontab)\b/i,
|
|
380
|
+
/\b(heartbeat) model\b/i
|
|
381
|
+
];
|
|
382
|
+
WARM_SIGNALS = [
|
|
383
|
+
/\b(we|our|us|together|both agents|cross-agent)\b/i,
|
|
384
|
+
/\b(prefers|wants|decided|likes|agreed)\b/i,
|
|
385
|
+
/\b(strategy|approach|decision|convention)\b/i,
|
|
386
|
+
/\b(project|milestone|roadmap|plan)\b/i
|
|
387
|
+
];
|
|
388
|
+
COLD_SIGNALS = [
|
|
389
|
+
/\b(PR #\d+|issue #\d+|bug #\d+)\b/i,
|
|
390
|
+
/\b(benchmark|pricing|per M tokens)\b/i,
|
|
391
|
+
/\b(workaround|root cause|known issue)\b/i,
|
|
392
|
+
/\b(research|found|discovered|learned)\b/i
|
|
393
|
+
];
|
|
394
|
+
if (import.meta.main) {
|
|
395
|
+
run2(process.argv.slice(2)).catch((err) => {
|
|
396
|
+
console.error("REM Sleep failed:", err);
|
|
397
|
+
process.exit(1);
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
// cli.ts
|
|
403
|
+
import { existsSync as existsSync2, mkdirSync, copyFileSync } from "fs";
|
|
404
|
+
import { join as join3, dirname as dirname3, resolve as resolve2 } from "path";
|
|
405
|
+
var VERSION = "0.1.0";
|
|
406
|
+
var HELP = `
|
|
407
|
+
memory v${VERSION} \u2014 Three-tier memory with REM Sleep
|
|
408
|
+
|
|
409
|
+
Commands:
|
|
410
|
+
stage "fact" Stage a fact for batch processing ($0)
|
|
411
|
+
--tier <t> Force tier: hot, warm, or cold (default: auto)
|
|
412
|
+
--context "c" Add context to the staged fact
|
|
413
|
+
--list List all pending staged facts
|
|
414
|
+
--count Count pending staged facts
|
|
415
|
+
|
|
416
|
+
sleep Run REM Sleep batch processor
|
|
417
|
+
--dry-run Preview decisions without executing
|
|
418
|
+
--stats Show staging statistics only
|
|
419
|
+
|
|
420
|
+
init Create config at ~/.pai/memory/
|
|
421
|
+
--path <dir> Custom config directory
|
|
422
|
+
|
|
423
|
+
config Show current config and paths
|
|
424
|
+
|
|
425
|
+
--help Show this help
|
|
426
|
+
--version Show version
|
|
427
|
+
|
|
428
|
+
Config search order:
|
|
429
|
+
1. --config flag
|
|
430
|
+
2. MEMORY_CONFIG env var
|
|
431
|
+
3. ~/.pai/memory/memory-config.json
|
|
432
|
+
4. ./memory-config.json
|
|
433
|
+
`.trim();
|
|
434
|
+
var args = process.argv.slice(2);
|
|
435
|
+
var command = args[0];
|
|
436
|
+
if (!command || command === "--help" || command === "-h") {
|
|
437
|
+
console.log(HELP);
|
|
438
|
+
process.exit(0);
|
|
439
|
+
}
|
|
440
|
+
if (command === "--version" || command === "-v") {
|
|
441
|
+
console.log(`memory v${VERSION}`);
|
|
442
|
+
process.exit(0);
|
|
443
|
+
}
|
|
444
|
+
var configFlag;
|
|
445
|
+
var configIdx = args.indexOf("--config");
|
|
446
|
+
if (configIdx !== -1 && args[configIdx + 1]) {
|
|
447
|
+
configFlag = args[configIdx + 1];
|
|
448
|
+
args.splice(configIdx, 2);
|
|
449
|
+
}
|
|
450
|
+
if (configFlag)
|
|
451
|
+
process.env.MEMORY_CONFIG = resolve2(configFlag);
|
|
452
|
+
switch (command) {
|
|
453
|
+
case "stage": {
|
|
454
|
+
const subArgs = args.slice(1);
|
|
455
|
+
const mod = await Promise.resolve().then(() => (init_stage(), exports_stage));
|
|
456
|
+
if (mod.run)
|
|
457
|
+
await mod.run(subArgs);
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
460
|
+
case "sleep": {
|
|
461
|
+
const subArgs = args.slice(1);
|
|
462
|
+
const mod = await Promise.resolve().then(() => (init_rem_sleep(), exports_rem_sleep));
|
|
463
|
+
if (mod.run)
|
|
464
|
+
await mod.run(subArgs);
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
467
|
+
case "init": {
|
|
468
|
+
const home = process.env.HOME || "~";
|
|
469
|
+
let targetDir = join3(home, ".pai", "memory");
|
|
470
|
+
const pathIdx = args.indexOf("--path");
|
|
471
|
+
if (pathIdx !== -1 && args[pathIdx + 1])
|
|
472
|
+
targetDir = resolve2(args[pathIdx + 1]);
|
|
473
|
+
const configDest = join3(targetDir, "memory-config.json");
|
|
474
|
+
const exampleSrc = join3(dirname3(Bun.main), "memory-config.example.json");
|
|
475
|
+
if (existsSync2(configDest)) {
|
|
476
|
+
console.log(`Config already exists: ${configDest}`);
|
|
477
|
+
console.log("Edit it directly or delete to reinitialize.");
|
|
478
|
+
process.exit(0);
|
|
479
|
+
}
|
|
480
|
+
mkdirSync(targetDir, { recursive: true });
|
|
481
|
+
if (existsSync2(exampleSrc)) {
|
|
482
|
+
copyFileSync(exampleSrc, configDest);
|
|
483
|
+
console.log(`Created: ${configDest}`);
|
|
484
|
+
console.log("Edit this file to configure your agent.");
|
|
485
|
+
} else {
|
|
486
|
+
const minimal = JSON.stringify({
|
|
487
|
+
agent: "my-agent",
|
|
488
|
+
hot: { path: "./MEMORY.md" },
|
|
489
|
+
warm: { path: "./warm.jsonl" },
|
|
490
|
+
cold: { mode: "mcp", endpoint: "http://localhost:8080", userId: "your-user-id" },
|
|
491
|
+
remSleep: { maxColdWrites: 5, dedupThreshold: 0.85, stagingPath: "./rem-staging.jsonl" }
|
|
492
|
+
}, null, 2);
|
|
493
|
+
__require("fs").writeFileSync(configDest, minimal + `
|
|
494
|
+
`);
|
|
495
|
+
console.log(`Created: ${configDest}`);
|
|
496
|
+
console.log("Edit this file to configure your agent.");
|
|
497
|
+
}
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
case "config": {
|
|
501
|
+
const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_lib(), exports_lib));
|
|
502
|
+
try {
|
|
503
|
+
const { config: config2, configDir: configDir2 } = loadConfig2(configFlag);
|
|
504
|
+
console.log(`Agent: ${config2.agent}`);
|
|
505
|
+
console.log(`Config dir: ${configDir2}`);
|
|
506
|
+
console.log(`Hot: ${config2.hot.path}`);
|
|
507
|
+
console.log(`Warm: ${config2.warm.path} (TTL: ${config2.warm.ttlDays}d)`);
|
|
508
|
+
console.log(`Cold: ${config2.cold.mode} \u2192 ${config2.cold.endpoint || "MCP"} (user: ${config2.cold.userId})`);
|
|
509
|
+
console.log(`Staging: ${config2.remSleep.stagingPath}`);
|
|
510
|
+
console.log(`Max cold writes: ${config2.remSleep.maxColdWrites}`);
|
|
511
|
+
console.log(`Dedup threshold: ${config2.remSleep.dedupThreshold}`);
|
|
512
|
+
} catch {
|
|
513
|
+
console.error("No config found. Run: memory init");
|
|
514
|
+
}
|
|
515
|
+
break;
|
|
516
|
+
}
|
|
517
|
+
default:
|
|
518
|
+
console.error(`Unknown command: ${command}`);
|
|
519
|
+
console.error("Run: memory --help");
|
|
520
|
+
process.exit(1);
|
|
521
|
+
}
|
package/dist/lib.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory Module — Shared Library
|
|
3
|
+
*
|
|
4
|
+
* Config loader, types, and cold tier adapters (MCP vs HTTP).
|
|
5
|
+
* All scripts import from here. No hardcoded paths anywhere.
|
|
6
|
+
*/
|
|
7
|
+
export interface MemoryConfig {
|
|
8
|
+
agent: string;
|
|
9
|
+
hot: {
|
|
10
|
+
path: string;
|
|
11
|
+
};
|
|
12
|
+
warm: {
|
|
13
|
+
path: string;
|
|
14
|
+
ttlDays: number;
|
|
15
|
+
};
|
|
16
|
+
cold: {
|
|
17
|
+
mode: "mcp" | "http";
|
|
18
|
+
endpoint?: string;
|
|
19
|
+
userId: string;
|
|
20
|
+
};
|
|
21
|
+
remSleep: {
|
|
22
|
+
maxColdWrites: number;
|
|
23
|
+
dedupThreshold: number;
|
|
24
|
+
stagingPath: string;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export interface StagedFact {
|
|
28
|
+
id: string;
|
|
29
|
+
fact: string;
|
|
30
|
+
timestamp: string;
|
|
31
|
+
context: string;
|
|
32
|
+
tier_hint: "auto" | "hot" | "warm" | "cold";
|
|
33
|
+
processed: boolean;
|
|
34
|
+
}
|
|
35
|
+
export interface ColdSearchResult {
|
|
36
|
+
score: number;
|
|
37
|
+
text: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Find and load config. Search order:
|
|
41
|
+
* 1. Explicit path (--config flag)
|
|
42
|
+
* 2. MEMORY_CONFIG env var
|
|
43
|
+
* 3. ~/.pai/memory/memory-config.json (canonical home)
|
|
44
|
+
* 4. Same directory as the calling script
|
|
45
|
+
* 5. ./memory-config.json (current working directory)
|
|
46
|
+
*/
|
|
47
|
+
export declare function loadConfig(explicitPath?: string): {
|
|
48
|
+
config: MemoryConfig;
|
|
49
|
+
configDir: string;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Search mem0 for duplicate facts.
|
|
53
|
+
* Uses MCP (via claude -p) or direct HTTP based on config.
|
|
54
|
+
*/
|
|
55
|
+
export declare function coldSearch(config: MemoryConfig, query: string): Promise<ColdSearchResult>;
|
|
56
|
+
/**
|
|
57
|
+
* Add a fact to mem0.
|
|
58
|
+
* Uses MCP (via claude -p) or direct HTTP based on config.
|
|
59
|
+
*/
|
|
60
|
+
export declare function coldAdd(config: MemoryConfig, fact: string): Promise<boolean>;
|
|
61
|
+
export declare function generateId(): string;
|
|
62
|
+
export declare function readJsonl<T>(path: string): T[];
|