memory-forge 0.3.13 → 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 +93 -54
- 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 +333 -64
- 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,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
|
}
|
|
@@ -130,8 +290,7 @@ else if (cmd === "hook") {
|
|
|
130
290
|
}
|
|
131
291
|
}
|
|
132
292
|
}
|
|
133
|
-
console.error(`[MemoryForge]
|
|
134
|
-
// Auto-capture conversation transcript (best-effort, don't crash hook)
|
|
293
|
+
console.error(`[MemoryForge] ${all.length} memories maintained, ${archived} archived`);
|
|
135
294
|
try {
|
|
136
295
|
const transcriptResult = captureTranscript();
|
|
137
296
|
console.error(`[MemoryForge] ${transcriptResult}`);
|
|
@@ -139,11 +298,21 @@ else if (cmd === "hook") {
|
|
|
139
298
|
catch (err) {
|
|
140
299
|
console.error(`[MemoryForge] transcript capture failed: ${err.message}`);
|
|
141
300
|
}
|
|
142
|
-
// Purge expired tombstones
|
|
143
301
|
try {
|
|
144
302
|
cleanupTombstones();
|
|
145
303
|
}
|
|
146
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
|
+
}
|
|
147
316
|
}
|
|
148
317
|
else if (hookType === "pre-compact") {
|
|
149
318
|
const s = new MemoryStore();
|
|
@@ -167,7 +336,7 @@ else if (cmd === "hook") {
|
|
|
167
336
|
additionalContext: preCompactContext,
|
|
168
337
|
},
|
|
169
338
|
}));
|
|
170
|
-
// Safety net: capture transcript
|
|
339
|
+
// Safety net: capture transcript + sync to cloud (survives forced close / VS Code panel close)
|
|
171
340
|
try {
|
|
172
341
|
const preCompactTranscript = captureTranscript();
|
|
173
342
|
if (!preCompactTranscript.includes("already captured")) {
|
|
@@ -177,6 +346,15 @@ else if (cmd === "hook") {
|
|
|
177
346
|
catch (err) {
|
|
178
347
|
console.error(`[MemoryForge] pre-compact transcript capture failed: ${err.message}`);
|
|
179
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
|
+
}
|
|
180
358
|
}
|
|
181
359
|
else if (hookType === "capture-transcript") {
|
|
182
360
|
try {
|
|
@@ -188,8 +366,21 @@ else if (cmd === "hook") {
|
|
|
188
366
|
process.exit(1);
|
|
189
367
|
}
|
|
190
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
|
+
}
|
|
191
374
|
process.exit(0);
|
|
192
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
|
+
}
|
|
193
384
|
else {
|
|
194
385
|
// Default: start MCP server
|
|
195
386
|
startMcpServer();
|
|
@@ -204,20 +395,22 @@ function startMcpServer() {
|
|
|
204
395
|
}
|
|
205
396
|
preload();
|
|
206
397
|
const server = new McpServer({ name: "memory-forge", version: pkg.version });
|
|
398
|
+
const hasPro = !!(process.env.SHELBY_API_KEY || getShelbyConfig().apiKey);
|
|
207
399
|
// ── memory_store ──────────────────────────────────────────
|
|
208
400
|
server.registerTool("memory_store", {
|
|
209
|
-
title: "
|
|
210
|
-
description: "
|
|
401
|
+
title: "Store memory",
|
|
402
|
+
description: "Store a context, knowledge, or preference into persistent memory. Auto-embeds for semantic retrieval.",
|
|
211
403
|
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("
|
|
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)."),
|
|
216
409
|
},
|
|
217
410
|
}, async (params) => {
|
|
218
|
-
const { content, category, tags, priority } = params;
|
|
411
|
+
const { content, category, tags, priority, name: customName } = params;
|
|
219
412
|
const vec = await embed(content);
|
|
220
|
-
const name = autoName(content);
|
|
413
|
+
const name = customName || autoName(content);
|
|
221
414
|
const memory = {
|
|
222
415
|
id: randomUUID(), name, content, category, tags, priority,
|
|
223
416
|
vector: vec ? Array.from(vec) : [],
|
|
@@ -226,26 +419,32 @@ function startMcpServer() {
|
|
|
226
419
|
const merged = await autoMerge(store, memory);
|
|
227
420
|
if (merged) {
|
|
228
421
|
saveMemory(merged);
|
|
422
|
+
console.error(`[MemoryForge] Merged duplicate: "${memory.name}" → "${merged.name}" (${(0.8 * 100).toFixed(0)}%+ overlap)`);
|
|
229
423
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
230
|
-
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),
|
|
231
425
|
}) }] };
|
|
232
426
|
}
|
|
233
427
|
saveMemory(memory);
|
|
234
428
|
store.add(memory);
|
|
235
429
|
// Pro: auto-upload to Shelby cloud
|
|
236
|
-
if (
|
|
430
|
+
if (hasPro) {
|
|
237
431
|
uploadMemory(memory).catch(() => { });
|
|
238
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;
|
|
239
437
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
240
|
-
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 } : {}),
|
|
241
440
|
}) }] };
|
|
242
441
|
});
|
|
243
442
|
// ── memory_search ─────────────────────────────────────────
|
|
244
443
|
server.registerTool("memory_search", {
|
|
245
|
-
title: "
|
|
246
|
-
description: "
|
|
444
|
+
title: "Search memories",
|
|
445
|
+
description: "Semantic search with vector similarity. Auto-falls back to keyword matching when model unavailable.",
|
|
247
446
|
inputSchema: {
|
|
248
|
-
query: z.string().describe("
|
|
447
|
+
query: z.string().describe("Natural language search query."),
|
|
249
448
|
limit: z.number().min(1).max(20).default(5),
|
|
250
449
|
min_similarity: z.number().min(0).max(1).default(0.6),
|
|
251
450
|
category: z.string().optional(),
|
|
@@ -268,7 +467,9 @@ function startMcpServer() {
|
|
|
268
467
|
query, count: results.length,
|
|
269
468
|
results: results.map((r) => ({
|
|
270
469
|
memory_id: r.id, name: r.name,
|
|
271
|
-
similarity: r.similarity
|
|
470
|
+
similarity: typeof r.similarity === "number" ? Number(r.similarity.toFixed(3)) : 0,
|
|
471
|
+
_score: r._score ?? null,
|
|
472
|
+
content: r.content,
|
|
272
473
|
_method: r._fallback || "vector",
|
|
273
474
|
})),
|
|
274
475
|
hint: results.length === 0 ? "No relevant memories found." : null,
|
|
@@ -276,9 +477,9 @@ function startMcpServer() {
|
|
|
276
477
|
});
|
|
277
478
|
// ── memory_recall ─────────────────────────────────────────
|
|
278
479
|
server.registerTool("memory_recall", {
|
|
279
|
-
title: "
|
|
280
|
-
description: "
|
|
281
|
-
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.") },
|
|
282
483
|
}, async (params) => {
|
|
283
484
|
const { memory_id } = params;
|
|
284
485
|
const memory = store.get(memory_id);
|
|
@@ -304,35 +505,45 @@ function startMcpServer() {
|
|
|
304
505
|
},
|
|
305
506
|
}, async (params) => {
|
|
306
507
|
const { category, tags, limit, offset } = params;
|
|
508
|
+
const isFiltered = !!(category || (tags && tags.length > 0));
|
|
307
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();
|
|
308
514
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
309
|
-
total:
|
|
515
|
+
total: matchingTotal, count: memories.length,
|
|
310
516
|
memories: memories.map((m) => ({
|
|
311
517
|
memory_id: m.id, name: m.name, category: m.category,
|
|
312
518
|
tags: m.tags, priority: m.priority, access_count: m.access_count,
|
|
313
519
|
created_at: m.created_at, last_accessed: m.last_accessed,
|
|
314
|
-
preview: m.content
|
|
520
|
+
preview: safeTruncate(m.content, 200),
|
|
315
521
|
})),
|
|
316
522
|
}) }] };
|
|
317
523
|
});
|
|
318
524
|
// ── memory_forget ─────────────────────────────────────────
|
|
319
525
|
server.registerTool("memory_forget", {
|
|
320
|
-
title: "
|
|
321
|
-
description: "
|
|
322
|
-
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.") },
|
|
323
529
|
}, async (params) => {
|
|
324
530
|
const { memory_id } = params;
|
|
325
531
|
const existed = store.remove(memory_id);
|
|
326
|
-
if (existed)
|
|
532
|
+
if (existed) {
|
|
327
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
|
+
}
|
|
328
539
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
329
540
|
success: existed, memory_id, action: existed ? "deleted" : "not_found",
|
|
330
541
|
}) }] };
|
|
331
542
|
});
|
|
332
543
|
// ── memory_context ────────────────────────────────────────
|
|
333
544
|
server.registerTool("memory_context", {
|
|
334
|
-
title: "
|
|
335
|
-
description: "
|
|
545
|
+
title: "Load context",
|
|
546
|
+
description: "Load current session context — returns top recent/high-priority memories.",
|
|
336
547
|
inputSchema: { limit: z.number().min(1).max(20).default(5) },
|
|
337
548
|
}, async (params) => {
|
|
338
549
|
const { limit } = params;
|
|
@@ -343,11 +554,11 @@ function startMcpServer() {
|
|
|
343
554
|
});
|
|
344
555
|
// ── memory_export ─────────────────────────────────────────
|
|
345
556
|
server.registerTool("memory_export", {
|
|
346
|
-
title: "
|
|
347
|
-
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.",
|
|
348
559
|
inputSchema: {
|
|
349
|
-
memory_ids: z.array(z.string()).optional().describe("
|
|
350
|
-
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)."),
|
|
351
562
|
},
|
|
352
563
|
}, async (params) => {
|
|
353
564
|
const { memory_ids, format } = params;
|
|
@@ -357,6 +568,7 @@ function startMcpServer() {
|
|
|
357
568
|
if (memories.length === 0) {
|
|
358
569
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
359
570
|
exported: 0, message: "No memories to export.",
|
|
571
|
+
...(!hasPro ? { hint: "💡 Pro auto-syncs across devices — no manual export needed: memory-forge pro" } : {}),
|
|
360
572
|
}) }] };
|
|
361
573
|
}
|
|
362
574
|
let output;
|
|
@@ -390,16 +602,19 @@ function startMcpServer() {
|
|
|
390
602
|
})),
|
|
391
603
|
}, null, 2);
|
|
392
604
|
}
|
|
393
|
-
|
|
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 }] };
|
|
394
609
|
});
|
|
395
610
|
// ── memory_share ──────────────────────────────────────────
|
|
396
611
|
server.registerTool("memory_share", {
|
|
397
|
-
title: "
|
|
398
|
-
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.",
|
|
399
614
|
inputSchema: {
|
|
400
|
-
memory_id: z.string().describe("
|
|
401
|
-
recipient: z.string().optional().describe("
|
|
402
|
-
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."),
|
|
403
618
|
},
|
|
404
619
|
}, async (params) => {
|
|
405
620
|
const { memory_id, recipient, note } = params;
|
|
@@ -426,25 +641,79 @@ function startMcpServer() {
|
|
|
426
641
|
};
|
|
427
642
|
return { content: [{ type: "text", text: JSON.stringify(sharePackage, null, 2) }] };
|
|
428
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
|
+
});
|
|
429
700
|
// ── 启动 ──────────────────────────────────────────────────
|
|
430
701
|
async function main() {
|
|
431
|
-
// Pro: auto-sync on startup (non-blocking
|
|
702
|
+
// Pro: auto-activate + sync on startup (non-blocking)
|
|
432
703
|
const proActive = !!process.env.SHELBY_API_KEY;
|
|
433
704
|
if (proActive) {
|
|
434
|
-
|
|
435
|
-
syncAll()
|
|
705
|
+
proAutoActivate()
|
|
436
706
|
.then(() => {
|
|
437
707
|
for (const m of loadAllMemories())
|
|
438
708
|
store.add(m);
|
|
439
|
-
console.error(`[MemoryForge] Pro sync complete — ${store.size()} memories`);
|
|
440
709
|
})
|
|
441
710
|
.catch((err) => console.error("[MemoryForge] Pro sync failed (server still available):", err.message));
|
|
442
711
|
}
|
|
443
712
|
const transport = new StdioServerTransport();
|
|
444
713
|
await server.connect(transport);
|
|
445
714
|
console.error(`[MemoryForge] MCP Server started — ${store.size()} memories loaded` +
|
|
446
|
-
(proActive ? " (Pro:
|
|
447
|
-
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");
|
|
448
717
|
}
|
|
449
718
|
main().catch((err) => {
|
|
450
719
|
console.error("[MemoryForge] Fatal:", err);
|