vault-cortex 0.10.2 → 0.10.3-beta.52

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/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/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";
@@ -154,8 +154,9 @@ const reportWrites = (params, prompts) => {
154
154
  /**
155
155
  * Offers to start the container, walking a gate ladder where each failed
156
156
  * gate degrades to instructions instead of an error: daemon running → user
157
- * consents → docker run succeeds → health check passes. Returns true only
158
- * when the server is confirmed up.
157
+ * consents → docker run succeeds → health check passes. Returns "running"
158
+ * when the server is confirmed up, "starting" when the container launched
159
+ * but the health check timed out, or "not-started" when a gate failed.
159
160
  */
160
161
  const offerDockerRun = async (params, deps) => {
161
162
  const { targetDir, port, mode, vaultPath } = params;
@@ -168,11 +169,11 @@ const offerDockerRun = async (params, deps) => {
168
169
  nextStep: `\nThen start the server with:\n ${startHint}`,
169
170
  })
170
171
  : buildDaemonNotRunningMessage(`, then run:\n ${startHint}`));
171
- return false;
172
+ return "not-started";
172
173
  }
173
174
  const startNow = await prompts.confirm("Start the server now?", true);
174
175
  if (!startNow)
175
- return false;
176
+ return "not-started";
176
177
  const containerStarted = docker.dockerRun({
177
178
  mode,
178
179
  envFilePath: join(targetDir, ".env"),
@@ -181,17 +182,18 @@ const offerDockerRun = async (params, deps) => {
181
182
  });
182
183
  if (!containerStarted) {
183
184
  prompts.error("docker run failed — see output above.");
184
- return false;
185
+ return "not-started";
185
186
  }
186
187
  const spinner = prompts.spinner();
187
188
  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);
189
+ const timeoutMs = healthPollTimeoutMs(mode);
190
+ const healthy = await pollHealth({ url: `http://127.0.0.1:${port}/healthz`, timeoutMs }, fetchFn);
189
191
  if (!healthy) {
190
- spinner.stop("Server did not respond within 2 minutes — check: docker logs vault-cortex");
191
- return false;
192
+ spinner.stop(healthTimeoutMessage(mode, timeoutMs));
193
+ return "starting";
192
194
  }
193
195
  spinner.stop("Server is up — health check passed.");
194
- return true;
196
+ return "running";
195
197
  };
196
198
  // Local flow: resolve vault path → resolve target dir → generate token →
197
199
  // write .env → optionally start the container → print connect instructions.
@@ -270,10 +272,16 @@ const runLocalInit = async (flags, deps) => {
270
272
  prompts.log("Generated MCP auth token (saved to .env).");
271
273
  const port = readEnvPort(join(targetDir, ".env"));
272
274
  // --yes is for scripts/CI, so it never starts Docker.
273
- const started = flags.yes
274
- ? false
275
+ const startStatus = flags.yes
276
+ ? "not-started"
275
277
  : await offerDockerRun({ targetDir, port, mode: "local", vaultPath }, deps);
276
- prompts.print(buildLocalConnectMessage({ targetDir, token, started, port, tokenWritten }));
278
+ prompts.print(buildLocalConnectMessage({
279
+ targetDir,
280
+ token,
281
+ startStatus,
282
+ port,
283
+ tokenWritten,
284
+ }));
277
285
  return 0;
278
286
  };
279
287
  // Remote flow (VPS + Obsidian Sync): resolve target dir → PUBLIC_URL →
