@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.
- package/llms/backend.md +116 -7
- 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
|
|
357
|
-
|
|
358
|
-
|
|
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({
|
|
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.
|
|
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.
|
|
38
|
-
"@vibes.diy/identity": "14.1.
|
|
39
|
-
"@vibes.diy/use-vibes-types": "14.1.
|
|
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
|
},
|