@vibes.diy/prompts 14.3.174 → 14.3.176

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/README.md CHANGED
@@ -21,7 +21,7 @@ From the group chat to the front counter. A character bot that answers in your v
21
21
 
22
22
  ## Yours to control
23
23
 
24
- Private by default — or open to the world. You choose. Keep an app to the people you invite, or publish it so anyone can use and remix it. You set who can read, write, and open it.
24
+ Private by default — or open to the world. You choose. Keep an app to the people you invite, or make it Public so anyone can use and remix it. You set who can read, write, and open it.
25
25
 
26
26
  > Your app's data lives in a centralized cloud database. "Private" means access-gated from other people — not encrypted, and not hidden from the platform.
27
27
 
package/llms/backend.md CHANGED
@@ -335,7 +335,11 @@ const routes = await ctx.github.routes(); // [{ repo, accountLogin, binderHandle
335
335
  calls to a service the owner holds a key for (`Authorization: Bearer
336
336
  ${ctx.secrets.KEY}` to an admitted API). Never write a value anywhere users
337
337
  can read it (a doc, a response body). Only the owner can set or rotate
338
- secrets; handlers see the current values on every invocation. **A visitor's
338
+ secrets; handlers see the current values on every invocation. A feature
339
+ that cannot work until the owner has set one is a setup item: the `/setup`
340
+ verdict and the owner-only Setup surface are the use-vibe docs' *Some apps
341
+ need setup before they work*, and the skill that needs the secret
342
+ contributes the item and its linked steps. **A visitor's
339
343
  own account (their GitHub, Threads, Instagram) is never a secret and never a
340
344
  pasted token — it is a connection**: declare it in `config.connections`, let
341
345
  the platform hold the credential, and read it through `@vibes.diy/social` or
package/llms/spotify.md CHANGED
@@ -19,31 +19,38 @@ visitor to a developer console to mint one: that is the flow connections exists
19
19
  replace. The console trip below is for the **owner** of this app, once, and for
20
20
  nobody else.
21
21
 
22
- ## 1. Bring your own app credentials, and explain how
23
-
24
- The owner registers an app with Spotify, once:
25
-
26
- 1. Open the Spotify developer dashboard and log in with a Spotify account.
27
- 2. Create an app — any name and description; a redirect URI is not used by this
28
- flow, so put the app's own URL in if the form insists on one.
29
- 3. Open the app's settings and copy the **Client ID**.
30
- 4. Reveal and copy the **Client secret**.
31
- 5. Set both on the vibe as `SPOTIFY_CLIENT_ID` and `SPOTIFY_CLIENT_SECRET` — the
32
- vibe's settings page, or `vibes-diy secrets set SPOTIFY_CLIENT_ID <the id>` and
33
- `vibes-diy secrets set SPOTIFY_CLIENT_SECRET <the secret>`.
34
-
35
- The app carries those five steps in its own UI, because the person reading them is
36
- the person who has to do them.
37
-
38
- When the credentials are missing, the app shows the owner the setup steps for a Spotify app instead of an error.
39
-
40
- The backend answers `{ ok: false, reason: "missing-key" }` when either
41
- `ctx.secrets.SPOTIFY_CLIENT_ID` or `ctx.secrets.SPOTIFY_CLIENT_SECRET` is absent,
42
- and the client renders the numbered steps, naming the Spotify developer dashboard
43
- and both secret names in so many words so the owner knows what to create and what
44
- to call it. Two secrets means two ways to be half-configured: say "missing-key"
45
- once and list both, rather than letting one set and one unset look like a broken
46
- API.
22
+ ## 1. The setup item: the owner's Spotify app credentials
23
+
24
+ The owner registers an app with Spotify, once. The Setup surface the item
25
+ lands on — the owner-only entry in the app's navigation, the verdict from
26
+ `/setup`, the re-read on focus that unlocks search in place — is the use-vibe
27
+ docs' *Some apps need setup before they work*; this recipe contributes one
28
+ item to it. The Spotify item is the credential pair: the backend answers missing-key while either ctx.secrets.SPOTIFY_CLIENT_ID or ctx.secrets.SPOTIFY_CLIENT_SECRET is absent, and its steps link the Spotify developer dashboard by URL and name both secrets in so many words.
29
+ The steps are written for someone who has never opened the dashboard, so each
30
+ one is a link to the exact page and uses the words that page uses:
31
+
32
+ 1. Open the Spotify developer dashboard: <https://developer.spotify.com/dashboard>
33
+ (log in with a Spotify account). Press **Create app** — any name and
34
+ description; for **Redirect URIs** put the app's own address, since this
35
+ flow never uses one — and save.
36
+ 2. Open the app you made and press **Settings**. Copy the **Client ID**, then
37
+ press **View client secret** and copy that too.
38
+ 3. Open this vibe's **Settings** tab on vibes.diy:
39
+ `https://vibes.diy/vibes/mine/<ownerHandle>/<appSlug>/settings` (that is
40
+ **My Vibes** → this vibe → **Settings**). Under **Secrets**, enter
41
+ `SPOTIFY_CLIENT_ID` and `SPOTIFY_CLIENT_SECRET`, paste each value, and
42
+ save. From a terminal the same thing is
43
+ `npx vibes-diy secrets set SPOTIFY_CLIENT_ID --vibe <ownerHandle>/<appSlug>`
44
+ and the same for `SPOTIFY_CLIENT_SECRET`.
45
+ 4. Come back to the app. Search works the moment both are saved; no redeploy.
46
+
47
+ Those steps are the item's copy on the Setup surface, with the vibe's real
48
+ handle and slug filled in from the `ownerHandle` and `appSlug` the `/setup`
49
+ route hands over. Two secrets means two ways to be half-configured: one item,
50
+ done only when both are present, so one set and one unset reads as *To do*
51
+ rather than as a broken API. In the consumer flow, a search whose pair is not
52
+ set reads *Search is not open yet*; the owner's Setup entry is where the
53
+ steps are, and later it is where a rotated secret goes.
47
54
 
