From ed970eff677bb5e65741af5bfe0679a908eaf98a Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Thu, 13 Aug 2026 16:49:18 -0400 Subject: [PATCH 01/11] feat(cli): surface daily notes folder and format in the settings chooser Co-Authored-By: Claude Fable 5 --- cli/src/__tests__/configure.test.ts | 41 ++++++++ cli/src/__tests__/optional-settings.test.ts | 106 ++++++++++++++++++++ cli/src/optional-settings.ts | 75 +++++++++++++- 3 files changed, 218 insertions(+), 4 deletions(-) diff --git a/cli/src/__tests__/configure.test.ts b/cli/src/__tests__/configure.test.ts index 0519b2d27..1d2d7937d 100644 --- a/cli/src/__tests__/configure.test.ts +++ b/cli/src/__tests__/configure.test.ts @@ -231,6 +231,47 @@ describe("runConfigure with picked settings", () => { ) }) + it("uncomments the template's daily notes folder line on a typed value", async () => { + // The generated .env carries the var as a commented template line — the + // chooser's value must land by uncommenting it, not by appending a + // duplicate. + const targetDir = makeTempTargetDir() + const envFilePath = join(targetDir, ".env") + writeFileSync( + envFilePath, + `${LOCAL_ENV_CONTENT}\n# DAILY_NOTES_FOLDER=Journal\n# DAILY_NOTES_FORMAT=YYYY-MM-DD\n`, + ) + const scripted = createScriptedPrompts([["DAILY_NOTES_FOLDER"], "Planner"]) + + const exitCode = await runConfigure( + { dir: targetDir }, + { prompts: scripted.prompts, docker: dockerDown, fetchFn: fetchNever }, + ) + + expect(exitCode).toBe(0) + expect(readFileSync(envFilePath, "utf8")).toBe( + `${LOCAL_ENV_CONTENT}\nDAILY_NOTES_FOLDER=Planner\n# DAILY_NOTES_FORMAT=YYYY-MM-DD\n`, + ) + }) + + it("treats a picked-then-blanked daily notes setting as nothing selected", async () => { + const targetDir = makeTempTargetDir() + const envFilePath = writeLocalEnv(targetDir) + const scripted = createScriptedPrompts([["DAILY_NOTES_FOLDER"], ""]) + + const exitCode = await runConfigure( + { dir: targetDir }, + { prompts: scripted.prompts, docker: dockerDown, fetchFn: fetchNever }, + ) + + expect(exitCode).toBe(0) + expect(readFileSync(envFilePath, "utf8")).toBe(LOCAL_ENV_CONTENT) + expect(scripted.logs).toEqual([ + "Left unset — the server keeps reading your vault's daily notes settings.", + "No settings selected — nothing changed.", + ]) + }) + it("keeps the saved change and exits 1 when the .env cannot start a container", async () => { const targetDir = makeTempTargetDir() const envFilePath = join(targetDir, ".env") diff --git a/cli/src/__tests__/optional-settings.test.ts b/cli/src/__tests__/optional-settings.test.ts index 1fb256ada..0f4ce4bce 100644 --- a/cli/src/__tests__/optional-settings.test.ts +++ b/cli/src/__tests__/optional-settings.test.ts @@ -144,6 +144,8 @@ describe("askOptionalSettings chooser", () => { ).toEqual([ "MEMORY_ENABLED", "MEMORY_DIR", + "DAILY_NOTES_FOLDER", + "DAILY_NOTES_FORMAT", "FILE_TOOLS_ENABLED", "EMBEDDING_ENABLED", "PORT", @@ -164,6 +166,8 @@ describe("askOptionalSettings chooser", () => { ).toEqual([ "MEMORY_ENABLED", "MEMORY_DIR", + "DAILY_NOTES_FOLDER", + "DAILY_NOTES_FORMAT", "FILE_TOOLS_ENABLED", "EMBEDDING_ENABLED", "PORT", @@ -185,6 +189,8 @@ describe("askOptionalSettings chooser", () => { ).toEqual([ "MEMORY_ENABLED · currently false", "MEMORY_DIR · currently not set · not used while Memory layer is off", + "DAILY_NOTES_FOLDER · currently not set", + "DAILY_NOTES_FORMAT · currently not set", "FILE_TOOLS_ENABLED · currently not set", "EMBEDDING_ENABLED · currently not set", "PORT · currently 9000", @@ -403,4 +409,104 @@ describe("askOptionalSettings per-setting prompts", () => { ]) expect(overrides).toEqual({ TZ: "Europe/London" }) }) + + it("skips an unset daily notes folder left blank, writing nothing", async () => { + const scripted = createScriptedPrompts([["DAILY_NOTES_FOLDER"], ""]) + + const overrides = await askOptionalSettings( + { mode: "local", envContent: "" }, + scripted.prompts, + ) + + // No pre-filled default when unset — the real default is the vault's + // own config, so the prompt must not offer a concrete value to accept. + expect(scripted.textCalls).toEqual([ + { message: "Vault folder for daily notes:", defaultValue: undefined }, + ]) + expect(overrides).toEqual({}) + expect(scripted.logs).toEqual([ + "Left unset — the server keeps reading your vault's daily notes settings.", + ]) + }) + + it("keeps the current daily notes folder on a blank submit", async () => { + // The prompt resolves an empty submit to its defaultValue (the current + // value), so blank never destroys an existing setting. + const scripted = createScriptedPrompts([["DAILY_NOTES_FOLDER"], ""]) + + const overrides = await askOptionalSettings( + { mode: "local", envContent: "DAILY_NOTES_FOLDER=Journal\n" }, + scripted.prompts, + ) + + expect(scripted.textCalls).toEqual([ + { message: "Vault folder for daily notes:", defaultValue: "Journal" }, + ]) + expect(overrides).toEqual({ DAILY_NOTES_FOLDER: "Journal" }) + }) + + it("collects a typed daily notes format, trimmed", async () => { + const scripted = createScriptedPrompts([ + ["DAILY_NOTES_FORMAT"], + " DD-MM-YYYY ", + ]) + + const overrides = await askOptionalSettings( + { mode: "local", envContent: "" }, + scripted.prompts, + ) + + expect(scripted.textCalls).toEqual([ + { + message: "Filename date format for daily notes (e.g. YYYY-MM-DD):", + defaultValue: undefined, + }, + ]) + expect(overrides).toEqual({ DAILY_NOTES_FORMAT: "DD-MM-YYYY" }) + }) + + it("does not clobber a set daily notes folder on whitespace input", async () => { + // Whitespace defeats the empty-submit-resolves-to-default behavior and + // trims to empty — the skip path must leave the existing value alone + // rather than write an empty one. + const scripted = createScriptedPrompts([["DAILY_NOTES_FOLDER"], " "]) + + const overrides = await askOptionalSettings( + { mode: "local", envContent: "DAILY_NOTES_FOLDER=Journal\n" }, + scripted.prompts, + ) + + expect(overrides).toEqual({}) + }) + + it("records only the typed setting when one of a pair is left blank", async () => { + const scripted = createScriptedPrompts([ + ["DAILY_NOTES_FOLDER", "DAILY_NOTES_FORMAT"], + "", // folder left blank — skipped + "YYYY/MM/DD", // format typed + ]) + + const overrides = await askOptionalSettings( + { mode: "local", envContent: "" }, + scripted.prompts, + ) + + expect(overrides).toEqual({ DAILY_NOTES_FORMAT: "YYYY/MM/DD" }) + }) + + it("shows a set daily notes folder in its chooser hint", async () => { + const scripted = createScriptedPrompts([[]]) + + await askOptionalSettings( + { mode: "local", envContent: "DAILY_NOTES_FOLDER=Journal\n" }, + scripted.prompts, + ) + + const dailyNotesFolderOption = scripted.multiselectCalls[0].options.find( + (option) => option.value === "DAILY_NOTES_FOLDER", + ) + expect(dailyNotesFolderOption?.hint).toBe( + "DAILY_NOTES_FOLDER · currently Journal", + ) + }) }) diff --git a/cli/src/optional-settings.ts b/cli/src/optional-settings.ts index 8efd1cd03..7a5cb89ca 100644 --- a/cli/src/optional-settings.ts +++ b/cli/src/optional-settings.ts @@ -30,6 +30,17 @@ type OptionalSetting = question: string defaultValue: string }) + | (OptionalSettingBase & { + kind: "optionalText" + question: string + /** + * Ghost text for the input. Unlike `folder`, there is no defaultValue: + * an unset var means the server reads the vault's own config, so + * pre-filling a concrete value would write an override that silently + * shadows it. Blank when unset = skip (write nothing). + */ + placeholder: string + }) | (OptionalSettingBase & { kind: "choice" question: string @@ -55,6 +66,20 @@ const OPTIONAL_SETTINGS: OptionalSetting[] = [ defaultValue: "About Me", requiresToggle: "MEMORY_ENABLED", }, + { + kind: "optionalText", + name: "DAILY_NOTES_FOLDER", + label: "Daily notes folder", + question: "Vault folder for daily notes:", + placeholder: "blank = use your vault's daily notes settings", + }, + { + kind: "optionalText", + name: "DAILY_NOTES_FORMAT", + label: "Daily notes format", + question: "Filename date format for daily notes (e.g. YYYY-MM-DD):", + placeholder: "blank = use your vault's daily notes settings", + }, { kind: "toggle", name: "FILE_TOOLS_ENABLED", @@ -259,11 +284,42 @@ const askFolder = async ( return askFolder(params, prompts) } -/** Routes a picked setting to its kind's prompt and returns the .env value. */ +/** + * Text prompt for a setting whose absence is meaningful — the server falls + * back to the vault's own config when the var is unset. Blank when unset = + * skip (write nothing); blank when set keeps the current value (the prompt + * resolves an empty submit to its defaultValue). There is no removal path — + * clearing a set value stays a manual .env edit. + */ +const askOptionalText = async ( + params: { + question: string + placeholder: string + currentValue: string | undefined + }, + prompts: Prompts, +): Promise => { + const answer = ( + await prompts.text(params.question, { + defaultValue: params.currentValue, + placeholder: params.placeholder, + }) + ).trim() + if (answer !== "") return answer + prompts.log( + "Left unset — the server keeps reading your vault's daily notes settings.", + ) + return undefined +} + +/** + * Routes a picked setting to its kind's prompt and returns the .env value — + * or undefined when an optionalText prompt was left blank (nothing to write). + */ const askSettingValue = async ( params: { setting: OptionalSetting; currentValue: string | undefined }, prompts: Prompts, -): Promise => { +): Promise => { const { setting, currentValue } = params switch (setting.kind) { case "toggle": { @@ -286,6 +342,15 @@ const askSettingValue = async ( }, prompts, ) + case "optionalText": + return askOptionalText( + { + question: setting.question, + placeholder: setting.placeholder, + currentValue, + }, + prompts, + ) case "choice": return prompts.select( setting.question, @@ -332,14 +397,16 @@ export const askOptionalSettings = async ( ) // Sequential prompting: answers are gathered one at a time in the curated - // order, so the record builds up inside an honest loop. + // order, so the record builds up inside an honest loop. An undefined answer + // (an optionalText prompt left blank) writes nothing. const overrides: Record = {} for (const setting of offeredSettings) { if (!pickedNames.includes(setting.name)) continue - overrides[setting.name] = await askSettingValue( + const value = await askSettingValue( { setting, currentValue: readOptionalValue(envContent, setting.name) }, prompts, ) + if (value !== undefined) overrides[setting.name] = value } return overrides } From 77d92cf5b8f81c0284e6a7e4270e0777c2a07ad9 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Thu, 13 Aug 2026 16:49:20 -0400 Subject: [PATCH 02/11] docs(cli): enumerate the daily notes settings in connect messages and README Co-Authored-By: Claude Fable 5 --- cli/README.md | 17 ++++++++++------- cli/src/__tests__/init.test.ts | 4 ++-- cli/src/messages.ts | 9 +++++---- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/cli/README.md b/cli/README.md index 2e466608a..2af1214be 100644 --- a/cli/README.md +++ b/cli/README.md @@ -49,9 +49,9 @@ What it does: - **Remote** — a VPS with [Obsidian Sync](https://obsidian.md/sync), reachable from any device 2. Offers the most common optional settings — memory layer and folder, - file tools, semantic search, port, timezone (plus sync direction for - remote) — press enter to keep the defaults, or pick the ones you want to - change + daily notes folder and format, file tools, semantic search, port, + timezone (plus sync direction for remote) — press enter to keep the + defaults, or pick the ones you want to change 3. Generates a `.env` file with a securely generated `MCP_AUTH_TOKEN` 4. Optionally starts the container and waits for the health check 5. Prints your connection details — the MCP URL, your auth token, and how to @@ -86,10 +86,13 @@ npx vault-cortex@latest configure ``` Shows the same settings chooser as [`init`](#init) — memory layer and -folder, file tools, semantic search, port, timezone (plus sync direction -for remote) — pre-filled with your current values, saves your picks to -`.env`, and offers to restart the container so they take effect. Settings not in the chooser -live in `.env` too: edit the value there, then run [`restart`](#restart). +folder, daily notes folder and format, file tools, semantic search, port, +timezone (plus sync direction for remote) — pre-filled with your current +values, saves your picks to `.env`, and offers to restart the container so +they take effect. Settings not in the chooser live in `.env` too: edit the +value there, then run [`restart`](#restart) — that's also how you clear a +daily notes setting back to your vault's own configuration (comment out or +delete its line). Use `--dir ` if your config isn't in `./vault-cortex`. diff --git a/cli/src/__tests__/init.test.ts b/cli/src/__tests__/init.test.ts index 269a57988..16bb9ef31 100644 --- a/cli/src/__tests__/init.test.ts +++ b/cli/src/__tests__/init.test.ts @@ -86,7 +86,7 @@ describe("runInit --yes (non-interactive local)", () => { expect(envContent).toMatch(/^MCP_AUTH_TOKEN=[0-9a-f]{64}$/m) expect(envContent).toContain(`VAULT_PATH=${vaultDir}\n`) expect(scripted.prints[0]).toContain( - "Adjust optional settings (memory layer and folder, file tools,\nsemantic search, port, timezone):", + "Adjust optional settings (memory layer and folder, daily notes\nfolder and format, file tools, semantic search, port, timezone):", ) }) @@ -544,7 +544,7 @@ describe("runInit remote flow", () => { expect(envContent).toContain("VAULT_NAME=MyVault\n") expect(envContent).toContain("OBSIDIAN_AUTH_TOKEN=sync-token-xyz\n") expect(scripted.prints[0]).toContain( - "Adjust optional settings (memory layer and folder, file tools,\nsemantic search, port, timezone, sync direction):", + "Adjust optional settings (memory layer and folder, daily notes\nfolder and format, file tools, semantic search, port, timezone,\nsync direction):", ) }) diff --git a/cli/src/messages.ts b/cli/src/messages.ts index dc81b9420..bff9c7700 100644 --- a/cli/src/messages.ts +++ b/cli/src/messages.ts @@ -239,8 +239,8 @@ ${nonOauthBlocks} ${sectionRule("Settings")} -Adjust optional settings (memory layer and folder, file tools, -semantic search, port, timezone): +Adjust optional settings (memory layer and folder, daily notes +folder and format, file tools, semantic search, port, timezone): npx vault-cortex@latest configure --dir "${targetDir}" Or edit ${targetDir}/.env directly — change a value (uncommenting it @@ -324,8 +324,9 @@ ${remoteHealthCheckBlock(`${publicUrl}/healthz`, started)} ${sectionRule("Settings")} -Adjust optional settings (memory layer and folder, file tools, -semantic search, port, timezone, sync direction): +Adjust optional settings (memory layer and folder, daily notes +folder and format, file tools, semantic search, port, timezone, +sync direction): npx vault-cortex@latest configure --dir "${targetDir}" Or edit ${targetDir}/.env directly — change a value (uncommenting it From 41ea3c5a3b24eb8f1a42d2c8f689f1de41cfd3cc Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Thu, 13 Aug 2026 17:03:18 -0400 Subject: [PATCH 03/11] fix(cli): truthful blank-submit behavior for set daily notes values Co-Authored-By: Claude Fable 5 --- cli/src/__tests__/command-stubs.ts | 12 +++++- cli/src/__tests__/configure.test.ts | 23 ++++++++++++ cli/src/__tests__/optional-settings.test.ts | 41 +++++++++++++++++---- cli/src/optional-settings.ts | 39 +++++++++++++------- 4 files changed, 93 insertions(+), 22 deletions(-) diff --git a/cli/src/__tests__/command-stubs.ts b/cli/src/__tests__/command-stubs.ts index 51ec57a9c..5c2be01a2 100644 --- a/cli/src/__tests__/command-stubs.ts +++ b/cli/src/__tests__/command-stubs.ts @@ -10,7 +10,11 @@ export type ScriptedAnswer = string | boolean | string[] export type MultiselectCall = { message: string; options: SelectOption[] } export type ConfirmCall = { message: string; initialValue: boolean } export type SelectCall = { message: string; initialValue: string } -export type TextCall = { message: string; defaultValue: string | undefined } +export type TextCall = { + message: string + defaultValue: string | undefined + placeholder: string | undefined +} export type ScriptedPrompts = { prompts: Prompts @@ -115,7 +119,11 @@ export const createScriptedPrompts = ( return answer }, text: async (message, options) => { - textCalls.push({ message, defaultValue: options?.defaultValue }) + textCalls.push({ + message, + defaultValue: options?.defaultValue, + placeholder: options?.placeholder, + }) const answer = nextStringAnswer(message) // Mirrors @clack/prompts: an empty submission resolves to defaultValue. if (answer === "" && options?.defaultValue !== undefined) diff --git a/cli/src/__tests__/configure.test.ts b/cli/src/__tests__/configure.test.ts index 1d2d7937d..9b2b01e8d 100644 --- a/cli/src/__tests__/configure.test.ts +++ b/cli/src/__tests__/configure.test.ts @@ -272,6 +272,29 @@ describe("runConfigure with picked settings", () => { ]) }) + it("neither claims an update nor offers a restart when a set daily notes folder is blank-kept", async () => { + // A blank submit on a set value keeps it — configure must not log + // "Updated ..." or offer a restart for a byte-identical file. + const targetDir = makeTempTargetDir() + const envFilePath = join(targetDir, ".env") + const envWithDailyNotes = `${LOCAL_ENV_CONTENT}DAILY_NOTES_FOLDER=Journal\n` + writeFileSync(envFilePath, envWithDailyNotes) + const scripted = createScriptedPrompts([["DAILY_NOTES_FOLDER"], ""]) + + const exitCode = await runConfigure( + { dir: targetDir }, + { prompts: scripted.prompts, docker: dockerDown, fetchFn: fetchNever }, + ) + + expect(exitCode).toBe(0) + expect(readFileSync(envFilePath, "utf8")).toBe(envWithDailyNotes) + expect(scripted.logs).toEqual([ + "Kept the current value (Journal).", + "No settings selected — nothing changed.", + ]) + expect(scripted.warnings).toEqual([]) + }) + it("keeps the saved change and exits 1 when the .env cannot start a container", async () => { const targetDir = makeTempTargetDir() const envFilePath = join(targetDir, ".env") diff --git a/cli/src/__tests__/optional-settings.test.ts b/cli/src/__tests__/optional-settings.test.ts index 0f4ce4bce..f7b083b95 100644 --- a/cli/src/__tests__/optional-settings.test.ts +++ b/cli/src/__tests__/optional-settings.test.ts @@ -344,6 +344,7 @@ describe("askOptionalSettings per-setting prompts", () => { { message: "Vault folder for the memory files:", defaultValue: "About Me", + placeholder: "About Me", }, ]) expect(overrides).toEqual({ MEMORY_DIR: "Memory Bank" }) @@ -421,7 +422,11 @@ describe("askOptionalSettings per-setting prompts", () => { // No pre-filled default when unset — the real default is the vault's // own config, so the prompt must not offer a concrete value to accept. expect(scripted.textCalls).toEqual([ - { message: "Vault folder for daily notes:", defaultValue: undefined }, + { + message: "Vault folder for daily notes:", + defaultValue: undefined, + placeholder: "blank = use your vault's daily notes settings", + }, ]) expect(overrides).toEqual({}) expect(scripted.logs).toEqual([ @@ -429,9 +434,11 @@ describe("askOptionalSettings per-setting prompts", () => { ]) }) - it("keeps the current daily notes folder on a blank submit", async () => { + it("keeps a set daily notes folder on a blank submit without recording a no-op", async () => { // The prompt resolves an empty submit to its defaultValue (the current - // value), so blank never destroys an existing setting. + // value), so blank never destroys an existing setting — and the unchanged + // value is not recorded, so the caller won't rewrite the file or offer a + // restart for a no-op. The placeholder states what blank actually does. const scripted = createScriptedPrompts([["DAILY_NOTES_FOLDER"], ""]) const overrides = await askOptionalSettings( @@ -440,9 +447,14 @@ describe("askOptionalSettings per-setting prompts", () => { ) expect(scripted.textCalls).toEqual([ - { message: "Vault folder for daily notes:", defaultValue: "Journal" }, + { + message: "Vault folder for daily notes:", + defaultValue: "Journal", + placeholder: "blank = keep the current value", + }, ]) - expect(overrides).toEqual({ DAILY_NOTES_FOLDER: "Journal" }) + expect(overrides).toEqual({}) + expect(scripted.logs).toEqual(["Kept the current value (Journal)."]) }) it("collects a typed daily notes format, trimmed", async () => { @@ -460,6 +472,7 @@ describe("askOptionalSettings per-setting prompts", () => { { message: "Filename date format for daily notes (e.g. YYYY-MM-DD):", defaultValue: undefined, + placeholder: "blank = use your vault's daily notes settings", }, ]) expect(overrides).toEqual({ DAILY_NOTES_FORMAT: "DD-MM-YYYY" }) @@ -467,8 +480,9 @@ describe("askOptionalSettings per-setting prompts", () => { it("does not clobber a set daily notes folder on whitespace input", async () => { // Whitespace defeats the empty-submit-resolves-to-default behavior and - // trims to empty — the skip path must leave the existing value alone - // rather than write an empty one. + // trims to empty — the keep path must leave the existing value alone + // rather than write an empty one, and must say "kept", not "left unset": + // the override stays active in .env. const scripted = createScriptedPrompts([["DAILY_NOTES_FOLDER"], " "]) const overrides = await askOptionalSettings( @@ -477,6 +491,19 @@ describe("askOptionalSettings per-setting prompts", () => { ) expect(overrides).toEqual({}) + expect(scripted.logs).toEqual(["Kept the current value (Journal)."]) + }) + + it("does not record retyping the value a daily notes folder already has", async () => { + const scripted = createScriptedPrompts([["DAILY_NOTES_FOLDER"], "Journal"]) + + const overrides = await askOptionalSettings( + { mode: "local", envContent: "DAILY_NOTES_FOLDER=Journal\n" }, + scripted.prompts, + ) + + expect(overrides).toEqual({}) + expect(scripted.logs).toEqual(["Kept the current value (Journal)."]) }) it("records only the typed setting when one of a pair is left blank", async () => { diff --git a/cli/src/optional-settings.ts b/cli/src/optional-settings.ts index 7a5cb89ca..3ec507fcb 100644 --- a/cli/src/optional-settings.ts +++ b/cli/src/optional-settings.ts @@ -34,10 +34,11 @@ type OptionalSetting = kind: "optionalText" question: string /** - * Ghost text for the input. Unlike `folder`, there is no defaultValue: - * an unset var means the server reads the vault's own config, so - * pre-filling a concrete value would write an override that silently - * shadows it. Blank when unset = skip (write nothing). + * Ghost text while the var is unset. Unlike `folder`, there is no + * defaultValue: an unset var means the server reads the vault's own + * config, so pre-filling a concrete value would write an override that + * silently shadows it. Once a value is set, the prompt shows a + * keep-current placeholder instead — blank then keeps, never clears. */ placeholder: string }) @@ -288,8 +289,12 @@ const askFolder = async ( * Text prompt for a setting whose absence is meaningful — the server falls * back to the vault's own config when the var is unset. Blank when unset = * skip (write nothing); blank when set keeps the current value (the prompt - * resolves an empty submit to its defaultValue). There is no removal path — - * clearing a set value stays a manual .env edit. + * resolves an empty submit to its defaultValue), and an unchanged value is + * also returned as undefined so the caller never rewrites the file or offers + * a restart for a no-op. The placeholder tells the truth for each state: + * "use your vault's settings" only while unset, "keep the current value" + * once set. There is no removal path — clearing a set value stays a manual + * .env edit. */ const askOptionalText = async ( params: { @@ -299,16 +304,24 @@ const askOptionalText = async ( }, prompts: Prompts, ): Promise => { + const { question, placeholder, currentValue } = params const answer = ( - await prompts.text(params.question, { - defaultValue: params.currentValue, - placeholder: params.placeholder, + await prompts.text(question, { + defaultValue: currentValue, + placeholder: + currentValue === undefined + ? placeholder + : "blank = keep the current value", }) ).trim() - if (answer !== "") return answer - prompts.log( - "Left unset — the server keeps reading your vault's daily notes settings.", - ) + if (answer !== "" && answer !== currentValue) return answer + if (currentValue === undefined) { + prompts.log( + "Left unset — the server keeps reading your vault's daily notes settings.", + ) + return undefined + } + prompts.log(`Kept the current value (${currentValue}).`) return undefined } From 49d6ee30b4816da2718f741c3ea83a9866f388aa Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Thu, 13 Aug 2026 17:14:51 -0400 Subject: [PATCH 04/11] style: compress askOptionalText comments and genericize skip message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The placeholder field comment and function docstring restated code-readable behavior; compressed to non-obvious constraints only. The skip message hardcoded "daily notes" in a generic function — generalized so future optionalText settings get an accurate log. Co-Authored-By: Claude Fable 5 --- cli/src/__tests__/configure.test.ts | 2 +- cli/src/__tests__/optional-settings.test.ts | 2 +- cli/src/optional-settings.ts | 22 ++++++--------------- 3 files changed, 8 insertions(+), 18 deletions(-) diff --git a/cli/src/__tests__/configure.test.ts b/cli/src/__tests__/configure.test.ts index 9b2b01e8d..2c095296c 100644 --- a/cli/src/__tests__/configure.test.ts +++ b/cli/src/__tests__/configure.test.ts @@ -267,7 +267,7 @@ describe("runConfigure with picked settings", () => { expect(exitCode).toBe(0) expect(readFileSync(envFilePath, "utf8")).toBe(LOCAL_ENV_CONTENT) expect(scripted.logs).toEqual([ - "Left unset — the server keeps reading your vault's daily notes settings.", + "Left unset — the server reads this setting from your vault's own config.", "No settings selected — nothing changed.", ]) }) diff --git a/cli/src/__tests__/optional-settings.test.ts b/cli/src/__tests__/optional-settings.test.ts index f7b083b95..774375974 100644 --- a/cli/src/__tests__/optional-settings.test.ts +++ b/cli/src/__tests__/optional-settings.test.ts @@ -430,7 +430,7 @@ describe("askOptionalSettings per-setting prompts", () => { ]) expect(overrides).toEqual({}) expect(scripted.logs).toEqual([ - "Left unset — the server keeps reading your vault's daily notes settings.", + "Left unset — the server reads this setting from your vault's own config.", ]) }) diff --git a/cli/src/optional-settings.ts b/cli/src/optional-settings.ts index 3ec507fcb..a0e68e812 100644 --- a/cli/src/optional-settings.ts +++ b/cli/src/optional-settings.ts @@ -33,13 +33,7 @@ type OptionalSetting = | (OptionalSettingBase & { kind: "optionalText" question: string - /** - * Ghost text while the var is unset. Unlike `folder`, there is no - * defaultValue: an unset var means the server reads the vault's own - * config, so pre-filling a concrete value would write an override that - * silently shadows it. Once a value is set, the prompt shows a - * keep-current placeholder instead — blank then keeps, never clears. - */ + /** Ghost text while unset — no defaultValue because pre-filling would silently shadow the vault's own config. */ placeholder: string }) | (OptionalSettingBase & { @@ -287,14 +281,10 @@ const askFolder = async ( /** * Text prompt for a setting whose absence is meaningful — the server falls - * back to the vault's own config when the var is unset. Blank when unset = - * skip (write nothing); blank when set keeps the current value (the prompt - * resolves an empty submit to its defaultValue), and an unchanged value is - * also returned as undefined so the caller never rewrites the file or offers - * a restart for a no-op. The placeholder tells the truth for each state: - * "use your vault's settings" only while unset, "keep the current value" - * once set. There is no removal path — clearing a set value stays a manual - * .env edit. + * back to the vault's own config when the var is unset. Blank-when-unset + * writes nothing; blank-when-set keeps the current value. Returns undefined + * on skip or no-op so the caller never rewrites for nothing. No removal + * path — clearing is a manual .env edit. */ const askOptionalText = async ( params: { @@ -317,7 +307,7 @@ const askOptionalText = async ( if (answer !== "" && answer !== currentValue) return answer if (currentValue === undefined) { prompts.log( - "Left unset — the server keeps reading your vault's daily notes settings.", + "Left unset — the server reads this setting from your vault's own config.", ) return undefined } From bf539665e6c48c99c3b8f75c471ab49c150625a8 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Thu, 13 Aug 2026 17:22:39 -0400 Subject: [PATCH 05/11] test(cli): cover updating an existing daily notes setting to a new value The "set value + typed different value" branch of askOptionalText and the configure-level active-line replacement for daily notes were untested. Co-Authored-By: Claude Fable 5 --- cli/src/__tests__/configure.test.ts | 21 +++++++++++++++++++++ cli/src/__tests__/optional-settings.test.ts | 18 ++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/cli/src/__tests__/configure.test.ts b/cli/src/__tests__/configure.test.ts index 2c095296c..0811bd2f9 100644 --- a/cli/src/__tests__/configure.test.ts +++ b/cli/src/__tests__/configure.test.ts @@ -231,6 +231,27 @@ describe("runConfigure with picked settings", () => { ) }) + it("replaces an active daily notes folder with a new value", async () => { + const targetDir = makeTempTargetDir() + const envFilePath = join(targetDir, ".env") + const envWithDailyNotes = `${LOCAL_ENV_CONTENT}DAILY_NOTES_FOLDER=Journal\n` + writeFileSync(envFilePath, envWithDailyNotes) + const scripted = createScriptedPrompts([["DAILY_NOTES_FOLDER"], "Planner"]) + + const exitCode = await runConfigure( + { dir: targetDir }, + { prompts: scripted.prompts, docker: dockerDown, fetchFn: fetchNever }, + ) + + expect(exitCode).toBe(0) + expect(readFileSync(envFilePath, "utf8")).toBe( + `${LOCAL_ENV_CONTENT}DAILY_NOTES_FOLDER=Planner\n`, + ) + expect(scripted.logs).toEqual([ + `Updated DAILY_NOTES_FOLDER in ${targetDir}/.env.`, + ]) + }) + it("uncomments the template's daily notes folder line on a typed value", async () => { // The generated .env carries the var as a commented template line — the // chooser's value must land by uncommenting it, not by appending a diff --git a/cli/src/__tests__/optional-settings.test.ts b/cli/src/__tests__/optional-settings.test.ts index 774375974..efdcf568d 100644 --- a/cli/src/__tests__/optional-settings.test.ts +++ b/cli/src/__tests__/optional-settings.test.ts @@ -494,6 +494,24 @@ describe("askOptionalSettings per-setting prompts", () => { expect(scripted.logs).toEqual(["Kept the current value (Journal)."]) }) + it("updates a set daily notes folder to a new value", async () => { + const scripted = createScriptedPrompts([["DAILY_NOTES_FOLDER"], "Planner"]) + + const overrides = await askOptionalSettings( + { mode: "local", envContent: "DAILY_NOTES_FOLDER=Journal\n" }, + scripted.prompts, + ) + + expect(scripted.textCalls).toEqual([ + { + message: "Vault folder for daily notes:", + defaultValue: "Journal", + placeholder: "blank = keep the current value", + }, + ]) + expect(overrides).toEqual({ DAILY_NOTES_FOLDER: "Planner" }) + }) + it("does not record retyping the value a daily notes folder already has", async () => { const scripted = createScriptedPrompts([["DAILY_NOTES_FOLDER"], "Journal"]) From 6d5aa73d0bae13d63f59b2fe605ddf13fe9a7432 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Thu, 13 Aug 2026 17:30:06 -0400 Subject: [PATCH 06/11] fix(cli): include folder and optionalText in OptionalSetting JSDoc The type's JSDoc enumerated toggle, port, timezone, and choice but omitted folder (pre-existing) and optionalText (new with this PR). Co-Authored-By: Claude Fable 5 --- cli/src/optional-settings.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cli/src/optional-settings.ts b/cli/src/optional-settings.ts index a0e68e812..06e750fdc 100644 --- a/cli/src/optional-settings.ts +++ b/cli/src/optional-settings.ts @@ -19,7 +19,8 @@ type OptionalSettingBase = { /** * One optional .env setting the guided flow can change. `kind` selects the * prompt shape a picked setting gets: toggles use a yes/no confirm, port and - * timezone use validated text inputs, and choice uses a single select. + * timezone use validated text inputs, folder requires a non-empty name, + * optionalText writes nothing when left blank, and choice uses a single select. */ type OptionalSetting = | (OptionalSettingBase & { kind: "toggle"; question: string }) From 9b61eb6b1dc1149e96090d669eb6e43b8af1159a Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Thu, 13 Aug 2026 17:51:06 -0400 Subject: [PATCH 07/11] fix(cli): unify configure's empty-overrides message so it never contradicts a keep log Co-Authored-By: Claude Fable 5 --- cli/src/__tests__/configure.test.ts | 8 ++++---- cli/src/configure.ts | 5 ++++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/cli/src/__tests__/configure.test.ts b/cli/src/__tests__/configure.test.ts index 0811bd2f9..d6d0dd796 100644 --- a/cli/src/__tests__/configure.test.ts +++ b/cli/src/__tests__/configure.test.ts @@ -77,7 +77,7 @@ describe("runConfigure with nothing picked", () => { expect(exitCode).toBe(0) expect(readFileSync(envFilePath, "utf8")).toBe(LOCAL_ENV_CONTENT) - expect(scripted.logs).toEqual(["No settings selected — nothing changed."]) + expect(scripted.logs).toEqual(["No changes to apply."]) expect(scripted.asked).toEqual([ "Any optional settings to change? (press enter to skip)", ]) @@ -275,7 +275,7 @@ describe("runConfigure with picked settings", () => { ) }) - it("treats a picked-then-blanked daily notes setting as nothing selected", async () => { + it("treats a picked-then-blanked daily notes setting as no changes to apply", async () => { const targetDir = makeTempTargetDir() const envFilePath = writeLocalEnv(targetDir) const scripted = createScriptedPrompts([["DAILY_NOTES_FOLDER"], ""]) @@ -289,7 +289,7 @@ describe("runConfigure with picked settings", () => { expect(readFileSync(envFilePath, "utf8")).toBe(LOCAL_ENV_CONTENT) expect(scripted.logs).toEqual([ "Left unset — the server reads this setting from your vault's own config.", - "No settings selected — nothing changed.", + "No changes to apply.", ]) }) @@ -311,7 +311,7 @@ describe("runConfigure with picked settings", () => { expect(readFileSync(envFilePath, "utf8")).toBe(envWithDailyNotes) expect(scripted.logs).toEqual([ "Kept the current value (Journal).", - "No settings selected — nothing changed.", + "No changes to apply.", ]) expect(scripted.warnings).toEqual([]) }) diff --git a/cli/src/configure.ts b/cli/src/configure.ts index c56aa7c69..712ad3b7d 100644 --- a/cli/src/configure.ts +++ b/cli/src/configure.ts @@ -53,7 +53,10 @@ export const runConfigure = async ( prompts, ) if (Object.keys(pickedOverrides).length === 0) { - prompts.log("No settings selected — nothing changed.") + // Covers both empty-overrides paths: nothing picked in the chooser, and + // picked-but-kept (an optionalText prompt left blank logs its own + // "Kept the current value" line, which this must not contradict). + prompts.log("No changes to apply.") prompts.outro("Done.") return 0 } From 610ab9819fdf8d2ca90aaa07b408a170cf3b9435 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Thu, 13 Aug 2026 19:33:25 -0400 Subject: [PATCH 08/11] docs(cli): split the configure blob; defer the chooser list to init Co-Authored-By: Claude Fable 5 --- cli/README.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/cli/README.md b/cli/README.md index 2af1214be..d102b6d7d 100644 --- a/cli/README.md +++ b/cli/README.md @@ -85,14 +85,13 @@ Change optional settings on an existing setup: npx vault-cortex@latest configure ``` -Shows the same settings chooser as [`init`](#init) — memory layer and -folder, daily notes folder and format, file tools, semantic search, port, -timezone (plus sync direction for remote) — pre-filled with your current -values, saves your picks to `.env`, and offers to restart the container so -they take effect. Settings not in the chooser live in `.env` too: edit the -value there, then run [`restart`](#restart) — that's also how you clear a -daily notes setting back to your vault's own configuration (comment out or -delete its line). +Shows the same settings chooser as [`init`](#init), pre-filled with your +current values, saves your picks to `.env`, and offers to restart the +container so they take effect. + +Settings not in the chooser live in `.env` too: edit the value there, then +run [`restart`](#restart). That's also how you clear a daily notes setting +back to your vault's own configuration — comment out or delete its line. Use `--dir ` if your config isn't in `./vault-cortex`. From 6626fb346d84fb46491c6ec5e00b2dadbe817722 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Thu, 13 Aug 2026 19:40:20 -0400 Subject: [PATCH 09/11] docs(cli): plainer wording for updating and removing daily notes settings Co-Authored-By: Claude Fable 5 --- cli/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cli/README.md b/cli/README.md index d102b6d7d..50adc6c41 100644 --- a/cli/README.md +++ b/cli/README.md @@ -90,8 +90,9 @@ current values, saves your picks to `.env`, and offers to restart the container so they take effect. Settings not in the chooser live in `.env` too: edit the value there, then -run [`restart`](#restart). That's also how you clear a daily notes setting -back to your vault's own configuration — comment out or delete its line. +run [`restart`](#restart). The daily notes settings can be updated the same +way — and removing a line entirely puts the server back on your vault's own +daily notes settings. Use `--dir ` if your config isn't in `./vault-cortex`. From 31b2789ff2b42da2accf347a847a0dc2727fe04c Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Thu, 13 Aug 2026 19:41:33 -0400 Subject: [PATCH 10/11] docs(cli): remove-a-setting phrasing over remove-a-line Co-Authored-By: Claude Fable 5 --- cli/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/README.md b/cli/README.md index 50adc6c41..94f39d1a3 100644 --- a/cli/README.md +++ b/cli/README.md @@ -91,8 +91,8 @@ container so they take effect. Settings not in the chooser live in `.env` too: edit the value there, then run [`restart`](#restart). The daily notes settings can be updated the same -way — and removing a line entirely puts the server back on your vault's own -daily notes settings. +way — and if you remove one from `.env`, the server goes back to your +vault's own daily notes settings. Use `--dir ` if your config isn't in `./vault-cortex`. From 70bf15b78c668d1e99b0e69947845fb1fc6c2bd4 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Thu, 13 Aug 2026 19:42:47 -0400 Subject: [PATCH 11/11] docs(cli): restore the original clearing sentence Co-Authored-By: Claude Fable 5 --- cli/README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cli/README.md b/cli/README.md index 94f39d1a3..d102b6d7d 100644 --- a/cli/README.md +++ b/cli/README.md @@ -90,9 +90,8 @@ current values, saves your picks to `.env`, and offers to restart the container so they take effect. Settings not in the chooser live in `.env` too: edit the value there, then -run [`restart`](#restart). The daily notes settings can be updated the same -way — and if you remove one from `.env`, the server goes back to your -vault's own daily notes settings. +run [`restart`](#restart). That's also how you clear a daily notes setting +back to your vault's own configuration — comment out or delete its line. Use `--dir ` if your config isn't in `./vault-cortex`.