@homie-rocks/studio 0.22.0 → 0.24.0

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 (45) hide show
  1. package/CHANGELOG.md +61 -1
  2. package/README.md +59 -0
  3. package/bin/homie-studio.mjs +68 -2
  4. package/chat/CHAT.md +101 -0
  5. package/chat/OWNERS.md +78 -0
  6. package/lib/build.mjs +20 -2
  7. package/lib/chat-cli.mjs +89 -0
  8. package/lib/cloudflare.mjs +5 -2
  9. package/lib/scaffold.mjs +59 -2
  10. package/lib/shop.mjs +270 -0
  11. package/lib/template-history.json +77 -0
  12. package/netplay/NETPLAY.md +194 -4
  13. package/netplay/netplay.ts +156 -3
  14. package/package.json +7 -1
  15. package/port/index.ts +3 -2
  16. package/port/view.ts +238 -0
  17. package/shop/SELLING.md +83 -0
  18. package/shop/SHOP.md +154 -0
  19. package/shop/shop.ts +130 -0
  20. package/site/SITE.md +22 -1
  21. package/starters/ember-vale/src/main.ts +22 -2
  22. package/starters/gem-rush/src/main.ts +21 -1
  23. package/starters/gem-rush-3d/src/main.ts +22 -1
  24. package/worker/account-page.mjs +14 -2
  25. package/worker/agents.mjs +3 -3
  26. package/worker/chat-page.mjs +556 -0
  27. package/worker/chat-store.mjs +161 -0
  28. package/worker/chat-words.mjs +46 -0
  29. package/worker/chat.mjs +419 -0
  30. package/worker/index.mjs +189 -7
  31. package/worker/office-page.mjs +120 -5
  32. package/worker/office.mjs +173 -6
  33. package/worker/pages.mjs +25 -8
  34. package/worker/players.mjs +8 -0
  35. package/worker/referrals.mjs +328 -0
  36. package/worker/room.mjs +291 -11
  37. package/worker/servers.mjs +5 -1
  38. package/worker/shop-page.mjs +532 -0
  39. package/worker/shop-rules.mjs +277 -0
  40. package/worker/shop-store.mjs +217 -0
  41. package/worker/shop.mjs +686 -0
  42. package/worker/site.mjs +4 -1
  43. package/worker/stats.mjs +4 -1
  44. package/worker/stripe.mjs +149 -0
  45. package/worker/version.mjs +1 -1
package/CHANGELOG.md CHANGED
@@ -17,9 +17,69 @@ To bring a studio up to date, tell Claude: "Upgrade my studio to the newest Homi
17
17
  `npx -y @homie-rocks/studio@latest upgrade`, which shows what's new since the version your studio pins (from this
18
18
  file) and what the upgrade would change, and changes nothing until you agree.
19
19
 
