memory-forge 0.3.13 → 0.4.1
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/dist/auto/index.d.ts +5 -3
- package/dist/auto/index.js +98 -57
- package/dist/auto/index.js.map +1 -1
- package/dist/embedding.d.ts +0 -2
- package/dist/embedding.js +13 -12
- package/dist/embedding.js.map +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +343 -69
- package/dist/index.js.map +1 -1
- package/dist/migrate/import.js +1 -14
- package/dist/migrate/import.js.map +1 -1
- package/dist/pro.d.ts +32 -0
- package/dist/pro.js +243 -28
- package/dist/pro.js.map +1 -1
- package/dist/scenario-test.d.ts +1 -0
- package/dist/scenario-test.js +177 -0
- package/dist/scenario-test.js.map +1 -0
- package/dist/setup.js +10 -4
- package/dist/setup.js.map +1 -1
- package/dist/storage/local.js +30 -4
- package/dist/storage/local.js.map +1 -1
- package/dist/storage/shelby.d.ts +23 -2
- package/dist/storage/shelby.js +123 -12
- package/dist/storage/shelby.js.map +1 -1
- package/dist/store.d.ts +11 -1
- package/dist/store.js +82 -9
- package/dist/store.js.map +1 -1
- package/dist/transcript.js +3 -1
- package/dist/transcript.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,29 +2,28 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* MemoryForge — AI Agent 持久记忆引擎 (MVP)
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* 9 MCP tools + 5 auto-engines + Pro Shelby cloud sync.
|
|
6
|
+
* Embedding: Transformers.js (23MB, in-process).
|
|
7
|
+
* Storage: Free = local Markdown; Pro = Shelby cloud.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* Quick start:
|
|
10
|
+
* npx memory-forge setup
|
|
11
11
|
*/
|
|
12
12
|
import { randomUUID } from "node:crypto";
|
|
13
13
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
14
14
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
15
15
|
import { z } from "zod";
|
|
16
|
-
import { MemoryStore } from "./store.js";
|
|
16
|
+
import { MemoryStore, safeTruncate } from "./store.js";
|
|
17
17
|
import { embed, preload } from "./embedding.js";
|
|
18
18
|
import { saveMemory, loadAllMemories, deleteMemoryFile, cleanupTombstones } from "./storage/local.js";
|
|
19
|
-
import { uploadMemory } from "./storage/shelby.js";
|
|
19
|
+
import { uploadMemory, deleteBlob, getBlobName, getShelbyConfig } from "./storage/shelby.js";
|
|
20
20
|
import { autoName, autoMerge, autoPriority, autoDecay, generateContextSummary } from "./auto/index.js";
|
|
21
21
|
import { setup } from "./setup.js";
|
|
22
|
-
import { pro,
|
|
22
|
+
import { pro, proStatus, proAutoActivate } from "./pro.js";
|
|
23
23
|
import { cliCaptureTranscript, captureTranscript } from "./transcript.js";
|
|
24
24
|
import { readFileSync } from "node:fs";
|
|
25
25
|
import { fileURLToPath } from "node:url";
|
|
26
|
-
import { dirname, join } from "node:path";
|
|
27
|
-
import * as path from "node:path";
|
|
26
|
+
import { basename, dirname, join } from "node:path";
|
|
28
27
|
const __filename = fileURLToPath(import.meta.url);
|
|
29
28
|
const __dirname = dirname(__filename);
|
|
30
29
|
const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
|
|
@@ -53,9 +52,151 @@ else if (cmd === "setup") {
|
|
|
53
52
|
// Don't start MCP server — rely on setup's process.exit
|
|
54
53
|
}
|
|
55
54
|
else if (cmd === "pro") {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
const sub = process.argv[3];
|
|
56
|
+
if (sub === "status") {
|
|
57
|
+
(async () => {
|
|
58
|
+
const { getBalances, getStorageUsage, initShelby, getShelbyConfig } = await import("./storage/shelby.js");
|
|
59
|
+
const s = proStatus();
|
|
60
|
+
if (!s.active) {
|
|
61
|
+
console.log("Pro: not active");
|
|
62
|
+
console.log(" Set SHELBY_API_KEY and restart your session to auto-activate.");
|
|
63
|
+
console.log(" Or run: SHELBY_API_KEY=\"...\" memory-forge pro");
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
const cfg = getShelbyConfig();
|
|
67
|
+
let balances = null;
|
|
68
|
+
let storage = null;
|
|
69
|
+
if (cfg.apiKey && cfg.accountAddress) {
|
|
70
|
+
const { readFileSync } = await import("node:fs");
|
|
71
|
+
const { join } = await import("node:path");
|
|
72
|
+
const { homedir } = await import("node:os");
|
|
73
|
+
let profile = null;
|
|
74
|
+
try {
|
|
75
|
+
profile = JSON.parse(readFileSync(join(homedir(), ".memory-forge", "pro.json"), "utf-8"));
|
|
76
|
+
}
|
|
77
|
+
catch { }
|
|
78
|
+
if (profile?.privateKey) {
|
|
79
|
+
initShelby(cfg.apiKey, profile.privateKey);
|
|
80
|
+
balances = await getBalances();
|
|
81
|
+
storage = await getStorageUsage();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const keyStatus = s.apiKeyValid === false ? "❌ invalid"
|
|
85
|
+
: s.apiKeyValid ? "✅ valid"
|
|
86
|
+
: "⚠️ not set (sync paused)";
|
|
87
|
+
console.log(`Pro: ${s.apiKeyValid ? "active ✅" : "paused ⚠️"}`);
|
|
88
|
+
console.log("");
|
|
89
|
+
console.log(" ── Account ──");
|
|
90
|
+
console.log(` Address: ${s.address}`);
|
|
91
|
+
console.log(` API key: ${keyStatus}`);
|
|
92
|
+
if (balances) {
|
|
93
|
+
const aptVal = parseFloat(balances.apt);
|
|
94
|
+
const usdVal = parseFloat(balances.shelbyUsd);
|
|
95
|
+
const aptWarn = aptVal < 0.01 ? " ⚠️ low (gas may fail)" : "";
|
|
96
|
+
const usdWarn = usdVal < 1.0 ? " ⚠️ low (storage uploads may fail)" : "";
|
|
97
|
+
console.log(` APT balance: ${balances.apt}${aptWarn}`);
|
|
98
|
+
console.log(` ShelbyUSD balance: ${balances.shelbyUsd}${usdWarn}`);
|
|
99
|
+
}
|
|
100
|
+
else if (cfg.apiKey) {
|
|
101
|
+
console.log(` Balances: (query failed — network or unfunded)`);
|
|
102
|
+
}
|
|
103
|
+
console.log("");
|
|
104
|
+
console.log(" ── Storage ──");
|
|
105
|
+
console.log(` Local memories: ${s.localCount}`);
|
|
106
|
+
if (storage) {
|
|
107
|
+
const kb = (storage.totalBytes / 1024).toFixed(1);
|
|
108
|
+
console.log(` Shelby blobs: ${storage.blobCount} (${kb} KB)`);
|
|
109
|
+
}
|
|
110
|
+
else if (cfg.apiKey) {
|
|
111
|
+
console.log(` Shelby usage: (query failed)`);
|
|
112
|
+
}
|
|
113
|
+
console.log("");
|
|
114
|
+
console.log(" ── Sync stats ──");
|
|
115
|
+
console.log(` Total uploaded: ${s.totalUploaded}`);
|
|
116
|
+
console.log(` Total downloaded: ${s.totalDownloaded}`);
|
|
117
|
+
console.log(` Total failed: ${s.totalFailed || "—"}`);
|
|
118
|
+
console.log(` Total conflicts: ${s.totalConflicts || "—"}`);
|
|
119
|
+
console.log(` Last sync: ${s.lastSync || "never"}`);
|
|
120
|
+
if (s.syncHistory && s.syncHistory.length > 0) {
|
|
121
|
+
console.log(` Recent syncs:`);
|
|
122
|
+
for (const entry of s.syncHistory.slice(-5).reverse()) {
|
|
123
|
+
const parts = [];
|
|
124
|
+
if (entry.up > 0)
|
|
125
|
+
parts.push(`↑${entry.up}`);
|
|
126
|
+
if (entry.down > 0)
|
|
127
|
+
parts.push(`↓${entry.down}`);
|
|
128
|
+
if (entry.failed > 0)
|
|
129
|
+
parts.push(`✗${entry.failed}`);
|
|
130
|
+
if (entry.conflicts && entry.conflicts > 0)
|
|
131
|
+
parts.push(`⚡${entry.conflicts}`);
|
|
132
|
+
console.log(` ${entry.time.slice(0, 19).replace("T", " ")} ${parts.join(" ") || "—"}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
process.exit(0);
|
|
137
|
+
})();
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
pro()
|
|
141
|
+
.then(() => process.exit(process.exitCode ?? 0))
|
|
142
|
+
.catch((err) => { console.error(err); process.exit(1); });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
else if (cmd === "list") {
|
|
146
|
+
// CLI: memory-forge list [category]
|
|
147
|
+
const cat = process.argv[3];
|
|
148
|
+
const s = new MemoryStore();
|
|
149
|
+
for (const m of loadAllMemories())
|
|
150
|
+
s.add(m);
|
|
151
|
+
let memories = s.list({ limit: 100, offset: 0 });
|
|
152
|
+
if (cat)
|
|
153
|
+
memories = memories.filter((m) => m.category === cat);
|
|
154
|
+
if (memories.length === 0) {
|
|
155
|
+
console.log(cat ? `No memories in category "${cat}".` : "No memories yet. Use memory_store to create one.");
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
console.log(`${memories.length} memories${cat ? ` (category: ${cat})` : ""}:`);
|
|
159
|
+
for (const m of memories) {
|
|
160
|
+
const date = new Date(m.created_at).toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
161
|
+
console.log(` ${m.id.slice(0, 8)} ${date} [${m.category}] ${m.name}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
process.exit(0);
|
|
165
|
+
}
|
|
166
|
+
else if (cmd === "search") {
|
|
167
|
+
// CLI: memory-forge search <query>
|
|
168
|
+
const query = process.argv.slice(3).join(" ");
|
|
169
|
+
if (!query) {
|
|
170
|
+
console.log("Usage: memory-forge search <query>");
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
const s = new MemoryStore();
|
|
174
|
+
for (const m of loadAllMemories())
|
|
175
|
+
s.add(m);
|
|
176
|
+
const results = s.search(query, { limit: 10, minSimilarity: 0 });
|
|
177
|
+
if (results.length === 0) {
|
|
178
|
+
console.log(`No memories matching "${query}".`);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
console.log(`${results.length} results for "${query}":`);
|
|
182
|
+
for (const r of results) {
|
|
183
|
+
console.log(` ${r.id.slice(0, 8)} [${r.category}] ${r.name}`);
|
|
184
|
+
console.log(` ${safeTruncate(r.content, 120)}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
process.exit(0);
|
|
188
|
+
}
|
|
189
|
+
else if (cmd === "stats") {
|
|
190
|
+
// CLI: memory-forge stats
|
|
191
|
+
const s = new MemoryStore();
|
|
192
|
+
for (const m of loadAllMemories())
|
|
193
|
+
s.add(m);
|
|
194
|
+
const st = s.stats();
|
|
195
|
+
console.log(`Total: ${st.total} | Accesses: ${st.total_accesses} | Oldest: ${st.oldest ?? "—"} | Newest: ${st.newest ?? "—"}`);
|
|
196
|
+
console.log("Categories:", Object.entries(st.categories).map(([k, v]) => `${k}(${v})`).join(" "));
|
|
197
|
+
if (st.top_tags.length)
|
|
198
|
+
console.log("Top tags:", st.top_tags.map(([t, n]) => `${t}(${n})`).join(" "));
|
|
199
|
+
process.exit(0);
|
|
59
200
|
}
|
|
60
201
|
else if (cmd === "capture-transcript") {
|
|
61
202
|
cliCaptureTranscript();
|
|
@@ -72,7 +213,7 @@ else if (cmd === "hook") {
|
|
|
72
213
|
if (stdinData) {
|
|
73
214
|
const hookInput = JSON.parse(stdinData);
|
|
74
215
|
if (hookInput.cwd) {
|
|
75
|
-
projectSlug =
|
|
216
|
+
projectSlug = basename(hookInput.cwd);
|
|
76
217
|
// Only add project note if it differs from default (avoids noise for home dir)
|
|
77
218
|
projectContextNote = `\nCurrent project: ${projectSlug}`;
|
|
78
219
|
}
|
|
@@ -86,19 +227,38 @@ else if (cmd === "hook") {
|
|
|
86
227
|
s.add(m);
|
|
87
228
|
const summary = generateContextSummary(s, 5);
|
|
88
229
|
const memoryCount = s.size();
|
|
230
|
+
// Pro status for session-start visibility
|
|
231
|
+
let proNote = "";
|
|
232
|
+
const homeDir = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
|
|
233
|
+
const profilePath = join(homeDir, ".memory-forge", "pro.json");
|
|
234
|
+
if (fs.existsSync(profilePath) && (process.env.SHELBY_API_KEY || getShelbyConfig().apiKey)) {
|
|
235
|
+
try {
|
|
236
|
+
const profile = JSON.parse(fs.readFileSync(profilePath, "utf-8"));
|
|
237
|
+
if (profile.address) {
|
|
238
|
+
const localCount = loadAllMemories().length;
|
|
239
|
+
const syncAge = profile.lastSync
|
|
240
|
+
? Math.round((Date.now() - new Date(profile.lastSync).getTime()) / 60000)
|
|
241
|
+
: null;
|
|
242
|
+
const ageStr = syncAge === null ? "" : syncAge < 1 ? " (just now)" : syncAge < 60 ? ` (${syncAge}m ago)` : ` (${Math.round(syncAge / 60)}h ago)`;
|
|
243
|
+
proNote = ` | Pro: ${localCount} memories synced${ageStr}`;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
catch { /* corrupted — ignore */ }
|
|
247
|
+
}
|
|
89
248
|
const sessionTitle = projectSlug
|
|
90
249
|
? `${projectSlug} (${memoryCount} memories)`
|
|
91
250
|
: `MemoryForge (${memoryCount} memories)`;
|
|
251
|
+
const systemMsgBase = memoryCount > 0
|
|
252
|
+
? `MemoryForge: ${memoryCount} memories loaded from previous sessions`
|
|
253
|
+
: "MemoryForge: No memories yet. Run `memory-forge setup` to get started.";
|
|
92
254
|
// Output with user-visible session title + agent context
|
|
93
255
|
const output = JSON.stringify({
|
|
94
256
|
hookSpecificOutput: {
|
|
95
257
|
hookEventName: "SessionStart",
|
|
96
258
|
additionalContext: (summary || "[MemoryForge] No memories yet.") + projectContextNote,
|
|
97
|
-
sessionTitle,
|
|
259
|
+
sessionTitle: sessionTitle + proNote,
|
|
98
260
|
},
|
|
99
|
-
systemMessage:
|
|
100
|
-
? `MemoryForge: ${memoryCount} memories loaded from previous sessions`
|
|
101
|
-
: "MemoryForge: No memories yet. Run `memory-forge setup` to get started.",
|
|
261
|
+
systemMessage: systemMsgBase + proNote,
|
|
102
262
|
});
|
|
103
263
|
console.log(output);
|
|
104
264
|
}
|
|
@@ -113,6 +273,10 @@ else if (cmd === "hook") {
|
|
|
113
273
|
deleteMemoryFile(m.id);
|
|
114
274
|
}
|
|
115
275
|
catch { }
|
|
276
|
+
// Upload cloud tombstone for cross-device archive propagation
|
|
277
|
+
if (process.env.SHELBY_API_KEY || getShelbyConfig().apiKey) {
|
|
278
|
+
deleteBlob(getBlobName(m.id)).catch(() => { });
|
|
279
|
+
}
|
|
116
280
|
archived++;
|
|
117
281
|
}
|
|
118
282
|
else {
|
|
@@ -130,8 +294,7 @@ else if (cmd === "hook") {
|
|
|
130
294
|
}
|
|
131
295
|
}
|
|
132
296
|
}
|
|
133
|
-
console.error(`[MemoryForge]
|
|
134
|
-
// Auto-capture conversation transcript (best-effort, don't crash hook)
|
|
297
|
+
console.error(`[MemoryForge] ${all.length} memories maintained, ${archived} archived`);
|
|
135
298
|
try {
|
|
136
299
|
const transcriptResult = captureTranscript();
|
|
137
300
|
console.error(`[MemoryForge] ${transcriptResult}`);
|
|
@@ -139,11 +302,20 @@ else if (cmd === "hook") {
|
|
|
139
302
|
catch (err) {
|
|
140
303
|
console.error(`[MemoryForge] transcript capture failed: ${err.message}`);
|
|
141
304
|
}
|
|
142
|
-
// Purge expired tombstones
|
|
143
305
|
try {
|
|
144
306
|
cleanupTombstones();
|
|
145
307
|
}
|
|
146
308
|
catch { }
|
|
309
|
+
// Pro: push to cloud before exit so other devices get everything
|
|
310
|
+
if (process.env.SHELBY_API_KEY || getShelbyConfig().apiKey) {
|
|
311
|
+
try {
|
|
312
|
+
await proAutoActivate();
|
|
313
|
+
console.error("[MemoryForge] All memories synced to cloud. Safe to close.");
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
console.error("[MemoryForge] Cloud sync skipped — will retry next session.");
|
|
317
|
+
}
|
|
318
|
+
}
|
|
147
319
|
}
|
|
148
320
|
else if (hookType === "pre-compact") {
|
|
149
321
|
const s = new MemoryStore();
|
|
@@ -167,7 +339,7 @@ else if (cmd === "hook") {
|
|
|
167
339
|
additionalContext: preCompactContext,
|
|
168
340
|
},
|
|
169
341
|
}));
|
|
170
|
-
// Safety net: capture transcript
|
|
342
|
+
// Safety net: capture transcript + sync to cloud (survives forced close / VS Code panel close)
|
|
171
343
|
try {
|
|
172
344
|
const preCompactTranscript = captureTranscript();
|
|
173
345
|
if (!preCompactTranscript.includes("already captured")) {
|
|
@@ -177,6 +349,14 @@ else if (cmd === "hook") {
|
|
|
177
349
|
catch (err) {
|
|
178
350
|
console.error(`[MemoryForge] pre-compact transcript capture failed: ${err.message}`);
|
|
179
351
|
}
|
|
352
|
+
// Pro: push to cloud now — VS Code panel close may skip Stop hook
|
|
353
|
+
if (process.env.SHELBY_API_KEY || getShelbyConfig().apiKey) {
|
|
354
|
+
try {
|
|
355
|
+
await proAutoActivate();
|
|
356
|
+
console.error("[MemoryForge] Pre-compact sync complete — memories safe on cloud.");
|
|
357
|
+
}
|
|
358
|
+
catch { /* non-fatal */ }
|
|
359
|
+
}
|
|
180
360
|
}
|
|
181
361
|
else if (hookType === "capture-transcript") {
|
|
182
362
|
try {
|
|
@@ -188,8 +368,21 @@ else if (cmd === "hook") {
|
|
|
188
368
|
process.exit(1);
|
|
189
369
|
}
|
|
190
370
|
}
|
|
371
|
+
else {
|
|
372
|
+
console.error(`Unknown hook type: ${hookType || "(none)"}`);
|
|
373
|
+
console.error("Hook types: session-start, stop, pre-compact, capture-transcript");
|
|
374
|
+
process.exit(1);
|
|
375
|
+
}
|
|
191
376
|
process.exit(0);
|
|
192
377
|
}
|
|
378
|
+
else if (cmd && cmd !== "serve" && cmd !== "start") {
|
|
379
|
+
// Unknown command — show help
|
|
380
|
+
console.error(`Unknown command: ${cmd}`);
|
|
381
|
+
console.error("Usage: memory-forge <command>");
|
|
382
|
+
console.error("Commands: setup, pro [status], list [category], search <query>, stats, hook <type>, capture-transcript, --version");
|
|
383
|
+
console.error("Default (no command): start MCP server (for Claude Code / Cursor integration)");
|
|
384
|
+
process.exit(1);
|
|
385
|
+
}
|
|
193
386
|
else {
|
|
194
387
|
// Default: start MCP server
|
|
195
388
|
startMcpServer();
|
|
@@ -204,20 +397,22 @@ function startMcpServer() {
|
|
|
204
397
|
}
|
|
205
398
|
preload();
|
|
206
399
|
const server = new McpServer({ name: "memory-forge", version: pkg.version });
|
|
400
|
+
const hasPro = !!(process.env.SHELBY_API_KEY || getShelbyConfig().apiKey);
|
|
207
401
|
// ── memory_store ──────────────────────────────────────────
|
|
208
402
|
server.registerTool("memory_store", {
|
|
209
|
-
title: "
|
|
210
|
-
description: "
|
|
403
|
+
title: "Store memory",
|
|
404
|
+
description: "Store a context, knowledge, or preference into persistent memory. Auto-embeds for semantic retrieval.",
|
|
211
405
|
inputSchema: {
|
|
212
|
-
content: z.string().min(1).describe("
|
|
213
|
-
category: z.string().default("general").describe("
|
|
214
|
-
tags: z.array(z.string()).default([]).describe("
|
|
215
|
-
priority: z.number().min(1).max(10).default(5).describe("
|
|
406
|
+
content: z.string().min(1).max(100000).refine((s) => s.trim().length > 0, "Content must not be whitespace-only").describe("Memory content (max 100KB)."),
|
|
407
|
+
category: z.string().default("general").describe("Category: user-preference, project-context, decision-log, code-pattern."),
|
|
408
|
+
tags: z.array(z.string().min(1)).default([]).describe("Tags list."),
|
|
409
|
+
priority: z.number().min(1).max(10).default(5).describe("Priority 1-10."),
|
|
410
|
+
name: z.string().min(1).max(120).optional().describe("Custom name (optional — auto-generated from content if not provided)."),
|
|
216
411
|
},
|
|
217
412
|
}, async (params) => {
|
|
218
|
-
const { content, category, tags, priority } = params;
|
|
413
|
+
const { content, category, tags, priority, name: customName } = params;
|
|
219
414
|
const vec = await embed(content);
|
|
220
|
-
const name = autoName(content);
|
|
415
|
+
const name = customName || autoName(content);
|
|
221
416
|
const memory = {
|
|
222
417
|
id: randomUUID(), name, content, category, tags, priority,
|
|
223
418
|
vector: vec ? Array.from(vec) : [],
|
|
@@ -226,26 +421,32 @@ function startMcpServer() {
|
|
|
226
421
|
const merged = await autoMerge(store, memory);
|
|
227
422
|
if (merged) {
|
|
228
423
|
saveMemory(merged);
|
|
424
|
+
console.error(`[MemoryForge] Merged duplicate: "${memory.name}" → "${merged.name}" (${(0.8 * 100).toFixed(0)}%+ overlap)`);
|
|
229
425
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
230
|
-
success: true, merged: true, memory_id: merged.id, name: merged.name, preview: content
|
|
426
|
+
success: true, merged: true, memory_id: merged.id, name: merged.name, preview: safeTruncate(content, 200),
|
|
231
427
|
}) }] };
|
|
232
428
|
}
|
|
233
429
|
saveMemory(memory);
|
|
234
430
|
store.add(memory);
|
|
235
431
|
// Pro: auto-upload to Shelby cloud
|
|
236
|
-
if (
|
|
432
|
+
if (hasPro) {
|
|
237
433
|
uploadMemory(memory).catch(() => { });
|
|
238
434
|
}
|
|
435
|
+
// Contextual upgrade hint: 20+ memories, no Pro yet
|
|
436
|
+
const hint = !hasPro && store.size() >= 20
|
|
437
|
+
? "💡 20+ memories! Upgrade to Pro for cross-device sync: memory-forge pro"
|
|
438
|
+
: null;
|
|
239
439
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
240
|
-
success: true, memory_id: memory.id, name: memory.name, preview: content
|
|
440
|
+
success: true, memory_id: memory.id, name: memory.name, preview: safeTruncate(content, 200),
|
|
441
|
+
...(hint ? { hint } : {}),
|
|
241
442
|
}) }] };
|
|
242
443
|
});
|
|
243
444
|
// ── memory_search ─────────────────────────────────────────
|
|
244
445
|
server.registerTool("memory_search", {
|
|
245
|
-
title: "
|
|
246
|
-
description: "
|
|
446
|
+
title: "Search memories",
|
|
447
|
+
description: "Semantic search with vector similarity. Auto-falls back to keyword matching when model unavailable.",
|
|
247
448
|
inputSchema: {
|
|
248
|
-
query: z.string().describe("
|
|
449
|
+
query: z.string().describe("Natural language search query."),
|
|
249
450
|
limit: z.number().min(1).max(20).default(5),
|
|
250
451
|
min_similarity: z.number().min(0).max(1).default(0.6),
|
|
251
452
|
category: z.string().optional(),
|
|
@@ -268,7 +469,9 @@ function startMcpServer() {
|
|
|
268
469
|
query, count: results.length,
|
|
269
470
|
results: results.map((r) => ({
|
|
270
471
|
memory_id: r.id, name: r.name,
|
|
271
|
-
similarity: r.similarity
|
|
472
|
+
similarity: typeof r.similarity === "number" ? Number(r.similarity.toFixed(3)) : 0,
|
|
473
|
+
_score: r._score ?? null,
|
|
474
|
+
content: r.content,
|
|
272
475
|
_method: r._fallback || "vector",
|
|
273
476
|
})),
|
|
274
477
|
hint: results.length === 0 ? "No relevant memories found." : null,
|
|
@@ -276,9 +479,9 @@ function startMcpServer() {
|
|
|
276
479
|
});
|
|
277
480
|
// ── memory_recall ─────────────────────────────────────────
|
|
278
481
|
server.registerTool("memory_recall", {
|
|
279
|
-
title: "
|
|
280
|
-
description: "
|
|
281
|
-
inputSchema: { memory_id: z.string().describe("
|
|
482
|
+
title: "Recall memory",
|
|
483
|
+
description: "Retrieve a single memory by its exact ID with full content.",
|
|
484
|
+
inputSchema: { memory_id: z.string().describe("Memory ID.") },
|
|
282
485
|
}, async (params) => {
|
|
283
486
|
const { memory_id } = params;
|
|
284
487
|
const memory = store.get(memory_id);
|
|
@@ -304,35 +507,45 @@ function startMcpServer() {
|
|
|
304
507
|
},
|
|
305
508
|
}, async (params) => {
|
|
306
509
|
const { category, tags, limit, offset } = params;
|
|
510
|
+
const isFiltered = !!(category || (tags && tags.length > 0));
|
|
307
511
|
const memories = store.list({ category: category ?? null, tags: tags ?? null, limit, offset });
|
|
512
|
+
// Count total matching (without pagination) when filtered, else full store
|
|
513
|
+
const matchingTotal = isFiltered
|
|
514
|
+
? store.list({ category: category ?? null, tags: tags ?? null, limit: 10000, offset: 0 }).length
|
|
515
|
+
: store.size();
|
|
308
516
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
309
|
-
total:
|
|
517
|
+
total: matchingTotal, count: memories.length,
|
|
310
518
|
memories: memories.map((m) => ({
|
|
311
519
|
memory_id: m.id, name: m.name, category: m.category,
|
|
312
520
|
tags: m.tags, priority: m.priority, access_count: m.access_count,
|
|
313
521
|
created_at: m.created_at, last_accessed: m.last_accessed,
|
|
314
|
-
preview: m.content
|
|
522
|
+
preview: safeTruncate(m.content, 200),
|
|
315
523
|
})),
|
|
316
524
|
}) }] };
|
|
317
525
|
});
|
|
318
526
|
// ── memory_forget ─────────────────────────────────────────
|
|
319
527
|
server.registerTool("memory_forget", {
|
|
320
|
-
title: "
|
|
321
|
-
description: "
|
|
322
|
-
inputSchema: { memory_id: z.string().describe("
|
|
528
|
+
title: "Forget memory",
|
|
529
|
+
description: "Delete a memory by ID — removes local file + uploads cloud tombstone.",
|
|
530
|
+
inputSchema: { memory_id: z.string().describe("Memory ID to delete.") },
|
|
323
531
|
}, async (params) => {
|
|
324
532
|
const { memory_id } = params;
|
|
325
533
|
const existed = store.remove(memory_id);
|
|
326
|
-
if (existed)
|
|
534
|
+
if (existed) {
|
|
327
535
|
deleteMemoryFile(memory_id);
|
|
536
|
+
// Pro: upload cloud tombstone to prevent sync resurrection
|
|
537
|
+
if (process.env.SHELBY_API_KEY || getShelbyConfig().apiKey) {
|
|
538
|
+
deleteBlob(getBlobName(memory_id)).catch(() => { });
|
|
539
|
+
}
|
|
540
|
+
}
|
|
328
541
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
329
542
|
success: existed, memory_id, action: existed ? "deleted" : "not_found",
|
|
330
543
|
}) }] };
|
|
331
544
|
});
|
|
332
545
|
// ── memory_context ────────────────────────────────────────
|
|
333
546
|
server.registerTool("memory_context", {
|
|
334
|
-
title: "
|
|
335
|
-
description: "
|
|
547
|
+
title: "Load context",
|
|
548
|
+
description: "Load current session context — returns top recent/high-priority memories.",
|
|
336
549
|
inputSchema: { limit: z.number().min(1).max(20).default(5) },
|
|
337
550
|
}, async (params) => {
|
|
338
551
|
const { limit } = params;
|
|
@@ -343,11 +556,11 @@ function startMcpServer() {
|
|
|
343
556
|
});
|
|
344
557
|
// ── memory_export ─────────────────────────────────────────
|
|
345
558
|
server.registerTool("memory_export", {
|
|
346
|
-
title: "
|
|
347
|
-
description: "
|
|
559
|
+
title: "Export memories",
|
|
560
|
+
description: "Export memories to portable JSON or Markdown. Free users can move between machines; Pro users can backup. Exports all if no memory_ids specified.",
|
|
348
561
|
inputSchema: {
|
|
349
|
-
memory_ids: z.array(z.string()).optional().describe("
|
|
350
|
-
format: z.enum(["json", "markdown"]).default("json").describe("
|
|
562
|
+
memory_ids: z.array(z.string()).optional().describe("Memory IDs to export. Exports all if not specified."),
|
|
563
|
+
format: z.enum(["json", "markdown"]).default("json").describe("Export format: json (structured) or markdown (human-readable)."),
|
|
351
564
|
},
|
|
352
565
|
}, async (params) => {
|
|
353
566
|
const { memory_ids, format } = params;
|
|
@@ -357,6 +570,7 @@ function startMcpServer() {
|
|
|
357
570
|
if (memories.length === 0) {
|
|
358
571
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
359
572
|
exported: 0, message: "No memories to export.",
|
|
573
|
+
...(!hasPro ? { hint: "💡 Pro auto-syncs across devices — no manual export needed: memory-forge pro" } : {}),
|
|
360
574
|
}) }] };
|
|
361
575
|
}
|
|
362
576
|
let output;
|
|
@@ -377,7 +591,7 @@ function startMcpServer() {
|
|
|
377
591
|
else {
|
|
378
592
|
output = JSON.stringify({
|
|
379
593
|
exported_at: new Date().toISOString(),
|
|
380
|
-
version:
|
|
594
|
+
version: `memory-forge-${pkg.version}`,
|
|
381
595
|
count: memories.length,
|
|
382
596
|
memories: memories.map((m) => ({
|
|
383
597
|
id: m.id,
|
|
@@ -390,16 +604,19 @@ function startMcpServer() {
|
|
|
390
604
|
})),
|
|
391
605
|
}, null, 2);
|
|
392
606
|
}
|
|
393
|
-
|
|
607
|
+
const hint = !hasPro
|
|
608
|
+
? "\n\n💡 Pro auto-syncs across devices — no manual export needed: memory-forge pro"
|
|
609
|
+
: "";
|
|
610
|
+
return { content: [{ type: "text", text: output + hint }] };
|
|
394
611
|
});
|
|
395
612
|
// ── memory_share ──────────────────────────────────────────
|
|
396
613
|
server.registerTool("memory_share", {
|
|
397
|
-
title: "
|
|
398
|
-
description: "
|
|
614
|
+
title: "Share memory",
|
|
615
|
+
description: "Package a single memory into a shareable JSON bundle for teammates or other agents to import via memory_store.",
|
|
399
616
|
inputSchema: {
|
|
400
|
-
memory_id: z.string().describe("
|
|
401
|
-
recipient: z.string().optional().describe("
|
|
402
|
-
note: z.string().optional().describe("
|
|
617
|
+
memory_id: z.string().describe("Memory ID to share."),
|
|
618
|
+
recipient: z.string().optional().describe("Recipient name (optional, written to share metadata)."),
|
|
619
|
+
note: z.string().optional().describe("Optional note attached to the share package."),
|
|
403
620
|
},
|
|
404
621
|
}, async (params) => {
|
|
405
622
|
const { memory_id, recipient, note } = params;
|
|
@@ -426,25 +643,82 @@ function startMcpServer() {
|
|
|
426
643
|
};
|
|
427
644
|
return { content: [{ type: "text", text: JSON.stringify(sharePackage, null, 2) }] };
|
|
428
645
|
});
|
|
646
|
+
// ── memory_update ────────────────────────────────────────
|
|
647
|
+
server.registerTool("memory_update", {
|
|
648
|
+
title: "Update memory",
|
|
649
|
+
description: "Partially update a memory by ID. Only provided fields are changed — unset fields stay untouched.",
|
|
650
|
+
inputSchema: {
|
|
651
|
+
memory_id: z.string().describe("Memory ID to update."),
|
|
652
|
+
content: z.string().min(1).max(100000).refine((s) => s.trim().length > 0, "Content must not be whitespace-only").optional().describe("New content (optional)."),
|
|
653
|
+
category: z.string().optional().describe("New category (optional)."),
|
|
654
|
+
tags: z.array(z.string()).optional().describe("New tags list (optional, replaces all existing tags)."),
|
|
655
|
+
priority: z.number().min(1).max(10).optional().describe("New priority 1-10 (optional)."),
|
|
656
|
+
name: z.string().min(1).max(120).optional().describe("New name (optional — auto-generated if not provided)."),
|
|
657
|
+
},
|
|
658
|
+
}, async (params) => {
|
|
659
|
+
const { memory_id, content, category, tags, priority, name: customName } = params;
|
|
660
|
+
// Guard: at least one optional field must be provided
|
|
661
|
+
if (content === undefined && category === undefined && tags === undefined && priority === undefined && customName === undefined) {
|
|
662
|
+
return { content: [{ type: "text", text: JSON.stringify({
|
|
663
|
+
error: "No fields to update", hint: "Provide at least one of: content, category, tags, priority.",
|
|
664
|
+
}) }] };
|
|
665
|
+
}
|
|
666
|
+
const memory = store.get(memory_id);
|
|
667
|
+
if (!memory) {
|
|
668
|
+
return { content: [{ type: "text", text: JSON.stringify({
|
|
669
|
+
error: "Not found", memory_id, hint: "Use memory_list to find the correct ID.",
|
|
670
|
+
}) }] };
|
|
671
|
+
}
|
|
672
|
+
// Apply partial updates — only override provided fields
|
|
673
|
+
if (content !== undefined) {
|
|
674
|
+
memory.content = content;
|
|
675
|
+
memory.name = customName || autoName(content);
|
|
676
|
+
const vec = await embed(content);
|
|
677
|
+
if (vec)
|
|
678
|
+
memory.vector = Array.from(vec);
|
|
679
|
+
}
|
|
680
|
+
else if (customName !== undefined) {
|
|
681
|
+
memory.name = customName;
|
|
682
|
+
}
|
|
683
|
+
if (category !== undefined)
|
|
684
|
+
memory.category = category;
|
|
685
|
+
if (tags !== undefined)
|
|
686
|
+
memory.tags = tags;
|
|
687
|
+
if (priority !== undefined)
|
|
688
|
+
memory.priority = priority;
|
|
689
|
+
memory.access_count++;
|
|
690
|
+
memory.last_accessed = new Date().toISOString();
|
|
691
|
+
saveMemory(memory);
|
|
692
|
+
store.add(memory); // update vectorCache for search
|
|
693
|
+
// Pro: sync updated memory to cloud
|
|
694
|
+
if (process.env.SHELBY_API_KEY || getShelbyConfig().apiKey) {
|
|
695
|
+
uploadMemory(memory).catch(() => { });
|
|
696
|
+
}
|
|
697
|
+
return { content: [{ type: "text", text: JSON.stringify({
|
|
698
|
+
success: true, memory_id: memory.id, name: memory.name,
|
|
699
|
+
preview: safeTruncate(memory.content, 200),
|
|
700
|
+
updated_fields: Object.keys(params).filter((k) => k !== "memory_id" && params[k] !== undefined),
|
|
701
|
+
}) }] };
|
|
702
|
+
});
|
|
429
703
|
// ── 启动 ──────────────────────────────────────────────────
|
|
430
704
|
async function main() {
|
|
431
|
-
// Pro: auto-sync
|
|
432
|
-
const proActive = !!process.env.SHELBY_API_KEY;
|
|
705
|
+
// Pro: auto-activate + sync before server starts (so all memories are loaded)
|
|
706
|
+
const proActive = !!(process.env.SHELBY_API_KEY || getShelbyConfig().apiKey);
|
|
433
707
|
if (proActive) {
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
.then(() => {
|
|
708
|
+
try {
|
|
709
|
+
await proAutoActivate();
|
|
437
710
|
for (const m of loadAllMemories())
|
|
438
711
|
store.add(m);
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
712
|
+
}
|
|
713
|
+
catch (err) {
|
|
714
|
+
console.error("[MemoryForge] Pro sync failed (server still available):", err.message);
|
|
715
|
+
}
|
|
442
716
|
}
|
|
443
717
|
const transport = new StdioServerTransport();
|
|
444
718
|
await server.connect(transport);
|
|
445
719
|
console.error(`[MemoryForge] MCP Server started — ${store.size()} memories loaded` +
|
|
446
|
-
(proActive ? " (Pro:
|
|
447
|
-
console.error("[MemoryForge]
|
|
720
|
+
(proActive ? " (Pro: cross-device sync)" : " (Free: local storage)"));
|
|
721
|
+
console.error("[MemoryForge] 9 tools: store / search / recall / list / forget / context / export / share / update");
|
|
448
722
|
}
|
|
449
723
|
main().catch((err) => {
|
|
450
724
|
console.error("[MemoryForge] Fatal:", err);
|