vault-cortex 0.8.0 → 0.9.0

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
@@ -15,12 +15,14 @@ and data files — see the
15
15
  The server runs as a Docker container; this CLI scaffolds the config and
16
16
  manages the container so you don't have to.
17
17
 
18
- ![npx vault-cortex init — the interactive setup wizard picks a mode, finds your vault, generates the config, and starts the server](https://raw.githubusercontent.com/aliasunder/vault-cortex/main/assets/demo-cli-init.gif)
18
+ ![npx vault-cortex init — the interactive setup wizard picks a mode, finds your vault, offers the optional settings, generates the config, and starts the server](https://raw.githubusercontent.com/aliasunder/vault-cortex/main/assets/demo-cli-init.gif)
19
19
 
20
20
  ## Commands
21
21
 
22
22
  - [`init`](#init) — interactive setup: scaffold the config, generate the auth
23
23
  token, start the server
24
+ - [`configure`](#configure) — change optional settings interactively and
25
+ restart to apply them
24
26
  - [`upgrade`](#upgrade) — pull the latest image and re-create the container;
25
27
  your data stays
26
28
  - [`restart`](#restart) — re-create the container so your `.env` edits take
@@ -44,9 +46,14 @@ What it does:
44
46
  - **Local** — Docker on this machine, your vault folder bind-mounted
45
47
  - **Remote** — a VPS with [Obsidian Sync](https://obsidian.md/sync),
46
48
  reachable from any device
47
- 2. Generates a `.env` file with a securely generated `MCP_AUTH_TOKEN`
48
- 3. Optionally starts the container and waits for the health check
49
- 4. Prints your connection details the MCP URL, your auth token, and how to
49
+ 2. Offers the most common optional settings memory layer and folder,
50
+ file tools, semantic search, port, timezone (plus sync direction for
51
+ remote) press enter to keep the defaults, or pick the ones you want to
52
+ change. Skipped when the target directory already has a `.env` (use
53
+ [`configure`](#configure) instead)
54
+ 3. Generates a `.env` file with a securely generated `MCP_AUTH_TOKEN`
55
+ 4. Optionally starts the container and waits for the health check
56
+ 5. Prints your connection details — the MCP URL, your auth token, and how to
50
57
  connect your client
51
58
 
52
59
  Existing files are never overwritten without asking. During a remote setup,
@@ -67,6 +74,22 @@ Non-interactive example:
67
74
  npx vault-cortex@latest init --yes --vault-path /path/to/YourVault
68
75
  ```
69
76
 
77
+ ## configure
78
+
79
+ Change optional settings on an existing setup:
80
+
81
+ ```bash
82
+ npx vault-cortex@latest configure
83
+ ```
84
+
85
+ Shows the same settings chooser as [`init`](#init) — memory layer and
86
+ folder, file tools, semantic search, port, timezone (plus sync direction
87
+ for remote) — pre-filled with your current values, saves your picks to
88
+ `.env`, and offers to restart the container so they take effect. Settings not in the chooser
89
+ live in `.env` too: edit the value there, then run [`restart`](#restart).
90
+
91
+ Use `--dir <path>` if your config isn't in `./vault-cortex`.
92
+
70
93
  ## upgrade
71
94
 
72
95
  Pull the latest image, re-create the container, and verify health:
@@ -0,0 +1,64 @@
1
+ import { readFileSync, writeFileSync } from "node:fs";
2
+ import { recreateContainer, requireInitializedDir, resolveDeployment, } from "./lifecycle.js";
3
+ import { applyOptionalSettings, askOptionalSettings, derivePublicUrlOverride, readOptionalValue, } from "./optional-settings.js";
4
+ /**
5
+ * Interactively changes optional settings in an existing deployment's .env,
6
+ * then offers to re-create the container so the new values take effect (the
7
+ * env-file is only read at container creation). The edit succeeds on its own:
8
+ * a stopped daemon or a declined restart still exits 0 with the settings
9
+ * saved and a restart hint printed.
10
+ */
11
+ export const runConfigure = async (flags, deps) => {
12
+ const { prompts, docker, fetchFn } = deps;
13
+ prompts.intro("vault-cortex configure");
14
+ // Editing settings only needs an init'd .env — the full start validation
15
+ // (VAULT_PATH, PUBLIC_URL) runs later, only when a restart is requested.
16
+ const initialized = requireInitializedDir(flags.dir, prompts);
17
+ if (!initialized)
18
+ return 1;
19
+ const { targetDir, envFilePath, mode } = initialized;
20
+ const envContent = readFileSync(envFilePath, "utf8");
21
+ const pickedOverrides = await askOptionalSettings({ mode, envContent }, prompts);
22
+ if (Object.keys(pickedOverrides).length === 0) {
23
+ prompts.log("No settings selected — nothing changed.");
24
+ prompts.outro("Done.");
25
+ return 0;
26
+ }
27
+ const overrides = derivePublicUrlOverride(envContent, pickedOverrides);
28
+ const changedNames = Object.keys(overrides);
29
+ writeFileSync(envFilePath, applyOptionalSettings(envContent, overrides));
30
+ prompts.log(`Updated ${changedNames.join(", ")} in ${targetDir}/.env.`);
31
+ // A custom PUBLIC_URL is never rewritten (see derivePublicUrlOverride), but
32
+ // a port change can still strand it — surface the consequence non-blocking.
33
+ const currentPublicUrl = readOptionalValue(envContent, "PUBLIC_URL");
34
+ if (pickedOverrides.PORT && !overrides.PUBLIC_URL && currentPublicUrl) {
35
+ prompts.warn(`PORT changed — make sure PUBLIC_URL (${currentPublicUrl}) still reaches the server.`);
36
+ }
37
+ const restartHint = `Apply the new settings with: npx vault-cortex restart --dir "${targetDir}"`;
38
+ if (!docker.isDaemonRunning()) {
39
+ prompts.warn(`Container runtime not running — settings saved.\n${restartHint}`);
40
+ prompts.outro("Done.");
41
+ return 0;
42
+ }
43
+ const restartNow = await prompts.confirm("Restart the container now to apply the new settings?", true);
44
+ if (!restartNow) {
45
+ prompts.log(restartHint);
46
+ prompts.outro("Done.");
47
+ return 0;
48
+ }
49
+ // Resolve from disk after the write so the restart honors the new values
50
+ // (a changed PORT must drive the port mapping and health URL).
51
+ const deployment = resolveDeployment(flags.dir, prompts);
52
+ if (!deployment) {
53
+ // The edit already succeeded — don't let the failed restart read as a
54
+ // failed configure.
55
+ prompts.warn(`The restart did not run — your settings are saved. Fix the issue above, then apply them with: npx vault-cortex restart --dir "${targetDir}"`);
56
+ return 1;
57
+ }
58
+ const exitCode = await recreateContainer({ deployment, healthTimeoutMs: deps.healthTimeoutMs }, { prompts, docker, fetchFn });
59
+ if (exitCode !== 0)
60
+ return exitCode;
61
+ prompts.log("Applied the current .env settings.");
62
+ prompts.outro("Configure complete.");
63
+ return 0;
64
+ };
package/dist/env.js CHANGED
@@ -10,9 +10,10 @@
10
10
  // └─────────────────────────────────────────────────────────────────────────┘
11
11
  // sync:local-optional:begin
12
12
  const LOCAL_OPTIONAL_BLOCK = `# Optional ──────────────────────────────────────────────────
13
- # To change a setting: edit its value (uncommenting it first if needed),
14
- # then apply with "npx vault-cortex restart" (plain docker restart does
15
- # not re-read this file).
13
+ # To change a setting: run "npx vault-cortex configure", or edit its
14
+ # value here (uncommenting it first if needed) and apply with
15
+ # "npx vault-cortex restart" (plain docker restart does not re-read
16
+ # this file).
16
17
 
17
18
  # Public URL for OAuth issuer URL in discovery metadata (default: http://localhost:8000).
18
19
  # Override if you expose the server on a different URL (e.g. via a reverse proxy).
@@ -100,9 +101,10 @@ WINDOWS_MODE=false
100
101
  // └─────────────────────────────────────────────────────────────────────────┘
101
102
  // sync:remote-optional:begin
102
103
  const REMOTE_OPTIONAL_BLOCK = `# Optional ──────────────────────────────────────────────────
103
- # To change a setting: edit its value (uncommenting it first if needed),
104
- # then apply with "npx vault-cortex restart" (plain docker restart does
105
- # not re-read this file).
104
+ # To change a setting: run "npx vault-cortex configure", or edit its
105
+ # value here (uncommenting it first if needed) and apply with
106
+ # "npx vault-cortex restart" (plain docker restart does not re-read
107
+ # this file).
106
108
 
107
109
  # Your IANA timezone — affects daily note resolution and memory timestamps.
108
110
  # TZ=America/New_York
package/dist/init.js CHANGED
@@ -1,8 +1,10 @@
1
+ import { existsSync } from "node:fs";
1
2
  import { join, resolve } from "node:path";
2
3
  import { buildLocalEnv, buildRemoteEnv } from "./env.js";
3
4
  import { captureObsidianToken } from "./get-sync-token.js";
4
5
  import { buildLocalConnectMessage, buildRemoteConnectMessage, } from "./messages.js";
5
6
  import { pollHealth } from "./docker.js";
7
+ import { applyOptionalSettings, askOptionalSettings, derivePublicUrlOverride, } from "./optional-settings.js";
6
8
  import { buildFilesToWrite, readEnvPort, writeFiles, } from "./scaffold.js";
7
9
  import { generateToken } from "./token.js";
8
10
  import { expandTilde, validateVaultPath } from "./vault.js";
@@ -203,10 +205,25 @@ const runLocalInit = async (flags, deps) => {
203
205
  placeholder: DEFAULT_TARGET_DIR,
204
206
  }))));
205
207
  const token = generateToken();
208
+ // Guided optional settings: the chooser reads current values from the
209
+ // generated defaults; enter with nothing picked keeps them all. --yes
210
+ // skips the chooser (non-interactive by contract), and so does an existing
211
+ // .env — the conflict prompt defaults to keeping it, which would discard
212
+ // the answers; settings on an existing deployment are configure's job.
213
+ const defaultEnvContent = buildLocalEnv({ mcpAuthToken: token, vaultPath });
214
+ const envAlreadyExists = existsSync(join(targetDir, ".env"));
215
+ const offerSettingsChooser = !flags.yes && !envAlreadyExists;
216
+ if (!flags.yes && envAlreadyExists) {
217
+ prompts.log('Found an existing .env — settings prompts skipped. Adjust settings with "npx vault-cortex configure".');
218
+ }
219
+ const optionalOverrides = offerSettingsChooser
220
+ ? await askOptionalSettings({ mode: "local", envContent: defaultEnvContent }, prompts)
221
+ : {};
222
+ const envContent = applyOptionalSettings(defaultEnvContent, derivePublicUrlOverride(defaultEnvContent, optionalOverrides));
206
223
  // Conflict policy: identical existing files are skipped silently;
207
224
  // differing ones prompt per file (default keep). --yes never overwrites —
208
225
  // any differing file becomes an exit-1 below, leaving it untouched.
209
- const files = buildFilesToWrite(buildLocalEnv({ mcpAuthToken: token, vaultPath }));
226
+ const files = buildFilesToWrite(envContent);
210
227
  const resolveConflict = flags.yes ? keepExisting : confirmOverwrite(prompts);
211
228
  const results = await writeFiles({ targetDir, files }, resolveConflict);
212
229
  reportWrites({ targetDir, results }, prompts);
@@ -262,13 +279,24 @@ const runRemoteInit = async (flags, deps) => {
262
279
  ? await prompts.password("Vault encryption password:")
263
280
  : undefined;
264
281
  const token = generateToken();
265
- const envContent = buildRemoteEnv({
282
+ // Guided optional settings, mirroring the local flow — remote also offers
283
+ // SYNC_MODE. Remote init is always interactive (no --yes), so only an
284
+ // existing .env skips the chooser here.
285
+ const defaultEnvContent = buildRemoteEnv({
266
286
  mcpAuthToken: token,
267
287
  publicUrl,
268
288
  obsidianAuthToken,
269
289
  vaultName,
270
290
  vaultPassword,
271
291
  });
292
+ const envAlreadyExists = existsSync(join(targetDir, ".env"));
293
+ if (envAlreadyExists) {
294
+ prompts.log('Found an existing .env — settings prompts skipped. Adjust settings with "npx vault-cortex configure".');
295
+ }
296
+ const optionalOverrides = envAlreadyExists
297
+ ? {}
298
+ : await askOptionalSettings({ mode: "remote", envContent: defaultEnvContent }, prompts);
299
+ const envContent = applyOptionalSettings(defaultEnvContent, derivePublicUrlOverride(defaultEnvContent, optionalOverrides));
272
300
  const files = buildFilesToWrite(envContent);
273
301
  const results = await writeFiles({ targetDir, files }, confirmOverwrite(prompts));
274
302
  reportWrites({ targetDir, results }, prompts);
package/dist/lifecycle.js CHANGED
@@ -8,7 +8,7 @@ const DEFAULT_TARGET_DIR = "./vault-cortex";
8
8
  * starting a container: resolves --dir and confirms an init'd .env exists.
9
9
  * Full start-readiness validation lives in resolveDeployment.
10
10
  */
11
- const requireInitializedDir = (dirFlag, prompts) => {
11
+ export const requireInitializedDir = (dirFlag, prompts) => {
12
12
  const targetDir = resolve(expandTilde(dirFlag ?? DEFAULT_TARGET_DIR));
13
13
  const envFilePath = join(targetDir, ".env");
14
14
  const mode = detectMode(envFilePath);
package/dist/main.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { runConfigure } from "./configure.js";
1
2
  import { createDockerRunner } from "./docker.js";
2
3
  import { runGetSyncToken } from "./get-sync-token.js";
3
4
  import { runInit } from "./init.js";
@@ -13,6 +14,11 @@ export const run = async (version) => {
13
14
  docker: createDockerRunner(),
14
15
  fetchFn: fetch,
15
16
  }),
17
+ runConfigure: (flags) => runConfigure(flags, {
18
+ prompts: createPrompts(),
19
+ docker: createDockerRunner(),
20
+ fetchFn: fetch,
21
+ }),
16
22
  runUpgrade: (flags) => runUpgrade(flags, {
17
23
  prompts: createPrompts(),
18
24
  docker: createDockerRunner(),
package/dist/messages.js CHANGED
@@ -110,10 +110,13 @@ ${smokeTest(`${baseUrl}/healthz`)}
110
110
 
111
111
  ${sectionRule("Settings")}
112
112
 
113
- Optional settings (timezone, memory folder, port, logging) live in
114
- ${targetDir}/.env edit a value (uncommenting it first if needed), then
115
- apply with "npx vault-cortex restart" (plain docker restart does not
116
- re-read .env).
113
+ Adjust optional settings (memory layer and folder, file tools,
114
+ semantic search, port, timezone):
115
+ npx vault-cortex configure --dir "${targetDir}"
116
+
117
+ Or edit ${targetDir}/.env directly — change a value (uncommenting it
118
+ first if needed), then apply with "npx vault-cortex restart" (plain
119
+ docker restart does not re-read .env).
117
120
 
118
121
  ${updateGuidance(targetDir)}
119
122
 
@@ -172,8 +175,11 @@ ${smokeTest(`${publicUrl}/healthz`)}
172
175
 
173
176
  ${sectionRule("Settings")}
174
177
 
175
- Optional settings (timezone, memory folder, port, logging, sync
176
- behavior) live in ${targetDir}/.env edit a value (uncommenting it
178
+ Adjust optional settings (memory layer and folder, file tools,
179
+ semantic search, port, timezone, sync direction):
180
+ npx vault-cortex configure --dir "${targetDir}"
181
+
182
+ Or edit ${targetDir}/.env directly — change a value (uncommenting it
177
183
  first if needed), then apply with "npx vault-cortex restart" (plain
178
184
  docker restart does not re-read .env).
179
185
 
@@ -0,0 +1,233 @@
1
+ import { DEFAULT_PORT } from "./scaffold.js";
2
+ // The curated prompt set — settings users most often want without reading
3
+ // .env comments. Everything else stays documented-only in the generated
4
+ // optional block, deliberately: every extra prompt costs init flow length.
5
+ const OPTIONAL_SETTINGS = [
6
+ {
7
+ kind: "toggle",
8
+ name: "MEMORY_ENABLED",
9
+ label: "Memory layer",
10
+ question: "Enable the memory layer (About Me/ folder + memory tools)?",
11
+ },
12
+ {
13
+ kind: "folder",
14
+ name: "MEMORY_DIR",
15
+ label: "Memory folder",
16
+ question: "Vault folder for the memory files:",
17
+ defaultValue: "About Me",
18
+ requiresToggle: "MEMORY_ENABLED",
19
+ },
20
+ {
21
+ kind: "toggle",
22
+ name: "FILE_TOOLS_ENABLED",
23
+ label: "File tools",
24
+ question: "Enable file tools (read images, PDFs, and other non-Markdown files)?",
25
+ },
26
+ {
27
+ kind: "toggle",
28
+ name: "EMBEDDING_ENABLED",
29
+ label: "Semantic search",
30
+ question: "Enable semantic search embeddings (richer search, slower first startup)?",
31
+ },
32
+ { kind: "port", name: "PORT", label: "Host port" },
33
+ { kind: "timezone", name: "TZ", label: "Timezone" },
34
+ {
35
+ kind: "choice",
36
+ name: "SYNC_MODE",
37
+ label: "Sync direction",
38
+ question: "Obsidian Sync direction:",
39
+ choices: [
40
+ {
41
+ value: "bidirectional",
42
+ label: "Bidirectional",
43
+ hint: "pull remote changes and push server-side edits",
44
+ },
45
+ {
46
+ value: "pull-only",
47
+ label: "Pull-only",
48
+ hint: "receive changes but never push",
49
+ },
50
+ {
51
+ value: "push-only",
52
+ label: "Push-only",
53
+ hint: "push changes but never pull",
54
+ },
55
+ ],
56
+ defaultValue: "bidirectional",
57
+ remoteOnly: true,
58
+ },
59
+ ];
60
+ /** Matches the full active (uncommented) assignment line for a var. */
61
+ const activeLinePattern = (name) => new RegExp(`^${name}=.*$`, "m");
62
+ /** Matches the full commented-out assignment line (`# VAR=...`) for a var. */
63
+ const commentedLinePattern = (name) => new RegExp(`^# ${name}=.*$`, "m");
64
+ /**
65
+ * Reads a var's current value from .env content. A commented-out or missing
66
+ * line returns undefined — both mean "the server uses its built-in default",
67
+ * which is exactly what the prompts need to distinguish. Duplicate lines
68
+ * report the LAST value, matching docker --env-file precedence — hints,
69
+ * prompt seeding, and the PUBLIC_URL derivation must reason from the value
70
+ * that actually takes effect.
71
+ */
72
+ export const readOptionalValue = (envContent, name) => {
73
+ const matches = [...envContent.matchAll(new RegExp(`^${name}=(.*)$`, "gm"))];
74
+ return matches.at(-1)?.[1].trim();
75
+ };
76
+ /**
77
+ * Applies chosen values to .env content as a pure text transform: every
78
+ * active line for the var is replaced (docker --env-file gives the last
79
+ * duplicate precedence, so a single-line replace could leave a stale
80
+ * duplicate winning), a commented-out line is uncommented and replaced, and
81
+ * a var with no line at all (a .env predating the setting) is appended —
82
+ * the chosen value must land in the file, never be silently dropped.
83
+ */
84
+ export const applyOptionalSettings = (envContent, overrides) => Object.entries(overrides).reduce((content, [name, value]) => {
85
+ // Fresh RegExp per use (the /g flag makes instances stateful via
86
+ // lastIndex); function replacements avoid $-pattern interpretation in
87
+ // values, same as patchEnvObsidianToken.
88
+ const everyActiveLine = new RegExp(`^${name}=.*$`, "gm");
89
+ if (activeLinePattern(name).test(content)) {
90
+ return content.replace(everyActiveLine, () => `${name}=${value}`);
91
+ }
92
+ if (commentedLinePattern(name).test(content)) {
93
+ return content.replace(commentedLinePattern(name), () => `${name}=${value}`);
94
+ }
95
+ return `${content.trimEnd()}\n\n${name}=${value}\n`;
96
+ }, envContent);
97
+ /**
98
+ * A PORT override moves the server, and the local quickstart derives
99
+ * PUBLIC_URL (the OAuth issuer) from that port. When the current PUBLIC_URL
100
+ * is exactly the derived http://localhost:<current port> form, it follows
101
+ * the new port — otherwise the advertised OAuth discovery endpoints would
102
+ * point at a port nothing listens on. A custom PUBLIC_URL (reverse proxy,
103
+ * remote domain) is the user's own and is never touched.
104
+ */
105
+ export const derivePublicUrlOverride = (envContent, overrides) => {
106
+ const newPort = overrides.PORT;
107
+ if (!newPort)
108
+ return overrides;
109
+ const currentPort = readOptionalValue(envContent, "PORT") ?? String(DEFAULT_PORT);
110
+ const currentPublicUrl = readOptionalValue(envContent, "PUBLIC_URL");
111
+ if (currentPublicUrl !== `http://localhost:${currentPort}`)
112
+ return overrides;
113
+ return { ...overrides, PUBLIC_URL: `http://localhost:${newPort}` };
114
+ };
115
+ /**
116
+ * The .env spellings the server reads as "off" — env-var's asBool accepts
117
+ * 0/1 alongside true/false. An absent or unrecognized value falls to the
118
+ * server default, which is enabled for every curated toggle.
119
+ */
120
+ const isDisabledToggleValue = (value) => ["false", "0"].includes((value ?? "").toLowerCase());
121
+ /**
122
+ * Plain digits in the TCP port range. Number() coercion is not enough:
123
+ * it accepts "1e4"/"0x1F40"/"+9000", which readEnvPort's /^PORT=(\d+)/
124
+ * would later fail to read back — silently falling to the default port.
125
+ */
126
+ const isValidPort = (value) => {
127
+ if (!/^\d+$/.test(value))
128
+ return false;
129
+ const port = Number(value);
130
+ return port >= 1 && port <= 65535;
131
+ };
132
+ /**
133
+ * The engine's own IANA zone validation: Intl.DateTimeFormat throws a
134
+ * RangeError for an unknown timeZone, so no hand-rolled zone list to drift.
135
+ */
136
+ const isValidTimezone = (value) => {
137
+ try {
138
+ new Intl.DateTimeFormat("en-US", { timeZone: value });
139
+ return true;
140
+ }
141
+ catch {
142
+ return false;
143
+ }
144
+ };
145
+ /** Re-prompts until the answer is a valid port number. */
146
+ const askPort = async (currentValue, prompts) => {
147
+ const answer = (await prompts.text("Host port for the server:", {
148
+ defaultValue: currentValue ?? String(DEFAULT_PORT),
149
+ placeholder: String(DEFAULT_PORT),
150
+ })).trim();
151
+ if (isValidPort(answer))
152
+ return answer;
153
+ prompts.error("PORT must be a whole number between 1 and 65535.");
154
+ return askPort(currentValue, prompts);
155
+ };
156
+ /** Re-prompts until the answer is a zone the runtime recognizes. */
157
+ const askTimezone = async (currentValue, prompts) => {
158
+ const answer = (await prompts.text("Your IANA timezone:", {
159
+ defaultValue: currentValue,
160
+ placeholder: "America/New_York",
161
+ })).trim();
162
+ if (answer !== "" && isValidTimezone(answer))
163
+ return answer;
164
+ prompts.error(`"${answer}" is not a recognized IANA timezone (e.g. America/New_York, Europe/London).`);
165
+ return askTimezone(currentValue, prompts);
166
+ };
167
+ /** Re-prompts until the answer is a non-empty folder name. */
168
+ const askFolder = async (params, prompts) => {
169
+ const { question, currentValue, defaultValue } = params;
170
+ const answer = (await prompts.text(question, {
171
+ defaultValue: currentValue ?? defaultValue,
172
+ placeholder: defaultValue,
173
+ })).trim();
174
+ if (answer !== "")
175
+ return answer;
176
+ prompts.error("The folder name can't be empty.");
177
+ return askFolder(params, prompts);
178
+ };
179
+ /** Routes a picked setting to its kind's prompt and returns the .env value. */
180
+ const askSettingValue = async (params, prompts) => {
181
+ const { setting, currentValue } = params;
182
+ switch (setting.kind) {
183
+ case "toggle": {
184
+ const enabled = await prompts.confirm(setting.question, !isDisabledToggleValue(currentValue));
185
+ return String(enabled);
186
+ }
187
+ case "port":
188
+ return askPort(currentValue, prompts);
189
+ case "timezone":
190
+ return askTimezone(currentValue, prompts);
191
+ case "folder":
192
+ return askFolder({
193
+ question: setting.question,
194
+ currentValue,
195
+ defaultValue: setting.defaultValue,
196
+ }, prompts);
197
+ case "choice":
198
+ return prompts.select(setting.question, setting.choices, currentValue ?? setting.defaultValue);
199
+ }
200
+ };
201
+ /**
202
+ * The guided optional-settings flow shared by init and configure: one
203
+ * multiselect chooser (enter with nothing picked = change nothing), then one
204
+ * prompt per picked setting. Returns the chosen values keyed by var name —
205
+ * the caller owns applying them via applyOptionalSettings.
206
+ */
207
+ export const askOptionalSettings = async (params, prompts) => {
208
+ const { mode, envContent } = params;
209
+ const offeredSettings = OPTIONAL_SETTINGS.filter((setting) => !setting.remoteOnly || mode === "remote");
210
+ const chooserOptions = offeredSettings.map((setting) => {
211
+ const currentValue = readOptionalValue(envContent, setting.name);
212
+ const requiredToggle = OPTIONAL_SETTINGS.find((candidate) => candidate.name === setting.requiresToggle);
213
+ const dependencyNote = requiredToggle &&
214
+ isDisabledToggleValue(readOptionalValue(envContent, requiredToggle.name))
215
+ ? ` · not used while ${requiredToggle.label} is off`
216
+ : "";
217
+ return {
218
+ value: setting.name,
219
+ label: setting.label,
220
+ hint: `${setting.name} · currently ${currentValue || "not set"}${dependencyNote}`,
221
+ };
222
+ });
223
+ const pickedNames = await prompts.multiselect("Any optional settings to change? (press enter to skip)", chooserOptions);
224
+ // Sequential prompting: answers are gathered one at a time in the curated
225
+ // order, so the record builds up inside an honest loop.
226
+ const overrides = {};
227
+ for (const setting of offeredSettings) {
228
+ if (!pickedNames.includes(setting.name))
229
+ continue;
230
+ overrides[setting.name] = await askSettingValue({ setting, currentValue: readOptionalValue(envContent, setting.name) }, prompts);
231
+ }
232
+ return overrides;
233
+ };
package/dist/program.js CHANGED
@@ -15,6 +15,13 @@ export const buildProgram = (options) => {
15
15
  .action(async (flags) => {
16
16
  process.exitCode = await options.runInit(flags);
17
17
  });
18
+ program
19
+ .command("configure")
20
+ .description("Change optional settings interactively and restart to apply them")
21
+ .option("--dir <path>", "directory containing .env (default: ./vault-cortex)")
22
+ .action(async (flags) => {
23
+ process.exitCode = await options.runConfigure(flags);
24
+ });
18
25
  program
19
26
  .command("upgrade")
20
27
  .description("Pull the latest image, re-create the container, and verify health")
package/dist/prompts.js CHANGED
@@ -28,6 +28,9 @@ export const createPrompts = () => ({
28
28
  warn: (message) => clack.log.warn(message),
29
29
  error: (message) => clack.log.error(message),
30
30
  select: async (message, options, initialValue) => exitOnCancel(await clack.select({ message, options, initialValue })),
31
+ // required: false makes an empty submission legal — the Prompts contract
32
+ // promises "no picks" resolves to [] instead of a re-prompt loop.
33
+ multiselect: async (message, options) => exitOnCancel(await clack.multiselect({ message, options, required: false })),
31
34
  text: async (message, options = {}) => exitOnCancel(await clack.text({
32
35
  message,
33
36
  placeholder: options.placeholder,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vault-cortex",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Set up a Vault Cortex MCP server for your Obsidian vault in one command: npx vault-cortex init",
5
5
  "license": "MIT",
6
6
  "type": "module",