@vibes.diy/prompts 14.3.76 → 14.3.78

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/llms/access.js CHANGED
@@ -1,7 +1,7 @@
1
1
  export const accessConfig = {
2
2
  name: "access",
3
3
  label: "access-js",
4
- description: "server-side access.js file — ONLY for apps that must RESTRICT who can read or write: private per-user data, membership/roles, invite or approval-to-join flows, moderation, DMs. Do NOT select for ordinary single-user apps or fully public/shared-with-everyone apps, which need no access.js at all",
4
+ description: "server-side access.js file — the record rules every app ships with: who reads and writes each record, the creator granted at creation, invites into an object, membership/roles, approval-to-join, moderation, private per-user data, DMs. Name it on every first build; the one app that leaves it out is the one whose person asked in their own words for a single-user app or no sharing",
5
5
  cues: [
6
6
  "private",
7
7
  "privately",
@@ -1 +1 @@
1
- {"version":3,"file":"access.js","sourceRoot":"","sources":["../../jsr/llms/access.ts"],"names":[],"mappings":"AAMA,MAAM,CAAC,MAAM,YAAY,GAAc;IACrC,IAAI,EAAE,QAAQ;IACd,KAAK,EAAE,WAAW;IAClB,WAAW,EACT,wSAAwS;IAC1S,IAAI,EAAE;QACJ,SAAS;QACT,WAAW;QACX,SAAS;QACT,OAAO;QACP,QAAQ;QACR,QAAQ;QACR,SAAS;QACT,MAAM;QACN,OAAO;QACP,QAAQ;QACR,SAAS;QACT,YAAY;QACZ,aAAa;QACb,MAAM;QACN,OAAO;QACP,YAAY;QACZ,aAAa;QACb,cAAc;QACd,QAAQ;QACR,SAAS;QACT,SAAS;QACT,YAAY;QACZ,aAAa;QACb,aAAa;QACb,cAAc;QACd,eAAe;QACf,cAAc;QACd,eAAe;QACf,eAAe;QACf,IAAI;QACJ,KAAK;QACL,gBAAgB;QAChB,iBAAiB;QACjB,UAAU;QACV,WAAW;QACX,SAAS;QACT,WAAW;QACX,YAAY;QACZ,YAAY;QACZ,UAAU;QACV,gBAAgB;QAChB,aAAa;QACb,cAAc;QACd,YAAY;QACZ,SAAS;QACT,cAAc;QACd,eAAe;QACf,iBAAiB;QACjB,YAAY;KACb;CACF,CAAC"}
1
+ {"version":3,"file":"access.js","sourceRoot":"","sources":["../../jsr/llms/access.ts"],"names":[],"mappings":"AAMA,MAAM,CAAC,MAAM,YAAY,GAAc;IACrC,IAAI,EAAE,QAAQ;IACd,KAAK,EAAE,WAAW;IAClB,WAAW,EACT,8XAA8X;IAChY,IAAI,EAAE;QACJ,SAAS;QACT,WAAW;QACX,SAAS;QACT,OAAO;QACP,QAAQ;QACR,QAAQ;QACR,SAAS;QACT,MAAM;QACN,OAAO;QACP,QAAQ;QACR,SAAS;QACT,YAAY;QACZ,aAAa;QACb,MAAM;QACN,OAAO;QACP,YAAY;QACZ,aAAa;QACb,cAAc;QACd,QAAQ;QACR,SAAS;QACT,SAAS;QACT,YAAY;QACZ,aAAa;QACb,aAAa;QACb,cAAc;QACd,eAAe;QACf,cAAc;QACd,eAAe;QACf,eAAe;QACf,IAAI;QACJ,KAAK;QACL,gBAAgB;QAChB,iBAAiB;QACjB,UAAU;QACV,WAAW;QACX,SAAS;QACT,WAAW;QACX,YAAY;QACZ,YAAY;QACZ,UAAU;QACV,gBAAgB;QAChB,aAAa;QACb,cAAc;QACd,YAAY;QACZ,SAAS;QACT,cAAc;QACd,eAAe;QACf,iBAAiB;QACjB,YAAY;KACb;CACF,CAAC"}
package/llms/access.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Access Control (`access.js`) — permission design
2
2
 
3
- This doc is in view because the app is getting its `access.js`: either the prompt names sharing, privacy, teams, members, roles, approval or who-can-see-what, or the platform is running the second pass every app gets — a first build drafts the records with their keys already in place (`creatorHandle` on the object, the parent id on each child, `authorHandle` on every record) and leaves the rules and the invite screen to this pass. Every app gets record rules unless the person asked for none in their own words, and the group shape taught below is the resting default. A person the ask names is let in by a record — see _A person the ask names is let in by a record_ below — so the build states the membership record's shape and the invite handler that writes it.
3
+ This doc is in view because the app is getting its `access.js`: a new app's first build writes its rules with its screens, and a later edit adds or changes them. Either way the records carry their keys (`creatorHandle` on the object, the parent id on each child, `authorHandle` on every record), and this build writes the rules that key on them. The rules and those keys are plumbing with nothing on screen of their own; the invite screen joins the build when the ask names other people, sharing, a group or a person by name, and is otherwise offered as an option after the build. Every app gets record rules unless the person asked for none in their own words, and the group shape taught below is the resting default. A person the ask names is let in by a record — see _A person the ask names is let in by a record_ below — so the build states the membership record's shape and the invite handler that writes it.
4
4
 
5
- **On a second pass over an app that already exists, the pass owes only what the rules need.** Read `App.jsx` for the object type, the field that names its creator and the parent id its children carry; name the channels from those records; write the complete `access.js`; then change `App.jsx` only where the rules require it — the invite screen, stable creator membership, typed content queries, and any write surface not yet gated on `can` — with SEARCH/REPLACE edits. A file the rules do not touch is not emitted, and stays as it is.
5
+ **When an edit adds the rules to an app that already exists, the edit owes only what the rules need.** Read `App.jsx` for the object type, the field that names its creator and the parent id its children carry; name the channels from those records; write the complete `access.js`; then change `App.jsx` only where the rules require it — stable creator membership, typed content queries, any write surface not yet gated on `can`, and the invite screen when the ask names other people, sharing or a group — with SEARCH/REPLACE edits. A file the rules do not touch is not emitted, and stays as it is.
6
6
 
7
7
  **Honest copy:** until `access.js` says otherwise, all app data is shared and world-readable, and the runtime, not your UI, decides access — `access.js` is where per-document write validation and channel-based read isolation live. Write UI copy that describes what the rules actually do.
8
8
 
@@ -59,7 +59,7 @@ type AccessDescriptor = {
59
59
 
60
60
  ### Person or place: `audience` or `grant`
61
61
 
62
- The question that decides which one a doc wants is whose thing it is: **personal content belongs to the PERSON; a contribution to a shared artifact belongs to the PLACE.** Someone's picks, favorites, streaks, or profile items are the person's — give them an `audience` arm keyed on their own handle, so the reach tracks that person's settings and follows them everywhere. Guestbook entries, posts on a communal wall, items on a shared board are the place's — route them to the place's channel with `grant.public` or explicit grants exactly as the rest of this doc shows, unaffected by the author's later profile flips: a guestbook entry doesn't vanish because the signer went private.
62
+ The question that decides which one a doc wants is whose thing it is: **personal content belongs to the PERSON; a contribution to a shared artifact belongs to the PLACE.** Someone's picks, favorites, badges, or profile items are the person's — give them an `audience` arm keyed on their own handle, so the reach tracks that person's settings and follows them everywhere. Guestbook entries, posts on a communal wall, items on a shared board are the place's — route them to the place's channel with `grant.public` or explicit grants exactly as the rest of this doc shows, unaffected by the author's later profile flips: a guestbook entry doesn't vanish because the signer went private.
63
63
 
64
64
  ### Two controls: a followers checkbox and an individual buddy picker
65
65
 
@@ -423,11 +423,11 @@ export function board(doc, oldDoc, user, ctx) {
423
423
 
424
424
  `ctx.requireAccess(channel)` gates on **membership** (a `grant.users`/`grant.roles` grant), not `grant.public` (read-only) — so an open feed anyone signed-in may post to must not gate writes on it; check the author and route the doc. For writes needing no sign-in ("anyone can sign/submit"), return `allowAnonymous: true` instead of throwing on `!user`. A grant/share/request doc must also return `channels` — a channel-less result is rejected. On updates of shared-visible docs, keep ownership immutable (`doc.authorHandle !== oldDoc.authorHandle` → throw) rather than requiring the updater to BE the author — image version appends run as the viewing user, and an author-only update gate turns every other viewer's generation into a billed deny-retry. Reserve author-only updates for genuinely private per-user docs; on shared docs, let a non-author through only when `ctx.isImgGenVersionAppend(doc, oldDoc)` accepts the write (as `wall.post` above shows) — it permits exactly one legitimate ImgGen version append and requires every other field unchanged.
425
425
 
426
- **Build the permission model around what a newcomer should be able to do.** When a stranger opens the app, they should immediately be able to do the thing it's _for_ — add their own todos, post a note, drop a pin, join a shared canvas. So the default is: every signed-in visitor is a first-class participant who creates their own objects and edits what they created (`doc.authorHandle === user.userHandle`, checking `oldDoc` on updates), from first load, with no one needing to let them in.
426
+ **Build the permission model around what a newcomer should be able to do.** When a stranger opens the app, they should immediately be able to do the thing it's _for_ — add their own entries, post a note, drop a pin, join a shared canvas. So the default is: every signed-in visitor is a first-class participant who creates their own objects and edits what they created (`doc.authorHandle === user.userHandle`, checking `oldDoc` on updates), from first load, with no one needing to let them in.
427
427
 
428
428
  **When the person says "no sharing", "no access control" or "keep it single-user", that is the one case where `access.js` is left out altogether:** the app ships with no rules file, and the object id and `authorHandle` its records already carry are what let rules be added later without touching a doc.
429
429
 
430
- **When the person asks for private per-user permissions — "each person sees only their own", "private to every user" — every visitor gets their own private space on a single per-user channel.** A todo list, a daily habit tracker, a reading list, a diary, a notes app, a workout log, or a budget where the data is one person's own routes every doc that visitor creates to the one channel keyed on their handle — `user:${user.userHandle}` — self-granted so only they read it, with `authorHandle: user.userHandle` fixed at create and held immutable on update (`oldDoc.authorHandle === user.userHandle`). Their whole collection lives on that single private channel, reachable from first load, each visitor's space entirely their own:
430
+ **When the person asks for private per-user permissions — "each person sees only their own", "private to every user" — every visitor gets their own private space on a single per-user channel.** A sleep log, a houseplant watering schedule, a savings jar, a notes app or a workout log where the data is one person's own routes every doc that visitor creates to the one channel keyed on their handle — `user:${user.userHandle}` — self-granted so only they read it, with `authorHandle: user.userHandle` fixed at create and held immutable on update (`oldDoc.authorHandle === user.userHandle`). Their whole collection lives on that single private channel, reachable from first load, each visitor's space entirely their own:
431
431
 
432
432
  ### `access.js`
433
433
  ```js partial
@@ -451,50 +451,50 @@ export function notes(doc, oldDoc, user, ctx) {
451
451
 
452
452
  Every item a visitor creates lands on their own `user:<handle>` channel, private to them from the first load. Pair it with a small "Only you can see this" cue near their content. This is the shape whenever the app is one person's own collection kept for themselves — the whole space is one private channel, no membership step in the way. The word "personal" routes on what the data is FOR: a personal tracker/journal/notes app is this private per-user shape, while a personal blog or portfolio exists to be READ — that one is the publication shape below (public read + author-owned posts, a roster of one), not a private channel.
453
453
 
454
- **When the prompt asks to share, invite, or collaborate — bring in a partner, a buddy, a friend, a team, or a group that co-edits — each shared thing becomes its own object others can be invited into.** A shopping list you invite your partner to, a board a group co-edits, a document you open to a collaborator: route that shared thing to its **own object channel** (`list:<id>`/`board:<id>`) and self-grant it at creation (`grant: { users: { [user.userHandle]: [ch] } }`), with the `authorHandle` create + `oldDoc` author checks so each item stays on its object. Then let the creator invite a chosen friend in: a `share` doc the creator authors grants that friend the same channel (`grant: { users: { [doc.invitee]: [ch] } }`), so they collaborate on that one list — sharing a single list or a whole space is the same grant at a different node of the object graph. A wall, guestbook, or map where each visitor adds their _own_ items is author-owned writes + public read: any signed-in visitor authors their own and everyone reads.
454
+ **When the prompt asks to share, invite, or collaborate — bring in a partner, a buddy, a friend, a team, or a group that co-edits — each shared thing becomes its own object others can be invited into.** A seed-swap shelf you invite your sibling to, a board a group co-edits, a document you open to a collaborator: route that shared thing to its **own object channel** (`list:<id>`/`board:<id>`) and self-grant it at creation (`grant: { users: { [user.userHandle]: [ch] } }`), with the `authorHandle` create + `oldDoc` author checks so each item stays on its object. Then let the creator invite a chosen friend in: a `share` doc the creator authors grants that friend the same channel (`grant: { users: { [doc.invitee]: [ch] } }`), so they collaborate on that one list — sharing a single list or a whole space is the same grant at a different node of the object graph. A wall, guestbook, or map where each visitor adds their _own_ items is author-owned writes + public read: any signed-in visitor authors their own and everyone reads.
455
455
 
456
456
  **When a user's work is private by default, show it — and offer a way to publish.** If everything a user does routes to a channel only they can read (a per-user `user:<handle>`, a private journal/notes/tracker with no `grant.public`), the UI must say so: a small, persistent "Only you can see this" / "Private to you" cue near their content, so no one wonders who's watching their unfinished work. Then, where sharing fits the app, give them a publish control — but publish at the granularity of a **channel**, not a doc: channels are the unit of read isolation, so adding `grant.public` to the shared `user:<handle>` channel would expose _every_ private item on it, not just the one they meant to share. To publish a single item, route it to its **own** channel and flip only that channel's read-grant from a `visibility` field the access fn reads — exactly the per-item-channel shape the worked example below uses (`const ch = \`entry:${doc._id}\`; const grant = { users: { [user.userHandle]: [ch] } }; if (doc.visibility === "public") grant.public = [ch];`) — for anonymous visitors, or grant a shared app channel for all granted members. Gate the control on `useVibe(dbName).can`, and reflect the result back in the affordance ("Published — anyone can see this", with an unpublish to flip it back). Making the user's _whole_ space public is fine when that's the intent; silently leaking their other private items by publishing one is the trap to avoid. **If the person asked, in their own words, to keep it to themselves — "only I", "just me", "nobody else" — do not add publish/share controls.** A privacy-sensitive domain on its own (a journal, a health tracker) is not that ask: it gets the two controls from _Two controls_ above, off until the person says so. Keep publish opt-in: for only-me apps, omit the publish UI entirely; for share-capable apps, make publish one tap for user-selected items. **Every app that is neither privacy-sensitive nor an only-me ask ships with a sharing path already in it: either an always-on shared channel its records land on, or a publish control.** **An open-nature app — an image or meme wall, a bingo card, a party game — puts its records on an always-on `grant.public` channel from the first rules it ships, builds no publish step, and admits anonymous readers**, so everyone with the link sees what people make the moment they make it. The always-on channel is the least-surprise shape for anything playful and low-stakes (an image or meme wall, a group list, a guestbook): a `grant.public` channel, a members' channel, or an `audience` arm, so a thing someone makes is seen the moment they make it. The publish control is the shape for work people draft first: the per-item channel whose read-grant flips from a `visibility` field, behind one tap. A publish control that names its destination — "Publish to the wall", "Share to the group" — is the best shape of the second, not a requirement. An ordinary app with neither is a bug: every record lands on a channel only its writer can read, and nobody asked for that.
457
457
 
458
458
  **A share control is a link only where the record's channel admits anonymous readers; everywhere else it is the person picker, which takes a handle or an email and shows the invite as pending until they arrive.** A link — Copy link, the browser's share sheet, a QR code — is a share only when the person who opens it can read the thing: a record on a `grant.public` channel, always on or flipped by its `visibility` field. A record whose channel is reached only by grants is unreadable through a link, so sharing it means letting a person in: the `HandleInput` picker writes the share doc that grants their handle, exactly as the buddy picker above does, and the same picker takes an email address for someone who is not on the platform yet — it mails them an invite and hands the app an opaque `invite:<id>` subject to store as the invitee, which binds to their handle when they arrive, so the app never holds the address and `ViewerTag` shows them as invited until then and by their handle after. The one link that works on a grant-only thing is the one that lands on a `request` doc: it invites a knock, not a read. An app with a publish control changes the share control's shape with the record's state — the picker before the flip, the link after it.
459
459
 
460
- **Worked example — a shared catalog people track against, with per-thing visibility (a social habit app, a reading challenge, a fitness ladder).** The catalog items are public objects anyone proposes; each person's progress is their own; and each person chooses — _once per item, never per entry_ — whether their streak is public (on the leaderboard) or buddy-only. The visibility choice lives on a per-`(person, item)` **tracking** record that sets the read-grant the entries routed to it inherit.
460
+ **Worked example — a shared catalog people track against, with per-thing visibility (a music practice ladder, a language drill board, a fitness ladder).** The catalog items are public objects anyone proposes; each person's progress is their own; and each person chooses — _once per item, never per entry_ — whether their practice tally is public (on the leaderboard) or buddy-only. The visibility choice lives on a per-`(person, item)` **tracking** record that sets the read-grant the entries routed to it inherit.
461
461
 
462
462
  ### `access.js`
463
463
  ```js partial
464
- export function habits(doc, oldDoc, user, ctx) {
464
+ export function pieces(doc, oldDoc, user, ctx) {
465
465
  if (!user) throw { forbidden: "sign in" };
466
466
 
467
- // A habit: a public object anyone proposes; everyone can read and adopt it.
468
- if (doc.type === "habit") {
467
+ // A piece: a public object anyone proposes; everyone can read and adopt it.
468
+ if (doc.type === "piece") {
469
469
  if (doc.authorHandle !== user.userHandle) throw { forbidden: "not author" };
470
470
  if (oldDoc && doc.authorHandle !== oldDoc.authorHandle) throw { forbidden: "creator is fixed" };
471
- return { channels: [`habit:${doc._id}`], grant: { public: [`habit:${doc._id}`] } };
471
+ return { channels: [`piece:${doc._id}`], grant: { public: [`piece:${doc._id}`] } };
472
472
  }
473
473
 
474
- // A tracking record: your enrollment in a habit + ONE visibility choice. It sets the
475
- // read-grant on your per-habit channel, so the check-ins routed there inherit it.
474
+ // A tracking record: your enrollment in a piece + ONE visibility choice. It sets the
475
+ // read-grant on your per-piece channel, so the check-ins routed there inherit it.
476
476
  if (doc.type === "tracking") {
477
477
  if (doc.authorHandle !== user.userHandle) throw { forbidden: "your own enrollment" };
478
478
  if (oldDoc && oldDoc.authorHandle !== user.userHandle) throw { forbidden: "your own enrollment" };
479
- const ch = `track:${doc.habitId}:${user.userHandle}`;
479
+ const ch = `track:${doc.pieceId}:${user.userHandle}`;
480
480
  const grant = { users: { [user.userHandle]: [ch] } }; // always yourself
481
481
  if (doc.visibility === "public") grant.public = [ch]; // public -> counts on the leaderboard
482
482
  else if (doc.buddyHandle) grant.users[doc.buddyHandle] = [ch]; // buddy-only -> you + your buddy
483
483
  return { channels: [ch], grant };
484
484
  }
485
485
 
486
- // A check-in: author-owned, routed to your per-habit channel — it inherits that habit's visibility.
486
+ // A check-in: author-owned, routed to your per-piece channel — it inherits that piece's visibility.
487
487
  if (doc.type === "checkin") {
488
488
  if (doc.authorHandle !== user.userHandle) throw { forbidden: "not your check-in" };
489
489
  if (oldDoc && oldDoc.authorHandle !== user.userHandle) throw { forbidden: "not your check-in" };
490
- return { channels: [`track:${doc.habitId}:${user.userHandle}`] };
490
+ return { channels: [`track:${doc.pieceId}:${user.userHandle}`] };
491
491
  }
492
492
 
493
493
  throw { forbidden: "unknown document type" };
494
494
  }
495
495
  ```
496
496
 
497
- The leaderboard is just the access model: read the public `track:` channels and sum them; each viewer additionally sees their own streaks and any buddy who granted them in. A habit tracker whose owner **asked to keep it to themselves** is instead the per-visitor shape shown above: every visitor's habits and check-ins on their single `user:${user.userHandle}` channel, self-granted and private to them.
497
+ The leaderboard is just the access model: read the public `track:` channels and sum them; each viewer additionally sees their own tallies and any buddy who granted them in. A practice log whose owner **asked to keep it to themselves** is instead the per-visitor shape shown above: every visitor's pieces and check-ins on their single `user:${user.userHandle}` channel, self-granted and private to them.
498
498
 
499
499
  **Per-object collaboration is the resting shape of almost every app** (the second worked example above), and certainly of anything that says "invite", "join", "people can join", "collaborate", "share with", "together", "with my partner/team", or names a board/canvas/room/whiteboard a group co-edits — each shared thing is ONE object its members reach directly; it needs no owner. Use the per-object recipe: a channel per object, named from the record (`board:${doc._id}` on the object, `board:${doc.boardId}` on its children); the creator self-grants at creation (`grant: { users: { [user.userHandle]: [ch] } }`); child docs gate on `ctx.requireAccess(ch)` so any member edits any child in it (not just their own); a member-authored `share` doc grants a peer the same channel; a `request` doc — which takes **no** `requireAccess` — lets a not-yet-member ask to join. Keep the _object's own_ creator field write-once (`if (oldDoc && doc.author !== oldDoc.author) throw`) and a child's object-id immutable. **Two traps to avoid:** don't build it as an open public feed where each person only owns their own items (that abandons the shared membership), and don't gate it behind a single writer (members self-serve via share/request). The app owner is not special in the data model: any signed-in person creates their own object and invites a friend into it, and the rule keys on `user.userHandle` or the object's creator, never on `user.isOwner`.
500
500
 
@@ -804,7 +804,7 @@ Channel `_id` is the channel identifier everywhere. The access function uses `do
804
804
 
805
805
  ### Example: Per-object sharing (collaborate on your own objects, no admin)
806
806
 
807
- Reach for this by default, and certainly whenever the prompt says **invite, join, collaborate, share with, together, with my partner/team** — a shared shopping list you invite a partner to, a whiteboard people can join, a trip a group plans together, a tracker one person starts and later shares with a coach. A list app where every signed-in user makes their own lists, sees only their own, and can invite anyone to collaborate on a specific list — peer to peer, with no app admin in the loop. The pattern: **a channel per object** (`list:<id>`); the creator grants themselves that channel at creation; child docs (items) gate on `ctx.requireAccess` of the list's channel, so **any member edits any item**; any current member shares the list by granting another user the same channel. Membership is direct `grant.users`, so each viewer's access scales with their own memberships. Nothing a person writes reaches anyone else until they chose it: a per-object channel starts with its creator as the only reader, and every other reader arrives through a grant that creator wrote.
807
+ Reach for this by default, and certainly whenever the prompt says **invite, join, collaborate, share with, together, with my partner/team** — a shared seed-swap shelf you invite a sibling to, a whiteboard people can join, a quilting circle's pattern queue a group builds together, a tracker one person starts and later shares with a coach. A list app where every signed-in user makes their own lists, sees only their own, and can invite anyone to collaborate on a specific list — peer to peer, with no app admin in the loop. The pattern: **a channel per object** (`list:<id>`); the creator grants themselves that channel at creation; child docs (items) gate on `ctx.requireAccess` of the list's channel, so **any member edits any item**; any current member shares the list by granting another user the same channel. Membership is direct `grant.users`, so each viewer's access scales with their own memberships. Nothing a person writes reaches anyone else until they chose it: a per-object channel starts with its creator as the only reader, and every other reader arrives through a grant that creator wrote.
808
808
 
809
809
  ### `access.js`
810
810
  ```js partial
@@ -886,7 +886,7 @@ App.jsx — `access.hasChannel()` shows only the lists the viewer belongs to; `u
886
886
 
887
887
  #### The same shape with a default object per person, and a member doc that names the friend
888
888
 
889
- Reach for this variant whenever the app is built around a *thing* people come back to — a board, a trip, a game night's scoreboard, a hiking group's trail list, a habit tracker one person keeps and a coach is invited into next month. Everything above still holds; four additions make it feel finished from the first load:
889
+ Reach for this variant whenever the app is built around a *thing* people come back to — a board, a seed-swap shelf, a quilting circle's pattern queue, a beekeeping club's hive log, a sourdough feeding log one person keeps and a baking friend is invited into next month. Everything above still holds; four additions make it feel finished from the first load:
890
890
 
891
891
  - **Every signed-in person gets a default object automatically.** Its stable board record uses `` `default-${me.userHandle}` `` as its id and grants its creator the board channel and admin channel. Establish that record before the first content write or invitation, including when inviting is the first action. The creator remains a member when the board is empty, when a collaborator writes first, and after the last creator-authored item is deleted. Read the board by its fixed id before creating it; an existing board keeps its name and other settings. Child records route to the board channel; the board record carries the lasting creator grant. **The child doc stores that board id rather than having each writer derive one**: the create uses the displayed object’s id, with `` `default-${me.userHandle}` `` for a first action before any object exists and the rule holds it fixed from then on, so a card stays on the board its creator put it on however many people edit it later.
892
892
  - **Membership is a `member` doc, and picking the friend is `HandleInput`** (`useViewer()`, see use-viewer.md — the platform's people-picker, so the handle on the doc is one the platform resolved). The doc grants the named person the object's channel: `` grant: { users: { [doc.userHandle]: [chan] } } ``.
@@ -1017,11 +1017,32 @@ function BoardWorkspace({ ensureDefaultBoard }) {
1017
1017
  }
1018
1018
  ```
1019
1019
 
1020
+ #### The list holds what the person made and what they were let into
1021
+
1022
+ **The app's list of objects reads them by their `type`, so it holds the ones the signed-in person made and the ones a membership lets them into, read through the same rules.** The `member` record grants its board's channel, so the friend's own `useLiveQuery("type", { key: "board" })` returns that board beside their own the moment the membership lands; `creatorHandle` then says who made each one. A shared board carries `ViewerTag` for the person who made it, in the same list or in a section that renders once there is at least one, so a person keeping the app to themselves sees one plain list. Opening a shared board is the same `BoardContents`: its cards are read by its id and written with that id, the rules route them to its channel, and `can` gates each write.
1023
+
1024
+ ```jsx
1025
+ const { ViewerTag } = useViewer();
1026
+ const { docs: boards } = useLiveQuery("type", { key: "board" });
1027
+ const sharedWithMe = boards.filter((board) => board.creatorHandle !== me?.userHandle);
1028
+ // …
1029
+ {sharedWithMe.length > 0 && (
1030
+ <section>
1031
+ <h2>Shared with you</h2>
1032
+ {sharedWithMe.map((board) => (
1033
+ <button key={board._id} type="button" onClick={() => setPickedBoardId(board._id)}>
1034
+ {board.name ?? board._id} <ViewerTag userHandle={board.creatorHandle} />
1035
+ </button>
1036
+ ))}
1037
+ </section>
1038
+ )}
1039
+ ```
1040
+
1020
1041
  A new access function has no use for `user.isOwner` at all: a rule about a **person** keys on `user.userHandle` or the object's own creator field, and a rule about the app's **own automation** keys on `user.backend`.
1021
1042
 
1022
1043
  #### A person the ask names is let in by a record
1023
1044
 
1024
- When the instruction names somebody by their handle — "let my friend priya-k see and add to my chores list", "an expedition notebook for me and theo" — **the build's part is the rule that admits a membership and the screen that writes one; the membership itself is a record.** It is the same `member` doc the invite surface above writes, saved for that handle on the asker's own default object once the rules are live, by whoever is driving the build acting as the maker, read back, and only then spoken of as in.
1045
+ When the instruction names somebody by their handle — "let my friend priya-k see and add to my seed-swap shelf", "an expedition notebook for me and theo" — **the build's part is the rule that admits a membership and the screen that writes one; the membership itself is a record.** It is the same `member` doc the rule admits and the invite surface above writes, saved for that handle on the asker's own default object once the rules are live, by whoever is driving the build acting as the maker, read back, and only then spoken of as in. An app whose first build carried the rules and no invite screen still states this shape in full, in `access.js`'s member branch: the type and fields that branch admits are the record a later save writes, and an edit that adds the screen afterwards writes that same record.
1025
1046
 
1026
1047
  So the record's shape is a contract the files state plainly. The `_id` is fixed by the object and the handle — `` `member:${boardId}:${userHandle}` `` — so a second attempt is the same record rather than a twin, and a read by that id answers "is this person already in?" on its own. The fields are the ones the rule reads: `type`, `boardId`, `userHandle`, and `addedBy` set to the writer's own handle. The invite handler first ensures the stable board record, then writes the membership. The `member` rule admits an identical re-put, so a replay lands quietly. The board record keeps the creator in the collaboration independently of every content and invitation record. One invite handler in `App.jsx` writes exactly that shape, which is what makes the app's own screen and any later save agree on one record.
1027
1048
 
@@ -1039,9 +1060,9 @@ const addMember = async (boardId, handle) => {
1039
1060
  <HandleInput onChange={(h) => h && addMember(board._id, h)} placeholder="Add a friend…" />;
1040
1061
  ```
1041
1062
 
1042
- **A handle is the thing the person typed as one — a platform handle, the same token `HandleInput` would resolve.** A plain first name ("share it with Priya") is a person the maker adds by handle through the invite screen: the build has no way to know which account that name means, so the invite screen ships as usual and the narration says the maker can add that person there.
1063
+ **A handle is the thing the person typed as one — a platform handle, the same token `HandleInput` would resolve.** A plain first name ("share it with Priya") or an email address is a person the maker adds through the invite screen, which takes a handle or an email: the build has no way to know which account that name means, so the invite screen ships with this build and the narration says the maker can add that person there.
1043
1064
 
1044
- That bounds what the build's own closing line says: the rules and the screen it just wrote are what the files carry, so the honest sentence is _"your chores list now admits a membership for priya-k, and the invite screen writes one"_. _"priya-k is in"_ belongs to the save that follows — the read for an existing membership, the write in this shape, and the read that finds it afterwards.
1065
+ That bounds what the build's own closing line says: the rules and the screen it just wrote are what the files carry, so the honest sentence is _"your seed-swap shelf now admits a membership for priya-k, and the invite screen writes one"_. _"priya-k is in"_ belongs to the save that follows — the read for an existing membership, the write in this shape, and the read that finds it afterwards.
1045
1066
 
1046
1067
  ## More worked round-trip examples
1047
1068
 
@@ -1 +1 @@
1
- {"version":3,"file":"backend.js","sourceRoot":"","sources":["../../jsr/llms/backend.ts"],"names":[],"mappings":"AAUA,MAAM,CAAC,MAAM,aAAa,GAAc;IACtC,IAAI,EAAE,SAAS;IACf,KAAK,EAAE,YAAY;IACnB,WAAW,EACT,uXAAuX;IAOzX,IAAI,EAAE,CAAC,QAAQ,EAAE,YAAY,CAAC;CAC/B,CAAC"}
1
+ {"version":3,"file":"backend.js","sourceRoot":"","sources":["../../jsr/llms/backend.ts"],"names":[],"mappings":"AASA,MAAM,CAAC,MAAM,aAAa,GAAc;IACtC,IAAI,EAAE,SAAS;IACf,KAAK,EAAE,YAAY;IACnB,WAAW,EACT,uXAAuX;IAOzX,IAAI,EAAE,CAAC,QAAQ,EAAE,YAAY,CAAC;CAC/B,CAAC"}
package/llms/backend.md CHANGED
@@ -696,6 +696,13 @@ upstream that can be down, input that can be malformed — catches it and answer
696
696
  `{ ok: false, reason }` with a status the app can show, and the platform's 500
697
697
  stays the signal for a bug to fix.
698
698
 
699
+ A connected GitHub repository's deliveries arrive here too, as a `POST` to
700
+ `/_api/github-webhook`, already verified by the platform, with the event name
701
+ in `x-vibes-github-event`, GitHub's delivery id in `x-vibes-github-delivery`
702
+ (key writes by it — a delivery can arrive twice), and an untrusted third-party
703
+ body; a `4xx` ends the delivery, and a throw marks it failed (GitHub does not
704
+ retry it).
705
+
699
706
  `/_api` opens to every caller once the app is published. Before its first
700
707
  publish, and after the owner unpublishes it, the routes answer the app's own
701
708
  people — its owner, its developers, and anyone it is shared with — and
@@ -881,7 +888,7 @@ Two rules keep change-reactions sane:
881
888
  an unguarded write-back loops on itself.
882
889
  2. **Write derived docs to a different database** than the one that triggered
883
890
  the event. A backend write triggers `onChange` again (one generation deeper);
884
- the platform caps runaway chains after a few generations, but a tight
891
+ the platform stops the chain silently after four generations, but a tight
885
892
  same-database ping-pong is still wasted work. Different db + dbName guard
886
893
  makes loops structurally impossible.
887
894
 
package/llms/callai.js CHANGED
@@ -2,7 +2,6 @@ export const callaiConfig = {
2
2
  name: "callai",
3
3
  label: "callAI",
4
4
  description: "easy API for LLM requests with streaming support",
5
- initialVariant: true,
6
5
  importModule: "call-ai",
7
6
  importName: "callAI",
8
7
  };
@@ -1 +1 @@
1
- {"version":3,"file":"callai.js","sourceRoot":"","sources":["../../jsr/llms/callai.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,YAAY,GAAc;IACrC,IAAI,EAAE,QAAQ;IACd,KAAK,EAAE,QAAQ;IACf,WAAW,EAAE,kDAAkD;IAC/D,cAAc,EAAE,IAAI;IACpB,YAAY,EAAE,SAAS;IACvB,UAAU,EAAE,QAAQ;CACrB,CAAC"}
1
+ {"version":3,"file":"callai.js","sourceRoot":"","sources":["../../jsr/llms/callai.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,YAAY,GAAc;IACrC,IAAI,EAAE,QAAQ;IACd,KAAK,EAAE,QAAQ;IACf,WAAW,EAAE,kDAAkD;IAC/D,YAAY,EAAE,SAAS;IACvB,UAAU,EAAE,QAAQ;CACrB,CAAC"}
package/llms/callai.md CHANGED
@@ -10,15 +10,15 @@ Name what the app does in the app's own words. The function name `callAI` stays
10
10
 
11
11
  ## Text to records
12
12
 
13
- An ordinary tracker with several facts per entry uses sentence creation even when the person never asks for AI, pasting, or a chat interface. A packing board with item, quantity, and who is bringing it, or a potluck board with dish, servings, and contributor, starts with this composer. Keep quantity steppers, claim buttons, and toggles on existing records as direct controls; they complement sentence creation. A name-only list, a simple message, and a timer stay direct because their input already is the whole record or an explicit setting.
13
+ When the person's own words describe input in the shape they already have it — typing entries in their own words, pasting a chat, replies or a list, a photo, adding things however they like — the sentence composer is the front door they asked for. When a specified request names its fields or features and describes no such input, the front door is a plain Add form for those fields; adding by sentence is the first of the closing `▸` options, phrased for this app, such as "Add plants by typing a sentence like 'basil and two tomatoes, bed 3'", and Ask is an option too. An open-ended request may open with the composer as one of its one or two alive defaults. Keep quantity steppers, claim buttons, and toggles on existing records as direct controls; they complement sentence creation. A name-only list, a simple message, and a timer stay direct because their input already is the whole record or an explicit setting.
14
14
 
15
- For an app that keeps structured records, wire these visible actions in the first working app, using its own record names. One composer has Add and Ask actions. Add extracts and normalizes facts, saves each record immediately, and shows saved records with edit actions; one item and a pasted batch share this action. Each saved row has Change by sentence: selecting it changes Add to Apply correction, which saves onto that same record id. Ask sends the typed question with the relevant saved records and displays a read-only answer beside the composer. Edit opens the selected row’s populated fields. One parent-owned Undo restores the latest successful creation batch or the previous document from either a sentence correction or a field edit.
15
+ When the composer is on screen, wire these visible actions in the same build, using the app's own record names. One composer has Add and Ask actions. Add extracts and normalizes facts, saves each record immediately, and shows saved records with edit actions; one item and a pasted batch share this action. Each saved row has Change by sentence: selecting it changes Add to Apply correction, which saves onto that same record id. Ask sends the typed question with the relevant saved records and displays a read-only answer beside the composer. Edit opens the selected row’s populated fields. One parent-owned Undo restores the latest successful creation batch or the previous document from either a sentence correction or a field edit.
16
16
 
17
- A record starts as a sentence. For structured intake, use one text box or the app's chat composer, extract the person's facts into the document's fields, normalize them, and save immediately in that same submit action. Show the saved record with editing controls. A pasted list becomes individual records through an array schema. Field-by-field controls belong to the edit view of a saved record. One new record and a pasted batch use the same composer. Opening a saved row’s Edit action reveals its populated field controls. A message whose text IS the record can save directly; extraction is useful when the person would otherwise fill several fields. Requested AI generation remains available for creative work.
17
+ A record the person describes in their own words starts as a sentence. For that intake, use one text box or the app's chat composer, extract the person's facts into the document's fields, normalize them, and save immediately in that same submit action. Show the saved record with editing controls. A pasted list becomes individual records through an array schema. Field-by-field controls belong to the edit view of a saved record. One new record and a pasted batch use the same composer. Opening a saved row’s Edit action reveals its populated field controls. A message whose text IS the record can save directly; extraction is useful when the person would otherwise fill several fields. Requested AI generation remains available for creative work.
18
18
 
19
19
  The extraction schema names the same editable fields the app writes to its documents. Normalize the parsed output into that shape before saving or rendering: check strings, finite numbers, enum values and arrays (including their members), and project only the fields the app owns. Keep `_id`, authorship, parent ids and sharing fields under app control. Submitting the sentence commits the normalized records automatically. The saved card shows the understood facts and is the confirmation; editing follows persistence. For an edit by sentence, pass the current editable fields plus the correction through the same single-record schema, then write the normalized fields onto that saved record's trusted identity.
20
20
 
21
- After each successful creation, sentence correction or field edit, the parent owns one Undo action. Keep the created ids or the previous document until the next successful write replaces them; a detail editor reports its previous document to that parent after its save succeeds. Undo deletes the created records or puts the previous document back. An explicit setting — a number, a date, a time, a choice, an on/off state — stays a direct control. The schema can extract ordinary quantities the person describes, such as two tents or six servings, while a saved row keeps its quantity control. Critical values (a dose, a price, a deadline, a timer, a recipient) are set directly. A multi-field record still starts with the composer.
21
+ After each successful creation, sentence correction or field edit, the parent owns one Undo action. Keep the created ids or the previous document until the next successful write replaces them; a detail editor reports its previous document to that parent after its save succeeds. Undo deletes the created records or puts the previous document back. An explicit setting — a number, a date, a time, a choice, an on/off state — stays a direct control. The schema can extract ordinary quantities the person describes, such as two tents or six servings, while a saved row keeps its quantity control. Critical values (a dose, a price, a deadline, a timer, a recipient) are set directly. A multi-field record the person describes in their own words starts with the composer.
22
22
 
23
23
  This complete example covers **one sentence → one task**, **pasted list → many tasks**, **a selected task + correction → the same task**, **questions from the same box**, and **Undo after a save**. Every new submission uses the array schema, whether it names one task or several. The person sets a due date in the saved task’s date control. A malformed response leaves the typed sentence and saved records available; the waiting state ends when the request settles.
24
24
 
package/llms/fireproof.js CHANGED
@@ -2,7 +2,6 @@ export const fireproofConfig = {
2
2
  name: "fireproof",
3
3
  label: "useFireproof",
4
4
  description: "cloud-backed document database with live sync",
5
- initialVariant: true,
6
5
  importModule: "use-fireproof",
7
6
  importName: "useFireproof",
8
7
  };
@@ -1 +1 @@
1
- {"version":3,"file":"fireproof.js","sourceRoot":"","sources":["../../jsr/llms/fireproof.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,eAAe,GAAc;IACxC,IAAI,EAAE,WAAW;IACjB,KAAK,EAAE,cAAc;IACrB,WAAW,EAAE,+CAA+C;IAC5D,cAAc,EAAE,IAAI;IACpB,YAAY,EAAE,eAAe;IAC7B,UAAU,EAAE,cAAc;CAC3B,CAAC"}
1
+ {"version":3,"file":"fireproof.js","sourceRoot":"","sources":["../../jsr/llms/fireproof.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,eAAe,GAAc;IACxC,IAAI,EAAE,WAAW;IACjB,KAAK,EAAE,cAAc;IACrB,WAAW,EAAE,+CAA+C;IAC5D,YAAY,EAAE,eAAe;IAC7B,UAAU,EAAE,cAAc;CAC3B,CAAC"}
package/llms/fireproof.md CHANGED
@@ -43,7 +43,7 @@ function NoteForm({ text }) {
43
43
 
44
44
  ### Basic Example
45
45
 
46
- Wire these visible actions in the first working app, using its own record names. One composer has Add and Ask actions. Add extracts and normalizes facts, saves each record immediately, and shows saved records with edit actions; one item and a pasted batch share this action. Each saved row has Change by sentence: selecting it changes Add to Apply correction, which saves onto that same record id. Ask sends the typed question with the relevant saved records and displays a read-only answer beside the composer. Edit opens the selected row’s populated fields. One parent-owned Undo restores the latest successful creation batch or the previous document from either a sentence correction or a field edit.
46
+ When the composer is on screen, wire these visible actions in the same build, using the app's own record names. One composer has Add and Ask actions. Add extracts and normalizes facts, saves each record immediately, and shows saved records with edit actions; one item and a pasted batch share this action. Each saved row has Change by sentence: selecting it changes Add to Apply correction, which saves onto that same record id. Ask sends the typed question with the relevant saved records and displays a read-only answer beside the composer. Edit opens the selected row’s populated fields. One parent-owned Undo restores the latest successful creation batch or the previous document from either a sentence correction or a field edit.
47
47
 
48
48
  With the callai skill available, submitting words extracts and normalizes every task they name, then immediately saves one document per task. The saved task is the confirmation and remains editable, with Undo beside the intake after each save. The parent keeps the ids from the last creation or the previous version from an edit, so intake and saved-detail editors share one Undo action. `useLiveQuery` keeps saved tasks visible; `useDocument` supplies the detail editor only after a record exists. For a plain message whose text is already the whole record, save that text directly.
49
49
 
@@ -540,7 +540,7 @@ Pass `{ offlineQueue: false }` for **server-first, fail-fast** writes: a `put` r
540
540
 
541
541
  ```js
542
542
  // Default (single-user apps): durable offline writes, no config needed.
543
- const { useLiveQuery } = useFireproof("todos");
543
+ const { useLiveQuery } = useFireproof("sightings");
544
544
 
545
545
  // Collaborative app: fail fast instead of queueing a stale write.
546
546
  const { useLiveQuery } = useFireproof("shared-board", { offlineQueue: false });
@@ -594,13 +594,13 @@ async function main() {
594
594
  >>>>>>> REPLACE
595
595
  ```
596
596
 
597
- To subscribe to real-time updates, use the `subscribe` method. This is useful for building backend event handlers or other server-side logic — for instance to send an email when the user completes a todo:
597
+ To subscribe to real-time updates, use the `subscribe` method. This is useful for building backend event handlers or other server-side logic — for instance to send an email when the user confirms a sighting:
598
598
 
599
599
  ```js
600
600
  database.subscribe((changes) => {
601
601
  changes.forEach((change) => {
602
- if (change.completed) {
603
- sendEmail(change.email, "Todo completed", "You have completed a todo.");
602
+ if (change.confirmed) {
603
+ sendEmail(change.email, "Sighting confirmed", "You have confirmed a sighting.");
604
604
  }
605
605
  });
606
606
  }, true);
@@ -750,7 +750,7 @@ You can use React's `useState` to manage validation states and error messages. V
750
750
 
751
751
  ## Example React Application
752
752
 
753
- Code listing for todo tracker App.jsx. Note the code ordering: hooks, then handlers, then classNames right before JSX.
753
+ Code listing for a bird sightings log App.jsx. Note the code ordering: hooks, then handlers, then classNames right before JSX.
754
754
 
755
755
  ### `App.jsx`
756
756
  ```js
@@ -760,32 +760,32 @@ import { Button, Card, CardContent, Input, Label } from "@vibes.diy/look";
760
760
 
761
761
  export default function App() {
762
762
  // 1. Hooks and document shapes
763
- const { useLiveQuery, useDocument, database } = useFireproof("todoList");
763
+ const { useLiveQuery, useDocument, database } = useFireproof("birdLog");
764
764
 
765
765
  const {
766
- doc: newTodo,
767
- merge: mergeNewTodo,
768
- submit: submitNewTodo,
766
+ doc: newSighting,
767
+ merge: mergeNewSighting,
768
+ submit: submitNewSighting,
769
769
  } = useDocument({
770
- todo: "",
771
- type: "todo",
772
- completed: false,
770
+ species: "",
771
+ type: "sighting",
772
+ confirmed: false,
773
773
  createdAt: Date.now(),
774
774
  });
775
775
 
776
- const { docs: todos } = useLiveQuery("type", {
777
- key: "todo",
776
+ const { docs: sightings } = useLiveQuery("type", {
777
+ key: "sighting",
778
778
  descending: true,
779
779
  });
780
780
 
781
781
  // 2. Event handlers
782
782
  const handleInputChange = (e) => {
783
- mergeNewTodo({ todo: e.target.value });
783
+ mergeNewSighting({ species: e.target.value });
784
784
  };
785
785
 
786
786
  const handleSubmit = (e) => {
787
787
  e.preventDefault();
788
- submitNewTodo();
788
+ submitNewSighting();
789
789
  };
790
790
 
791
791
  // 3. ClassNames — right before JSX, for the plain elements only; the look's
@@ -798,15 +798,15 @@ export default function App() {
798
798
  // 4. JSX return
799
799
  return (
800
800
  <div className={`min-h-screen max-w-md mx-auto p-6 ${c.page}`}>
801
- <h2 className="text-2xl font-bold mb-4">Todo List</h2>
801
+ <h2 className="text-2xl font-bold mb-4">Bird Sightings</h2>
802
802
  <form onSubmit={handleSubmit} className="mb-4">
803
- <Label htmlFor="todo" className="mb-2 block">
804
- Todo
803
+ <Label htmlFor="species" className="mb-2 block">
804
+ Species
805
805
  </Label>
806
- <Input className="w-full" id="todo" type="text" onChange={handleInputChange} value={newTodo.todo} />
806
+ <Input className="w-full" id="species" type="text" onChange={handleInputChange} value={newSighting.species} />
807
807
  </form>
808
808
  <ul className="space-y-3">
809
- {todos.map((doc) => (
809
+ {sightings.map((doc) => (
810
810
  <li key={doc._id}>
811
811
  <Card>
812
812
  <CardContent className="flex flex-col items-start">
@@ -815,10 +815,10 @@ export default function App() {
815
815
  <input
816
816
  className="mr-2"
817
817
  type="checkbox"
818
- checked={doc.completed}
819
- onChange={() => database.put({ ...doc, completed: !doc.completed })}
818
+ checked={doc.confirmed}
819
+ onChange={() => database.put({ ...doc, confirmed: !doc.confirmed })}
820
820
  />
821
- <span className="font-medium">{doc.todo}</span>
821
+ <span className="font-medium">{doc.species}</span>
822
822
  </div>
823
823
  <Button variant="destructive" size="sm" onClick={() => database.del(doc._id)}>
824
824
  Delete
package/llms/image-gen.js CHANGED
@@ -2,7 +2,6 @@ export const imageGenConfig = {
2
2
  name: "image-gen",
3
3
  label: "Image Generation",
4
4
  description: "Generate and edit images",
5
- initialVariant: true,
6
5
  cues: [
7
6
  "image",
8
7
  "images",
@@ -1 +1 @@
1
- {"version":3,"file":"image-gen.js","sourceRoot":"","sources":["../../jsr/llms/image-gen.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,cAAc,GAAc;IACvC,IAAI,EAAE,WAAW;IACjB,KAAK,EAAE,kBAAkB;IACzB,WAAW,EAAE,0BAA0B;IACvC,cAAc,EAAE,IAAI;IACpB,IAAI,EAAE;QACJ,OAAO;QACP,QAAQ;QACR,KAAK;QACL,OAAO;QACP,QAAQ;QACR,SAAS;QACT,UAAU;QACV,QAAQ;QACR,cAAc;QACd,MAAM;QACN,cAAc;QACd,gBAAgB;KACjB;IACD,YAAY,EAAE,WAAW;IACzB,UAAU,EAAE,QAAQ;CACrB,CAAC"}
1
+ {"version":3,"file":"image-gen.js","sourceRoot":"","sources":["../../jsr/llms/image-gen.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,cAAc,GAAc;IACvC,IAAI,EAAE,WAAW;IACjB,KAAK,EAAE,kBAAkB;IACzB,WAAW,EAAE,0BAA0B;IACvC,IAAI,EAAE;QACJ,OAAO;QACP,QAAQ;QACR,KAAK;QACL,OAAO;QACP,QAAQ;QACR,SAAS;QACT,UAAU;QACV,QAAQ;QACR,cAAc;QACd,MAAM;QACN,cAAc;QACd,gBAAgB;KACjB;IACD,YAAY,EAAE,WAAW;IACzB,UAAU,EAAE,QAAQ;CACrB,CAAC"}
@@ -1,6 +1,6 @@
1
1
  # Pouf Event
2
2
 
3
- A check-in desk for the door of an event: a fundraiser's guest list, a wedding reception, a meetup's RSVPs, a workshop's sign-in sheet. Reach for it whenever the app's core is a list of people expected somewhere who get marked as arrived, with a running count of who is in and who is still on the way. Guests and their check-ins persist, so every volunteer at the door sees the same list on their own phone and it survives a reload.
3
+ A check-in desk for the door of an event: an astronomy night's arrivals, a wedding reception, a meetup's RSVPs, a workshop's sign-in sheet. Reach for it whenever the app's core is a list of people expected somewhere who get marked as arrived, with a running count of who is in and who is still on the way. Guests and their check-ins persist, so every volunteer at the door sees the same list on their own phone and it survives a reload.
4
4
 
5
5
  Built from @vibes.diy/pouf components; persisted state lives in Fireproof. Every write surface is gated on `useVibe(dbName).can`.
6
6
 
@@ -181,7 +181,7 @@ export default function App() {
181
181
 
182
182
  {visible.length === 0 ? (
183
183
  <Empty icon="search" title={guests.length === 0 ? "No guests yet" : "No guests found"}>
184
- {guests.length === 0 ? "Add the guest list to start checking people in." : "Check the spelling or show everyone."}
184
+ {guests.length === 0 ? "Add the arrivals list to start checking people in." : "Check the spelling or show everyone."}
185
185
  </Empty>
186
186
  ) : (
187
187
  <Stack gap={2}>
@@ -1,6 +1,6 @@
1
1
  # Pouf Login
2
2
 
3
- The join step a member sees once they are signed in: a running club's "tell us who you are", a writing group's member card, a team space's first-visit profile. Reach for it whenever the app wants its own profile for each person (the name they go by here, a short bio) before they take part. It is **not** a sign-in form: the platform owns sign-in, so this block never asks for an email, a password or a magic link. The visitor signs in through `useViewer().requestLogin()`, and only then fills in the profile the app keeps. Each profile persists, so a member joins once and finds their card on every visit and device.
3
+ The join step a member sees once they are signed in: a quilting circle's "tell us who you are", a writing group's member card, a team space's first-visit profile. Reach for it whenever the app wants its own profile for each person (the name they go by here, a short bio) before they take part. It is **not** a sign-in form: the platform owns sign-in, so this block never asks for an email, a password or a magic link. The visitor signs in through `useViewer().requestLogin()`, and only then fills in the profile the app keeps. Each profile persists, so a member joins once and finds their card on every visit and device.
4
4
 
5
5
  Built from @vibes.diy/pouf components; persisted state lives in Fireproof. Every write surface is gated on `useVibe(dbName).can`.
6
6
 
package/llms/pouf-quiz.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Pouf Quiz
2
2
 
3
- A short multiple-choice quiz that keeps score: a book club's chapter check, a practice round before a test, a pub quiz warm-up, a training check for new staff. Reach for it when the app asks a handful of questions one at a time, says right or wrong after each answer, ends on a score and lets people come back to beat it. Each finished attempt someone saves is its own document, so their past scores are there on the next visit and on any device.
3
+ A short multiple-choice quiz that keeps score: an astronomy club's star-chart check, a refresher round before a test, a pub quiz warm-up, a training check for new staff. Reach for it when the app asks a handful of questions one at a time, says right or wrong after each answer, ends on a score and lets people come back to beat it. Each finished attempt someone saves is its own document, so their past scores are there on the next visit and on any device.
4
4
 
5
5
  Built from @vibes.diy/pouf components; persisted state lives in Fireproof. Every write surface is gated on `useVibe(dbName).can`.
6
6
 
@@ -1,6 +1,6 @@
1
1
  # Pouf Settings
2
2
 
3
- A settings screen for each person's own preferences: the display name in a habit tracker, reminder emails for a reading log, a compact or comfortable layout, how loud an alert should be. Reach for it whenever the app lets someone tune how it treats them and expects those choices to stick. Each person's settings persist as their own document, so a choice made on the phone is there on the laptop.
3
+ A settings screen for each person's own preferences: the display name in a sleep log, reminder emails for a reading log, a compact or comfortable layout, how loud an alert should be. Reach for it whenever the app lets someone tune how it treats them and expects those choices to stick. Each person's settings persist as their own document, so a choice made on the phone is there on the laptop.
4
4
 
5
5
  Built from @vibes.diy/pouf components; persisted state lives in Fireproof. Every write surface is gated on `useVibe(dbName).can`.
6
6
 
package/llms/pouf-todo.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Pouf Todo
2
2
 
3
- A checklist the person adds to, filters and ticks off: chores, a packing list, today's errands, a reading queue. Reach for it whenever the app's core is a list of short items that are either done or not. The tasks persist, so the list is still there on the next visit and on every device that opens the app.
3
+ A checklist the person adds to, filters and ticks off: a beekeeper's hive inspection steps, a seed-swap shelf's restock run, today's errands, a quilting circle's supply checklist. Reach for it whenever the app's core is a list of short items that are either done or not. The tasks persist, so the list is still there on the next visit and on every device that opens the app.
4
4
 
5
5
  Built from @vibes.diy/pouf components; persisted state lives in Fireproof. Every write surface is gated on `useVibe(dbName).can`.
6
6
 
package/llms/types.d.ts CHANGED
@@ -3,7 +3,6 @@ export interface LlmConfig {
3
3
  label: string;
4
4
  description: string;
5
5
  cues?: string[];
6
- initialVariant?: boolean;
7
6
  importModule?: string;
8
7
  importName?: string;
9
8
  importType?: "named" | "namespace" | "default";
package/llms/use-vibe.js CHANGED
@@ -2,7 +2,6 @@ export const useVibeConfig = {
2
2
  name: "use-vibe",
3
3
  label: "Vibe Write Gating",
4
4
  description: "Gate write surfaces on the app's own access.js via useVibe().can",
5
- initialVariant: true,
6
5
  importModule: "use-vibes",
7
6
  importName: "useVibe",
8
7
  };
@@ -1 +1 @@
1
- {"version":3,"file":"use-vibe.js","sourceRoot":"","sources":["../../jsr/llms/use-vibe.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,aAAa,GAAc;IACtC,IAAI,EAAE,UAAU;IAChB,KAAK,EAAE,mBAAmB;IAC1B,WAAW,EAAE,kEAAkE;IAC/E,cAAc,EAAE,IAAI;IACpB,YAAY,EAAE,WAAW;IACzB,UAAU,EAAE,SAAS;CACtB,CAAC"}
1
+ {"version":3,"file":"use-vibe.js","sourceRoot":"","sources":["../../jsr/llms/use-vibe.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,aAAa,GAAc;IACtC,IAAI,EAAE,UAAU;IAChB,KAAK,EAAE,mBAAmB;IAC1B,WAAW,EAAE,kEAAkE;IAC/E,YAAY,EAAE,WAAW;IACzB,UAAU,EAAE,SAAS;CACtB,CAAC"}
@@ -2,7 +2,6 @@ export const useViewerConfig = {
2
2
  name: "use-viewer",
3
3
  label: "Viewer Identity",
4
4
  description: "Get the current viewer's identity and capability gates",
5
- initialVariant: true,
6
5
  importModule: "use-vibes",
7
6
  importName: "useViewer",
8
7
  };
@@ -1 +1 @@
1
- {"version":3,"file":"use-viewer.js","sourceRoot":"","sources":["../../jsr/llms/use-viewer.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,eAAe,GAAc;IACxC,IAAI,EAAE,YAAY;IAClB,KAAK,EAAE,iBAAiB;IACxB,WAAW,EAAE,wDAAwD;IACrE,cAAc,EAAE,IAAI;IACpB,YAAY,EAAE,WAAW;IACzB,UAAU,EAAE,WAAW;CACxB,CAAC"}
1
+ {"version":3,"file":"use-viewer.js","sourceRoot":"","sources":["../../jsr/llms/use-viewer.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,eAAe,GAAc;IACxC,IAAI,EAAE,YAAY;IAClB,KAAK,EAAE,iBAAiB;IACxB,WAAW,EAAE,wDAAwD;IACrE,YAAY,EAAE,WAAW;IACzB,UAAU,EAAE,WAAW;CACxB,CAAC"}
package/llms/web-audio.md CHANGED
@@ -242,7 +242,7 @@ Tempo transport and lookahead
242
242
  - Tempo mapping: `secondsPerBeat = 60 / bpm`; compute bars:beats:ticks → seconds on the audio clock (choose PPQ, e.g., 480/960).
243
243
  - Lookahead window: maintain ~50–200 ms rolling schedule; enqueue with absolute `when` times in audio seconds.
244
244
 
245
- Multi‑channel drum machine
245
+ Multi‑channel sample playback
246
246
 
247
247
  - Pre‑decode all samples; never decode on hit.
248
248
  - Per hit: create a fresh `AudioBufferSourceNode` and call `.start(when)`.
package/mcp-footer.md CHANGED
@@ -1,12 +1,15 @@
1
1
  ## MCP Server Setup
2
2
 
3
- Tools: vibes_list_apps, vibes_list_databases, vibes_get, vibes_put, vibes_delete, vibes_query, vibes_generate
3
+ Tools: vibes_list_apps, vibes_list_databases, vibes_get, vibes_put, vibes_delete, vibes_query, vibes_generate, vibes_add_seed_data, vibes_remove_seed_data
4
4
 
5
5
  `vibes_generate` builds a brand-new vibe from a text prompt and deploys it live —
6
6
  it takes minutes and spends your AI credits. The new app is separate from the
7
7
  `--app-slug` this server was started with; the data tools keep pointing at that
8
8
  one.
9
9
 
10
+ `vibes_add_seed_data` adds the live version's sample data (its `seed.json`) to the
11
+ app's databases, keeping your edits; `vibes_remove_seed_data` takes it out again.
12
+
10
13
  Requires: npx vibes-diy login (one time)
11
14
 
12
15
  ### Claude Desktop / Cowork
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibes.diy/prompts",
3
- "version": "14.3.76",
3
+ "version": "14.3.78",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "exports": {
@@ -39,9 +39,9 @@
39
39
  "license": "Apache-2.0",
40
40
  "dependencies": {
41
41
  "@adviser/cement": "~0.5.34",
42
- "@vibes.diy/call-ai-v2": "14.3.76",
43
- "@vibes.diy/identity": "14.3.76",
44
- "@vibes.diy/use-vibes-types": "14.3.76",
42
+ "@vibes.diy/call-ai-v2": "14.3.78",
43
+ "@vibes.diy/identity": "14.3.78",
44
+ "@vibes.diy/use-vibes-types": "14.3.78",
45
45
  "arktype": "~2.2.6",
46
46
  "json-schema-faker": "~0.6.3"
47
47
  },