virlow-mcp 3.81.0 → 3.82.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.
Files changed (3) hide show
  1. package/README.md +49 -15
  2. package/dist/cli.js +88 -14
  3. package/package.json +4 -4
package/README.md CHANGED
@@ -7,8 +7,8 @@ entire time.
7
7
 
8
8
  ## What it is
9
9
 
10
- virlow-mcp runs on your machine as a local process (`npx -y virlow-mcp`, launched by your
11
- AI client over stdio). It exposes 24 tools for three things:
10
+ virlow-mcp runs on your machine as a local process (`npx -y virlow-mcp@latest`, launched by your
11
+ AI client over stdio). It exposes tools for four things:
12
12
 
13
13
  - **Memories** — short, durable facts ("the user prefers TypeScript", "this repo's CI
14
14
  runs on self-hosted runners") that any AI tool can add, search semantically, and recall
@@ -30,6 +30,16 @@ AI client over stdio). It exposes 24 tools for three things:
30
30
  schema and values in the note's encrypted attrs, so the server sees neither. Properties
31
31
  ship in the Virlow app as of its epic 3, so what is written here is what the user sees in
32
32
  the note, the table, the board and the calendar.
33
+ - **Attachments**: files on a note. `list_attachments`, `add_attachment`,
34
+ `read_attachment`, `rename_attachment`, `move_attachment`, `delete_attachment` and
35
+ `restore_attachment`. Files are encrypted in this process before upload, the same way the
36
+ app does it. `add_attachment` takes `text` or `content_base64` (up to 2 MB), or `path`, an
37
+ absolute path to a file on this machine (up to 35 MB). `read_attachment` returns text and
38
+ small files inline, and with `save_to` writes the decrypted file to disk instead; it never
39
+ replaces an existing file unless `overwrite` is true. `delete_attachment` moves a file to the
40
+ note's Recently deleted, where `restore_attachment` can bring it back for 30 days. `path` and
41
+ `save_to` exist only when this server runs on your machine over stdio (or `--http` on
42
+ loopback); the hosted connector and a non-loopback `--http` take inline content only.
33
43
 
34
44
  Everything is **end-to-end encrypted, zero-knowledge**: the encryption key is derived
35
45
  from your Virlow master password inside this local process and never leaves it. Every
@@ -44,7 +54,7 @@ through a tool call, and never seen by the AI model.
44
54
  ### This is the local server, and that distinction is the whole security model
45
55
 
46
56
  Every claim above is about **this package**, running on **your machine**. Virlow also
47
- offers a **hosted connector**: the same 24 tools, served from Virlow's own servers, so a
57
+ offers a **hosted connector**: the same tools, served from Virlow's own servers, so a
48
58
  client can connect by URL with nothing installed. It is a genuinely different trust model,
49
59
  and the sentence "the key never leaves it" is the part that stops being true:
50
60
 
@@ -66,15 +76,25 @@ guarantee.
66
76
 
67
77
  ## Install / configure
68
78
 
69
- virlow-mcp needs no separate install — `npx -y virlow-mcp` fetches and runs it directly.
79
+ virlow-mcp needs no separate install: `npx -y virlow-mcp@latest` fetches and runs it directly.
70
80
  Add it to your client's MCP config:
71
81
 
72
82
  ### Claude Code
73
83
 
74
84
  ```
75
- claude mcp add virlow -- npx -y virlow-mcp
85
+ claude mcp add virlow -- npx -y virlow-mcp@latest
76
86
  ```
77
87
 
88
+ On Windows, `npx` is a batch script, so wrap it in `cmd /c`:
89
+
90
+ ```
91
+ claude mcp add virlow -- cmd /c npx -y virlow-mcp@latest
92
+ ```
93
+
94
+ Run that from PowerShell or cmd. Git Bash rewrites `/c` into a path; there, edit
95
+ `~/.claude.json` so the server's `"command"` is `"cmd"` and its `"args"` are
96
+ `["/c", "npx", "-y", "virlow-mcp@latest"]`.
97
+
78
98
  ### Claude Desktop
