@vibes.diy/prompts 14.1.36 → 14.1.38

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.
Files changed (2) hide show
  1. package/llms/backend.md +116 -7
  2. package/package.json +4 -4
package/llms/backend.md CHANGED
@@ -149,7 +149,7 @@ const json = JSON.parse(
149
149
  await ctx.callAI("rate this", { schema: { properties: { verdict: { type: "string" }, score: { type: "number" } } } })
150
150
  ); // structured output
151
151
  const res = await ctx.fetch("https://api.example.com/x", { headers }); // outbound HTTP — use this, not bare fetch()
152
- await ctx.notify({ toHandle: "alice", body: "Your turn", dedupeKey: "game-7-turn-3" }); // one participant; throws on failure
152
+ await ctx.notify({ toHandle: "alice", body: "Your turn", dedupeKey: "game-7-turn-3", path: "#game-7" }); // one participant; `path` optional; throws on failure
153
153
  ctx.log("info", "tick finished", { written: 3 }); // diagnostics — read back with: vibes-diy app logs owner/slug
154
154
  const gh = await ctx.github.fetch("/repos/acme/site/issues", { method: "POST", body: { title: "Filed from the vibe" } }); // connected GitHub repo
155
155
  ```
@@ -352,11 +352,11 @@ const gh = await ctx.github.fetch("/repos/acme/site/issues", { method: "POST", b
352
352
  Keep interactive/streaming AI in `App.jsx` via `callAI`; use `ctx.callAI`
353
353
  when the result must be server-authoritative (moderation, digests,
354
354
  summaries users shouldn't be able to forge).
355
- - **`ctx.notify({ toHandle, body, dedupeKey })` sends one person a notification.**
356
- It takes a single object with all three fields — match the object form in the
357
- example above, which is the calling convention (codegen has been observed
358
- passing these positionally). The message lands in that person's bell, inbox,
359
- and push notifications.
355
+ - **`ctx.notify({ toHandle, body, dedupeKey, path })` sends one person a notification.**
356
+ It takes a single object; `toHandle`, `body` and `dedupeKey` are required and
357
+ `path` is optional — match the object form in the example above, which is the
358
+ calling convention (codegen has been observed passing these positionally).
359
+ The message lands in that person's bell, inbox, and push notifications.
360
360
  - `toHandle` is one handle, and the platform delivers to people already in
361
361
  this app: the owner, an accepted invite or approved request, or a handle
362
362
  named in the access function's persisted grant state.
@@ -366,6 +366,28 @@ const gh = await ctx.github.fetch("/repos/acme/site/issues", { method: "POST", b
366
366
  - `dedupeKey` names the happening, so one event delivers once however many
367
367
  times the handler runs: `"round-4-started"`, `"order-42-shipped"`,
368
368
  `"game-7-turn-3"`. The platform keys it per (app, recipient).
369
+ - `path` says WHERE in your app the notification points, so the bell and the
370
+ email land on the thing it is about instead of your front page. It must be
371
+ a `#` fragment — `"#game-7"`, `"#order/42"` — of at most 200 characters with
372
+ no spaces or line breaks, and nothing else: a fragment can only ever address
373
+ a position inside your own app, which is why it is the only form allowed.
374
+ Omit it and the notification links to the app itself, exactly as before.
375
+ **A malformed `path` fails the whole `ctx.notify` call** (the promise
376
+ rejects) rather than being quietly ignored — so a dead deep link is
377
+ something you find out about, not something your users find out about.
378
+ - Read it back in `App.jsx` from your own hash, and listen for changes,
379
+ because a click while the app is already open moves the hash without
380
+ reloading:
381
+
382
+ ```jsx
383
+ const [target, setTarget] = useState(() => window.location.hash.slice(1));
384
+ useEffect(() => {
385
+ const onHash = () => setTarget(window.location.hash.slice(1));
386
+ window.addEventListener("hashchange", onHash);
387
+ return () => window.removeEventListener("hashchange", onHash);
388
+ }, []);
389
+ // target === "game-7" → open that game
390
+ ```
369
391
  - `await` it and let it throw. Success resolves with nothing to inspect; a
370
392
  missing field, an over-long body, or a handle outside the app rejects the
371
393
  promise. Usually the surrounding work should survive an undeliverable
@@ -373,7 +395,12 @@ const gh = await ctx.github.fetch("/repos/acme/site/issues", { method: "POST", b
373
395
 
374
396
  ```js
375
397
  try {
376
- await ctx.notify({ toHandle: game.opponent, body: "Your turn", dedupeKey: `game-${game._id}-turn-${game.turn}` });
398
+ await ctx.notify({
399
+ toHandle: game.opponent,
400
+ body: "Your turn",
401
+ dedupeKey: `game-${game._id}-turn-${game.turn}`,
402
+ path: `#game-${game._id}`, // lands them on THAT game, not the lobby
403
+ });
377
404
  } catch (err) {
378
405
  ctx.log("warn", "notify failed", { game: game._id, reason: err.message });
379
406
  }
@@ -815,6 +842,87 @@ And **only say a thing you computed.** "Tonight", "this morning", "in an hour"
815
842
  are claims about the reader's clock; if the code did not derive them from that
816
843
  reader's zone, name the moment instead and let them place it.
817
844
 
