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 +26 -1
- package/dist/init.js +25 -17
- package/dist/lifecycle.js +4 -3
- package/dist/messages.js +23 -16
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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
|
|
172
|
+
return "not-started";
|
|
172
173
|
}
|
|
173
174
|
const startNow = await prompts.confirm("Start the server now?", true);
|
|
174
175
|
if (!startNow)
|
|
175
|
-
return
|
|
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
|
|
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
|
|
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(
|
|
191
|
-
return
|
|
192
|
+
spinner.stop(healthTimeoutMessage(mode, timeoutMs));
|
|
193
|
+
return "starting";
|
|
192
194
|
}
|
|
193
195
|
spinner.stop("Server is up — health check passed.");
|
|
194
|
-
return
|
|
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
|
|
274
|
-
?
|
|
275
|
+
const startStatus = flags.yes
|
|
276
|
+
? "not-started"
|
|
275
277
|
: await offerDockerRun({ targetDir, port, mode: "local", vaultPath }, deps);
|
|
276
|
-
prompts.print(buildLocalConnectMessage({
|
|
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
|
|
350
|
-
?
|
|
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 (
|
|
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
|
-
|
|
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
|
|
129
|
+
timeoutMs,
|
|
129
130
|
}, fetchFn);
|
|
130
131
|
if (!healthy) {
|
|
131
|
-
spinner.stop(
|
|
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
|
-
|
|
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,
|
|
56
|
-
if (
|
|
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.
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
* the plain smoke test to
|
|
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,
|
|
108
|
-
if (
|
|
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,
|
|
127
|
+
const { targetDir, token, startStatus, port, tokenWritten } = params;
|
|
123
128
|
const baseUrl = `http://localhost:${port}`;
|
|
124
|
-
const startLine =
|
|
129
|
+
const startLine = startStatus === "running"
|
|
125
130
|
? "The server is running."
|
|
126
|
-
:
|
|
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
|
-
|
|
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,
|
|
197
|
+
const { targetDir, token, publicUrl, startStatus, obsidianTokenMissing, tokenWritten, } = params;
|
|
191
198
|
const startLine = remoteStartLine({
|
|
192
199
|
targetDir,
|
|
193
|
-
|
|
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`,
|
|
237
|
+
${remoteHealthCheckBlock(`${publicUrl}/healthz`, startStatus)}
|
|
231
238
|
|
|
232
239
|
${sectionRule("Settings")}
|
|
233
240
|
|
package/package.json
CHANGED