vault-cortex 0.10.2 → 0.10.3-beta.54

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/README.md CHANGED
@@ -49,9 +49,9 @@ What it does:
49
49
  - **Remote** — a VPS with [Obsidian Sync](https://obsidian.md/sync),
50
50
  reachable from any device
51
51
  2. Offers the most common optional settings — memory layer and folder,
52
- daily notes folder and format, file tools, semantic search, port,
53
- timezone (plus sync direction for remote) — press enter to keep the
54
- defaults, or pick the ones you want to change
52
+ daily notes folder and format, file tools, read-only mode, semantic
53
+ search, port, timezone (plus sync direction for remote) — press enter
54
+ to keep the defaults, or pick the ones you want to change
55
55
  3. Generates a `.env` file with a securely generated `MCP_AUTH_TOKEN`
56
56
  4. Optionally starts the container and waits for the health check
57
57
  5. Prints your connection details — the MCP URL, your auth token, and how to
package/dist/docker.js CHANGED
@@ -86,7 +86,10 @@ export const buildDockerRunArgs = (params) => {
86
86
  args.push("--health-interval", "15s");
87
87
  args.push("--health-timeout", "5s");
88
88
  args.push("--health-retries", "5");
89
- args.push("--health-start-period", "60s");
89
+ // 180s: the remote image's init chain runs the first vault sync to
90
+ // completion before the MCP server boots, so /healthz appears late on
91
+ // fresh deploys.
92
+ args.push("--health-start-period", "180s");
90
93
  args.push("--log-driver", "json-file");
91
94
  args.push("--log-opt", "max-size=10m");
92
95
  args.push("--log-opt", "max-file=3");
@@ -191,6 +194,28 @@ export const probeHealth = async (params, fetchFn) => {
191
194
  return false;
192
195
  }
193
196
  };
197
+ /**
198
+ * Health-poll budget by deployment mode. Remote gets a longer window because
199
+ * the container's init chain runs the first vault sync to completion before
200
+ * the server boots — /healthz can legitimately take minutes to appear on a
201
+ * fresh deploy (matches the 180s container health start period plus boot
202
+ * margin).
203
+ */
204
+ export const healthPollTimeoutMs = (mode) => {
205
+ return mode === "remote" ? 240_000 : 120_000;
206
+ };
207
+ /** Health-poll failure message with the duration derived from the actual
208
+ * timeout, so mode-specific budgets can't drift out of the copy. Remote
209
+ * adds a first-sync hint: the container's init chain retries the first
210
+ * sync with no upper time bound, so an expired poll does not mean the
211
+ * server failed — it may flip healthy after the CLI stops waiting. */
212
+ export const healthTimeoutMessage = (mode, timeoutMs) => {
213
+ const baseMessage = `Server did not respond within ${timeoutMs / 60_000} minutes — check: docker logs ${CONTAINER_NAME}`;
214
+ if (mode === "remote") {
215
+ return `${baseMessage} (a long first sync may still be running — the container keeps starting in the background)`;
216
+ }
217
+ return baseMessage;
218
+ };
194
219
  /**
195
220
  * Polls the health endpoint until it responds OK or the timeout elapses.
196
221
  * The first `docker run` pulls the image, so the default window is generous.
package/dist/env.js CHANGED
@@ -58,6 +58,15 @@ MEMORY_ENABLED=true
58
58
  # Enable or disable file tools — vault_read_file and vault_list_files (default: true).
59
59
  # Set to false when Obsidian Sync has attachment syncing disabled.
60
60
  FILE_TOOLS_ENABLED=true
61
+ # Run the server in read-only mode (default: false).
62
+ # Set to true to hide every tool that changes the vault — clients can only
63
+ # read and search. The memory folder is not auto-created in this mode.
64
+ READONLY_MODE=false
65
+ # Hide individual tools by name, comma-separated (default: none hidden).
66
+ # Names match the README tools table: https://github.com/aliasunder/vault-cortex#tools
67
+ # Subtractive only — it cannot re-enable a tool another setting hides; an
68
+ # unknown tool name stops the server at startup so typos surface immediately.
69
+ # DISABLED_TOOLS=vault_delete_note,vault_move_note
61
70
  # Memory folder name in your vault (default: About Me).
62
71
  MEMORY_DIR=About Me
63
72
 
@@ -160,6 +169,15 @@ MEMORY_ENABLED=true
160
169
  # Enable or disable file tools — vault_read_file and vault_list_files (default: true).
161
170
  # Set to false when Obsidian Sync has attachment syncing disabled.
162
171
  FILE_TOOLS_ENABLED=true
172
+ # Run the server in read-only mode (default: false).
173
+ # Set to true to hide every tool that changes the vault — clients can only
174
+ # read and search. The memory folder is not auto-created in this mode.
175
+ READONLY_MODE=false
176
+ # Hide individual tools by name, comma-separated (default: none hidden).
177
+ # Names match the README tools table: https://github.com/aliasunder/vault-cortex#tools
178
+ # Subtractive only — it cannot re-enable a tool another setting hides; an
179
+ # unknown tool name stops the server at startup so typos surface immediately.
180
+ # DISABLED_TOOLS=vault_delete_note,vault_move_note
163
181
  # Memory folder name in your vault (default: About Me).
164
182
  MEMORY_DIR=About Me
165
183
 
package/dist/init.js CHANGED
@@ -3,7 +3,7 @@ import { join, resolve } from "node:path";
3
3
  import { buildLocalEnv, buildRemoteEnv } from "./env.js";
4
4
  import { captureObsidianToken } from "./get-sync-token.js";
5
5
  import { buildDaemonNotRunningMessage, buildDockerNotInstalledMessage, buildLocalConnectMessage, buildRemoteConnectMessage, startCommand, } from "./messages.js";
6
- import { pollHealth } from "./docker.js";
6
+ import { healthPollTimeoutMs, healthTimeoutMessage, pollHealth, } from "./docker.js";
7
7
  import { reportPublicUrlProbe } from "./lifecycle.js";
8
8
  import { applyOptionalSettings, askOptionalSettings, derivePublicUrlOverride, } from "./optional-settings.js";
9
9
  import { buildFilesToWrite, readEnvPort, readEnvPublicUrl, writeFiles, } from "./scaffold.js";
@@ -185,9 +185,10 @@ const offerDockerRun = async (params, deps) => {
185
185
  }
186
186
  const spinner = prompts.spinner();
187
187
  spinner.start("Waiting for the server to come up (first run may take a moment)");
188
- const healthy = await pollHealth({ url: `http://127.0.0.1:${port}/healthz` }, fetchFn);
188
+ const timeoutMs = healthPollTimeoutMs(mode);
189
+ const healthy = await pollHealth({ url: `http://127.0.0.1:${port}/healthz`, timeoutMs }, fetchFn);
189
190
  if (!healthy) {
190
- spinner.stop("Server did not respond within 2 minutes — check: docker logs vault-cortex");
191
+ spinner.stop(healthTimeoutMessage(mode, timeoutMs));
191
192
  return false;
192
193
  }
193
194
  spinner.stop("Server is up — health check passed.");
package/dist/lifecycle.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { join, resolve } from "node:path";
2
- import { CONTAINER_NAME, pollHealth, probeHealth, } from "./docker.js";
2
+ import { CONTAINER_NAME, healthPollTimeoutMs, healthTimeoutMessage, pollHealth, probeHealth, } from "./docker.js";
3
3
  import { buildDaemonNotRunningMessage, buildDockerNotInstalledMessage, } from "./messages.js";
4
4
  import { detectMode, hasEnvPublicUrl, readEnvPort, readEnvPublicUrl, readEnvVaultPath, } from "./scaffold.js";
5
5
  import { expandTilde } from "./vault.js";
@@ -123,12 +123,13 @@ export const recreateContainer = async (params, deps) => {
123
123
  }
124
124
  const spinner = prompts.spinner();
125
125
  spinner.start("Waiting for the server to come up");
126
+ const timeoutMs = healthTimeoutMs ?? healthPollTimeoutMs(deployment.mode);
126
127
  const healthy = await pollHealth({
127
128
  url: `http://127.0.0.1:${deployment.port}/healthz`,
128
- timeoutMs: healthTimeoutMs,
129
+ timeoutMs,
129
130
  }, fetchFn);
130
131
  if (!healthy) {
131
- spinner.stop(`Server did not respond within 2 minutes — check: docker logs ${CONTAINER_NAME}`);
132
+ spinner.stop(healthTimeoutMessage(deployment.mode, timeoutMs));
132
133
  return 1;
133
134
  }
134
135
  spinner.stop("Server is up — health check passed.");
@@ -37,6 +37,13 @@ const OPTIONAL_SETTINGS = [
37
37
  label: "File tools",
38
38
  question: "Enable file tools (read images, PDFs, and other non-Markdown files)?",
39
39
  },
40
+ {
41
+ kind: "toggle",
42
+ name: "READONLY_MODE",
43
+ label: "Read-only mode",
44
+ question: "Run the server in read-only mode (hide all tools that change the vault)?",
45
+ defaultEnabled: false,
46
+ },
40
47
  {
41
48
  kind: "toggle",
42
49
  name: "EMBEDDING_ENABLED",
@@ -127,11 +134,12 @@ export const derivePublicUrlOverride = (envContent, overrides) => {
127
134
  return { ...overrides, PUBLIC_URL: `http://localhost:${newPort}` };
128
135
  };
129
136
  /**
130
- * The .env spellings the server reads as "off" — env-var's asBool accepts
131
- * 0/1 alongside true/false. An absent or unrecognized value falls to the
132
- * server default, which is enabled for every curated toggle.
137
+ * True unless the .env value is an explicit "off" spelling — env-var's asBool
138
+ * accepts 0/1 alongside true/false. An absent or unrecognized value falls to
139
+ * the server default, declared per-toggle via defaultEnabled (enabled unless
140
+ * stated otherwise).
133
141
  */
134
- const isDisabledToggleValue = (value) => ["false", "0"].includes((value ?? "").toLowerCase());
142
+ const isEnabledToggleValue = (value) => !["false", "0"].includes((value ?? "").toLowerCase());
135
143
  /**
136
144
  * Plain digits in the TCP port range. Number() coercion is not enough:
137
145
  * it accepts "1e4"/"0x1F40"/"+9000", which readEnvPort's /^PORT=(\d+)/
@@ -222,7 +230,15 @@ const askSettingValue = async (params, prompts) => {
222
230
  const { setting, currentValue } = params;
223
231
  switch (setting.kind) {
224
232
  case "toggle": {
225
- const enabled = await prompts.confirm(setting.question, !isDisabledToggleValue(currentValue));
233
+ // An unset or empty var means the server default applies — seed the
234
+ // confirm from defaultEnabled, not from the enabled-unless-"false"
235
+ // heuristic (wrong for default-off toggles like READONLY_MODE).
236
+ // Empty string matters: `READONLY_MODE=` in .env is read as unset
237
+ // by Compose's `${VAR:-default}` and env-var's `.default()`.
238
+ const currentlyEnabled = !currentValue
239
+ ? (setting.defaultEnabled ?? true)
240
+ : isEnabledToggleValue(currentValue);
241
+ const enabled = await prompts.confirm(setting.question, currentlyEnabled);
226
242
  return String(enabled);
227
243
  }
228
244
  case "port":
@@ -258,7 +274,7 @@ export const askOptionalSettings = async (params, prompts) => {
258
274
  const currentValue = readOptionalValue(envContent, setting.name);
259
275
  const requiredToggle = OPTIONAL_SETTINGS.find((candidate) => candidate.name === setting.requiresToggle);
260
276
  const dependencyNote = requiredToggle &&
261
- isDisabledToggleValue(readOptionalValue(envContent, requiredToggle.name))
277
+ !isEnabledToggleValue(readOptionalValue(envContent, requiredToggle.name))
262
278
  ? ` · not used while ${requiredToggle.label} is off`
263
279
  : "";
264
280
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vault-cortex",
3
- "version": "0.10.2",
3
+ "version": "0.10.3-beta.54",
4
4
  "description": "Set up a Vault Cortex MCP server for your Obsidian vault in one command: npx vault-cortex@latest init",
5
5
  "license": "MIT",
6
6
  "type": "module",