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