memory-forge 0.3.13 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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,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: memoryCount > 0
100
- ? `MemoryForge: ${memoryCount} memories loaded from previous sessions`
101
- : "MemoryForge: No memories yet. Run `memory-forge setup` to get started.",
261
+ systemMessage: systemMsgBase + proNote,
102
262
  });
103
263
  console.log(output);
104
264
  }
@@ -113,6 +273,10 @@ else if (cmd === "hook") {
113
273
  deleteMemoryFile(m.id);
114
274
  }
115
275
  catch { }
276
+ // Upload cloud tombstone for cross-device archive propagation
277
+ if (process.env.SHELBY_API_KEY || getShelbyConfig().apiKey) {
278
+ deleteBlob(getBlobName(m.id)).catch(() => { });
279
+ }
116
280
  archived++;
117
281
  }
118
282
  else {
@@ -130,8 +294,7 @@ else if (cmd === "hook") {
130
294
  }
131
295
  }
132
296
  }
133
- console.error(`[MemoryForge] auto-capture: ${updated} updated, ${archived} archived`);
134
- // Auto-capture conversation transcript (best-effort, don't crash hook)
297
+ console.error(`[MemoryForge] ${all.length} memories maintained, ${archived} archived`);
135
298
  try {
136
299
  const transcriptResult = captureTranscript();
137
300
  console.error(`[MemoryForge] ${transcriptResult}`);
@@ -139,11 +302,20 @@ else if (cmd === "hook") {
139
302
  catch (err) {
140
303
  console.error(`[MemoryForge] transcript capture failed: ${err.message}`);
141
304
  }
142
- // Purge expired tombstones
143
305
  try {
144
306
  cleanupTombstones();
145
307
  }
146
308
  catch { }
309
+ // Pro: push to cloud before exit so other devices get everything
310
+ if (process.env.SHELBY_API_KEY || getShelbyConfig().apiKey) {
311
+ try {
312
+ await proAutoActivate();
313
+ console.error("[MemoryForge] All memories synced to cloud. Safe to close.");
314
+ }
315
+ catch {
316
+ console.error("[MemoryForge] Cloud sync skipped — will retry next session.");
317
+ }
318
+ }
147
319
  }
148
320
  else if (hookType === "pre-compact") {
149
321
  const s = new MemoryStore();
@@ -167,7 +339,7 @@ else if (cmd === "hook") {
167
339
  additionalContext: preCompactContext,
168
340
  },
169
341
  }));
