@openparachute/vault 0.1.0 → 0.2.0

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.
Files changed (87) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/CLAUDE.md +2 -2
  3. package/README.md +289 -44
  4. package/core/src/core.test.ts +802 -346
  5. package/core/src/expand.ts +140 -0
  6. package/core/src/hooks.test.ts +27 -27
  7. package/core/src/hooks.ts +1 -1
  8. package/core/src/mcp.ts +102 -39
  9. package/core/src/notes.ts +82 -4
  10. package/core/src/obsidian.test.ts +11 -11
  11. package/core/src/paths.test.ts +46 -46
  12. package/core/src/schema.ts +18 -2
  13. package/core/src/store.ts +51 -51
  14. package/core/src/types.ts +29 -29
  15. package/core/src/wikilinks.test.ts +61 -61
  16. package/docs/HTTP_API.md +4 -2
  17. package/package.json +1 -1
  18. package/src/auth.test.ts +319 -0
  19. package/src/backup-launchd.test.ts +90 -0
  20. package/src/backup-launchd.ts +169 -0
  21. package/src/backup.test.ts +715 -0
  22. package/src/backup.ts +699 -0
  23. package/src/cli.ts +923 -31
  24. package/src/config.test.ts +173 -0
  25. package/src/config.ts +345 -15
  26. package/src/daemon.ts +136 -0
  27. package/src/doctor.test.ts +356 -0
  28. package/src/health.test.ts +201 -0
  29. package/src/health.ts +115 -0
  30. package/src/launchd.test.ts +91 -0
  31. package/src/launchd.ts +37 -40
  32. package/src/mcp-http.ts +1 -1
  33. package/src/mcp-tools.ts +7 -9
  34. package/src/oauth.test.ts +289 -8
  35. package/src/oauth.ts +57 -12
  36. package/src/published.test.ts +21 -21
  37. package/src/routes.ts +152 -70
  38. package/src/routing.test.ts +347 -0
  39. package/src/routing.ts +365 -0
  40. package/src/server.ts +7 -278
  41. package/src/systemd.test.ts +15 -0
  42. package/src/systemd.ts +18 -11
  43. package/src/triggers.test.ts +7 -7
  44. package/src/triggers.ts +6 -6
  45. package/src/vault-store.ts +20 -3
  46. package/src/vault.test.ts +356 -262
  47. package/.claude/settings.local.json +0 -31
  48. package/.playwright-mcp/console-2026-04-14T04-17-25-395Z.log +0 -2
  49. package/.playwright-mcp/console-2026-04-14T04-18-11-767Z.log +0 -1
  50. package/.playwright-mcp/console-2026-04-14T04-19-07-733Z.log +0 -2
  51. package/.playwright-mcp/console-2026-04-14T04-20-45-440Z.log +0 -2
  52. package/.playwright-mcp/page-2026-04-14T04-17-25-536Z.yml +0 -1
  53. package/.playwright-mcp/page-2026-04-14T04-18-11-816Z.yml +0 -1
  54. package/.playwright-mcp/page-2026-04-14T04-18-31-674Z.yml +0 -211
  55. package/.playwright-mcp/page-2026-04-14T04-19-07-795Z.yml +0 -59
  56. package/.playwright-mcp/page-2026-04-14T04-19-36-239Z.yml +0 -232
  57. package/.playwright-mcp/page-2026-04-14T04-19-58-327Z.yml +0 -182
  58. package/.playwright-mcp/page-2026-04-14T04-20-10-517Z.yml +0 -91
  59. package/.playwright-mcp/page-2026-04-14T04-20-14-796Z.yml +0 -70
  60. package/.playwright-mcp/page-2026-04-14T04-20-45-509Z.yml +0 -59
  61. package/religions-abrahamic-filter.png +0 -0
  62. package/religions-buddhism-v2.png +0 -0
  63. package/religions-buddhism.png +0 -0
  64. package/religions-final.png +0 -0
  65. package/religions-v1.png +0 -0
  66. package/religions-v2.png +0 -0
  67. package/religions-zen.png +0 -0
  68. package/web/README.md +0 -73
  69. package/web/bun.lock +0 -827
  70. package/web/eslint.config.js +0 -23
  71. package/web/index.html +0 -15
  72. package/web/package.json +0 -36
  73. package/web/public/favicon.svg +0 -1
  74. package/web/public/icons.svg +0 -24
  75. package/web/src/App.tsx +0 -149
  76. package/web/src/Graph.tsx +0 -200
  77. package/web/src/NoteView.tsx +0 -155
  78. package/web/src/Sidebar.tsx +0 -186
  79. package/web/src/api.ts +0 -21
  80. package/web/src/index.css +0 -50
  81. package/web/src/main.tsx +0 -10
  82. package/web/src/types.ts +0 -37
  83. package/web/src/utils.ts +0 -107
  84. package/web/tsconfig.app.json +0 -25
  85. package/web/tsconfig.json +0 -7
  86. package/web/tsconfig.node.json +0 -24
  87. package/web/vite.config.ts +0 -15
