virlow-mcp 3.69.1 → 3.70.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.
Files changed (2) hide show
  1. package/dist/cli.js +117 -23
  2. package/package.json +4 -4
package/dist/cli.js CHANGED
@@ -2121,7 +2121,7 @@ function buildGraph(memories) {
2121
2121
  const key = entityKey(name);
2122
2122
  let entity = entities.get(key);
2123
2123
  if (!entity) {
2124
- entity = { key, name: name.trim(), memoryIds: /* @__PURE__ */ new Set(), edges: [] };
2124
+ entity = { key, name: name.trim(), memoryIds: /* @__PURE__ */ new Set(), mentionIds: /* @__PURE__ */ new Set(), edges: [] };
2125
2125
  entities.set(key, entity);
2126
2126
  spellings.set(key, /* @__PURE__ */ new Map());
2127
2127
  }
@@ -2130,7 +2130,8 @@ function buildGraph(memories) {
2130
2130
  counts.set(name.trim(), (counts.get(name.trim()) ?? 0) + 1);
2131
2131
  return entity;
2132
2132
  };
2133
- for (const memory of memories) {
2133
+ const all = [...memories];
2134
+ for (const memory of all) {
2134
2135
  for (const name of memory.meta.about ?? [])
2135
2136
  touch(name, memory.id);
2136
2137
  for (const relation of memory.meta.relations ?? []) {
@@ -2148,7 +2149,16 @@ function buildGraph(memories) {
2148
2149
  if (best)
2149
2150
  entities.get(key).name = best[0];
2150
2151
  }
2151
- return { entities, edges };
2152
+ const graph = { entities, edges };
2153
+ for (const memory of all) {
2154
+ for (const entity of entitiesMentioned(graph, `${memory.label} ${memory.text}`)) {
2155
+ if (entity.memoryIds.has(memory.id))
2156
+ continue;
2157
+ entity.memoryIds.add(memory.id);
2158
+ entity.mentionIds.add(memory.id);
2159
+ }
2160
+ }
2161
+ return graph;
2152
2162
  }
2153
2163
  function isEntityLike(value) {
2154
2164
  return /^[A-Z0-9]/.test(value.trim());
@@ -2227,7 +2237,8 @@ function supersededBy(graph, relation, at) {
2227
2237
  });
2228
2238
  }
2229
2239
  function graphHealth(graph, memories, at = Date.now()) {
2230
- const unlinked = memories.filter((m) => !(m.meta.about?.length || m.meta.relations?.length)).map((m) => m.id);
2240
+ const mentioned = new Set([...graph.entities.values()].flatMap((e) => [...e.mentionIds]));
2241
+ const unlinked = memories.filter((m) => !(m.meta.about?.length || m.meta.relations?.length) && !mentioned.has(m.id)).map((m) => m.id);
2231
2242
  const names = [...graph.entities.values()];
2232
2243
  const possibleDuplicates = [];
2233
2244
  for (let i = 0; i < names.length; i++) {
@@ -2263,6 +2274,31 @@ function graphHealth(graph, memories, at = Date.now()) {
2263
2274
  }
2264
2275
  return { unlinked, possibleDuplicates, contradictions };
2265
2276
  }
2277
+ function renameEntityInMeta(meta, from, into) {
2278
+ const fromKey = entityKey(from);
2279
+ const names = [...meta.about ?? [], ...(meta.relations ?? []).flatMap((r) => [r.s, r.o])];
2280
+ if (!names.some((n) => entityKey(n) === fromKey))
2281
+ return null;
2282
+ const swap = (n) => entityKey(n) === fromKey ? into.trim() : n;
2283
+ const next = { ...meta };
2284
+ if (meta.about)
2285
+ next.about = uniqueNames(meta.about.map(swap));
2286
+ if (meta.relations) {
2287
+ const byKey = /* @__PURE__ */ new Map();
2288
+ for (const r of meta.relations.map((r2) => ({ ...r2, s: swap(r2.s), o: swap(r2.o) }))) {
2289
+ byKey.set(`${entityKey(r.s)}|${normalizePredicate(r.p)}|${entityKey(r.o)}`, r);
2290
+ }
2291
+ next.relations = [...byKey.values()];
2292
+ }
2293
+ return next;
2294
+ }
2295
+ function uniqueNames(names) {
2296
+ const byKey = /* @__PURE__ */ new Map();
2297
+ for (const n of names)
2298
+ if (!byKey.has(entityKey(n)))
2299
+ byKey.set(entityKey(n), n);
2300
+ return [...byKey.values()];
2301
+ }
2266
2302
 
2267
2303
  // ../../packages/mcp-core/dist/memory-secrets.js
2268
2304
  var PATTERNS = [
@@ -2716,19 +2752,11 @@ var MemoryStore = class {
2716
2752
  await this.sync();
2717
2753
  const key = this.requireSession();
2718
2754
  const startEpoch = this.epoch;
2719
- const fromKey = entityKey(from);
2720
- const swap = (n) => entityKey(n) === fromKey ? into.trim() : n;
2721
2755
  let changed = 0;
2722
2756
  for (const memory of [...this.cache.values()]) {
2723
- const names = [...memory.meta.about ?? [], ...(memory.meta.relations ?? []).flatMap((r) => [r.s, r.o])];
2724
- if (!names.some((n) => entityKey(n) === fromKey))
2757
+ const meta = renameEntityInMeta(memory.meta, from, into);
2758
+ if (!meta)
2725
2759
  continue;
2726
- const meta = { ...memory.meta };
2727
- if (memory.meta.about)
2728
- meta.about = unionNames([], memory.meta.about.map(swap));
2729
- if (memory.meta.relations) {
2730
- meta.relations = unionRelations([], memory.meta.relations.map((r) => ({ ...r, s: swap(r.s), o: swap(r.o) })));
2731
- }
2732
2760
  const row = await this.writeNote(key, memory.id, memory.label, memory.text, meta);
2733
2761
  if (this.epoch === startEpoch) {
2734
2762
  this.cache.set(memory.id, { ...memory, meta, updatedAt: row.updatedAt });
@@ -2763,6 +2791,11 @@ var MemoryStore = class {
2763
2791
  const memories = this.list(namespace);
2764
2792
  return graphHealth(this.graph(namespace), memories);
2765
2793
  }
2794
+ /** The namespace of a memory this session has seen, without a sync;
2795
+ * undefined for a global memory or one not cached yet. */
2796
+ namespaceOfCached(id) {
2797
+ return this.cache.get(id)?.namespace;
2798
+ }
2766
2799
  /** Requires a prior sync() in the same unlock to have populated the cache. */
2767
2800
  list(namespace) {
2768
2801
  this.requireSession();
@@ -2979,10 +3012,48 @@ async function memoriesAllowed(api) {
2979
3012
  }
2980
3013
  }
2981
3014
  var MEMORIES_DISABLED_MESSAGE = 'AI memories are switched off for this account. Turn on "AI memories" in Settings \u2192 Security in the Virlow app to use this tool.';
3015
+ async function requireMemoriesSetting(api) {
3016
+ let me;
3017
+ try {
3018
+ me = await api.me();
3019
+ } catch (err) {
3020
+ throw new Error(memoriesUnreadableMessage(err));
3021
+ }
3022
+ if (me.memoriesEnabled !== true)
3023
+ throw new Error(MEMORIES_DISABLED_MESSAGE);
3024
+ }
3025
+ function memoriesUnreadableMessage(err) {
3026
+ const status = err instanceof ApiError ? err.status : void 0;
3027
+ const reason = status === 429 ? "Virlow is rate limiting requests right now" : `Virlow could not be reached (${err instanceof Error ? err.message : String(err)})`;
3028
+ return `Could not check whether AI memories are on: ${reason}. Nothing was changed. Wait a minute and try again; this is not the AI memories switch.`;
3029
+ }
2982
3030
 
2983
3031
  // ../../packages/mcp-core/dist/tools.js
2984
3032
  import { z } from "zod";
2985
3033
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3034
+
3035
+ // ../../packages/mcp-core/dist/memory-pause.js
3036
+ async function capturePausedReason(api, namespace) {
3037
+ try {
3038
+ const me = await api.me();
3039
+ if (me.memoriesPaused === true)
3040
+ return MEMORIES_PAUSED_MESSAGE;
3041
+ if (namespace === void 0)
3042
+ return null;
3043
+ const { folders } = await api.listFolders();
3044
+ const root = folders.find((f) => f.kind === "memories");
3045
+ const paused = folders.some((f) => root !== void 0 && f.parentId === root.id && f.name === namespace && f.memoryPaused === true);
3046
+ return paused ? projectPausedMessage(namespace) : null;
3047
+ } catch {
3048
+ return "Not saved: could not confirm that saving memories is allowed. Try again shortly.";
3049
+ }
3050
+ }
3051
+ var MEMORIES_PAUSED_MESSAGE = "Not saved: saving memories is paused in the Virlow app. Recall still works; do not retry or ask the user to save this another way.";
3052
+ function projectPausedMessage(namespace) {
3053
+ return `Not saved: saving memories is paused for the project "${namespace}" in the Virlow app. Recall still works; do not retry or ask the user to save this another way.`;
3054
+ }
3055
+
3056
+ // ../../packages/mcp-core/dist/tools.js
2986
3057
  function textResult(text) {
2987
3058
  return { content: [{ type: "text", text }] };
2988
3059
  }
@@ -3109,9 +3180,14 @@ function buildServer(opts) {
3109
3180
  const requireMemories = async () => {
3110
3181
  if (!vault.isUnlocked())
3111
3182
  return;
3112
- if (!await memoriesAllowed(api)) {
3113
- throw new Error(MEMORIES_DISABLED_MESSAGE);
3114
- }
3183
+ await requireMemoriesSetting(api);
3184
+ };
3185
+ const requireCapture = async (namespace) => {
3186
+ if (!vault.isUnlocked())
3187
+ return;
3188
+ const reason = await capturePausedReason(api, namespace);
3189
+ if (reason)
3190
+ throw new Error(reason);
3115
3191
  };
3116
3192
  const shownMemories = /* @__PURE__ */ new Set();
3117
3193
  const relatedMemories = async (text) => {
@@ -3159,6 +3235,7 @@ function buildServer(opts) {
3159
3235
  if (args.valid_from !== void 0)
3160
3236
  meta.validFrom = args.valid_from;
3161
3237
  await requireMemories();
3238
+ await requireCapture(args.namespace);
3162
3239
  const result = await memoryStore.add(args.label, args.text, args.namespace, meta);
3163
3240
  let message = result.action === "updated" ? `updated existing memory ${result.id}` : `stored ${result.id}`;
3164
3241
  if (result.closed.length > 0) {
@@ -3199,12 +3276,18 @@ ${formatSyncStats(stats)}`);
3199
3276
  description: "List cached memories, optionally filtered by namespace. Syncs with the server first so the list reflects the latest state across devices. Requires the vault to be unlocked.",
3200
3277
  inputSchema: {
3201
3278
  namespace: z.string().max(100).optional(),
3202
- limit: z.number().int().min(1).max(50).optional()
3279
+ limit: z.number().int().min(1).max(50).optional(),
3280
+ unlinked_only: z.boolean().optional().describe("Only memories linked to no entity, for organising them with link_memory.")
3203
3281
  }
3204
3282
  }, wrap(async (args) => {
3205
3283
  await requireMemories();
3206
3284
  const stats = await memoryStore.sync();
3207
- const items = memoryStore.list(args.namespace).slice(0, args.limit ?? 50);
3285
+ let all = memoryStore.list(args.namespace);
3286
+ if (args.unlinked_only) {
3287
+ const unlinked = new Set((await memoryStore.health(args.namespace)).unlinked);
3288
+ all = all.filter((m) => unlinked.has(m.id));
3289
+ }
3290
+ const items = all.slice(0, args.limit ?? 50);
3208
3291
  const lines = items.map((m, i) => `${i + 1}. [${m.id}] ${m.label} \u2014 ${m.text}${m.namespace ? ` (namespace: ${m.namespace})` : ""}${formatMeta(m.meta)}`);
3209
3292
  const body = lines.length > 0 ? lines.join("\n") : "(no memories found)";
3210
3293
  return textResult(`${body}
@@ -3241,6 +3324,7 @@ ${formatSyncStats(stats)}`);
3241
3324
  }
3242
3325
  }, wrap(async (args) => {
3243
3326
  await requireMemories();
3327
+ await requireCapture(memoryStore.namespaceOfCached(args.id));
3244
3328
  const closed = await memoryStore.link(args.id, {
3245
3329
  ...args.entities && { about: args.entities },
3246
3330
  ...args.relations && { relations: args.relations }
@@ -3256,6 +3340,7 @@ ${formatSyncStats(stats)}`);
3256
3340
  }
3257
3341
  }, wrap(async (args) => {
3258
3342
  await requireMemories();
3343
+ await requireCapture();
3259
3344
  const changed = await memoryStore.mergeEntities(args.from, args.into);
3260
3345
  return textResult(`merged "${args.from}" into "${args.into}" in ${changed} memor${changed === 1 ? "y" : "ies"}`);
3261
3346
  }));
@@ -3269,6 +3354,9 @@ ${formatSyncStats(stats)}`);
3269
3354
  await requireMemories();
3270
3355
  const { entities, facts } = await memoryStore.context(args.namespace, args.limit ?? 12);
3271
3356
  const parts = [];
3357
+ const paused = await capturePausedReason(api, args.namespace);
3358
+ if (paused)
3359
+ parts.push("Saving memories is paused (recall only). Do not call add_memory this session.");
3272
3360
  if (entities.length) {
3273
3361
  parts.push("Key entities: " + entities.map((e) => `${e.name} (${e.facts})`).join(", "));
3274
3362
  }
@@ -3297,17 +3385,23 @@ ${formatSyncStats(stats)}`);
3297
3385
  label: z.string().min(1).max(80).optional(),
3298
3386
  text: z.string().min(1).max(4e3).optional(),
3299
3387
  tags: z.array(z.string().max(50)).max(20).optional(),
3300
- confidence: z.string().max(20).optional()
3388
+ confidence: z.string().max(20).optional(),
3389
+ valid_from: z.string().max(40).optional().describe("When the fact became true (YYYY-MM-DD)."),
3390
+ valid_to: z.string().max(40).optional().describe("When the fact stopped being true (YYYY-MM-DD). It is kept as history, not deleted.")
3301
3391
  }
3302
3392
  }, wrap(async (args) => {
3303
- if (args.label === void 0 && args.text === void 0 && args.tags === void 0 && args.confidence === void 0) {
3304
- throw new Error("update_memory needs at least one of label, text, tags, or confidence");
3393
+ if (args.label === void 0 && args.text === void 0 && args.tags === void 0 && args.confidence === void 0 && args.valid_from === void 0 && args.valid_to === void 0) {
3394
+ throw new Error("update_memory needs at least one of label, text, tags, confidence, valid_from or valid_to");
3305
3395
  }
3306
3396
  const meta = {};
3307
3397
  if (args.tags !== void 0)
3308
3398
  meta.tags = args.tags;
3309
3399
  if (args.confidence !== void 0)
3310
3400
  meta.confidence = args.confidence;
3401
+ if (args.valid_from !== void 0)
3402
+ meta.validFrom = args.valid_from;
3403
+ if (args.valid_to !== void 0)
3404
+ meta.validTo = args.valid_to;
3311
3405
  const changes = { meta };
3312
3406
  if (args.label !== void 0)
3313
3407
  changes.label = args.label;
@@ -4005,7 +4099,7 @@ function startUnlockServer(api, vault, opts = {}) {
4005
4099
  }
4006
4100
 
4007
4101
  // src/version.ts
4008
- var VERSION = true ? "3.69.1" : "dev";
4102
+ var VERSION = true ? "3.70.1" : "dev";
4009
4103
 
4010
4104
  // src/server.ts
4011
4105
  function defaultEmbeddingCacheDir() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "virlow-mcp",
3
- "version": "3.69.1",
3
+ "version": "3.70.1",
4
4
  "description": "Local MCP server for Virlow Secure Notes: end-to-end-encrypted AI memories and notes for Cursor, Claude, Codex, and any MCP client.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -34,9 +34,9 @@
34
34
  "esbuild": "^0.25.0",
35
35
  "typescript": "^5.8.0",
36
36
  "vitest": "^3.0.0",
37
- "@batalabs/virlow-mcp-core": "3.69.1",
38
- "@batalabs/virlow-memory": "3.69.1",
39
- "@batalabs/virlow-crypto": "3.69.1"
37
+ "@batalabs/virlow-memory": "3.70.1",
38
+ "@batalabs/virlow-mcp-core": "3.70.1",
39
+ "@batalabs/virlow-crypto": "3.70.1"
40
40
  },
41
41
  "scripts": {
42
42
  "build": "node build.mjs",