20
+ ## [0.24.0] - 2026-10-03
21
+
22
+ **Plugin 0.25.0** · [#34](https://github.com/homie-rocks/homie/pull/34)
23
+
24
+ Sell things in your games with your own Stripe account: a supporter pack, cosmetics, a pass, an unlock or a tip, in real money, with refunds from your office and the kids rules built in; Homie never sees the money.
25
+
26
+ ### Added
27
+
28
+ - The shop: `shop.json` at your studio's root lists what you sell (`cosmetic`, `supporter`, `pass`, `unlock`, `tip`), each in real money with the entitlement keys your game reads. `homie-studio shop init --supporter` writes a US$5 Supporter pack (a badge on the player's profile and beside their name in rooms, for a year) and `SELLING.md`, plain words on what selling makes you responsible for (refunds, disputes, tax, kids; not legal advice).
29
+ - Checkout is Stripe's own hosted page, opened by your studio's Worker with your own restricted key, one item at a time. Stripe Tax is on by default; Stripe Managed Payments (Stripe as the seller of record: it registers, files and pays the tax and answers disputes, for 3.5% more) is one switch, `"till": "stripe-managed"`. Stripe's receipt email is the receipt.
30
+ - A signed webhook (`/api/shop/hook`: Stripe's signature, within five minutes, each event once) is the only thing that marks an order paid, refunded or disputed. What a player owns is tied to their passkey account and follows them to every device.
31
+ - `homie-studio shop connect`: a page on your own computer where you paste the restricted key and the webhook secret. It says exactly what to make in Stripe, offers Managed Payments in plain words, and puts both straight into your Worker's secrets: never the chat, a file or a log. Test keys only unless `--live`.
32
+ - In a game: `createShop()` from `@homie-rocks/studio/shop` gives `shop.has('skin:ember')`, `shop.entitlements()`, `shop.on('change')`, `shop.open(item)` and `shop.used(key)`. The play page has a store sheet (from the game, or Shop in the room sheet) that opens Stripe in a new tab while the game keeps running; on a television it shows only a code to buy on a phone.
33
+ - A supporter's badge rides on their seat in every room (`peer.badge`, set by your Worker from what the account owns; a hello can never claim one), and their account page lists their purchases and badges.
34
+ - Refunds: one tap in your office (`/_studio/office/shop`), and a player's own refund of an unused item within your refund window (at least 14 days). Your AI can only ask for a refund (`homie-studio shop refund <order>`), and you confirm it with one tap.
35
+ - The office's shop page: the last 30 days, every order with its Stripe page, disputes, and payouts, balance and tax as links to your own Stripe Dashboard, plus a CSV for your accountant with no names in it.
36
+ - Referrals: a `?via=` link from another site (another studio, or homie.rocks on the same terms) is remembered on a new player's first visit; a kept sale owes that referrer the rate in `shop.json`. `homie-studio shop statements` signs each referrer's monthly statement with your studio's key; the referrer checks it against your manifest and invoices you. Nothing moves through Homie.
37
+ - New D1 migration `0008_studio_shop.sql`: orders, entitlements, the age band, parent links, webhook events and referral books. No card, no address, no email.
38
+ - The kids and fairness rules are built in, not settings. No shop on a kids server, and none at all in a studio whose `studio.json` says `"audience": "kids"`.
39
+ - Spending is off on every account until one neutral question (the year you were born, no default) says adult; the answer is kept only as adult, teen or child. Under 13: nothing, ever. 13 to 17: a one-time link a parent opens on their own phone and pays in their own name.
40
+ - Nothing random for money, no gems or coins, no countdown offers, and nothing that changes play on a beginner server (`"advantage": true` items are not sold or counted there). `shop check` and every build refuse a `shop.json` that breaks these. A monthly cap per player (US$50 at most).
41
+ - A card dispute never locks or deletes an account: nothing changes while it is open, and a lost one takes back only that item.
42
+
43
+ ### Changed
44
+
45
+ - Deleting a player account that owns things asks first, and keeps the orders without the player. A player's export includes their orders.
46
+ - The directory manifest says whether the shop is open and its till, and how this studio takes referral statements (with the public half of its statement key). Never a key or a sale.
47
+
48
+ ### Upgrade notes
49
+
50
+ - `homie-studio upgrade --apply`, then `npm run deploy` applies migration 0008. Nothing is sold until you add `shop.json` and connect a key.
51
+ ## [0.23.0] - 2026-10-03
52
+
53
+ **Plugin 0.24.0** · [#33](https://github.com/homie-rocks/homie/pull/33) · [release-2026-10-03-studio-0.23.0](https://github.com/homie-rocks/homie/releases/tag/release-2026-10-03-studio-0.23.0)
54
+
55
+ Room chat in every game: reactions that float up every screen, quick lines, typing where the rules allow, speech bubbles over characters, rules per game and server, moderation in your own Worker, and the owner's tools.
56
+
57
+ ### Added
58
+
59
+ - Room chat on every game's play page, with no game code: a Chat pill beside the room button (the corner `screen.share` keeps clear of the game's HUD) and its sheet: the room's last minutes, the five reactions (fire, clap, laugh, heart, wow, in homie.rocks's order, plus up to three of the game's own), the game's quick lines, typing where the rules allow it (and why not where they do not), and Report on a line. New lines show for a moment where the status chip sits (game.json `"screen": { "chat": … }` moves them).
60
+ - Reactions float up every screen in the room, as on the television: the players', the watchers' and the big screen's (at most six a second, eighteen at once). The big screen (`/<game>/tv`) shows the room's lines in a corner, and the watch page has a Chat button and panel. Measured in Chrome with four browsers in one room: a reaction is on the other screens within about 15 ms of the press locally.
61
+ - Speech bubbles over characters: the port kit's `createBubbles` and `paintBubbles` (beside `createLabels`), and the helper's `net.on('say')`. Gem Rush, Ember Vale and Gem Rush 3D draw what a player says over their name. Each player chooses "Show my messages over my character" and "Show chat on this screen".
62
+ - Rules per game and per server: game.json `"chat"` sets a game's defaults (off, emoji, quick lines or typing; who may type and who may react: anyone, signed in with a passkey, or members; slow mode, length, links, swears, the review, bubbles, watchers, homie.rocks), and the owner overrides them in the office or with `homie-studio chat rules`. A kids server and every beginner server keep chat to emoji and quick lines. By default emoji and quick lines are open to everyone and typing needs a signed-in player account.
63
+ - Moderation in the studio's own Worker before a typed line reaches anyone: a built-in word list and patterns (slurs, sexual words, self-harm, personal questions such as a player's age or where they live, contact details and other apps, links, swears) plus your own words, then a review on your own Workers AI with Cloudflare's Clef decision model (`@cf/cloudflare/clef-flash`), within a daily budget of 2,000 neurons (about 850 typed messages; inside the free allocation). Emoji and quick lines are never reviewed. When the review cannot answer, the word list decides alone. `chat/CHAT.md` says why Clef and what the alternatives cost.
64
+ - The owner's tools: every live room's last minutes in the office with Remove (from every screen), Mute and Kick from a line (a watcher too, their lines taken down with them), the same three in the owner's own game, players' reports with Dismiss, the review's day, and an announcement as a studio line in the chat. `homie-studio chat` (rules, rooms, reports), `chat rules`, `chat remove`, `chat budget` and `chat words`.
65
+ - A studio's `/api/rooms` says which rooms let homie.rocks show their chat (`chat: true`), so homie.rocks's room cards can show a room's chat and emoji live from the room's own socket (the hub's side ships with homie.rocks). Turn it off per game or server with the `hub` rule.
66
+ - `chat/OWNERS.md`: a plain note for studio owners on what the chat tooling does, what is stored and for how long, and children's data (not legal advice).
67
+ - Netplay revision 8 (NETPLAY.md section 19): `say` and `react` frames up, `line`, `react`, `lines`, `unline` and `held` down, `policy.chat`, and the owner's `unsay`; the helper's `net.say`, `net.sayLine`, `net.react`, `net.chatRules` and `on('chat' | 'say' | 'unchat' | 'held')`.
68
+
69
+ ### Changed
70
+
71
+ - `homie-studio deploy` binds Workers AI when a game lets its players type (the review), not only when an AI guide thinks with it.
72
+ - `homie-studio dev --remote-ai` works again: Wrangler's `--local` turned every remote binding off.
73
+
74
+ ### Upgrade notes
75
+
76
+ - `homie-studio upgrade --apply` (or the next deploy) adds migration `0007_studio_chat.sql`: the owner's chat rules and players' reports. The chat itself is never stored.
77
+ - Games get the chat panel, the float and the ticker as soon as the studio deploys this version. Rebuild a game to draw speech bubbles (its netplay helper is revision 8).
78
+ - Typing needs a signed-in player account by default. To let guests type, or to keep a game to emoji and quick lines, set game.json `"chat"` or use `homie-studio chat rules`.
79
+
20
80
  ## [0.22.0] - 2026-10-02
21
81
 
22
- **Plugin 0.23.0** · [#32](https://github.com/homie-rocks/homie/pull/32)
82
+ **Plugin 0.23.0** · [#32](https://github.com/homie-rocks/homie/pull/32) · [release-2026-10-03-studio-0.22.0](https://github.com/homie-rocks/homie/releases/tag/release-2026-10-03-studio-0.22.0)
23
83
 
24
84
  Your game's look as decisions you can steer and lock, a style board the engine draws for free, free CC0 3D models, props made on your own fal account, and every asset licensed and checked for phones.
25
85
 
package/README.md CHANGED
@@ -26,6 +26,7 @@ npx homie-studio publish # the homie.rocks directo
26
26
  npx homie-studio stats # the studio's own numbers, for its owner
27
27
  npx homie-studio office # who is playing now, in every live room (the back office)
28
28
  npx homie-studio servers # each game's servers: humans-only, hybrid AI seats, beginner guides
29
+ npx homie-studio chat # room chat: each game's rules, the rooms' last minutes, reports
29
30
  npx homie-studio upgrade # what a newer template adds to this studio (--apply to take it)
30
31
  ```
31
32
 
@@ -286,6 +287,64 @@ npx homie-studio agents pass night-rush --label Claude
286
287
  - Everything is in the studio's own Worker and D1 (migration `0006_studio_servers.sql`). The site's side is in
287
288
  [site/SITE.md](site/SITE.md); the room's in [netplay/NETPLAY.md](netplay/NETPLAY.md) sections 17 and 18.
288
289
 
290
+ ## Room chat
291
+
292
+ Every game has room chat (0.23.0), with no game code: reactions that float up every screen in the room (players,
293
+ watchers, the big screen), the game's quick lines, and typing where the room's rules allow it. It is the Homie app's
294
+ live-room chat and homie.rocks's room chat in a studio's own game: the same five reactions in the same order, the
295
+ same message shapes, the same float on a television.
296
+
297
+ ```sh
298
+ npx homie-studio chat rules night-rush --mode lines # emoji and quick lines only
299
+ npx homie-studio chat rules night-rush --who anyone --slow 5 # guests may type, one line every 5 s
300
+ npx homie-studio chat rules night-rush --server first-light --mode emoji
301
+ npx homie-studio chat # rules, every room's last minutes, reports
302
+ npx homie-studio chat remove night-rush pub-3 <line id> # take a line down on every screen
303
+ npx homie-studio chat words # the built-in word list the floor holds
304
+ ```
305
+
306
+ - **The play page:** a Chat pill beside the room button (the corner `screen.share` keeps clear of the HUD), its
307
+ sheet (the last minutes, the reactions, the quick lines, typing, Report), a short ticker of new lines, and two
308
+ switches each player keeps: "Show my messages over my character" and "Show chat on this screen". The big screen
309
+ shows the lines in a corner; the watch page has a Chat button; homie.rocks shows a room's chat on its card.
310
+ - **Rules** per game (game.json `"chat"`) and per server, in the office or with `chat rules`: off, emoji, quick
311
+ lines or typing; who may type and who may react (anyone, signed in with a passkey, members); slow mode, length,
312
+ links, swears, the review, bubbles, watchers, homie.rocks. Default: emoji and quick lines for anyone, typing for
313
+ signed-in players. A kids server and every beginner server keep chat to emoji and quick lines.
314
+ - **Moderation, in the studio's own Worker, before anyone sees a typed line:** a built-in word list and patterns
315
+ (slurs, sexual words, self-harm, personal questions and contact details, links, swears, your own words), then
316
+ Cloudflare's **Clef** decision model on the studio's own Workers AI (`@cf/cloudflare/clef-flash`, about 2.3
317
+ neurons a message measured, 2,000 a day by default: inside the free allocation). Emoji and quick lines are never reviewed.
318
+ - **The owner:** every live room's last minutes in the office with Remove, Mute and Kick (from a line, with their
319
+ lines taken down), the same in the owner's own game, the reports players make, and Announce (a studio line).
320
+ - **Speech bubbles:** `net.on('say')` and the port kit's `createBubbles` / `paintBubbles`; Gem Rush, Ember Vale and
321
+ Gem Rush 3D draw them over the speaker.
322
+ - **Nothing is stored** but a report (the one message, 30 days): the room keeps 15 minutes in memory.
323
+ [chat/CHAT.md](chat/CHAT.md) is the short version and why Clef; [chat/OWNERS.md](chat/OWNERS.md) a plain note for
324
+ studio owners on chat and children's data; NETPLAY.md section 19 the wire.
325
+
326
+ ## Selling things: the shop
327
+
328
+ A studio sells items for its games (0.24.0) with **its own Stripe account**: the studio is the seller, money goes
329
+ straight from players to its Stripe, and homie.rocks never sees, holds or moves it (Homie takes no cut; no shared
330
+ currency; not a Connect platform). Real-money prices only, nothing random, the kids rules built in (no shop on a kids
331
+ server, spending off until a neutral age question says adult, nothing for under-13s, 13-17 through a parent's own
332
+ checkout, a monthly cap), and the television only shows a code to buy on a phone.
333
+
334
+ ```sh
335
+ npx homie-studio shop init --supporter # shop.json with a US$5 Supporter pack, and SELLING.md (the owner's plain words)
336
+ npx homie-studio shop check # the kit's rules; every build checks them too
337
+ npx homie-studio shop connect # a page on THIS computer: the owner pastes a restricted key and the webhook secret
338
+ npx homie-studio shop # open (test or live) or what is missing
339
+ npx homie-studio shop refund ord_… # an ASK: the owner taps once (or Refund in /_studio/office/shop)
340
+ ```
341
+
342
+ In a game: `createShop()` from `@homie-rocks/studio/shop` (`has`, `entitlements`, `on('change')`, `open`, `used`); a
343
+ supporter's badge rides on their seat (`peer.badge`). Stripe Checkout (hosted), Stripe Tax on, or Stripe Managed
344
+ Payments (Stripe as seller of record, 3.5% more) as one switch; a signed, idempotent webhook; refunds from the
345
+ office; a dispute never touches the account; referral statements signed per referrer (homie.rocks is one more
346
+ referrer). The guide: `shop/SHOP.md`.
347
+
289
348
  ## Setup status
290
349
 
291
350
  ```sh
@@ -100,6 +100,10 @@
100
100
  * tap in their own browser, from the link this prints)
101
101
  * homie-studio office revoke (every office key, play ticket and pending ask ends)
102
102
  *
103
+ * homie-studio chat [--game <id>] room chat (NETPLAY.md section 19): each game's rules, every live room's last
104
+ * minutes, players' reports, the review's day; chat rules <game> [--server <id>]
105
+ * --mode off|emoji|lines|text --who anyone|signed-in|members … | --reset;
106
+ * chat remove <game> <room> <line id>; chat budget <neurons>; chat words
103
107
  * homie-studio servers [--game <id>] every server of every game (worker/servers.mjs): its policy, door, the AI's
104
108
  * level, live rooms and AI, members, and builds that predate servers
105
109
  * homie-studio servers new <game> "<Name>" --policy open|humans-only|hybrid|beginner [--ai <n>] [--guides <n>]
@@ -117,6 +121,16 @@
117
121
  * time AI guides may talk are ASKED for, like office kick; --budget is
118
122
  * Workers AI neurons a day, or dollars a day for the owner's key)
119
123
  * homie-studio agents brain key [--remove] (the owner's AI key, typed into a page on this computer only)
124
+ * homie-studio shop is the studio's shop selling (its own Stripe), and if not, what is missing
125
+ * homie-studio shop init [--supporter] [--currency usd] [--price 500] [--managed] shop.json and SELLING.md
126
+ * homie-studio shop check shop.json against the kit's rules (real money, nothing random, the kids rules)
127
+ * homie-studio shop connect [--managed] [--live] a page on THIS computer where the owner pastes the studio's
128
+ * restricted Stripe key and webhook secret: straight to the Worker secrets,
129
+ * never a chat or a file (test keys only unless --live)
130
+ * homie-studio shop disconnect both secrets gone: the shop closes
131
+ * homie-studio shop orders the latest orders (never a card or an email)
132
+ * homie-studio shop refund <order> [--reason …] [--note "<why>"] ASKS the owner (a one-tap link)
133
+ * homie-studio shop statements [--period YYYY-MM] [--send] signed referral statements this studio owes
120
134
  * homie-studio agents sit <game> [--server <id>] [--pass hap_…] [--label Claude]
121
135
  * (an AI guide's seat from this terminal: then lines of `do <goal> {args}`,
122
136
  * `say <line> {args}`, `look`, `stand`)
@@ -219,8 +233,10 @@ import { STUDIO_VERSION } from '../lib/version.mjs';
219
233
  import { statsKey, statsLink, statsRevoke, statsShare, statsShow } from '../lib/stats.mjs';
220
234
  import { playersOwner, playersShow } from '../lib/players.mjs';
221
235
  import { officeAnnounce, officeClose, officeInvite, officeKey, officeKick, officeLaunch, officeLines, officeLink, officeMute, officeRevoke, officeShow } from '../lib/office.mjs';
236
+ import { chatBudget, chatLines, chatRemove, chatRulesSet, chatShow, chatWords } from '../lib/chat-cli.mjs';
222
237
  import { agentsBrain, agentsBrainKey, agentsPass, agentsPasses, agentsRevoke, serversClose, serversLevel, serversLines, serversList, serversMember, serversNew, serversSet } from '../lib/servers.mjs';
223
238
  import { AgentSeat } from '../lib/agent-seat.mjs';
239
+ import { shopCheck, shopConnect, shopDisconnect, shopInit, shopLines, shopOrders, shopRefund, shopStatements, shopStatus } from '../lib/shop.mjs';
224
240
  import { Feed, currentFeed, currentId, flushProgress, publicFeed, readFeed, recordChange, startProgress } from '../lib/progress.mjs';
225
241
  import { formatStatus, setupStatus } from '../lib/doctor.mjs';
226
242
  import { codexTarget, newCodex, writeCodexPage } from '../lib/codex.mjs';
@@ -233,7 +249,7 @@ import { formatHandoff, handoff } from '../lib/handoff.mjs';
233
249
 
234
250
  const argv = process.argv.slice(2);
235
251
  const flags = new Map();
236
- const BOOL_FLAGS = ['off', 'revoke', 'json', 'yes', 'detach', 'no-install', 'plan', 'stop', 'share', 'apply', 'diff', 'template', 'ci', 'fresh', 'install', 'remove', 'replace', 'artifact', 'open', 'reopen', 'kids', 'remote-ai', 'dry-run', 'verify', 'maps', 'profile', 'hands-on', 'automatic', 'unlock', 'confirm', 'no-library', 'no-validate', 'rigged'];
252
+ const BOOL_FLAGS = ['off', 'revoke', 'json', 'yes', 'detach', 'no-install', 'plan', 'stop', 'share', 'apply', 'diff', 'template', 'ci', 'fresh', 'install', 'remove', 'replace', 'artifact', 'open', 'reopen', 'kids', 'remote-ai', 'dry-run', 'verify', 'maps', 'profile', 'hands-on', 'automatic', 'unlock', 'confirm', 'no-library', 'no-validate', 'rigged', 'supporter', 'managed', 'live', 'send'];
237
253
  const positional = [];
238
254
  for (let i = 0; i < argv.length; i++) {
239
255
  const a = argv[i];
@@ -426,6 +442,30 @@ function print(result) {
426
442
  case 'servers':
427
443
  lines.push(...serversLines(result));
428
444
  break;
445
+ case 'chat':
446
+ lines.push(...chatLines(result));
447
+ break;
448
+ case 'chat words':
449
+ for (const [k, v] of Object.entries(result.groups)) lines.push(`${k}: ${v.join(', ')}`);
450
+ lines.push(result.note);
451
+ break;
452
+ case 'chat remove':
453
+ lines.push(result.message);
454
+ break;
455
+ case 'shop':
456
+ case 'shop orders':
457
+ case 'shop check':
458
+ case 'shop init':
459
+ case 'shop statements':
460
+ case 'shop connect':
461
+ case 'shop disconnect':
462
+ lines.push(...shopLines(result));
463
+ break;
464
+ case 'chat rules':
465
+ case 'chat budget':
466
+ case 'shop refund':
467
+ lines.push(result.asked ? `Asked: ${result.what}` : result.message, ...(result.asked ? [`Owner's one-tap link (until the ask ends, 15 min): ${result.link}`, result.use] : []));
468
+ break;
429
469
  case 'servers new':
430
470
  lines.push(result.message, ...(result.notes ?? []).map((n) => ` ${n}`));
431
471
  break;
@@ -590,6 +630,8 @@ async function main() {
590
630
  if (cmd === 'version' || flags.has('version')) return { ok: true, command: 'version', version: STUDIO_VERSION };
591
631
  if (cmd === 'new') return newStudio(positional[1], { name: flags.get('name'), homie: flags.get('homie'), slug: flags.get('slug'), install: !flags.has('no-install'), template: flags.has('template') });
592
632
  if (cmd === 'starters') return { ok: true, command: 'starters', starters: starters() };
633
+ // The floor's built-in words need no studio: anyone can read what a studio's chat always holds.
634
+ if (cmd === 'chat' && sub === 'words') return chatWords();
593
635
  if (cmd === 'demo') return demoGames();
594
636
  if (cmd === 'mcp') {
595
637
  // stdout carries MCP messages only from here on (lib/mcp.mjs); every word for a person goes to stderr.
@@ -706,6 +748,16 @@ async function main() {
706
748
  if (sub === 'close') return officeClose(root, positional[2], positional[3], { url, minutes: flags.get('minutes'), reopen: flags.has('reopen') });
707
749
  if (sub === 'revoke') return officeRevoke(root, { url });
708
750
  }
751
+ if (cmd === 'chat') {
752
+ // Room chat (0.23.0, NETPLAY.md section 19): rules, the room's last minutes, reports, the review's budget.
753
+ const url = flags.get('url');
754
+ if (sub === 'words') return chatWords();
755
+ if (!sub) return chatShow(root, { url, game: flags.get('game') });
756
+ if (sub === 'rules') return chatRulesSet(root, positional[2], flags, { url });
757
+ if (sub === 'remove') return chatRemove(root, positional[2], positional[3], positional[4], { url, all: flags.has('all') });
758
+ if (sub === 'budget') return chatBudget(root, positional[2], { url });
759
+ return { ok: false, command: 'chat', why: `unknown: chat ${sub} (rules, remove, budget, words)` };
760
+ }
709
761
  if (cmd === 'servers') {
710
762
  const url = flags.get('url');
711
763
  if (!sub || sub === 'list') return serversList(root, { url, game: flags.get('game') });
@@ -716,6 +768,18 @@ async function main() {
716
768
  if (sub === 'member') return serversMember(root, positional[2], positional[3], positional[4], { url, role: flags.get('role'), remove: flags.has('remove') });
717
769
  return { ok: false, command: 'servers', why: `unknown: servers ${sub} (list, new, set, close, level, member)` };
718
770
  }
771
+ if (cmd === 'shop') {
772
+ const url = flags.get('url');
773
+ if (!sub) return shopStatus(root, { url });
774
+ if (sub === 'init') return shopInit(root, { supporter: flags.has('supporter'), currency: flags.get('currency') ?? 'usd', price: flags.get('price') ?? 500, managed: flags.has('managed') });
775
+ if (sub === 'check') return shopCheck(root);
776
+ if (sub === 'connect') return shopConnect(root, { managed: flags.has('managed') ? true : null, live: flags.has('live'), log });
777
+ if (sub === 'disconnect') return shopDisconnect(root);
778
+ if (sub === 'orders') return shopOrders(root, { url });
779
+ if (sub === 'refund') return shopRefund(root, positional[2], { url, reason: flags.get('reason'), note: flags.get('note') });
780
+ if (sub === 'statements') return shopStatements(root, { url, period: flags.get('period'), send: flags.has('send') });
781
+ return { ok: false, command: 'shop', why: `unknown: shop ${sub} (init, check, connect, disconnect, orders, refund, statements)` };
782
+ }
719
783
  if (cmd === 'agents') {
720
784
  const url = flags.get('url');
721
785
  if (sub === 'pass') return agentsPass(root, positional[2], { url, label: flags.get('label'), server: flags.get('server'), hands: flags.get('hands'), role: flags.get('role'), days: flags.get('days') });
@@ -928,7 +992,9 @@ async function dev(root) {
928
992
  log(`Local site: http://127.0.0.1:${port}/ (each game: http://127.0.0.1:${port}/<id>/play — open it in two browsers)`);
929
993
  const ai = devConfig(root, flags.has('remote-ai'));
930
994
  if (ai.note) log(ai.note);
931
- const child = spawn(bin, ['dev', '--local', '--ip', '127.0.0.1', '--port', port, ...ai.args], { cwd: workerDir(root), env, stdio: 'inherit' });
995
+ // --remote-ai: Wrangler's --local turns every remote binding off ("not supported"), so a dev with the real Workers AI
996
+ // (the guides' brains, room chat's review) runs without it; everything else stays local all the same.
997
+ const child = spawn(bin, ['dev', ...(flags.has('remote-ai') ? [] : ['--local']), '--ip', '127.0.0.1', '--port', port, ...ai.args], { cwd: workerDir(root), env, stdio: 'inherit' });
932
998
  mkdirSync(dirname(devFile(root)), { recursive: true });
933
999
  writeFileSync(devFile(root), `${JSON.stringify({ pid: process.pid, child: child.pid, port: Number(port), at: new Date().toISOString() })}\n`);
934
1000
  log(`Stop it with: npx --no-install homie-studio dev --stop (this studio's dev server only)`);
package/chat/CHAT.md ADDED
@@ -0,0 +1,101 @@
1
+ # Room chat
2
+
3
+ Every studio game has room chat from `@homie-rocks/studio` 0.23.0: reactions that float up every
4
+ screen in the room (the players', the watchers', the big screen's), the game's quick lines, and
5
+ typed messages where the room's rules allow them. It feels like the Homie app's live rooms and
6
+ homie.rocks's room chat because it is the same thing: the same five reactions in the same order,
7
+ the same message shapes, the same float on a television.
8
+
9
+ The wire, the rules and the helper are in [NETPLAY.md section 19](../netplay/NETPLAY.md). This
10
+ page is the short version for a game's author, and why the review uses what it uses. For what a
11
+ studio's owner is responsible for, read [OWNERS.md](OWNERS.md).
12
+
13
+ ## What a game gets with no work
14
+
15
+ - **The Chat pill** on the play page, beside the room button: the room's last minutes, the
16
+ reactions, the quick lines, typing where allowed, Report on a line, and two switches ("Show my
17
+ messages over my character", "Show chat on this screen").
18
+ - **The float:** every reaction rises up every screen in the room.
19
+ - **The ticker:** a new line shows for a moment where the play page's status chip sits.
20
+ - **The big screen** (`/<game>/tv`) shows the room's lines in a corner; the **watch page** has a
21
+ Chat button; **homie.rocks** shows the room's chat on its card when the studio allows it.
22
+
23
+ ## What a game can add
24
+
25
+ **Its rules, in game.json** (the owner can change every one in the office):
26
+
27
+ ```json
28
+ "chat": {
29
+ "mode": "text",
30
+ "who": "signed-in",
31
+ "react": "anyone",
32
+ "slow": 2,
33
+ "max": 140,
34
+ "emoji": { "gem": "💎" },
35
+ "lines": { "gg": "Good game!", "gem": "Grab that gem!", "help": "Help me!", "wait": "Wait for me!" }
36
+ }
37
+ ```
38
+
39
+ | Field | | Default |
40
+ |---|---|---|
41
+ | `mode` | `off`, `emoji` (reactions only), `lines` (and quick lines), `text` (and typing) | `text` |
42
+ | `who` | who may type: `anyone`, `signed-in` (a player account with a passkey), `members` (of the room's server) | `signed-in` |
43
+ | `react` | who may send reactions and quick lines (the same three) | `anyone` |
44
+ | `slow`, `max` | seconds between one person's lines; a typed line's length | 2, 140 |
45
+ | `links`, `swears` | `block` or `allow` (a link is never clickable; slurs, sexual words, threats and contact details are always held) | `block` |
46
+ | `ai` | review typed messages with the studio's own Workers AI | `true` |
47
+ | `bubbles`, `watchers`, `hub` | over the speaker's character; watchers may send; homie.rocks may show it | `true` |
48
+ | `emoji`, `lines` | up to 3 more reactions; up to 12 quick lines (each passes the chat floor at build) | the five; 8 friendly lines |
49
+ | `block`, `allow` | the studio's own words to hold (`word*` inside words) or to let through | none |
50
+
51
+ `"chat": false` turns it off. A kids server, and every beginner server, keeps chat to emoji and
52
+ quick lines whatever the game says. Where new lines show for a moment: game.json
53
+ `"screen": { "chat": "bottom-right" }` (any corner, `top-center` or `bottom-center`), or `false`
54
+ for only the pill's count.
55
+
56
+ **Speech bubbles over characters**, with the port kit (the starters draw them):
57
+
58
+ ```ts
59
+ import { createBubbles, paintBubbles, BUBBLE_FONT } from '@homie-rocks/studio/port';
60
+ const bubbles = createBubbles({ measure: (t) => { ctx.font = BUBBLE_FONT; return ctx.measureText(t).width; } });
61
+ net.on('say', (s) => bubbles.say(s.seat, s.text, { id: s.id, kind: s.kind }));
62
+ net.on('unchat', (e) => e.ids.forEach((id) => bubbles.remove(id)));
63
+ // each frame, after the names: the anchor is just over each speaking body's name
64
+ paintBubbles(ctx, bubbles.place(speakers.map((s) => ({ key: s.seat, x: s.x, y: s.nameTop, self: s.mine }))));
65
+ ```
66
+
67
+ A 3D game draws them the same way on its HUD canvas (Gem Rush 3D does), or reads `place()`'s boxes
68
+ and draws its own. **The game's own chat UI** (a key that opens a wheel of quick lines, say):
69
+ `net.sayLine('gg')`, `net.react('fire')`, `net.say(text)`; `net.on('chat')` for a log of your own.
70
+
71
+ ## Moderation: what it uses, and why
72
+
73
+ The owner asked for "Cloudflare's new decisions API". That is **Clef**, Cloudflare's decision
74
+ models, launched on Workers AI on 2026-10-01 (Birthday Week): `@cf/cloudflare/clef-flash` (9B) and
75
+ `@cf/cloudflare/clef` (27B). They do not write text: you send a `state` (here, the message) and
76
+ typed questions, and they return a probability for every allowed answer. Cloudflare names "trust
77
+ and safety: score user submissions against your own policy rubric" as a use.
78
+
79
+ | | Clef-flash (the default) | Clef | Llama Guard 3 8B | AI Gateway Guardrails |
80
+ |---|---|---|---|---|
81
+ | What | a decision model: our own question, a probability per answer | the same, larger | Meta's fixed safety categories (S1–S14) | Llama Guard run on prompts and responses of model calls through a gateway |
82
+ | Price | $0.09 per M input tokens, no output charge: measured about 2.3 neurons a short message (the question is most of it) | $0.24 per M: about 6 neurons | $0.48 per M in, $0.03 out: up to about 10 neurons a message with its policy prompt | Llama Guard's price, plus the model call it guards |
83
+ | Speed | about 40 ms median, 120 ms p95 (Cloudflare's numbers) | about 210 ms median | about 500 ms (Cloudflare's guardrails notes) | a model call plus about 500 ms |
84
+ | Fits game chat | our categories: insults, hate, sexual, grooming, harm, spam | the same, more accurate | no insults or bullying category; most game-chat abuse reads as "safe" | not for arbitrary text: it guards model traffic |
85
+
86
+ Clef-flash is the default because it is the cheapest per message by about five times, the fastest
87
+ by far (a message waits for it before anyone sees it), and lets the studio ask exactly what game
88
+ chat needs. Honest caveats: it is one day old as this ships, Cloudflare publishes no moderation
89
+ accuracy numbers for it, and the message it reads is a player's, so a player can try to steer it
90
+ (less of a risk than with a model that writes, since it only scores fixed answers). That is why
91
+ **the word list always runs first and decides alone** whenever the review cannot (no binding, the
92
+ day's budget used, an error, a slow answer). `HOMIE_CHAT_MODEL` switches to Clef or Llama Guard.
93
+ Firewall for AI (now "AI Security for Apps") was ruled out: it reads HTTP requests at the zone, not
94
+ messages inside a Durable Object, and its detections are an Enterprise add-on.
95
+
96
+ **What it costs a studio.** Workers AI gives every account 10,000 neurons a day free, on the free
97
+ Workers plan too, with no payment method. The review's day is 2,000 neurons by default (about
98
+ 850 typed messages, measured), leaving the AI guides their 8,000; `homie-studio chat budget <n>` changes it. On the free plan, past the allocation
99
+ Workers AI refuses (the floor decides); on Workers Paid, past it is $0.011 per 1,000 neurons, which
100
+ is why the review stops at its own budget. `homie-studio deploy` binds Workers AI when a game lets
101
+ players type; `homie-studio dev --remote-ai` uses the real one (billed to the signed-in account).
package/chat/OWNERS.md ADDED
@@ -0,0 +1,78 @@
1
+ # Room chat: a note for studio owners
2
+
3
+ **This is not legal advice.** It says what Homie's tooling does and does not do, so you can decide
4
+ what your studio needs. Laws about online chat and children's data differ by country and change;
5
+ if your games are for children, or many children play them, talk to someone who advises on that
6
+ where you and your players are.
7
+
8
+ ## Your studio, your rooms, your responsibility
9
+
10
+ A Homie studio runs on **your own Cloudflare account**: your Worker, your Durable Objects, your D1
11
+ database, your Workers AI. Homie (and homie.rocks) does not run your rooms, does not see your
12
+ players' chat, and does not moderate it for you. You choose the rules, you hold the owner's
13
+ controls, and you are the one responsible for what happens in your rooms, as the operator of the
14
+ service. Homie gives you the tools to do that well; it cannot do it for you.
15
+
16
+ What that means in practice:
17
+
18
+ - **Choose rules that fit your players.** Each game sets its defaults in game.json, and you change
19
+ them per game and per server in the office (`/_studio/office`) or with `homie-studio chat rules`.
20
+ If your players are young, keep chat to emoji and quick lines, or put them on a kids server (a
21
+ beginner server with `kids` on): typing is then off whatever the game says.
22
+ - **Watch your rooms, and act.** The office shows every live room's last minutes. Remove a message,
23
+ mute or kick its sender (their messages come down with them), and read what players report.
24
+ - **Answer reports.** Players can report a message. You see it in the office and with
25
+ `homie-studio chat`; dismiss it once you have dealt with it.
26
+ - **Say who your game is for, and what chat it has,** on your game's page and in your own terms or
27
+ rules, if you have them.
28
+
29
+ ## What the tooling does
30
+
31
+ | | |
32
+ |---|---|
33
+ | **Who can say what** | By default: everyone can send emoji and the game's quick lines; only **signed-in players** (an account with a passkey on your studio) can type. You can open typing to guests, keep it to members of a server, keep a room to emoji or quick lines, or turn chat off. |
34
+ | **Before anyone sees a typed message** | 1) a built-in word list and patterns (slurs, sexual words, telling someone to hurt themselves, asking a player's age or where they live, asking for pictures, moving a player to another app, email addresses and phone numbers, links, swears), plus your own words; 2) a review on your own Workers AI (Cloudflare's Clef decision model) that holds insults, hate, sexual content, grooming, threats and spam. The list always runs; the review runs within a daily budget. Neither is perfect: both are English-first, and a determined person can get past any filter. |
35
+ | **Emoji and quick lines** | Fixed words you (the game) chose. They are never reviewed and reach every screen at once. |
36
+ | **Children** | A kids server keeps chat to emoji and quick lines, gives players handles instead of names, and never asks anyone's age. Homie never asks for a player's age, birthday or address anywhere. |
37
+ | **AI players** | An AI never types in room chat. |
38
+ | **Slow mode and limits** | One line every 2 s by default (you choose), and message length, repeats and floods are capped. |
39
+
40
+ ## What is stored, and for how long
41
+
42
+ | What | Where | How long |
43
+ |---|---|---|
44
+ | The chat itself (every message and reaction) | Only in the room's memory, on your Cloudflare account | The last 50 messages of the last 15 minutes, for someone who just opened the room. **Never written to storage.** An empty room forgets them a minute after the last person leaves. |
45
+ | Who said what | The same memory, for your office's Mute and Kick | As long as the message is in that memory. Never an IP address. |
46
+ | A **report** | Your D1 (`chat_reports`) | The one message reported (its words, its sender's name and account id if they had one, the room, when, the reason picked), for **30 days** or until you dismiss it. Never who reported it. |
47
+ | Your chat rules and your words | Your D1 (`chat_rules`) | Until you change them. |
48
+ | Counts (messages sent, held and why, reviews) | Your D1 (`stats_daily`) | Daily numbers, never a message and never a person. |
49
+ | Player accounts | Your D1 (the players tables, `saves/SAVES.md`) | A random id, a display name, a passkey's public key. No email unless a player adds one; players can download and delete everything of theirs at `/account/`. |
50
+
51
+ **The review** sends a typed message's words (and nothing about who wrote it) to Workers AI on your
52
+ own Cloudflare account. Cloudflare says it does not store or train on Clef's requests or responses.
53
+ **homie.rocks** stores nothing of your chat: when your rules allow it, a visitor's own browser opens
54
+ your room's socket to show its chat on your room's card there. Turn that off with `hub` off.
55
+
56
+ ## Children's data, in plain words
57
+
58
+ Laws in many places (for example COPPA in the United States, GDPR and the UK's Children's Code in
59
+ Europe and the UK) set rules for online services that collect personal information from children,
60
+ or that are directed at children. A typed message can contain personal information. What helps:
61
+
62
+ - **Keep chat to emoji and quick lines for games aimed at children** (or a kids server). Then no
63
+ child types anything, and there is nothing personal in your chat to keep or lose.
64
+ - **Typing for signed-in players only** (the default) means a person behind each typed line has an
65
+ account you can act on.
66
+ - **Little is stored**, and only briefly (see above). Reports are the exception, kept 30 days.
67
+ - **You can delete**: a report with Dismiss, a message with Remove, a player's whole account in
68
+ their own account page (or `players` in the office).
69
+
70
+ Homie does not know your players' ages and does not verify them. Whether your game is "directed at
71
+ children", and what that requires of you, is for you to decide with advice where you need it.
72
+
73
+ ## What the tooling cannot promise
74
+
75
+ No filter catches everything, and a review model can be wrong both ways. The word list is English;
76
+ other languages rely on the review. A studio past its daily review budget, or whose Worker has no
77
+ Workers AI binding yet, relies on the word list alone until the next day (the office says so). Your
78
+ attention, your rules and your owner's controls are the real safety net.
package/lib/build.mjs CHANGED
@@ -9,9 +9,10 @@
9
9
  * (games/<id>/assets/manifest.json; lib/asset-manifest.mjs servedAssets)
10
10
  * site/dist/games/<id>/agents.json the AI guides' vocabulary (games/<id>/agents.json, checked; NETPLAY.md
11
11
  * section 18): the only goals and lines an AI in its rooms has
12
- * site/dist/games.json { studio, games[], songs[], videos[], posts[], site } from studio.json,
12
+ * site/dist/games.json { studio, games[], songs[], videos[], posts[], site, shop } from studio.json,
13
13
  * game.json files, the music/ and videos/ manifests (media/MEDIA.md),
14
- * posts/*.md and the studio's site/ folder (site/SITE.md)
14
+ * posts/*.md, the studio's site/ folder (site/SITE.md) and shop.json (checked
15
+ * against the kit's rules: a shop that breaks them stops the build)
15
16
  * site/dist/games/<id>/_landing/... what the game's landing shows (hero footage, its cover, licence texts)
16
17
  * site/dist/_site/ posts.json (the posts' HTML), the studio's own pages (site/pages)
17
18
  * site/dist/_studio/codex/<id>/ each game's Game Codex (games/<id>/CODEX.md, lib/codex.mjs): served only to
@@ -45,7 +46,10 @@ import { SEAT_MAX } from '../worker/seats.mjs';
45
46
  import { STUDIO_VERSION } from './version.mjs';
46
47
  import { licenseOf, remixRow } from '../worker/license.mjs';
47
48
  import { SERVER_LIMITS, serverOf } from '../worker/servers.mjs';
49
+ import { chatProblems } from '../worker/chat.mjs';
48
50
  import { vocabularyOf } from '../worker/brain.mjs';
51
+ import { shopForBuild } from './shop.mjs';
52
+ import { audienceOf } from '../worker/shop-rules.mjs';
49
53
 
50
54
  const SOURCE_SKIP = new Set(['node_modules', 'dist', '.git', '.wrangler', '.port']);
51
55
  /** Never copied into a static game's served folder. */
@@ -299,6 +303,8 @@ export async function buildGameFiles(esbuild, root, g, out, { maps = false, sour
299
303
  export async function build(root, { only = null, log = () => {}, deploy = process.env.WORKERS_CI === '1', maps = false } = {}) {
300
304
  const esbuild = await studioEsbuild(root);
301
305
  const studio = readStudio(root);
306
+ // The shop first (shop/SHOP.md): a shop.json that breaks the kit's rules stops the build before anything is built.
307
+ const shop = shopForBuild(root, { log });
302
308
  const dist = join(root, 'site', 'dist');
303
309
  const games = listGames(root).filter((g) => !only || g.id === only);
304
310
  if (only && !games.length) throw new Error(`no game "${only}" in games/`);
@@ -312,6 +318,9 @@ export async function build(root, { only = null, log = () => {}, deploy = proces
312
318
  const { mode, warnings } = await buildGameFiles(esbuild, root, g, out, { maps, cache, log });
313
319
  // The guides' vocabulary (NETPLAY.md section 18): checked here, so a room never meets a line it cannot say.
314
320
  vocabFor(g, out);
321
+ // Room chat (NETPLAY.md section 19): game.json "chat", checked here (its quick lines pass the chat floor too).
322
+ const chatBad = chatProblems(g.chat);
323
+ if (chatBad.length) throw new Error(`games/${g.id}/game.json: ${chatBad.join('; ')}`);
315
324
  // The game's own source, for other studios to remix (game.json "share": { "source": false } keeps it private).
316
325
  if (g.share?.source !== false) writeFileSync(join(out, 'source.json'), `${JSON.stringify(sourceOf(g.dir, g.id, { studio: studio.name ?? null, game: g.name ?? g.id, license: g.license }))}\n`);
317
326
  // Its assets' licences, and the address and SHA-256 of each one a remix may carry (`game remix` fetches them).
@@ -356,6 +365,9 @@ export async function build(root, { only = null, log = () => {}, deploy = proces
356
365
  // game.json `"watch"` (NETPLAY.md section 16): watchers see any player's view (the default), only the whole room
357
366
  // ("overview": hidden hands or roles), or nothing (false: no watch door).
358
367
  ...(g.watch === 'overview' ? { watch: 'overview' } : g.watch === false || g.watch === 'off' ? { watch: 'off' } : {}),
368
+ // game.json `"chat"` (0.23.0, NETPLAY.md section 19): the room chat's defaults for this game (the owner's office
369
+ // overrides them), or false for none.
370
+ ...(g.chat === false ? { chat: false } : g.chat && typeof g.chat === 'object' ? { chat: g.chat } : {}),
359
371
  // Servers (0.16.0): the seeds game.json ships, the netplay revision its build speaks (the office warns when it
360
372
  // predates servers), and game.json "agents": { "vote": "game" | false } (the game draws its own vote card, or none).
361
373
  ...(seeds.length ? { servers: seeds } : {}),
@@ -395,6 +407,10 @@ export async function build(root, { only = null, log = () => {}, deploy = proces
395
407
  : studio.homie?.directory === false || studio.homie?.directory === null ? null : String(studio.homie?.directory ?? 'https://homie.rocks'),
396
408
  // A copy of the public template that nobody has named yet shows the name typed in Cloudflare's form.
397
409
  ...(studio.template === true ? { template: true } : {}),
410
+ // studio.json "audience": "kids" (a studio made for children: no shop at all) or "teens"; and "referrals": false
411
+ // (this studio takes no referral statements as a referrer).
412
+ ...(audienceOf(studio) !== 'general' ? { audience: audienceOf(studio) } : {}),
413
+ ...(studio.referrals === false ? { referrals: false } : {}),
398
414
  build: buildInfo(root),
399
415
  },
400
416
  games: rows,
@@ -403,6 +419,8 @@ export async function build(root, { only = null, log = () => {}, deploy = proces
403
419
  // Summaries only; each post's HTML is in _site/posts.json.
404
420
  posts: posts.map(({ html, record, ...p }) => p),
405
421
  site: { pages: site.pages, partials: site.partials, ...(site.css ? { css: site.css } : {}) },
422
+ // shop.json, checked (nothing secret is ever in it): the Worker sells from this and checks it again.
423
+ ...(shop ? { shop } : {}),
406
424
  };
407
425
  writeFileSync(join(dist, 'games.json'), `${JSON.stringify(catalogue, null, 2)}\n`);
408
426
  return {