@openparachute/vault 0.6.5-rc.2 → 0.6.5

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 (70) hide show
  1. package/.parachute/module.json +1 -0
  2. package/core/src/core.test.ts +7 -7
  3. package/core/src/do-param-cap.test.ts +161 -0
  4. package/core/src/links.ts +8 -13
  5. package/core/src/mcp.ts +1 -1
  6. package/core/src/notes.ts +33 -15
  7. package/core/src/onboarding.ts +14 -296
  8. package/core/src/schema.ts +5 -5
  9. package/core/src/seed-packs.test.ts +357 -0
  10. package/core/src/seed-packs.ts +823 -0
  11. package/core/src/sql-in.test.ts +58 -0
  12. package/core/src/sql-in.ts +76 -0
  13. package/core/src/tag-hierarchy.ts +1 -1
  14. package/core/src/tag-schemas.ts +2 -2
  15. package/core/src/transcription/provider.ts +141 -0
  16. package/core/src/types.ts +1 -1
  17. package/core/src/vault-projection.ts +1 -1
  18. package/core/src/wikilinks.ts +10 -4
  19. package/package.json +1 -1
  20. package/src/add-pack.test.ts +142 -0
  21. package/src/admin-spa.ts +2 -1
  22. package/src/auth.ts +1 -1
  23. package/src/cli.ts +806 -7
  24. package/src/export-watch.ts +1 -1
  25. package/src/live-frame-parity.test.ts +201 -0
  26. package/src/module-manifest.ts +8 -0
  27. package/src/onboarding-seed.test.ts +188 -40
  28. package/src/onboarding-seed.ts +41 -46
  29. package/src/routes.ts +20 -3
  30. package/src/routing.test.ts +2 -2
  31. package/src/routing.ts +18 -6
  32. package/src/self-register.test.ts +19 -0
  33. package/src/self-register.ts +6 -1
  34. package/src/server.ts +133 -31
  35. package/src/services-manifest.ts +8 -0
  36. package/src/subscriptions.ts +100 -42
  37. package/src/tag-scope.ts +3 -3
  38. package/src/test-support/live-frame-corpus.ts +72 -0
  39. package/src/token-store.ts +2 -2
  40. package/src/transcription/build.test.ts +305 -0
  41. package/src/transcription/build.ts +332 -0
  42. package/src/transcription/capability.test.ts +118 -0
  43. package/src/transcription/capability.ts +96 -0
  44. package/src/transcription/download.test.ts +101 -0
  45. package/src/transcription/download.ts +71 -0
  46. package/src/transcription/install-python.test.ts +366 -0
  47. package/src/transcription/install-python.ts +471 -0
  48. package/src/transcription/install.test.ts +167 -0
  49. package/src/transcription/install.ts +296 -0
  50. package/src/transcription/providers/onnx-asr.test.ts +229 -0
  51. package/src/transcription/providers/onnx-asr.ts +239 -0
  52. package/src/transcription/providers/parakeet-mlx.test.ts +290 -0
  53. package/src/transcription/providers/parakeet-mlx.ts +242 -0
  54. package/src/transcription/providers/scribe-http.test.ts +195 -0
  55. package/src/transcription/providers/scribe-http.ts +144 -0
  56. package/src/transcription/providers/transcribe-cpp.test.ts +314 -0
  57. package/src/transcription/providers/transcribe-cpp.ts +293 -0
  58. package/src/transcription/select.test.ts +299 -0
  59. package/src/transcription/select.ts +397 -0
  60. package/src/transcription/tiers.test.ts +197 -0
  61. package/src/transcription/tiers.ts +184 -0
  62. package/src/transcription-worker.test.ts +44 -0
  63. package/src/transcription-worker.ts +57 -122
  64. package/src/vault-create.test.ts +43 -10
  65. package/src/vault.test.ts +49 -1
  66. package/src/ws-server.ts +408 -0
  67. package/src/ws-subscribe.test.ts +474 -0
  68. package/src/ws-subscribe.ts +242 -0
  69. package/src/subscribe.test.ts +0 -609
  70. package/src/subscribe.ts +0 -248