79
99
 
80
100
  Add to your `claude_desktop_config.json`
@@ -86,7 +106,7 @@ Windows: `%APPDATA%\Claude\claude_desktop_config.json`):
86
106
  "mcpServers": {
87
107
  "virlow": {
88
108
  "command": "npx",
89
- "args": ["-y", "virlow-mcp"]
109
+ "args": ["-y", "virlow-mcp@latest"]
90
110
  }
91
111
  }
92
112
  }
@@ -101,7 +121,7 @@ Add to `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (project-local):
101
121
  "mcpServers": {
102
122
  "virlow": {
103
123
  "command": "npx",
104
- "args": ["-y", "virlow-mcp"]
124
+ "args": ["-y", "virlow-mcp@latest"]
105
125
  }
106
126
  }
107
127
  }
@@ -114,7 +134,7 @@ Codex's MCP config is TOML, not JSON. Add to `~/.codex/config.toml`:
114
134
  ```toml
115
135
  [mcp_servers.virlow]
116
136
  command = "npx"
117
- args = ["-y", "virlow-mcp"]
137
+ args = ["-y", "virlow-mcp@latest"]
118
138
  ```
119
139
 
120
140
  ### Self-hosting Virlow
@@ -128,7 +148,7 @@ Claude Desktop / Cursor:
128
148
  "mcpServers": {
129
149
  "virlow": {
130
150
  "command": "npx",
131
- "args": ["-y", "virlow-mcp"],
151
+ "args": ["-y", "virlow-mcp@latest"],
132
152
  "env": { "VIRLOW_API_URL": "https://api.your-domain.com" }
133
153
  }
134
154
  }
@@ -147,7 +167,7 @@ only spawn a command use this package as a pipe:
147
167
  "mcpServers": {
148
168
  "virlow": {
149
169
  "command": "npx",
150
- "args": ["-y", "virlow-mcp", "--connect", "http://127.0.0.1:47821/mcp",
170
+ "args": ["-y", "virlow-mcp@latest", "--connect", "http://127.0.0.1:47821/mcp",
151
171
  "--token-file", "<path the app shows you>"]
152
172
  }
153
173
  }
@@ -156,6 +176,20 @@ only spawn a command use this package as a pipe:
156
176
 
157
177
  In this mode nothing is decrypted in this process. It forwards to the app and back.
158
178
 
179
+ ### Staying up to date
180
+
181
+ With `virlow-mcp@latest` in the command, your client gets the newest release each time it
182
+ starts the server, so restarting the client (or opening a new session) is the whole update.
183
+ Without `@latest`, `npx` can keep running a copy it downloaded earlier; add `@latest` to an
184
+ existing config to fix that. Pin an exact version (`virlow-mcp@3.81.0`) only if you want
185
+ updates to wait for you. The desktop app updates itself and its built-in server, and the
186
+ hosted connector is updated on Virlow's side.
187
+
188
+ The server checks npm once at startup, and the `status` tool and the unlock page say when a
189
+ newer version exists. The request tells npm only that someone at your IP checked
190
+ virlow-mcp's version; it carries no account data. Turn it off with
191
+ `VIRLOW_MCP_NO_UPDATE_CHECK=1`.
192
+
159
193
  ## First use
160
194
 
161
195
  1. Ask your AI client to run the **`unlock`** tool. This opens a browser window pointed
@@ -206,13 +240,13 @@ just give you two versions to keep in sync.
206
240
  ## Reaching your vault from a session that is not on this machine
207
241
 
208
242
  By default `virlow-mcp` speaks stdio, so your AI client spawns it as a child
209
- process and everything stays on one machine. `--http` serves the same 24 tools
243
+ process and everything stays on one machine. `--http` serves the same tools
210
244
  over HTTP instead, so a client that cannot spawn a local process — a cloud
211
245
  coding session, another laptop — can reach this vault:
212
246
 
213
247
  ```bash
