@openparachute/vault 0.2.3 → 0.3.0-rc.1

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 (58) hide show
  1. package/.claude/settings.local.json +8 -0
  2. package/CHANGELOG.md +70 -0
  3. package/CLAUDE.md +17 -7
  4. package/README.md +169 -136
  5. package/core/src/core.test.ts +603 -19
  6. package/core/src/indexed-fields.test.ts +285 -0
  7. package/core/src/indexed-fields.ts +238 -0
  8. package/core/src/mcp.ts +127 -6
  9. package/core/src/notes.ts +157 -11
  10. package/core/src/query-operators.ts +174 -0
  11. package/core/src/schema.ts +69 -2
  12. package/core/src/store.ts +92 -0
  13. package/core/src/tag-schemas.ts +5 -0
  14. package/core/src/types.ts +29 -1
  15. package/docs/HTTP_API.md +105 -1
  16. package/package/package.json +32 -0
  17. package/package.json +2 -2
  18. package/src/auth.test.ts +83 -114
  19. package/src/auth.ts +68 -6
  20. package/src/backup-launchd.ts +1 -1
  21. package/src/backup.test.ts +1 -1
  22. package/src/backup.ts +18 -17
  23. package/src/cli.ts +179 -121
  24. package/src/config-triggers.test.ts +49 -0
  25. package/src/config.test.ts +317 -2
  26. package/src/config.ts +420 -40
  27. package/src/context.test.ts +136 -0
  28. package/src/context.ts +115 -0
  29. package/src/daemon.ts +17 -16
  30. package/src/doctor.test.ts +9 -7
  31. package/src/launchd.test.ts +1 -1
  32. package/src/launchd.ts +6 -6
  33. package/src/mcp-http.ts +75 -21
  34. package/src/mcp-install.test.ts +125 -0
  35. package/src/mcp-install.ts +60 -0
  36. package/src/mcp-tools.ts +34 -96
  37. package/src/module-config.ts +109 -0
  38. package/src/oauth.test.ts +345 -57
  39. package/src/oauth.ts +155 -35
  40. package/src/published.test.ts +2 -2
  41. package/src/routes.ts +209 -33
  42. package/src/routing.test.ts +817 -300
  43. package/src/routing.ts +204 -202
  44. package/src/scopes.test.ts +136 -0
  45. package/src/scopes.ts +105 -0
  46. package/src/scribe-env.test.ts +49 -0
  47. package/src/scribe-env.ts +33 -0
  48. package/src/server.ts +57 -5
  49. package/src/services-manifest.test.ts +140 -0
  50. package/src/services-manifest.ts +99 -0
  51. package/src/systemd.ts +3 -3
  52. package/src/token-store.ts +42 -9
  53. package/src/transcription-worker.test.ts +583 -0
  54. package/src/transcription-worker.ts +346 -0
  55. package/src/triggers.test.ts +191 -1
  56. package/src/triggers.ts +17 -2
  57. package/src/vault.test.ts +693 -77
  58. package/src/version.test.ts +1 -1