170
- // Safety net: capture transcript now (survives forced terminal close)
342
+ // Safety net: capture transcript + sync to cloud (survives forced close / VS Code panel close)
171
343
  try {
172
344
  const preCompactTranscript = captureTranscript();
173
345
  if (!preCompactTranscript.includes("already captured")) {
@@ -177,6 +349,14 @@ else if (cmd === "hook") {
177
349
  catch (err) {
178
350
  console.error(`[MemoryForge] pre-compact transcript capture failed: ${err.message}`);
179
351
  }
352
+ // Pro: push to cloud now — VS Code panel close may skip Stop hook
353
+ if (process.env.SHELBY_API_KEY || getShelbyConfig().apiKey) {
354
+ try {
355
+ await proAutoActivate();
356
+ console.error("[MemoryForge] Pre-compact sync complete — memories safe on cloud.");
357
+ }
358
+ catch { /* non-fatal */ }
359
+ }
180
360
  }
181
361
  else if (hookType === "capture-transcript") {
182
362
  try {
@@ -188,8 +368,21 @@ else if (cmd === "hook") {
188
368
  process.exit(1);
189
369
  }
190
370
  }
371
+ else {
372
+ console.error(`Unknown hook type: ${hookType || "(none)"}`);
373
+ console.error("Hook types: session-start, stop, pre-compact, capture-transcript");
374
+ process.exit(1);
375
+ }
191
376
  process.exit(0);
192
377
  }
378
+ else if (cmd && cmd !== "serve" && cmd !== "start") {
379
+ // Unknown command — show help
380
+ console.error(`Unknown command: ${cmd}`);
381
+ console.error("Usage: memory-forge <command>");
382
+ console.error("Commands: setup, pro [status], list [category], search <query>, stats, hook <type>, capture-transcript, --version");
383
+ console.error("Default (no command): start MCP server (for Claude Code / Cursor integration)");
384
+ process.exit(1);
385
+ }
193
386
  else {
194
387
  // Default: start MCP server
195
388
  startMcpServer();
@@ -204,20 +397,22 @@ function startMcpServer() {
204
397
  }
205
398
  preload();
206
399
  const server = new McpServer({ name: "memory-forge", version: pkg.version });
400
+ const hasPro = !!(process.env.SHELBY_API_KEY || getShelbyConfig().apiKey);
207
401
  // ── memory_store ──────────────────────────────────────────
208
402
  server.registerTool("memory_store", {
209
- title: "存储记忆",
210
- description: "存储一条上下文、知识或偏好到持久记忆中。自动向量化以支持语义检索。",
403
+ title: "Store memory",
404
+ description: "Store a context, knowledge, or preference into persistent memory. Auto-embeds for semantic retrieval.",
211
405
  inputSchema: {
212
- content: z.string().min(1).describe("要存储的记忆内容。"),
213
- category: z.string().default("general").describe("分类标签: user-preference, project-context, decision-log, code-pattern"),
214
- tags: z.array(z.string()).default([]).describe("标签列表。"),
215
- priority: z.number().min(1).max(10).default(5).describe("优先级 1-10"),
406
+ content: z.string().min(1).max(100000).refine((s) => s.trim().length > 0, "Content must not be whitespace-only").describe("Memory content (max 100KB)."),
407
+ category: z.string().default("general").describe("Category: user-preference, project-context, decision-log, code-pattern."),
408
+ tags: z.array(z.string().min(1)).default([]).describe("Tags list."),
409
+ priority: z.number().min(1).max(10).default(5).describe("Priority 1-10."),
410
+ name: z.string().min(1).max(120).optional().describe("Custom name (optional — auto-generated from content if not provided)."),
216
411
  },
217
412
  }, async (params) => {
218
- const { content, category, tags, priority } = params;
413
+ const { content, category, tags, priority, name: customName } = params;
219
414
  const vec = await embed(content);
220
- const name = autoName(content);
415
+ const name = customName || autoName(content);
221
416
  const memory = {
222
417
  id: randomUUID(), name, content, category, tags, priority,
223
418
  vector: vec ? Array.from(vec) : [],
@@ -226,26 +421,32 @@ function startMcpServer() {
226
421
  const merged = await autoMerge(store, memory);
227
422
  if (merged) {
228
423
  saveMemory(merged);
424
+ console.error(`[MemoryForge] Merged duplicate: "${memory.name}" → "${merged.name}" (${(0.8 * 100).toFixed(0)}%+ overlap)`);
229
425
  return { content: [{ type: "text", text: JSON.stringify({
230
- success: true, merged: true, memory_id: merged.id, name: merged.name, preview: content.slice(0, 200),
426
+ success: true, merged: true, memory_id: merged.id, name: merged.name, preview: safeTruncate(content, 200),
231
427
  }) }] };
232
428
  }
233
429
  saveMemory(memory);
234
430
  store.add(memory);
235
431
  // Pro: auto-upload to Shelby cloud
236
- if (process.env.SHELBY_API_KEY) {
432
+ if (hasPro) {
237
433
  uploadMemory(memory).catch(() => { });
238
434
  }
435
+ // Contextual upgrade hint: 20+ memories, no Pro yet
436
+ const hint = !hasPro && store.size() >= 20
437
+ ? "💡 20+ memories! Upgrade to Pro for cross-device sync: memory-forge pro"
438
+ : null;
239
439
  return { content: [{ type: "text", text: JSON.stringify({
240
- success: true, memory_id: memory.id, name: memory.name, preview: content.slice(0, 200),
440
+ success: true, memory_id: memory.id, name: memory.name, preview: safeTruncate(content, 200),
441
+ ...(hint ? { hint } : {}),
241
442
  }) }] };
242
443
  });
243
444
  // ── memory_search ─────────────────────────────────────────
244
445
  server.registerTool("memory_search", {
245
- title: "语义检索记忆",
246
- description: "通过语义相似度搜索相关记忆。向量模型不可用时自动回退到关键词匹配。",
446
+ title: "Search memories",
447
+ description: "Semantic search with vector similarity. Auto-falls back to keyword matching when model unavailable.",
247
448
  inputSchema: {
248
- query: z.string().describe("自然语言查询。"),
449
+ query: z.string().describe("Natural language search query."),
249
450
  limit: z.number().min(1).max(20).default(5),
250
451
  min_similarity: z.number().min(0).max(1).default(0.6),
251
452
  category: z.string().optional(),
@@ -268,7 +469,9 @@ function startMcpServer() {
268
469
  query, count: results.length,
269
470
  results: results.map((r) => ({
270
471
  memory_id: r.id, name: r.name,
271
- similarity: r.similarity?.toFixed(3) ?? 0, content: r.content,
472
+ similarity: typeof r.similarity === "number" ? Number(r.similarity.toFixed(3)) : 0,
473
+ _score: r._score ?? null,
474
+ content: r.content,
272
475
  _method: r._fallback || "vector",
273
476
  })),
274
477
  hint: results.length === 0 ? "No relevant memories found." : null,
@@ -276,9 +479,9 @@ function startMcpServer() {
276
479
  });
277
480
  // ── memory_recall ─────────────────────────────────────────
278
481
  server.registerTool("memory_recall", {
279
- title: "精确获取记忆",
280
- description: "通过 memory_id 精确获取一条记忆的完整内容。",
281
- inputSchema: { memory_id: z.string().describe("记忆 ID") },
482
+ title: "Recall memory",
483
+ description: "Retrieve a single memory by its exact ID with full content.",
484
+ inputSchema: { memory_id: z.string().describe("Memory ID.") },
282
485
  }, async (params) => {
283
486
  const { memory_id } = params;
284
487
  const memory = store.get(memory_id);
@@ -304,35 +507,45 @@ function startMcpServer() {
304
507
  },
305
508
  }, async (params) => {
306
509
  const { category, tags, limit, offset } = params;
510
+ const isFiltered = !!(category || (tags && tags.length > 0));
307
511
  const memories = store.list({ category: category ?? null, tags: tags ?? null, limit, offset });
512
+ // Count total matching (without pagination) when filtered, else full store
513
+ const matchingTotal = isFiltered
514
+ ? store.list({ category: category ?? null, tags: tags ?? null, limit: 10000, offset: 0 }).length
515
+ : store.size();
308
516
  return { content: [{ type: "text", text: JSON.stringify({
309
- total: store.size(), count: memories.length,
517
+ total: matchingTotal, count: memories.length,
310
518
  memories: memories.map((m) => ({
311
519
  memory_id: m.id, name: m.name, category: m.category,
312
520
  tags: m.tags, priority: m.priority, access_count: m.access_count,
313
521
  created_at: m.created_at, last_accessed: m.last_accessed,
314
- preview: m.content.slice(0, 200),
522
+ preview: safeTruncate(m.content, 200),
315
523
  })),
316
524
  }) }] };
317
525
  });
318
526
  // ── memory_forget ─────────────────────────────────────────
319
527
  server.registerTool("memory_forget", {
320
- title: "遗忘记忆",
321
- description: "删除一条记忆,同时删除本地文件。",
322
- inputSchema: { memory_id: z.string().describe("要删除的记忆 ID") },
528
+ title: "Forget memory",
529
+ description: "Delete a memory by ID — removes local file + uploads cloud tombstone.",
530
+ inputSchema: { memory_id: z.string().describe("Memory ID to delete.") },
323
531
  }, async (params) => {
324
532
  const { memory_id } = params;
325
533
  const existed = store.remove(memory_id);
326
- if (existed)
534
+ if (existed) {
327
535
  deleteMemoryFile(memory_id);
536
+ // Pro: upload cloud tombstone to prevent sync resurrection
537
+ if (process.env.SHELBY_API_KEY || getShelbyConfig().apiKey) {
538
+ deleteBlob(getBlobName(memory_id)).catch(() => { });
539
+ }
540
+ }
328
541
  return { content: [{ type: "text", text: JSON.stringify({
329
542
  success: existed, memory_id, action: existed ? "deleted" : "not_found",
330
543
  }) }] };
331
544
  });
332
545
  // ── memory_context ────────────────────────────────────────
333
546
  server.registerTool("memory_context", {
334
- title: "加载上下文",
335
- description: "加载当前会话的上下文——返回最近创建或访问的高优先级记忆。",
547
+ title: "Load context",
548
+ description: "Load current session context — returns top recent/high-priority memories.",
336
549
  inputSchema: { limit: z.number().min(1).max(20).default(5) },
337
550
  }, async (params) => {
338
551
  const { limit } = params;
@@ -343,11 +556,11 @@ function startMcpServer() {
343
556
  });
344
557
  // ── memory_export ─────────────────────────────────────────
345
558
  server.registerTool("memory_export", {
346
- title: "导出记忆",
347
- description: "导出记忆为可移植格式(JSON Markdown)。Free 用户换电脑时手动带走记忆,Pro 用户备份。不指定 memory_ids 则导出全部。",
559
+ title: "Export memories",
560
+ description: "Export memories to portable JSON or Markdown. Free users can move between machines; Pro users can backup. Exports all if no memory_ids specified.",
348
561
  inputSchema: {
349
- memory_ids: z.array(z.string()).optional().describe("要导出的记忆 ID 列表。不指定则导出全部。"),
350
- format: z.enum(["json", "markdown"]).default("json").describe("导出格式: json(结构化)或 markdown(人类可读)。"),
562
+ memory_ids: z.array(z.string()).optional().describe("Memory IDs to export. Exports all if not specified."),
563
+ format: z.enum(["json", "markdown"]).default("json").describe("Export format: json (structured) or markdown (human-readable)."),
351
564
  },
352
565
  }, async (params) => {
353
566
  const { memory_ids, format } = params;
@@ -357,6 +570,7 @@ function startMcpServer() {
357
570
  if (memories.length === 0) {
358
571
  return { content: [{ type: "text", text: JSON.stringify({
359
572
  exported: 0, message: "No memories to export.",
573
+ ...(!hasPro ? { hint: "💡 Pro auto-syncs across devices — no manual export needed: memory-forge pro" } : {}),
360
574
  }) }] };
361
575
  }
362
576
  let output;
@@ -377,7 +591,7 @@ function startMcpServer() {
377
591
  else {
378
592
  output = JSON.stringify({
379
593
  exported_at: new Date().toISOString(),
380
- version: "memory-forge-1.0",
594
+ version: `memory-forge-${pkg.version}`,
381
595
  count: memories.length,
382
596
  memories: memories.map((m) => ({
383
597
  id: m.id,
@@ -390,16 +604,19 @@ function startMcpServer() {
390
604
  })),
391
605
  }, null, 2);
392
606
  }
393
- return { content: [{ type: "text", text: output }] };
607
+ const hint = !hasPro
608
+ ? "\n\n💡 Pro auto-syncs across devices — no manual export needed: memory-forge pro"
609
+ : "";
610
+ return { content: [{ type: "text", text: output + hint }] };
394
611
  });
395
612
  // ── memory_share ──────────────────────────────────────────
396
613
  server.registerTool("memory_share", {
397
- title: "分享记忆",
398
- description: "将一条记忆打包为可分享的格式,方便发送给队友或其他 Agent 导入。返回一个独立 JSON 包,对方可用 memory_store 重新存入。",
614
+ title: "Share memory",
615
+ description: "Package a single memory into a shareable JSON bundle for teammates or other agents to import via memory_store.",
399
616
  inputSchema: {
400
- memory_id: z.string().describe("要分享的记忆 ID"),
401
- recipient: z.string().optional().describe("接收者名称(可选,写入分享包元数据)。"),
402
- note: z.string().optional().describe("附注消息(可选,写入分享包)。"),
617
+ memory_id: z.string().describe("Memory ID to share."),
618
+ recipient: z.string().optional().describe("Recipient name (optional, written to share metadata)."),
619
+ note: z.string().optional().describe("Optional note attached to the share package."),
403
620
  },
404
621
  }, async (params) => {
405
622
  const { memory_id, recipient, note } = params;
@@ -426,25 +643,82 @@ function startMcpServer() {
426
643
  };
427
644
  return { content: [{ type: "text", text: JSON.stringify(sharePackage, null, 2) }] };
428
645
  });
646
+ // ── memory_update ────────────────────────────────────────
647
+ server.registerTool("memory_update", {
648
+ title: "Update memory",
649
+ description: "Partially update a memory by ID. Only provided fields are changed — unset fields stay untouched.",
650
+ inputSchema: {
651
+ memory_id: z.string().describe("Memory ID to update."),
652
+ content: z.string().min(1).max(100000).refine((s) => s.trim().length > 0, "Content must not be whitespace-only").optional().describe("New content (optional)."),
653
+ category: z.string().optional().describe("New category (optional)."),
654
+ tags: z.array(z.string()).optional().describe("New tags list (optional, replaces all existing tags)."),
655
+ priority: z.number().min(1).max(10).optional().describe("New priority 1-10 (optional)."),
656
+ name: z.string().min(1).max(120).optional().describe("New name (optional — auto-generated if not provided)."),
657
+ },
658
+ }, async (params) => {
659
+ const { memory_id, content, category, tags, priority, name: customName } = params;
660
+ // Guard: at least one optional field must be provided
661
+ if (content === undefined && category === undefined && tags === undefined && priority === undefined && customName === undefined) {
662
+ return { content: [{ type: "text", text: JSON.stringify({
663
+ error: "No fields to update", hint: "Provide at least one of: content, category, tags, priority.",
664
+ }) }] };
665
+ }
666
+ const memory = store.get(memory_id);
667
+ if (!memory) {
668
+ return { content: [{ type: "text", text: JSON.stringify({
669
+ error: "Not found", memory_id, hint: "Use memory_list to find the correct ID.",
670
+ }) }] };
671
+ }
672
+ // Apply partial updates — only override provided fields
673
+ if (content !== undefined) {
674
+ memory.content = content;
675
+ memory.name = customName || autoName(content);
676
+ const vec = await embed(content);
677
+ if (vec)
678
+ memory.vector = Array.from(vec);
679
+ }
680
+ else if (customName !== undefined) {
681
+ memory.name = customName;
682
+ }
683
+ if (category !== undefined)
684
+ memory.category = category;
685
+ if (tags !== undefined)
686
+ memory.tags = tags;
687
+ if (priority !== undefined)
688
+ memory.priority = priority;
689
+ memory.access_count++;
690
+ memory.last_accessed = new Date().toISOString();
691
+ saveMemory(memory);
692
+ store.add(memory); // update vectorCache for search
693
+ // Pro: sync updated memory to cloud
694
+ if (process.env.SHELBY_API_KEY || getShelbyConfig().apiKey) {
695
+ uploadMemory(memory).catch(() => { });
696
+ }
697
+ return { content: [{ type: "text", text: JSON.stringify({
698
+ success: true, memory_id: memory.id, name: memory.name,
699
+ preview: safeTruncate(memory.content, 200),
700
+ updated_fields: Object.keys(params).filter((k) => k !== "memory_id" && params[k] !== undefined),
701
+ }) }] };
702
+ });
429
703
  // ── 启动 ──────────────────────────────────────────────────
430
704
  async function main() {
431
- // Pro: auto-sync on startup (non-blocking server starts regardless)
432
- const proActive = !!process.env.SHELBY_API_KEY;
705
+ // Pro: auto-activate + sync before server starts (so all memories are loaded)
706
+ const proActive = !!(process.env.SHELBY_API_KEY || getShelbyConfig().apiKey);
433
707
  if (proActive) {
434
- console.error("[MemoryForge] Pro: Syncing with Shelby cloud...");
435
- syncAll()
436
- .then(() => {
708
+ try {
709
+ await proAutoActivate();
437
710
  for (const m of loadAllMemories())
438
711
  store.add(m);
439
- console.error(`[MemoryForge] Pro sync complete — ${store.size()} memories`);
440
- })
441
- .catch((err) => console.error("[MemoryForge] Pro sync failed (server still available):", err.message));
712
+ }
713
+ catch (err) {
714
+ console.error("[MemoryForge] Pro sync failed (server still available):", err.message);
715
+ }
442
716
  }
443
717
  const transport = new StdioServerTransport();
444
718
  await server.connect(transport);
445
719
  console.error(`[MemoryForge] MCP Server started — ${store.size()} memories loaded` +
446
- (proActive ? " (Pro: Shelby cloud sync)" : " (Free: local storage)"));
447
- console.error("[MemoryForge] 8 tools: store / search / recall / list / forget / context / export / share");
720
+ (proActive ? " (Pro: cross-device sync)" : " (Free: local storage)"));
721
+ console.error("[MemoryForge] 9 tools: store / search / recall / list / forget / context / export / share / update");
448
722
  }
449
723
  main().catch((err) => {
450
724
  console.error("[MemoryForge] Fatal:", err);