vault-cortex 0.4.6 → 0.5.0-beta.28
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 +43 -4
- package/dist/docker.js +104 -9
- package/dist/env.js +41 -37
- package/dist/get-sync-token.js +150 -0
- package/dist/init.js +58 -74
- package/dist/main.js +11 -0
- package/dist/messages.js +9 -10
- package/dist/program.js +15 -2
- package/dist/scaffold.js +59 -11
- package/dist/upgrade.js +66 -0
- package/package.json +1 -2
- package/templates/local/docker-compose.yml +0 -66
- package/templates/remote/docker-compose.yml +0 -85
package/README.md
CHANGED
|
@@ -21,14 +21,53 @@ container; this CLI scaffolds the config so you don't have to.
|
|
|
21
21
|
1. Asks how you want to run it:
|
|
22
22
|
- **Local** — Docker on this machine, your vault folder bind-mounted
|
|
23
23
|
- **Remote** — a VPS with [Obsidian Sync](https://obsidian.md/sync), reachable from any device
|
|
24
|
-
2. Generates a `
|
|
25
|
-
|
|
26
|
-
3. Optionally runs `docker compose up -d` and waits for the health check
|
|
24
|
+
2. Generates a `.env` file with a securely generated `MCP_AUTH_TOKEN`
|
|
25
|
+
3. Optionally starts the container and waits for the health check
|
|
27
26
|
4. Prints your connection details — the MCP URL, your auth token, and how to
|
|
28
27
|
connect your client
|
|
29
28
|
|
|
30
29
|
Existing files are never overwritten without asking.
|
|
31
30
|
|
|
31
|
+
## Get Sync Token
|
|
32
|
+
|
|
33
|
+
Generate an Obsidian Sync auth token without leaving the CLI:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx vault-cortex get-sync-token
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The command opens the Obsidian login inside Docker. Once you've signed
|
|
40
|
+
in, it captures your token and prints it — nothing to dig out of the
|
|
41
|
+
login output. Use `--dir` to write the token straight into an existing
|
|
42
|
+
`.env` instead:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npx vault-cortex get-sync-token --dir ./vault-cortex
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
During `init --mode remote`, this flow is offered automatically when Docker
|
|
49
|
+
is available.
|
|
50
|
+
|
|
51
|
+
## Upgrade
|
|
52
|
+
|
|
53
|
+
Pull the latest image, re-create the container, and verify health:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npx vault-cortex upgrade
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Run it from the same directory where you ran `init` — it looks for your
|
|
60
|
+
config in `./vault-cortex/.env` (pass `--dir <path>` if you scaffolded
|
|
61
|
+
somewhere else).
|
|
62
|
+
|
|
63
|
+
Your vault data, search index, and `.env` settings are preserved across
|
|
64
|
+
upgrades — only the server image is replaced. Also applies `.env` changes
|
|
65
|
+
(`docker restart` does not re-read env files).
|
|
66
|
+
|
|
67
|
+
Prefer Docker Compose? The CLI uses `docker run` for simplicity, but the
|
|
68
|
+
[deploy guides](https://github.com/aliasunder/vault-cortex/blob/main/deploy/)
|
|
69
|
+
include Compose files you can use directly.
|
|
70
|
+
|
|
32
71
|
## Non-interactive
|
|
33
72
|
|
|
34
73
|
```bash
|
|
@@ -41,7 +80,7 @@ Flags: `--mode local|remote`, `--vault-path <path>`, `--dir <path>` (default
|
|
|
41
80
|
## Requirements
|
|
42
81
|
|
|
43
82
|
- Node.js >= 20.12 (only for this CLI — the server itself runs in Docker)
|
|
44
|
-
- [Docker](https://docs.docker.com/get-docker/) to run the server
|
|
83
|
+
- [Docker](https://docs.docker.com/get-docker/) (or any OCI-compatible runtime) to run the server
|
|
45
84
|
|
|
46
85
|
## Docs
|
|
47
86
|
|
package/dist/docker.js
CHANGED
|
@@ -1,19 +1,114 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
|
|
3
|
-
export const
|
|
2
|
+
export const LOCAL_IMAGE = "ghcr.io/aliasunder/vault-cortex:latest";
|
|
3
|
+
export const REMOTE_IMAGE = "ghcr.io/aliasunder/vault-cortex:remote";
|
|
4
|
+
export const CONTAINER_NAME = "vault-cortex";
|
|
5
|
+
/**
|
|
6
|
+
* Builds the `docker run` args for the Obsidian login with a volume mount
|
|
7
|
+
* that captures the auth token file. Runs `ob login` directly instead of
|
|
8
|
+
* the image's get-sync-token script: the script's additions are locating and
|
|
9
|
+
* printing the token, and the mount makes both unnecessary — the CLI reads
|
|
10
|
+
* the token file itself, and not echoing a credential keeps it out of
|
|
11
|
+
* terminal scrollback. Pure function for testability.
|
|
12
|
+
*
|
|
13
|
+
* On Linux, includes `--user uid:gid` when uid/gid are provided — Node
|
|
14
|
+
* exposes process.getuid/getgid on every POSIX platform, so in practice the
|
|
15
|
+
* flag is always set there — keeping the token file host-user-owned. macOS
|
|
16
|
+
* Docker Desktop translates UIDs automatically, so no flag is needed.
|
|
17
|
+
*/
|
|
18
|
+
export const buildObsidianLoginArgs = (params) => {
|
|
19
|
+
const { configMountPath, platform = process.platform, uid, gid } = params;
|
|
20
|
+
const args = [
|
|
21
|
+
"run",
|
|
22
|
+
"--rm",
|
|
23
|
+
"-it",
|
|
24
|
+
"--entrypoint",
|
|
25
|
+
"ob",
|
|
26
|
+
"-v",
|
|
27
|
+
`${configMountPath}:/home/obsidian/.config`,
|
|
28
|
+
];
|
|
29
|
+
if (platform === "linux" && uid !== undefined && gid !== undefined) {
|
|
30
|
+
args.push("--user", `${uid}:${gid}`);
|
|
31
|
+
}
|
|
32
|
+
args.push(REMOTE_IMAGE, "login");
|
|
33
|
+
return args;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Container-internal env vars that must override the user's .env values.
|
|
37
|
+
* VAULT_PATH in .env is the host path (for the -v mount); the container
|
|
38
|
+
* must see /vault. PORT/HOST/INDEX_DB_PATH are hardcoded infrastructure.
|
|
39
|
+
*/
|
|
40
|
+
const CONTAINER_ENV_OVERRIDES = [
|
|
41
|
+
"VAULT_PATH=/vault",
|
|
42
|
+
"PORT=8000",
|
|
43
|
+
"HOST=0.0.0.0",
|
|
44
|
+
"INDEX_DB_PATH=/data/index.db",
|
|
45
|
+
];
|
|
46
|
+
/** Node one-liner matching the compose healthcheck — exits 0 on HTTP 200. */
|
|
47
|
+
const HEALTH_CMD = "node -e \"fetch('http://127.0.0.1:8000/healthz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\"";
|
|
48
|
+
/**
|
|
49
|
+
* Builds the `docker run` args array. Pure function — no I/O — so it's
|
|
50
|
+
* testable without spawning processes.
|
|
51
|
+
*/
|
|
52
|
+
export const buildDockerRunArgs = (params) => {
|
|
53
|
+
const { mode, envFilePath, port, vaultPath } = params;
|
|
54
|
+
const image = mode === "local" ? LOCAL_IMAGE : REMOTE_IMAGE;
|
|
55
|
+
const args = [
|
|
56
|
+
"run",
|
|
57
|
+
"-d",
|
|
58
|
+
"--name",
|
|
59
|
+
CONTAINER_NAME,
|
|
60
|
+
"--restart",
|
|
61
|
+
"unless-stopped",
|
|
62
|
+
"--env-file",
|
|
63
|
+
envFilePath,
|
|
64
|
+
...CONTAINER_ENV_OVERRIDES.flatMap((override) => ["-e", override]),
|
|
65
|
+
"-p",
|
|
66
|
+
`${port}:8000`,
|
|
67
|
+
];
|
|
68
|
+
if (mode === "local") {
|
|
69
|
+
if (!vaultPath) {
|
|
70
|
+
throw new Error("vaultPath is required for local mode");
|
|
71
|
+
}
|
|
72
|
+
args.push("-v", `${vaultPath}:/vault:rw`);
|
|
73
|
+
args.push("-v", "vault-cortex_mcp_data:/data");
|
|
74
|
+
args.push("--health-cmd", HEALTH_CMD);
|
|
75
|
+
args.push("--health-interval", "15s");
|
|
76
|
+
args.push("--health-timeout", "5s");
|
|
77
|
+
args.push("--health-retries", "3");
|
|
78
|
+
args.push("--health-start-period", "20s");
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
args.push("--hostname", CONTAINER_NAME);
|
|
82
|
+
args.push("-v", "vault-cortex_vault_data:/vault");
|
|
83
|
+
args.push("-v", "vault-cortex_mcp_data:/data");
|
|
84
|
+
args.push("-v", "vault-cortex_obsidian_config:/home/obsidian/.config");
|
|
85
|
+
args.push("--health-cmd", HEALTH_CMD);
|
|
86
|
+
args.push("--health-interval", "15s");
|
|
87
|
+
args.push("--health-timeout", "5s");
|
|
88
|
+
args.push("--health-retries", "5");
|
|
89
|
+
args.push("--health-start-period", "60s");
|
|
90
|
+
args.push("--log-driver", "json-file");
|
|
91
|
+
args.push("--log-opt", "max-size=10m");
|
|
92
|
+
args.push("--log-opt", "max-file=3");
|
|
93
|
+
}
|
|
94
|
+
args.push(image);
|
|
95
|
+
return args;
|
|
96
|
+
};
|
|
4
97
|
export const createDockerRunner = () => ({
|
|
5
|
-
isComposeAvailable: () => spawnSync("docker", ["compose", "version"]).status === 0,
|
|
6
98
|
isDaemonRunning: () => spawnSync("docker", ["info"], { timeout: 5_000 }).status === 0,
|
|
7
|
-
|
|
99
|
+
dockerRun: (params) => spawnSync("docker", buildDockerRunArgs(params), { stdio: "inherit" })
|
|
8
100
|
.status === 0,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
101
|
+
pullImage: (image) => spawnSync("docker", ["pull", image], { stdio: "inherit" }).status === 0,
|
|
102
|
+
stopAndRemoveContainer: () => spawnSync("docker", ["rm", "-f", CONTAINER_NAME]).status === 0,
|
|
103
|
+
runObsidianLogin: (configMountPath) => spawnSync("docker", buildObsidianLoginArgs({
|
|
104
|
+
configMountPath,
|
|
105
|
+
uid: process.getuid?.(),
|
|
106
|
+
gid: process.getgid?.(),
|
|
107
|
+
}), { stdio: "inherit" }).status === 0,
|
|
12
108
|
});
|
|
13
109
|
/**
|
|
14
110
|
* Polls the health endpoint until it responds OK or the timeout elapses.
|
|
15
|
-
* The first `docker
|
|
16
|
-
* is generous.
|
|
111
|
+
* The first `docker run` pulls the image, so the default window is generous.
|
|
17
112
|
*
|
|
18
113
|
* Native Date.now() rather than the server's Luxon convention: the published
|
|
19
114
|
* CLI deliberately keeps its dependency set to two packages, and this is an
|
package/dist/env.js
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
import { GET_TOKEN_IMAGE } from "./docker.js";
|
|
2
1
|
// Optional env blocks are synced from deploy/<mode>/.env.example by
|
|
3
|
-
// npm run sync:cli-
|
|
4
|
-
// cli/src/templates.test.ts asserts
|
|
5
|
-
//
|
|
6
|
-
//
|
|
2
|
+
// npm run sync:cli-env-blocks. Edit the deploy/ files, then re-run the script.
|
|
3
|
+
// cli/src/templates.test.ts asserts the CLI optional block vars match the
|
|
4
|
+
// deploy/ .env.example optional vars, so a new var breaks CI until both
|
|
5
|
+
// surfaces carry it.
|
|
7
6
|
// ┌─────────────────────────────────────────────────────────────────────────┐
|
|
8
7
|
// │ GENERATED — do not edit between sync markers. │
|
|
9
|
-
// │ Source: deploy/local/.env.example → npm run sync:cli-
|
|
8
|
+
// │ Source: deploy/local/.env.example → npm run sync:cli-env-blocks │
|
|
10
9
|
// │ The script replaces everything between :begin and :end on each run. │
|
|
11
10
|
// └─────────────────────────────────────────────────────────────────────────┘
|
|
12
11
|
// sync:local-optional:begin
|
|
13
12
|
const LOCAL_OPTIONAL_BLOCK = `# Optional ──────────────────────────────────────────────────
|
|
14
|
-
# To
|
|
15
|
-
#
|
|
13
|
+
# To change a setting: edit its value (uncommenting it first if needed),
|
|
14
|
+
# then apply with "npx vault-cortex upgrade" (restart alone does not
|
|
15
|
+
# re-read this file).
|
|
16
16
|
|
|
17
17
|
# Public URL for OAuth issuer URL in discovery metadata (default: http://localhost:8000).
|
|
18
18
|
# Override if you expose the server on a different URL (e.g. via a reverse proxy).
|
|
19
|
-
|
|
19
|
+
PUBLIC_URL=http://localhost:8000
|
|
20
20
|
|
|
21
21
|
# Your IANA timezone — affects daily note resolution and memory timestamps.
|
|
22
22
|
# TZ=America/New_York
|
|
@@ -27,20 +27,20 @@ const LOCAL_OPTIONAL_BLOCK = `# Optional ─────────────
|
|
|
27
27
|
# similarity fused via RRF. First startup is slow (~5min for 700 notes);
|
|
28
28
|
# subsequent starts are fast via content-hash caching.
|
|
29
29
|
# Set to false to disable model download and use FTS5 search only.
|
|
30
|
-
|
|
30
|
+
EMBEDDING_ENABLED=true
|
|
31
31
|
|
|
32
32
|
# Reranking mode for hybrid search results (default: blended).
|
|
33
33
|
# "blended" uses a cross-encoder to refine result ordering with
|
|
34
34
|
# position-aware score blending (~200ms added latency).
|
|
35
35
|
# "none" skips reranking for lower latency.
|
|
36
36
|
# Only takes effect when EMBEDDING_ENABLED=true.
|
|
37
|
-
|
|
37
|
+
RERANK_MODE=blended
|
|
38
38
|
|
|
39
39
|
# Enable or disable the memory layer (default: true).
|
|
40
40
|
# Set to false to hide memory tools and skip About Me/ creation.
|
|
41
|
-
|
|
41
|
+
MEMORY_ENABLED=true
|
|
42
42
|
# Memory folder name in your vault (default: About Me).
|
|
43
|
-
|
|
43
|
+
MEMORY_DIR=About Me
|
|
44
44
|
|
|
45
45
|
# Comma-separated folders protected from deletion (default: MEMORY_DIR, Daily Notes).
|
|
46
46
|
# If your daily notes folder has a custom name (e.g. "Journal"), override to include it.
|
|
@@ -55,10 +55,10 @@ const LOCAL_OPTIONAL_BLOCK = `# Optional ─────────────
|
|
|
55
55
|
# SERVICE_DOCUMENTATION_URL=https://github.com/youruser/your-fork
|
|
56
56
|
|
|
57
57
|
# Host port to expose (default: 8000).
|
|
58
|
-
|
|
58
|
+
PORT=8000
|
|
59
59
|
|
|
60
60
|
# Log verbosity: debug | info | warn | error (default: info).
|
|
61
|
-
|
|
61
|
+
LOG_LEVEL=info
|
|
62
62
|
|
|
63
63
|
# Directory for persistent log files inside the container.
|
|
64
64
|
# Unset by default — logs go to stdout only. Set a path to also write
|
|
@@ -66,24 +66,25 @@ const LOCAL_OPTIONAL_BLOCK = `# Optional ─────────────
|
|
|
66
66
|
# LOG_DIR=/data/logs
|
|
67
67
|
|
|
68
68
|
# Days to retain persistent log files before cleanup (default: 30).
|
|
69
|
-
|
|
69
|
+
LOG_RETENTION_DAYS=30
|
|
70
70
|
|
|
71
71
|
# Windows users: set this to true. Makes a vault stored on a C: drive work
|
|
72
72
|
# through Docker Desktop (switches the file watcher to polling and note moves
|
|
73
73
|
# to rename-based writes). Only strictly needed when your vault is on a C:
|
|
74
74
|
# drive rather than inside WSL2, but harmless to enable for any Windows setup.
|
|
75
|
-
|
|
75
|
+
WINDOWS_MODE=false
|
|
76
76
|
`;
|
|
77
77
|
// sync:local-optional:end
|
|
78
78
|
// ┌─────────────────────────────────────────────────────────────────────────┐
|
|
79
79
|
// │ GENERATED — do not edit between sync markers. │
|
|
80
|
-
// │ Source: deploy/remote/.env.example → npm run sync:cli-
|
|
80
|
+
// │ Source: deploy/remote/.env.example → npm run sync:cli-env-blocks │
|
|
81
81
|
// │ VAULT_PASSWORD is excluded (handled conditionally in buildRemoteEnv). │
|
|
82
82
|
// └─────────────────────────────────────────────────────────────────────────┘
|
|
83
83
|
// sync:remote-optional:begin
|
|
84
84
|
const REMOTE_OPTIONAL_BLOCK = `# Optional ──────────────────────────────────────────────────
|
|
85
|
-
# To
|
|
86
|
-
#
|
|
85
|
+
# To change a setting: edit its value (uncommenting it first if needed),
|
|
86
|
+
# then apply with "npx vault-cortex upgrade" (restart alone does not
|
|
87
|
+
# re-read this file).
|
|
87
88
|
|
|
88
89
|
# Your IANA timezone — affects daily note resolution and memory timestamps.
|
|
89
90
|
# TZ=America/New_York
|
|
@@ -94,26 +95,26 @@ const REMOTE_OPTIONAL_BLOCK = `# Optional ────────────
|
|
|
94
95
|
# similarity fused via RRF. First startup is slow (~5min for 700 notes);
|
|
95
96
|
# subsequent starts are fast via content-hash caching.
|
|
96
97
|
# Set to false to disable model download and use FTS5 search only.
|
|
97
|
-
|
|
98
|
+
EMBEDDING_ENABLED=true
|
|
98
99
|
|
|
99
100
|
# Reranking mode for hybrid search results (default: blended).
|
|
100
101
|
# "blended" uses a cross-encoder to refine result ordering with
|
|
101
102
|
# position-aware score blending (~200ms added latency).
|
|
102
103
|
# "none" skips reranking for lower latency.
|
|
103
104
|
# Only takes effect when EMBEDDING_ENABLED=true.
|
|
104
|
-
|
|
105
|
+
RERANK_MODE=blended
|
|
105
106
|
|
|
106
107
|
# Windows bind-mount mode (default: false).
|
|
107
108
|
# Set to true when your vault is on a Windows drive (Docker Desktop).
|
|
108
109
|
# Enables polling for the file watcher and rename-based moves across
|
|
109
110
|
# the Docker Desktop/WSL2 bridge.
|
|
110
|
-
|
|
111
|
+
WINDOWS_MODE=false
|
|
111
112
|
|
|
112
113
|
# Enable or disable the memory layer (default: true).
|
|
113
114
|
# Set to false to hide memory tools and skip About Me/ creation.
|
|
114
|
-
|
|
115
|
+
MEMORY_ENABLED=true
|
|
115
116
|
# Memory folder name in your vault (default: About Me).
|
|
116
|
-
|
|
117
|
+
MEMORY_DIR=About Me
|
|
117
118
|
|
|
118
119
|
# Comma-separated folders protected from deletion (default: MEMORY_DIR, Daily Notes).
|
|
119
120
|
# If your daily notes folder has a custom name (e.g. "Journal"), override to include it.
|
|
@@ -128,31 +129,31 @@ const REMOTE_OPTIONAL_BLOCK = `# Optional ────────────
|
|
|
128
129
|
# SERVICE_DOCUMENTATION_URL=https://github.com/youruser/your-fork
|
|
129
130
|
|
|
130
131
|
# Host port to expose (default: 8000).
|
|
131
|
-
|
|
132
|
+
PORT=8000
|
|
132
133
|
|
|
133
134
|
# Log verbosity: debug | info | warn | error (default: info).
|
|
134
|
-
|
|
135
|
+
LOG_LEVEL=info
|
|
135
136
|
|
|
136
137
|
# Directory for persistent log files inside the container (default: /data/logs).
|
|
137
138
|
# Set to empty to disable file logging (logs still go to stdout either way).
|
|
138
|
-
|
|
139
|
+
LOG_DIR=/data/logs
|
|
139
140
|
|
|
140
141
|
# Days to retain persistent log files before cleanup (default: 30).
|
|
141
|
-
|
|
142
|
+
LOG_RETENTION_DAYS=30
|
|
142
143
|
|
|
143
144
|
# User/group IDs for obsidian-sync (default: 1000).
|
|
144
|
-
|
|
145
|
-
|
|
145
|
+
PUID=1000
|
|
146
|
+
PGID=1000
|
|
146
147
|
|
|
147
148
|
# Device name shown in Obsidian Sync settings.
|
|
148
|
-
|
|
149
|
+
DEVICE_NAME=vault-cortex
|
|
149
150
|
|
|
150
151
|
# Obsidian Sync conflict resolution: merge | conflict (default: merge).
|
|
151
152
|
# 'merge' integrates changes automatically; 'conflict' writes a separate conflict file.
|
|
152
|
-
|
|
153
|
+
CONFLICT_STRATEGY=merge
|
|
153
154
|
|
|
154
155
|
# Sync direction: bidirectional | pull-only | push-only (default: bidirectional).
|
|
155
|
-
|
|
156
|
+
SYNC_MODE=bidirectional
|
|
156
157
|
`;
|
|
157
158
|
// sync:remote-optional:end
|
|
158
159
|
export const buildLocalEnv = (answers) => `# vault-cortex — local quickstart
|
|
@@ -167,6 +168,10 @@ MCP_AUTH_TOKEN=${answers.mcpAuthToken}
|
|
|
167
168
|
# Absolute path to your Obsidian vault on this machine.
|
|
168
169
|
VAULT_PATH=${answers.vaultPath}
|
|
169
170
|
|
|
171
|
+
# Public URL for OAuth issuer URL in discovery metadata.
|
|
172
|
+
# Override if you expose the server on a different URL (e.g. via a reverse proxy).
|
|
173
|
+
PUBLIC_URL=http://localhost:8000
|
|
174
|
+
|
|
170
175
|
${LOCAL_OPTIONAL_BLOCK}`;
|
|
171
176
|
export const buildRemoteEnv = (answers) => {
|
|
172
177
|
const vaultPasswordLines = answers.vaultPassword === undefined
|
|
@@ -175,10 +180,9 @@ export const buildRemoteEnv = (answers) => {
|
|
|
175
180
|
: `# Vault end-to-end encryption password.
|
|
176
181
|
VAULT_PASSWORD=${answers.vaultPassword}`;
|
|
177
182
|
const obsidianTokenComment = answers.obsidianAuthToken === ""
|
|
178
|
-
? `# Obsidian Sync auth token — FILL THIS IN before
|
|
183
|
+
? `# Obsidian Sync auth token — FILL THIS IN before starting the server.
|
|
179
184
|
# Generate once with:
|
|
180
|
-
#
|
|
181
|
-
# ${GET_TOKEN_IMAGE}`
|
|
185
|
+
# npx vault-cortex get-sync-token`
|
|
182
186
|
: `# Obsidian Sync auth token.`;
|
|
183
187
|
return `# vault-cortex — remote quickstart (Obsidian Sync)
|
|
184
188
|
# Generated by \`npx vault-cortex init\`. Full option reference:
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { patchEnvObsidianToken } from "./scaffold.js";
|
|
5
|
+
import { expandTilde } from "./vault.js";
|
|
6
|
+
/** Message from an unknown throw — Error instances keep their message. */
|
|
7
|
+
const describeError = (error) => error instanceof Error ? error.message : String(error);
|
|
8
|
+
/**
|
|
9
|
+
* Creates the temp dir the container's config mount writes into.
|
|
10
|
+
* Returns undefined (after warning) when creation fails.
|
|
11
|
+
*/
|
|
12
|
+
const makeTempMountDir = (prompts) => {
|
|
13
|
+
try {
|
|
14
|
+
return mkdtempSync(join(tmpdir(), "vault-cortex-sync-token-"));
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
prompts.warn(`Could not create a temp directory for token capture — ${describeError(error)}`);
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Runs the interactive Obsidian login container. A throw from the Docker
|
|
23
|
+
* runner is reported and treated the same as a non-zero exit.
|
|
24
|
+
*/
|
|
25
|
+
const runLoginContainer = (configMountPath, deps) => {
|
|
26
|
+
const { docker, prompts } = deps;
|
|
27
|
+
try {
|
|
28
|
+
return docker.runObsidianLogin(configMountPath);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
prompts.warn(`Docker run failed — ${describeError(error)}`);
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Reads the captured token file from the config mount. Returns undefined
|
|
37
|
+
* when the file is missing, empty, or unreadable — the caller treats all
|
|
38
|
+
* three as "no token captured".
|
|
39
|
+
*/
|
|
40
|
+
const readCapturedTokenFile = (configMountPath) => {
|
|
41
|
+
const tokenPath = join(configMountPath, "obsidian-headless", "auth_token");
|
|
42
|
+
try {
|
|
43
|
+
if (!existsSync(tokenPath))
|
|
44
|
+
return undefined;
|
|
45
|
+
const token = readFileSync(tokenPath, "utf8").trim();
|
|
46
|
+
return token || undefined;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Best-effort removal of the temp mount dir. Failing to remove it (e.g.
|
|
54
|
+
* root-owned files left by the container) must not turn a successful
|
|
55
|
+
* capture into a failure, so it warns instead of throwing.
|
|
56
|
+
*/
|
|
57
|
+
const removeTempMountDir = (configMountPath, prompts) => {
|
|
58
|
+
try {
|
|
59
|
+
rmSync(configMountPath, { recursive: true, force: true });
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
prompts.warn(`Could not remove temp directory ${configMountPath} — ${describeError(error)}`);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Runs the Obsidian login (`ob login`) inside a Docker container with a
|
|
67
|
+
* volume mount that captures the auth token file. The interactive login
|
|
68
|
+
* (email, password, MFA) shows in the terminal, but the resulting token is
|
|
69
|
+
* read from the mounted config dir — never printed, so it stays out of
|
|
70
|
+
* terminal scrollback.
|
|
71
|
+
*
|
|
72
|
+
* tokenDestinationMessage finishes the handoff message by telling the user
|
|
73
|
+
* where the captured token ends up — the destination differs per flow
|
|
74
|
+
* (init stores it in the generated .env; the subcommand prints it, or
|
|
75
|
+
* writes it to an existing .env with --dir).
|
|
76
|
+
*
|
|
77
|
+
* Returns the token string on success, undefined on any failure — each
|
|
78
|
+
* fallible operation is wrapped individually by the helpers above, so no
|
|
79
|
+
* catch-all is needed here. The bare try/finally only scopes the temp dir
|
|
80
|
+
* (acquire → release); it has no catch and swallows nothing.
|
|
81
|
+
*/
|
|
82
|
+
export const captureObsidianToken = (deps, tokenDestinationMessage) => {
|
|
83
|
+
const { prompts } = deps;
|
|
84
|
+
const configMountPath = makeTempMountDir(prompts);
|
|
85
|
+
if (!configMountPath)
|
|
86
|
+
return undefined;
|
|
87
|
+
try {
|
|
88
|
+
prompts.log("Handing the terminal to the Obsidian login — it will ask for your " +
|
|
89
|
+
`account email, password, and MFA code. ${tokenDestinationMessage}`);
|
|
90
|
+
const loginSucceeded = runLoginContainer(configMountPath, deps);
|
|
91
|
+
if (!loginSucceeded) {
|
|
92
|
+
prompts.warn("The Obsidian login did not complete — you can run it later with:\n" +
|
|
93
|
+
" npx vault-cortex get-sync-token");
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
const token = readCapturedTokenFile(configMountPath);
|
|
97
|
+
if (!token) {
|
|
98
|
+
prompts.warn("The Obsidian login finished, but no token was captured — the " +
|
|
99
|
+
"token file was missing, empty, or unreadable. You can retry with:\n" +
|
|
100
|
+
" npx vault-cortex get-sync-token");
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
return token;
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
removeTempMountDir(configMountPath, prompts);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* Subcommand entry: generate an Obsidian Sync token via Docker.
|
|
111
|
+
* Without --dir, prints the token to stdout.
|
|
112
|
+
* With --dir, writes it directly to `<dir>/.env`.
|
|
113
|
+
*/
|
|
114
|
+
export const runGetSyncToken = async (flags, deps) => {
|
|
115
|
+
const { prompts, docker } = deps;
|
|
116
|
+
if (!docker.isDaemonRunning()) {
|
|
117
|
+
prompts.error("Container runtime not running — start Docker Desktop, Colima,\n" +
|
|
118
|
+
"OrbStack, or another Docker-compatible runtime and try again.");
|
|
119
|
+
return 1;
|
|
120
|
+
}
|
|
121
|
+
prompts.intro("vault-cortex get-sync-token");
|
|
122
|
+
// Resolve the destination up front so the login handoff message can tell
|
|
123
|
+
// the user where the token will end up.
|
|
124
|
+
const envFilePath = flags.dir
|
|
125
|
+
? join(resolve(expandTilde(flags.dir)), ".env")
|
|
126
|
+
: undefined;
|
|
127
|
+
const tokenDestinationMessage = envFilePath
|
|
128
|
+
? `The token is captured automatically and written to ${envFilePath}.`
|
|
129
|
+
: "The token is captured automatically and printed at the end.";
|
|
130
|
+
const token = captureObsidianToken({ docker, prompts }, tokenDestinationMessage);
|
|
131
|
+
if (!token) {
|
|
132
|
+
prompts.error("Could not capture the auth token.");
|
|
133
|
+
return 1;
|
|
134
|
+
}
|
|
135
|
+
if (!envFilePath) {
|
|
136
|
+
prompts.log("Your OBSIDIAN_AUTH_TOKEN:");
|
|
137
|
+
prompts.print(`\n ${token}\n`);
|
|
138
|
+
prompts.outro("Done.");
|
|
139
|
+
return 0;
|
|
140
|
+
}
|
|
141
|
+
const patched = patchEnvObsidianToken(envFilePath, token);
|
|
142
|
+
if (!patched) {
|
|
143
|
+
prompts.error(`Could not patch ${envFilePath} — the file is missing or has no ` +
|
|
144
|
+
"OBSIDIAN_AUTH_TOKEN line. Run init first.");
|
|
145
|
+
return 1;
|
|
146
|
+
}
|
|
147
|
+
prompts.log(`Token written to ${envFilePath}`);
|
|
148
|
+
prompts.outro("Done.");
|
|
149
|
+
return 0;
|
|
150
|
+
};
|
package/dist/init.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { join, resolve } from "node:path";
|
|
2
2
|
import { buildLocalEnv, buildRemoteEnv } from "./env.js";
|
|
3
|
+
import { captureObsidianToken } from "./get-sync-token.js";
|
|
3
4
|
import { buildLocalConnectMessage, buildRemoteConnectMessage, } from "./messages.js";
|
|
4
|
-
import {
|
|
5
|
+
import { pollHealth } from "./docker.js";
|
|
5
6
|
import { buildFilesToWrite, readEnvPort, writeFiles, } from "./scaffold.js";
|
|
6
7
|
import { generateToken } from "./token.js";
|
|
7
8
|
import { expandTilde, validateVaultPath } from "./vault.js";
|
|
@@ -20,28 +21,18 @@ const askMode = async (prompts) => {
|
|
|
20
21
|
hint: "VPS + Obsidian Sync, access from anywhere",
|
|
21
22
|
},
|
|
22
23
|
], "local");
|
|
23
|
-
// The select only offers mode values; the guard narrows without a cast.
|
|
24
24
|
return isMode(selected) ? selected : "local";
|
|
25
25
|
};
|
|
26
|
-
const GET_TOKEN_COMMAND = `docker run --rm -it --entrypoint get-token \\
|
|
27
|
-
${GET_TOKEN_IMAGE}`;
|
|
28
26
|
/**
|
|
29
|
-
* Offers to
|
|
30
|
-
* Returns
|
|
31
|
-
*
|
|
32
|
-
* way to raw docker output — image pull, then the tool's own login prompts.
|
|
27
|
+
* Offers to auto-capture the Obsidian Sync token via a Docker volume mount.
|
|
28
|
+
* Returns the captured token string, or undefined when the user declines or
|
|
29
|
+
* the capture fails (the caller falls back to a paste prompt).
|
|
33
30
|
*/
|
|
34
|
-
const
|
|
35
|
-
const runNow = await prompts.confirm("
|
|
31
|
+
const offerSyncTokenCapture = async (prompts, docker) => {
|
|
32
|
+
const runNow = await prompts.confirm("Generate the token now?", true);
|
|
36
33
|
if (!runNow)
|
|
37
|
-
return
|
|
38
|
-
|
|
39
|
-
"account login and print a token at the end.");
|
|
40
|
-
if (!docker.runGetToken()) {
|
|
41
|
-
prompts.warn("get-token did not complete — you can run it later and edit .env.");
|
|
42
|
-
return false;
|
|
43
|
-
}
|
|
44
|
-
return true;
|
|
34
|
+
return undefined;
|
|
35
|
+
return captureObsidianToken({ docker, prompts }, "The token is captured automatically and stored in your .env — nothing to copy.");
|
|
45
36
|
};
|
|
46
37
|
/**
|
|
47
38
|
* Asks for the vault path, recursing to re-prompt until it gets a usable
|
|
@@ -60,8 +51,6 @@ const askVaultPath = async (prompts) => {
|
|
|
60
51
|
return askVaultPath(prompts);
|
|
61
52
|
}
|
|
62
53
|
if (validation.kind === "warn") {
|
|
63
|
-
// Renders as: "<path> doesn't look like an Obsidian vault (no .obsidian
|
|
64
|
-
// folder). Use it anyway? (Y/n)"
|
|
65
54
|
const useAnyway = await prompts.confirm(`${validation.message} Use it anyway?`, true);
|
|
66
55
|
if (!useAnyway)
|
|
67
56
|
return askVaultPath(prompts);
|
|
@@ -140,45 +129,46 @@ const reportWrites = (params, prompts) => {
|
|
|
140
129
|
}
|
|
141
130
|
};
|
|
142
131
|
/**
|
|
143
|
-
* Offers to start the
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
* the caller uses that to pick the right connect message.
|
|
132
|
+
* Offers to start the container, walking a gate ladder where each failed
|
|
133
|
+
* gate degrades to instructions instead of an error: daemon running → user
|
|
134
|
+
* consents → docker run succeeds → health check passes. Returns true only
|
|
135
|
+
* when the server is confirmed up.
|
|
148
136
|
*/
|
|
149
|
-
const
|
|
150
|
-
const { targetDir, port } = params;
|
|
137
|
+
const offerDockerRun = async (params, deps) => {
|
|
138
|
+
const { targetDir, port, mode, vaultPath } = params;
|
|
151
139
|
const { prompts, docker, fetchFn } = deps;
|
|
152
|
-
if (!docker.isComposeAvailable()) {
|
|
153
|
-
prompts.warn("Docker Compose not found — install Docker to start the server:\n" +
|
|
154
|
-
"https://docs.docker.com/get-docker/");
|
|
155
|
-
return false;
|
|
156
|
-
}
|
|
157
140
|
if (!docker.isDaemonRunning()) {
|
|
158
|
-
prompts.warn("
|
|
159
|
-
"
|
|
141
|
+
prompts.warn("Container runtime not running — start Docker Desktop, Colima,\n" +
|
|
142
|
+
"OrbStack, or another Docker-compatible runtime, then run:\n" +
|
|
143
|
+
` npx vault-cortex upgrade --dir "${targetDir}"`);
|
|
160
144
|
return false;
|
|
161
145
|
}
|
|
162
|
-
const startNow = await prompts.confirm("Start the server now?
|
|
146
|
+
const startNow = await prompts.confirm("Start the server now?", true);
|
|
163
147
|
if (!startNow)
|
|
164
148
|
return false;
|
|
165
|
-
|
|
166
|
-
|
|
149
|
+
const containerStarted = docker.dockerRun({
|
|
150
|
+
mode,
|
|
151
|
+
envFilePath: join(targetDir, ".env"),
|
|
152
|
+
port,
|
|
153
|
+
vaultPath,
|
|
154
|
+
});
|
|
155
|
+
if (!containerStarted) {
|
|
156
|
+
prompts.error("docker run failed — see output above.");
|
|
167
157
|
return false;
|
|
168
158
|
}
|
|
169
159
|
const spinner = prompts.spinner();
|
|
170
|
-
spinner.start("Waiting for the server to come up (first run
|
|
160
|
+
spinner.start("Waiting for the server to come up (first run may take a moment)");
|
|
171
161
|
const healthy = await pollHealth({ url: `http://127.0.0.1:${port}/healthz` }, fetchFn);
|
|
172
162
|
if (!healthy) {
|
|
173
|
-
spinner.stop("Server did not respond within 2 minutes — check: docker
|
|
163
|
+
spinner.stop("Server did not respond within 2 minutes — check: docker logs vault-cortex");
|
|
174
164
|
return false;
|
|
175
165
|
}
|
|
176
166
|
spinner.stop("Server is up — health check passed.");
|
|
177
167
|
return true;
|
|
178
168
|
};
|
|
179
169
|
// Local flow: resolve vault path → resolve target dir → generate token →
|
|
180
|
-
// write
|
|
181
|
-
//
|
|
170
|
+
// write .env → optionally start the container → print connect instructions.
|
|
171
|
+
// Returns a process exit code.
|
|
182
172
|
const runLocalInit = async (flags, deps) => {
|
|
183
173
|
const { prompts } = deps;
|
|
184
174
|
// Vault path comes from --vault-path when given and valid; interactive
|
|
@@ -188,7 +178,7 @@ const runLocalInit = async (flags, deps) => {
|
|
|
188
178
|
? undefined
|
|
189
179
|
: validateVaultPath(flags.vaultPath);
|
|
190
180
|
if (flags.yes) {
|
|
191
|
-
if (vaultPathResult
|
|
181
|
+
if (!vaultPathResult || vaultPathResult.kind === "error") {
|
|
192
182
|
prompts.error(vaultPathResult?.message ?? "--yes requires --vault-path.");
|
|
193
183
|
return 1;
|
|
194
184
|
}
|
|
@@ -200,7 +190,7 @@ const runLocalInit = async (flags, deps) => {
|
|
|
200
190
|
}
|
|
201
191
|
// A warn-level flag path (no .obsidian/) is accepted without the confirm a
|
|
202
192
|
// prompted path gets — passing the flag is already an explicit choice.
|
|
203
|
-
const vaultPath = vaultPathResult
|
|
193
|
+
const vaultPath = vaultPathResult && vaultPathResult.kind !== "error"
|
|
204
194
|
? vaultPathResult.path
|
|
205
195
|
: await askVaultPath(prompts);
|
|
206
196
|
// expandTilde before resolve: resolve() treats a leading `~` as a literal
|
|
@@ -216,7 +206,7 @@ const runLocalInit = async (flags, deps) => {
|
|
|
216
206
|
// Conflict policy: identical existing files are skipped silently;
|
|
217
207
|
// differing ones prompt per file (default keep). --yes never overwrites —
|
|
218
208
|
// any differing file becomes an exit-1 below, leaving it untouched.
|
|
219
|
-
const files = buildFilesToWrite(
|
|
209
|
+
const files = buildFilesToWrite(buildLocalEnv({ mcpAuthToken: token, vaultPath }));
|
|
220
210
|
const resolveConflict = flags.yes ? keepExisting : confirmOverwrite(prompts);
|
|
221
211
|
const results = await writeFiles({ targetDir, files }, resolveConflict);
|
|
222
212
|
reportWrites({ targetDir, results }, prompts);
|
|
@@ -226,8 +216,8 @@ const runLocalInit = async (flags, deps) => {
|
|
|
226
216
|
return 1;
|
|
227
217
|
}
|
|
228
218
|
// When an existing .env was kept, this run's generated token was never
|
|
229
|
-
// saved — the connect message must point at the token (and PORT) actually
|
|
230
|
-
// or a pasted token fails auth with no hint why.
|
|
219
|
+
// saved — the connect message must point at the token (and PORT) actually
|
|
220
|
+
// on disk, or a pasted token fails auth with no hint why.
|
|
231
221
|
const envResult = results.find((result) => result.name === ".env");
|
|
232
222
|
const tokenWritten = envResult?.status === "created" || envResult?.status === "overwritten";
|
|
233
223
|
if (tokenWritten)
|
|
@@ -236,15 +226,15 @@ const runLocalInit = async (flags, deps) => {
|
|
|
236
226
|
// --yes is for scripts/CI, so it never starts Docker.
|
|
237
227
|
const started = flags.yes
|
|
238
228
|
? false
|
|
239
|
-
: await
|
|
229
|
+
: await offerDockerRun({ targetDir, port, mode: "local", vaultPath }, deps);
|
|
240
230
|
prompts.print(buildLocalConnectMessage({ targetDir, token, started, port, tokenWritten }));
|
|
241
231
|
return 0;
|
|
242
232
|
};
|
|
243
233
|
// Remote flow (VPS + Obsidian Sync): resolve target dir → PUBLIC_URL →
|
|
244
|
-
// VAULT_NAME → Obsidian Sync token (optionally running
|
|
245
|
-
// Docker) → optional E2E vault password → generate token → write
|
|
246
|
-
//
|
|
247
|
-
//
|
|
234
|
+
// VAULT_NAME → Obsidian Sync token (optionally running the Obsidian login via
|
|
235
|
+
// Docker) → optional E2E vault password → generate token → write .env →
|
|
236
|
+
// optionally start → print connect instructions. Always interactive —
|
|
237
|
+
// the sync-token step can't be defaulted.
|
|
248
238
|
const runRemoteInit = async (flags, deps) => {
|
|
249
239
|
const { prompts, docker } = deps;
|
|
250
240
|
// expandTilde before resolve: resolve() treats a leading `~` as a literal
|
|
@@ -256,21 +246,17 @@ const runRemoteInit = async (flags, deps) => {
|
|
|
256
246
|
}))));
|
|
257
247
|
const publicUrl = await askPublicUrl(prompts);
|
|
258
248
|
const vaultName = await askVaultName(prompts);
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const pastePrompt = getTokenRan
|
|
271
|
-
? "Paste the Obsidian Sync token printed above (leave blank to fill in .env later):"
|
|
272
|
-
: "Paste the Obsidian Sync token (leave blank to fill in .env later):";
|
|
273
|
-
const obsidianAuthToken = (await prompts.text(pastePrompt, { defaultValue: "" })).trim();
|
|
249
|
+
// Auto-capture the Obsidian Sync token via a Docker volume mount when
|
|
250
|
+
// the daemon is reachable. Falls back to a paste prompt when capture
|
|
251
|
+
// fails or the user declines.
|
|
252
|
+
const capturedToken = docker.isDaemonRunning()
|
|
253
|
+
? await offerSyncTokenCapture(prompts, docker)
|
|
254
|
+
: undefined;
|
|
255
|
+
// Masked prompt: the sync token is a credential and must not echo into
|
|
256
|
+
// the terminal or scrollback. An empty submission still means "fill in
|
|
257
|
+
// .env later" — clack's password prompt accepts blank input.
|
|
258
|
+
const obsidianAuthToken = capturedToken ??
|
|
259
|
+
(await prompts.password("Paste the Obsidian Sync token (leave blank to fill in .env later):")).trim();
|
|
274
260
|
const usesEncryption = await prompts.confirm("Does your vault use end-to-end encryption?", false);
|
|
275
261
|
const vaultPassword = usesEncryption
|
|
276
262
|
? await prompts.password("Vault encryption password:")
|
|
@@ -283,24 +269,23 @@ const runRemoteInit = async (flags, deps) => {
|
|
|
283
269
|
vaultName,
|
|
284
270
|
vaultPassword,
|
|
285
271
|
});
|
|
286
|
-
const files = buildFilesToWrite(
|
|
272
|
+
const files = buildFilesToWrite(envContent);
|
|
287
273
|
const results = await writeFiles({ targetDir, files }, confirmOverwrite(prompts));
|
|
288
274
|
reportWrites({ targetDir, results }, prompts);
|
|
289
275
|
// Same kept-.env handling as the local flow: the server only reads config
|
|
290
276
|
// from the .env on disk, so when an existing file was kept, this run's
|
|
291
277
|
// generated token was never saved (printing it would fail auth) and PORT
|
|
292
|
-
// may differ from the default
|
|
293
|
-
// run, not the one this run intended to configure.
|
|
278
|
+
// may differ from the default.
|
|
294
279
|
const envResult = results.find((result) => result.name === ".env");
|
|
295
280
|
const tokenWritten = envResult?.status === "created" || envResult?.status === "overwritten";
|
|
296
281
|
if (tokenWritten)
|
|
297
282
|
prompts.log("Generated MCP auth token (saved to .env).");
|
|
298
283
|
const port = readEnvPort(join(targetDir, ".env"));
|
|
299
284
|
// Without the sync token the container can't start (init-check-auth fails
|
|
300
|
-
// and s6 stops it), so only offer
|
|
285
|
+
// and s6 stops it), so only offer docker run when it was provided.
|
|
301
286
|
const started = obsidianAuthToken === ""
|
|
302
287
|
? false
|
|
303
|
-
: await
|
|
288
|
+
: await offerDockerRun({ targetDir, port, mode: "remote" }, deps);
|
|
304
289
|
prompts.print(buildRemoteConnectMessage({
|
|
305
290
|
targetDir,
|
|
306
291
|
token,
|
|
@@ -326,9 +311,8 @@ export const runInit = async (flags, deps) => {
|
|
|
326
311
|
return 1;
|
|
327
312
|
}
|
|
328
313
|
prompts.intro("vault-cortex init");
|
|
329
|
-
// Mode resolution: explicit --mode wins
|
|
330
|
-
//
|
|
331
|
-
// it's the activation path.
|
|
314
|
+
// Mode resolution: explicit --mode wins; --yes implies local; otherwise
|
|
315
|
+
// ask, defaulting to local — it's the simpler activation path.
|
|
332
316
|
const mode = flags.mode !== undefined && isMode(flags.mode)
|
|
333
317
|
? flags.mode
|
|
334
318
|
: flags.yes
|
package/dist/main.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { createDockerRunner } from "./docker.js";
|
|
2
|
+
import { runGetSyncToken } from "./get-sync-token.js";
|
|
2
3
|
import { runInit } from "./init.js";
|
|
3
4
|
import { buildProgram } from "./program.js";
|
|
4
5
|
import { createPrompts } from "./prompts.js";
|
|
6
|
+
import { runUpgrade } from "./upgrade.js";
|
|
5
7
|
export const run = async (version) => {
|
|
6
8
|
const program = buildProgram({
|
|
7
9
|
version,
|
|
@@ -10,6 +12,15 @@ export const run = async (version) => {
|
|
|
10
12
|
docker: createDockerRunner(),
|
|
11
13
|
fetchFn: fetch,
|
|
12
14
|
}),
|
|
15
|
+
runUpgrade: (flags) => runUpgrade(flags, {
|
|
16
|
+
prompts: createPrompts(),
|
|
17
|
+
docker: createDockerRunner(),
|
|
18
|
+
fetchFn: fetch,
|
|
19
|
+
}),
|
|
20
|
+
runGetSyncToken: (flags) => runGetSyncToken(flags, {
|
|
21
|
+
prompts: createPrompts(),
|
|
22
|
+
docker: createDockerRunner(),
|
|
23
|
+
}),
|
|
13
24
|
});
|
|
14
25
|
await program.parseAsync();
|
|
15
26
|
};
|
package/dist/messages.js
CHANGED
|
@@ -12,15 +12,15 @@ const bottomRule = () => paint("dim", `╰${"─".repeat(RULE_WIDTH - 2)}╯`);
|
|
|
12
12
|
const sectionRule = (label) => paint("dim", `── ${label} ${"─".repeat(Math.max(0, RULE_WIDTH - label.length - 4))}`);
|
|
13
13
|
// targetDir is quoted: these lines are meant to be copy-pasted into a
|
|
14
14
|
// shell, and an unquoted path breaks on spaces or special characters.
|
|
15
|
-
const
|
|
16
|
-
const startServerLine = (targetDir) => `Start the server:\n ${
|
|
15
|
+
const upgradeCommand = (targetDir) => `npx vault-cortex upgrade --dir "${targetDir}"`;
|
|
16
|
+
const startServerLine = (targetDir) => `Start the server:\n ${upgradeCommand(targetDir)}`;
|
|
17
17
|
/** Remote start line: running, blocked on the missing sync token, or ready to start. */
|
|
18
18
|
const remoteStartLine = (params) => {
|
|
19
19
|
const { targetDir, started, obsidianTokenMissing } = params;
|
|
20
20
|
if (started)
|
|
21
21
|
return "The server is running.";
|
|
22
22
|
if (obsidianTokenMissing) {
|
|
23
|
-
return `Fill in OBSIDIAN_AUTH_TOKEN in ${targetDir}/.env, then start the server:\n ${
|
|
23
|
+
return `Fill in OBSIDIAN_AUTH_TOKEN in ${targetDir}/.env, then start the server:\n ${upgradeCommand(targetDir)}`;
|
|
24
24
|
}
|
|
25
25
|
return startServerLine(targetDir);
|
|
26
26
|
};
|
|
@@ -62,10 +62,8 @@ const curlGuidance = (mcpUrl) => `Clients without OAuth, scripts, and curl send
|
|
|
62
62
|
curl -H "Authorization: Bearer <token>" ${mcpUrl}`;
|
|
63
63
|
const smokeTest = (healthUrl) => `Smoke test:
|
|
64
64
|
curl ${healthUrl}`;
|
|
65
|
-
// Compose does not pull new images on `up` — without this hint users stay
|
|
66
|
-
// on the image from init day forever while believing they track releases.
|
|
67
65
|
const updateGuidance = (targetDir) => `Update to the latest release:
|
|
68
|
-
|
|
66
|
+
${upgradeCommand(targetDir)}`;
|
|
69
67
|
/**
|
|
70
68
|
* Local-mode "Connect" message. port comes from the .env on disk: a kept file
|
|
71
69
|
* may override the default, so the message must describe the server that will
|
|
@@ -114,7 +112,7 @@ ${sectionRule("Settings")}
|
|
|
114
112
|
|
|
115
113
|
Optional settings (timezone, memory folder, port, logging) are commented
|
|
116
114
|
out in ${targetDir}/.env — uncomment, set a value, then apply with
|
|
117
|
-
"
|
|
115
|
+
"npx vault-cortex upgrade" (restart alone does not re-read .env).
|
|
118
116
|
|
|
119
117
|
${updateGuidance(targetDir)}
|
|
120
118
|
|
|
@@ -142,7 +140,8 @@ export const buildRemoteConnectMessage = (params) => {
|
|
|
142
140
|
// know which case it is, so the http branch states it rather than asking.
|
|
143
141
|
// Case-insensitive: askPublicUrl stores the scheme as typed, so an HTTPS://
|
|
144
142
|
// input is valid and must still route to the https branch.
|
|
145
|
-
const
|
|
143
|
+
const isHttps = publicUrl.toLowerCase().startsWith("https://");
|
|
144
|
+
const clientGuidance = isHttps
|
|
146
145
|
? `${connectGuidance(`${publicUrl}/mcp`)}
|
|
147
146
|
|
|
148
147
|
Reachable over https from any MCP client — Claude Desktop, claude.ai (web
|
|
@@ -174,8 +173,8 @@ ${sectionRule("Settings")}
|
|
|
174
173
|
|
|
175
174
|
Optional settings (timezone, memory folder, port, logging, sync
|
|
176
175
|
behavior) are commented out in ${targetDir}/.env — uncomment, set a
|
|
177
|
-
value, then apply with "
|
|
178
|
-
re-read .env).
|
|
176
|
+
value, then apply with "npx vault-cortex upgrade" (restart alone does
|
|
177
|
+
not re-read .env).
|
|
179
178
|
|
|
180
179
|
${updateGuidance(targetDir)}
|
|
181
180
|
|
package/dist/program.js
CHANGED
|
@@ -7,7 +7,7 @@ export const buildProgram = (options) => {
|
|
|
7
7
|
.version(options.version);
|
|
8
8
|
program
|
|
9
9
|
.command("init")
|
|
10
|
-
.description("Scaffold
|
|
10
|
+
.description("Scaffold .env and optionally start the server")
|
|
11
11
|
.option("--mode <mode>", 'deployment mode: "local" (default) or "remote"')
|
|
12
12
|
.option("--vault-path <path>", "absolute path to your Obsidian vault (local mode)")
|
|
13
13
|
.option("--dir <path>", "directory to write config files into (default: ./vault-cortex)")
|
|
@@ -15,7 +15,20 @@ export const buildProgram = (options) => {
|
|
|
15
15
|
.action(async (flags) => {
|
|
16
16
|
process.exitCode = await options.runInit(flags);
|
|
17
17
|
});
|
|
18
|
-
|
|
18
|
+
program
|
|
19
|
+
.command("upgrade")
|
|
20
|
+
.description("Pull the latest image, re-create the container, and verify health")
|
|
21
|
+
.option("--dir <path>", "directory containing .env (default: ./vault-cortex)")
|
|
22
|
+
.action(async (flags) => {
|
|
23
|
+
process.exitCode = await options.runUpgrade(flags);
|
|
24
|
+
});
|
|
25
|
+
program
|
|
26
|
+
.command("get-sync-token")
|
|
27
|
+
.description("Generate an Obsidian Sync auth token via Docker and print it or write it to .env")
|
|
28
|
+
.option("--dir <path>", "directory containing .env to update with the token")
|
|
29
|
+
.action(async (flags) => {
|
|
30
|
+
process.exitCode = await options.runGetSyncToken(flags);
|
|
31
|
+
});
|
|
19
32
|
program.action(() => {
|
|
20
33
|
program.help();
|
|
21
34
|
});
|
package/dist/scaffold.js
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync, } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
|
|
4
|
-
/** Default host port — matches the compose templates' `${PORT:-8000}`. */
|
|
3
|
+
/** Default host port — matches the container's internal port. */
|
|
5
4
|
export const DEFAULT_PORT = 8000;
|
|
6
5
|
/** Matches an active (uncommented) PORT line in a .env file. */
|
|
7
6
|
const ENV_PORT_LINE = /^PORT=(\d+)\s*$/m;
|
|
8
|
-
/**
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
export const buildFilesToWrite = (
|
|
15
|
-
{ name: "docker-compose.yml", content: readComposeTemplate(mode) },
|
|
7
|
+
/** Matches an active (uncommented) VAULT_PATH line in a .env file. */
|
|
8
|
+
const ENV_VAULT_PATH_LINE = /^VAULT_PATH=(.+)\s*$/m;
|
|
9
|
+
/** Matches an active (uncommented) PUBLIC_URL line. */
|
|
10
|
+
const ENV_PUBLIC_URL_LINE = /^PUBLIC_URL=/m;
|
|
11
|
+
/** Matches an active (uncommented) OBSIDIAN_AUTH_TOKEN line. */
|
|
12
|
+
const OBSIDIAN_AUTH_TOKEN_LINE = /^OBSIDIAN_AUTH_TOKEN=/m;
|
|
13
|
+
export const buildFilesToWrite = (envContent) => [
|
|
16
14
|
// .env holds the bearer token (and possibly a vault password) — owner-only.
|
|
17
15
|
{ name: ".env", content: envContent, mode: 0o600 },
|
|
18
16
|
];
|
|
@@ -26,7 +24,57 @@ export const readEnvPort = (envFilePath) => {
|
|
|
26
24
|
if (!existsSync(envFilePath))
|
|
27
25
|
return DEFAULT_PORT;
|
|
28
26
|
const match = ENV_PORT_LINE.exec(readFileSync(envFilePath, "utf8"));
|
|
29
|
-
return match
|
|
27
|
+
return match ? Number(match[1]) : DEFAULT_PORT;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Reads the host vault path from a .env file. Returns undefined when the
|
|
31
|
+
* file is missing or has no uncommented VAULT_PATH line.
|
|
32
|
+
*/
|
|
33
|
+
export const readEnvVaultPath = (envFilePath) => {
|
|
34
|
+
if (!existsSync(envFilePath))
|
|
35
|
+
return undefined;
|
|
36
|
+
const match = ENV_VAULT_PATH_LINE.exec(readFileSync(envFilePath, "utf8"));
|
|
37
|
+
return match?.[1].trim();
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Returns true when the .env file has an active (uncommented) PUBLIC_URL line.
|
|
41
|
+
* Used by upgrade to detect .env files from the old compose-based CLI, where
|
|
42
|
+
* PUBLIC_URL was provided by docker-compose defaults rather than the .env.
|
|
43
|
+
*/
|
|
44
|
+
export const hasEnvPublicUrl = (envFilePath) => {
|
|
45
|
+
if (!existsSync(envFilePath))
|
|
46
|
+
return false;
|
|
47
|
+
return ENV_PUBLIC_URL_LINE.test(readFileSync(envFilePath, "utf8"));
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Detects the deployment mode from a .env file. Remote mode requires
|
|
51
|
+
* OBSIDIAN_AUTH_TOKEN (absent from local). Returns undefined when the
|
|
52
|
+
* .env file does not exist.
|
|
53
|
+
*/
|
|
54
|
+
export const detectMode = (envFilePath) => {
|
|
55
|
+
if (!existsSync(envFilePath))
|
|
56
|
+
return undefined;
|
|
57
|
+
const content = readFileSync(envFilePath, "utf8");
|
|
58
|
+
return OBSIDIAN_AUTH_TOKEN_LINE.test(content) ? "remote" : "local";
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Patches the OBSIDIAN_AUTH_TOKEN value in an existing .env file.
|
|
62
|
+
* Returns true when the patch succeeded, false when the file is missing
|
|
63
|
+
* or has no active OBSIDIAN_AUTH_TOKEN line (e.g. a local-mode .env).
|
|
64
|
+
*/
|
|
65
|
+
export const patchEnvObsidianToken = (envFilePath, token) => {
|
|
66
|
+
if (!existsSync(envFilePath))
|
|
67
|
+
return false;
|
|
68
|
+
const content = readFileSync(envFilePath, "utf8");
|
|
69
|
+
/** Matches the full OBSIDIAN_AUTH_TOKEN line for replacement. */
|
|
70
|
+
const fullTokenLine = /^OBSIDIAN_AUTH_TOKEN=.*$/m;
|
|
71
|
+
if (!fullTokenLine.test(content))
|
|
72
|
+
return false;
|
|
73
|
+
// Function replacement avoids $ pattern interpretation ($&, $', etc.)
|
|
74
|
+
// that String.prototype.replace applies to string replacements.
|
|
75
|
+
const patched = content.replace(fullTokenLine, () => `OBSIDIAN_AUTH_TOKEN=${token}`);
|
|
76
|
+
writeFileSync(envFilePath, patched);
|
|
77
|
+
return true;
|
|
30
78
|
};
|
|
31
79
|
/**
|
|
32
80
|
* Writes the files into targetDir (created if missing). Existing files
|
package/dist/upgrade.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { join, resolve } from "node:path";
|
|
2
|
+
import { CONTAINER_NAME, LOCAL_IMAGE, REMOTE_IMAGE, pollHealth, } from "./docker.js";
|
|
3
|
+
import { detectMode, hasEnvPublicUrl, readEnvPort, readEnvVaultPath, } from "./scaffold.js";
|
|
4
|
+
import { expandTilde } from "./vault.js";
|
|
5
|
+
const DEFAULT_TARGET_DIR = "./vault-cortex";
|
|
6
|
+
export const runUpgrade = async (flags, deps) => {
|
|
7
|
+
const { prompts, docker, fetchFn } = deps;
|
|
8
|
+
prompts.intro("vault-cortex upgrade");
|
|
9
|
+
const targetDir = resolve(expandTilde(flags.dir ?? DEFAULT_TARGET_DIR));
|
|
10
|
+
const envFilePath = join(targetDir, ".env");
|
|
11
|
+
const mode = detectMode(envFilePath);
|
|
12
|
+
if (!mode) {
|
|
13
|
+
prompts.error(`No .env found in ${targetDir} — run \`npx vault-cortex init\` first.`);
|
|
14
|
+
return 1;
|
|
15
|
+
}
|
|
16
|
+
const port = readEnvPort(envFilePath);
|
|
17
|
+
const vaultPath = mode === "local" ? readEnvVaultPath(envFilePath) : undefined;
|
|
18
|
+
if (mode === "local" && !vaultPath) {
|
|
19
|
+
prompts.error(`VAULT_PATH is empty or missing in ${targetDir}/.env — cannot start the container.`);
|
|
20
|
+
return 1;
|
|
21
|
+
}
|
|
22
|
+
if (mode === "local" && !hasEnvPublicUrl(envFilePath)) {
|
|
23
|
+
prompts.error(`PUBLIC_URL not found in ${targetDir}/.env — the server requires it.\n` +
|
|
24
|
+
`Add this line to your .env:\n PUBLIC_URL=http://localhost:${port}`);
|
|
25
|
+
return 1;
|
|
26
|
+
}
|
|
27
|
+
const image = mode === "local" ? LOCAL_IMAGE : REMOTE_IMAGE;
|
|
28
|
+
if (!docker.isDaemonRunning()) {
|
|
29
|
+
prompts.error("Container runtime not running — start Docker Desktop, Colima,\n" +
|
|
30
|
+
"OrbStack, or another Docker-compatible runtime.");
|
|
31
|
+
return 1;
|
|
32
|
+
}
|
|
33
|
+
const spinner = prompts.spinner();
|
|
34
|
+
spinner.start(`Pulling ${image}`);
|
|
35
|
+
const imagePulled = docker.pullImage(image);
|
|
36
|
+
if (!imagePulled) {
|
|
37
|
+
spinner.stop("Image pull failed — see output above.");
|
|
38
|
+
return 1;
|
|
39
|
+
}
|
|
40
|
+
spinner.stop("Image pulled.");
|
|
41
|
+
docker.stopAndRemoveContainer();
|
|
42
|
+
prompts.log("Starting container...");
|
|
43
|
+
const containerStarted = docker.dockerRun({
|
|
44
|
+
mode,
|
|
45
|
+
envFilePath,
|
|
46
|
+
port,
|
|
47
|
+
vaultPath,
|
|
48
|
+
});
|
|
49
|
+
if (!containerStarted) {
|
|
50
|
+
prompts.error("docker run failed — see output above.");
|
|
51
|
+
return 1;
|
|
52
|
+
}
|
|
53
|
+
spinner.start("Waiting for the server to come up");
|
|
54
|
+
const healthy = await pollHealth({
|
|
55
|
+
url: `http://127.0.0.1:${port}/healthz`,
|
|
56
|
+
timeoutMs: deps.healthTimeoutMs,
|
|
57
|
+
}, fetchFn);
|
|
58
|
+
if (!healthy) {
|
|
59
|
+
spinner.stop(`Server did not respond within 2 minutes — check: docker logs ${CONTAINER_NAME}`);
|
|
60
|
+
return 1;
|
|
61
|
+
}
|
|
62
|
+
spinner.stop("Server is up — health check passed.");
|
|
63
|
+
prompts.log("Your vault data, search index, and settings are preserved.");
|
|
64
|
+
prompts.outro("Upgrade complete.");
|
|
65
|
+
return 0;
|
|
66
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vault-cortex",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0-beta.28",
|
|
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",
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"dist",
|
|
12
|
-
"templates",
|
|
13
12
|
"README.md"
|
|
14
13
|
],
|
|
15
14
|
"engines": {
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
# vault-cortex — local quickstart
|
|
2
|
-
#
|
|
3
|
-
# Run vault-cortex against a local Obsidian vault with just Docker.
|
|
4
|
-
# No cloud, no Obsidian Sync, no AWS — just your vault folder and a bearer token.
|
|
5
|
-
#
|
|
6
|
-
# 1. cp .env.example .env (then fill in MCP_AUTH_TOKEN and VAULT_PATH)
|
|
7
|
-
# 2. docker compose up
|
|
8
|
-
# 3. Connect your MCP client to http://localhost:8000/mcp
|
|
9
|
-
# (Claude Desktop's connector dialog requires https — register a
|
|
10
|
-
# localhost server via the mcp-remote bridge; see the local README)
|
|
11
|
-
#
|
|
12
|
-
# Full docs: https://github.com/aliasunder/vault-cortex
|
|
13
|
-
|
|
14
|
-
name: vault-cortex
|
|
15
|
-
|
|
16
|
-
services:
|
|
17
|
-
vault-cortex:
|
|
18
|
-
image: ghcr.io/aliasunder/vault-cortex:latest
|
|
19
|
-
container_name: vault-cortex
|
|
20
|
-
restart: unless-stopped
|
|
21
|
-
environment:
|
|
22
|
-
PORT: "8000"
|
|
23
|
-
HOST: "0.0.0.0"
|
|
24
|
-
VAULT_PATH: /vault
|
|
25
|
-
INDEX_DB_PATH: /data/index.db
|
|
26
|
-
MCP_AUTH_TOKEN: "${MCP_AUTH_TOKEN:?Set MCP_AUTH_TOKEN in .env — see .env.example}"
|
|
27
|
-
PUBLIC_URL: ${PUBLIC_URL:-http://localhost:8000}
|
|
28
|
-
EMBEDDING_ENABLED: ${EMBEDDING_ENABLED:-true}
|
|
29
|
-
RERANK_MODE: ${RERANK_MODE:-blended}
|
|
30
|
-
MEMORY_ENABLED: ${MEMORY_ENABLED:-true}
|
|
31
|
-
MEMORY_DIR: ${MEMORY_DIR:-About Me}
|
|
32
|
-
LOG_LEVEL: ${LOG_LEVEL:-info}
|
|
33
|
-
LOG_DIR: ${LOG_DIR:-}
|
|
34
|
-
LOG_RETENTION_DAYS: ${LOG_RETENTION_DAYS:-30}
|
|
35
|
-
TZ: ${TZ:-UTC}
|
|
36
|
-
# Windows: set WINDOWS_MODE=true in .env when your vault is on a C: drive
|
|
37
|
-
# (polling watcher + rename-based moves across the Docker Desktop/WSL2 bridge).
|
|
38
|
-
WINDOWS_MODE: ${WINDOWS_MODE:-false}
|
|
39
|
-
# Optional overrides. When unset, the server applies smart defaults
|
|
40
|
-
# (<MEMORY_DIR> below is the resolved MEMORY_DIR value, default "About Me"):
|
|
41
|
-
# PROTECTED_PATHS default: "<MEMORY_DIR>, Daily Notes" (blocked from vault_delete_note)
|
|
42
|
-
# ORPHAN_EXCLUDE_FOLDERS default: "Daily Notes, Templates, <MEMORY_DIR>" (excluded from vault_find_orphans)
|
|
43
|
-
# SERVICE_DOCUMENTATION_URL default: https://github.com/aliasunder/vault-cortex
|
|
44
|
-
PROTECTED_PATHS: ${PROTECTED_PATHS:-}
|
|
45
|
-
ORPHAN_EXCLUDE_FOLDERS: ${ORPHAN_EXCLUDE_FOLDERS:-}
|
|
46
|
-
SERVICE_DOCUMENTATION_URL: ${SERVICE_DOCUMENTATION_URL:-}
|
|
47
|
-
volumes:
|
|
48
|
-
- "${VAULT_PATH:?Set VAULT_PATH to your Obsidian vault folder}:/vault:rw"
|
|
49
|
-
- mcp_data:/data
|
|
50
|
-
ports:
|
|
51
|
-
- "${PORT:-8000}:8000"
|
|
52
|
-
healthcheck:
|
|
53
|
-
test:
|
|
54
|
-
[
|
|
55
|
-
"CMD",
|
|
56
|
-
"node",
|
|
57
|
-
"-e",
|
|
58
|
-
"fetch('http://127.0.0.1:8000/healthz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))",
|
|
59
|
-
]
|
|
60
|
-
interval: 15s
|
|
61
|
-
timeout: 5s
|
|
62
|
-
retries: 3
|
|
63
|
-
start_period: 20s
|
|
64
|
-
|
|
65
|
-
volumes:
|
|
66
|
-
mcp_data:
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
# vault-cortex — remote quickstart (Obsidian Sync)
|
|
2
|
-
#
|
|
3
|
-
# Run vault-cortex on a VPS with Obsidian Sync for remote access from any device.
|
|
4
|
-
# One container: the :remote image bundles Obsidian Sync and the MCP server
|
|
5
|
-
# under s6-overlay supervision.
|
|
6
|
-
#
|
|
7
|
-
# 1. cp .env.example .env (then fill in required values)
|
|
8
|
-
# 2. docker compose up -d
|
|
9
|
-
# 3. Connect your MCP client to http://<your-server>:8000/mcp
|
|
10
|
-
#
|
|
11
|
-
# Compose is optional — the same container runs with plain `docker run`
|
|
12
|
-
# (or podman/nerdctl); see the README for the one-liner.
|
|
13
|
-
#
|
|
14
|
-
# Full docs: https://github.com/aliasunder/vault-cortex
|
|
15
|
-
|
|
16
|
-
name: vault-cortex
|
|
17
|
-
|
|
18
|
-
services:
|
|
19
|
-
vault-cortex:
|
|
20
|
-
image: ghcr.io/aliasunder/vault-cortex:remote
|
|
21
|
-
container_name: vault-cortex
|
|
22
|
-
hostname: ${DEVICE_NAME:-vault-cortex}
|
|
23
|
-
restart: unless-stopped
|
|
24
|
-
environment:
|
|
25
|
-
# --- Obsidian Sync (s6 init chain + sync service) ---
|
|
26
|
-
OBSIDIAN_AUTH_TOKEN: "${OBSIDIAN_AUTH_TOKEN:?Set OBSIDIAN_AUTH_TOKEN — see .env.example}"
|
|
27
|
-
VAULT_NAME: "${VAULT_NAME:?Set VAULT_NAME to your Obsidian vault name (case-sensitive)}"
|
|
28
|
-
VAULT_PASSWORD: ${VAULT_PASSWORD:-}
|
|
29
|
-
PUID: ${PUID:-1000}
|
|
30
|
-
PGID: ${PGID:-1000}
|
|
31
|
-
DEVICE_NAME: ${DEVICE_NAME:-vault-cortex}
|
|
32
|
-
CONFLICT_STRATEGY: ${CONFLICT_STRATEGY:-merge}
|
|
33
|
-
SYNC_MODE: ${SYNC_MODE:-bidirectional}
|
|
34
|
-
# --- MCP server ---
|
|
35
|
-
PORT: "8000"
|
|
36
|
-
HOST: "0.0.0.0"
|
|
37
|
-
VAULT_PATH: /vault
|
|
38
|
-
INDEX_DB_PATH: /data/index.db
|
|
39
|
-
MCP_AUTH_TOKEN: "${MCP_AUTH_TOKEN:?Set MCP_AUTH_TOKEN in .env — see .env.example}"
|
|
40
|
-
PUBLIC_URL: "${PUBLIC_URL:?Set PUBLIC_URL to your server's public URL}"
|
|
41
|
-
EMBEDDING_ENABLED: ${EMBEDDING_ENABLED:-true}
|
|
42
|
-
RERANK_MODE: ${RERANK_MODE:-blended}
|
|
43
|
-
MEMORY_ENABLED: ${MEMORY_ENABLED:-true}
|
|
44
|
-
MEMORY_DIR: ${MEMORY_DIR:-About Me}
|
|
45
|
-
LOG_LEVEL: ${LOG_LEVEL:-info}
|
|
46
|
-
LOG_DIR: ${LOG_DIR:-/data/logs}
|
|
47
|
-
LOG_RETENTION_DAYS: ${LOG_RETENTION_DAYS:-30}
|
|
48
|
-
TZ: ${TZ:-UTC}
|
|
49
|
-
WINDOWS_MODE: ${WINDOWS_MODE:-false}
|
|
50
|
-
# Optional overrides. When unset, the server applies smart defaults
|
|
51
|
-
# (<MEMORY_DIR> below is the resolved MEMORY_DIR value, default "About Me"):
|
|
52
|
-
# PROTECTED_PATHS default: "<MEMORY_DIR>, Daily Notes" (blocked from vault_delete_note)
|
|
53
|
-
# ORPHAN_EXCLUDE_FOLDERS default: "Daily Notes, Templates, <MEMORY_DIR>" (excluded from vault_find_orphans)
|
|
54
|
-
# SERVICE_DOCUMENTATION_URL default: https://github.com/aliasunder/vault-cortex
|
|
55
|
-
PROTECTED_PATHS: ${PROTECTED_PATHS:-}
|
|
56
|
-
ORPHAN_EXCLUDE_FOLDERS: ${ORPHAN_EXCLUDE_FOLDERS:-}
|
|
57
|
-
SERVICE_DOCUMENTATION_URL: ${SERVICE_DOCUMENTATION_URL:-}
|
|
58
|
-
volumes:
|
|
59
|
-
- vault_data:/vault
|
|
60
|
-
- mcp_data:/data
|
|
61
|
-
- obsidian_config:/home/obsidian/.config
|
|
62
|
-
ports:
|
|
63
|
-
- "0.0.0.0:${PORT:-8000}:8000"
|
|
64
|
-
# start_period covers the s6 init chain (Obsidian login + sync-setup)
|
|
65
|
-
# that runs before the MCP server boots.
|
|
66
|
-
healthcheck:
|
|
67
|
-
test:
|
|
68
|
-
[
|
|
69
|
-
"CMD",
|
|
70
|
-
"node",
|
|
71
|
-
"-e",
|
|
72
|
-
"fetch('http://127.0.0.1:8000/healthz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))",
|
|
73
|
-
]
|
|
74
|
-
interval: 15s
|
|
75
|
-
timeout: 5s
|
|
76
|
-
retries: 5
|
|
77
|
-
start_period: 60s
|
|
78
|
-
logging:
|
|
79
|
-
driver: json-file
|
|
80
|
-
options: { max-size: "10m", max-file: "3" }
|
|
81
|
-
|
|
82
|
-
volumes:
|
|
83
|
-
vault_data:
|
|
84
|
-
mcp_data:
|
|
85
|
-
obsidian_config:
|