package/README.md CHANGED
@@ -11,7 +11,7 @@ Requires [Bun](https://bun.sh) (`curl -fsSL https://bun.sh/install | bash`) and
11
11
  ```bash
12
12
  # Install globally (registers the `parachute` CLI)
13
13
  bun add -g @openparachute/vault
14
- parachute vault init
14
+ parachute-vault init
15
15
 
16
16
  # Or clone and run directly
17
17
  git clone https://github.com/ParachuteComputer/parachute-vault
@@ -44,27 +44,34 @@ A mental model for "where is my data?" and "what can I poke at?" after the one-c
44
44
  ### On disk — `~/.parachute/`
45
45
 
46
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)
47
+ ~/.parachute/ # ecosystem root — shared with sibling services
48
+ services.json # CLI-owned manifest (used by the dispatcher for
49
+ # discovery + routing across Parachute services)
50
+ well-known/ # CLI-owned (.well-known serving)
51
+ vault/ # everything vault owns
52
+ config.yaml # global config port, default_vault, owner password
53
+ # hash, TOTP secret, backup-codes hashes, backup
54
+ # schedule. 0600.
55
+ .env # runtime env vars (PORT=1940 by default; any webhook
56
+ # API keys you add later). Sourced by the daemon wrapper.
57
+ vault.log # stdout of the running daemon (tail via `parachute-vault logs`)
58
+ vault.err # stderr of the running daemon
59
+ server-path # text file: absolute path to the repo's src/server.ts —
60
+ # how the daemon wrapper finds the source after you move it
61
+ start.sh # the wrapper launchd/systemd execs. Knows the absolute
62
+ # path to `bun` so a later PATH change doesn't break the daemon
63
+ assets/ # legacy top-level uploads dir (attachments now land per-vault)
64
+ vaults/ # one subdirectory per vault
65
+ default/
66
+ vault.db # the SQLite database — notes, tags, links, attachments,
67
+ # per-vault tokens, OAuth clients + codes, tag schemas
68
+ vault.yaml # per-vault config — description (sent as MCP session
69
+ # instruction), published_tag override, legacy api_keys
70
+ assets/ # per-vault uploaded attachments (audio, images)
66
71
  ```
67
72
 
73
+ `~/.parachute/` itself is the ecosystem root shared across sibling services — `services.json` and `well-known/` live at the root and are managed by the top-level CLI. Everything vault owns is scoped under `~/.parachute/vault/`. Pre-0.3 installs kept vault state directly at the root; any legacy paths still there are auto-migrated into `vault/` on first post-upgrade run (see CHANGELOG).
74
+
68
75
  `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
76
 
70
77
  ### Registered externally
@@ -73,19 +80,19 @@ A mental model for "where is my data?" and "what can I poke at?" after the one-c
73
80
  - **Linux + systemd**: a user service named `parachute-vault.service` (managed via `systemctl --user`).
74
81
  - **Neither of the above**: `vault init` prints a reminder to start the server yourself (`bun src/server.ts` or Docker). No service registration.
75
82
 
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.
83
+ 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
84
 
78
85
  ### `~/.claude.json`
79
86
 
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.
87
+ `vault init` adds one entry — `mcpServers["parachute-vault"]` — pointing at `http://127.0.0.1:<port>/vault/<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
88
 
82
89
  ### Your API token
83
90
 
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`.
91
+ 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
92
 
86
93
  ### Owner password prompt
87
94
 
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).
95
+ 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
96
 
90
97
  ## Connecting a client
91
98
 
@@ -94,7 +101,7 @@ Two ways to authenticate — pick based on the client, not the deployment:
94
101
  | Path | When to use | User action |
95
102
  |---|---|---|
96
103
  | **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` |
104
+ | **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
105
 
99
106
  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
107
 
@@ -103,12 +110,12 @@ Both paths end up with the same kind of token in the vault's DB — a `pvt_` str
103
110
  `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
111
 
105
112
  ```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
113
+ parachute-vault set-password # set / change
114
+ parachute-vault set-password --clear # remove (reverts to token fallback)
115
+ parachute-vault 2fa enroll # optional: add TOTP 2FA on top
109
116
  ```
110
117
 
111
- Password and 2FA secrets live in `~/.parachute/config.yaml` at mode 0600 (bcrypt hash + base32 TOTP secret).
118
+ Password and 2FA secrets live in `~/.parachute/vault/config.yaml` at mode 0600 (bcrypt hash + base32 TOTP secret).
112
119
 
113
120
  ### Claude Code
114
121
 
@@ -119,7 +126,7 @@ Password and 2FA secrets live in `~/.parachute/config.yaml` at mode 0600 (bcrypt
119
126
  "mcpServers": {
120
127
  "parachute-vault": {
121
128
  "type": "http",
122
- "url": "http://127.0.0.1:1940/vaults/{name}/mcp",
129
+ "url": "http://127.0.0.1:1940/vault/{name}/mcp",
123
130
  "headers": { "Authorization": "Bearer pvt_..." }
124
131
  }
125
132
  }
@@ -128,19 +135,19 @@ Password and 2FA secrets live in `~/.parachute/config.yaml` at mode 0600 (bcrypt
128
135
 
129
136
  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
137
 
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.)
138
+ To re-point Claude Code at a different vault, change `default_vault` in `~/.parachute/vault/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
139
 
133
140
  ### Claude Desktop (OAuth)
134
141
 
135
142
  For Claude Desktop — or any install where the server is on a different machine from the client — use the browser-based OAuth flow:
136
143
 
137
144
  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.
145
+ 2. Enter the URL: `https://vault.yourdomain.com/vault/{name}/mcp` (replace `{name}` with your vault name — e.g. `default`). **Do not paste a bearer token** — leave the auth field empty.
146
+ 3. An OAuth-capable MCP client discovers the vault's authorization server at `/vault/{name}/.well-known/oauth-authorization-server`, registers itself via Dynamic Client Registration (RFC 7591), and opens your browser to the vault's consent page.
140
147
  4. Enter your owner password (plus TOTP code / backup code if 2FA is enabled), pick a scope (`full` or `read`), click Authorize.
141
148
  5. Browser redirects back. The connection is live. The client now holds a `pvt_` token scoped to this vault.
142
149
 
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.
150
+ 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
151
 
145
152
  ### Parachute Daily (mobile)
146
153
 
@@ -151,75 +158,73 @@ Daily uses the same OAuth flow. On first launch: enter the server URL, pick the
151
158
  One server, many vaults. Each vault is its own SQLite DB with its own MCP endpoint, its own OAuth, and its own tokens.
152
159
 
153
160
  ```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
161
+ parachute-vault create work # new vault named "work"
162
+ parachute-vault list # show all vaults on this server
163
+ parachute-vault remove work --yes
157
164
  ```
158
165
 
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.
166
+ **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`.
162
167
 
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.
168
+ **URL shape.** Every vault-touching route lives under `/vault/{name}/...`: `/vault/{name}/mcp`, `/vault/{name}/oauth/authorize`, `/vault/{name}/api/notes`, `/vault/{name}/view/{id}`. There is no unscoped fallback — pick the vault in the URL even if you only have one. OAuth tokens are scoped to the vault in their issuing URL; cross-vault substitution is rejected at the OAuth layer (an auth code minted for one vault cannot be redeemed at another vault's token endpoint).
164
169
 
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.
170
+ **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/vault/config.yaml` to make `/vaults/list` return 404.
166
171
 
167
172
  ## CLI
168
173
 
169
174
  ```bash
170
175
  # Setup
171
- parachute vault init # one-command setup (idempotent — safe to re-run)
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)
176
+ parachute-vault init # one-command setup (idempotent — safe to re-run)
177
+ parachute-vault status # check what's running
178
+ parachute-vault doctor # diagnose install/config issues (see Troubleshooting)
179
+ parachute-vault url # print the local server URL (for scripts)
175
180
  parachute --version # print the installed version (aliases: -v, version)
176
- parachute vault uninstall # remove daemon + MCP entry; keeps user data
177
- parachute vault uninstall --wipe # ...and also remove vaults, .env, config.yaml, logs
178
- parachute vault uninstall --yes --wipe # scripted destructive wipe (prints an audit line)
181
+ parachute-vault uninstall # remove daemon + MCP entry; keeps user data
182
+ parachute-vault uninstall --wipe # ...and also remove vaults, .env, config.yaml, logs
183
+ parachute-vault uninstall --yes --wipe # scripted destructive wipe (prints an audit line)
179
184
 
180
185
  # Vaults
181
- parachute vault create work # create a new vault
182
- parachute vault list # list all vaults (alias: `ls`)
183
- parachute vault remove work --yes # delete a vault (alias: `rm`)
184
- parachute vault mcp-install # (re)write the ~/.claude.json MCP entry for the default vault
186
+ parachute-vault create work # create a new vault
187
+ parachute-vault list # list all vaults (alias: `ls`)
188
+ parachute-vault remove work --yes # delete a vault (alias: `rm`)
189
+ parachute-vault mcp-install # (re)write the ~/.claude.json MCP entry for the default vault
185
190
 
186
191
  # OAuth — owner password + 2FA
187
- parachute vault set-password # set/change the owner password (OAuth consent page)
188
- parachute vault set-password --clear # remove the owner password (falls back to vault-token auth)
189
- parachute vault 2fa status # show 2FA state + remaining backup codes
190
- parachute vault 2fa enroll # enroll TOTP (shows QR + prints one-time backup codes)
191
- parachute vault 2fa disable # disable 2FA (requires password or TOTP/backup code)
192
- parachute vault 2fa backup-codes # regenerate backup codes (invalidates the old set)
192
+ parachute-vault set-password # set/change the owner password (OAuth consent page)
193
+ parachute-vault set-password --clear # remove the owner password (falls back to vault-token auth)
194
+ parachute-vault 2fa status # show 2FA state + remaining backup codes
195
+ parachute-vault 2fa enroll # enroll TOTP (shows QR + prints one-time backup codes)
196
+ parachute-vault 2fa disable # disable 2FA (requires password or TOTP/backup code)
197
+ parachute-vault 2fa backup-codes # regenerate backup codes (invalidates the old set)
193
198
 
194
199
  # Tokens
195
- parachute vault tokens # list all tokens across all vaults
196
- parachute vault tokens create # full-access token in the default vault
197
- parachute vault tokens create --vault work # ...in a specific vault
198
- parachute vault tokens create --read # read-only token
199
- parachute vault tokens create --expires 30d # expiring token (N{h|d|w|m|y})
200
- parachute vault tokens create --label mobile # labeled token
201
- parachute vault tokens revoke <token-id> # revoke (default vault; add --vault to target)
200
+ parachute-vault tokens # list all tokens across all vaults
201
+ parachute-vault tokens create # full-access token in the default vault
202
+ parachute-vault tokens create --vault work # ...in a specific vault
203
+ parachute-vault tokens create --read # read-only token
204
+ parachute-vault tokens create --expires 30d # expiring token (N{h|d|w|m|y})
205
+ parachute-vault tokens create --label mobile # labeled token
206
+ parachute-vault tokens revoke <token-id> # revoke (default vault; add --vault to target)
202
207
 
203
208
  # Obsidian
204
- parachute vault import ~/Obsidian/MyVault # import into default vault
205
- parachute vault import ~/Obsidian/Work --vault work # import into a specific vault
206
- parachute vault import <path> --dry-run # preview without importing
207
- parachute vault export ./output --vault work # export a specific vault
209
+ parachute-vault import ~/Obsidian/MyVault # import into default vault
210
+ parachute-vault import ~/Obsidian/Work --vault work # import into a specific vault
211
+ parachute-vault import <path> --dry-run # preview without importing
212
+ parachute-vault export ./output --vault work # export a specific vault
208
213
 
209
214
  # Config
210
- parachute vault config # show current configuration
211
- parachute vault config set KEY value # set an env var (e.g. PORT=1940)
212
- parachute vault config unset KEY # remove an env var
213
- parachute vault restart # apply config changes (bounces the daemon)
215
+ parachute-vault config # show current configuration
216
+ parachute-vault config set KEY value # set an env var (e.g. PORT=1940)
217
+ parachute-vault config unset KEY # remove an env var
218
+ parachute-vault restart # apply config changes (bounces the daemon)
214
219
 
215
220
  # Server
216
- parachute vault serve # run the server in the foreground (no daemon)
217
- parachute vault logs # stream vault.log + vault.err (tail -f)
221
+ parachute-vault serve # run the server in the foreground (no daemon)
222
+ parachute-vault logs # stream vault.log + vault.err (tail -f)
218
223
 
219
224
  # Backup
220
- parachute vault backup # one-shot backup to configured destinations
221
- parachute vault backup --schedule daily # hourly | daily | weekly | manual (macOS launchd)
222
- parachute vault backup status # schedule, last run, destinations, next run
225
+ parachute-vault backup # one-shot backup to configured destinations
226
+ parachute-vault backup --schedule daily # hourly | daily | weekly | manual (macOS launchd)
227
+ parachute-vault backup status # schedule, last run, destinations, next run
223
228
  ```
224
229
 
225
230
  ## MCP tools (9)
@@ -234,7 +239,7 @@ parachute vault backup status # schedule, last run, destination
234
239
  Each vault teaches AI agents how to use it:
235
240
 
236
241
  ```yaml
237
- # ~/.parachute/vaults/default/vault.yaml
242
+ # ~/.parachute/vault/vaults/default/vault.yaml
238
243
  name: default
239
244
  description: |
240
245
  Personal knowledge vault. Tags in use: daily, voice, reader, project.
@@ -252,9 +257,9 @@ Sent as the MCP server instruction at session start.
252
257
  ### Obsidian import/export
253
258
 
254
259
  ```bash
255
- parachute vault create research
256
- parachute vault import ~/Obsidian/Research --vault research
257
- parachute vault export ./output --vault research
260
+ parachute-vault create research
261
+ parachute-vault import ~/Obsidian/Research --vault research
262
+ parachute-vault export ./output --vault research
258
263
  ```
259
264
 
260
265
  Imports: frontmatter → metadata, `#tags` → tags table, `[[wikilinks]]` → links table, file paths → note.path. Skips duplicates.
@@ -265,7 +270,7 @@ Note paths work like Obsidian file paths (`Projects/Parachute/README`). Normaliz
265
270
 
266
271
  ### Webhook triggers
267
272
 
268
- Declarative config-driven webhooks that fire when a note mutation matches a predicate. Configured in `~/.parachute/config.yaml`:
273
+ Declarative config-driven webhooks that fire when a note mutation matches a predicate. Configured in `~/.parachute/vault/config.yaml`:
269
274
 
270
275
  ```yaml
271
276
  triggers:
@@ -319,18 +324,39 @@ triggers:
319
324
 
320
325
  Webhook servers (scribe, narrate) are stateless — they don't need vault's API key.
321
326
 
327
+ ### On-upload transcription
328
+
329
+ The trigger system above handles the "tag a note and let a webhook fill in content later" shape. Clients that already know at upload time that an audio file should be transcribed can take a shorter path: `POST /api/notes/{id}/attachments` with `{transcribe: true}` in the body. The vault stamps the attachment with `transcribe_status: "pending"` and the note with `transcribe_stub: true`, then a background worker drains the queue FIFO.
330
+
331
+ Enable the worker by pointing the server at a Whisper-compatible endpoint:
332
+
333
+ ```
334
+ SCRIBE_URL=http://localhost:3200
335
+ SCRIBE_TOKEN=optional-bearer-token
336
+ ```
337
+
338
+ The worker POSTs audio as multipart to `${SCRIBE_URL}/v1/audio/transcriptions` and expects `{ text: string }` back. On success it replaces the `_Transcript pending._` placeholder in the note body (or the whole body if the placeholder is absent), clears `transcribe_stub`, and records `transcript` + `transcribe_done_at` on the attachment. If the user edited the note and cleared `transcribe_stub` before the transcript landed, the note is left alone — but the transcript is still stored on the attachment. Failures retry with exponential backoff up to three attempts before flipping `transcribe_status` to `"failed"`.
339
+
340
+ Per-vault `audio_retention` controls what happens to the audio file on disk once the worker reaches a terminal state. It's readable and mutable at runtime via `GET` / `PATCH /api/vault` (under `config.audio_retention`), or by hand-editing `vault.yaml`.
341
+
342
+ - `keep` (default) — leave the file on disk indefinitely.
343
+ - `until_transcribed` — unlink on successful transcription; on failure the file is kept so you can retry or re-upload.
344
+ - `never` — unlink on any terminal state, **including failure**. Users who opt in accept that losing a bad transcription also loses the source audio. The file stays during mid-queue retries so in-flight attempts still have something to send.
345
+
346
+ In every mode the attachment row (and any stored transcript) is preserved; only the file on disk is affected.
347
+
322
348
  ### Backing up your vault
323
349
 
324
- 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.
350
+ Your vault is just SQLite DBs + a handful of YAML files under `~/.parachute/vault/`. `parachute-vault backup` snapshots everything into a single timestamped tarball, for a one-shot or a scheduled run.
325
351
 
326
352
  ```bash
327
- parachute vault backup # one-shot — snapshot + ship to destinations
328
- parachute vault backup --schedule daily # register a launchd agent (macOS)
329
- parachute vault backup --schedule manual # stop scheduled backups
330
- parachute vault backup status # schedule, last run, destinations, next run
353
+ parachute-vault backup # one-shot — snapshot + ship to destinations
354
+ parachute-vault backup --schedule daily # register a launchd agent (macOS)
355
+ parachute-vault backup --schedule manual # stop scheduled backups
356
+ parachute-vault backup status # schedule, last run, destinations, next run
331
357
  ```
332
358
 
333
- Configure destinations in `~/.parachute/config.yaml`:
359
+ Configure destinations in `~/.parachute/vault/config.yaml`:
334
360
 
335
361
  ```yaml
336
362
  backup:
@@ -358,11 +384,11 @@ A snapshot that qualifies for multiple tiers is kept once. Set any tier to `0` t
358
384
 
359
385
  **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.
360
386
 
361
- **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).
387
+ **Restore**: extract the tarball into a fresh `~/.parachute/vault/` 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).
362
388
 