214
- npx -y virlow-mcp --http # loopback, random port
215
- npx -y virlow-mcp --http --port 8787 # pick the port
248
+ npx -y virlow-mcp@latest --http # loopback, random port
249
+ npx -y virlow-mcp@latest --http --port 8787 # pick the port
216
250
  ```
217
251
 
218
252
  It prints the URL and the path to a bearer token, generated on first run and
@@ -295,12 +329,12 @@ account cannot be reached, the tools refuse rather than assume consent.
295
329
  unregistered, so they do not appear in `tools/list` at all:
296
330
 
297
331
  ```bash
298
- npx -y virlow-mcp --no-memories
332
+ npx -y virlow-mcp@latest --no-memories
299
333
  ```
300
334
 
301
335
  Use it when you want a connector that structurally cannot touch memories,
302
336
  regardless of the account setting. Notes tools, `unlock`, `lock`, and `status`
303
- are unaffected either way (18 tools instead of 23).
337
+ are unaffected either way.
304
338
 
305
339
  ## Suggested agent rules
306
340
 
package/dist/cli.js CHANGED
@@ -3859,7 +3859,8 @@ function buildServer(opts) {
3859
3859
  server.registerTool("status", {
3860
3860
  description: "Report whether the vault is currently locked or unlocked, and which account it's unlocked as. Call this after unlock to confirm the user finished the browser form, and before any memory/notes tool if you are unsure of the current lock state."
3861
3861
  }, wrap(async () => {
3862
- return textResult(JSON.stringify(vault.status()));
3862
+ const notice = opts.updateNotice?.() ?? null;
3863
+ return textResult(JSON.stringify(notice === null ? vault.status() : { ...vault.status(), update: notice }));
3863
3864
  }));
3864
3865
  const requireMemories = async () => {
3865
3866
  if (!vault.isUnlocked())
@@ -4518,8 +4519,9 @@ async function performUnlock(api, vault, creds) {
4518
4519
  vault.touch();
4519
4520
  };
4520
4521
  }
4521
- function renderForm(nonce, error) {
4522
+ function renderForm(nonce, error, updateNotice) {
4522
4523
  const errorHtml = error ? `<p class="error">${escapeHtml(error)}</p>` : "";
4524
+ const updateHtml = updateNotice ? `<p class="update">${escapeHtml(updateNotice)}</p>` : "";
4523
4525
  return `<!doctype html>
4524
4526
  <html lang="en">
4525
4527
  <head>
@@ -4528,6 +4530,7 @@ function renderForm(nonce, error) {
4528
4530
  <style>
4529
4531
  body { font-family: system-ui, sans-serif; max-width: 420px; margin: 48px auto; padding: 0 16px; color: #1a1a1a; }
4530
4532
  h1 { font-size: 1.4rem; }
4533
+ p.update { color: #555; font-size: 0.9rem; }
4531
4534
  label { display: block; margin-top: 12px; font-weight: 600; }
4532
4535
  input { width: 100%; padding: 8px; margin-top: 4px; box-sizing: border-box; font-size: 1rem; }
4533
4536
  button { margin-top: 20px; padding: 10px 16px; font-size: 1rem; cursor: pointer; }
@@ -4542,6 +4545,7 @@ function renderForm(nonce, error) {
4542
4545
  Virlow API. They are never seen by any AI model or logged.
4543
4546
  </p>
4544
4547
  ${errorHtml}
4548
+ ${updateHtml}
4545
4549
  <form method="POST" action="/unlock?nonce=${encodeURIComponent(nonce)}" autocomplete="off">
4546
4550
  <label for="email">Email</label>
4547
4551
  <input type="email" id="email" name="email" autocomplete="off" required />
@@ -4686,7 +4690,7 @@ function startUnlockServer(api, vault, opts = {}) {
4686
4690
  harden(res);
4687
4691
  res.statusCode = 200;
4688
4692
  res.setHeader("content-type", "text/html; charset=utf-8");
4689
- res.end(renderForm(nonce));
4693
+ res.end(renderForm(nonce, void 0, opts.updateNotice?.() ?? null));
4690
4694
  return;
4691
4695
  }
4692
4696
  if (req.method === "POST") {
@@ -4710,7 +4714,7 @@ function startUnlockServer(api, vault, opts = {}) {
4710
4714
  harden(res);
4711
4715
  res.statusCode = 200;
4712
4716
  res.setHeader("content-type", "text/html; charset=utf-8");
4713
- res.end(renderForm(nonce, message));
4717
+ res.end(renderForm(nonce, message, opts.updateNotice?.() ?? null));
4714
4718
  }
4715
4719
  return;
4716
4720
  }
@@ -4742,25 +4746,30 @@ function startUnlockServer(api, vault, opts = {}) {
4742
4746
  }
4743
4747
 
4744
4748
  // src/version.ts
4745
- var VERSION = true ? "3.81.0" : "dev";
4749
+ var VERSION = true ? "3.82.0" : "dev";
4746
4750
 
4747
4751
  // src/server.ts
4748
4752
  function defaultEmbeddingCacheDir() {
4749
4753
  return path3.join(os.homedir(), ".virlow-mcp", "cache");
4750
4754
  }
4751
- var localBrowserUnlock = {
4752
- description: "Open a local browser window where the USER signs in and enters their Virlow master password. This tool returns immediately once the browser is opened \u2014 it does NOT wait for the user to finish. Call the status tool afterward to confirm the vault unlocked. Never ask the user for their password or account credentials yourself, and never relay a password through this tool or any other \u2014 credentials are entered only in the browser form and never seen by any AI model.",
4753
- async begin({ api, vault }) {
4754
- const { url } = await startUnlockServer(api, vault, { openBrowser: true });
4755
- return `Browser opened at ${url}. Complete the unlock form there, then run status to confirm.`;
4756
- }
4757
- };
4755
+ function localBrowserUnlock(updateNotice) {
4756
+ return {
4757
+ description: "Open a local browser window where the USER signs in and enters their Virlow master password. This tool returns immediately once the browser is opened \u2014 it does NOT wait for the user to finish. Call the status tool afterward to confirm the vault unlocked. Never ask the user for their password or account credentials yourself, and never relay a password through this tool or any other \u2014 credentials are entered only in the browser form and never seen by any AI model.",
4758
+ async begin({ api, vault }) {
4759
+ const { url } = await startUnlockServer(api, vault, {
4760
+ openBrowser: true,
4761
+ ...updateNotice ? { updateNotice } : {}
4762
+ });
4763
+ return `Browser opened at ${url}. Complete the unlock form there, then run status to confirm.`;
4764
+ }
4765
+ };
4766
+ }
4758
4767
  function withLocalDefaults(opts) {
4759
4768
  const { embeddingCacheDir, ...rest } = opts;
4760
4769
  return {
4761
4770
  ...rest,
4762
4771
  serverInfo: { name: "virlow-mcp", version: VERSION },
4763
- unlock: localBrowserUnlock,
4772
+ unlock: localBrowserUnlock(opts.updateNotice),
4764
4773
  createEmbeddingCache: (userId) => new EmbeddingCache({
4765
4774
  dir: embeddingCacheDir ?? defaultEmbeddingCacheDir(),
4766
4775
  userId,
@@ -4828,6 +4837,63 @@ async function runConnectBridge(opts) {
4828
4837
  };
4829
4838
  }
4830
4839
 
4840
+ // src/update-check.ts
4841
+ var REGISTRY_URL = "https://registry.npmjs.org/virlow-mcp/latest";
4842
+ var TIMEOUT_MS = 3e3;
4843
+ var OPT_OUT_ENV = "VIRLOW_MCP_NO_UPDATE_CHECK";
4844
+ function parseVersion(v) {
4845
+ const m = /^(\d+)\.(\d+)\.(\d+)$/.exec(v);
4846
+ return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
4847
+ }
4848
+ function isNewer(candidate, current) {
4849
+ const a = parseVersion(candidate);
4850
+ const b = parseVersion(current);
4851
+ if (!a || !b) return false;
4852
+ for (let i = 0; i < 3; i++) {
4853
+ if (a[i] !== b[i]) return a[i] > b[i];
4854
+ }
4855
+ return false;
4856
+ }
4857
+ function optedOut(env) {
4858
+ const v = env[OPT_OUT_ENV];
4859
+ return v === "1" || v === "true";
4860
+ }
4861
+ function updateCheckApplies(argv, env) {
4862
+ if (optedOut(env)) return false;
4863
+ if (argv.includes("--version") || argv.includes("-v")) return false;
4864
+ if (argv.some((a) => a === "--connect" || a.startsWith("--connect="))) return false;
4865
+ return true;
4866
+ }
4867
+ function startUpdateCheck(opts) {
4868
+ const env = opts.env ?? process.env;
4869
+ const fetchImpl = opts.fetchImpl ?? fetch;
4870
+ const log = opts.log ?? ((line) => process.stderr.write(`${line}
4871
+ `));
4872
+ let text = null;
4873
+ if (optedOut(env)) return { notice: () => null, done: Promise.resolve() };
4874
+ const done = (async () => {
4875
+ try {
4876
+ const res = await fetchImpl(REGISTRY_URL, {
4877
+ signal: AbortSignal.timeout(TIMEOUT_MS),
4878
+ headers: { accept: "application/json" }
4879
+ });
4880
+ if (!res.ok) return;
4881
+ const body = await res.json();
4882
+ const latest = body.version;
4883
+ if (typeof latest === "string" && isNewer(latest, opts.current)) {
4884
+ text = `virlow-mcp ${latest} is available (you are running ${opts.current}). Restart your AI client to update. If your MCP config runs plain virlow-mcp, change it to virlow-mcp@latest.`;
4885
+ }
4886
+ } catch (err) {
4887
+ const why = err instanceof Error ? err.message : String(err);
4888
+ try {
4889
+ log(`virlow-mcp: update check skipped: ${why.replace(/\s+/g, " ")}`);
4890
+ } catch {
4891
+ }
4892
+ }
4893
+ })();
4894
+ return { notice: () => text, done };
4895
+ }
4896
+
4831
4897
  // src/cli.ts
4832
4898
  function flagValue(name) {
4833
4899
  const prefix = `--${name}=`;
@@ -4860,7 +4926,15 @@ async function main() {
4860
4926
  const http2 = process.argv.includes("--http");
4861
4927
  const host = flagValue("host") ?? "127.0.0.1";
4862
4928
  const fileAccess = localFileAccess({ http: http2, host });
4863
- const serverOpts = { apiUrl, modelCacheDir, embeddingCacheDir, memoriesEnabled, fileAccess };
4929
+ const updates = updateCheckApplies(process.argv, process.env) ? startUpdateCheck({ current: VERSION }) : null;
4930
+ const serverOpts = {
4931
+ apiUrl,
4932
+ modelCacheDir,
4933
+ embeddingCacheDir,
4934
+ memoriesEnabled,
4935
+ fileAccess,
4936
+ ...updates ? { updateNotice: () => updates.notice() } : {}
4937
+ };
4864
4938
  const services = createServices2(serverOpts);
4865
4939
  const { server } = buildServer2({ ...serverOpts, services });
4866
4940
  if (http2) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "virlow-mcp",
3
- "version": "3.81.0",
3
+ "version": "3.82.0",
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-crypto": "3.81.0",
38
- "@batalabs/virlow-mcp-core": "3.81.0",
39
- "@batalabs/virlow-memory": "3.81.0"
37
+ "@batalabs/virlow-crypto": "3.82.0",
38
+ "@batalabs/virlow-memory": "3.82.0",
39
+ "@batalabs/virlow-mcp-core": "3.82.0"
40
40
  },
41
41
  "scripts": {
42
42
  "build": "node build.mjs",