vault-cortex 0.4.6-beta.26 → 0.5.0-beta.28

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
@@ -28,6 +28,26 @@ container; this CLI scaffolds the config so you don't have to.
28
28
 
29
29
  Existing files are never overwritten without asking.
30
30
 
31
+ ## Get Sync Token
32
+
33
+ Generate an Obsidian Sync auth token without leaving the CLI:
34
+
35
+ ```bash
36
+ npx vault-cortex get-sync-token
37
+ ```
38
+
39
+ The command opens the Obsidian login inside Docker. Once you've signed
40
+ in, it captures your token and prints it — nothing to dig out of the
41
+ login output. Use `--dir` to write the token straight into an existing
42
+ `.env` instead:
43
+
44
+ ```bash
45
+ npx vault-cortex get-sync-token --dir ./vault-cortex
46
+ ```
47
+
48
+ During `init --mode remote`, this flow is offered automatically when Docker
49
+ is available.
50
+
31
51
  ## Upgrade
32
52
 
33
53
  Pull the latest image, re-create the container, and verify health:
@@ -36,7 +56,13 @@ Pull the latest image, re-create the container, and verify health:
36
56
  npx vault-cortex upgrade
37
57
  ```
38
58
 
39
- Also applies `.env` changes (`docker restart` does not re-read env files).
59
+ Run it from the same directory where you ran `init` it looks for your
60
+ config in `./vault-cortex/.env` (pass `--dir <path>` if you scaffolded
61
+ somewhere else).
62
+
63
+ Your vault data, search index, and `.env` settings are preserved across
64
+ upgrades — only the server image is replaced. Also applies `.env` changes
65
+ (`docker restart` does not re-read env files).
40
66
 
41
67
  Prefer Docker Compose? The CLI uses `docker run` for simplicity, but the
42
68
  [deploy guides](https://github.com/aliasunder/vault-cortex/blob/main/deploy/)
package/dist/docker.js CHANGED
@@ -2,6 +2,36 @@ import { spawnSync } from "node:child_process";
2
2
  export const LOCAL_IMAGE = "ghcr.io/aliasunder/vault-cortex:latest";
3
3
  export const REMOTE_IMAGE = "ghcr.io/aliasunder/vault-cortex:remote";
4
4
  export const CONTAINER_NAME = "vault-cortex";
5
+ /**
6
+ * Builds the `docker run` args for the Obsidian login with a volume mount
7
+ * that captures the auth token file. Runs `ob login` directly instead of
8
+ * the image's get-sync-token script: the script's additions are locating and
9
+ * printing the token, and the mount makes both unnecessary — the CLI reads
10
+ * the token file itself, and not echoing a credential keeps it out of
11
+ * terminal scrollback. Pure function for testability.
12
+ *
13
+ * On Linux, includes `--user uid:gid` when uid/gid are provided — Node
14
+ * exposes process.getuid/getgid on every POSIX platform, so in practice the
15
+ * flag is always set there — keeping the token file host-user-owned. macOS
16
+ * Docker Desktop translates UIDs automatically, so no flag is needed.
17
+ */
18
+ export const buildObsidianLoginArgs = (params) => {
19
+ const { configMountPath, platform = process.platform, uid, gid } = params;
20
+ const args = [
21
+ "run",
22
+ "--rm",
23
+ "-it",
24
+ "--entrypoint",
25
+ "ob",
26
+ "-v",
27
+ `${configMountPath}:/home/obsidian/.config`,
28
+ ];
29
+ if (platform === "linux" && uid !== undefined && gid !== undefined) {
30
+ args.push("--user", `${uid}:${gid}`);
31
+ }
32
+ args.push(REMOTE_IMAGE, "login");
33
+ return args;
34
+ };
5
35
  /**
6
36
  * Container-internal env vars that must override the user's .env values.
7
37
  * VAULT_PATH in .env is the host path (for the -v mount); the container
@@ -70,7 +100,11 @@ export const createDockerRunner = () => ({
70
100
  .status === 0,
71
101
  pullImage: (image) => spawnSync("docker", ["pull", image], { stdio: "inherit" }).status === 0,
72
102
  stopAndRemoveContainer: () => spawnSync("docker", ["rm", "-f", CONTAINER_NAME]).status === 0,
73
- runGetToken: () => spawnSync("docker", ["run", "--rm", "-it", "--entrypoint", "get-token", REMOTE_IMAGE], { stdio: "inherit" }).status === 0,
103
+ runObsidianLogin: (configMountPath) => spawnSync("docker", buildObsidianLoginArgs({
104
+ configMountPath,
105
+ uid: process.getuid?.(),
106
+ gid: process.getgid?.(),
107
+ }), { stdio: "inherit" }).status === 0,
74
108
  });
75
109
  /**
76
110
  * Polls the health endpoint until it responds OK or the timeout elapses.
package/dist/env.js CHANGED
@@ -1,4 +1,3 @@
1
- import { REMOTE_IMAGE } from "./docker.js";
2
1
  // Optional env blocks are synced from deploy/<mode>/.env.example by
3
2
  // npm run sync:cli-env-blocks. Edit the deploy/ files, then re-run the script.
4
3
  // cli/src/templates.test.ts asserts the CLI optional block vars match the
@@ -11,12 +10,13 @@ import { REMOTE_IMAGE } from "./docker.js";
11
10
  // └─────────────────────────────────────────────────────────────────────────┘
12
11
  // sync:local-optional:begin
13
12
  const LOCAL_OPTIONAL_BLOCK = `# Optional ──────────────────────────────────────────────────