363
389
  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.
364
390
 
365
- 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.
391
+ 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.
366
392
 
367
393
  ### View endpoint
368
394
 
@@ -373,29 +399,35 @@ Serve notes as clean HTML pages at `/view/:noteId`:
373
399
  - **Custom tag**: set `published_tag` in vault.yaml to use a different tag name (default: `publish`).
374
400
 
375
401
  ```yaml
376
- # ~/.parachute/vaults/default/vault.yaml
402
+ # ~/.parachute/vault/vaults/default/vault.yaml
377
403
  published_tag: public
378
404
  ```
379
405
 
380
406
  ## REST API
381
407
 
408
+ Every vault-touching path is scoped under `/vault/{name}/` — substitute your vault name (e.g. `default`) for `{name}`:
409
+
382
410
  ```
383
- GET/POST /api/notes query or create notes
384
- GET/PATCH/DEL /api/notes/:idOrPath read, update, delete a single note
385
- GET/POST /api/notes/:id/attachments list or add attachments
386
- GET /api/tags list tags (?include_schema=true for schemas)
387
- GET/PUT/DEL /api/tags/:name get, update, or delete a tag
388
- GET /api/find-path?source=...&target=... shortest path between two notes
389
- GET/PATCH /api/vault vault info (get or update description)
390
- POST /api/storage/upload upload file (audio/image)
391
- GET /api/storage/:path download file
392
- POST /mcp MCP endpoint (unified, all vaults)
393
- GET /view/:idOrPath render note as HTML (public or auth)
394
- GET /health health check
411
+ GET/POST /vault/{name}/api/notes query or create notes
412
+ GET/PATCH/DEL /vault/{name}/api/notes/:idOrPath read, update, delete a single note
413
+ GET/POST /vault/{name}/api/notes/:id/attachments list or add attachments
414
+ GET /vault/{name}/api/tags list tags (?include_schema=true for schemas)
415
+ GET/PUT/DEL /vault/{name}/api/tags/:name get, update, or delete a tag
416
+ POST /vault/{name}/api/tags/:name/rename atomic rename (409 if target_exists)
417
+ POST /vault/{name}/api/tags/merge atomic multi-source merge into a target tag
418
+ GET /vault/{name}/api/find-path?source=...&target=... shortest path between two notes
419
+ GET/PATCH /vault/{name}/api/vault vault info (get or update description)
420
+ POST /vault/{name}/api/storage/upload upload file (audio/image)
421
+ GET /vault/{name}/api/storage/:path download file
422
+ POST /vault/{name}/mcp MCP endpoint (per-vault session)
423
+ * /vault/{name}/oauth/{register,authorize,token} OAuth 2.1 + PKCE flow
424
+ * /vault/{name}/.well-known/oauth-* RFC 8414 / RFC 9728 discovery
425
+ GET /vault/{name}/view/:idOrPath render note as HTML (public or auth)
426
+ GET /vaults authenticated: full metadata
427
+ GET /vaults/list public: vault names only (unless discovery: disabled)
428
+ GET /health health check
395
429
  ```