48
55
  Only the owner can set or rotate a secret, and handlers see the current value on
49
56
  every invocation — so the page starts working the moment the pair lands, with no
@@ -141,9 +148,10 @@ API, and `{ "error", "error_description" }` on the token endpoint. Classify and
141
148
  pass the reason through:
142
149
 
143
150
  - `invalid_client` from the token endpoint — the id and secret do not match a real
144
- app. Show the setup steps from section 1 again.
151
+ app. Say so, and point the owner at the Setup entry: the item reads *Done*
152
+ because both values are present, and one of them is wrong.
145
153
  - `401` from the Web API — mint a fresh token and retry once; if the second try is
146
- also 401, show the setup steps.
154
+ also 401, say so and point the owner at the Setup entry the same way.
147
155
  - `429` — Spotify is rate-limiting the owner's app. The `Retry-After` header is
148
156
  seconds; say "Spotify asked us to wait N seconds" rather than "no results".
149
157
  - `404` — no such artist, album or track. Keep the id on screen.
@@ -247,6 +255,16 @@ export async function fetch(request, ctx) {
247
255
 
248
256
  // An uncaught throw reaches the app as a bare 500 naming only the error class, so every route answers with a reason.
249
257
  try {
258
+ // The setup verdict: ready for everyone, the item (and the handle + slug the Setup screen
259
+ // links with) for the owner alone.
260
+ if (url.pathname === "/setup") {
261
+ const owner = ctx.userInfo?.userHandle === ctx.appInfo.ownerHandle;
262
+ const items = [{ id: "spotify-app", label: "Spotify app credentials", done: credentials(ctx) !== null }];
263
+ const ready = items.every((item) => item.done);
264
+ if (!owner) return json({ ok: true, ready, owner });
265
+ return json({ ok: true, ready, owner, items, ownerHandle: ctx.appInfo.ownerHandle, appSlug: ctx.appInfo.appSlug });
266
+ }
267
+
250
268
  if (url.pathname === "/search") {
251
269
  const q = (url.searchParams.get("q") ?? "").trim();
252
270
  if (q === "") return json({ ok: true, tracks: [], artists: [] });
@@ -305,21 +323,71 @@ function artistRow(a) {
305
323
 
306
324
  ### `App.jsx`
307
325
  ```jsx app=spotify-browser
308
- import React, { useState } from "react";
309
- import { Button, Field, Input, Row } from "@vibes.diy/pouf";
326
+ import React, { useEffect, useRef, useState } from "react";
327
+ import { apiFetch } from "use-vibes";
328
+ import { Badge, Button, Field, Heading, Input, Row, Stack, Text } from "@vibes.diy/pouf";
329
+
330
+ // The setup verdict, read on mount and again whenever the app comes back into view: the owner
331
+ // sets the pair in another tab and returns, and search unlocks without a reload.
332
+ function useSetup() {
333
+ const [setup, setSetup] = useState(null); // { ready, owner, items?, ownerHandle?, appSlug? }
334
+ const latest = useRef(0);
335
+ useEffect(() => {
336
+ // Mount, focus and visibility reads overlap; only the latest one may land, or a slow stale
337
+ // answer overwrites a fresh one.
338
+ const load = () => {
339
+ const n = ++latest.current;
340
+ apiFetch("/setup").then((r) => r.json()).catch(() => ({ ready: false, owner: false })).then((s) => { if (n === latest.current) setSetup(s); });
341
+ };
342
+ load();
343
+ const onVisible = () => { if (document.visibilityState === "visible") load(); };
344
+ document.addEventListener("visibilitychange", onVisible);
345
+ window.addEventListener("focus", load);
346
+ return () => { document.removeEventListener("visibilitychange", onVisible); window.removeEventListener("focus", load); };
347
+ }, []);
348
+ return setup;
349
+ }
350
+
351
+ // The item's steps: each one a link to the exact page, in that page's own words, with this
352
+ // vibe's real handle and slug filled in.
353
+ function setupSteps(vibe) {
354
+ return [
355
+ { href: "https://developer.spotify.com/dashboard", link: "Spotify developer dashboard", text: ": log in, press Create app (any name; put this app's own address under Redirect URIs), and save." },
356
+ { href: "https://developer.spotify.com/dashboard", link: "your app's Settings", text: ": copy the Client ID, then press View client secret and copy that too." },
357
+ { href: `https://vibes.diy/vibes/mine/${vibe}/settings`, link: "this vibe's Settings tab", text: `: under Secrets, enter SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET, paste each value, and save — or run npx vibes-diy secrets set SPOTIFY_CLIENT_ID --vibe ${vibe} and the same for SPOTIFY_CLIENT_SECRET.` },
358
+ ];
359
+ }
310
360
 
311
- const SETUP_STEPS = [
312
- "Open the Spotify developer dashboard and log in.",
313
- "Create an app — any name and description will do.",
314
- "Copy the Client ID from the app's settings.",
315
- "Reveal and copy the Client secret.",
316
- "Set them on this vibe as SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET.",
317
- ];
361
+ // The Setup surface, the owner's alone.
362
+ function SetupScreen({ setup }) {
363
+ const item = setup.items.find((i) => i.id === "spotify-app");
364
+ return (
365
+ <Stack>
366
+ <Heading>Setup</Heading>
367
+ <Row gap={2}>
368
+ <Text>{item.label}</Text>
369
+ <Badge tone={item.done ? "up" : "info"}>{item.done ? "Done" : "To do"}</Badge>
370
+ </Row>
371
+ <ol>
372
+ {setupSteps(`${setup.ownerHandle}/${setup.appSlug}`).map((step) => (
373
+ <li key={step.link}>
374
+ Open <a href={step.href} target="_blank" rel="noreferrer">{step.link}</a>
375
+ {step.text}
376
+ </li>
377
+ ))}
378
+ </ol>
379
+ <Text muted>Both values stay on the server and are never sent to the browser. Search works the moment they are saved.</Text>
380
+ </Stack>
381
+ );
382
+ }
318
383
 
319
384
  export default function App() {
385
+ const setup = useSetup();
386
+ const [screen, setScreen] = useState("search");
320
387
  const [query, setQuery] = useState("");
321
388
  const [tracks, setTracks] = useState([]);
322
389
  const [problem, setProblem] = useState(null);
390
+ const ready = setup?.ready === true;
323
391
 
324
392
  const search = async () => {
325
393
  const res = await fetch(`/_api/search?q=${encodeURIComponent(query)}`);
@@ -333,46 +401,55 @@ export default function App() {
333
401
  };
334
402
 
335
403
  return (
336
- <div>
337
- <Row gap={2} align="top">
338
- <Field label="Search">{(id, describedBy) => <Input id={id} describedBy={describedBy} value={query} onChange={setQuery} placeholder="Search Spotify" />}</Field>
339
- <Button type="button" onClick={search}>Search</Button>
404
+ <Stack>
405
+ <Row gap={2}>
406
+ <Button variant="quiet" onClick={() => setScreen("search")}>Search</Button>
407
+ {/* The Setup entry: the owner's alone, highlighted while the item is still to do, quiet once done. */}
408
+ {setup?.owner && (
409
+ <Button variant={ready ? "quiet" : "solid"} onClick={() => setScreen("setup")}>
410
+ Setup{ready ? "" : " · to do"}
411
+ </Button>
412
+ )}
340
413
  </Row>
341
414
 
342
- {problem?.reason === "missing-key" && (
343
- <section>
344
- <h2>Add your Spotify app credentials</h2>
345
- <ol>
346
- {SETUP_STEPS.map((step) => (
347
- <li key={step}>{step}</li>
415
+ {screen === "setup" && setup?.owner ? (
416
+ <SetupScreen setup={setup} />
417
+ ) : (
418
+ <Stack>
419
+ {ready ? (
420
+ <Row gap={2} align="top">
421
+ <Field label="Search">{(id, describedBy) => <Input id={id} describedBy={describedBy} value={query} onChange={setQuery} placeholder="Search Spotify" />}</Field>
422
+ <Button type="button" onClick={search}>Search</Button>
423
+ </Row>
424
+ ) : (
425
+ <Text muted>Search is not open yet.</Text>
426
+ )}
427
+
428
+ {problem?.reason === "rate-limited" && <Text>Spotify asked us to wait {problem.retryAfter} seconds.</Text>}
429
+ {problem && problem.reason !== "rate-limited" && <Text role="alert">{problem.message}</Text>}
430
+
431
+ <ul>
432
+ {tracks.map((t) => (
433
+ <li key={t.id}>
434
+ {t.image && <img src={t.image} alt="" width={64} height={64} />}
435
+ <a href={t.url} target="_blank" rel="noreferrer">
436
+ {t.name}
437
+ </a>{" "}
438
+ — {t.artists}
439
+ </li>
348
440
  ))}
349
- </ol>
350
- <p>Both values stay on the server and are never sent to the browser.</p>
351
- </section>
441
+ </ul>
442
+
443
+ <section>
444
+ <h3>What this page can see</h3>
445
+ <p>
446
+ Spotify's public catalog — search, artists, albums and tracks. Your own top tracks, playlists and saved songs
447
+ belong to your Spotify account and are not part of this.
448
+ </p>
449
+ </section>
450
+ </Stack>
352
451
  )}
353
- {problem?.reason === "rate-limited" && <p>Spotify asked us to wait {problem.retryAfter} seconds.</p>}
354
- {problem && !["missing-key", "rate-limited"].includes(problem.reason) && <p role="alert">{problem.message}</p>}
355
-
356
- <ul>
357
- {tracks.map((t) => (
358
- <li key={t.id}>
359
- {t.image && <img src={t.image} alt="" width={64} height={64} />}
360
- <a href={t.url} target="_blank" rel="noreferrer">
361
- {t.name}
362
- </a>{" "}
363
- — {t.artists}
364
- </li>
365
- ))}
366
- </ul>
367
-
368
- <section>
369
- <h3>What this page can see</h3>
370
- <p>
371
- Spotify's public catalog — search, artists, albums and tracks. Your own top tracks, playlists and saved songs
372
- belong to your Spotify account and are not part of this.
373
- </p>
374
- </section>
375
- </div>
452
+ </Stack>
376
453
  );
377
454
  }
378
455
  ```