vault-cortex 0.2.4 → 0.2.6
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/init.js +50 -12
- package/dist/messages.js +106 -63
- package/dist/prompts.js +3 -0
- package/package.json +1 -1
package/dist/init.js
CHANGED
|
@@ -4,7 +4,7 @@ import { buildLocalConnectMessage, buildRemoteConnectMessage, } from "./messages
|
|
|
4
4
|
import { OBSIDIAN_SYNC_IMAGE, pollHealth } from "./docker.js";
|
|
5
5
|
import { buildFilesToWrite, readEnvPort, writeFiles, } from "./scaffold.js";
|
|
6
6
|
import { generateToken } from "./token.js";
|
|
7
|
-
import { validateVaultPath } from "./vault.js";
|
|
7
|
+
import { expandTilde, validateVaultPath } from "./vault.js";
|
|
8
8
|
const DEFAULT_TARGET_DIR = "./vault-cortex";
|
|
9
9
|
const isMode = (value) => value === "local" || value === "remote";
|
|
10
10
|
const askMode = async (prompts) => {
|
|
@@ -68,16 +68,50 @@ const askVaultPath = async (prompts) => {
|
|
|
68
68
|
}
|
|
69
69
|
return validation.path;
|
|
70
70
|
};
|
|
71
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* A trailing `/mcp` path segment, optionally followed by slashes, anchored to
|
|
73
|
+
* the end of a URL's pathname (`/MCP`, `/mcp/` match too via the `i` flag —
|
|
74
|
+
* WHATWG URL preserves path case). The server owns the `/mcp` endpoint and
|
|
75
|
+
* appends it when building the connect URL, so PUBLIC_URL must be the base
|
|
76
|
+
* origin; a re-included `/mcp` is rejected, not silently rewritten.
|
|
77
|
+
*/
|
|
78
|
+
const TRAILING_MCP_PATH = /\/mcp\/*$/i;
|
|
79
|
+
/**
|
|
80
|
+
* Parses an http(s) URL with the WHATWG `URL` constructor, returning null for
|
|
81
|
+
* anything it can't be: bad syntax, a missing scheme, or a non-http(s)
|
|
82
|
+
* protocol (`ws:`, `file:`, ...). More robust than a `startsWith` check, which
|
|
83
|
+
* would pass malformed inputs like `https://` or `https://a b.com`.
|
|
84
|
+
*/
|
|
85
|
+
const parseHttpUrl = (value) => {
|
|
86
|
+
try {
|
|
87
|
+
const url = new URL(value);
|
|
88
|
+
return url.protocol === "http:" || url.protocol === "https:" ? url : null;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
/** Re-prompts until the answer is a valid base http(s) URL (no /mcp path). */
|
|
72
95
|
const askPublicUrl = async (prompts) => {
|
|
73
|
-
const answer = await prompts.text("Public URL clients will use to reach this server:", {
|
|
96
|
+
const answer = await prompts.text("Public base URL clients will use to reach this server (no /mcp — it's added for you):", {
|
|
74
97
|
placeholder: "https://vault.example.com or http://203.0.113.10:8000",
|
|
75
98
|
});
|
|
76
99
|
const trimmed = answer.trim();
|
|
77
|
-
|
|
78
|
-
|
|
100
|
+
const url = parseHttpUrl(trimmed);
|
|
101
|
+
if (url === null) {
|
|
102
|
+
prompts.error("PUBLIC_URL must be a full http:// or https:// URL (e.g. https://vault.example.com).");
|
|
103
|
+
return askPublicUrl(prompts);
|
|
104
|
+
}
|
|
105
|
+
// Reject a re-included endpoint path instead of stripping it silently —
|
|
106
|
+
// PUBLIC_URL is the base origin and the server adds /mcp itself.
|
|
107
|
+
if (TRAILING_MCP_PATH.test(url.pathname)) {
|
|
108
|
+
prompts.error("Leave /mcp off PUBLIC_URL — it's the base URL and the server adds /mcp itself (e.g. https://vault.example.com).");
|
|
79
109
|
return askPublicUrl(prompts);
|
|
80
110
|
}
|
|
111
|
+
// Store the input as typed, trimming only a trailing slash so the connect
|
|
112
|
+
// URL is `${base}/mcp`, never `${base}//mcp`. URL's own normalization is
|
|
113
|
+
// unusable here: `.href` adds a trailing slash and `.origin` drops the path,
|
|
114
|
+
// so neither round-trips a reverse-proxy subpath like https://host/api.
|
|
81
115
|
return trimmed.replace(/\/+$/, "");
|
|
82
116
|
};
|
|
83
117
|
/** Re-prompts until non-empty. */
|
|
@@ -169,13 +203,15 @@ const runLocalInit = async (flags, deps) => {
|
|
|
169
203
|
const vaultPath = vaultPathResult !== undefined && vaultPathResult.kind !== "error"
|
|
170
204
|
? vaultPathResult.path
|
|
171
205
|
: await askVaultPath(prompts);
|
|
172
|
-
|
|
206
|
+
// expandTilde before resolve: resolve() treats a leading `~` as a literal
|
|
207
|
+
// path segment, so a quoted "~/path" would create a directory named "~".
|
|
208
|
+
const targetDir = resolve(expandTilde(flags.dir ??
|
|
173
209
|
(flags.yes
|
|
174
210
|
? DEFAULT_TARGET_DIR
|
|
175
211
|
: await prompts.text("Where should I put the config files?", {
|
|
176
212
|
defaultValue: DEFAULT_TARGET_DIR,
|
|
177
213
|
placeholder: DEFAULT_TARGET_DIR,
|
|
178
|
-
})));
|
|
214
|
+
}))));
|
|
179
215
|
const token = generateToken();
|
|
180
216
|
// Conflict policy: identical existing files are skipped silently;
|
|
181
217
|
// differing ones prompt per file (default keep). --yes never overwrites —
|
|
@@ -201,7 +237,7 @@ const runLocalInit = async (flags, deps) => {
|
|
|
201
237
|
const started = flags.yes
|
|
202
238
|
? false
|
|
203
239
|
: await offerComposeUp({ targetDir, port }, deps);
|
|
204
|
-
prompts.
|
|
240
|
+
prompts.print(buildLocalConnectMessage({ targetDir, token, started, port, tokenWritten }));
|
|
205
241
|
return 0;
|
|
206
242
|
};
|
|
207
243
|
// Remote flow (VPS + Obsidian Sync): resolve target dir → PUBLIC_URL →
|
|
@@ -211,11 +247,13 @@ const runLocalInit = async (flags, deps) => {
|
|
|
211
247
|
// instructions. Always interactive — the sync-token step can't be defaulted.
|
|
212
248
|
const runRemoteInit = async (flags, deps) => {
|
|
213
249
|
const { prompts, docker } = deps;
|
|
214
|
-
|
|
250
|
+
// expandTilde before resolve: resolve() treats a leading `~` as a literal
|
|
251
|
+
// path segment, so a quoted "~/path" would create a directory named "~".
|
|
252
|
+
const targetDir = resolve(expandTilde(flags.dir ??
|
|
215
253
|
(await prompts.text("Where should I put the config files?", {
|
|
216
254
|
defaultValue: DEFAULT_TARGET_DIR,
|
|
217
255
|
placeholder: DEFAULT_TARGET_DIR,
|
|
218
|
-
})));
|
|
256
|
+
}))));
|
|
219
257
|
const publicUrl = await askPublicUrl(prompts);
|
|
220
258
|
const vaultName = await askVaultName(prompts);
|
|
221
259
|
// The Obsidian Sync token comes from an interactive docker run (the
|
|
@@ -263,14 +301,14 @@ const runRemoteInit = async (flags, deps) => {
|
|
|
263
301
|
const started = obsidianAuthToken === ""
|
|
264
302
|
? false
|
|
265
303
|
: await offerComposeUp({ targetDir, port }, deps);
|
|
266
|
-
prompts.
|
|
304
|
+
prompts.print(buildRemoteConnectMessage({
|
|
267
305
|
targetDir,
|
|
268
306
|
token,
|
|
269
307
|
publicUrl,
|
|
270
308
|
started,
|
|
271
309
|
obsidianTokenMissing: obsidianAuthToken === "",
|
|
272
310
|
tokenWritten,
|
|
273
|
-
})
|
|
311
|
+
}));
|
|
274
312
|
return 0;
|
|
275
313
|
};
|
|
276
314
|
export const runInit = async (flags, deps) => {
|
package/dist/messages.js
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
import { styleText } from "node:util";
|
|
2
|
+
// Strip styling when stdout isn't a color TTY (piped output, NO_COLOR, CI) so
|
|
3
|
+
// captured/redirected output stays plain — no stray escape codes in copied
|
|
4
|
+
// commands or logs.
|
|
5
|
+
const paint = (style, text) => process.stdout.isTTY && !process.env.NO_COLOR ? styleText(style, text) : text;
|
|
6
|
+
// Section header that replaces the old clack note box: a bold title over a
|
|
7
|
+
// dim rule. The rule is its own line (not a per-line prefix), so it never
|
|
8
|
+
// touches a copyable command.
|
|
9
|
+
const connectHeader = () => `${paint("bold", "Connect")}\n${paint("dim", "─".repeat(56))}`;
|
|
1
10
|
const composeUpCommand = (targetDir) => `cd ${targetDir} && docker compose up -d`;
|
|
2
11
|
const startServerLine = (targetDir) => `Start the server:\n ${composeUpCommand(targetDir)}`;
|
|
3
12
|
/** Remote start line: running, blocked on the missing sync token, or ready to start. */
|
|
@@ -11,58 +20,82 @@ const remoteStartLine = (params) => {
|
|
|
11
20
|
return startServerLine(targetDir);
|
|
12
21
|
};
|
|
13
22
|
/**
|
|
14
|
-
*
|
|
15
|
-
* generated token actually landed in .env — when an existing .env
|
|
16
|
-
* the connect message must point at the token on disk instead of one
|
|
17
|
-
* saved (pasting it would fail auth with no hint why).
|
|
18
|
-
*
|
|
23
|
+
* Auth-token block shared by both modes. tokenWritten distinguishes whether
|
|
24
|
+
* this run's generated token actually landed in .env — when an existing .env
|
|
25
|
+
* was kept, the connect message must point at the token on disk instead of one
|
|
26
|
+
* that was never saved (pasting it would fail auth with no hint why). When the
|
|
27
|
+
* token was written, it goes alone on its own line so selecting that line
|
|
28
|
+
* copies just the token — no "Auth token: " prefix to trim.
|
|
29
|
+
*/
|
|
30
|
+
const tokenBlock = (params) => {
|
|
31
|
+
const { targetDir, token, tokenWritten } = params;
|
|
32
|
+
return tokenWritten
|
|
33
|
+
? `${paint("dim", "Auth token:")}\n ${paint("cyan", token)}`
|
|
34
|
+
: `${paint("dim", "Auth token:")} use the existing MCP_AUTH_TOKEN in ${targetDir}/.env`;
|
|
35
|
+
};
|
|
36
|
+
// ── Shared connect-message blocks ───────────────────────────────────────────
|
|
37
|
+
// Both modes print the same skeleton; only the URL and the bits the topology
|
|
38
|
+
// forces apart (start line, the Claude-apps caveat, optional-settings list,
|
|
39
|
+
// docs link) differ. Sharing these blocks keeps the two messages in lockstep.
|
|
40
|
+
// `mcpUrl` is the full endpoint (`<base>/mcp`); `healthUrl` is `<base>/healthz`.
|
|
41
|
+
const connectUrlBlock = (mcpUrl, tokenLine) => `Connect your MCP client:
|
|
42
|
+
${paint("dim", "URL:")} ${paint("cyan", mcpUrl)}
|
|
43
|
+
${tokenLine}`;
|
|
44
|
+
// OAuth connect instruction + the Claude Code walkthrough — shared by every
|
|
45
|
+
// variant. You register the server by its URL however your MCP client allows:
|
|
46
|
+
// a CLI (`claude mcp add`, `opencode mcp add`), a connector dialog, or a
|
|
47
|
+
// config file — then approve the consent page. Claude Code is the worked
|
|
48
|
+
// example. The per-mode caveats (which clients need https, the localhost
|
|
49
|
+
// bridge) are appended by the builders.
|
|
50
|
+
const connectGuidance = (mcpUrl) => `Add the URL above as a remote MCP server (leave Client ID/Secret empty),
|
|
51
|
+
then approve the consent page with the token. For example, Claude Code:
|
|
52
|
+
1. claude mcp add --scope user --transport http vault-cortex ${mcpUrl}
|
|
53
|
+
2. approve the browser consent page with the token above
|
|
54
|
+
3. done — the client holds auto-refreshing access tokens; the token
|
|
55
|
+
never sits in client config`;
|
|
56
|
+
const curlGuidance = (mcpUrl) => `Clients without OAuth, scripts, and curl send the token directly:
|
|
57
|
+
curl -H "Authorization: Bearer <token>" ${mcpUrl}`;
|
|
58
|
+
const smokeTest = (healthUrl) => `Smoke test:
|
|
59
|
+
curl ${healthUrl}`;
|
|
60
|
+
/**
|
|
61
|
+
* Local-mode "Connect" message. port comes from the .env on disk: a kept file
|
|
62
|
+
* may override the default, so the message must describe the server that will
|
|
63
|
+
* actually run.
|
|
19
64
|
*/
|
|
20
65
|
export const buildLocalConnectMessage = (params) => {
|
|
21
66
|
const { targetDir, token, started, port, tokenWritten } = params;
|
|
67
|
+
const baseUrl = `http://localhost:${port}`;
|
|
22
68
|
const startLine = started
|
|
23
69
|
? "The server is running."
|
|
24
70
|
: startServerLine(targetDir);
|
|
25
|
-
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
Claude Desktop only accepts https URLs in its connector dialog, so
|
|
46
|
-
register the server in claude_desktop_config.json via the mcp-remote
|
|
47
|
-
bridge instead:
|
|
71
|
+
const tokenLine = tokenBlock({ targetDir, token, tokenWritten });
|
|
72
|
+
// Flush-left on purpose: this is printed as plain text (see paint), so
|
|
73
|
+
// leading whitespace would render as literal indentation. Local is always
|
|
74
|
+
// localhost http, so it shares the http guidance; its only divergences are
|
|
75
|
+
// that claude.ai can't reach localhost at all and Claude Desktop needs the
|
|
76
|
+
// mcp-remote bridge (the dialog rejects http, but mcp-remote exempts
|
|
77
|
+
// localhost, so no --allow-http).
|
|
78
|
+
const connectMessage = `${connectHeader()}
|
|
79
|
+
|
|
80
|
+
${startLine}
|
|
81
|
+
|
|
82
|
+
${connectUrlBlock(`${baseUrl}/mcp`, tokenLine)}
|
|
83
|
+
|
|
84
|
+
${connectGuidance(`${baseUrl}/mcp`)}
|
|
85
|
+
|
|
86
|
+
Other clients (opencode, Cursor, …) take the URL above too. claude.ai
|
|
87
|
+
(web) can't reach localhost — connect from a client on this machine.
|
|
88
|
+
Claude Desktop only accepts https URLs in its connector dialog, so bridge
|
|
89
|
+
it with mcp-remote:
|
|
48
90
|
"vault-cortex": {
|
|
49
91
|
"command": "npx",
|
|
50
|
-
"args": ["-y", "mcp-remote", "
|
|
92
|
+
"args": ["-y", "mcp-remote", "${baseUrl}/mcp",
|
|
51
93
|
"--header", "Authorization: Bearer <token above>"]
|
|
52
94
|
}
|
|
53
95
|
|
|
54
|
-
|
|
55
|
-
remote MCP server, leaving Client ID/Secret empty ("remote" = HTTP —
|
|
56
|
-
the server still runs on your machine), then approve the consent page.
|
|
57
|
-
|
|
58
|
-
Clients without OAuth, scripts, and curl send the token directly:
|
|
59
|
-
curl -H "Authorization: Bearer <token>" http://localhost:${port}/mcp
|
|
96
|
+
${curlGuidance(`${baseUrl}/mcp`)}
|
|
60
97
|
|
|
61
|
-
|
|
62
|
-
access, or Claude Desktop with the mcp-remote bridge.
|
|
63
|
-
|
|
64
|
-
Smoke test:
|
|
65
|
-
curl http://localhost:${port}/healthz
|
|
98
|
+
${smokeTest(`${baseUrl}/healthz`)}
|
|
66
99
|
|
|
67
100
|
Optional settings (timezone, memory folder, port, logging) are commented
|
|
68
101
|
out in ${targetDir}/.env — uncomment, set a value, then apply with
|
|
@@ -72,8 +105,9 @@ Full docs: https://github.com/aliasunder/vault-cortex/blob/main/deploy/local/REA
|
|
|
72
105
|
return connectMessage;
|
|
73
106
|
};
|
|
74
107
|
/**
|
|
75
|
-
* Remote-mode "Connect" message. See buildLocalConnectMessage for the
|
|
76
|
-
* rationale; remote URLs come from PUBLIC_URL, so no port
|
|
108
|
+
* Remote-mode "Connect" message. See buildLocalConnectMessage for the
|
|
109
|
+
* tokenWritten rationale; remote URLs come from PUBLIC_URL, so no port
|
|
110
|
+
* handling here.
|
|
77
111
|
*/
|
|
78
112
|
export const buildRemoteConnectMessage = (params) => {
|
|
79
113
|
const { targetDir, token, publicUrl, started, obsidianTokenMissing, tokenWritten, } = params;
|
|
@@ -82,27 +116,36 @@ export const buildRemoteConnectMessage = (params) => {
|
|
|
82
116
|
started,
|
|
83
117
|
obsidianTokenMissing,
|
|
84
118
|
});
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
119
|
+
const tokenLine = tokenBlock({ targetDir, token, tokenWritten });
|
|
120
|
+
// Both branches share the connect walkthrough; only the caveat differs. Over
|
|
121
|
+
// https every client converges — nothing to set up. Over http the Claude
|
|
122
|
+
// apps' connector dialog needs TLS while other clients are fine. We already
|
|
123
|
+
// know which case it is, so the http branch states it rather than asking.
|
|
124
|
+
// Case-insensitive: askPublicUrl stores the scheme as typed, so an HTTPS://
|
|
125
|
+
// input is valid and must still route to the https branch.
|
|
126
|
+
const clientGuidance = publicUrl.toLowerCase().startsWith("https://")
|
|
127
|
+
? `${connectGuidance(`${publicUrl}/mcp`)}
|
|
128
|
+
|
|
129
|
+
Reachable over https from any MCP client — Claude Desktop, claude.ai (web
|
|
130
|
+
and mobile), opencode, Cursor — from any device.`
|
|
131
|
+
: `${connectGuidance(`${publicUrl}/mcp`)}
|
|
132
|
+
|
|
133
|
+
Other clients (opencode, Cursor, …) work over http too. claude.ai and
|
|
134
|
+
Claude Desktop only accept https URLs — set up HTTPS for those clients
|
|
135
|
+
(see the HTTPS section in the remote guide).`;
|
|
136
|
+
// Flush-left on purpose: this is printed as plain text (see paint), so
|
|
137
|
+
// leading whitespace would render as literal indentation.
|
|
138
|
+
const connectMessage = `${connectHeader()}
|
|
139
|
+
|
|
140
|
+
${startLine}
|
|
141
|
+
|
|
142
|
+
${connectUrlBlock(`${publicUrl}/mcp`, tokenLine)}
|
|
143
|
+
|
|
144
|
+
${clientGuidance}
|
|
145
|
+
|
|
146
|
+
${curlGuidance(`${publicUrl}/mcp`)}
|
|
147
|
+
|
|
148
|
+
${smokeTest(`${publicUrl}/healthz`)}
|
|
106
149
|
|
|
107
150
|
Optional settings (timezone, memory folder, port, logging, sync
|
|
108
151
|
behavior) are commented out in ${targetDir}/.env — uncomment, set a
|
package/dist/prompts.js
CHANGED
|
@@ -19,6 +19,9 @@ export const createPrompts = () => ({
|
|
|
19
19
|
intro: (message) => clack.intro(message),
|
|
20
20
|
outro: (message) => clack.outro(message),
|
|
21
21
|
note: (message, title) => clack.note(message, title),
|
|
22
|
+
// Leading + trailing newline sets the block off from the surrounding clack
|
|
23
|
+
// output without a box around it.
|
|
24
|
+
print: (message) => process.stdout.write(`\n${message}\n`),
|
|
22
25
|
log: (message) => clack.log.info(message),
|
|
23
26
|
warn: (message) => clack.log.warn(message),
|
|
24
27
|
error: (message) => clack.log.error(message),
|