396
430
 
397
- Per-vault routes at `/vaults/{name}/api/...`, `/vaults/{name}/mcp`, and `/vaults/{name}/view/:idOrPath`.
398
-
399
431
  ## Data model
400
432
 
401
433
  ```
@@ -412,24 +444,24 @@ Metadata is a JSON column. Vaults start blank — no predefined tags or schema.
412
444
 
413
445
  **All API and MCP requests require a valid API key.** No exceptions — localhost gets no special treatment.
414
446
 
415
- 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`).
447
+ 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`, `/vaults/list`, published notes at `/vault/{name}/view/:id`).
416
448
 
417
449
  ### Passing the key
418
450
 
419
451
  Tokens come in two shapes. Both work interchangeably at every authenticated endpoint:
420
452
 
421
- - `pvt_...` — per-vault scoped tokens (the modern format; what `vault init` mints, what OAuth issues, what `parachute vault tokens create` produces)
453
+ - `pvt_...` — per-vault scoped tokens (the modern format; what `vault init` mints, what OAuth issues, what `parachute-vault tokens create` produces)
422
454
  - `pvk_...` — legacy global API keys from `config.yaml` (still honored for existing deployments)
423
455
 
424
456
  ```bash
425
457
  # Header (preferred)
426
- curl -H "Authorization: Bearer pvt_..." http://localhost:1940/api/notes
458
+ curl -H "Authorization: Bearer pvt_..." http://localhost:1940/vault/default/api/notes
427
459
 
