vault-cortex 0.10.1 → 0.10.2-beta.50
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 +10 -8
- package/dist/configure.js +4 -1
- package/dist/docker.js +26 -1
- package/dist/env.js +38 -8
- package/dist/init.js +4 -3
- package/dist/lifecycle.js +4 -3
- package/dist/messages.js +5 -4
- package/dist/optional-settings.js +53 -3
- package/package.json +1 -1
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
|
-
file tools, semantic search, port,
|
|
53
|
-
remote) — press enter to keep the
|
|
54
|
-
change
|
|
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
|
|
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
|
|
@@ -85,11 +85,13 @@ Change optional settings on an existing setup:
|
|
|
85
85
|
npx vault-cortex@latest configure
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
-
Shows the same settings chooser as [`init`](#init)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
live in `.env` too: edit the value there, then
|
|
88
|
+
Shows the same settings chooser as [`init`](#init), pre-filled with your
|
|
89
|
+
current values, saves your picks to `.env`, and offers to restart the
|
|
90
|
+
container so they take effect.
|
|
91
|
+
|
|
92
|
+
Settings not in the chooser live in `.env` too: edit the value there, then
|
|
93
|
+
run [`restart`](#restart). That's also how you clear a daily notes setting
|
|
94
|
+
back to your vault's own configuration — comment out or delete its line.
|
|
93
95
|
|
|
94
96
|
Use `--dir <path>` if your config isn't in `./vault-cortex`.
|
|
95
97
|
|
package/dist/configure.js
CHANGED
|
@@ -20,7 +20,10 @@ export const runConfigure = async (flags, deps) => {
|
|
|
20
20
|
const envContent = readFileSync(envFilePath, "utf8");
|
|
21
21
|
const pickedOverrides = await askOptionalSettings({ mode, envContent }, prompts);
|
|
22
22
|
if (Object.keys(pickedOverrides).length === 0) {
|
|
23
|
-
|
|
23
|
+
// Covers both empty-overrides paths: nothing picked in the chooser, and
|
|
24
|
+
// picked-but-kept (an optionalText prompt left blank logs its own
|
|
25
|
+
// "Kept the current value" line, which this must not contradict).
|
|
26
|
+
prompts.log("No changes to apply.");
|
|
24
27
|
prompts.outro("Done.");
|
|
25
28
|
return 0;
|
|
26
29
|
}
|
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/env.js
CHANGED
|
@@ -61,12 +61,21 @@ FILE_TOOLS_ENABLED=true
|
|
|
61
61
|
# Memory folder name in your vault (default: About Me).
|
|
62
62
|
MEMORY_DIR=About Me
|
|
63
63
|
|
|
64
|
-
#
|
|
65
|
-
#
|
|
64
|
+
# Daily notes folder and filename format (default: read from the
|
|
65
|
+
# vault's .obsidian/daily-notes.json, falling back to "Daily Notes" and
|
|
66
|
+
# YYYY-MM-DD). Folder is any vault-relative path (Journal, Planner/Daily);
|
|
67
|
+
# format takes the same tokens as Obsidian's date format setting.
|
|
68
|
+
# DAILY_NOTES_FOLDER=Journal
|
|
69
|
+
# DAILY_NOTES_FORMAT=YYYY-MM-DD
|
|
70
|
+
|
|
71
|
+
# Comma-separated folders protected from deletion (default: MEMORY_DIR plus
|
|
72
|
+
# the daily notes folder — DAILY_NOTES_FOLDER when set, otherwise "Daily Notes").
|
|
73
|
+
# A custom folder set only in daily-notes.json is not auto-protected.
|
|
66
74
|
# PROTECTED_PATHS=About Me,Daily Notes
|
|
67
75
|
|
|
68
|
-
# Comma-separated folders excluded from orphan detection
|
|
69
|
-
#
|
|
76
|
+
# Comma-separated folders excluded from orphan detection (default: the daily
|
|
77
|
+
# notes folder — DAILY_NOTES_FOLDER when set, otherwise "Daily Notes" — plus
|
|
78
|
+
# Templates and MEMORY_DIR).
|
|
70
79
|
# ORPHAN_EXCLUDE_FOLDERS=Daily Notes,Templates,About Me
|
|
71
80
|
|
|
72
81
|
# URL shown in OAuth discovery metadata
|
|
@@ -154,12 +163,22 @@ FILE_TOOLS_ENABLED=true
|
|
|
154
163
|
# Memory folder name in your vault (default: About Me).
|
|
155
164
|
MEMORY_DIR=About Me
|
|
156
165
|
|
|
157
|
-
#
|
|
158
|
-
#
|
|
166
|
+
# Daily notes folder and filename format (default: read from the
|
|
167
|
+
# vault's .obsidian/daily-notes.json, synced to the server via SYNC_CONFIGS
|
|
168
|
+
# below; falls back to "Daily Notes" and YYYY-MM-DD). Folder is any
|
|
169
|
+
# vault-relative path (Journal, Planner/Daily); format takes the same tokens
|
|
170
|
+
# as Obsidian's date format setting.
|
|
171
|
+
# DAILY_NOTES_FOLDER=Journal
|
|
172
|
+
# DAILY_NOTES_FORMAT=YYYY-MM-DD
|
|
173
|
+
|
|
174
|
+
# Comma-separated folders protected from deletion (default: MEMORY_DIR plus
|
|
175
|
+
# the daily notes folder — DAILY_NOTES_FOLDER when set, otherwise "Daily Notes").
|
|
176
|
+
# A custom folder set only in daily-notes.json is not auto-protected.
|
|
159
177
|
# PROTECTED_PATHS=About Me,Daily Notes
|
|
160
178
|
|
|
161
|
-
# Comma-separated folders excluded from orphan detection
|
|
162
|
-
#
|
|
179
|
+
# Comma-separated folders excluded from orphan detection (default: the daily
|
|
180
|
+
# notes folder — DAILY_NOTES_FOLDER when set, otherwise "Daily Notes" — plus
|
|
181
|
+
# Templates and MEMORY_DIR).
|
|
163
182
|
# ORPHAN_EXCLUDE_FOLDERS=Daily Notes,Templates,About Me
|
|
164
183
|
|
|
165
184
|
# URL shown in OAuth discovery metadata
|
|
@@ -192,6 +211,17 @@ CONFLICT_STRATEGY=merge
|
|
|
192
211
|
|
|
193
212
|
# Sync direction: bidirectional | pull-only | push-only (default: bidirectional).
|
|
194
213
|
SYNC_MODE=bidirectional
|
|
214
|
+
|
|
215
|
+
# Obsidian settings categories to sync into the server's .obsidian/ folder
|
|
216
|
+
# (default: the two the server reads — daily notes settings and community
|
|
217
|
+
# plugin settings such as the Tasks plugin's format; "none" disables).
|
|
218
|
+
# A category only syncs after your desktop pushes it: Obsidian Settings →
|
|
219
|
+
# Sync → "Vault configuration sync" (per device). Some community plugins
|
|
220
|
+
# keep API keys in their settings — server tools never read .obsidian/,
|
|
221
|
+
# but synced settings do live on the server volume. Values: app,
|
|
222
|
+
# appearance, appearance-data, hotkey, core-plugin, core-plugin-data,
|
|
223
|
+
# community-plugin, community-plugin-data — comma-separated.
|
|
224
|
+
SYNC_CONFIGS=core-plugin-data,community-plugin-data
|
|
195
225
|
`;
|
|
196
226
|
// sync:remote-optional:end
|
|
197
227
|
export const buildLocalEnv = (answers) => `# vault-cortex — local quickstart
|
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
|
|
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(
|
|
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
|
|
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
|
@@ -166,8 +166,8 @@ ${nonOauthBlocks}
|
|
|
166
166
|
|
|
167
167
|
${sectionRule("Settings")}
|
|
168
168
|
|
|
169
|
-
Adjust optional settings (memory layer and folder,
|
|
170
|
-
semantic search, port, timezone):
|
|
169
|
+
Adjust optional settings (memory layer and folder, daily notes
|
|
170
|
+
folder and format, file tools, semantic search, port, timezone):
|
|
171
171
|
npx vault-cortex@latest configure --dir "${targetDir}"
|
|
172
172
|
|
|
173
173
|
Or edit ${targetDir}/.env directly — change a value (uncommenting it
|
|
@@ -231,8 +231,9 @@ ${remoteHealthCheckBlock(`${publicUrl}/healthz`, started)}
|
|
|
231
231
|
|
|
232
232
|
${sectionRule("Settings")}
|
|
233
233
|
|
|
234
|
-
Adjust optional settings (memory layer and folder,
|
|
235
|
-
semantic search, port, timezone,
|
|
234
|
+
Adjust optional settings (memory layer and folder, daily notes
|
|
235
|
+
folder and format, file tools, semantic search, port, timezone,
|
|
236
|
+
sync direction):
|
|
236
237
|
npx vault-cortex@latest configure --dir "${targetDir}"
|
|
237
238
|
|
|
238
239
|
Or edit ${targetDir}/.env directly — change a value (uncommenting it
|
|
@@ -17,6 +17,20 @@ const OPTIONAL_SETTINGS = [
|
|
|
17
17
|
defaultValue: "About Me",
|
|
18
18
|
requiresToggle: "MEMORY_ENABLED",
|
|
19
19
|
},
|
|
20
|
+
{
|
|
21
|
+
kind: "optionalText",
|
|
22
|
+
name: "DAILY_NOTES_FOLDER",
|
|
23
|
+
label: "Daily notes folder",
|
|
24
|
+
question: "Vault folder for daily notes:",
|
|
25
|
+
placeholder: "blank = use your vault's daily notes settings",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
kind: "optionalText",
|
|
29
|
+
name: "DAILY_NOTES_FORMAT",
|
|
30
|
+
label: "Daily notes format",
|
|
31
|
+
question: "Filename date format for daily notes (e.g. YYYY-MM-DD):",
|
|
32
|
+
placeholder: "blank = use your vault's daily notes settings",
|
|
33
|
+
},
|
|
20
34
|
{
|
|
21
35
|
kind: "toggle",
|
|
22
36
|
name: "FILE_TOOLS_ENABLED",
|
|
@@ -176,7 +190,34 @@ const askFolder = async (params, prompts) => {
|
|
|
176
190
|
prompts.error("The folder name can't be empty.");
|
|
177
191
|
return askFolder(params, prompts);
|
|
178
192
|
};
|
|
179
|
-
/**
|
|
193
|
+
/**
|
|
194
|
+
* Text prompt for a setting whose absence is meaningful — the server falls
|
|
195
|
+
* back to the vault's own config when the var is unset. Blank-when-unset
|
|
196
|
+
* writes nothing; blank-when-set keeps the current value. Returns undefined
|
|
197
|
+
* on skip or no-op so the caller never rewrites for nothing. No removal
|
|
198
|
+
* path — clearing is a manual .env edit.
|
|
199
|
+
*/
|
|
200
|
+
const askOptionalText = async (params, prompts) => {
|
|
201
|
+
const { question, placeholder, currentValue } = params;
|
|
202
|
+
const answer = (await prompts.text(question, {
|
|
203
|
+
defaultValue: currentValue,
|
|
204
|
+
placeholder: currentValue === undefined
|
|
205
|
+
? placeholder
|
|
206
|
+
: "blank = keep the current value",
|
|
207
|
+
})).trim();
|
|
208
|
+
if (answer !== "" && answer !== currentValue)
|
|
209
|
+
return answer;
|
|
210
|
+
if (currentValue === undefined) {
|
|
211
|
+
prompts.log("Left unset — the server reads this setting from your vault's own config.");
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
prompts.log(`Kept the current value (${currentValue}).`);
|
|
215
|
+
return undefined;
|
|
216
|
+
};
|
|
217
|
+
/**
|
|
218
|
+
* Routes a picked setting to its kind's prompt and returns the .env value —
|
|
219
|
+
* or undefined when an optionalText prompt was left blank (nothing to write).
|
|
220
|
+
*/
|
|
180
221
|
const askSettingValue = async (params, prompts) => {
|
|
181
222
|
const { setting, currentValue } = params;
|
|
182
223
|
switch (setting.kind) {
|
|
@@ -194,6 +235,12 @@ const askSettingValue = async (params, prompts) => {
|
|
|
194
235
|
currentValue,
|
|
195
236
|
defaultValue: setting.defaultValue,
|
|
196
237
|
}, prompts);
|
|
238
|
+
case "optionalText":
|
|
239
|
+
return askOptionalText({
|
|
240
|
+
question: setting.question,
|
|
241
|
+
placeholder: setting.placeholder,
|
|
242
|
+
currentValue,
|
|
243
|
+
}, prompts);
|
|
197
244
|
case "choice":
|
|
198
245
|
return prompts.select(setting.question, setting.choices, currentValue ?? setting.defaultValue);
|
|
199
246
|
}
|
|
@@ -222,12 +269,15 @@ export const askOptionalSettings = async (params, prompts) => {
|
|
|
222
269
|
});
|
|
223
270
|
const pickedNames = await prompts.multiselect("Any optional settings to change? (press enter to skip)", chooserOptions);
|
|
224
271
|
// Sequential prompting: answers are gathered one at a time in the curated
|
|
225
|
-
// order, so the record builds up inside an honest loop.
|
|
272
|
+
// order, so the record builds up inside an honest loop. An undefined answer
|
|
273
|
+
// (an optionalText prompt left blank) writes nothing.
|
|
226
274
|
const overrides = {};
|
|
227
275
|
for (const setting of offeredSettings) {
|
|
228
276
|
if (!pickedNames.includes(setting.name))
|
|
229
277
|
continue;
|
|
230
|
-
|
|
278
|
+
const value = await askSettingValue({ setting, currentValue: readOptionalValue(envContent, setting.name) }, prompts);
|
|
279
|
+
if (value !== undefined)
|
|
280
|
+
overrides[setting.name] = value;
|
|
231
281
|
}
|
|
232
282
|
return overrides;
|
|
233
283
|
};
|
package/package.json
CHANGED