14
- # To override a setting: uncomment it, set a value, then apply with
15
- # "npx vault-cortex upgrade" (restart alone does not re-read this file).
13
+ # To change a setting: edit its value (uncommenting it first if needed),
14
+ # then apply with "npx vault-cortex upgrade" (restart alone does not
15
+ # re-read this file).
16
16
 
17
17
  # Public URL for OAuth issuer URL in discovery metadata (default: http://localhost:8000).
18
18
  # Override if you expose the server on a different URL (e.g. via a reverse proxy).
19
- # PUBLIC_URL=http://localhost:8000
19
+ PUBLIC_URL=http://localhost:8000
20
20
 
21
21
  # Your IANA timezone — affects daily note resolution and memory timestamps.
22
22
  # TZ=America/New_York
@@ -27,20 +27,20 @@ const LOCAL_OPTIONAL_BLOCK = `# Optional ─────────────
27
27
  # similarity fused via RRF. First startup is slow (~5min for 700 notes);
28
28
  # subsequent starts are fast via content-hash caching.
29
29
  # Set to false to disable model download and use FTS5 search only.
30
- # EMBEDDING_ENABLED=true
30
+ EMBEDDING_ENABLED=true
31
31
 
32
32
  # Reranking mode for hybrid search results (default: blended).
33
33
  # "blended" uses a cross-encoder to refine result ordering with
34
34
  # position-aware score blending (~200ms added latency).
35
35
  # "none" skips reranking for lower latency.
36
36
  # Only takes effect when EMBEDDING_ENABLED=true.
37
- # RERANK_MODE=blended
37
+ RERANK_MODE=blended
38
38
 
39
39
  # Enable or disable the memory layer (default: true).
40
40
  # Set to false to hide memory tools and skip About Me/ creation.
41
- # MEMORY_ENABLED=true
41
+ MEMORY_ENABLED=true
42
42
  # Memory folder name in your vault (default: About Me).
43
- # MEMORY_DIR=About Me
43
+ MEMORY_DIR=About Me
44
44
 
45
45
  # Comma-separated folders protected from deletion (default: MEMORY_DIR, Daily Notes).
46
46
  # If your daily notes folder has a custom name (e.g. "Journal"), override to include it.
@@ -55,10 +55,10 @@ const LOCAL_OPTIONAL_BLOCK = `# Optional ─────────────
55
55
  # SERVICE_DOCUMENTATION_URL=https://github.com/youruser/your-fork
56
56
 
57
57
  # Host port to expose (default: 8000).
58
- # PORT=8000
58
+ PORT=8000
59
59
 
60
60
  # Log verbosity: debug | info | warn | error (default: info).
61
- # LOG_LEVEL=info
61
+ LOG_LEVEL=info
62
62
 
63
63
  # Directory for persistent log files inside the container.
64
64
  # Unset by default — logs go to stdout only. Set a path to also write