428
460
  # Alternative header
429
- curl -H "X-API-Key: pvt_..." http://localhost:1940/api/notes
461
+ curl -H "X-API-Key: pvt_..." http://localhost:1940/vault/default/api/notes
430
462
 
431
463
  # Query param (for /view endpoint only — convenient for browsers)
432
- curl http://localhost:1940/view/noteId?key=pvt_...
464
+ curl http://localhost:1940/vault/default/view/noteId?key=pvt_...
433
465
  ```
434
466
 
435
467
  ### Token management
@@ -442,12 +474,12 @@ Per-vault tokens with two permission levels:
442
474
  | `read` | Query, list, find-path, vault-info only |
443
475
 
444
476
  ```bash
445
- parachute vault tokens # list all tokens
446
- parachute vault tokens create --vault work # full-access token
447
- parachute vault tokens create --vault work --read # read-only
448
- parachute vault tokens create --vault work --expires 30d # with expiry
449
- parachute vault tokens create --vault work --label phone # labeled token
450
- parachute vault tokens revoke <token-id> --vault work # revoke
477
+ parachute-vault tokens # list all tokens
478
+ parachute-vault tokens create --vault work # full-access token
479
+ parachute-vault tokens create --vault work --read # read-only
480
+ parachute-vault tokens create --vault work --expires 30d # with expiry
481
+ parachute-vault tokens create --vault work --label phone # labeled token
482
+ parachute-vault tokens revoke <token-id> --vault work # revoke
451
483
  ```
452
484
 
453
485
  Tokens are shown once at creation — save them immediately. SHA-256 hashed at rest.
@@ -456,9 +488,10 @@ Legacy API keys (`pvk_...`) from config.yaml still work at runtime but the `vaul
456
488
 
