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 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/belphemur/obsidian-headless-sync-docker:latest";
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 | ours | theirs (default: merge).
60
- # CONFLICT_STRATEGY=merge
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/belphemur/obsidian-headless-sync-docker:latest`
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
- /** Re-prompts until the answer is a plausible http(s) URL. */
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
- if (!trimmed.startsWith("http://") && !trimmed.startsWith("https://")) {
78
- prompts.error("PUBLIC_URL must start with http:// or https://");
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.note(buildLocalConnectMessage({ targetDir, token, started, port, tokenWritten }), "Connect");
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.note(buildRemoteConnectMessage({
304
+ prompts.print(buildRemoteConnectMessage({
271
305
  targetDir,
272
306
  token,
273
307
  publicUrl,
274
308
  started,
275
309
  obsidianTokenMissing: obsidianAuthToken === "",
276
310
  tokenWritten,
277
- }), "Connect");
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
- * Local-mode "Connect" message. tokenWritten distinguishes whether this run's
15
- * generated token actually landed in .env — when an existing .env was kept,
16
- * the connect message must point at the token on disk instead of one that was never
17
- * saved (pasting it would fail auth with no hint why). port comes from the
18
- * .env on disk for the same reason: a kept file may override the default.
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
- // When the token was written, put it alone on its own line so selecting
26
- // that line copies just the token no "Auth token: " prefix to trim and
27
- // no chance of grabbing the surrounding label.
28
- const tokenLine = tokenWritten
29
- ? `Auth token:\n ${token}`
30
- : `Auth token: use the existing MCP_AUTH_TOKEN in ${targetDir}/.env`;
31
- // Flush-left on purpose: template literals keep leading whitespace, so
32
- // indenting these lines would indent the rendered output.
33
- const connectMessage = `${startLine}
34
-
35
- Connect your MCP client:
36
- URL: http://localhost:${port}/mcp
37
- ${tokenLine}
38
-
39
- Claude Code:
40
- 1. claude mcp add --scope user --transport http vault-cortex http://localhost:${port}/mcp
41
- (--scope user registers it for every project; drop it to scope
42
- the server to the current directory only)
43
- 2. Approve the browser consent page with the token above
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", "http://localhost:${port}/mcp",
92
+ "args": ["-y", "mcp-remote", "${baseUrl}/mcp",
53
93
  "--header", "Authorization: Bearer <token above>"]
54
94
  }
55
95
 
56
- Other OAuth clients (Cursor, most MCP clients) add the URL above as a
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
- Note: claude.ai (web) cannot reach localhost — use Claude Code for local
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 tokenWritten
78
- * rationale; remote URLs come from PUBLIC_URL, so no port handling here.
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 approveLine = tokenWritten
88
- ? `approve with your MCP_AUTH_TOKEN:\n ${token}`
89
- : `approve with the existing MCP_AUTH_TOKEN in ${targetDir}/.env`;
90
- const httpUrlWarning = publicUrl.startsWith("https://")
91
- ? ""
92
- : `
93
-
94
- Note: claude.ai and Claude Desktop only accept https URLs — set up
95
- HTTPS when you're ready for those clients (see the HTTPS section in
96
- the remote guide). Claude Code works with http:
97
- claude mcp add --scope user --transport http vault-cortex ${publicUrl}/mcp`;
98
- // Flush-left on purpose: template literals keep leading whitespace, so
99
- // indenting these lines would indent the rendered output.
100
- const connectMessage = `${startLine}
101
-
102
- Connect your MCP client:
103
- URL: ${publicUrl}/mcp
104
-
105
- OAuth clients (Claude Desktop, Claude Code, claude.ai): add a remote MCP
106
- server with that URL and leave Client ID/Secret empty — a consent page
107
- opens; ${approveLine}${httpUrlWarning}
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,6 +1,6 @@
1
1
  {
2
2
  "name": "vault-cortex",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
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",
@@ -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
- # Three services: init-config-perms → obsidian-sync → vault-mcp.
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/belphemur/obsidian-headless-sync-docker:latest
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:-merge}
26
+ CONFLICT_STRATEGY: ${CONFLICT_STRATEGY:-conflict}
42
27
  SYNC_MODE: ${SYNC_MODE:-bidirectional}
43
28
  TZ: ${TZ:-UTC}
44
29
  volumes: