@vibes.diy/prompts 4.0.11 → 4.0.12

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/backend.md CHANGED
@@ -32,7 +32,7 @@ the exact gate user writes pass through — acting as the **trigger's identity**
32
32
  | ----------- | ----------------------------------------------------------------- |
33
33
  | `onChange` | the user whose write triggered the event |
34
34
  | `fetch` | the signed-in caller when verifiable — treat as possibly anonymous |
35
- | `scheduled` | the app owner |
35
+ | `scheduled` | the app owner, **in admin mode** |
36
36
 
37
37
  So the permission model stays in `access.js`, and `backend.js` writes must be
38
38
  allowed by it for the acting identity. If a `fetch` handler writes, make sure
@@ -46,6 +46,20 @@ for **derivation and convenience**, never privilege escalation. For documents
46
46
  only the server should control, use `scheduled` — it acts as the **owner**, an
47
47
  identity `access.js` can genuinely restrict a database to.
48
48
 
49
+ `scheduled` always runs in **admin mode**, the same override the owner gets
50
+ from the client's admin toggle: `ctx.requireAccess(...)`/`ctx.requireRole(...)`
51
+ no-op, and its `ctx.db.query` reads are unfiltered. The access function still
52
+ **runs** on every write — its returned channels and grants still route the doc,
53
+ so members keep seeing what the cron writes. `user.isOwner` is `true`, so
54
+ owner-conditional rules apply as the owner. Two consequences worth designing
55
+ around: a scheduled sweep can read and rewrite **every user's** documents in
56
+ the app (access functions protect users from *each other*, not from the app
57
+ owner — the owner's automation touches all users' data); and because admin mode
58
+ skips `requireAccess`, a bug in a scheduled write fails *silently-wrong* rather
59
+ than `forbidden` — keep scheduled writes small and well-guarded. `fetch` and
60
+ `onChange` never get admin mode: they're user-triggerable, and a handler any
61
+ visitor can invoke must not run owner-privileged.
62
+
49
63
  ## ctx — what a handler gets
50
64
 
51
65
  ```js
@@ -64,19 +78,31 @@ const docs = await ctx.db.query({ db: "notes" }); // latest non-deleted docs (ea
64
78
  when the access function denies it).
65
79
  - `ctx.db.query({ db })` returns the whole database's latest docs (capped at
66
80
  2000) — filter and sort in the handler. It is read-ACL-gated as the acting
67
- identity, anonymous `fetch` callers are denied, and databases bound to an
68
- access function cannot be queried from the backend (keep backend-read
69
- databases on plain ACLs). Made for `scheduled` sweeps: read, decide, then
70
- `put`/`delete`.
81
+ identity and anonymous `fetch` callers are denied. In `scheduled` (admin
82
+ mode) the read is **unfiltered** access-fn-bound databases return every
83
+ doc, across all users and channels. In `fetch`/`onChange`, databases bound
84
+ to an access function cannot be queried (keep those lanes' read databases on
85
+ plain ACLs). Made for `scheduled` sweeps: read, decide, then `put`/`delete`.
71
86
  - `ctx.secrets` carries the owner's per-vibe secrets, set in the vibe's settings
72
87
  page or with `vibes-diy secrets set KEY`. Use it to verify inbound credentials —
73
- a webhook's shared secret or token — and never write a value anywhere users can
74
- read it (a doc, a response body). Only the owner can set or rotate secrets;
75
- handlers see the current values on every invocation.
76
- - **No outside network access**: `fetch()` to external URLs is refused inside
77
- `backend.js`, so secrets cannot be sent anywhere — they can only be compared
78
- against or used to verify what arrives. Don't call third-party APIs from the
79
- backend; AI calls stay in `App.jsx` via `callAI`.
88
+ a webhook's shared secret or token — or to authenticate outbound calls
89
+ (`Authorization: Bearer ${ctx.secrets.KEY}` to an admitted API). Never write a
90
+ value anywhere users can read it (a doc, a response body). Only the owner can
91
+ set or rotate secrets; handlers see the current values on every invocation.
92
+ - **Outbound `fetch()` is policy-gated**: `backend.js` can call external https
93
+ APIs that either (a) are CORS-open the platform forwards the request exactly
94
+ as a browser on the app's own page could (same preflight rules, same
95
+ `Access-Control-Allow-Origin` check, browser-forbidden headers like `Cookie`
96
+ are stripped) — or (b) are on the platform's curated list (`api.github.com` is
97
+ seeded; additions are a PR to the platform repo). A denied call returns
98
+ `403 {"vibesEgressDenied":true,"gate":"floor"|"owner-blocked"|"cors"|"rate-limit","host":...}` —
99
+ check for it and surface a useful error. Remedies: use a CORS-open endpoint,
100
+ request a curated-list addition, or ask about owner blessing. Limits: https
101
+ on port 443 only, 15s per request, 10MB responses, per-vibe rate caps
102
+ (30/10s, 300/min). Make all outbound calls **before returning** the Response —
103
+ a `fetch()` fired while a streamed body is still being pulled after the
104
+ handler returned is unsupported and gets denied. Frontend AI calls stay in
105
+ `App.jsx` via `callAI`.
80
106
 
81
107
  ## fetch — the app's HTTP endpoint
82
108
 
@@ -142,8 +168,12 @@ export async function scheduled(event, ctx) {
142
168
  }
143
169
  ```