@@ -0,0 +1,823 @@
1
+ /**
2
+ * Named seed packs — curated starter content a vault can be seeded with, plus
3
+ * the generic idempotent applier.
4
+ *
5
+ * A pack is a named bundle of `tags` (upserted) + `notes` (created only when
6
+ * no note already lives at the path). Three packs ship today:
7
+ *
8
+ * - `welcome` — the five-guide welcome ring (Welcome, Capture anything, Tags
9
+ * and the graph, Connect your AI, Yours to keep) + the `capture` / `guide`
10
+ * tags the Notes surface expects. Default-seeded on creation.
11
+ * - `getting-started` — the AI-facing start-here guide (SKILL.md-style
12
+ * doctrine addressed to a connected assistant). Default-seeded.
13
+ * - `surface-starter` — the living starter guide for building a custom
14
+ * surface (UI) over the vault. NOT default-seeded (ratified 2026-07-02:
15
+ * Surface Starter is out of the default seed) — added on demand via
16
+ * `parachute-vault add-pack surface-starter` or a console affordance.
17
+ *
18
+ * Guides are the vault's skill files — the Parachute equivalent of a
19
+ * `SKILL.md`. They're tagged `#guide` (GUIDE_TAG): notes that teach how this
20
+ * vault works and how to work it, for you and your AI alike. Some (the welcome
21
+ * ring) are voiced to the person; some (Getting Started) to the assistant; all
22
+ * are plain markdown either reads — so no per-guide audience field is needed.
23
+ *
24
+ * This module is the single source of truth for pack content across BOTH
25
+ * runtimes: the bun vault (`src/onboarding-seed.ts` default seed + the
26
+ * `add-pack` CLI verb) and the cloud vault DO (parachute-cloud
27
+ * `workers/vault` — a sibling PR consumes these packs in place of its own
28
+ * `welcome.ts` copy). Keep `applySeedPack` to single-item Store calls: the
29
+ * cloud DO routes them through its real `transactionSync` seam, and its
30
+ * conformance suite pins a zero-raw-BEGIN tripwire.
31
+ *
32
+ * `core/src/onboarding.ts` remains as a deprecated re-export shim for the
33
+ * Getting Started / Surface Starter path + content constants.
34
+ */
35
+
36
+ import type { Store, TagFieldSchema } from "./types.ts";
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Default vault descriptions — the single source of truth
40
+ // ---------------------------------------------------------------------------
41
+
42
+ /**
43
+ * The `vault-info` description a **brand-new** vault ships with. It's the first
44
+ * thing a connected AI reads about the vault, so it doubles as a nudge: orient
45
+ * the person, read the Getting Started brief, do one small real thing, and — once
46
+ * real structure lands — REPLACE this text with a current picture of the vault.
47
+ *
48
+ * Exported as the single source of truth; the runtimes (bun vault default seed +
49
+ * cloud vault DO) consume it in a follow-up PR. Not wired into `applySeedPack`
50
+ * here — a description is vault metadata, not a pack note.
51
+ */
52
+ export const DEFAULT_VAULT_DESCRIPTION =
53
+ 'This is a brand-new personal vault — just its starter guides so far; the person it belongs to is probably new to Parachute. Before helping them set it up, read the "Getting Started" note — it\'s your onboarding brief. Short version: start with a conversation, not a filing system; learn how they want to use this and where their notes live today; do one small real thing first. When the vault has real structure, replace this description (vault-info { description: "..." }) with a current picture of what lives here and how to work it — so every future session, yours or another AI\'s, starts oriented.';
54
+
55
+ /**
56
+ * The `vault-info` description a vault gets when it was **imported/restored** —
57
+ * it arrived with content. Flips the AI's first job from seed-it to learn-it:
58
+ * orient over the existing shape, reflect it back, and only then replace this
59
+ * text with a current picture.
60
+ */
61
+ export const IMPORTED_VAULT_DESCRIPTION =
62
+ 'This vault was imported — it arrived with its own notes, tags, and history. Orient before you write: vault-info { include_stats: true }, list-tags, read a sample, and read "Getting Started" if present. Your first job is to learn the shape that\'s already here and reflect it back to the person — not to propose new structure cold. When you understand it, replace this description (vault-info { description: "..." }) with a current picture.';
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // Pack shape
66
+ // ---------------------------------------------------------------------------
67
+
68
+ /**
69
+ * A tag declaration a pack upserts (identity row, not a schema migration).
70
+ *
71
+ * `fields` carries an optional typed-metadata schema — the same
72
+ * `Record<field, { type, enum?, indexed? }>` shape `update-tag` accepts — so a
73
+ * pack can declare a schema-carrying tag. The applier passes it straight
74
+ * through to `upsertTagRecord`.
75
+ */
76
+ export interface SeedPackTag {
77
+ name: string;
78
+ description: string;
79
+ parent_names?: string[];
80
+ fields?: Record<string, TagFieldSchema>;
81
+ }
82
+
83
+ /**
84
+ * A note a pack seeds — created only when no note exists at `path`. `tags` +
85
+ * `metadata` are applied at create time (both runtimes' `createNote` accepts
86
+ * them); a guide note carries `tags: ["guide"]`.
87
+ */
88
+ export interface SeedPackNote {
89
+ path: string;
90
+ content: string;
91
+ tags?: string[];
92
+ metadata?: Record<string, unknown>;
93
+ }
94
+
95
+ /** A named bundle of starter tags + notes. */
96
+ export interface SeedPack {
97
+ /** Stable pack name — the `add-pack <name>` handle. */
98
+ name: string;
99
+ /** One-line human description (shown by the pack listing). */
100
+ description: string;
101
+ tags: ReadonlyArray<SeedPackTag>;
102
+ notes: ReadonlyArray<SeedPackNote>;
103
+ }
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // `welcome` pack — person-voiced first-minute content
107
+ // ---------------------------------------------------------------------------
108
+
109
+ /**
110
+ * The tag Notes requires — name/description must stay BYTE-EQUAL to notes-ui's
111
+ * `NOTES_REQUIRED_SCHEMA`
112
+ * (parachute-surface/packages/notes-ui/src/lib/vault/schema.ts): the PWA's
113
+ * connect-time audit (schema-audit.ts) compares `description` verbatim, so its
114
+ * schema banner clears because the tag genuinely exists with the semantics
115
+ * Notes declares — not because we gamed the check. Do not edit these strings
116
+ * without changing notes-ui in lockstep.
117
+ *
118
+ * ONE tag since 2026-07-03 (Aaron-ratified): `#capture` carries the sacred
119
+ * raw-input semantics; entry method (text vs voice) is provenance, not
120
+ * user-facing taxonomy, and moves to note `metadata.source` (`text` | `voice`)
121
+ * — a sibling notes-ui PR lands the client side. The old `capture/text` /
122
+ * `capture/voice` subtype tags are no longer seeded; existing vaults that
123
+ * carry them remain valid (tags are user data — this changes only what NEW
124
+ * vaults get).
125
+ */
126
+ export const NOTES_REQUIRED_TAGS: ReadonlyArray<SeedPackTag> = [
127
+ {
128
+ name: "capture",
129
+ description: "Notes captured directly by the user (text or voice).",
130
+ },
131
+ ];
132
+
133
+ /**
134
+ * The `guide` tag — the vault's skill-file tag. Guides are the Parachute
135
+ * equivalent of a `SKILL.md`: notes that teach how this vault works and how to
136
+ * work it, for you and your AI alike. Plain markdown, so either reads them.
137
+ *
138
+ * Declared by every pack that ships guide notes (welcome / getting-started /
139
+ * surface-starter). The upserts converge on one row, so each pack stays
140
+ * self-sufficient — applying any one of them alone still lands the tag.
141
+ */
142
+ export const GUIDE_TAG: SeedPackTag = {
143
+ name: "guide",
144
+ description:
145
+ "Guides — the vault's skill files. Notes that teach how this vault works and how to work it, for you and your AI alike. Plain markdown, so either reads them.",
146
+ };
147
+
148
+ export const WELCOME_PATH = "Welcome to your vault 🪂";
149
+ export const CAPTURE_ANYTHING_PATH = "Capture anything";
150
+ export const TAGS_GRAPH_PATH = "Tags and the graph";
151
+ export const CONNECT_AI_PATH = "Connect your AI";
152
+ export const YOURS_TO_KEEP_PATH = "Yours to keep";
153
+
154
+ /**
155
+ * Build the `welcome` pack: the five-guide welcome ring — Welcome, Capture
156
+ * anything, Tags and the graph, Connect your AI, Yours to keep — plus the
157
+ * `capture` / `guide` tags. The guides are ordinary notes tagged `#guide`
158
+ * (the vault's skill-file tag). They form a
159
+ * small linked web (Welcome → all four; the rest chain 2→3→4→5→1; Connect
160
+ * your AI also links the AI-facing [[Getting Started]]) so the graph view
161
+ * shows a connected structure from minute one. Deletable like anything else.
162
+ *
163
+ * These five bodies are the ratified guides (already live at
164
+ * parachute.computer/guides). `consoleOrigin` is the origin of the operator's
165
+ * console (the cloud console, or a hub portal) — when known, the Connect-your-AI
166
+ * note names it; when omitted (the bun vault seeds at create time, often
167
+ * pre-expose, and must never bake in a loopback origin) the line stays generic.
168
+ */
169
+ export function welcomePack(opts: { consoleOrigin?: string } = {}): SeedPack {
170
+ const consoleLine = opts.consoleOrigin
171
+ ? `Grab the connection URL from your console at ${opts.consoleOrigin}.`
172
+ : `Grab the connection URL from your console.`;
173
+ const guideNote = { tags: ["guide"] };
174
+ return {
175
+ name: "welcome",
176
+ description:
177
+ "The five-guide welcome ring (Welcome, Capture anything, Tags and the graph, Connect your AI, Yours to keep) — the vault's #guide skill files — plus the capture/guide tags. Seeded by default on new vaults.",
178
+ // capture (Notes surface) + guide (the skill-file tag). Upserts are idempotent.
179
+ tags: [...NOTES_REQUIRED_TAGS, GUIDE_TAG],
180
+ // `[[wikilinks]]` resolve by note path — pending links auto-resolve when
181
+ // the target is created, so order only affects how briefly a link sits
182
+ // unresolved during the seed. Every [[target]] here resolves to a note the
183
+ // default seed writes (the four siblings + [[Getting Started]] from the
184
+ // getting-started pack) — no dangling links on a fresh vault.
185
+ notes: [
186
+ {
187
+ path: WELCOME_PATH,
188
+ tags: ["guide"],
189
+ content: `# ${WELCOME_PATH}
190
+
191
+ This vault is yours.
192
+
193
+ Write anything — ideas, people, plans, wifi passwords. Notes can link to each
194
+ other, like this: [[${CAPTURE_ANYTHING_PATH}]]. Follow that link and keep going — these
195
+ guides form a small web you can wander:
196
+
197
+ - [[${CAPTURE_ANYTHING_PATH}]] — get thoughts in and link them together
198
+ - [[${TAGS_GRAPH_PATH}]] — structure that grows out of what you write
199
+ - [[${CONNECT_AI_PATH}]] — any AI can read and write your vault
200
+ - [[${YOURS_TO_KEEP_PATH}]] — export everything, anytime
201
+
202
+ They're ordinary notes, tagged #guide. Edit them, add to them, or delete the
203
+ lot — the vault is yours, remember.
204
+ `,
205
+ },
206
+ {
207
+ path: CAPTURE_ANYTHING_PATH,
208
+ ...guideNote,
209
+ content: `# ${CAPTURE_ANYTHING_PATH}
210
+
211
+ The one habit that makes a vault work: when a thought strikes, write it down.
212
+ Don't sort it. Don't file it. Let it land.
213
+
214
+ A note can be two words or two pages. The blog idea on a trail, the follow-up
215
+ from a meeting, the dream at 2am — capture first. Organizing can happen later,
216
+ or never; that's what tags and your AI are for.
217
+
218
+ To connect two notes, wrap a note's name in double square brackets:
219
+ [[${WELCOME_PATH}]] is a link back to where you started. If the note
220
+ doesn't exist yet, the link waits patiently and connects the moment it does.
221
+
222
+ That's the whole trick. Storage is easy — connection is everything. A linked
223
+ note is a thought your future self, and your AI, can find again.
224
+
225
+ Next: [[${TAGS_GRAPH_PATH}]].
226
+ `,
227
+ },
228
+ {
229
+ path: TAGS_GRAPH_PATH,
230
+ ...guideNote,
231
+ content: `# ${TAGS_GRAPH_PATH}
232
+
233
+ Notes and links come first. Tags come later, when you notice a pattern.
234
+
235
+ A tag answers "what kind of thing is this?" — #person, #recipe, #idea. Tag a
236
+ few notes the same way and a view lights up: all your people, all your recipes,
237
+ everything half-finished. Your vault starts with just two:
238
+
239
+ - #capture — the Notes app puts it on whatever you write or speak in directly,
240
+ marking your own raw words.
241
+ - #guide — these notes.
242
+
243
+ Underneath, everything is one graph: every note a node, every link an edge.
244
+ Open the graph view and watch it grow; ask a connected AI "what's near this
245
+ note?" and it walks the edges for you.
246
+
247
+ The structure isn't a filing system you design on day one. It grows out of what
248
+ you actually write — so don't over-organize an empty vault. Write, link, and
249
+ let the shape appear.
250
+
251
+ Next: [[${CONNECT_AI_PATH}]].
252
+ `,
253
+ },
254
+ {
255
+ path: CONNECT_AI_PATH,
256
+ ...guideNote,
257
+ content: `# ${CONNECT_AI_PATH}
258
+
259
+ Your vault speaks MCP — an open standard — so any AI can read and write it:
260
+ Claude, ChatGPT, Claude Code, Cursor, or an agent you build. ${consoleLine}
261
+
262
+ Once you're connected, try:
263
+
264
+ - "Read the Getting Started note and help me set this vault up."
265
+ - "What did I want to write about?"
266
+ - "Tag my untagged notes."
267
+
268
+ Your AI sees what you see — the same notes, links, and tags — and what it
269
+ writes lands here, as notes you can read, edit, or delete. There's even a note
270
+ in this vault written for it: [[${GETTING_STARTED_PATH}]], the vault-design brief your
271
+ AI reads so you don't have to.
272
+
273
+ One memory, shared with every AI you choose to connect. That's the point.
274
+
275
+ Next: [[${YOURS_TO_KEEP_PATH}]].
276
+ `,
277
+ },
278
+ {
279
+ path: YOURS_TO_KEEP_PATH,
280
+ ...guideNote,
281
+ content: `# ${YOURS_TO_KEEP_PATH}
282
+
283
+ Everything here — every note, tag, and link — exports as plain markdown files
284
+ with the structure intact. On Parachute Cloud, it's the Export button in your
285
+ console. Self-hosting, it's one command: \`parachute-vault export <dir>\`.
286
+
287
+ What you get is a folder any app can open, and another Parachute can import
288
+ losslessly. Move from our cloud to your own machine, or the other way — same
289
+ software, same format, nothing held hostage between the doors.
290
+
291
+ We build it this way on purpose. A connection you can't leave isn't a
292
+ relationship. You can always leave — which is exactly why you can trust
293
+ staying.
294
+
295
+ Export isn't a feature we grudgingly support; it's the moral center of the
296
+ design. We test it constantly, because a promise you don't test is just a
297
+ hope.
298
+
299
+ Back to [[${WELCOME_PATH}]].
300
+ `,
301
+ },
302
+ ],
303
+ };
304
+ }
305
+
306
+ // ---------------------------------------------------------------------------
307
+ // `getting-started` pack — the AI-facing start-here guide
308
+ // ---------------------------------------------------------------------------
309
+
310
+ /** Canonical path of the seeded onboarding guide. Top-level, title-cased so it
311
+ * reads as a doc and sorts to the top of a casual file listing. */
312
+ export const GETTING_STARTED_PATH = "Getting Started";
313
+
314
+ /** Canonical path of the seeded surface-build starter (the `surface-starter`
315
+ * pack — NOT seeded by default; see the module doc). */
316
+ export const SURFACE_STARTER_PATH = "Surface Starter";
317
+
318
+ /**
319
+ * Body of the seeded `Getting Started` note.
320
+ *
321
+ * Voice: addressed to the connected AI ("you"), warm and practical, SKILL.md-style.
322
+ * ONE note, two labeled parts:
323
+ * - Part 1 — Onboarding the person: lead with a real conversation, leverage
324
+ * prior context, offer to survey their content, do one small real thing
325
+ * first, the data-in menu (import UI / MCP bridge / paste-and-file — NOT the
326
+ * CLI, NOT Obsidian-first), the arrived-with-content branch, and closing the
327
+ * loop by updating the vault description (see DEFAULT_VAULT_DESCRIPTION).
328
+ * - Part 2 — Vault mechanics: the preserved reference — what a vault is + the
329
+ * tool list, tags-vs-paths-vs-schemas, write gotchas, the custom-surface
330
+ * pointer, and "Adapt this note."
331
+ *
332
+ * Deliberately points AIs at the import UI + MCP bridge, never at a
333
+ * `parachute-vault import` CLI (that verb doesn't exist on cloud — the old copy
334
+ * shipped that live bug to every hosted user). Mentions the Surface Starter PACK
335
+ * by name (no wikilink — the note isn't seeded by default, and a dangling
336
+ * `[[Surface Starter]]` would sit unresolved on every fresh vault).
337
+ */
338
+ export const GETTING_STARTED_CONTENT = `# Getting Started
339
+
340
+ This is the **start-here guide** for this Parachute vault — instructions for a
341
+ connected AI (you), like a \`SKILL.md\`. Read it when the vault is new, when
342
+ you're orienting yourself, or when the person says *"help me set up my
343
+ parachute."* You don't need to re-read it every session. It has two parts:
344
+ **how to onboard the person** (start there if they're new) and **vault
345
+ mechanics** (reference for operating the vault). It's a starting point, not a
346
+ script — and it's editable (see "Adapt this note" at the end).
347
+
348
+ ---
349
+
350
+ ## Part 1 — Onboarding the person
351
+
352
+ ### Start with a conversation, not a filing system
353
+
354
+ If this vault is fresh, this is probably the person's first real session with
355
+ it. Don't start building — orient them, briefly, in your own words: this vault
356
+ is **their** memory, one place every AI they choose to connect can read and
357
+ write, and they can export all of it anytime as plain markdown. Then have a
358
+ real conversation:
359
+
360
+ - **How do they imagine using this?** A journal, a project brain, a people
361
+ directory, meeting notes, a commonplace book — or they don't know yet
362
+ (that's fine; capture-first works).
363
+ - **Where do their notes and data live now?** Another app, a pile of markdown,
364
+ another Parachute — or nowhere yet.
365
+ - **What's alive for them right now?** A project, a decision, a trip, a draft.
366
+ The first note should be about something real.
367
+
368
+ **Use what you already know.** If you have memory or context about this person
369
+ from working with them, bring it — propose the two or three kinds of
370
+ information *they specifically* would want here ("you mention a lot of book
371
+ ideas — want a place for those?"). Propose, confirm, then write; never
372
+ bulk-create structure they didn't agree to. You can also **offer to survey**
373
+ what they already have — "want me to look through what you've brought in and
374
+ suggest a few ways to organize it?" — then read a sample and propose, don't
375
+ impose.
376
+
377
+ ### Do one small real thing first
378
+
379
+ The failure mode of every notes tool is over-organizing an empty vault. So:
380
+ **one small real thing this session.** Capture a thought they actually have,
381
+ file one document, or bring in one small batch — then stop and show them what
382
+ happened. Structure grows from real notes (that's Part 2's design vocabulary).
383
+ Setup is a relationship over many sessions, not an install step.
384
+
385
+ Five short guides ship in this vault for the person to read (tagged \`#guide\`):
386
+ [[Welcome to your vault 🪂]], [[Capture anything]], [[Tags and the graph]],
387
+ [[Connect your AI]], and [[Yours to keep]]. Point them there for anything you'd
388
+ otherwise lecture about — they're the return-point between sessions. If a
389
+ session ends mid-setup, leave a breadcrumb (update this note or the vault
390
+ description) so the next one picks up where you left off.
391
+
392
+ ### If this vault already has content
393
+
394
+ Some vaults arrive full — an import, a restore, months of use. Then your first
395
+ job is to **learn it, not seed it**: \`vault-info { include_stats: true }\`,
396
+ \`list-tags\`, read a sample (\`query-notes { search: "..." }\`), then reflect the
397
+ shape back to the person and ask what's missing or wrong. Propose structure
398
+ only once you can describe what's already there.
399
+
400
+ ### Close the loop: describe the vault
401
+
402
+ When the first real structure lands, update the vault description —
403
+ \`vault-info { description: "..." }\` — so every future session (yours or another
404
+ AI's) starts oriented: what this vault is for, its main tags, its conventions.
405
+ The default description says "brand-new vault"; once that stops being true,
406
+ replace it. Keep it a few sentences (the projection carries the schema detail,
407
+ and this note carries the richer conventions — see "Adapt this note"). Bringing
408
+ their existing data in is covered under "Bringing existing notes in" below.
409
+
410
+ ---
411
+
412
+ ## Part 2 — Vault mechanics
413
+
414
+ ## What a Parachute vault is
415
+
416
+ A vault is **notes + tags + links** in one graph, reachable over MCP (you, now),
417
+ a REST API (scripts), and any surface (a UI). It ships *nearly blank* — just a
418
+ small welcome web and the \`capture\` tag the Notes surface uses; no other
419
+ predefined tags or schema. You and the operator design the structure that fits
420
+ *their* life and work. The vault is the engine; the meaning is yours to bring.
421
+
422
+ Core moves you already have as MCP tools:
423
+ - \`create-note\` / \`update-note\` / \`delete-note\` — write notes (single or batch).
424
+ - \`query-notes\` — by id/path, by tag, full-text \`search\`, or graph \`near\` a note.
425
+ - \`list-tags\` / \`update-tag\` / \`delete-tag\` — manage the tag vocabulary + schemas.
426
+ - \`find-path\` — shortest link path between two notes.
427
+ - \`vault-info\` — refresh the live schema/stats projection any time.
428
+
429
+ \`[[wikilinks]]\` in note content auto-link to the note at that path — use them
430
+ freely; they resolve even if the target is created later.
431
+
432
+ ## Tags vs paths vs schemas — the design vocabulary
433
+
434
+ These three axes are the heart of vault design. Use the right one for the job:
435
+
436
+ - **Tags = types / membership.** A tag answers *"what kind of thing is this?"*
437
+ (\`#person\`, \`#meeting\`, \`#project\`). Queries **expand over tags**: a tag can
438
+ declare \`parent_names\` so \`tag:X\` also returns its subtypes (e.g. tagging a
439
+ note \`#meeting/standup\` with \`parent_names: [meeting]\` means \`query-notes
440
+ { tag: "meeting" }\` finds it). Tags are how you ask *"show me all my people."*
441
+ This is the primary structure — reach for a tag first.
442
+
443
+ - **Paths = organization / filing.** A path (\`Projects/Acme/Kickoff\`) is *where*
444
+ a note lives — a human-browsable address, unique per note. Paths are for
445
+ folders and named docs (like this one). They do **not** drive type queries;
446
+ don't encode meaning in a path that a tag should carry. A note can have a
447
+ path, tags, or both.
448
+
449
+ - **Schemas = typed metadata fields.** Attach a schema to a tag (via
450
+ \`update-tag\`) to declare typed metadata fields — e.g. \`#meeting\` with a
451
+ \`held_on\` date, \`#person\` with an \`email\`. Each field can **optionally** be
452
+ marked \`indexed: true\` to make it **queryable with operators** (\`query-notes
453
+ { tag: "meeting", metadata: { held_on: { gte: "2026-01-01" } } }\`); indexing
454
+ is opt-in per field, not automatic. Add a schema (and index a field) when you
455
+ find yourself wanting to filter or sort on a value, not before.
456
+
457
+ Rule of thumb: **type with tags, file with paths, make-it-queryable with
458
+ schemas.** Start minimal — invent tags as real notes need them, declare a
459
+ schema only when a query demands it. Over-designing an empty vault is the
460
+ common mistake.
461
+
462
+ Declaring a schema is one \`update-tag\` call — the \`fields\` object maps each
463
+ field name to \`{ type, enum?, indexed? }\` (\`type\` is \`"string"\`, \`"boolean"\`,
464
+ or \`"integer"\`):
465
+
466
+ \`\`\`
467
+ update-tag {
468
+ tag: "meeting",
469
+ description: "A meeting with notes",
470
+ fields: {
471
+ held_on: { type: "string", indexed: true }, // queryable with operators
472
+ status: { type: "string", enum: ["scheduled", "done"] }, // first enum value is the default
473
+ rating: { type: "integer" }
474
+ }
475
+ }
476
+ \`\`\`
477
+
478
+ \`fields\` is **merged** (new keys added, existing replaced); \`parent_names\` and
479
+ \`relationships\` are replaced wholesale when passed. Only \`indexed: true\` fields
480
+ support operator queries (\`metadata: { held_on: { gte: "..." } }\`) and
481
+ \`order_by\`; all tags declaring the same field must agree on its \`type\` and
482
+ \`indexed\` flag.
483
+
484
+ ## Write gotchas
485
+
486
+ A few behaviors worth knowing before you write at scale:
487
+
488
+ - **\`update-note\` requires optimistic concurrency by default.** Pass
489
+ \`if_updated_at\` with the \`updated_at\` you last read; a mismatch returns a
490
+ conflict error (re-read, reconcile, retry). For bulk/scripted writes where
491
+ concurrency is known-safe, pass \`force: true\` to waive the *requirement to
492
+ supply* it. \`append\`/\`prepend\`-only updates are exempt (no-conflict-by-design).
493
+ - **A schema field's default is filled in on write, so it shows up even when you
494
+ didn't set it.** When a note gets a tag whose schema declares a field, the
495
+ missing field is back-filled: an \`enum\` field → its **first listed value**, an
496
+ \`integer\` → \`0\`, a \`boolean\` → \`false\`, a plain string → \`""\`. So a
497
+ \`rating: { type: "integer" }\` reads as \`0\` on notes nobody rated — that \`0\`
498
+ is "unset," not "rated zero." Order an \`enum\`'s values so the first is a sane
499
+ default, and don't read a back-filled \`0\`/\`""\`/\`false\` as a real value.
500
+ - **Validation is advisory, never blocking.** A type/enum mismatch comes back as
501
+ a \`validation_status\` warning on the write response — the write still lands.
502
+ Read those warnings and self-correct on the next turn.
503
+
504
+ (Full design guide, with copy-paste examples: https://parachute.computer/scripting/)
505
+
506
+ ## Bringing existing notes in
507
+
508
+ If the person already keeps notes somewhere, help them bring what matters —
509
+ start with what's alive for them, not the whole archive. What works today:
510
+
511
+ - **Connect the source over MCP (the flexible path).** If their AI client can
512
+ add other MCP servers — a Google Drive server, a Notion server, a filesystem
513
+ server — connect it alongside this vault in the same session. Then read from
514
+ there and \`create-note\` here: a selective, conversational migration, no
515
+ export file needed.
516
+ - **Paste and file.** They paste anything — a doc, a list, an export — and you
517
+ \`create-note\` it (batch mode for many at once).
518
+ - **Import a Parachute export.** Moving between Parachute vaults (cloud ⇄
519
+ self-host) is lossless — on Parachute Cloud, the console's Import button —
520
+ ids, tags, links, schemas, and attachments round-trip.
521
+
522
+ Bringing a whole Obsidian or Markdown library in is getting easier, and a
523
+ flexible import is on the way; for now the connect-over-MCP and paste paths
524
+ handle it without anyone touching a terminal. Whatever they bring, offer to
525
+ survey it and suggest a first structure — read a sample, propose a small tag
526
+ vocabulary, confirm, then apply. Don't impose structure silently.
527
+
528
+ After bringing content in, orient: \`vault-info\` for the schema picture,
529
+ \`list-tags\` for the vocabulary that arrived, \`query-notes { search: "..." }\`
530
+ to spot-check.
531
+
532
+ ## Later: a custom surface
533
+
534
+ Building a custom UI over the vault (a dashboard, a notes app) is usually **not**
535
+ the starting point — get the notes and structure right first. When the person
536
+ wants one, add the **Surface Starter** guide to this vault — an optional pack
537
+ (not installed by default) that covers building a surface with
538
+ \`@openparachute/surface-client\` + \`@openparachute/surface-render\`. On Parachute
539
+ Cloud it's the console's add-pack affordance.
540
+
541
+ ## Adapt this note
542
+
543
+ This guide is a **default starting point, not gospel** — edit it to fit this
544
+ vault. As you and the operator settle on a tag vocabulary, conventions, or a
545
+ surface, you can record that here so a future session inherits the current shape
546
+ of the vault instead of this blank-slate default. Useful things to capture:
547
+ - the tag vocabulary you've settled on and what each tag means;
548
+ - naming/path conventions for this vault;
549
+ - which schemas exist and why;
550
+ - anything a fresh AI would need to be immediately useful.
551
+
552
+ Treat setup as a relationship, not a one-time install.
553
+ `;
554
+
555
+ export const GETTING_STARTED_PACK: SeedPack = {
556
+ name: "getting-started",
557
+ description:
558
+ "The AI-facing start-here guide: vault design vocabulary (tags/paths/schemas), imports, write gotchas. Seeded by default on new vaults.",
559
+ // Declares GUIDE_TAG too — each pack is self-sufficient; the upsert converges
560
+ // with the welcome pack's. This guide is written FOR the AI.
561
+ tags: [GUIDE_TAG],
562
+ notes: [
563
+ {
564
+ path: GETTING_STARTED_PATH,
565
+ tags: ["guide"],
566
+ content: GETTING_STARTED_CONTENT,
567
+ },
568
+ ],
569
+ };
570
+
571
+ // ---------------------------------------------------------------------------
572
+ // `surface-starter` pack — the surface-build guide (opt-in)
573
+ // ---------------------------------------------------------------------------
574
+
575
+ /**
576
+ * Body of the seeded `Surface Starter` note.
577
+ *
578
+ * A concise, *living* starter prompt for building a custom surface (UI) over
579
+ * the vault using the published surface packages. Tells the AI to import the
580
+ * packages rather than hand-roll OAuth/API/rendering.
581
+ */
582
+ export const SURFACE_STARTER_CONTENT = `# Surface Starter
583
+
584
+ A **surface** is a custom UI over this vault — a dashboard, a notes app, a
585
+ single-purpose tool. This note is a living starter for building one *with the
586
+ operator*. Update it as you settle on a stack, conventions, or a deployed
587
+ surface for this vault.
588
+
589
+ ## ⚠️ Build a surface in your editor, not from this session
590
+
591
+ A surface runs **in a browser**: it needs a real OAuth round-trip (a redirect to
592
+ the hub's consent screen and back), a dev server to serve the app, and a CORS
593
+ origin the hub trusts. **None of that exists in this MCP/chat session** — there's
594
+ no browser, no redirect, no dev server. So **don't try to "run" a surface from
595
+ the vault session.** Build it in **Claude Code (or your editor)** against a local
596
+ dev server (\`vite\`/\`bun dev\`), sign in through the browser there, and iterate.
597
+ From *this* session you design the vault structure the surface will consume and
598
+ write the code — you can't exercise the OAuth/render loop here.
599
+
600
+ ## Don't hand-roll the plumbing
601
+
602
+ Two published packages do the heavy lifting — import them instead of writing
603
+ OAuth, the vault API client, or note rendering by hand:
604
+
605
+ - **\`@openparachute/surface-client\`** — \`createVaultSurface(...)\` wires up
606
+ Parachute OAuth (sign-in on first connect) and a typed vault API client
607
+ (query/create/update notes, tags, links) so your app code just calls methods.
608
+ - **\`@openparachute/surface-render\`** — \`<NoteRenderer>\` and friends render note
609
+ content (Markdown, wikilinks, embeds) the way the rest of the ecosystem does,
610
+ so your surface looks native without re-implementing the renderer.
611
+
612
+ ## Minimal end-to-end (config → sign-in → query → render)
613
+
614
+ A React sketch wiring all four steps. \`createVaultSurface\` is the only required
615
+ config (its \`clientName\` is the sole required option; \`hubUrl\` defaults to the
616
+ page origin, \`vaultName\` to \`"default"\`, \`scope\` to \`"vault:read vault:write"\`).
617
+ \`getClient()\` returns a \`VaultClient\` (or \`null\` until signed in) whose
618
+ \`queryNotes()\` takes the same query grammar you use over MCP. See
619
+ [[Getting Started]] / \`vault-info\` for this vault's NAME and hub origin.
620
+
621
+ \`\`\`tsx
622
+ import { useEffect, useState } from "react";
623
+ import { createVaultSurface, type Note } from "@openparachute/surface-client";
624
+ import { NoteRenderer } from "@openparachute/surface-render";
625
+
626
+ // One surface per (hub, vault) config. clientName shows on the consent screen.
627
+ const surface = createVaultSurface({
628
+ clientName: "My Vault Surface",
629
+ hubUrl: "https://your-hub.example", // omit to default to window.location.origin
630
+ vaultName: "default", // this vault's name (see vault-info)
631
+ });
632
+
633
+ export function App() {
634
+ const [notes, setNotes] = useState<Note[] | null>(null);
635
+
636
+ useEffect(() => {
637
+ (async () => {
638
+ // OAuth: finish a redirect callback if we're on it, else send the browser
639
+ // off to sign in. handleCallback() needs BOTH code + state, so guard on
640
+ // both. (Real apps route /oauth/callback to its own component.)
641
+ const q = new URLSearchParams(location.search);
642
+ if (q.get("code") && q.get("state")) await surface.handleCallback();
643
+ // getClient() builds a FRESH VaultClient on each call — fine here (one-shot
644
+ // effect); in a real component keep it in state/ref, don't call it per render.
645
+ const client = surface.getClient(); // VaultClient | null (null = not signed in)
646
+ if (!client) return void surface.login();
647
+ setNotes(await client.queryNotes({ tag: "note", limit: 20 }));
648
+ })();
649
+ }, []);
650
+
651
+ if (!notes) return <p>Connecting…</p>;
652
+ return (
653
+ <>
654
+ {notes.map((n) => (
655
+ // resolve maps a [[wikilink]] target → { href, exists } (or null = inert).
656
+ // You own this href's trust boundary — keep it a fragment (or validate the
657
+ // target). Don't build a raw passthrough href: a vault note could carry a
658
+ // javascript: target.
659
+ <NoteRenderer
660
+ key={n.id}
661
+ note={n}
662
+ resolve={(target) => ({ href: \`#/n/\${encodeURIComponent(target)}\`, exists: true })}
663
+ />
664
+ ))}
665
+ </>
666
+ );
667
+ }
668
+ \`\`\`
669
+
670
+ That's the whole spine. \`<NoteRenderer>\` also takes \`linkComponent\` (your
671
+ router's \`<Link>\`) and \`fetchBlob\` (\`(url) => Promise<Blob>\`, for auth'd
672
+ image/audio embeds) when you need them — both optional.
673
+
674
+ ## Build order
675
+
676
+ 1. **Auth + data first.** Stand up \`createVaultSurface\` pointed at this vault;
677
+ confirm you can sign in and \`queryNotes\` round-trips before any UI polish.
678
+ 2. **Render next.** Drop in \`<NoteRenderer>\` to display note content; wire
679
+ wikilink/embed resolution through the package, not by hand.
680
+ 3. **UX last.** Layout, navigation, and the surface's actual purpose — now that
681
+ auth, data, and rendering are solid.
682
+
683
+ ## Design it around this vault's structure
684
+
685
+ A good surface is shaped by the vault's tags + schemas (see [[Getting Started]]
686
+ for the tags-vs-paths-vs-schemas design vocabulary). Query by the tags that
687
+ matter to the operator; surface the indexed fields they filter on. If the vault
688
+ doesn't yet have the structure a surface wants, that's a signal to design tags +
689
+ schemas first.
690
+
691
+ ## Adapt this note
692
+
693
+ When you build a surface for this vault, record it here: what it's for, the
694
+ stack, how to run it, the queries it depends on. The next session should be able
695
+ to pick it up from this note.
696
+
697
+ (More on the surface stack + examples: https://parachute.computer/scripting/)
698
+ `;
699
+
700
+ export const SURFACE_STARTER_PACK: SeedPack = {
701
+ name: "surface-starter",
702
+ description:
703
+ "A living starter guide for building a custom surface (UI) over this vault with the published surface packages. Opt-in — not seeded by default.",
704
+ // A guide too (written for the AI) — declares GUIDE_TAG so the opt-in
705
+ // add-pack path lands the tag even if this pack is applied on its own.
706
+ tags: [GUIDE_TAG],
707
+ notes: [
708
+ {
709
+ path: SURFACE_STARTER_PATH,
710
+ tags: ["guide"],
711
+ content: SURFACE_STARTER_CONTENT,
712
+ },
713
+ ],
714
+ };
715
+
716
+ // ---------------------------------------------------------------------------
717
+ // Registry
718
+ // ---------------------------------------------------------------------------
719
+
720
+ /** The stable pack-name handles, in listing order. */
721
+ export const SEED_PACK_NAMES = [
722
+ "welcome",
723
+ "getting-started",
724
+ "surface-starter",
725
+ ] as const;
726
+
727
+ export type SeedPackName = (typeof SEED_PACK_NAMES)[number];
728
+
729
+ /** List the available packs (name + description) for CLI/console listings. */
730
+ export function listSeedPacks(): ReadonlyArray<{ name: string; description: string }> {
731
+ return SEED_PACK_NAMES.map((name) => {
732
+ const pack = getSeedPack(name)!;
733
+ return { name: pack.name, description: pack.description };
734
+ });
735
+ }
736
+
737
+ /**
738
+ * Resolve a pack by name. `opts` only affects the `welcome` pack (its
739
+ * Connect-your-AI note can name the operator's console origin). Returns
740
+ * `null` for an unknown name — callers list the available packs.
741
+ */
742
+ export function getSeedPack(
743
+ name: string,
744
+ opts: { consoleOrigin?: string } = {},
745
+ ): SeedPack | null {
746
+ switch (name) {
747
+ case "welcome":
748
+ return welcomePack(opts);
749
+ case "getting-started":
750
+ return GETTING_STARTED_PACK;
751
+ case "surface-starter":
752
+ return SURFACE_STARTER_PACK;
753
+ default:
754
+ return null;
755
+ }
756
+ }
757
+
758
+ // ---------------------------------------------------------------------------
759
+ // Applier
760
+ // ---------------------------------------------------------------------------
761
+
762
+ export interface ApplySeedPackResult {
763
+ /** The pack that was applied. */
764
+ pack: string;
765
+ /** Tag names upserted (upserts are idempotent; always every declared tag). */
766
+ tags: string[];
767
+ /** Note paths written this run (absent ones). */
768
+ seededNotes: string[];
769
+ /** Note paths skipped because a note already lives there (idempotency). */
770
+ skippedNotes: string[];
771
+ }
772
+
773
+ /**
774
+ * Apply a pack to `store`, idempotently per item: each note is created only
775
+ * when no note exists at its path, and the tag upserts converge on the same
776
+ * rows — so a re-run can never duplicate, and never clobbers a note the
777
+ * operator/AI has since edited or recreated.
778
+ *
779
+ * Errors propagate — best-effort semantics (seed-must-never-fail-a-create)
780
+ * belong to the caller, which knows its failure policy.
781
+ *
782
+ * Uses only single-item Store calls — on the cloud DO they route through the
783
+ * real `transactionSync` seam, keeping the conformance suite's zero-raw-BEGIN
784
+ * tripwire honest.
785
+ */
786
+ export async function applySeedPack(
787
+ store: Store,
788
+ pack: SeedPack,
789
+ ): Promise<ApplySeedPackResult> {
790
+ const result: ApplySeedPackResult = {
791
+ pack: pack.name,
792
+ tags: [],
793
+ seededNotes: [],
794
+ skippedNotes: [],
795
+ };
796
+
797
+ // Parents first so `parent_names` reads naturally in logs (the store accepts
798
+ // forward references, but the order matches the conceptual model).
799
+ for (const decl of pack.tags) {
800
+ await store.upsertTagRecord(decl.name, {
801
+ description: decl.description,
802
+ ...(decl.parent_names ? { parent_names: decl.parent_names } : {}),
803
+ ...(decl.fields ? { fields: decl.fields } : {}),
804
+ });
805
+ result.tags.push(decl.name);
806
+ }
807
+
808
+ for (const note of pack.notes) {
809
+ const existing = await store.getNoteByPath(note.path);
810
+ if (existing) {
811
+ result.skippedNotes.push(note.path);
812
+ continue;
813
+ }
814
+ await store.createNote(note.content, {
815
+ path: note.path,
816
+ tags: note.tags,
817
+ metadata: note.metadata,
818
+ });
819
+ result.seededNotes.push(note.path);
820
+ }
821
+
822
+ return result;
823
+ }