457
489
  ### Public endpoints
458
490
 
459
- Only two endpoints work without auth:
491
+ Three endpoints work without auth:
460
492
  - `GET /health` — returns `{ status: "ok" }` (no sensitive data)
461
- - `GET /view/:noteId` — serves published notes only (returns 404 for unpublished)
493
+ - `GET /vaults/list` — vault names only (set `discovery: disabled` in `config.yaml` to hide)
494
+ - `GET /vault/{name}/view/:noteId` — serves published notes only (returns 404 for unpublished)
462
495
 
463
496
  ## Network security
464
497
 
@@ -479,7 +512,7 @@ For remote access, always use a TLS-terminating proxy:
479
512
 
480
513
  ## Troubleshooting
481
514
 
482
- ### `parachute vault doctor` is your first stop
515
+ ### `parachute-vault doctor` is your first stop
483
516
 
484
517
  `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.
485
518
 
@@ -487,26 +520,26 @@ The checks, in the order they're emitted:
487
520
 
488
521
  | Check | What it verifies | Typical fix when failing |
489
522
  |---|---|---|
490
- | 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. |
491
- | wrapper script | `~/.parachute/start.sh` exists. Without it, launchd / systemd has nothing to exec. | `parachute vault init`. |
492
- | 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`. |
523
+ | server-path pointer | `~/.parachute/vault/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. |
524
+ | wrapper script | `~/.parachute/vault/start.sh` exists. Without it, launchd / systemd has nothing to exec. | `parachute-vault init`. |
525
+ | 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`. |
493
526
  | 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. |