@@ -66,13 +66,13 @@ const LOCAL_OPTIONAL_BLOCK = `# Optional ─────────────
66
66
  # LOG_DIR=/data/logs
67
67
 
68
68
  # Days to retain persistent log files before cleanup (default: 30).
69
- # LOG_RETENTION_DAYS=30
69
+ LOG_RETENTION_DAYS=30
70
70
 
71
71
  # Windows users: set this to true. Makes a vault stored on a C: drive work
72
72
  # through Docker Desktop (switches the file watcher to polling and note moves
73
73
  # to rename-based writes). Only strictly needed when your vault is on a C:
74
74
  # drive rather than inside WSL2, but harmless to enable for any Windows setup.
75
- # WINDOWS_MODE=true
75
+ WINDOWS_MODE=false
76
76
  `;
77
77
  // sync:local-optional:end
78
78
  // ┌─────────────────────────────────────────────────────────────────────────┐
@@ -82,8 +82,9 @@ const LOCAL_OPTIONAL_BLOCK = `# Optional ─────────────
82
82
  // └─────────────────────────────────────────────────────────────────────────┘
83
83
  // sync:remote-optional:begin
84
84
  const REMOTE_OPTIONAL_BLOCK = `# Optional ──────────────────────────────────────────────────
85
- # To override a setting: uncomment it, set a value, then apply with
86
- # "npx vault-cortex upgrade" (restart alone does not re-read this file).
85
+ # To change a setting: edit its value (uncommenting it first if needed),
86
+ # then apply with "npx vault-cortex upgrade" (restart alone does not
87
+ # re-read this file).
87
88
 
88
89
  # Your IANA timezone — affects daily note resolution and memory timestamps.
89
90
  # TZ=America/New_York
@@ -94,26 +95,26 @@ const REMOTE_OPTIONAL_BLOCK = `# Optional ────────────
94
95
  # similarity fused via RRF. First startup is slow (~5min for 700 notes);
95
96
  # subsequent starts are fast via content-hash caching.
96
97
  # Set to false to disable model download and use FTS5 search only.
97
- # EMBEDDING_ENABLED=true
98
+ EMBEDDING_ENABLED=true
98
99
 
99
100
  # Reranking mode for hybrid search results (default: blended).
100
101
  # "blended" uses a cross-encoder to refine result ordering with
101
102
  # position-aware score blending (~200ms added latency).
102
103
  # "none" skips reranking for lower latency.
103
104
  # Only takes effect when EMBEDDING_ENABLED=true.
104
- # RERANK_MODE=blended
105
+ RERANK_MODE=blended
105
106
 
106
107
  # Windows bind-mount mode (default: false).
107
108
  # Set to true when your vault is on a Windows drive (Docker Desktop).
108
109
  # Enables polling for the file watcher and rename-based moves across
109
110
  # the Docker Desktop/WSL2 bridge.
110
- # WINDOWS_MODE=false
111
+ WINDOWS_MODE=false
111
112
 
112
113
  # Enable or disable the memory layer (default: true).
113
114
  # Set to false to hide memory tools and skip About Me/ creation.
114
- # MEMORY_ENABLED=true
115
+ MEMORY_ENABLED=true
115
116
  # Memory folder name in your vault (default: About Me).
116
- # MEMORY_DIR=About Me
117
+ MEMORY_DIR=About Me
117
118
 
118
119
  # Comma-separated folders protected from deletion (default: MEMORY_DIR, Daily Notes).
119
120
  # If your daily notes folder has a custom name (e.g. "Journal"), override to include it.
@@ -128,31 +129,31 @@ const REMOTE_OPTIONAL_BLOCK = `# Optional ────────────
128
129
  # SERVICE_DOCUMENTATION_URL=https://github.com/youruser/your-fork
129
130
 
130
131
  # Host port to expose (default: 8000).
131
- # PORT=8000
132
+ PORT=8000
132
133
 
133
134
  # Log verbosity: debug | info | warn | error (default: info).
134
- # LOG_LEVEL=info
135
+ LOG_LEVEL=info
135
136
 
136
137
  # Directory for persistent log files inside the container (default: /data/logs).
137
138
  # Set to empty to disable file logging (logs still go to stdout either way).
138
- # LOG_DIR=/data/logs
139
+ LOG_DIR=/data/logs
139
140
 
140
141
  # Days to retain persistent log files before cleanup (default: 30).
141
- # LOG_RETENTION_DAYS=30
142
+ LOG_RETENTION_DAYS=30
142
143
 
