vault-cortex 0.2.5 → 0.2.7
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/docker.js +1 -1
- package/dist/env.js +3 -3
- package/dist/init.js +41 -7
- package/dist/messages.js +106 -65
- package/dist/prompts.js +3 -0
- package/package.json +1 -1
- package/templates/remote/docker-compose.yml +3 -18
package/dist/docker.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
/** The image whose `get-token` entrypoint issues Obsidian Sync auth tokens. */
|
|
3
|
-
export const OBSIDIAN_SYNC_IMAGE = "ghcr.io/
|
|
3
|
+
export const OBSIDIAN_SYNC_IMAGE = "ghcr.io/aliasunder/obsidian-headless-sync-docker:latest";
|
|
4
4
|
export const createDockerRunner = () => ({
|
|
5
5
|
isComposeAvailable: () => spawnSync("docker", ["compose", "version"]).status === 0,
|
|
6
6
|
isDaemonRunning: () => spawnSync("docker", ["info"], { timeout: 5_000 }).status === 0,
|
package/dist/env.js
CHANGED
|
@@ -56,8 +56,8 @@ const REMOTE_OPTIONAL_BLOCK = `# Optional ────────────
|
|
|
56
56
|
# Device name shown in Obsidian Sync settings.
|
|
57
57
|
# DEVICE_NAME=vault-cortex
|
|
58
58
|
|
|
59
|
-
# Obsidian Sync conflict resolution: merge |
|
|
60
|
-
# CONFLICT_STRATEGY=
|
|
59
|
+
# Obsidian Sync conflict resolution: merge | conflict (default: conflict).
|
|
60
|
+
# CONFLICT_STRATEGY=conflict
|
|
61
61
|
|
|
62
62
|
# Sync direction: bidirectional | pull-only | push-only (default: bidirectional).
|
|
63
63
|
# SYNC_MODE=bidirectional
|
|
@@ -85,7 +85,7 @@ VAULT_PASSWORD=${answers.vaultPassword}`;
|
|
|
85
85
|
? `# Obsidian Sync auth token — FILL THIS IN before docker compose up.
|
|
86
86
|
# Generate once with:
|
|
87
87
|
# docker run --rm -it --entrypoint get-token \\
|
|
88
|
-
# ghcr.io/
|
|
88
|
+
# ghcr.io/aliasunder/obsidian-headless-sync-docker:latest`
|
|
89
89
|
: `# Obsidian Sync auth token.`;
|
|
90
90
|
return `# vault-cortex — remote quickstart (Obsidian Sync)
|
|
91
91
|
# Generated by \`npx vault-cortex init\`. Full option reference:
|
package/dist/init.js
CHANGED
|
@@ -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. */
|
|
@@ -203,7 +237,7 @@ const runLocalInit = async (flags, deps) => {
|
|
|
203
237
|
const started = flags.yes
|
|
204
238
|
? false
|
|
205
239
|
: await offerComposeUp({ targetDir, port }, deps);
|
|
206
|
-
prompts.
|
|
240
|
+
prompts.print(buildLocalConnectMessage({ targetDir, token, started, port, tokenWritten }));
|
|
207
241
|
return 0;
|
|
208
242
|
};
|
|
209
243
|
// Remote flow (VPS + Obsidian Sync): resolve target dir → PUBLIC_URL →
|
|
@@ -267,14 +301,14 @@ const runRemoteInit = async (flags, deps) => {
|
|
|
267
301
|
const started = obsidianAuthToken === ""
|
|
268
302
|
? false
|
|
269
303
|
: await offerComposeUp({ targetDir, port }, deps);
|
|
270
|
-
prompts.
|
|
304
|
+
prompts.print(buildRemoteConnectMessage({
|
|
271
305
|
targetDir,
|
|
272
306
|
token,
|
|
273
307
|
publicUrl,
|
|
274
308
|
started,
|
|
275
309
|
obsidianTokenMissing: obsidianAuthToken === "",
|
|
276
310
|
tokenWritten,
|
|
277
|
-
})
|
|
311
|
+
}));
|
|
278
312
|
return 0;
|
|
279
313
|
};
|
|
280
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,60 +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
|
-
3. Done. The client holds auto-refreshing access tokens; the
|
|
45
|
-
token never sits in client config
|
|
46
|
-
|
|
47
|
-
Claude Desktop only accepts https URLs in its connector dialog, so
|
|
48
|
-
register the server in claude_desktop_config.json via the mcp-remote
|
|
49
|
-
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:
|
|
50
90
|
"vault-cortex": {
|
|
51
91
|
"command": "npx",
|
|
52
|
-
"args": ["-y", "mcp-remote", "
|
|
92
|
+
"args": ["-y", "mcp-remote", "${baseUrl}/mcp",
|
|
53
93
|
"--header", "Authorization: Bearer <token above>"]
|
|
54
94
|
}
|
|
55
95
|
|
|
56
|
-
|
|
57
|
-
remote MCP server, leaving Client ID/Secret empty ("remote" = HTTP —
|
|
58
|
-
the server still runs on your machine), then approve the consent page.
|
|
59
|
-
|
|
60
|
-
Clients without OAuth, scripts, and curl send the token directly:
|
|
61
|
-
curl -H "Authorization: Bearer <token>" http://localhost:${port}/mcp
|
|
96
|
+
${curlGuidance(`${baseUrl}/mcp`)}
|
|
62
97
|
|
|
63
|
-
|
|
64
|
-
access, or Claude Desktop with the mcp-remote bridge.
|
|
65
|
-
|
|
66
|
-
Smoke test:
|
|
67
|
-
curl http://localhost:${port}/healthz
|
|
98
|
+
${smokeTest(`${baseUrl}/healthz`)}
|
|
68
99
|
|
|
69
100
|
Optional settings (timezone, memory folder, port, logging) are commented
|
|
70
101
|
out in ${targetDir}/.env — uncomment, set a value, then apply with
|
|
@@ -74,8 +105,9 @@ Full docs: https://github.com/aliasunder/vault-cortex/blob/main/deploy/local/REA
|
|
|
74
105
|
return connectMessage;
|
|
75
106
|
};
|
|
76
107
|
/**
|
|
77
|
-
* Remote-mode "Connect" message. See buildLocalConnectMessage for the
|
|
78
|
-
* 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.
|
|
79
111
|
*/
|
|
80
112
|
export const buildRemoteConnectMessage = (params) => {
|
|
81
113
|
const { targetDir, token, publicUrl, started, obsidianTokenMissing, tokenWritten, } = params;
|
|
@@ -84,27 +116,36 @@ export const buildRemoteConnectMessage = (params) => {
|
|
|
84
116
|
started,
|
|
85
117
|
obsidianTokenMissing,
|
|
86
118
|
});
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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`)}
|
|
108
149
|
|
|
109
150
|
Optional settings (timezone, memory folder, port, logging, sync
|
|
110
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),
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# vault-cortex — remote quickstart (Obsidian Sync)
|
|
2
2
|
#
|
|
3
3
|
# Run vault-cortex on a VPS with Obsidian Sync for remote access from any device.
|
|
4
|
-
#
|
|
4
|
+
# Two services: obsidian-sync → vault-mcp.
|
|
5
5
|
#
|
|
6
6
|
# 1. cp .env.example .env (then fill in required values)
|
|
7
7
|
# 2. docker compose up -d
|
|
@@ -12,25 +12,10 @@
|
|
|
12
12
|
name: vault-cortex
|
|
13
13
|
|
|
14
14
|
services:
|
|
15
|
-
# Workaround: the upstream Dockerfile creates /home/obsidian/.config as
|
|
16
|
-
# root. Docker named volumes inherit this root ownership, so the obsidian
|
|
17
|
-
# user (UID 1000) can't write sync state. This init container chowns the
|
|
18
|
-
# volume before obsidian-sync starts.
|
|
19
|
-
# Remove when upstream fixes: github.com/Belphemur/obsidian-headless-sync-docker
|
|
20
|
-
init-config-perms:
|
|
21
|
-
image: alpine:latest
|
|
22
|
-
command: sh -c "chown -R ${PUID:-1000}:${PGID:-1000} /config"
|
|
23
|
-
volumes:
|
|
24
|
-
- obsidian_config:/config
|
|
25
|
-
restart: "no"
|
|
26
|
-
|
|
27
15
|
obsidian-sync:
|
|
28
|
-
image: ghcr.io/
|
|
16
|
+
image: ghcr.io/aliasunder/obsidian-headless-sync-docker:latest
|
|
29
17
|
container_name: obsidian-sync
|
|
30
18
|
restart: unless-stopped
|
|
31
|
-
depends_on:
|
|
32
|
-
init-config-perms:
|
|
33
|
-
condition: service_completed_successfully
|
|
34
19
|
environment:
|
|
35
20
|
OBSIDIAN_AUTH_TOKEN: "${OBSIDIAN_AUTH_TOKEN:?Set OBSIDIAN_AUTH_TOKEN — see .env.example}"
|
|
36
21
|
VAULT_NAME: "${VAULT_NAME:?Set VAULT_NAME to your Obsidian vault name (case-sensitive)}"
|
|
@@ -38,7 +23,7 @@ services:
|
|
|
38
23
|
PUID: ${PUID:-1000}
|
|
39
24
|
PGID: ${PGID:-1000}
|
|
40
25
|
DEVICE_NAME: ${DEVICE_NAME:-vault-cortex}
|
|
41
|
-
CONFLICT_STRATEGY: ${CONFLICT_STRATEGY:-
|
|
26
|
+
CONFLICT_STRATEGY: ${CONFLICT_STRATEGY:-conflict}
|
|
42
27
|
SYNC_MODE: ${SYNC_MODE:-bidirectional}
|
|
43
28
|
TZ: ${TZ:-UTC}
|
|
44
29
|
volumes:
|