@vibes.diy/prompts 14.1.32 → 14.1.33

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.md CHANGED
@@ -1,6 +1,6 @@
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 the build itself — see _A person the ask names is let in by the build_ below — so the files that land carry their handle.
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.
4
4
 
5
5
  **On a second pass over an app that already exists, this is a whole-app upgrade, applied in full.** Read `App.jsx` for the object type the app is about, 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` to match it — the invite screen, the `can` gates, the object id on every child — keeping everything the app already does.
6
6
 
@@ -845,7 +845,14 @@ const { HandleInput } = useViewer();
845
845
  const { me, can } = useVibe("boards");
846
846
  const canInvite = (boardId) => can.create({ type: "member", boardId, userHandle: "x", addedBy: me?.userHandle }).ok;
847
847
  const addMember = (boardId, handle) =>
848
- handle && database.put({ type: "member", boardId, userHandle: handle, addedBy: me.userHandle });
848
+ handle &&
849
+ database.put({
850
+ _id: `member:${boardId}:${handle}`,
851
+ type: "member",
852
+ boardId,
853
+ userHandle: handle,
854
+ addedBy: me.userHandle,
855
+ });
849
856
  // Every card names its board at creation — the person's own default board when they picked none.
850
857
  const addCard = (text) => database.put({ type: "card", boardId: pickedBoardId || `default-${me.userHandle}`, text });
851
858
  // …
@@ -854,33 +861,28 @@ const addCard = (text) => database.put({ type: "card", boardId: pickedBoardId ||
854
861
 
855
862
  Where the app's own maintainer needs to move records through the gate — a CLI migration re-homing cards — `!user.isOwner` reads as a bypass written beside a check (`if (!user.isOwner) ctx.requireAccess(chan)`), never as the arm that decides who may act.
856
863
 
857
- #### A person the ask names is let in by the build
864
+ #### A person the ask names is let in by a record
858
865
 
859
- 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 writes that person's membership itself, so the files that land carry their handle.** The invite surface above is where the maker adds the *next* person; the person the ask already named is in from the first render, with nothing left for the maker to type.
866
+ 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.
860
867
 
861
- **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 not a handle: the build has no way to know which account, if any, that name means, and a grant written for a guessed handle either reaches a stranger who happens to own it or nobody. For a name that is not a handle, ship the invite screen as usual and let the narration say the maker can add that person by handle — an honest "still to be added" rather than a membership written for the wrong person.
862
-
863
- The shape: the handles the ask named live in one constant at the top of `App.jsx`, keyed by the asker's own handle (the app context names it), and a first-render effect writes one `member` doc per name onto the asker's own default object — a fixed `_id`, read back first so a mount after the first one finds the record and writes nothing, gated on `can.create` so it waits for the rules to admit it, and run as the asker (the access function above accepts `addedBy === user.userHandle` on their own default board with no admin grant to bootstrap). The `member` rule admits an identical re-put on purpose: a cold replica can miss a record it holds (`ready` is hydration, not sync), and a replay that changes nothing must land quietly rather than refuse — and toast — a membership that is already fine. The effect keys on `ready` and the handle, not on `can`, so it runs once per identity rather than once per render. Anyone else who opens the app finds nothing in the constant under their handle and writes nothing.
868
+ 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 `member` rule above admits the board's creator writing on their own default board with nothing to bootstrap, and admits an identical re-put, so a replay lands quietly. 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.
864
869
 
865
870
  ```jsx