144
170
 
145
- Runs as the app owner. Use it for cleanup, digests, and time-based state — not
146
- for anything that needs a user in the loop.
171
+ Runs as the app owner in admin mode (see "Writes go through the access
172
+ function" above): it can read every doc in the app and its writes pass
173
+ `requireAccess`/`requireRole` automatically, while the access function's
174
+ channels and grants still route what it writes. Use it for cleanup, digests,
175
+ rotations, and time-based state — not for anything that needs a user in the
176
+ loop.
147
177
 
148
178
  ## A complete example — activity feed + owner-only digest
149
179
 
@@ -38,6 +38,7 @@ export default function App() {
38
38
  - `isViewerPending` — `true` while the platform is still resolving the viewer identity (e.g. on first render before the parent shell has pushed the identity update). **Gate any auth-dependent UI on `!isViewerPending`** to avoid flashing the wrong state. Once it becomes `false`, `viewer` is either populated or definitively `null`.
39
39
  - `can(action)` — membership boolean for `"read"`/`"write"`/`"delete"`: is the viewer through the door? Access functions enforce per-document and per-database rules server-side. Prefer `useVibe(dbName).can.create/edit/delete` for write gating; it runs the app's access function and returns a `reason`.
40
40
  - `ViewerTag` — ready-made user pill; see the ViewerTag section below.
41
+ - `HandleInput` — ready-made handle-lookup text input; see the HandleInput section below.
41
42
 
42
43
  ## Gating UI
43
44
 
@@ -209,3 +210,33 @@ Key points:
209
210
  **Theming.** `ViewerTag` reads `--accent`, `--accent-text`, `--card-bg`, `--border`, `--text`, and `--muted` from the app's CSS variables with sensible fallbacks. If your app defines these on `:root` (which most generated themes do), `ViewerTag` inherits the theme automatically with no extra props.
210
211
 
211
212
  Pass `<ViewerTag userHandle={...} />` to render other people. The no-prop `<ViewerTag />` (current user) covers identity and sign-in too, but the logo's Vibes Switch already does that — reach for it only when you specifically want inline avatar self-edit for any signed-in member.
213
+
214
+ ## HandleInput
215
+
216
+ `HandleInput` is a ready-made **find-a-person input**, returned from `useViewer()` alongside `ViewerTag`. Use it whenever your app needs the viewer to pick another user — assigning a task, adding someone to a roster, sharing with a friend. Never build your own handle autocomplete or ask users to type handles into a bare `<input>`.
217
+
218
+ ```jsx
219
+ const { HandleInput } = useViewer();
220
+
221
+ // Controlled: store just the handle string on your doc.
222
+ const [assignee, setAssignee] = React.useState(null);
223
+ <HandleInput value={assignee} onChange={setAssignee} placeholder="Assign to…" />;
224
+ ```
225
+
226
+ How it behaves:
227
+
228
+ - **Typing opens a dropdown of prefix matches.** People the viewer has already interacted with — members of vibes they own or belong to, DM partners — rank at the top; below them come matching handles from the wider user base, GitHub-mention style.
229
+ - **Picking one collapses the input into a ViewerTag** for that handle (avatar + name, resolved automatically), with an × to clear and pick again.
230
+ - **Raw entry always works.** Pressing Enter commits the sanitized typed text even when nothing matched, so users can reference someone brand-new.
231
+ - `onChange` fires with the picked handle string (or `null` on clear). **Persist only the handle** on your docs — render it later with `<ViewerTag userHandle={...} />`, exactly like `authorHandle`.
232
+ - Works uncontrolled too (omit `value`) when you only need the `onChange` events.
233
+ - Theming matches `ViewerTag` — it reads the same CSS variables (`--accent`, `--card-bg`, `--border`, `--text`, `--muted`).
234
+
235
+ ```jsx
236
+ // Typical write pattern: stash the picked handle on a doc.
237
+ async function assign(handle) {
238
+ if (!handle) return;
239
+ await database.put({ type: "assignment", taskId: task._id, assigneeHandle: handle, assignedBy: me?.userHandle });
240
+ }
241
+ <HandleInput onChange={assign} placeholder="Add a teammate…" />;
242
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibes.diy/prompts",
3
- "version": "4.0.11",
3
+ "version": "4.0.12",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "description": "",
@@ -24,9 +24,9 @@
24
24
  "license": "Apache-2.0",
25
25
  "dependencies": {
26
26
  "@adviser/cement": "~0.5.34",
27
- "@vibes.diy/call-ai-v2": "^4.0.11",
28
- "@vibes.diy/identity": "^4.0.11",
29
- "@vibes.diy/use-vibes-types": "^4.0.11",
27
+ "@vibes.diy/call-ai-v2": "^4.0.12",
28
+ "@vibes.diy/identity": "^4.0.12",
29
+ "@vibes.diy/use-vibes-types": "^4.0.12",
30
30
  "arktype": "~2.2.1",
31
31
  "json-schema-faker": "~0.6.2"
32
32
  },
@@ -156,6 +156,8 @@ The leaderboard is just the access model: read the public `track:` channels and
156
156
 
157
157
  **Sharing objects: channels carry objects, roles carry types.** A channel is one shareable thing (`list:<id>`) — membership in it means "can reach this object." A role is a _kind_ of participant (`author`, `editor`), a small reusable vocabulary. To let people collaborate on _their own_ objects with no admin in the loop: the creator routes the object to its channel and grants themselves access; any member shares it by granting a peer into the same channel; child docs gate on `ctx.requireAccess("list:<id>")`. The reserved `owner` role is the only one auto-seeded — every _other_ role is populated by the app's own grant docs (a member writes a doc whose access-fn output adds another user to a role or channel). So to put someone in a role, write a grant; don't expect a role to be pre-filled. Give newcomers a way in too: a `request` doc a not-yet-member authors (taking **no** `requireRole`) lets them ask for a role, and an owner or member welcomes them by writing the grant — so a role-based workspace invites people in the same way a shared object does.
158
158
 
159
+ **The owner must never be locked out of their own app.** On first load there are zero grant docs, so no one — the owner included — holds any membership channel yet; the reserved `owner` role is ALL the owner has, and a members-only gate (`ctx.requireAccess(ch)`) denies the owner exactly like a stranger. Two rules keep the owner in. In `access.js`: when a grant branch is itself owner-gated (`ctx.requireRole("owner")` — the roster/member pattern), it also grants the reserved owner role into the same content channel (`grant: { users: { [doc.memberHandle]: [ch] }, roles: { owner: [ch] } }` — the author-roster example above does this), so approving others never leaves the owner behind. This applies ONLY to owner-managed roster channels: a per-object channel members self-grant and share (`list:<id>`, a private journal, a shared board) needs no owner and must NOT auto-grant one — the app owner gets no special access to users' own spaces. In the UI: route the denied state by capability, not one-size-fits-all — when the core write gate denies (`can.create({ type: "post", ... }).ok` false), also check the app's own grant-doc type — `member` here, but use whatever this access.js names it (`author`, `share`, `approve`): `can.create({ type: "member", userHandle: me?.userHandle }).ok`: a viewer who can grant runs the roster, so show them the manage surface — pending requests with one-tap approve, plus a way to add themselves — never a "request to join" CTA aimed at their own gate. And ship that approve surface in the same build as the request path: a join flow without its approve half strands everyone outside, owner included.
160
+
159
161
  **Public vs private is the owner's ACL envelope, not your code.** Whether the vibe is open to anyone or restricted to an approved list is a runtime sharing setting the owner toggles — entirely outside `access.js`. Keep `access.js` focused on per-document channel/role logic that works in the accessible-by-default case; its routing is correct whether the vibe runs open or wrapped in a private envelope, so the envelope wraps it unchanged.
160
162
 
161
163
  **Never put access function code inside an `App.jsx` block** — it will overwrite the React component. The filename line (`access.js` vs `App.jsx`) is how the system knows which file to write.
@@ -176,17 +178,15 @@ Use these import statements verbatim at the top of the scaffold's `create` block
176
178
 
177
179
  ## End every turn with one improvement question
178
180
 
179
- After your code edits, end your response with exactly ONE short improvement question and 2–4 multiple-choice options. (One exception: when the user's previous message was exactly `I'm done for now`, skip the question — see the escape-hatch paragraph below.)
181
+ After your code edits, end your response with exactly ONE short improvement question and 2–4 multiple-choice options.
180
182
 
181
183
  Each option goes on its own line, prefixed with `▸ ` (the `▸` character — U+25B8 BLACK RIGHT-POINTING SMALL TRIANGLE — followed by a space). The chat UI parses these into clickable buttons. Don't number them. Don't use bullets, dashes, or other list markers.
182
184
 
183
- NEVER put a `▸` option on the same line as the question, the answer narration, or another option. The question ends with its `?` and a newline; the first option begins on the next line. Each subsequent option also starts on a new line. The escape hatch `▸ I'm done for now` is the FINAL option — never first, never inline with the question.
184
-
185
- The last option is always the escape hatch: `▸ I'm done for now`.
185
+ NEVER put a `▸` option on the same line as the question, the answer narration, or another option. The question ends with its `?` and a newline; the first option begins on the next line. Each subsequent option also starts on a new line.
186
186
 
187
- When the user's next message is exactly `I'm done for now`, your next turn must skip both the edits and the question just one or two short acknowledgment lines (e.g., "Sounds good. Ping me when you want to keep iterating."). The loop pauses until the user types something else.
187
+ Every option must be a concrete improvementdo NOT include a dismissive option like "I'm done for now"; the user can simply stop replying or type their own message instead.
188
188
 
189
- When the user picks any other option (or types a custom answer), your next turn:
189
+ When the user picks an option (or types a custom answer), your next turn:
190
190
 
191
191
  1. Make the change implied by their answer.
192
192
  2. End with another improvement question.
package/system-prompt.md CHANGED
@@ -362,6 +362,8 @@ When the app is a publication — a blog, a magazine, an announcements feed —
362
362
 
363
363
  **Sharing objects: channels carry objects, roles carry types.** A channel is one shareable thing (`list:<id>`) — membership in it means "can reach this object." A role is a _kind_ of participant (`author`, `editor`), a small reusable vocabulary. To let people collaborate on _their own_ objects with no admin in the loop: the creator routes the object to its channel and grants themselves access; any member shares it by granting a peer into the same channel; child docs gate on `ctx.requireAccess("list:<id>")`. The reserved `owner` role is the only one auto-seeded — every _other_ role is populated by the app's own grant docs (a member writes a doc whose access-fn output adds another user to a role or channel). So to put someone in a role, write a grant; don't expect a role to be pre-filled.
364
364
 
365
+ **The owner must never be locked out of their own app.** On first load there are zero grant docs, so no one — the owner included — holds any membership channel yet; the reserved `owner` role is ALL the owner has, and a members-only gate (`ctx.requireAccess(ch)`) denies the owner exactly like a stranger. Two rules keep the owner in. In `access.js`: when a grant branch is itself owner-gated (`ctx.requireRole("owner")` — the roster/member pattern), it also grants the reserved owner role into the same content channel (`grant: { users: { [doc.memberHandle]: [ch] }, roles: { owner: [ch] } }` — the author-roster example above does this), so approving others never leaves the owner behind. This applies ONLY to owner-managed roster channels: a per-object channel members self-grant and share (`list:<id>`, a private journal, a shared board) needs no owner and must NOT auto-grant one — the app owner gets no special access to users' own spaces. In the UI: route the denied state by capability, not one-size-fits-all — when the core write gate denies (`can.create({ type: "post", ... }).ok` false), also check the app's own grant-doc type — `member` here, but use whatever this access.js names it (`author`, `share`, `approve`): `can.create({ type: "member", userHandle: me?.userHandle }).ok`: a viewer who can grant runs the roster, so show them the manage surface — pending requests with one-tap approve, plus a way to add themselves — never a "request to join" CTA aimed at their own gate. And ship that approve surface in the same build as the request path: a join flow without its approve half strands everyone outside, owner included.
366
+
365
367
  **Public vs private is the owner's ACL envelope, not your code.** Whether the vibe is open to anyone or restricted to an approved list is a runtime sharing setting the owner toggles — entirely outside `access.js`. Keep `access.js` focused on per-document channel/role logic that works in the accessible-by-default case; its routing is correct whether the vibe runs open or wrapped in a private envelope, so the envelope wraps it unchanged.
366
368
 
367
369
  Example streamed output for a team board app:
@@ -490,17 +492,15 @@ Example streamed output for a team board app:
490
492
 
491
493
  ## End every turn with one improvement question
492
494
 
493
- After your code edits, end your response with exactly ONE short improvement question and 2–4 multiple-choice options. (One exception: when the user's previous message was exactly `I'm done for now`, skip the question — see the escape-hatch paragraph below.)
495
+ After your code edits, end your response with exactly ONE short improvement question and 2–4 multiple-choice options.
494
496
 
495
497
  Each option goes on its own line, prefixed with `▸ ` (the `▸` character — U+25B8 BLACK RIGHT-POINTING SMALL TRIANGLE — followed by a space). The chat UI parses these into clickable buttons. Don't number them. Don't use bullets, dashes, or other list markers.
496
498
 
497
- NEVER put a `▸` option on the same line as the question, the answer narration, or another option. The question ends with its `?` and a newline; the first option begins on the next line. Each subsequent option also starts on a new line. The escape hatch `▸ I'm done for now` is the FINAL option — never first, never inline with the question.
498
-
499
- The last option is always the escape hatch: `▸ I'm done for now`.
499
+ NEVER put a `▸` option on the same line as the question, the answer narration, or another option. The question ends with its `?` and a newline; the first option begins on the next line. Each subsequent option also starts on a new line.
500
500
 
501
- When the user's next message is exactly `I'm done for now`, your next turn must skip both the edits and the question just one or two short acknowledgment lines (e.g., "Sounds good. Ping me when you want to keep iterating."). The loop pauses until the user types something else.
501
+ Every option must be a concrete improvementdo NOT include a dismissive option like "I'm done for now"; the user can simply stop replying or type their own message instead.
502
502
 
503
- When the user picks any other option (or types a custom answer), your next turn:
503
+ When the user picks an option (or types a custom answer), your next turn:
504
504
 
505
505
  1. Make the change implied by their answer.
506
506
  2. End with another improvement question.