494
- | 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. |
495
- | 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`. |
496
- | 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. |
497
- | 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. |
527
+ | 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. |
528
+ | 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/vault/.env` and re-run `vault init`. |
529
+ | 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. |
530
+ | backup destinations (only when `backup.schedule != manual`) | At least one destination is configured; each configured destination is writable. | Edit `~/.parachute/vault/config.yaml` under `backup.destinations`, or fix the path's permissions. |
498
531
 
499
532
  ### Common failure modes
500
533
 
501
- - **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`.
502
- - **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.
503
- - **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.
504
- - **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.
505
- - **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.
534
+ - **Daemon won't start after a port change.** `~/.parachute/vault/.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`.
535
+ - **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.
536
+ - **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.
537
+ - **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.
538
+ - **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.
506
539
 
507
540
  ### Getting help
508
541
 
509
- 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.
542
+ 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.
510
543
 
511
544
  ## Deployment
512
545
 
@@ -545,7 +578,7 @@ sudo cloudflared service install
545
578
  sudo systemctl start cloudflared
546
579
  ```
547
580
 
548
- 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.
581
+ Then point any client at `https://vault.yourdomain.com/vault/{name}/mcp`. 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.
549
582
 
550
583
  ### Remote access via Tailscale Funnel
551
584
 
@@ -572,7 +605,7 @@ tailscale funnel reset
572
605
  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.
573
606
 
574
607
  Point any MCP client at the Tailscale URL:
575
- - 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)).
608
+ - Claude Desktop → Settings → Integrations → Add MCP → `https://<your-device>.<your-tailnet>.ts.net/vault/{name}/mcp` (leave the Authorization field empty; the OAuth flow will handle it — see [Connecting a client](#connecting-a-client)).
576
609
  - Parachute Daily → enter the base URL `https://<your-device>.<your-tailnet>.ts.net`, pick the vault, tap Connect.
577
610
 
578
611
  **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.