vault-cortex 0.13.0 → 0.13.1-beta.64
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/dist/env.js +11 -8
- package/dist/init.js +41 -20
- package/dist/messages.js +4 -5
- package/dist/optional-settings.js +45 -3
- package/dist/vault.js +5 -0
- package/package.json +1 -1
package/dist/env.js
CHANGED
|
@@ -15,7 +15,7 @@ const LOCAL_OPTIONAL_BLOCK = `# Optional ─────────────
|
|
|
15
15
|
# "npx vault-cortex@latest restart" (plain docker restart does not
|
|
16
16
|
# re-read this file).
|
|
17
17
|
|
|
18
|
-
# Public URL for OAuth issuer
|
|
18
|
+
# Public URL for OAuth issuer and access-token binding (default: http://localhost:8000).
|
|
19
19
|
# Override if you expose the server on a different URL (e.g. via a reverse proxy).
|
|
20
20
|
PUBLIC_URL=http://localhost:8000
|
|
21
21
|
|
|
@@ -77,9 +77,10 @@ MEMORY_DIR=About Me
|
|
|
77
77
|
# DAILY_NOTES_FOLDER=Journal
|
|
78
78
|
# DAILY_NOTES_FORMAT=YYYY-MM-DD
|
|
79
79
|
|
|
80
|
-
# Comma-separated folders protected from deletion
|
|
81
|
-
# the daily notes folder
|
|
82
|
-
#
|
|
80
|
+
# Comma-separated folders protected from deletion and moves. Default: MEMORY_DIR plus
|
|
81
|
+
# the daily notes folder, read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json
|
|
82
|
+
# (default "Daily Notes"). When set, replaces the whole default — include the
|
|
83
|
+
# daily notes folder in your list if needed.
|
|
83
84
|
# PROTECTED_PATHS=About Me,Daily Notes
|
|
84
85
|
|
|
85
86
|
# Comma-separated folders excluded from orphan detection (default: the daily
|
|
@@ -215,9 +216,10 @@ MEMORY_DIR=About Me
|
|
|
215
216
|
# DAILY_NOTES_FOLDER=Journal
|
|
216
217
|
# DAILY_NOTES_FORMAT=YYYY-MM-DD
|
|
217
218
|
|
|
218
|
-
# Comma-separated folders protected from deletion
|
|
219
|
-
# the daily notes folder
|
|
220
|
-
#
|
|
219
|
+
# Comma-separated folders protected from deletion and moves. Default: MEMORY_DIR plus
|
|
220
|
+
# the daily notes folder, read from DAILY_NOTES_FOLDER or .obsidian/daily-notes.json
|
|
221
|
+
# (default "Daily Notes"). When set, replaces the whole default — include the
|
|
222
|
+
# daily notes folder in your list if needed.
|
|
221
223
|
# PROTECTED_PATHS=About Me,Daily Notes
|
|
222
224
|
|
|
223
225
|
# Comma-separated folders excluded from orphan detection (default: the daily
|
|
@@ -339,7 +341,8 @@ VAULT_PASSWORD=${answers.vaultPassword}`;
|
|
|
339
341
|
MCP_AUTH_TOKEN=${answers.mcpAuthToken}
|
|
340
342
|
|
|
341
343
|
# Public URL that MCP clients use to reach this server.
|
|
342
|
-
# Used as the OAuth issuer URL in discovery metadata
|
|
344
|
+
# Used as the OAuth issuer URL in discovery metadata and stamped on every
|
|
345
|
+
# access token; changing it invalidates connected clients' tokens.
|
|
343
346
|
PUBLIC_URL=${answers.publicUrl}
|
|
344
347
|
|
|
345
348
|
${obsidianTokenComment}
|
package/dist/init.js
CHANGED
|
@@ -86,28 +86,52 @@ const parseHttpUrl = (value) => {
|
|
|
86
86
|
return null;
|
|
87
87
|
}
|
|
88
88
|
};
|
|
89
|
+
/** Validates a PUBLIC_URL value: must be http(s), no credentials, no /mcp suffix. */
|
|
90
|
+
export const validatePublicUrl = (input) => {
|
|
91
|
+
const trimmed = input.trim();
|
|
92
|
+
const url = parseHttpUrl(trimmed);
|
|
93
|
+
if (!url) {
|
|
94
|
+
return {
|
|
95
|
+
kind: "error",
|
|
96
|
+
message: "PUBLIC_URL must be a full http:// or https:// URL (e.g. https://vault.example.com).",
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (url.username || url.password) {
|
|
100
|
+
return {
|
|
101
|
+
kind: "error",
|
|
102
|
+
message: "PUBLIC_URL must not contain credentials (user:password@).",
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
// Raw-string check: url.search/url.hash return "" for bare delimiters
|
|
106
|
+
// (WHATWG spec treats empty-string and null query/fragment identically),
|
|
107
|
+
// so a parsed-property check misses "https://host/?" and "https://host/#".
|
|
108
|
+
if (trimmed.includes("?") || trimmed.includes("#")) {
|
|
109
|
+
return {
|
|
110
|
+
kind: "error",
|
|
111
|
+
message: "PUBLIC_URL must be a bare origin or path — no query string (?...) or fragment (#...).",
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
if (TRAILING_MCP_PATH.test(url.pathname)) {
|
|
115
|
+
return {
|
|
116
|
+
kind: "error",
|
|
117
|
+
message: "Leave /mcp off PUBLIC_URL — it's the base URL and the server adds /mcp itself (e.g. https://vault.example.com).",
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
// Trim trailing slashes so the connect URL is `${base}/mcp`, never
|
|
121
|
+
// `${base}//mcp` — URL.href/.origin don't round-trip reverse-proxy subpaths.
|
|
122
|
+
return { kind: "ok", url: trimmed.replace(/\/+$/, "") };
|
|
123
|
+
};
|
|
89
124
|
/** Re-prompts until the answer is a valid base http(s) URL (no /mcp path). */
|
|
90
125
|
const askPublicUrl = async (prompts) => {
|
|
91
126
|
const answer = await prompts.text("Public base URL clients will use to reach this server (no /mcp — it's added for you):", {
|
|
92
127
|
placeholder: "https://vault.example.com or http://203.0.113.10:8000",
|
|
93
128
|
});
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
prompts.error("PUBLIC_URL must be a full http:// or https:// URL (e.g. https://vault.example.com).");
|
|
98
|
-
return askPublicUrl(prompts);
|
|
99
|
-
}
|
|
100
|
-
// Reject a re-included endpoint path instead of stripping it silently —
|
|
101
|
-
// PUBLIC_URL is the base origin and the server adds /mcp itself.
|
|
102
|
-
if (TRAILING_MCP_PATH.test(url.pathname)) {
|
|
103
|
-
prompts.error("Leave /mcp off PUBLIC_URL — it's the base URL and the server adds /mcp itself (e.g. https://vault.example.com).");
|
|
129
|
+
const result = validatePublicUrl(answer);
|
|
130
|
+
if (result.kind === "error") {
|
|
131
|
+
prompts.error(result.message);
|
|
104
132
|
return askPublicUrl(prompts);
|
|
105
133
|
}
|
|
106
|
-
|
|
107
|
-
// URL is `${base}/mcp`, never `${base}//mcp`. URL's own normalization is
|
|
108
|
-
// unusable here: `.href` adds a trailing slash and `.origin` drops the path,
|
|
109
|
-
// so neither round-trips a reverse-proxy subpath like https://host/api.
|
|
110
|
-
return trimmed.replace(/\/+$/, "");
|
|
134
|
+
return result.url;
|
|
111
135
|
};
|
|
112
136
|
/** Re-prompts until non-empty. */
|
|
113
137
|
const askVaultName = async (prompts) => {
|
|
@@ -395,11 +419,8 @@ export const runInit = async (flags, deps) => {
|
|
|
395
419
|
prompts.intro("vault-cortex init");
|
|
396
420
|
// Mode resolution: explicit --mode wins; --yes implies local; otherwise
|
|
397
421
|
// ask, defaulting to local — it's the simpler activation path.
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
: flags.yes
|
|
401
|
-
? "local"
|
|
402
|
-
: await askMode(prompts);
|
|
422
|
+
const flagMode = flags.mode && isMode(flags.mode) ? flags.mode : undefined;
|
|
423
|
+
const mode = flagMode ?? (flags.yes ? "local" : await askMode(prompts));
|
|
403
424
|
const exitCode = mode === "local"
|
|
404
425
|
? await runLocalInit(flags, deps)
|
|
405
426
|
: await runRemoteInit(flags, deps);
|
package/dist/messages.js
CHANGED
|
@@ -126,11 +126,10 @@ const updateGuidance = (targetDir) => `Update to the latest release:
|
|
|
126
126
|
export const buildLocalConnectMessage = (params) => {
|
|
127
127
|
const { targetDir, token, startStatus, port, tokenWritten } = params;
|
|
128
128
|
const baseUrl = `http://localhost:${port}`;
|
|
129
|
-
const
|
|
130
|
-
?
|
|
131
|
-
:
|
|
132
|
-
|
|
133
|
-
: startServerLine(targetDir);
|
|
129
|
+
const nonRunningLine = startStatus === "starting"
|
|
130
|
+
? startingInBackgroundLine()
|
|
131
|
+
: startServerLine(targetDir);
|
|
132
|
+
const startLine = startStatus === "running" ? "The server is running." : nonRunningLine;
|
|
134
133
|
const tokenLine = tokenBlock({ targetDir, token, tokenWritten });
|
|
135
134
|
// Once the server is confirmed up, the smoke test is dropped — the CLI just
|
|
136
135
|
// verified this exact URL, so re-printing it reads as leftover homework.
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { DEFAULT_PORT } from "./scaffold.js";
|
|
2
|
+
/** Matches Moment.js [...] literal escape groups, splitting format spans from literal content. */
|
|
3
|
+
const MOMENT_BRACKET_ESCAPE = /\[([^\]]*)\]/g;
|
|
2
4
|
// The curated prompt set — settings users most often want without reading
|
|
3
5
|
// .env comments. Everything else stays documented-only in the generated
|
|
4
6
|
// optional block, deliberately: every extra prompt costs init flow length.
|
|
@@ -23,6 +25,13 @@ const OPTIONAL_SETTINGS = [
|
|
|
23
25
|
label: "Daily notes folder",
|
|
24
26
|
question: "Vault folder for daily notes:",
|
|
25
27
|
placeholder: "blank = use your vault's daily notes settings",
|
|
28
|
+
validate: (value) => {
|
|
29
|
+
if (value.includes(".."))
|
|
30
|
+
return "Path traversal (..) is not allowed in folder names.";
|
|
31
|
+
if (value.startsWith("/"))
|
|
32
|
+
return "Absolute paths are not allowed — use a vault-relative folder name.";
|
|
33
|
+
return undefined;
|
|
34
|
+
},
|
|
26
35
|
},
|
|
27
36
|
{
|
|
28
37
|
kind: "optionalText",
|
|
@@ -30,6 +39,21 @@ const OPTIONAL_SETTINGS = [
|
|
|
30
39
|
label: "Daily notes format",
|
|
31
40
|
question: "Filename date format for daily notes (e.g. YYYY-MM-DD):",
|
|
32
41
|
placeholder: "blank = use your vault's daily notes settings",
|
|
42
|
+
validate: (value) => {
|
|
43
|
+
if (value.includes(".."))
|
|
44
|
+
return "Date format must not contain path traversal (..).";
|
|
45
|
+
if (value.startsWith("/"))
|
|
46
|
+
return "Date format must not start with a path separator.";
|
|
47
|
+
if (value.endsWith("/"))
|
|
48
|
+
return "Date format must not end with a path separator.";
|
|
49
|
+
// Moment format tokens are all letters — digits outside of [...]
|
|
50
|
+
// bracket escapes are almost always a mistake.
|
|
51
|
+
const formatSegments = value.split(MOMENT_BRACKET_ESCAPE);
|
|
52
|
+
const hasDigitsInFormat = formatSegments.some((segment, index) => index % 2 === 0 && /\d/.test(segment));
|
|
53
|
+
if (hasDigitsInFormat)
|
|
54
|
+
return "Date format should use Moment tokens (YYYY, MM, DD), not digits — wrap literal text in [...] brackets.";
|
|
55
|
+
return undefined;
|
|
56
|
+
},
|
|
33
57
|
},
|
|
34
58
|
{
|
|
35
59
|
kind: "toggle",
|
|
@@ -193,8 +217,17 @@ const askFolder = async (params, prompts) => {
|
|
|
193
217
|
defaultValue: currentValue ?? defaultValue,
|
|
194
218
|
placeholder: defaultValue,
|
|
195
219
|
})).trim();
|
|
196
|
-
if (answer !== "")
|
|
220
|
+
if (answer !== "") {
|
|
221
|
+
if (answer.includes("..")) {
|
|
222
|
+
prompts.error("Path traversal (..) is not allowed in folder names.");
|
|
223
|
+
return askFolder(params, prompts);
|
|
224
|
+
}
|
|
225
|
+
if (answer.startsWith("/")) {
|
|
226
|
+
prompts.error("Absolute paths are not allowed — use a vault-relative folder name.");
|
|
227
|
+
return askFolder(params, prompts);
|
|
228
|
+
}
|
|
197
229
|
return answer;
|
|
230
|
+
}
|
|
198
231
|
prompts.error("The folder name can't be empty.");
|
|
199
232
|
return askFolder(params, prompts);
|
|
200
233
|
};
|
|
@@ -251,12 +284,21 @@ const askSettingValue = async (params, prompts) => {
|
|
|
251
284
|
currentValue,
|
|
252
285
|
defaultValue: setting.defaultValue,
|
|
253
286
|
}, prompts);
|
|
254
|
-
case "optionalText":
|
|
255
|
-
|
|
287
|
+
case "optionalText": {
|
|
288
|
+
const value = await askOptionalText({
|
|
256
289
|
question: setting.question,
|
|
257
290
|
placeholder: setting.placeholder,
|
|
258
291
|
currentValue,
|
|
259
292
|
}, prompts);
|
|
293
|
+
if (value && setting.validate) {
|
|
294
|
+
const error = setting.validate(value);
|
|
295
|
+
if (error) {
|
|
296
|
+
prompts.error(error);
|
|
297
|
+
return askSettingValue(params, prompts);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return value;
|
|
301
|
+
}
|
|
260
302
|
case "choice":
|
|
261
303
|
return prompts.select(setting.question, setting.choices, currentValue ?? setting.defaultValue);
|
|
262
304
|
}
|
package/dist/vault.js
CHANGED
|
@@ -21,6 +21,11 @@ export const validateVaultPath = (input) => {
|
|
|
21
21
|
const trimmed = input.trim();
|
|
22
22
|
if (trimmed === "")
|
|
23
23
|
return { kind: "error", message: "Vault path is required." };
|
|
24
|
+
if (/[*?[]/.test(trimmed))
|
|
25
|
+
return {
|
|
26
|
+
kind: "error",
|
|
27
|
+
message: "Vault path must not contain glob characters (*, ?, [).",
|
|
28
|
+
};
|
|
24
29
|
const absolutePath = resolve(expandTilde(trimmed));
|
|
25
30
|
if (!existsSync(absolutePath)) {
|
|
26
31
|
return { kind: "error", message: `Path does not exist: ${absolutePath}` };
|
package/package.json
CHANGED