143
144
  # User/group IDs for obsidian-sync (default: 1000).
144
- # PUID=1000
145
- # PGID=1000
145
+ PUID=1000
146
+ PGID=1000
146
147
 
147
148
  # Device name shown in Obsidian Sync settings.
148
- # DEVICE_NAME=vault-cortex
149
+ DEVICE_NAME=vault-cortex
149
150
 
150
151
  # Obsidian Sync conflict resolution: merge | conflict (default: merge).
151
152
  # 'merge' integrates changes automatically; 'conflict' writes a separate conflict file.
152
- # CONFLICT_STRATEGY=merge
153
+ CONFLICT_STRATEGY=merge
153
154
 
154
155
  # Sync direction: bidirectional | pull-only | push-only (default: bidirectional).
155
- # SYNC_MODE=bidirectional
156
+ SYNC_MODE=bidirectional
156
157
  `;
157
158
  // sync:remote-optional:end
158
159
  export const buildLocalEnv = (answers) => `# vault-cortex — local quickstart
@@ -181,8 +182,7 @@ VAULT_PASSWORD=${answers.vaultPassword}`;
181
182
  const obsidianTokenComment = answers.obsidianAuthToken === ""
182
183
  ? `# Obsidian Sync auth token — FILL THIS IN before starting the server.
183
184
  # Generate once with:
184
- # docker run --rm -it --entrypoint get-token \\
185
- # ${REMOTE_IMAGE}`
185
+ # npx vault-cortex get-sync-token`
186
186
  : `# Obsidian Sync auth token.`;