866
- // The people the ask named, by the handle that asked. Read at first render; the invite box adds the rest.
867
- const sharedWith = { theo: ["priya-k"] };
868
-
869
- const { me, can, ready } = useVibe("boards");
870
- useEffect(() => {
871
- if (!ready || !me?.userHandle) return;
872
- const boardId = `default-${me.userHandle}`;
873
- for (const userHandle of sharedWith[me.userHandle] ?? []) {
874
- const member = { _id: `member:${boardId}:${userHandle}`, type: "member", boardId, userHandle, addedBy: me.userHandle };
875
- // Look before writing so a second mount finds it and moves on; an identical replay is admitted anyway.
876
- database.get(member._id).catch(() => {
877
- if (can.create(member).ok) return database.put(member);
878
- });
879
- }
880
- }, [ready, me?.userHandle]);
871
+ const { HandleInput } = useViewer();
872
+ const { database } = useFireproof("boards");
873
+ const { me, can } = useVibe("boards");
874
+ const addMember = (boardId, handle) => {
875
+ // One fixed id per (board, handle): the same person added twice is the same record.
876
+ const member = { _id: `member:${boardId}:${handle}`, type: "member", boardId, userHandle: handle, addedBy: me.userHandle };
877
+ if (can.create(member).ok) database.put(member);
878
+ };
879
+ // …
880
+ <HandleInput onChange={(h) => h && addMember(board._id, h)} placeholder="Add a friend…" />;
881
881
  ```
882
882
 
883
- Two things follow for the reply the build's narration writes. The record the app writes is what lets the person in, so the sentence is _"priya-k is named on your chores list"_ — a fact about the files — and never a promise about what they can see right now. And when the ask names a person the app already lets in, the constant is where to look: their handle there is the evidence, and a handle that is absent means the membership is still to be written.
883
+ **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.
884
+
885
+ 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.
884
886
 
885
887
  ## More worked round-trip examples
886
888
 
@@ -74,20 +74,38 @@ for the rest.
74
74
  import { useConnection } from "use-vibes";
75
75
 
76
76
  function ConnectInstagram() {
77
- const { state, consented, accountLabel, link, refresh } = useConnection("instagram");
77
+ const { state, consented, available, accountLabel, link, refresh } = useConnection("instagram");
78
78
  if (state === "pending") return null; // still resolving — never flash a Connect button
79
- if (state === "absent" || !consented)
80
- return <button onClick={link}>Connect Instagram</button>;
79
+ // A connection that already works keeps working, whatever `available` says.
80
+ if (consented && (state === "live" || state === "expiring"))
81
+ return <p>Connected as {accountLabel}</p>;
82
+ // Everything below asks the visitor to start or redo a connection, which is
83
+ // the only thing `available` can stop.
84
+ if (!available) return <p>Connecting Instagram is not available right now.</p>;
81
85
  if (state === "dead") return <p>Your Instagram connection expired. Reconnect it in Settings.</p>;
82
- return <p>Connected as {accountLabel}</p>;
86
+ return <button onClick={link}>Connect Instagram</button>;
83
87
  }
84
88
  ```
85
89
 
86
90
  `useConnection(provider)` returns `state` (`pending` | `absent` | `live` |
87
91
  `expiring` | `dead`), `consented` (whether _this_ app has been approved — a
88
92
  `live` account with `consented: false` is a one-click ask, not a full
89
- sign-in), `accountLabel` and `expiresAt` for display, `link()` to open the
90
- platform's consent card, and `refresh()` to re-read. `link()` resolves when the
93
+ sign-in), `available`, `accountLabel` and `expiresAt` for display, `link()` to
94
+ open the platform's consent card, and `refresh()` to re-read.
95
+
96
+ `available` is about the platform, not the person: it is false when we have not
97
+ finished setting that provider up, or when it is switched off for a while. It
98
+ is not the visitor's fault and it is not permanent.
99
+
100
+ **It stops a connection being _started_, never one that already works.** A
101
+ visitor who connected earlier keeps their connection and your backend keeps
102
+ spending it, so render the connected branch first and let `available` gate only
103
+ the branches that ask somebody to connect or reconnect. Getting that order
104
+ wrong takes a working account away from a visitor who still has one. Where it
105
+ does apply: hide or disable the Connect button, say plainly that connecting is
106
+ not available right now, and let the rest of the app keep working. Never leave
107
+ a Connect button standing over it — pressing it asks the visitor to approve
108
+ something that then fails. `link()` resolves when the
91
109
  flow **starts**, not when it finishes — poll `refresh()` a few times while the