@@ -346,12 +354,12 @@ const runRemoteInit = async (flags, deps) => {
346
354
  const effectivePublicUrl = readEnvPublicUrl(join(targetDir, ".env")) ?? publicUrl;
347
355
  // Without the sync token the container can't start (init-check-auth fails
348
356
  // and s6 stops it), so only offer docker run when it was provided.
349
- const started = obsidianAuthToken === ""
350
- ? false
357
+ const startStatus = obsidianAuthToken === ""
358
+ ? "not-started"
351
359
  : await offerDockerRun({ targetDir, port, mode: "remote" }, deps);
352
360
  // The container check above hit localhost on this machine; the public URL
353
361
  // is the ingress path clients actually use — probe it too, informationally.
354
- if (started) {
362
+ if (startStatus === "running") {
355
363
  await reportPublicUrlProbe(effectivePublicUrl, {
356
364
  prompts,
357
365
  fetchFn: deps.fetchFn,
@@ -361,7 +369,7 @@ const runRemoteInit = async (flags, deps) => {
361
369
  targetDir,
362
370
  token,
363
371
  publicUrl: effectivePublicUrl,
364
- started,
372
+ startStatus,
365
373
  obsidianTokenMissing: obsidianAuthToken === "",
366
374
  tokenWritten,
367
375
  }));
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.");
package/dist/messages.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { styleText } from "node:util";
2
+ import { CONTAINER_NAME } from "./docker.js";
2
3
  // Strip styling when stdout isn't a color TTY (piped output, CI) or NO_COLOR
3
4
  // is set (any value, including empty — per the NO_COLOR spec) so
4
5
  // captured/redirected output stays plain — no stray escape codes in copied
@@ -50,11 +51,14 @@ const upgradeCommand = (targetDir) => `npx vault-cortex@latest upgrade --dir "${
50
51
  // have. `start` runs the same re-create cycle and pulls the image on demand.
51
52
  export const startCommand = (targetDir) => `npx vault-cortex@latest start --dir "${targetDir}"`;
52
53
  const startServerLine = (targetDir) => `Start the server:\n ${startCommand(targetDir)}`;
53
- /** Remote start line: running, blocked on the missing sync token, or ready to start. */
54
+ const startingInBackgroundLine = () => `The server is starting in the background check progress:\n docker logs ${CONTAINER_NAME}`;
55
+ /** Remote start line: running, starting, blocked on the missing sync token, or ready to start. */
54
56
  const remoteStartLine = (params) => {
55
- const { targetDir, started, obsidianTokenMissing } = params;
56
- if (started)
57
+ const { targetDir, startStatus, obsidianTokenMissing } = params;
58
+ if (startStatus === "running")
57
59
  return "The server is running.";
60
+ if (startStatus === "starting")
61
+ return startingInBackgroundLine();
58
62
  if (obsidianTokenMissing) {
59
63
  return `Fill in OBSIDIAN_AUTH_TOKEN in ${targetDir}/.env, then start the server:\n ${startCommand(targetDir)}`;
60
64
  }
@@ -99,13 +103,14 @@ const curlGuidance = (mcpUrl) => `Clients without OAuth, scripts, and curl send
99
103
  const smokeTest = (healthUrl) => `Smoke test:
100
104
  curl ${healthUrl}`;
101
105
  /**
102
- * Remote health-check block. Started: the CLI verified localhost on the VPS,
103
- * but the public URL is a different check (ingress DNS, TLS, proxy), so the
104
- * command stays, reworded as the works-from-any-device check. Not started:
105
- * the plain smoke test to run after starting.
106
+ * Remote health-check block. Running or starting: the CLI verified localhost
107
+ * on the VPS (or the container is still coming up), but the public URL is a
108
+ * different check (ingress — DNS, TLS, proxy), so the command stays, reworded
109
+ * as the works-from-any-device check. Not started: the plain smoke test to
110
+ * run after starting.
106
111
  */
107
- const remoteHealthCheckBlock = (healthUrl, started) => {
108
- if (started) {
112
+ const remoteHealthCheckBlock = (healthUrl, startStatus) => {
113
+ if (startStatus === "running" || startStatus === "starting") {
109
114
  return `Health check — works from any device that can reach the URL:
110
115
  curl ${healthUrl}`;
111
116
  }
@@ -119,18 +124,20 @@ const updateGuidance = (targetDir) => `Update to the latest release:
119
124
  * actually run.
120
125
  */
121
126
  export const buildLocalConnectMessage = (params) => {
122
- const { targetDir, token, started, port, tokenWritten } = params;
127
+ const { targetDir, token, startStatus, port, tokenWritten } = params;
123
128
  const baseUrl = `http://localhost:${port}`;
124
- const startLine = started
129
+ const startLine = startStatus === "running"
125
130
  ? "The server is running."
126
- : startServerLine(targetDir);
131
+ : startStatus === "starting"
132
+ ? startingInBackgroundLine()
133
+ : startServerLine(targetDir);
127
134
  const tokenLine = tokenBlock({ targetDir, token, tokenWritten });
128
135
  // Once the server is confirmed up, the smoke test is dropped — the CLI just
129
136
  // verified this exact URL, so re-printing it reads as leftover homework.
130
137
  // Assembled as a filtered list so the omission leaves no stray blank line.
131
138
  const nonOauthBlocks = [
132
139
  curlGuidance(`${baseUrl}/mcp`),
133
- started ? undefined : smokeTest(`${baseUrl}/healthz`),
140
+ startStatus === "running" ? undefined : smokeTest(`${baseUrl}/healthz`),
134
141
  ]
135
142
  .filter(Boolean)
136
143
  .join("\n\n");
@@ -187,10 +194,10 @@ ${bottomRule()}`;
187
194
  * handling here.
188
195
  */
189
196
  export const buildRemoteConnectMessage = (params) => {
190
- const { targetDir, token, publicUrl, started, obsidianTokenMissing, tokenWritten, } = params;
197
+ const { targetDir, token, publicUrl, startStatus, obsidianTokenMissing, tokenWritten, } = params;
191
198
  const startLine = remoteStartLine({
192
199
  targetDir,
193
- started,
200
+ startStatus,
194
201
  obsidianTokenMissing,
195
202
  });
196
203
  const tokenLine = tokenBlock({ targetDir, token, tokenWritten });
@@ -227,7 +234,7 @@ ${sectionRule("Non-OAuth")}
227
234
 
228
235
  ${curlGuidance(`${publicUrl}/mcp`)}
229
236
 
230
- ${remoteHealthCheckBlock(`${publicUrl}/healthz`, started)}
237
+ ${remoteHealthCheckBlock(`${publicUrl}/healthz`, startStatus)}
231
238
 
232
239
  ${sectionRule("Settings")}
233
240
 
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.52",
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",