@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.
- package/.parachute/module.json +1 -0
- package/core/src/core.test.ts +7 -7
- package/core/src/do-param-cap.test.ts +161 -0
- package/core/src/links.ts +8 -13
- package/core/src/mcp.ts +1 -1
- package/core/src/notes.ts +33 -15
- package/core/src/onboarding.ts +14 -296
- package/core/src/schema.ts +5 -5
- package/core/src/seed-packs.test.ts +357 -0
- package/core/src/seed-packs.ts +823 -0
- package/core/src/sql-in.test.ts +58 -0
- package/core/src/sql-in.ts +76 -0
- package/core/src/tag-hierarchy.ts +1 -1
- package/core/src/tag-schemas.ts +2 -2
- package/core/src/transcription/provider.ts +141 -0
- package/core/src/types.ts +1 -1
- package/core/src/vault-projection.ts +1 -1
- package/core/src/wikilinks.ts +10 -4
- package/package.json +1 -1
- package/src/add-pack.test.ts +142 -0
- package/src/admin-spa.ts +2 -1
- package/src/auth.ts +1 -1
- package/src/cli.ts +806 -7
- package/src/export-watch.ts +1 -1
- package/src/live-frame-parity.test.ts +201 -0
- package/src/module-manifest.ts +8 -0
- package/src/onboarding-seed.test.ts +188 -40
- package/src/onboarding-seed.ts +41 -46
- package/src/routes.ts +20 -3
- package/src/routing.test.ts +2 -2
- package/src/routing.ts +18 -6
- package/src/self-register.test.ts +19 -0
- package/src/self-register.ts +6 -1
- package/src/server.ts +133 -31
- package/src/services-manifest.ts +8 -0
- package/src/subscriptions.ts +100 -42
- package/src/tag-scope.ts +3 -3
- package/src/test-support/live-frame-corpus.ts +72 -0
- package/src/token-store.ts +2 -2
- package/src/transcription/build.test.ts +305 -0
- package/src/transcription/build.ts +332 -0
- package/src/transcription/capability.test.ts +118 -0
- package/src/transcription/capability.ts +96 -0
- package/src/transcription/download.test.ts +101 -0
- package/src/transcription/download.ts +71 -0
- package/src/transcription/install-python.test.ts +366 -0
- package/src/transcription/install-python.ts +471 -0
- package/src/transcription/install.test.ts +167 -0
- package/src/transcription/install.ts +296 -0
- package/src/transcription/providers/onnx-asr.test.ts +229 -0
- package/src/transcription/providers/onnx-asr.ts +239 -0
- package/src/transcription/providers/parakeet-mlx.test.ts +290 -0
- package/src/transcription/providers/parakeet-mlx.ts +242 -0
- package/src/transcription/providers/scribe-http.test.ts +195 -0
- package/src/transcription/providers/scribe-http.ts +144 -0
- package/src/transcription/providers/transcribe-cpp.test.ts +314 -0
- package/src/transcription/providers/transcribe-cpp.ts +293 -0
- package/src/transcription/select.test.ts +299 -0
- package/src/transcription/select.ts +397 -0
- package/src/transcription/tiers.test.ts +197 -0
- package/src/transcription/tiers.ts +184 -0
- package/src/transcription-worker.test.ts +44 -0
- package/src/transcription-worker.ts +57 -122
- package/src/vault-create.test.ts +43 -10
- package/src/vault.test.ts +49 -1
- package/src/ws-server.ts +408 -0
- package/src/ws-subscribe.test.ts +474 -0
- package/src/ws-subscribe.ts +242 -0
- package/src/subscribe.test.ts +0 -609
- 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
|
+
}
|