845
+ ### Recurring reminders on somebody's clock
846
+
847
+ **A recurring reminder fires on each person's own clock, never the server's.**
848
+ Once a reminder repeats — "every weekday at 7" — the zone stops being one
849
+ setting on one schedule document and becomes a property of each person the
850
+ reminder is for. Everything below is per person, and none of it is the
851
+ server's day or the server's hour.
852
+
853
+ **Capture the zone as an effect, not as a question.** Do not ask them for a
854
+ timezone: the browser already knows it, so write it once when they first sign
855
+ in and leave it alone. Store it where the reminder is addressed — on the
856
+ person's own profile document when the list is shared — and guard the write so
857
+ it only ever happens on a real change, or the effect re-fires forever.
858
+
859
+ **Derive the weekday and the hour together, in that zone, from one
860
+ `formatToParts` call.** `getUTCDay()`/`getUTCHours()` answer a question about
861
+ the machine the handler happens to be running on; at 07:00 in Los Angeles it is
862
+ already a different day in UTC, so a weekday gate on the server's clock silently
863
+ drops Monday mornings and fires on Saturday ones.
864
+
865
+ **An unrecognised zone string throws.** `Intl.DateTimeFormat` raises a
866
+ `RangeError` on a zone it does not know, so in a loop over people the formatting
867
+ goes in a `try` and a bad profile is skipped — one stale record must not stop
868
+ everybody else's reminder.
869
+
870
+ The unit of work is that person's local date (`en-CA` formats it as
871
+ `YYYY-MM-DD`), keyed per person, per "A tick that repeats is not a run that
872
+ repeats" above: the interval is only how often the handler wakes up.
873
+
874
+ App.jsx — write the zone once, when they arrive:
875
+
876
+ ```jsx
877
+ useEffect(() => {
878
+ if (!me?.userHandle) return;
879
+ const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
880
+ if (!timeZone || myProfile?.timeZone === timeZone) return; // already stored — don't churn
881
+ database.put({ _id: `profile:${me.userHandle}`, type: "profile", userHandle: me.userHandle, timeZone });
882
+ }, [me?.userHandle, myProfile?.timeZone]);
883
+ ```
884
+
885
+ backend.js — evaluate each person in their own zone:
886
+
887
+ ```js
888
+ export const config = { scheduled: { interval: "15m" } };
889
+ const WEEKDAYS = new Set(["Mon", "Tue", "Wed", "Thu", "Fri"]);
890
+
891
+ export async function scheduled(event, ctx) {
892
+ const at = new Date(event.scheduledTime);
893
+ const profiles = await ctx.db.query({ db: DB, field: "type", key: "profile", limit: 500 });
894
+ ctx.log("debug", "reminder tick", { profiles: profiles.length });
895
+
896
+ for (const profile of profiles) {
897
+ const { timeZone, userHandle } = profile;
898
+ if (!timeZone || !userHandle) continue;
899
+ let weekday, hour, dateKey;
900
+ try {
901
+ // One formatter, one zone: the weekday and the hour must agree about the day.
902
+ const parts = new Intl.DateTimeFormat("en-US", {
903
+ timeZone,
904
+ weekday: "short",
905
+ hour: "2-digit",
906
+ hourCycle: "h23",
907
+ }).formatToParts(at);
908
+ weekday = parts.find((p) => p.type === "weekday")?.value;
909
+ hour = Number(parts.find((p) => p.type === "hour")?.value);
910
+ dateKey = new Intl.DateTimeFormat("en-CA", { timeZone }).format(at); // "2026-09-18", their day
911
+ } catch {
912
+ continue; // unrecognised zone on one profile — skip the person, not the tick
913
+ }
914
+ if (!WEEKDAYS.has(weekday) || hour !== 7) continue;
915
+
916
+ const id = `morningReminder:${userHandle}:${dateKey}`; // the unit of work, per person
917
+ if (await ctx.db.get(id, { db: DB })) continue; // this morning is already done
918
+ await ctx.db.put(
919
+ { _id: id, type: "morningReminder", ownerHandle: userHandle, date: dateKey, checkedAt: at.toISOString() },
920
+ { db: DB }
921
+ );
922
+ }
923
+ }
924
+ ```
925
+
818
926
  ### An unattended tick reports what it did
819
927
 
820
928
  **An unattended tick earns its spend.** Nobody is watching a scheduled run, so
@@ -983,6 +1091,7 @@ export async function scheduled(event, ctx) {
983
1091
  toHandle: ctx.appInfo.ownerHandle,
984
1092
  body: "Your six jokes are drafted and ready for the show.",
985
1093
  dedupeKey: `draft-${dayKey}`,
1094
+ path: `#draft/${dayKey}`,
986
1095
  });
987
1096
  } catch (err) {
988
1097
  // The day stays claimed: the owner sees what went wrong and is not billed twice for it.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibes.diy/prompts",
3
- "version": "14.1.36",
3
+ "version": "14.1.38",
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.36",
38
- "@vibes.diy/identity": "14.1.36",
39
- "@vibes.diy/use-vibes-types": "14.1.36",
37
+ "@vibes.diy/call-ai-v2": "14.1.38",
38
+ "@vibes.diy/identity": "14.1.38",
39
+ "@vibes.diy/use-vibes-types": "14.1.38",
40
40
  "arktype": "~2.2.3",
41
41
  "json-schema-faker": "~0.6.3"
42
42
  },