package/CHANGELOG.md ADDED
@@ -0,0 +1,80 @@
1
+ # Changelog
2
+
3
+ All notable changes to Parachute Vault are documented here.
4
+
5
+ This project loosely follows [Keep a Changelog](https://keepachangelog.com) and [Semantic Versioning](https://semver.org).
6
+
7
+ ## [0.2.0] — 2026-04-17
8
+
9
+ First tagged public release. Ships the auth, backup, and onboarding surface the project needs for first-wave users.
10
+
11
+ ### Authentication
12
+
13
+ - **OAuth 2.1 + PKCE** with Dynamic Client Registration (RFC 7591). Claude Desktop, Parachute Daily, and any OAuth-capable MCP client can connect with no manual token paste — user clicks "Add integration", browser opens to the vault's consent page, done.
14
+ - **Owner password** (bcrypt-hashed, min 12 characters) for the OAuth consent page. Prompt fires at `vault init`; manage later with `parachute vault set-password` / `--clear`.
15
+ - **TOTP 2FA with single-use backup codes**. `parachute vault 2fa enroll` prints a QR and one-time backup codes; `status` / `disable` / `backup-codes` subcommands for lifecycle.
16
+ - **Per-vault OAuth scope** — discovery at `/vaults/{name}/.well-known/oauth-authorization-server` returns vault-scoped endpoints. Tokens minted there authenticate only against that vault.
17
+ - **Cross-vault substitution blocked**: an OAuth code issued for one vault cannot be redeemed at another vault's token endpoint (schema-enforced via a `vault_name` column on `oauth_codes`).
18
+ - **Honest token response**: `/oauth/token` returns `{ access_token, token_type, scope, vault }` so the client knows which vault it just connected to.
19
+ - **Two permission tiers**: `full` (CRUD + delete + token management) and `read` (query / list / find-path / vault-info). Tokens default to `full`; pass `--read` to `tokens create` for read-only.
20
+ - **Token CLI**: `parachute vault tokens` (list), `tokens create [--vault] [--read] [--expires <N{h|d|w|m|y}>] [--label]`, `tokens revoke <id> [--vault]`. Tokens are SHA-256 hashed at rest.
21
+ - **Query-param auth for `/view`**: `?key=pvt_...` works alongside `Authorization: Bearer` and `X-API-Key` headers, convenient for browsers.
22
+
23
+ ### Backup
24
+
25
+ - **`parachute vault backup`** — one-shot snapshot: atomic `VACUUM INTO` of every vault's `vault.db`, plus `config.yaml` and each vault's `vault.yaml`, bundled as a timestamped `.tar.gz`. Safe under concurrent reads/writes.
26
+ - **Scheduled runs** via `parachute vault backup --schedule hourly|daily|weekly|manual` (macOS launchd). Linux systemd-timer support is a follow-up; wire cron yourself for now.
27
+ - **`backup status`** shows schedule, last run, destinations, next run, and per-destination tier breakdown.
28
+ - **Tiered (grandfather-father-son) retention**. Default: `daily: 7 / weekly: 4 / monthly: 12 / yearly: null` (unbounded). Set any tier to `0` to disable. Local-timezone bucketing.
29
+ - **Pluggable destinations**. `local` (any filesystem path — iCloud Drive, external disk, rsync/Syncthing folder) ships in 0.2.0. `s3`, `rsync`, and `cloud` destinations designed but not yet implemented.
30
+ - **`vault uninstall` tears down the backup agent too** on macOS, so scheduled backups don't keep firing on a removed install.
31
+
32
+ ### Reliability
33
+
34
+ - **`parachute vault doctor`** — diagnostic suite covering server-path pointer, wrapper script, launchd agent (macOS) / systemd service (Linux), bun-on-PATH, MCP entry in `~/.claude.json` (presence + URL port match + reachability), port-collision (free / ours / foreign via `lsof` or `ss`), and — when scheduled backups are configured — backup agent + per-destination writability. Exits 1 on any `fail`.
35
+ - **`vault status`** is healthcheck-aware and reports live daemon state, not just service registration.
36
+ - **`vault restart`** blocks until `/health` returns 200, with a sensible budget and progress indicator.
37
+ - **Path-resilient `start.sh`** — the wrapper launchd/systemd executes embeds an absolute `bun` path + points at `~/.parachute/server-path`, which resolves to the current repo location. Move the repo, re-run `vault init`, and the daemon follows you.
38
+ - **Idempotent `vault init`** — safe to re-run after a folder move or config edit; refreshes the pointer, wrapper, and service registration without touching user data.
39
+ - **Graceful shutdown**: in-flight webhook triggers get a 5 s drain window before the daemon exits on SIGTERM/SIGINT.
40
+
41
+ ### Multi-vault
42
+
43
+ - **Public `GET /vaults/list`** — unauthenticated discovery endpoint returning only vault names (no descriptions, timestamps, counts, or keys). Lets a client populate a vault picker before OAuth. Operators who want to hide vault existence can set `discovery: disabled` in `~/.parachute/config.yaml` to make the endpoint return 404.
44
+ - **Single-vault auto-default** — when the server has exactly one vault, the unscoped `/mcp`, `/api/*`, and `/oauth/*` paths transparently resolve to it regardless of its name. A lone vault named `journal` works at `/mcp` with no vault-in-URL needed.
45
+ - **Vault-management CLI**: `parachute vault create <name>`, `list` (alias `ls`), `remove <name> --yes` (alias `rm`).
46
+ - **Automatic `default_vault` management** — `vault create` promotes a new vault to default when none is set or the configured default points at a missing vault. `vault remove` promotes the sole survivor when you delete the default and one vault remains.
47
+
48
+ ### Install / uninstall
49
+
50
+ - **`vault uninstall`** — removes the daemon registration, the `start.sh` wrapper, the `~/.parachute/server-path` pointer, and the `parachute-vault` entry in `~/.claude.json`. On macOS, tears down both the main vault agent and the backup agent. Preserves all user data.
51
+ - **`vault uninstall --wipe`** — additionally removes `vaults/`, `.env`, `config.yaml`, `vault.log`, and `vault.err` after a second interactive confirm (default NO).
52
+ - **`vault uninstall --yes --wipe`** — scripted destructive path. Skips both confirms and prints an ISO-timestamped audit line to stdout naming the target paths.
53
+ - **`vault url`** prints the local server URL in a script-friendly form.
54
+
55
+ ### API / primitives
56
+
57
+ - **Optimistic concurrency on `update-note`** via an `if_updated_at` parameter. When supplied and it doesn't match the note's current `updated_at`, the update is rejected (MCP: `ConflictError`; HTTP: 409). Batch updates fail fast on the first conflict.
58
+ - **Link expansion on `query-notes`** — new `expand_links` / `expand_depth` (0–3) / `expand_mode` (`"full"` | `"summary"`) parameters inline `[[wikilink]]` targets directly into the returned content. Works on the MCP tool and the HTTP routes (single-note, search, and structured-list).
59
+ - **9 composable MCP tools** (was 30): `query-notes`, `create-note`, `update-note`, `delete-note`, `list-tags`, `update-tag`, `delete-tag`, `find-path`, `vault-info`. Every note parameter accepts either an ID or a path.
60
+ - **Webhook triggers** — declarative config-driven webhooks fire on note mutations matching tag / metadata predicates. Three send modes: `json` (general), `attachment` (Whisper-compatible transcription), `content` (OpenAI-compatible TTS).
61
+
62
+ ### Documentation
63
+
64
+ - Entirely overhauled onboarding path: OAuth walkthrough, doctor + troubleshooting, first-run narrative (what `vault init` does on disk), multi-vault subsection, Tailscale Funnel walkthrough, prerequisites block.
65
+ - Honest token-shape documentation (`pvt_` is modern; `pvk_` is legacy and still accepted).
66
+ - README tells the truth about what `vault init` writes to `~/.claude.json` — a vault-scoped URL with a baked-in `pvt_` bearer, not OAuth.
67
+
68
+ ### Removed
69
+
70
+ - **Semantic / vector search** — the embeddings path (`sqlite-vec`, `semantic-search` tool, embedding-provider setup wizard, `/api/ingest` endpoint). Full-text search via `query-notes` `search=` remains.
71
+ - **`parachute vault keys` subcommand** — superseded by `parachute vault tokens`. Legacy `pvk_...` keys in `config.yaml` are still honored at runtime.
72
+
73
+ ### For contributors
74
+
75
+ - **Async `Store` interface**, renamed to `BunSqliteStore`. Paves the way for Durable Object SQLite and R2 blob backends (in flight).
76
+ - **`src/routing.ts`** extracted from `src/server.ts` so the request dispatcher is unit-testable without spinning up `Bun.serve()`.
77
+ - **`core/src/test-preload.ts`** isolates `PARACHUTE_HOME` for tests so `bun test` never touches a user's real `~/.parachute/`.
78
+ - Test suite at release cut: **538 passing / 0 failing / 3 skipped** across 22 files (541 tests total).
79
+
80
+ [0.2.0]: https://github.com/ParachuteComputer/parachute-vault/releases/tag/v0.2.0
package/CLAUDE.md CHANGED
@@ -6,9 +6,9 @@ Agent-native knowledge graph. Notes, tags, links over MCP. Self-hosted, one comm
6
6
 
7
7
  ```
8
8
  parachute vault init → ~/.parachute/ (config, .env, daemon, MCP)
9
- parachute vault create → new vault (SQLite DB + vault.yaml + API key)
9
+ parachute vault create → new vault (SQLite DB + vault.yaml + pvt_ token)
10
10
  parachute vault config → manage env vars (PORT, etc.)
11
- parachute vault keys → list / create / revoke API keys
11
+ parachute vault tokens → list / create / revoke per-vault tokens
12
12
 
13
13
  CLI → Bun server (port 1940) → multiple vaults (each its own SQLite DB)
14
14
 
package/README.md CHANGED
@@ -1,14 +1,16 @@
1
1
  # Parachute Vault
2
2
 
3
- A self-hosted knowledge graph for AI agents. Notes, tags, links exposed over MCP. Any AI gets a personal knowledge vault in one command.
3
+ **Parachute Vault is a self-hosted knowledge graph that any AI can read and write, over the open [MCP](https://modelcontextprotocol.io) protocol.** Your notes, tags, links, and attachments live on your machine in plain SQLite databases under `~/.parachute/`, not in a vendor's cloud.
4
+
5
+ Works with Claude, ChatGPT, Gemini, or any future MCP-capable AI. Switch tools without losing your knowledge. No vendor lock-in, no re-import step when the next model lands. One command to install; one OAuth consent to connect each AI client.
4
6
 
5
7
  ## Quick start
6
8
 
7
- Requires [Bun](https://bun.sh) (`curl -fsSL https://bun.sh/install | bash`).
9
+ Requires [Bun](https://bun.sh) (`curl -fsSL https://bun.sh/install | bash`) and macOS 13+ or Linux. No root needed — see [Requirements](#requirements) below for specifics.
8
10
 
9
11
  ```bash
10
12
  # Install globally (registers the `parachute` CLI)
11
- bun add -g github:ParachuteComputer/parachute-vault
13
+ bun add -g @openparachute/vault
12
14
  parachute vault init
13
15
 
14
16
  # Or clone and run directly
@@ -35,17 +37,167 @@ A server on port 1940 with:
35
37
 
36
38
  Each vault is its own SQLite database. Run multiple vaults on one server.
37
39
 
40
+ ## What `vault init` does
41
+
42
+ A mental model for "where is my data?" and "what can I poke at?" after the one-command setup.
43
+
44
+ ### On disk — `~/.parachute/`
45
+
46
+ ```
47
+ ~/.parachute/
48
+ config.yaml # global config — port, default_vault, owner password hash,
49
+ # TOTP secret, backup-codes hashes, backup schedule. 0600.
50
+ .env # runtime env vars (PORT=1940 by default; any webhook API
51
+ # keys you add later). Sourced by the daemon wrapper.
52
+ vault.log # stdout of the running daemon (tail via `parachute vault logs`)
53
+ vault.err # stderr of the running daemon
54
+ server-path # text file: absolute path to the repo's src/server.ts —
55
+ # how the daemon wrapper finds the source after you move it
56
+ start.sh # the wrapper launchd/systemd execs. Knows the absolute
57
+ # path to `bun` so a later PATH change doesn't break the daemon
58
+ assets/ # legacy top-level uploads dir (attachments now land per-vault)
59
+ vaults/ # one subdirectory per vault
60
+ default/
61
+ vault.db # the SQLite database — notes, tags, links, attachments,
62
+ # per-vault tokens, OAuth clients + codes, tag schemas
63
+ vault.yaml # per-vault config — description (sent as MCP session
64
+ # instruction), published_tag override, legacy api_keys
65
+ assets/ # per-vault uploaded attachments (audio, images)
66
+ ```
67
+
68
+ `config.yaml` is the one file written at 0600 because it holds the bcrypt owner-password hash and the plaintext TOTP secret. `.env` is written with your umask default (typically 0644); if you add webhook API keys there, tighten the mode yourself. SQLite DBs follow your umask.
69
+
70
+ ### Registered externally
71
+
72
+ - **macOS**: a launchd user agent labelled `computer.parachute.vault` (plus `computer.parachute.vault.backup` if you ran `vault backup --schedule`).
73
+ - **Linux + systemd**: a user service named `parachute-vault.service` (managed via `systemctl --user`).
74
+ - **Neither of the above**: `vault init` prints a reminder to start the server yourself (`bun src/server.ts` or Docker). No service registration.
75
+
76
+ The daemon binds `0.0.0.0:1940` (or whatever you set in `PORT`) and serves REST, MCP, and OAuth routes. `parachute vault status` is the fast check; `parachute vault url` prints just the URL for use in scripts.
77
+
78
+ ### `~/.claude.json`
79
+
80
+ `vault init` adds one entry — `mcpServers["parachute-vault"]` — pointing at `http://127.0.0.1:<port>/vaults/<default-vault>/mcp` with a baked-in `Authorization: Bearer pvt_...` header. Next Claude Code session picks it up; there's no further wiring. See [Connecting a client](#connecting-a-client) for rotating that token or pointing it elsewhere.
81
+
82
+ ### Your API token
83
+
84
+ The `pvt_...` token printed at init is the one baked into `~/.claude.json`. It's not stored anywhere retrievable — save it if you need it for `curl`, cron, or any other script. Lost it? Just mint a new one: `parachute vault tokens create`. Tokens are SHA-256 hashed at rest in each vault's `vault.db`.
85
+
86
+ ### Owner password prompt
87
+
88
+ Init pauses for one interactive prompt: "Set an owner password for OAuth consent?" The password is what the consent page asks for when Claude Desktop / Parachute Daily / any browser-OAuth client connects. You can skip it and set it later with `parachute vault set-password`; without it, the consent page falls back to pasting a vault token. See [Connecting a client → Owner password](#owner-password-needed-for-oauth).
89
+
90
+ ## Connecting a client
91
+
92
+ Two ways to authenticate — pick based on the client, not the deployment:
93
+
94
+ | Path | When to use | User action |
95
+ |---|---|---|
96
+ | **OAuth 2.1 + PKCE (browser flow)** | Claude Desktop, Parachute Daily, any third-party MCP client set up interactively | Click "Add integration", enter server URL, a browser opens to the vault's consent page, you enter the owner password, done — no token ever touches your clipboard |
97
+ | **Bearer token** | Claude Code (auto-wired by `vault init`), CLI scripts, cron jobs, any non-interactive caller | `curl -H "Authorization: Bearer pvt_..."` — the token is printed once at `vault init` (save it) or minted on demand with `parachute vault tokens create` |
98
+
99
+ Both paths end up with the same kind of token in the vault's DB — a `pvt_` string, scoped to one vault and one permission level (`full` or `read`). OAuth just moves the "how does the client get that token" step from "human copy-pastes it" to "browser-based handshake with the owner's consent."
100
+
101
+ ### Owner password (needed for OAuth)
102
+
103
+ `vault init` prompts you to set an owner password (minimum 12 characters). This is what the OAuth consent page asks for when a client requests access. If you skip the prompt, OAuth still works but the consent page falls back to asking for a vault token instead — functional but clunky. Set it later with:
104
+
105
+ ```bash
106
+ parachute vault set-password # set / change
107
+ parachute vault set-password --clear # remove (reverts to token fallback)
108
+ parachute vault 2fa enroll # optional: add TOTP 2FA on top
109
+ ```
110
+
111
+ Password and 2FA secrets live in `~/.parachute/config.yaml` at mode 0600 (bcrypt hash + base32 TOTP secret).
112
+
113
+ ### Claude Code
114
+
115
+ `vault init` fully auto-configures `~/.claude.json` — there's nothing else to do. The entry it writes uses a baked-in `pvt_` token rather than OAuth:
116
+
117
+ ```json
118
+ {
119
+ "mcpServers": {
120
+ "parachute-vault": {
121
+ "type": "http",
122
+ "url": "http://127.0.0.1:1940/vaults/{name}/mcp",
123
+ "headers": { "Authorization": "Bearer pvt_..." }
124
+ }
125
+ }
126
+ }
127
+ ```
128
+
129
+ Where `{name}` is `default` on a fresh install, or whatever vault you pointed `vault init` at. **First MCP call after `vault init` requires no browser handoff — Claude Code uses the baked-in token and the vault's tools show up in your next session.** This is intentional: for an owner connecting their own machine's vault to their own Claude Code, the token is already there and OAuth would add friction.
130
+
131
+ To re-point Claude Code at a different vault, change `default_vault` in `~/.parachute/config.yaml` and re-run `parachute vault init` — which re-mints an API token and re-writes the `~/.claude.json` entry end-to-end. To rotate the token only, edit `~/.claude.json` and replace the `Authorization` header value with a fresh token from `parachute vault tokens create`. (Running `parachute vault mcp-install` on its own overwrites the MCP entry *without* an `Authorization` header and is intended for the rare case where you want to drop the token and connect via OAuth instead.)
132
+
133
+ ### Claude Desktop (OAuth)
134
+
135
+ For Claude Desktop — or any install where the server is on a different machine from the client — use the browser-based OAuth flow:
136
+
137
+ 1. Claude Desktop → Settings → Integrations → Add MCP server.
138
+ 2. Enter the URL: `https://vault.yourdomain.com/vaults/{name}/mcp` (replace `{name}`, or use the unscoped `https://vault.yourdomain.com/mcp` on a single-vault deployment). **Do not paste a bearer token** — leave the auth field empty.
139
+ 3. An OAuth-capable MCP client discovers the vault's authorization server at `/.well-known/oauth-authorization-server`, registers itself via Dynamic Client Registration (RFC 7591), and opens your browser to the vault's consent page.
140
+ 4. Enter your owner password (plus TOTP code / backup code if 2FA is enabled), pick a scope (`full` or `read`), click Authorize.
141
+ 5. Browser redirects back. The connection is live. The client now holds a `pvt_` token scoped to this vault.
142
+
143
+ If you'd rather skip OAuth — e.g. you're scripting the setup — Claude Desktop also accepts a bearer token via the integration's auth header field. Use a token from `parachute vault tokens create` (or the one from `vault init` if you still have it). This is the "manual bearer" fallback; OAuth is the recommended path.
144
+
145
+ ### Parachute Daily (mobile)
146
+
147
+ Daily uses the same OAuth flow. On first launch: enter the server URL, pick the vault from the drop-down (populated from the public `GET /vaults/list` endpoint), tap **Connect to Vault**. The same consent-page handoff runs in your phone's browser, then redirects back to the app via the `parachute://oauth/callback` deep link. The app stores the `pvt_` token in platform secure storage.
148
+
149
+ ### Multi-vault
150
+
151
+ One server, many vaults. Each vault is its own SQLite DB with its own MCP endpoint, its own OAuth, and its own tokens.
152
+
153
+ ```bash
154
+ parachute vault create work # new vault named "work"
155
+ parachute vault list # show all vaults on this server
156
+ parachute vault remove work --yes
157
+ ```
158
+
159
+ **The default vault is managed for you.** `vault init` creates `default` on first install and records it as `default_vault` in `~/.parachute/config.yaml`. `vault create <name>` promotes the newly-created vault to default when no default exists or when the configured default points at a missing vault. `vault remove <name>` promotes the sole survivor when you delete the default and one vault remains; if multiple remain after removing the default, it clears the setting and tells you to edit `config.yaml` yourself. There is no `vault set-default` subcommand — to point the server at a different existing vault, edit the `default_vault:` line in `~/.parachute/config.yaml` and `parachute vault restart`.
160
+
161
+ **Single-vault rule.** When the server has exactly one vault, the unscoped `/oauth/*` and `/mcp` paths transparently resolve to it — regardless of its name. A lone vault named `journal` works at `https://vault.example.com/mcp` with no vault-in-URL needed.
162
+
163
+ **Multi-vault rule.** When the server has two or more vaults, always use the vault-scoped path (`/vaults/{name}/mcp`, `/vaults/{name}/oauth/authorize`). OAuth tokens minted there are scoped to that vault alone — cross-vault substitution is enforced at the OAuth layer: an auth code minted for one vault cannot be redeemed at another vault's token endpoint.
164
+
165
+ **Listing vaults from a client.** The authenticated `GET /vaults` endpoint returns full vault metadata. The public `GET /vaults/list` endpoint returns names only, no metadata, no auth required — this is what Parachute Daily's vault picker calls before the user authenticates. Operators who want to hide the vault list from unauthenticated callers can set `discovery: disabled` in `~/.parachute/config.yaml` to make `/vaults/list` return 404.
166
+
38
167
  ## CLI
39
168
 
40
169
  ```bash
41
170
  # Setup
42
- parachute vault init # one-command setup
171
+ parachute vault init # one-command setup (idempotent — safe to re-run)
43
172
  parachute vault status # check what's running
173
+ parachute vault doctor # diagnose install/config issues (see Troubleshooting)
174
+ parachute vault url # print the local server URL (for scripts)
175
+ parachute vault uninstall # remove daemon + MCP entry; keeps user data
176
+ parachute vault uninstall --wipe # ...and also remove vaults, .env, config.yaml, logs
177
+ parachute vault uninstall --yes --wipe # scripted destructive wipe (prints an audit line)
44
178
 
45
179
  # Vaults
46
180
  parachute vault create work # create a new vault
47
- parachute vault list # list all vaults
48
- parachute vault remove work --yes # delete a vault
181
+ parachute vault list # list all vaults (alias: `ls`)
182
+ parachute vault remove work --yes # delete a vault (alias: `rm`)
183
+ parachute vault mcp-install # (re)write the ~/.claude.json MCP entry for the default vault
184
+
185
+ # OAuth — owner password + 2FA
186
+ parachute vault set-password # set/change the owner password (OAuth consent page)
187
+ parachute vault set-password --clear # remove the owner password (falls back to vault-token auth)
188
+ parachute vault 2fa status # show 2FA state + remaining backup codes
189
+ parachute vault 2fa enroll # enroll TOTP (shows QR + prints one-time backup codes)
190
+ parachute vault 2fa disable # disable 2FA (requires password or TOTP/backup code)
191
+ parachute vault 2fa backup-codes # regenerate backup codes (invalidates the old set)
192
+
193
+ # Tokens
194
+ parachute vault tokens # list all tokens across all vaults
195
+ parachute vault tokens create # full-access token in the default vault
196
+ parachute vault tokens create --vault work # ...in a specific vault
197
+ parachute vault tokens create --read # read-only token
198
+ parachute vault tokens create --expires 30d # expiring token (N{h|d|w|m|y})
199
+ parachute vault tokens create --label mobile # labeled token
200
+ parachute vault tokens revoke <token-id> # revoke (default vault; add --vault to target)
49
201
 
50
202
  # Obsidian
51
203
  parachute vault import ~/Obsidian/MyVault # import into default vault
@@ -53,18 +205,20 @@ parachute vault import ~/Obsidian/Work --vault work # import into a specific
53
205
  parachute vault import <path> --dry-run # preview without importing
54
206
  parachute vault export ./output --vault work # export a specific vault
55
207
 
56
- # Tokens
57
- parachute vault tokens # list all tokens
58
- parachute vault tokens create --vault work # new full-access token
59
- parachute vault tokens create --vault work --read # read-only token
60
- parachute vault tokens create --vault work --expires 30d # token with expiry
61
- parachute vault tokens create --vault work --label mobile # labeled token
62
- parachute vault tokens revoke <token-id> --vault work # revoke a token
63
-
64
208
  # Config
65
- parachute vault config # show all options
66
- parachute vault config set KEY value # set a config value
67
- parachute vault restart # apply changes
209
+ parachute vault config # show current configuration
210
+ parachute vault config set KEY value # set an env var (e.g. PORT=1940)
211
+ parachute vault config unset KEY # remove an env var
212
+ parachute vault restart # apply config changes (bounces the daemon)
213
+
214
+ # Server
215
+ parachute vault serve # run the server in the foreground (no daemon)
216
+ parachute vault logs # stream vault.log + vault.err (tail -f)
217
+
218
+ # Backup
219
+ parachute vault backup # one-shot backup to configured destinations
220
+ parachute vault backup --schedule daily # hourly | daily | weekly | manual (macOS launchd)
221
+ parachute vault backup status # schedule, last run, destinations, next run
68
222
  ```
69
223
 
70
224
  ## MCP tools (9)
@@ -164,12 +318,57 @@ triggers:
164
318
 
165
319
  Webhook servers (scribe, narrate) are stateless — they don't need vault's API key.
166
320
 
321
+ ### Backing up your vault
322
+
323
+ Your vault is just SQLite DBs + a handful of YAML files under `~/.parachute/`. `parachute vault backup` snapshots everything into a single timestamped tarball, for a one-shot or a scheduled run.
324
+
325
+ ```bash
326
+ parachute vault backup # one-shot — snapshot + ship to destinations
327
+ parachute vault backup --schedule daily # register a launchd agent (macOS)
328
+ parachute vault backup --schedule manual # stop scheduled backups
329
+ parachute vault backup status # schedule, last run, destinations, next run
330
+ ```
331
+
332
+ Configure destinations in `~/.parachute/config.yaml`:
333
+
334
+ ```yaml
335
+ backup:
336
+ schedule: daily # hourly | daily | weekly | manual
337
+ retention:
338
+ daily: 7 # last 7 daily snapshots
339
+ weekly: 4 # last-of-week for 4 weeks
340
+ monthly: 12 # last-of-month for 12 months
341
+ yearly: null # last-of-year, unbounded (null = keep every year forever)
342
+ destinations:
343
+ - kind: local
344
+ path: ~/Library/Mobile Documents/com~apple~CloudDocs/parachute-backups
345
+ ```
346
+
347
+ **Retention is tiered** (grandfather / father / son). After each run, the pruner keeps the union of four tiers:
348
+
349
+ | Tier | What it keeps |
350
+ |---------|--------------------------------------------------------|
351
+ | daily | The N most recent snapshots. |
352
+ | weekly | The last snapshot of each of the last N ISO weeks. |
353
+ | monthly | The last snapshot of each of the last N calendar months.|
354
+ | yearly | The last snapshot of each year — `null` means unbounded.|
355
+
356
+ A snapshot that qualifies for multiple tiers is kept once. Set any tier to `0` to disable it; sparse data (days without a backup) just means some tiers contribute nothing that day. Bucketing uses your local timezone, so calendars line up with what you see, not UTC.
357
+
358
+ **What's in a snapshot**: atomic `VACUUM INTO` copies of every `vaults/<name>/vault.db`, your `config.yaml`, and each vault's `vault.yaml`, bundled as `parachute-backup-<timestamp>.tar.gz`. Safe under concurrent reads/writes — no need to stop the daemon.
359
+
360
+ **Restore**: extract the tarball into a fresh `~/.parachute/` and run `parachute vault init` to re-register the daemon. The DBs and configs drop in place; you don't need any special restore command (for now — a dedicated `vault restore` is coming soon).
361
+
362
+ Destination kinds shipping in this release: `local` (any filesystem path — including iCloud Drive, a mounted external disk, or an rsync/Syncthing-backed folder). `s3`, `rsync`, and `cloud` destinations are planned but not yet implemented.
363
+
364
+ On Linux, scheduled runs via systemd timers are a follow-up; for now `parachute vault backup` works on Linux but you'll need to wire the cron yourself.
365
+
167
366
  ### View endpoint
168
367
 
169
368
  Serve notes as clean HTML pages at `/view/:noteId`:
170
369
 
171
370
  - **Without auth**: only serves notes tagged `published` (or with `metadata.published: true`). Returns 404 for unpublished notes.
172
- - **With auth**: serves any note. Pass API key via `Authorization: Bearer pvk_...` header or `?key=pvk_...` query param.
371
+ - **With auth**: serves any note. Pass your token via `Authorization: Bearer pvt_...` header or `?key=pvt_...` query param.
173
372
  - **Custom tag**: set `published_tag` in vault.yaml to use a different tag name (default: `publish`).
174
373
 
175
374
  ```yaml
@@ -212,11 +411,14 @@ Metadata is a JSON column. Vaults start blank — no predefined tags or schema.
212
411
 
213
412
  **All API and MCP requests require a valid API key.** No exceptions — localhost gets no special treatment.
214
413
 
215
- `vault init` generates an API key automatically and configures Claude Code's MCP with it.
414
+ For wiring up an AI client (Claude Code, Claude Desktop, Parachute Daily), see [Connecting a client](#connecting-a-client) above. This section covers token-level details: how to pass a key, how to manage tokens, and which endpoints are public by design (`/health`, published notes at `/view/:id`).
216
415
 
217
416
  ### Passing the key
218
417
 
219
- Both legacy API keys (`pvk_...`) and scoped tokens (`pvt_...`) work interchangeably:
418
+ Tokens come in two shapes. Both work interchangeably at every authenticated endpoint:
419
+
420
+ - `pvt_...` — per-vault scoped tokens (the modern format; what `vault init` mints, what OAuth issues, what `parachute vault tokens create` produces)
421
+ - `pvk_...` — legacy global API keys from `config.yaml` (still honored for existing deployments)
220
422
 
221
423
  ```bash
222
424
  # Header (preferred)
@@ -229,26 +431,6 @@ curl -H "X-API-Key: pvt_..." http://localhost:1940/api/notes
229
431
  curl http://localhost:1940/view/noteId?key=pvt_...
230
432
  ```
231
433
 
232
- ### Claude Desktop
233
-
234
- Settings → Integrations → Add MCP → URL: `https://vault.yourdomain.com/mcp`, Header: `Authorization: Bearer pvk_...`
235
-
236
- ### Claude Code
237
-
238
- `vault init` auto-configures `~/.claude.json`. To set manually:
239
-
240
- ```json
241
- {
242
- "mcpServers": {
243
- "parachute-vault": {
244
- "type": "http",
245
- "url": "http://127.0.0.1:1940/mcp",
246
- "headers": { "Authorization": "Bearer pvk_..." }
247
- }
248
- }
249
- }
250
- ```
251
-
252
434
  ### Token management
253
435
 
254
436
  Per-vault tokens with two permission levels:
@@ -294,6 +476,37 @@ For remote access, always use a TLS-terminating proxy:
294
476
  | Direct LAN IP (no TLS) | Plaintext on WiFi | Avoid |
295
477
  | Direct internet (no TLS) | Plaintext on internet | Never do this |
296
478
 
479
+ ## Troubleshooting
480
+
481
+ ### `parachute vault doctor` is your first stop
482
+
483
+ `doctor` inspects the install and prints one line per check with a status (`✓` pass, `!` warn, `✗` fail) and, when relevant, a suggested fix. It exits 1 on any `fail` and 0 otherwise. Run it any time something feels off.
484
+
485
+ The checks, in the order they're emitted:
486
+
487
+ | Check | What it verifies | Typical fix when failing |
488
+ |---|---|---|
489
+ | server-path pointer | `~/.parachute/server-path` exists, is non-empty, and points at a `src/server.ts` that actually exists. This is where the stale-path failure after a repo move shows up first. | `parachute vault init` from the current repo location. |
490
+ | wrapper script | `~/.parachute/start.sh` exists. Without it, launchd / systemd has nothing to exec. | `parachute vault init`. |
491
+ | launchd agent (macOS) / systemd service (Linux) | The daemon is registered and loaded/active. On Linux without systemd, the check is silently skipped. | `parachute vault restart` or re-run `vault init`. |
492
+ | bun on PATH | `bun` is resolvable via your shell's PATH. Not required once the daemon is installed (`start.sh` embeds an absolute bun path at init time) but missing bun is the #1 first-time-user failure. | `curl -fsSL https://bun.sh/install \| bash` and restart the shell. |
493
+ | MCP entry in `~/.claude.json` | An entry is present. When it is, two follow-ups: the URL's port matches the running vault's port, and the MCP URL is reachable over HTTP (any response — even 401 — counts as reachable). | `parachute vault mcp-install` to rewrite the entry, or `parachute vault restart` if the daemon is down. |
494
+ | port `1940` availability | Probes via `lsof` / `ss` and classifies: free, held by our daemon (pass), held by a foreign process (warn), or unknown (tool unavailable → check silently omitted). | Stop the conflicting process, or set a different `PORT` in `~/.parachute/.env` and re-run `vault init`. |
495
+ | backup agent (macOS, only when `backup.schedule != manual`) | The scheduled-backup launchd agent is loaded. | `parachute vault backup --schedule <hourly\|daily\|weekly>` to reinstall the agent. |
496
+ | backup destinations (only when `backup.schedule != manual`) | At least one destination is configured; each configured destination is writable. | Edit `~/.parachute/config.yaml` under `backup.destinations`, or fix the path's permissions. |
497
+
498
+ ### Common failure modes
499
+
500
+ - **Daemon won't start after a port change.** `~/.parachute/.env` has the new `PORT=...` but the daemon is still trying to bind the old one, or something else already holds the new port. `parachute vault doctor` surfaces both conditions. Fix the holder (or pick a different port) and `parachute vault restart`.
501
+ - **MCP entry is stale after moving the repo.** launchd/systemd keeps pointing at the old path. `doctor` flags this as a failed `server.ts at pointer target` check; `parachute vault init` from the new location rewrites the pointer, wrapper, and daemon registration.
502
+ - **Claude Code shows no vault tools.** Check in order: (1) is the daemon up (`parachute vault status`)? (2) does `~/.claude.json` have a `parachute-vault` entry with both `url` and a valid `Authorization` header? (3) does the URL's vault name match an existing vault? `parachute vault doctor` catches the first two. A missing or stale `Authorization` header after a bare `vault mcp-install` is the usual culprit for #2 — see the Claude Code section of [Connecting a client](#connecting-a-client) for how to rewrite it.
503
+ - **Claude Desktop / Daily won't connect via OAuth.** If the owner-password prompt was skipped at `vault init`, the consent page falls back to requiring a vault token in place of the password (functional but clunky). Set one now with `parachute vault set-password`. If 2FA is enrolled, have your authenticator app ready before starting the flow; lost TOTP access recovers via the backup codes printed at enrollment.
504
+ - **Scheduled backups aren't running.** On macOS: `doctor` flags `backup agent: not loaded` when `schedule` isn't `manual` but the launchd agent is missing — rerun `parachute vault backup --schedule <freq>` to reinstall it. On Linux: systemd-timer support for backup isn't shipped yet, so `--schedule daily` silently skips the scheduler. Run `parachute vault backup` from cron (or similar) until that lands.
505
+
506
+ ### Getting help
507
+
508
+ If `doctor` is all-green but something still isn't working, capture the output alongside `parachute vault status` and open an issue at <https://github.com/ParachuteComputer/parachute-vault/issues>. Redact tokens from any logs before attaching.
509
+
297
510
  ## Deployment
298
511
 
299
512
  ### Remote access via Cloudflare Tunnel (free)
@@ -331,7 +544,37 @@ sudo cloudflared service install
331
544
  sudo systemctl start cloudflared
332
545
  ```
333
546
 
334
- Then in Claude Desktop: Settings Integrations → Add MCP → `https://vault.yourdomain.com/mcp` with `Authorization: Bearer pvk_...`.
547
+ Then point any client at `https://vault.yourdomain.com/vaults/{name}/mcp` (or `https://vault.yourdomain.com/mcp` for a single-vault deployment). See [Connecting a client → Claude Desktop (OAuth)](#claude-desktop-oauth) — the flow is identical to the local case once the URL is remote; the browser-based OAuth handshake makes the connection without pasting a bearer token.
548
+
549
+ ### Remote access via Tailscale Funnel
550
+
551
+ If you're already on [Tailscale](https://tailscale.com), Funnel is the shortest path to a public HTTPS URL — no custom domain, no reverse-proxy config, no cert management. Good for a single-user vault or a vault shared with a handful of people; the edge has bandwidth and connection-count caps, so not suited to heavy traffic.
552
+
553
+ Prerequisites: Tailscale v1.52 or later (earlier versions use a two-command `tailscale serve` + `tailscale funnel on` form that is now deprecated), the tailnet must have MagicDNS and HTTPS enabled in the admin console, and the `funnel` node attribute must be granted in your ACLs. The CLI adds the ACL entry on first use if you're the tailnet owner.
554
+
555
+ Expose the vault:
556
+
557
+ ```bash
558
+ # One command — Tailscale provisions the HTTPS cert, updates the ACL if needed,
559
+ # and registers a persistent funnel that survives reboots.
560
+ tailscale funnel --bg --https=443 localhost:1940
561
+
562
+ # See what's being served and on which tailnet hostname
563
+ tailscale funnel status
564
+
565
+ # Take it down later
566
+ tailscale funnel --https=443 localhost:1940 off
567
+ # ...or nuke the whole funnel config
568
+ tailscale funnel reset
569
+ ```
570
+
571
+ The resulting URL is `https://<your-device>.<your-tailnet>.ts.net/` — `tailscale funnel status` prints it verbatim. You can also use ports `8443` or `10000` via `--https=<port>`; no other public ports are available to Funnel.
572
+
573
+ Point any MCP client at the Tailscale URL:
574
+ - Claude Desktop → Settings → Integrations → Add MCP → `https://<your-device>.<your-tailnet>.ts.net/vaults/{name}/mcp` (leave the Authorization field empty; the OAuth flow will handle it — see [Connecting a client](#connecting-a-client)).
575
+ - Parachute Daily → enter the base URL `https://<your-device>.<your-tailnet>.ts.net`, pick the vault, tap Connect.
576
+
577
+ **Cloudflare vs Tailscale, at a glance.** Pick Cloudflare when you want a custom domain, bandwidth headroom for heavier traffic, or to share the vault with people who aren't on your tailnet. Pick Tailscale when you're already running it, you're fine with a `*.ts.net` URL, and you want the setup to fit in two commands.
335
578
 
336
579
  ### Docker
337
580
 
@@ -347,9 +590,11 @@ docker compose up -d
347
590
 
348
591
  ## Requirements
349
592
 
350
- - [Bun](https://bun.sh) — `curl -fsSL https://bun.sh/install | bash`
351
- - macOS (launchd) or Linux (systemd) for background daemon
352
- - [cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/) for remote access (optional)
593
+ - **Bun** — `curl -fsSL https://bun.sh/install | bash` ([bun.sh](https://bun.sh)).
594
+ - **macOS 13+** (launchd user agent) **or Linux** (systemd user service; other init systems work if you start the server yourself).
595
+ - **No root / sudo required** `vault init` writes to your user home (`~/.parachute/`) and registers the daemon in your user scope only. Never touches system paths or global services.
596
+ - **Not supported on Windows natively.** WSL2 hasn't been tested; file an issue if you try it and want it to work.
597
+ - Optional, for remote access: [cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/) (Cloudflare Tunnel) or [Tailscale](https://tailscale.com) — see [Deployment](#deployment).
353
598
 
354
599
  ## License
355
600