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/index.js CHANGED
@@ -2,29 +2,28 @@
2
2
  /**
3
3
  * MemoryForge — AI Agent 持久记忆引擎 (MVP)
4
4
  *
5
- * 8 MCP 工具 + 5 个后台自动化引擎 + Pro Shelby 云同步。
6
- * 嵌入: Transformers.js (23MB, 进程内)
7
- * 存储: Free 层本地 Markdown; Pro Shelby 云。
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
- * claude mcp add memory-forge -- npx memory-forge
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, syncAll } from "./pro.js";
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
- pro()
57
- .then(() => process.exit(0))
58
- .catch((err) => { console.error(err); process.exit(1); });
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 = path.basename(hookInput.cwd);
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
- // Output as hookSpecificOutput for silent, token-efficient context injection
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] auto-capture: ${updated} updated, ${archived} archived`);
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 now (survives forced terminal close)
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("分类标签: user-preference, project-context, decision-log, code-pattern"),
211
- tags: z.array(z.string()).default([]).describe("标签列表。"),
212
- priority: z.number().min(1).max(10).default(5).describe("优先级 1-10"),
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.slice(0, 200),
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 (process.env.SHELBY_API_KEY) {
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.slice(0, 200),
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?.toFixed(3) ?? 0, content: r.content,
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: "通过 memory_id 精确获取一条记忆的完整内容。",
278
- inputSchema: { memory_id: z.string().describe("记忆 ID") },
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: store.size(), count: memories.length,
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.slice(0, 200),
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("要删除的记忆 ID") },
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: "导出记忆为可移植格式(JSON Markdown)。Free 用户换电脑时手动带走记忆,Pro 用户备份。不指定 memory_ids 则导出全部。",
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("要导出的记忆 ID 列表。不指定则导出全部。"),
347
- format: z.enum(["json", "markdown"]).default("json").describe("导出格式: json(结构化)或 markdown(人类可读)。"),
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
- return { content: [{ type: "text", text: output }] };
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: "将一条记忆打包为可分享的格式,方便发送给队友或其他 Agent 导入。返回一个独立 JSON 包,对方可用 memory_store 重新存入。",
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("要分享的记忆 ID"),
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 — server starts regardless)
702
+ // Pro: auto-activate + sync on startup (non-blocking)
429
703
  const proActive = !!process.env.SHELBY_API_KEY;
430
704
  if (proActive) {
431
- console.error("[MemoryForge] Pro: Syncing with Shelby cloud...");
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: Shelby cloud sync)" : " (Free: local storage)"));
444
- console.error("[MemoryForge] 8 tools: store / search / recall / list / forget / context / export / share");
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);