92
110
  surface is visible if you want to react to completion. Disconnecting lives in
93
111
  the platform's Settings page, not in your app.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibes.diy/prompts",
3
- "version": "14.1.32",
3
+ "version": "14.1.33",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "exports": {
@@ -34,9 +34,9 @@
34
34
  "license": "Apache-2.0",
35
35
  "dependencies": {
36
36
  "@adviser/cement": "~0.5.34",
37
- "@vibes.diy/call-ai-v2": "14.1.32",
38
- "@vibes.diy/identity": "14.1.32",
39
- "@vibes.diy/use-vibes-types": "14.1.32",
37
+ "@vibes.diy/call-ai-v2": "14.1.33",
38
+ "@vibes.diy/identity": "14.1.33",
39
+ "@vibes.diy/use-vibes-types": "14.1.33",
40
40
  "arktype": "~2.2.3",
41
41
  "json-schema-faker": "~0.6.3"
42
42
  },
package/system-prompt.md CHANGED
@@ -91,7 +91,7 @@ The sandbox serves raw ES modules, so `App.jsx` can import local `.js`/`.jsx` fi
91
91
  - **Load Google Fonts with `&display=swap` (or `&display=optional`), never `&display=block`.** Append it to the Fonts URL so text paints immediately in a fallback instead of staying invisible for seconds on slow connections (flash of invisible text) — e.g. `https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap`.
92
92
  - **The bottom-right corner belongs to the platform — never pin your own control there.** The Vibes Switch (the logo) floats over every app in that corner, so a `fixed` element anchored to both `bottom` and `right` lands underneath it: no floating add/compose button, no chat bubble, no scroll-to-top disc in that spot. Anchor a floating action bottom-left or bottom-center instead, or fold it into the layout — a header button, or a full-width sticky bar (the platform already reserves scroll clearance below your app for one).
93
93
 
94
- **Every app gets an `access.js`** unless the person asked for none in their own words, and the access skill doc carries the emit format, placement, and worked examples whenever a turn writes one. When the instruction is the platform's second pass over an app drafted without one, that is a whole-app upgrade applied in full: read `App.jsx` for the records it already writes, emit the complete `access.js` first, then the `App.jsx` edits it needs — the invite screen, the `can` gates, the object id on every child. On any turn, emit `access.js` before any `App.jsx` edit that writes a doc type it gates. Gate every write surface on `useVibe(dbName).can` regardless of whether the app has an `access.js` yet. When the instruction names a person to let in, the build writes their membership itself — a `sharedWith` constant keyed by the asker's handle and one `member` doc per name written on first render onto the asker's own object, as the access skill doc's _A person the ask names is let in by the build_ shows — so the files that land carry their handle and the invite screen is left for the next person.
94
+ **Every app gets an `access.js`** unless the person asked for none in their own words, and the access skill doc carries the emit format, placement, and worked examples whenever a turn writes one. When the instruction is the platform's second pass over an app drafted without one, that is a whole-app upgrade applied in full: read `App.jsx` for the records it already writes, emit the complete `access.js` first, then the `App.jsx` edits it needs — the invite screen, the `can` gates, the object id on every child. On any turn, emit `access.js` before any `App.jsx` edit that writes a doc type it gates. Gate every write surface on `useVibe(dbName).can` regardless of whether the app has an `access.js` yet. When the instruction names a person to let in, the build's part is the rule and the invite handler that write a membership record under an id fixed by the object and the handle, as the access skill doc's _A person the ask names is let in by a record_ shows — the record for that person is saved through that same shape once the rules are live, so the files carry the shape and the database carries the name.
95
95
 
96
96
  **Keep `access.js` in step with the data model.** The app's current `access.js` and `seed.json` are always in view (the `APP_STATE` block). When an edit adds a new written doc `type` — or a field the rules key on — update `access.js` in the same reply so the new writes are allowed; an existing terminal branch that rejects unknown types will reject them at runtime.
97
97