memory-forge 0.3.12 → 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/dist/auto/index.d.ts +5 -3
- package/dist/auto/index.js +99 -58
- package/dist/auto/index.js.map +1 -1
- package/dist/embedding.js +13 -1
- package/dist/embedding.js.map +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +334 -62
- 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 +211 -25
- 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/shelby.d.ts +21 -2
- package/dist/storage/shelby.js +103 -8
- package/dist/storage/shelby.js.map +1 -1
- package/dist/store.d.ts +11 -1
- package/dist/store.js +80 -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,16 +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)`;
|
|
92
|
-
|
|
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.";
|
|
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
|
},
|
|
261
|
+
systemMessage: systemMsgBase + proNote,
|
|
99
262
|
});
|
|
100
263
|
console.log(output);
|
|
101
264
|
}
|
|
@@ -127,8 +290,7 @@ else if (cmd === "hook") {
|
|
|
127
290
|
}
|
|
128
291
|
}
|
|
129
292
|
}
|
|
130
|
-
console.error(`[MemoryForge]
|
|
131
|
-
// Auto-capture conversation transcript (best-effort, don't crash hook)
|
|
293
|
+
console.error(`[MemoryForge] ${all.length} memories maintained, ${archived} archived`);
|
|
132
294
|
try {
|
|
133
295
|
const transcriptResult = captureTranscript();
|
|
134
296
|
console.error(`[MemoryForge] ${transcriptResult}`);
|
|
@@ -136,11 +298,21 @@ else if (cmd === "hook") {
|
|
|
136
298
|
catch (err) {
|
|
137
299
|
console.error(`[MemoryForge] transcript capture failed: ${err.message}`);
|
|
138
300
|
}
|
|
139
|
-
// Purge expired tombstones
|
|
140
301
|
try {
|
|
141
302
|
cleanupTombstones();
|
|
142
303
|
}
|
|
143
304
|
catch { }
|
|
305
|
+
// Pro: push to cloud before exit so other devices get everything
|
|
306
|
+
if (process.env.SHELBY_API_KEY || getShelbyConfig().apiKey) {
|
|
307
|
+
try {
|
|
308
|
+
const { proAutoActivate } = await import("./pro.js");
|
|
309
|
+
await proAutoActivate();
|
|
310
|
+
console.error("[MemoryForge] All memories synced to cloud. Safe to close.");
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
console.error("[MemoryForge] Cloud sync skipped — will retry next session.");
|
|
314
|
+
}
|
|
315
|
+
}
|
|
144
316
|
}
|
|
145
317
|
else if (hookType === "pre-compact") {
|
|
146
318
|
const s = new MemoryStore();
|
|
@@ -164,7 +336,7 @@ else if (cmd === "hook") {
|
|
|
164
336
|
additionalContext: preCompactContext,
|
|
165
337
|
},
|
|
166
338
|
}));
|
|
167
|
-
// Safety net: capture transcript
|
|
339
|
+
// Safety net: capture transcript + sync to cloud (survives forced close / VS Code panel close)
|
|
168
340
|
try {
|
|
169
341
|
const preCompactTranscript = captureTranscript();
|
|
170
342
|
if (!preCompactTranscript.includes("already captured")) {
|
|
@@ -174,6 +346,15 @@ else if (cmd === "hook") {
|
|
|
174
346
|
catch (err) {
|
|
175
347
|
console.error(`[MemoryForge] pre-compact transcript capture failed: ${err.message}`);
|
|
176
348
|
}
|
|
349
|
+
// Pro: push to cloud now — VS Code panel close may skip Stop hook
|
|
350
|
+
if (process.env.SHELBY_API_KEY || getShelbyConfig().apiKey) {
|
|
351
|
+
try {
|
|
352
|
+
const { proAutoActivate } = await import("./pro.js");
|
|
353
|
+
await proAutoActivate();
|
|
354
|
+
console.error("[MemoryForge] Pre-compact sync complete — memories safe on cloud.");
|
|
355
|
+
}
|
|
356
|
+
catch { /* non-fatal */ }
|
|
357
|
+
}
|
|
177
358
|
}
|
|
178
359
|
else if (hookType === "capture-transcript") {
|
|
179
360
|
try {
|
|
@@ -185,8 +366,21 @@ else if (cmd === "hook") {
|
|
|
185
366
|
process.exit(1);
|
|
186
367
|
}
|
|
187
368
|
}
|
|
369
|
+
else {
|
|
370
|
+
console.error(`Unknown hook type: ${hookType || "(none)"}`);
|
|
371
|
+
console.error("Hook types: session-start, stop, pre-compact, capture-transcript");
|
|
372
|
+
process.exit(1);
|
|
373
|
+
}
|
|
188
374
|
process.exit(0);
|
|
189
375
|
}
|
|
376
|
+
else if (cmd && cmd !== "serve" && cmd !== "start") {
|
|
377
|
+
// Unknown command — show help
|
|
378
|
+
console.error(`Unknown command: ${cmd}`);
|
|
379
|
+
console.error("Usage: memory-forge <command>");
|
|
380
|
+
console.error("Commands: setup, pro [status], list [category], search <query>, stats, hook <type>, capture-transcript, --version");
|
|
381
|
+
console.error("Default (no command): start MCP server (for Claude Code / Cursor integration)");
|
|
382
|
+
process.exit(1);
|
|
383
|
+
}
|
|
190
384
|
else {
|
|
191
385
|
// Default: start MCP server
|
|
192
386
|
startMcpServer();
|
|
@@ -201,20 +395,22 @@ function startMcpServer() {
|
|
|
201
395
|
}
|
|
202
396
|
preload();
|
|
203
397
|
const server = new McpServer({ name: "memory-forge", version: pkg.version });
|
|
398
|
+
const hasPro = !!(process.env.SHELBY_API_KEY || getShelbyConfig().apiKey);
|
|
204
399
|
// ── memory_store ──────────────────────────────────────────
|
|
205
400
|
server.registerTool("memory_store", {
|
|
206
|
-
title: "
|
|
207
|
-
description: "
|
|
401
|
+
title: "Store memory",
|
|
402
|
+
description: "Store a context, knowledge, or preference into persistent memory. Auto-embeds for semantic retrieval.",
|
|
208
403
|
inputSchema: {
|
|
209
|
-
content: z.string().min(1).describe("
|
|
210
|
-
category: z.string().default("general").describe("
|
|
211
|
-
tags: z.array(z.string()).default([]).describe("
|
|
212
|
-
priority: z.number().min(1).max(10).default(5).describe("
|
|
404
|
+
content: z.string().min(1).max(100000).refine((s) => s.trim().length > 0, "Content must not be whitespace-only").describe("Memory content (max 100KB)."),
|
|
405
|
+
category: z.string().default("general").describe("Category: user-preference, project-context, decision-log, code-pattern."),
|
|
406
|
+
tags: z.array(z.string().min(1)).default([]).describe("Tags list."),
|
|
407
|
+
priority: z.number().min(1).max(10).default(5).describe("Priority 1-10."),
|
|
408
|
+
name: z.string().min(1).max(120).optional().describe("Custom name (optional — auto-generated from content if not provided)."),
|
|
213
409
|
},
|
|
214
410
|
}, async (params) => {
|
|
215
|
-
const { content, category, tags, priority } = params;
|
|
411
|
+
const { content, category, tags, priority, name: customName } = params;
|
|
216
412
|
const vec = await embed(content);
|
|
217
|
-
const name = autoName(content);
|
|
413
|
+
const name = customName || autoName(content);
|
|
218
414
|
const memory = {
|
|
219
415
|
id: randomUUID(), name, content, category, tags, priority,
|
|
220
416
|
vector: vec ? Array.from(vec) : [],
|
|
@@ -223,26 +419,32 @@ function startMcpServer() {
|
|
|
223
419
|
const merged = await autoMerge(store, memory);
|
|
224
420
|
if (merged) {
|
|
225
421
|
saveMemory(merged);
|
|
422
|
+
console.error(`[MemoryForge] Merged duplicate: "${memory.name}" → "${merged.name}" (${(0.8 * 100).toFixed(0)}%+ overlap)`);
|
|
226
423
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
227
|
-
success: true, merged: true, memory_id: merged.id, name: merged.name, preview: content
|
|
424
|
+
success: true, merged: true, memory_id: merged.id, name: merged.name, preview: safeTruncate(content, 200),
|
|
228
425
|
}) }] };
|
|
229
426
|
}
|
|
230
427
|
saveMemory(memory);
|
|
231
428
|
store.add(memory);
|
|
232
429
|
// Pro: auto-upload to Shelby cloud
|
|
233
|
-
if (
|
|
430
|
+
if (hasPro) {
|
|
234
431
|
uploadMemory(memory).catch(() => { });
|
|
235
432
|
}
|
|
433
|
+
// Contextual upgrade hint: 20+ memories, no Pro yet
|
|
434
|
+
const hint = !hasPro && store.size() >= 20
|
|
435
|
+
? "💡 20+ memories! Upgrade to Pro for cross-device sync: memory-forge pro"
|
|
436
|
+
: null;
|
|
236
437
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
237
|
-
success: true, memory_id: memory.id, name: memory.name, preview: content
|
|
438
|
+
success: true, memory_id: memory.id, name: memory.name, preview: safeTruncate(content, 200),
|
|
439
|
+
...(hint ? { hint } : {}),
|
|
238
440
|
}) }] };
|
|
239
441
|
});
|
|
240
442
|
// ── memory_search ─────────────────────────────────────────
|
|
241
443
|
server.registerTool("memory_search", {
|
|
242
|
-
title: "
|
|
243
|
-
description: "
|
|
444
|
+
title: "Search memories",
|
|
445
|
+
description: "Semantic search with vector similarity. Auto-falls back to keyword matching when model unavailable.",
|
|
244
446
|
inputSchema: {
|
|
245
|
-
query: z.string().describe("
|
|
447
|
+
query: z.string().describe("Natural language search query."),
|
|
246
448
|
limit: z.number().min(1).max(20).default(5),
|
|
247
449
|
min_similarity: z.number().min(0).max(1).default(0.6),
|
|
248
450
|
category: z.string().optional(),
|
|
@@ -265,7 +467,9 @@ function startMcpServer() {
|
|
|
265
467
|
query, count: results.length,
|
|
266
468
|
results: results.map((r) => ({
|
|
267
469
|
memory_id: r.id, name: r.name,
|
|
268
|
-
similarity: r.similarity
|
|
470
|
+
similarity: typeof r.similarity === "number" ? Number(r.similarity.toFixed(3)) : 0,
|
|
471
|
+
_score: r._score ?? null,
|
|
472
|
+
content: r.content,
|
|
269
473
|
_method: r._fallback || "vector",
|
|
270
474
|
})),
|
|
271
475
|
hint: results.length === 0 ? "No relevant memories found." : null,
|
|
@@ -273,9 +477,9 @@ function startMcpServer() {
|
|
|
273
477
|
});
|
|
274
478
|
// ── memory_recall ─────────────────────────────────────────
|
|
275
479
|
server.registerTool("memory_recall", {
|
|
276
|
-
title: "
|
|
277
|
-
description: "
|
|
278
|
-
inputSchema: { memory_id: z.string().describe("
|
|
480
|
+
title: "Recall memory",
|
|
481
|
+
description: "Retrieve a single memory by its exact ID with full content.",
|
|
482
|
+
inputSchema: { memory_id: z.string().describe("Memory ID.") },
|
|
279
483
|
}, async (params) => {
|
|
280
484
|
const { memory_id } = params;
|
|
281
485
|
const memory = store.get(memory_id);
|
|
@@ -301,35 +505,45 @@ function startMcpServer() {
|
|
|
301
505
|
},
|
|
302
506
|
}, async (params) => {
|
|
303
507
|
const { category, tags, limit, offset } = params;
|
|
508
|
+
const isFiltered = !!(category || (tags && tags.length > 0));
|
|
304
509
|
const memories = store.list({ category: category ?? null, tags: tags ?? null, limit, offset });
|
|
510
|
+
// Count total matching (without pagination) when filtered, else full store
|
|
511
|
+
const matchingTotal = isFiltered
|
|
512
|
+
? store.list({ category: category ?? null, tags: tags ?? null, limit: 10000, offset: 0 }).length
|
|
513
|
+
: store.size();
|
|
305
514
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
306
|
-
total:
|
|
515
|
+
total: matchingTotal, count: memories.length,
|
|
307
516
|
memories: memories.map((m) => ({
|
|
308
517
|
memory_id: m.id, name: m.name, category: m.category,
|
|
309
518
|
tags: m.tags, priority: m.priority, access_count: m.access_count,
|
|
310
519
|
created_at: m.created_at, last_accessed: m.last_accessed,
|
|
311
|
-
preview: m.content
|
|
520
|
+
preview: safeTruncate(m.content, 200),
|
|
312
521
|
})),
|
|
313
522
|
}) }] };
|
|
314
523
|
});
|
|
315
524
|
// ── memory_forget ─────────────────────────────────────────
|
|
316
525
|
server.registerTool("memory_forget", {
|
|
317
|
-
title: "
|
|
318
|
-
description: "
|
|
319
|
-
inputSchema: { memory_id: z.string().describe("
|
|
526
|
+
title: "Forget memory",
|
|
527
|
+
description: "Delete a memory by ID — removes local file + uploads cloud tombstone.",
|
|
528
|
+
inputSchema: { memory_id: z.string().describe("Memory ID to delete.") },
|
|
320
529
|
}, async (params) => {
|
|
321
530
|
const { memory_id } = params;
|
|
322
531
|
const existed = store.remove(memory_id);
|
|
323
|
-
if (existed)
|
|
532
|
+
if (existed) {
|
|
324
533
|
deleteMemoryFile(memory_id);
|
|
534
|
+
// Pro: upload cloud tombstone to prevent sync resurrection
|
|
535
|
+
if (process.env.SHELBY_API_KEY || getShelbyConfig().apiKey) {
|
|
536
|
+
deleteBlob(getBlobName(memory_id)).catch(() => { });
|
|
537
|
+
}
|
|
538
|
+
}
|
|
325
539
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
326
540
|
success: existed, memory_id, action: existed ? "deleted" : "not_found",
|
|
327
541
|
}) }] };
|
|
328
542
|
});
|
|
329
543
|
// ── memory_context ────────────────────────────────────────
|
|
330
544
|
server.registerTool("memory_context", {
|
|
331
|
-
title: "
|
|
332
|
-
description: "
|
|
545
|
+
title: "Load context",
|
|
546
|
+
description: "Load current session context — returns top recent/high-priority memories.",
|
|
333
547
|
inputSchema: { limit: z.number().min(1).max(20).default(5) },
|
|
334
548
|
}, async (params) => {
|
|
335
549
|
const { limit } = params;
|
|
@@ -340,11 +554,11 @@ function startMcpServer() {
|
|
|
340
554
|
});
|
|
341
555
|
// ── memory_export ─────────────────────────────────────────
|
|
342
556
|
server.registerTool("memory_export", {
|
|
343
|
-
title: "
|
|
344
|
-
description: "
|
|
557
|
+
title: "Export memories",
|
|
558
|
+
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.",
|
|
345
559
|
inputSchema: {
|
|
346
|
-
memory_ids: z.array(z.string()).optional().describe("
|
|
347
|
-
format: z.enum(["json", "markdown"]).default("json").describe("
|
|
560
|
+
memory_ids: z.array(z.string()).optional().describe("Memory IDs to export. Exports all if not specified."),
|
|
561
|
+
format: z.enum(["json", "markdown"]).default("json").describe("Export format: json (structured) or markdown (human-readable)."),
|
|
348
562
|
},
|
|
349
563
|
}, async (params) => {
|
|
350
564
|
const { memory_ids, format } = params;
|
|
@@ -354,6 +568,7 @@ function startMcpServer() {
|
|
|
354
568
|
if (memories.length === 0) {
|
|
355
569
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
356
570
|
exported: 0, message: "No memories to export.",
|
|
571
|
+
...(!hasPro ? { hint: "💡 Pro auto-syncs across devices — no manual export needed: memory-forge pro" } : {}),
|
|
357
572
|
}) }] };
|
|
358
573
|
}
|
|
359
574
|
let output;
|
|
@@ -387,16 +602,19 @@ function startMcpServer() {
|
|
|
387
602
|
})),
|
|
388
603
|
}, null, 2);
|
|
389
604
|
}
|
|
390
|
-
|
|
605
|
+
const hint = !hasPro
|
|
606
|
+
? "\n\n💡 Pro auto-syncs across devices — no manual export needed: memory-forge pro"
|
|
607
|
+
: "";
|
|
608
|
+
return { content: [{ type: "text", text: output + hint }] };
|
|
391
609
|
});
|
|
392
610
|
// ── memory_share ──────────────────────────────────────────
|
|
393
611
|
server.registerTool("memory_share", {
|
|
394
|
-
title: "
|
|
395
|
-
description: "
|
|
612
|
+
title: "Share memory",
|
|
613
|
+
description: "Package a single memory into a shareable JSON bundle for teammates or other agents to import via memory_store.",
|
|
396
614
|
inputSchema: {
|
|
397
|
-
memory_id: z.string().describe("
|
|
398
|
-
recipient: z.string().optional().describe("
|
|
399
|
-
note: z.string().optional().describe("
|
|
615
|
+
memory_id: z.string().describe("Memory ID to share."),
|
|
616
|
+
recipient: z.string().optional().describe("Recipient name (optional, written to share metadata)."),
|
|
617
|
+
note: z.string().optional().describe("Optional note attached to the share package."),
|
|
400
618
|
},
|
|
401
619
|
}, async (params) => {
|
|
402
620
|
const { memory_id, recipient, note } = params;
|
|
@@ -423,25 +641,79 @@ function startMcpServer() {
|
|
|
423
641
|
};
|
|
424
642
|
return { content: [{ type: "text", text: JSON.stringify(sharePackage, null, 2) }] };
|
|
425
643
|
});
|
|
644
|
+
// ── memory_update ────────────────────────────────────────
|
|
645
|
+
server.registerTool("memory_update", {
|
|
646
|
+
title: "Update memory",
|
|
647
|
+
description: "Partially update a memory by ID. Only provided fields are changed — unset fields stay untouched.",
|
|
648
|
+
inputSchema: {
|
|
649
|
+
memory_id: z.string().describe("Memory ID to update."),
|
|
650
|
+
content: z.string().min(1).max(100000).refine((s) => s.trim().length > 0, "Content must not be whitespace-only").optional().describe("New content (optional)."),
|
|
651
|
+
category: z.string().optional().describe("New category (optional)."),
|
|
652
|
+
tags: z.array(z.string()).optional().describe("New tags list (optional, replaces all existing tags)."),
|
|
653
|
+
priority: z.number().min(1).max(10).optional().describe("New priority 1-10 (optional)."),
|
|
654
|
+
name: z.string().min(1).max(120).optional().describe("New name (optional — auto-generated if not provided)."),
|
|
655
|
+
},
|
|
656
|
+
}, async (params) => {
|
|
657
|
+
const { memory_id, content, category, tags, priority, name: customName } = params;
|
|
658
|
+
// Guard: at least one optional field must be provided
|
|
659
|
+
if (content === undefined && category === undefined && tags === undefined && priority === undefined && customName === undefined) {
|
|
660
|
+
return { content: [{ type: "text", text: JSON.stringify({
|
|
661
|
+
error: "No fields to update", hint: "Provide at least one of: content, category, tags, priority.",
|
|
662
|
+
}) }] };
|
|
663
|
+
}
|
|
664
|
+
const memory = store.get(memory_id);
|
|
665
|
+
if (!memory) {
|
|
666
|
+
return { content: [{ type: "text", text: JSON.stringify({
|
|
667
|
+
error: "Not found", memory_id, hint: "Use memory_list to find the correct ID.",
|
|
668
|
+
}) }] };
|
|
669
|
+
}
|
|
670
|
+
// Apply partial updates — only override provided fields
|
|
671
|
+
if (content !== undefined) {
|
|
672
|
+
memory.content = content;
|
|
673
|
+
memory.name = customName || autoName(content);
|
|
674
|
+
const vec = await embed(content);
|
|
675
|
+
if (vec)
|
|
676
|
+
memory.vector = Array.from(vec);
|
|
677
|
+
}
|
|
678
|
+
else if (customName !== undefined) {
|
|
679
|
+
memory.name = customName;
|
|
680
|
+
}
|
|
681
|
+
if (category !== undefined)
|
|
682
|
+
memory.category = category;
|
|
683
|
+
if (tags !== undefined)
|
|
684
|
+
memory.tags = tags;
|
|
685
|
+
if (priority !== undefined)
|
|
686
|
+
memory.priority = priority;
|
|
687
|
+
memory.access_count++;
|
|
688
|
+
memory.last_accessed = new Date().toISOString();
|
|
689
|
+
saveMemory(memory);
|
|
690
|
+
// Pro: sync updated memory to cloud
|
|
691
|
+
if (process.env.SHELBY_API_KEY || getShelbyConfig().apiKey) {
|
|
692
|
+
uploadMemory(memory).catch(() => { });
|
|
693
|
+
}
|
|
694
|
+
return { content: [{ type: "text", text: JSON.stringify({
|
|
695
|
+
success: true, memory_id: memory.id, name: memory.name,
|
|
696
|
+
preview: safeTruncate(memory.content, 200),
|
|
697
|
+
updated_fields: Object.keys(params).filter((k) => k !== "memory_id" && params[k] !== undefined),
|
|
698
|
+
}) }] };
|
|
699
|
+
});
|
|
426
700
|
// ── 启动 ──────────────────────────────────────────────────
|
|
427
701
|
async function main() {
|
|
428
|
-
// Pro: auto-sync on startup (non-blocking
|
|
702
|
+
// Pro: auto-activate + sync on startup (non-blocking)
|
|
429
703
|
const proActive = !!process.env.SHELBY_API_KEY;
|
|
430
704
|
if (proActive) {
|
|
431
|
-
|
|
432
|
-
syncAll()
|
|
705
|
+
proAutoActivate()
|
|
433
706
|
.then(() => {
|
|
434
707
|
for (const m of loadAllMemories())
|
|
435
708
|
store.add(m);
|
|
436
|
-
console.error(`[MemoryForge] Pro sync complete — ${store.size()} memories`);
|
|
437
709
|
})
|
|
438
710
|
.catch((err) => console.error("[MemoryForge] Pro sync failed (server still available):", err.message));
|
|
439
711
|
}
|
|
440
712
|
const transport = new StdioServerTransport();
|
|
441
713
|
await server.connect(transport);
|
|
442
714
|
console.error(`[MemoryForge] MCP Server started — ${store.size()} memories loaded` +
|
|
443
|
-
(proActive ? " (Pro:
|
|
444
|
-
console.error("[MemoryForge]
|
|
715
|
+
(proActive ? " (Pro: cross-device sync)" : " (Free: local storage)"));
|
|
716
|
+
console.error("[MemoryForge] 9 tools: store / search / recall / list / forget / context / export / share / update");
|
|
445
717
|
}
|
|
446
718
|
main().catch((err) => {
|
|
447
719
|
console.error("[MemoryForge] Fatal:", err);
|