vault-cortex 0.12.0-beta.61 → 0.13.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 +5 -6
- package/dist/env.js +5 -5
- package/dist/get-sync-token.js +55 -57
- package/dist/init.js +20 -12
- package/dist/lifecycle.js +2 -1
- package/dist/scaffold.js +48 -5
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -60,9 +60,8 @@ What it does:
|
|
|
60
60
|
Re-running init where a setup already exists asks first — declining leaves
|
|
61
61
|
everything unchanged and points you at [`configure`](#configure), the right
|
|
62
62
|
tool for changing settings in place. Existing files are never overwritten
|
|
63
|
-
without asking. During a remote setup, init offers to
|
|
64
|
-
[
|
|
65
|
-
account right from the terminal.
|
|
63
|
+
without asking. During a remote setup, init offers to generate your
|
|
64
|
+
[Obsidian Sync token](#get-sync-token) as part of the flow.
|
|
66
65
|
|
|
67
66
|
Flags:
|
|
68
67
|
|
|
@@ -196,9 +195,8 @@ npx vault-cortex@latest get-sync-token
|
|
|
196
195
|
```
|
|
197
196
|
|
|
198
197
|
The command prompts for your Obsidian account email, password, and MFA code
|
|
199
|
-
(if enabled), signs in via the Obsidian API, and prints the token.
|
|
200
|
-
|
|
201
|
-
`.env` instead:
|
|
198
|
+
(if enabled), signs in via the Obsidian API, and prints the token. Use
|
|
199
|
+
`--dir <path>` to write the token straight into an existing `.env` instead:
|
|
202
200
|
|
|
203
201
|
```bash
|
|
204
202
|
npx vault-cortex@latest get-sync-token --dir ./vault-cortex
|
|
@@ -218,4 +216,5 @@ During `init --mode remote`, this flow is offered automatically.
|
|
|
218
216
|
|
|
219
217
|
- [Local quickstart](https://github.com/aliasunder/vault-cortex/blob/main/deploy/local/README.md)
|
|
220
218
|
- [Remote quickstart (VPS + Obsidian Sync)](https://github.com/aliasunder/vault-cortex/blob/main/deploy/remote/README.md)
|
|
219
|
+
- [One-click deploy on Render or Railway](https://github.com/aliasunder/vault-cortex#one-click-deploy)
|
|
221
220
|
- [Full project README](https://github.com/aliasunder/vault-cortex)
|
package/dist/env.js
CHANGED
|
@@ -324,11 +324,11 @@ export const buildRemoteEnv = (answers) => {
|
|
|
324
324
|
# VAULT_PASSWORD=`
|
|
325
325
|
: `# Vault end-to-end encryption password.
|
|
326
326
|
VAULT_PASSWORD=${answers.vaultPassword}`;
|
|
327
|
-
const obsidianTokenComment = answers.obsidianAuthToken
|
|
328
|
-
? `# Obsidian Sync auth token
|
|
327
|
+
const obsidianTokenComment = answers.obsidianAuthToken
|
|
328
|
+
? `# Obsidian Sync auth token.`
|
|
329
|
+
: `# Obsidian Sync auth token — FILL THIS IN before starting the server.
|
|
329
330
|
# Generate once with:
|
|
330
|
-
# npx vault-cortex@latest get-sync-token
|
|
331
|
-
: `# Obsidian Sync auth token.`;
|
|
331
|
+
# npx vault-cortex@latest get-sync-token`;
|
|
332
332
|
return `# vault-cortex — remote quickstart (Obsidian Sync)
|
|
333
333
|
# Generated by \`npx vault-cortex@latest init\`. Full option reference:
|
|
334
334
|
# https://github.com/aliasunder/vault-cortex/blob/main/deploy/remote/.env.example
|
|
@@ -343,7 +343,7 @@ MCP_AUTH_TOKEN=${answers.mcpAuthToken}
|
|
|
343
343
|
PUBLIC_URL=${answers.publicUrl}
|
|
344
344
|
|
|
345
345
|
${obsidianTokenComment}
|
|
346
|
-
OBSIDIAN_AUTH_TOKEN=${answers.obsidianAuthToken}
|
|
346
|
+
OBSIDIAN_AUTH_TOKEN=${answers.obsidianAuthToken ?? ""}
|
|
347
347
|
|
|
348
348
|
# Exact name of your Obsidian vault (case-sensitive).
|
|
349
349
|
VAULT_NAME=${answers.vaultName}
|
package/dist/get-sync-token.js
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { join, resolve } from "node:path";
|
|
2
2
|
import { patchEnvObsidianToken } from "./scaffold.js";
|
|
3
3
|
import { expandTilde } from "./vault.js";
|
|
4
|
-
const OBSIDIAN_SIGNIN_URL = "https://api.obsidian.md/user/signin";
|
|
4
|
+
const OBSIDIAN_SIGNIN_URL = process.env.OBSIDIAN_SIGNIN_URL ?? "https://api.obsidian.md/user/signin";
|
|
5
5
|
const SIGNIN_TIMEOUT_MS = 30_000;
|
|
6
6
|
const describeError = (error) => error instanceof Error ? error.message : String(error);
|
|
7
|
+
class ObsidianApiError extends Error {
|
|
8
|
+
constructor(message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "ObsidianApiError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
const isJsonObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7
14
|
/**
|
|
8
15
|
* Calls the Obsidian Sync signin API. Returns the parsed JSON on success,
|
|
9
16
|
* or throws on HTTP/network errors. The API returns { error: string } for
|
|
@@ -23,37 +30,37 @@ const callSigninApi = async (params, fetchFn) => {
|
|
|
23
30
|
}),
|
|
24
31
|
signal: AbortSignal.timeout(SIGNIN_TIMEOUT_MS),
|
|
25
32
|
});
|
|
26
|
-
if (!response.ok)
|
|
33
|
+
if (!response.ok)
|
|
27
34
|
throw new Error(`HTTP Error ${response.status}`);
|
|
28
|
-
}
|
|
29
|
-
let body;
|
|
30
35
|
try {
|
|
31
|
-
body = await response.json();
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
const body = await response.json();
|
|
37
|
+
if (!isJsonObject(body))
|
|
38
|
+
throw new Error("not a JSON object");
|
|
39
|
+
if (typeof body.error === "string")
|
|
40
|
+
throw new ObsidianApiError(body.error);
|
|
41
|
+
if (typeof body.token !== "string" || !body.token)
|
|
42
|
+
throw new Error("no token field");
|
|
43
|
+
return body.token;
|
|
39
44
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (!token) {
|
|
45
|
-
throw new Error("Unexpected response from Obsidian API (no token)");
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (error instanceof ObsidianApiError)
|
|
47
|
+
throw error;
|
|
48
|
+
throw new Error(`Unexpected response from Obsidian API (${describeError(error)})`, { cause: error });
|
|
46
49
|
}
|
|
47
|
-
const name = "name" in body && typeof body.name === "string" ? body.name : "";
|
|
48
|
-
const email = "email" in body && typeof body.email === "string" ? body.email : "";
|
|
49
|
-
return { token, name, email };
|
|
50
50
|
};
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Warns the user about a signin failure with a message tailored to the
|
|
53
|
+
* error type. Called by both the initial signin and MFA retry paths.
|
|
54
|
+
*/
|
|
55
|
+
const warnSigninError = (error, prompts, isMfaRetry) => {
|
|
56
|
+
if (error instanceof Error && error.name === "TimeoutError") {
|
|
57
|
+
prompts.warn("Request timed out — check your internet connection and try again.");
|
|
58
|
+
return;
|
|
55
59
|
}
|
|
56
|
-
|
|
60
|
+
const isMfaError = error instanceof ObsidianApiError && error.message.includes("2FA code");
|
|
61
|
+
const mfaHint = isMfaRetry && isMfaError ? "\n Check your 2FA code and try again." : "";
|
|
62
|
+
prompts.warn(`Could not sign in: ${describeError(error)}${mfaHint}`);
|
|
63
|
+
};
|
|
57
64
|
/**
|
|
58
65
|
* Signs in to the user's Obsidian account via the Sync API and returns
|
|
59
66
|
* the auth token. Prompts for email, password, and MFA code (when 2FA
|
|
@@ -68,45 +75,35 @@ export const captureObsidianToken = async (deps) => {
|
|
|
68
75
|
const spinner = prompts.spinner();
|
|
69
76
|
spinner.start("Signing in to Obsidian...");
|
|
70
77
|
try {
|
|
71
|
-
const
|
|
72
|
-
spinner.stop(`Signed in as ${
|
|
73
|
-
return
|
|
78
|
+
const token = await callSigninApi({ email, password, mfa: "" }, fetchFn);
|
|
79
|
+
spinner.stop(`Signed in as ${email}.`);
|
|
80
|
+
return token;
|
|
74
81
|
}
|
|
75
82
|
catch (error) {
|
|
76
83
|
// MFA required: the API returns an error containing "2FA code" — prompt
|
|
77
84
|
// and retry. "2FA code is incorrect" is a wrong-code rejection, not a
|
|
78
85
|
// prompt-for-code signal. Mirrors the obsidian-headless v0.0.14 logic.
|
|
79
|
-
|
|
86
|
+
const needsMfa = error instanceof ObsidianApiError &&
|
|
80
87
|
error.message.includes("2FA code") &&
|
|
81
|
-
!error.message.includes("2FA code is incorrect")
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
return undefined;
|
|
95
|
-
}
|
|
96
|
-
const retryHint = retryError instanceof ObsidianApiError
|
|
97
|
-
? "\n Check your 2FA code and try again."
|
|
98
|
-
: "";
|
|
99
|
-
prompts.warn(`Could not sign in: ${describeError(retryError)}${retryHint}`);
|
|
100
|
-
return undefined;
|
|
101
|
-
}
|
|
88
|
+
!error.message.includes("2FA code is incorrect");
|
|
89
|
+
if (!needsMfa) {
|
|
90
|
+
spinner.stop("Sign-in failed.");
|
|
91
|
+
warnSigninError(error, prompts, false);
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
spinner.stop("Two-factor authentication required.");
|
|
95
|
+
const mfaCode = await prompts.text("2FA code:");
|
|
96
|
+
spinner.start("Verifying...");
|
|
97
|
+
try {
|
|
98
|
+
const token = await callSigninApi({ email, password, mfa: mfaCode }, fetchFn);
|
|
99
|
+
spinner.stop(`Signed in as ${email}.`);
|
|
100
|
+
return token;
|
|
102
101
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
102
|
+
catch (retryError) {
|
|
103
|
+
spinner.stop("Sign-in failed.");
|
|
104
|
+
warnSigninError(retryError, prompts, true);
|
|
106
105
|
return undefined;
|
|
107
106
|
}
|
|
108
|
-
prompts.warn(`Could not sign in: ${describeError(error)}`);
|
|
109
|
-
return undefined;
|
|
110
107
|
}
|
|
111
108
|
};
|
|
112
109
|
/**
|
|
@@ -137,7 +134,8 @@ export const runGetSyncToken = async (flags, deps) => {
|
|
|
137
134
|
"OBSIDIAN_AUTH_TOKEN line. Run init first.");
|
|
138
135
|
return 1;
|
|
139
136
|
}
|
|
140
|
-
prompts.log(`Token written to ${envFilePath}`
|
|
137
|
+
prompts.log(`Token written to ${envFilePath}\n\n` +
|
|
138
|
+
`Start the server:\n npx vault-cortex start --dir "${flags.dir}"`);
|
|
141
139
|
prompts.outro("Done.");
|
|
142
140
|
return 0;
|
|
143
141
|
};
|
package/dist/init.js
CHANGED
|
@@ -6,7 +6,7 @@ import { buildDaemonNotRunningMessage, buildDockerNotInstalledMessage, buildLoca
|
|
|
6
6
|
import { healthPollTimeoutMs, healthTimeoutMessage, pollHealth, } from "./docker.js";
|
|
7
7
|
import { reportPublicUrlProbe } from "./lifecycle.js";
|
|
8
8
|
import { applyOptionalSettings, askOptionalSettings, derivePublicUrlOverride, } from "./optional-settings.js";
|
|
9
|
-
import { buildFilesToWrite, readEnvPort, readEnvPublicUrl, writeFiles, } from "./scaffold.js";
|
|
9
|
+
import { buildFilesToWrite, readEnvObsidianToken, readEnvPort, readEnvPublicUrl, stripEnvQuotedValues, writeFiles, } from "./scaffold.js";
|
|
10
10
|
import { generateToken } from "./token.js";
|
|
11
11
|
import { expandTilde, validateVaultPath } from "./vault.js";
|
|
12
12
|
const DEFAULT_TARGET_DIR = "./vault-cortex";
|
|
@@ -29,9 +29,12 @@ const askMode = async (prompts) => {
|
|
|
29
29
|
/**
|
|
30
30
|
* Offers to sign in to the Obsidian account and capture the Sync token.
|
|
31
31
|
* Returns the captured token string, or undefined when the user declines
|
|
32
|
-
* or the capture fails (the caller falls back to
|
|
32
|
+
* or the capture fails (the caller falls back to any token already in the
|
|
33
|
+
* on-disk .env, or shows get-sync-token guidance).
|
|
33
34
|
*/
|
|
34
35
|
const offerSyncTokenCapture = async (prompts, fetchFn) => {
|
|
36
|
+
prompts.log("Your server needs an Obsidian Sync token to access your vault.\n" +
|
|
37
|
+
"You can sign in to your Obsidian account now to generate one.");
|
|
35
38
|
const runNow = await prompts.confirm("Generate the token now?", true);
|
|
36
39
|
if (!runNow)
|
|
37
40
|
return undefined;
|
|
@@ -174,9 +177,11 @@ const offerDockerRun = async (params, deps) => {
|
|
|
174
177
|
const startNow = await prompts.confirm("Start the server now?", true);
|
|
175
178
|
if (!startNow)
|
|
176
179
|
return "not-started";
|
|
180
|
+
const envFilePath = join(targetDir, ".env");
|
|
181
|
+
stripEnvQuotedValues(envFilePath);
|
|
177
182
|
const containerStarted = docker.dockerRun({
|
|
178
183
|
mode,
|
|
179
|
-
envFilePath
|
|
184
|
+
envFilePath,
|
|
180
185
|
port,
|
|
181
186
|
vaultPath,
|
|
182
187
|
});
|
|
@@ -304,13 +309,16 @@ const runRemoteInit = async (flags, deps) => {
|
|
|
304
309
|
const publicUrl = await askPublicUrl(prompts);
|
|
305
310
|
const vaultName = await askVaultName(prompts);
|
|
306
311
|
// Sign in to Obsidian and capture the Sync token directly via the API.
|
|
307
|
-
//
|
|
312
|
+
// When the user declines or capture fails, fall back to any token already
|
|
313
|
+
// in the on-disk .env (a re-init over an existing deployment). Only show
|
|
314
|
+
// the "run get-sync-token later" guidance when neither source has a token.
|
|
308
315
|
const capturedToken = await offerSyncTokenCapture(prompts, fetchFn);
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
316
|
+
const existingEnvToken = readEnvObsidianToken(join(targetDir, ".env"));
|
|
317
|
+
const hasExistingToken = Boolean(capturedToken ?? existingEnvToken);
|
|
318
|
+
if (!hasExistingToken) {
|
|
319
|
+
prompts.log("No token yet — run this later to add it to your .env:\n" +
|
|
320
|
+
` npx vault-cortex@latest get-sync-token --dir "${targetDir}"`);
|
|
321
|
+
}
|
|
314
322
|
const usesEncryption = await prompts.confirm("Does your vault use end-to-end encryption?", false);
|
|
315
323
|
const vaultPassword = usesEncryption
|
|
316
324
|
? await prompts.password("Vault encryption password:")
|
|
@@ -324,7 +332,7 @@ const runRemoteInit = async (flags, deps) => {
|
|
|
324
332
|
const defaultEnvContent = buildRemoteEnv({
|
|
325
333
|
mcpAuthToken: token,
|
|
326
334
|
publicUrl,
|
|
327
|
-
obsidianAuthToken,
|
|
335
|
+
obsidianAuthToken: capturedToken ?? existingEnvToken,
|
|
328
336
|
vaultName,
|
|
329
337
|
vaultPassword,
|
|
330
338
|
});
|
|
@@ -349,7 +357,7 @@ const runRemoteInit = async (flags, deps) => {
|
|
|
349
357
|
const effectivePublicUrl = readEnvPublicUrl(join(targetDir, ".env")) ?? publicUrl;
|
|
350
358
|
// Without the sync token the container can't start (init-check-auth fails
|
|
351
359
|
// and s6 stops it), so only offer docker run when it was provided.
|
|
352
|
-
const startStatus =
|
|
360
|
+
const startStatus = !hasExistingToken
|
|
353
361
|
? "not-started"
|
|
354
362
|
: await offerDockerRun({ targetDir, port, mode: "remote" }, deps);
|
|
355
363
|
// The container check above hit localhost on this machine; the public URL
|
|
@@ -365,7 +373,7 @@ const runRemoteInit = async (flags, deps) => {
|
|
|
365
373
|
token,
|
|
366
374
|
publicUrl: effectivePublicUrl,
|
|
367
375
|
startStatus,
|
|
368
|
-
obsidianTokenMissing:
|
|
376
|
+
obsidianTokenMissing: !hasExistingToken,
|
|
369
377
|
tokenWritten,
|
|
370
378
|
}));
|
|
371
379
|
return 0;
|
package/dist/lifecycle.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { join, resolve } from "node:path";
|
|
2
2
|
import { CONTAINER_NAME, healthPollTimeoutMs, healthTimeoutMessage, pollHealth, probeHealth, } from "./docker.js";
|
|
3
3
|
import { buildDaemonNotRunningMessage, buildDockerNotInstalledMessage, } from "./messages.js";
|
|
4
|
-
import { detectMode, hasEnvPublicUrl, readEnvPort, readEnvPublicUrl, readEnvVaultPath, } from "./scaffold.js";
|
|
4
|
+
import { detectMode, hasEnvPublicUrl, readEnvPort, readEnvPublicUrl, readEnvVaultPath, stripEnvQuotedValues, } from "./scaffold.js";
|
|
5
5
|
import { expandTilde } from "./vault.js";
|
|
6
6
|
const DEFAULT_TARGET_DIR = "./vault-cortex";
|
|
7
7
|
/**
|
|
@@ -110,6 +110,7 @@ export const recreateContainer = async (params, deps) => {
|
|
|
110
110
|
prompts.error(`Could not remove the existing container — check: docker rm -f ${CONTAINER_NAME}`);
|
|
111
111
|
return 1;
|
|
112
112
|
}
|
|
113
|
+
stripEnvQuotedValues(deployment.envFilePath);
|
|
113
114
|
prompts.log("Starting container...");
|
|
114
115
|
const containerStarted = docker.dockerRun({
|
|
115
116
|
mode: deployment.mode,
|
package/dist/scaffold.js
CHANGED
|
@@ -2,8 +2,8 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync, } from "
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
/** Default host port — matches the container's internal port. */
|
|
4
4
|
export const DEFAULT_PORT = 8000;
|
|
5
|
-
/** Matches an active (uncommented) PORT line
|
|
6
|
-
const ENV_PORT_LINE = /^PORT=(\d+)
|
|
5
|
+
/** Matches an active (uncommented) PORT line, with optional surrounding quotes. */
|
|
6
|
+
const ENV_PORT_LINE = /^PORT=["']?(\d+)["']?\s*$/m;
|
|
7
7
|
/** Matches an active (uncommented) VAULT_PATH line in a .env file. */
|
|
8
8
|
const ENV_VAULT_PATH_LINE = /^VAULT_PATH=(.+)\s*$/m;
|
|
9
9
|
/** Matches an active (uncommented) PUBLIC_URL line. */
|
|
@@ -12,6 +12,17 @@ const ENV_PUBLIC_URL_LINE = /^PUBLIC_URL=/m;
|
|
|
12
12
|
const ENV_PUBLIC_URL_VALUE_LINE = /^PUBLIC_URL=(.+)\s*$/m;
|
|
13
13
|
/** Matches an active (uncommented) OBSIDIAN_AUTH_TOKEN line. */
|
|
14
14
|
const OBSIDIAN_AUTH_TOKEN_LINE = /^OBSIDIAN_AUTH_TOKEN=/m;
|
|
15
|
+
/** Matches an env line whose value is wrapped in matching quotes. */
|
|
16
|
+
const QUOTED_ENV_VALUE = /^([A-Za-z_][A-Za-z0-9_]*=)(["'])(.*)\2(\s*)$/gm;
|
|
17
|
+
/**
|
|
18
|
+
* Strips matching surrounding quotes from a value — `"foo"` → `foo`,
|
|
19
|
+
* `'bar'` → `bar`, `unquoted` → `unquoted`. Only strips when the
|
|
20
|
+
* opening and closing quote characters match.
|
|
21
|
+
*/
|
|
22
|
+
const stripSurroundingQuotes = (value) => {
|
|
23
|
+
const quoteMatch = /^(["'])(.*)\1$/.exec(value);
|
|
24
|
+
return quoteMatch ? quoteMatch[2] : value;
|
|
25
|
+
};
|
|
15
26
|
export const buildFilesToWrite = (envContent) => [
|
|
16
27
|
// .env holds the bearer token (and possibly a vault password) — owner-only.
|
|
17
28
|
{ name: ".env", content: envContent, mode: 0o600 },
|
|
@@ -36,7 +47,8 @@ export const readEnvVaultPath = (envFilePath) => {
|
|
|
36
47
|
if (!existsSync(envFilePath))
|
|
37
48
|
return undefined;
|
|
38
49
|
const match = ENV_VAULT_PATH_LINE.exec(readFileSync(envFilePath, "utf8"));
|
|
39
|
-
|
|
50
|
+
const rawValue = match?.[1].trim();
|
|
51
|
+
return rawValue ? stripSurroundingQuotes(rawValue) : undefined;
|
|
40
52
|
};
|
|
41
53
|
/**
|
|
42
54
|
* Returns true when the .env file has an active (uncommented) PUBLIC_URL line.
|
|
@@ -63,11 +75,12 @@ export const readEnvPublicUrl = (envFilePath) => {
|
|
|
63
75
|
const match = ENV_PUBLIC_URL_VALUE_LINE.exec(readFileSync(envFilePath, "utf8"));
|
|
64
76
|
// A whitespace-only line matches the regex and trims to "" — normalize to
|
|
65
77
|
// undefined so the non-empty contract holds ("" is never a legitimate URL).
|
|
66
|
-
const
|
|
78
|
+
const rawValue = match?.[1].trim();
|
|
79
|
+
const unquotedValue = rawValue ? stripSurroundingQuotes(rawValue) : undefined;
|
|
67
80
|
// Strip trailing slashes (mirroring askPublicUrl's prompt-side
|
|
68
81
|
// normalization): consumers append paths to this base, and a hand-edited
|
|
69
82
|
// `https://host/` would otherwise print broken `//mcp` connect URLs.
|
|
70
|
-
const normalizedPublicUrl =
|
|
83
|
+
const normalizedPublicUrl = unquotedValue?.replace(/\/+$/, "");
|
|
71
84
|
return normalizedPublicUrl || undefined;
|
|
72
85
|
};
|
|
73
86
|
/**
|
|
@@ -100,6 +113,36 @@ export const patchEnvObsidianToken = (envFilePath, token) => {
|
|
|
100
113
|
writeFileSync(envFilePath, patched);
|
|
101
114
|
return true;
|
|
102
115
|
};
|
|
116
|
+
/**
|
|
117
|
+
* Reads the OBSIDIAN_AUTH_TOKEN value from an existing .env file. Returns
|
|
118
|
+
* undefined when the file is missing, has no active line, or the value is
|
|
119
|
+
* empty — an empty `OBSIDIAN_AUTH_TOKEN=` line is not a valid token.
|
|
120
|
+
*/
|
|
121
|
+
export const readEnvObsidianToken = (envFilePath) => {
|
|
122
|
+
if (!existsSync(envFilePath))
|
|
123
|
+
return undefined;
|
|
124
|
+
const match = /^OBSIDIAN_AUTH_TOKEN=(.+)$/m.exec(readFileSync(envFilePath, "utf8"));
|
|
125
|
+
return match?.[1].trim() || undefined;
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Strips surrounding quotes from env values in the file. `docker run
|
|
129
|
+
* --env-file` passes quotes literally (`VAULT_NAME="My Vault"` becomes
|
|
130
|
+
* the value `"My Vault"` with embedded quotes), while Compose strips
|
|
131
|
+
* them. Removing quotes makes the file work correctly for both paths.
|
|
132
|
+
* Returns true when the file was modified.
|
|
133
|
+
*/
|
|
134
|
+
export const stripEnvQuotedValues = (envFilePath) => {
|
|
135
|
+
if (!existsSync(envFilePath))
|
|
136
|
+
return false;
|
|
137
|
+
const content = readFileSync(envFilePath, "utf8");
|
|
138
|
+
// Reset lastIndex — the /g flag makes the regex stateful.
|
|
139
|
+
QUOTED_ENV_VALUE.lastIndex = 0;
|
|
140
|
+
const sanitized = content.replace(QUOTED_ENV_VALUE, "$1$3$4");
|
|
141
|
+
if (sanitized === content)
|
|
142
|
+
return false;
|
|
143
|
+
writeFileSync(envFilePath, sanitized);
|
|
144
|
+
return true;
|
|
145
|
+
};
|
|
103
146
|
/**
|
|
104
147
|
* Writes the files into targetDir (created if missing). Existing files
|
|
105
148
|
* are never overwritten silently: identical content is skipped, and differing
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vault-cortex",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Set up a Vault Cortex MCP server for your Obsidian vault in one command: npx vault-cortex@latest init",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -32,7 +32,6 @@
|
|
|
32
32
|
"obsidian-vault",
|
|
33
33
|
"obsidian-sync",
|
|
34
34
|
"ai-agents",
|
|
35
|
-
"ai-memory",
|
|
36
35
|
"ai-memory-system",
|
|
37
36
|
"knowledge-base",
|
|
38
37
|
"note-taking",
|
|
@@ -42,7 +41,7 @@
|
|
|
42
41
|
"task-management",
|
|
43
42
|
"semantic-search",
|
|
44
43
|
"attachments",
|
|
45
|
-
"
|
|
44
|
+
"one-click-deploy",
|
|
46
45
|
"self-hosted",
|
|
47
46
|
"docker",
|
|
48
47
|
"claude",
|