187
187
  return `# vault-cortex — remote quickstart (Obsidian Sync)
188
188
  # Generated by \`npx vault-cortex init\`. Full option reference:
@@ -0,0 +1,150 @@
1
+ import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join, resolve } from "node:path";
4
+ import { patchEnvObsidianToken } from "./scaffold.js";
5
+ import { expandTilde } from "./vault.js";
6
+ /** Message from an unknown throw — Error instances keep their message. */
7
+ const describeError = (error) => error instanceof Error ? error.message : String(error);
8
+ /**
9
+ * Creates the temp dir the container's config mount writes into.
10
+ * Returns undefined (after warning) when creation fails.
11
+ */
12
+ const makeTempMountDir = (prompts) => {
13
+ try {
14
+ return mkdtempSync(join(tmpdir(), "vault-cortex-sync-token-"));
15
+ }
16
+ catch (error) {
17
+ prompts.warn(`Could not create a temp directory for token capture — ${describeError(error)}`);
18
+ return undefined;
19
+ }
20
+ };
21
+ /**
22
+ * Runs the interactive Obsidian login container. A throw from the Docker
23
+ * runner is reported and treated the same as a non-zero exit.
24
+ */
25
+ const runLoginContainer = (configMountPath, deps) => {
26
+ const { docker, prompts } = deps;
27
+ try {
28
+ return docker.runObsidianLogin(configMountPath);
29
+ }
30
+ catch (error) {
31
+ prompts.warn(`Docker run failed — ${describeError(error)}`);
32
+ return false;
33
+ }
34
+ };
35
+ /**
36
+ * Reads the captured token file from the config mount. Returns undefined
37
+ * when the file is missing, empty, or unreadable — the caller treats all
38
+ * three as "no token captured".
39
+ */
40
+ const readCapturedTokenFile = (configMountPath) => {
41
+ const tokenPath = join(configMountPath, "obsidian-headless", "auth_token");
42
+ try {
43
+ if (!existsSync(tokenPath))
44
+ return undefined;
45
+ const token = readFileSync(tokenPath, "utf8").trim();
46
+ return token || undefined;
47
+ }
48
+ catch {
49
+ return undefined;
50
+ }
51
+ };
52
+ /**
53
+ * Best-effort removal of the temp mount dir. Failing to remove it (e.g.
54
+ * root-owned files left by the container) must not turn a successful
55
+ * capture into a failure, so it warns instead of throwing.
56
+ */
57
+ const removeTempMountDir = (configMountPath, prompts) => {
58
+ try {
59
+ rmSync(configMountPath, { recursive: true, force: true });
60
+ }
61
+ catch (error) {
62
+ prompts.warn(`Could not remove temp directory ${configMountPath} — ${describeError(error)}`);
63
+ }
64
+ };
65
+ /**
66
+ * Runs the Obsidian login (`ob login`) inside a Docker container with a
67
+ * volume mount that captures the auth token file. The interactive login
68
+ * (email, password, MFA) shows in the terminal, but the resulting token is
69
+ * read from the mounted config dir — never printed, so it stays out of
70
+ * terminal scrollback.
71
+ *
72
+ * tokenDestinationMessage finishes the handoff message by telling the user
73
+ * where the captured token ends up — the destination differs per flow
74
+ * (init stores it in the generated .env; the subcommand prints it, or
75
+ * writes it to an existing .env with --dir).
76
+ *
77
+ * Returns the token string on success, undefined on any failure — each
78
+ * fallible operation is wrapped individually by the helpers above, so no
79
+ * catch-all is needed here. The bare try/finally only scopes the temp dir
80
+ * (acquire → release); it has no catch and swallows nothing.
81
+ */
82
+ export const captureObsidianToken = (deps, tokenDestinationMessage) => {
83
+ const { prompts } = deps;
84
+ const configMountPath = makeTempMountDir(prompts);
85
+ if (!configMountPath)
86
+ return undefined;
87
+ try {
88
+ prompts.log("Handing the terminal to the Obsidian login — it will ask for your " +
89
+ `account email, password, and MFA code. ${tokenDestinationMessage}`);
90
+ const loginSucceeded = runLoginContainer(configMountPath, deps);
91
+ if (!loginSucceeded) {
92
+ prompts.warn("The Obsidian login did not complete — you can run it later with:\n" +
93
+ " npx vault-cortex get-sync-token");
94
+ return undefined;
95
+ }
96
+ const token = readCapturedTokenFile(configMountPath);
97
+ if (!token) {
98
+ prompts.warn("The Obsidian login finished, but no token was captured — the " +
99
+ "token file was missing, empty, or unreadable. You can retry with:\n" +
100
+ " npx vault-cortex get-sync-token");
101
+ return undefined;
102
+ }
103
+ return token;
104
+ }
105
+ finally {
106
+ removeTempMountDir(configMountPath, prompts);
107
+ }
108
+ };
109
+ /**
110
+ * Subcommand entry: generate an Obsidian Sync token via Docker.
111
+ * Without --dir, prints the token to stdout.
112
+ * With --dir, writes it directly to `<dir>/.env`.
113
+ */
114
+ export const runGetSyncToken = async (flags, deps) => {
115
+ const { prompts, docker } = deps;
116
+ if (!docker.isDaemonRunning()) {
117
+ prompts.error("Container runtime not running — start Docker Desktop, Colima,\n" +
118
+ "OrbStack, or another Docker-compatible runtime and try again.");
119
+ return 1;
120
+ }
121
+ prompts.intro("vault-cortex get-sync-token");
122
+ // Resolve the destination up front so the login handoff message can tell
123
+ // the user where the token will end up.
124
+ const envFilePath = flags.dir
125
+ ? join(resolve(expandTilde(flags.dir)), ".env")
126
+ : undefined;
127
+ const tokenDestinationMessage = envFilePath
128
+ ? `The token is captured automatically and written to ${envFilePath}.`
129
+ : "The token is captured automatically and printed at the end.";
130
+ const token = captureObsidianToken({ docker, prompts }, tokenDestinationMessage);
131
+ if (!token) {
132
+ prompts.error("Could not capture the auth token.");
133
+ return 1;
134
+ }
135
+ if (!envFilePath) {
136
+ prompts.log("Your OBSIDIAN_AUTH_TOKEN:");
137
+ prompts.print(`\n ${token}\n`);
138
+ prompts.outro("Done.");
139
+ return 0;
140
+ }
141
+ const patched = patchEnvObsidianToken(envFilePath, token);
142
+ if (!patched) {
143
+ prompts.error(`Could not patch ${envFilePath} — the file is missing or has no ` +
144
+ "OBSIDIAN_AUTH_TOKEN line. Run init first.");
145
+ return 1;
146
+ }
147
+ prompts.log(`Token written to ${envFilePath}`);
148
+ prompts.outro("Done.");
149
+ return 0;
150
+ };
package/dist/init.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { join, resolve } from "node:path";
2
2
  import { buildLocalEnv, buildRemoteEnv } from "./env.js";
3
+ import { captureObsidianToken } from "./get-sync-token.js";
3
4
  import { buildLocalConnectMessage, buildRemoteConnectMessage, } from "./messages.js";
4
- import { REMOTE_IMAGE, pollHealth } from "./docker.js";
5
+ import { pollHealth } from "./docker.js";
5
6
  import { buildFilesToWrite, readEnvPort, writeFiles, } from "./scaffold.js";
6
7
  import { generateToken } from "./token.js";
7
8
  import { expandTilde, validateVaultPath } from "./vault.js";
@@ -22,26 +23,16 @@ const askMode = async (prompts) => {
22
23
  ], "local");
23
24
  return isMode(selected) ? selected : "local";
24
25
  };
25
- const GET_TOKEN_COMMAND = `docker run --rm -it --entrypoint get-token \\
26
- ${REMOTE_IMAGE}`;
27
26
  /**
28
- * Offers to run the vault-cortex image's get-token flow in this terminal.
29
- * Returns true only when it ran to completion (and so printed a token the
30
- * user can scroll up to). The handoff log exists because the clack UI gives
31
- * way to raw docker output — image pull, then the tool's own login prompts.
27
+ * Offers to auto-capture the Obsidian Sync token via a Docker volume mount.
28
+ * Returns the captured token string, or undefined when the user declines or
29
+ * the capture fails (the caller falls back to a paste prompt).
32
30
  */
33
- const offerGetTokenRun = async (prompts, docker) => {
34
- const runNow = await prompts.confirm("Run the get-token command now?", true);
31
+ const offerSyncTokenCapture = async (prompts, docker) => {
32
+ const runNow = await prompts.confirm("Generate the token now?", true);
35
33
  if (!runNow)
36
- return false;
37
- prompts.log("Handing the terminal to get-token it will ask for your Obsidian " +
38
- "account login and print a token at the end.");
39
- const tokenGenerated = docker.runGetToken();
40
- if (!tokenGenerated) {
41
- prompts.warn("get-token did not complete — you can run it later and edit .env.");
42
- return false;
43
- }
44
- return true;
34
+ return undefined;
35
+ return captureObsidianToken({ docker, prompts }, "The token is captured automatically and stored in your .env — nothing to copy.");
45
36
  };
46
37
  /**
47
38
  * Asks for the vault path, recursing to re-prompt until it gets a usable
@@ -240,7 +231,7 @@ const runLocalInit = async (flags, deps) => {
240
231
  return 0;
241
232
  };
242
233
  // Remote flow (VPS + Obsidian Sync): resolve target dir → PUBLIC_URL →
243
- // VAULT_NAME → Obsidian Sync token (optionally running get-token via
234
+ // VAULT_NAME → Obsidian Sync token (optionally running the Obsidian login via
244
235
  // Docker) → optional E2E vault password → generate token → write .env →
245
236
  // optionally start → print connect instructions. Always interactive —
246
237
  // the sync-token step can't be defaulted.
@@ -255,21 +246,17 @@ const runRemoteInit = async (flags, deps) => {
255
246
  }))));
256
247
  const publicUrl = await askPublicUrl(prompts);
257
248
  const vaultName = await askVaultName(prompts);
258
- // The Obsidian Sync token comes from an interactive docker run (the
259
- // get-token entrypoint logs into Obsidian). We print the command, offer to
260
- // run it when Docker is usable, then ask the user to paste the result —
261
- // get-token writes to the terminal, so it can't be captured automatically.
262
- // A blank answer is allowed: the .env is written with an empty
263
- // OBSIDIAN_AUTH_TOKEN and a fill-this-in comment.
264
- prompts.note(GET_TOKEN_COMMAND, "Obsidian Sync token generate once with");
265
- const getTokenRan = docker.isDaemonRunning()
266
- ? await offerGetTokenRun(prompts, docker)
267
- : false;
268
- // "printed above" is only true when get-token actually ran to completion.
269
- const pastePrompt = getTokenRan
270
- ? "Paste the Obsidian Sync token printed above (leave blank to fill in .env later):"
271
- : "Paste the Obsidian Sync token (leave blank to fill in .env later):";
272
- const obsidianAuthToken = (await prompts.text(pastePrompt, { defaultValue: "" })).trim();
249
+ // Auto-capture the Obsidian Sync token via a Docker volume mount when
250
+ // the daemon is reachable. Falls back to a paste prompt when capture
251
+ // fails or the user declines.
252
+ const capturedToken = docker.isDaemonRunning()
253
+ ? await offerSyncTokenCapture(prompts, docker)
254
+ : undefined;
255
+ // Masked prompt: the sync token is a credential and must not echo into
256
+ // the terminal or scrollback. An empty submission still means "fill in
257
+ // .env later" — clack's password prompt accepts blank input.
258
+ const obsidianAuthToken = capturedToken ??
259
+ (await prompts.password("Paste the Obsidian Sync token (leave blank to fill in .env later):")).trim();
273
260
  const usesEncryption = await prompts.confirm("Does your vault use end-to-end encryption?", false);
274
261
  const vaultPassword = usesEncryption
275
262
  ? await prompts.password("Vault encryption password:")
package/dist/main.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { createDockerRunner } from "./docker.js";
2
+ import { runGetSyncToken } from "./get-sync-token.js";
2
3
  import { runInit } from "./init.js";
3
4
  import { buildProgram } from "./program.js";
4
5
  import { createPrompts } from "./prompts.js";
@@ -16,6 +17,10 @@ export const run = async (version) => {
16
17
  docker: createDockerRunner(),
17
18
  fetchFn: fetch,
18
19
  }),
20
+ runGetSyncToken: (flags) => runGetSyncToken(flags, {
21
+ prompts: createPrompts(),
22
+ docker: createDockerRunner(),
23
+ }),
19
24
  });
20
25
  await program.parseAsync();
21
26
  };
package/dist/program.js CHANGED
@@ -22,6 +22,13 @@ export const buildProgram = (options) => {
22
22
  .action(async (flags) => {
23
23
  process.exitCode = await options.runUpgrade(flags);
24
24
  });
25
+ program
26
+ .command("get-sync-token")
27
+ .description("Generate an Obsidian Sync auth token via Docker and print it or write it to .env")
28
+ .option("--dir <path>", "directory containing .env to update with the token")
29
+ .action(async (flags) => {
30
+ process.exitCode = await options.runGetSyncToken(flags);
31
+ });
25
32
  program.action(() => {
26
33
  program.help();
27
34
  });
package/dist/scaffold.js CHANGED
@@ -57,6 +57,25 @@ export const detectMode = (envFilePath) => {
57
57
  const content = readFileSync(envFilePath, "utf8");
58
58
  return OBSIDIAN_AUTH_TOKEN_LINE.test(content) ? "remote" : "local";
59
59
  };
60
+ /**
61
+ * Patches the OBSIDIAN_AUTH_TOKEN value in an existing .env file.
62
+ * Returns true when the patch succeeded, false when the file is missing
63
+ * or has no active OBSIDIAN_AUTH_TOKEN line (e.g. a local-mode .env).
64
+ */
65
+ export const patchEnvObsidianToken = (envFilePath, token) => {
66
+ if (!existsSync(envFilePath))
67
+ return false;
68
+ const content = readFileSync(envFilePath, "utf8");
69
+ /** Matches the full OBSIDIAN_AUTH_TOKEN line for replacement. */
70
+ const fullTokenLine = /^OBSIDIAN_AUTH_TOKEN=.*$/m;
71
+ if (!fullTokenLine.test(content))
72
+ return false;
73
+ // Function replacement avoids $ pattern interpretation ($&, $', etc.)
74
+ // that String.prototype.replace applies to string replacements.
75
+ const patched = content.replace(fullTokenLine, () => `OBSIDIAN_AUTH_TOKEN=${token}`);
76
+ writeFileSync(envFilePath, patched);
77
+ return true;
78
+ };
60
79
  /**
61
80
  * Writes the files into targetDir (created if missing). Existing files
62
81
  * are never overwritten silently: identical content is skipped, and differing
package/dist/upgrade.js CHANGED
@@ -60,6 +60,7 @@ export const runUpgrade = async (flags, deps) => {
60
60
  return 1;
61
61
  }
62
62
  spinner.stop("Server is up — health check passed.");
63
+ prompts.log("Your vault data, search index, and settings are preserved.");
63
64
  prompts.outro("Upgrade complete.");
64
65
  return 0;
65
66
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vault-cortex",
3
- "version": "0.4.6-beta.26",
3
+ "version": "0.5.0-beta.28",
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",