@homie-rocks/studio 0.22.0 → 0.23.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.
package/CHANGELOG.md CHANGED
@@ -17,9 +17,38 @@ 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.23.0] - 2026-10-03
21
+
22
+ **Plugin 0.24.0** · [#33](https://github.com/homie-rocks/homie/pull/33)
23
+
24
+ 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.
25
+
26
+ ### Added
27
+
28
+ - 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).
29
+ - 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.
30
+ - 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".
31
+ - 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.
32
+ - 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.
33
+ - 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`.
34
+ - 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.
35
+ - `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).
36
+ - 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')`.
37
+
38
+ ### Changed
39
+
40
+ - `homie-studio deploy` binds Workers AI when a game lets its players type (the review), not only when an AI guide thinks with it.
41
+ - `homie-studio dev --remote-ai` works again: Wrangler's `--local` turned every remote binding off.
42
+
43
+ ### Upgrade notes
44
+
45
+ - `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.
46
+ - 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).
47
+ - 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`.
48
+
20
49
  ## [0.22.0] - 2026-10-02
21
50
 
22
- **Plugin 0.23.0** · [#32](https://github.com/homie-rocks/homie/pull/32)
51
+ **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
52
 
24
53
  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
54
 
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,42 @@ 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
+
289
326
  ## Setup status
290
327
 
291
328
  ```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>]
@@ -219,6 +223,7 @@ import { STUDIO_VERSION } from '../lib/version.mjs';
219
223
  import { statsKey, statsLink, statsRevoke, statsShare, statsShow } from '../lib/stats.mjs';
220
224
  import { playersOwner, playersShow } from '../lib/players.mjs';
221
225
  import { officeAnnounce, officeClose, officeInvite, officeKey, officeKick, officeLaunch, officeLines, officeLink, officeMute, officeRevoke, officeShow } from '../lib/office.mjs';
226
+ import { chatBudget, chatLines, chatRemove, chatRulesSet, chatShow, chatWords } from '../lib/chat-cli.mjs';
222
227
  import { agentsBrain, agentsBrainKey, agentsPass, agentsPasses, agentsRevoke, serversClose, serversLevel, serversLines, serversList, serversMember, serversNew, serversSet } from '../lib/servers.mjs';
223
228
  import { AgentSeat } from '../lib/agent-seat.mjs';
224
229
  import { Feed, currentFeed, currentId, flushProgress, publicFeed, readFeed, recordChange, startProgress } from '../lib/progress.mjs';
@@ -426,6 +431,20 @@ function print(result) {
426
431
  case 'servers':
427
432
  lines.push(...serversLines(result));
428
433
  break;
434
+ case 'chat':
435
+ lines.push(...chatLines(result));
436
+ break;
437
+ case 'chat words':
438
+ for (const [k, v] of Object.entries(result.groups)) lines.push(`${k}: ${v.join(', ')}`);
439
+ lines.push(result.note);
440
+ break;
441
+ case 'chat remove':
442
+ lines.push(result.message);
443
+ break;
444
+ case 'chat rules':
445
+ case 'chat budget':
446
+ 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] : []));
447
+ break;
429
448
  case 'servers new':
430
449
  lines.push(result.message, ...(result.notes ?? []).map((n) => ` ${n}`));
431
450
  break;
@@ -590,6 +609,8 @@ async function main() {
590
609
  if (cmd === 'version' || flags.has('version')) return { ok: true, command: 'version', version: STUDIO_VERSION };
591
610
  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
611
  if (cmd === 'starters') return { ok: true, command: 'starters', starters: starters() };
612
+ // The floor's built-in words need no studio: anyone can read what a studio's chat always holds.
613
+ if (cmd === 'chat' && sub === 'words') return chatWords();
593
614
  if (cmd === 'demo') return demoGames();
594
615
  if (cmd === 'mcp') {
595
616
  // stdout carries MCP messages only from here on (lib/mcp.mjs); every word for a person goes to stderr.
@@ -706,6 +727,16 @@ async function main() {
706
727
  if (sub === 'close') return officeClose(root, positional[2], positional[3], { url, minutes: flags.get('minutes'), reopen: flags.has('reopen') });
707
728
  if (sub === 'revoke') return officeRevoke(root, { url });
708
729
  }
730
+ if (cmd === 'chat') {
731
+ // Room chat (0.23.0, NETPLAY.md section 19): rules, the room's last minutes, reports, the review's budget.
732
+ const url = flags.get('url');
733
+ if (sub === 'words') return chatWords();
734
+ if (!sub) return chatShow(root, { url, game: flags.get('game') });
735
+ if (sub === 'rules') return chatRulesSet(root, positional[2], flags, { url });
736
+ if (sub === 'remove') return chatRemove(root, positional[2], positional[3], positional[4], { url, all: flags.has('all') });
737
+ if (sub === 'budget') return chatBudget(root, positional[2], { url });
738
+ return { ok: false, command: 'chat', why: `unknown: chat ${sub} (rules, remove, budget, words)` };
739
+ }
709
740
  if (cmd === 'servers') {
710
741
  const url = flags.get('url');
711
742
  if (!sub || sub === 'list') return serversList(root, { url, game: flags.get('game') });
@@ -928,7 +959,9 @@ async function dev(root) {
928
959
  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
960
  const ai = devConfig(root, flags.has('remote-ai'));
930
961
  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' });
962
+ // --remote-ai: Wrangler's --local turns every remote binding off ("not supported"), so a dev with the real Workers AI
963
+ // (the guides' brains, room chat's review) runs without it; everything else stays local all the same.
964
+ 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
965
  mkdirSync(dirname(devFile(root)), { recursive: true });
933
966
  writeFileSync(devFile(root), `${JSON.stringify({ pid: process.pid, child: child.pid, port: Number(port), at: new Date().toISOString() })}\n`);
934
967
  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
@@ -45,6 +45,7 @@ import { SEAT_MAX } from '../worker/seats.mjs';
45
45
  import { STUDIO_VERSION } from './version.mjs';
46
46
  import { licenseOf, remixRow } from '../worker/license.mjs';
47
47
  import { SERVER_LIMITS, serverOf } from '../worker/servers.mjs';
48
+ import { chatProblems } from '../worker/chat.mjs';
48
49
  import { vocabularyOf } from '../worker/brain.mjs';
49
50
 
50
51
  const SOURCE_SKIP = new Set(['node_modules', 'dist', '.git', '.wrangler', '.port']);
@@ -312,6 +313,9 @@ export async function build(root, { only = null, log = () => {}, deploy = proces
312
313
  const { mode, warnings } = await buildGameFiles(esbuild, root, g, out, { maps, cache, log });
313
314
  // The guides' vocabulary (NETPLAY.md section 18): checked here, so a room never meets a line it cannot say.
314
315
  vocabFor(g, out);
316
+ // Room chat (NETPLAY.md section 19): game.json "chat", checked here (its quick lines pass the chat floor too).
317
+ const chatBad = chatProblems(g.chat);
318
+ if (chatBad.length) throw new Error(`games/${g.id}/game.json: ${chatBad.join('; ')}`);
315
319
  // The game's own source, for other studios to remix (game.json "share": { "source": false } keeps it private).
316
320
  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
321
  // Its assets' licences, and the address and SHA-256 of each one a remix may carry (`game remix` fetches them).
@@ -356,6 +360,9 @@ export async function build(root, { only = null, log = () => {}, deploy = proces
356
360
  // game.json `"watch"` (NETPLAY.md section 16): watchers see any player's view (the default), only the whole room
357
361
  // ("overview": hidden hands or roles), or nothing (false: no watch door).
358
362
  ...(g.watch === 'overview' ? { watch: 'overview' } : g.watch === false || g.watch === 'off' ? { watch: 'off' } : {}),
363
+ // game.json `"chat"` (0.23.0, NETPLAY.md section 19): the room chat's defaults for this game (the owner's office
364
+ // overrides them), or false for none.
365
+ ...(g.chat === false ? { chat: false } : g.chat && typeof g.chat === 'object' ? { chat: g.chat } : {}),
359
366
  // Servers (0.16.0): the seeds game.json ships, the netplay revision its build speaks (the office warns when it
360
367
  // predates servers), and game.json "agents": { "vote": "game" | false } (the game draws its own vote card, or none).
361
368
  ...(seeds.length ? { servers: seeds } : {}),
@@ -0,0 +1,89 @@
1
+ /**
2
+ * `homie-studio chat` — a studio's room chat from the studio folder (worker/chat.mjs says what it is; NETPLAY.md
3
+ * section 19 the rules and the wire). The office key it uses is the same as `office`'s: minted with the studio's own
4
+ * Cloudflare login, ten minutes long, dropped as soon as the answer is in.
5
+ *
6
+ * homie-studio chat [--game <id>] each game's chat rules, every live room's last minutes, the
7
+ * reports, and the review's day (neurons used of its budget)
8
+ * homie-studio chat rules <game> [--server <id>] [--mode off|emoji|lines|text] [--who anyone|signed-in|members]
9
+ * [--react …] [--slow <s>] [--max <n>] [--links block|allow] [--swears block|allow]
10
+ * [--ai on|off] [--bubbles on|off] [--watchers on|off] [--hub on|off] [--block "a,b"] [--reset]
11
+ * homie-studio chat remove <game> <room> <line id>|--all
12
+ * homie-studio chat budget <neurons> the review's day for the whole studio (default 2,000)
13
+ * homie-studio chat words the built-in word list the floor holds (worker/chat-words.mjs)
14
+ *
15
+ * A change that opens chat up (a wider mode or audience, less slow mode, links or swears allowed, the review off) or
16
+ * a bigger budget is only ASKED for here: the answer is the owner's one-tap link, as `office kick` gives.
17
+ */
18
+ import { builtinWords } from '../worker/chat.mjs';
19
+ import { askedFor, withKey } from './office.mjs';
20
+
21
+ const GAME = /^[a-z0-9][a-z0-9-]{0,39}$/;
22
+ const onOff = (v) => (v === undefined ? undefined : v === true || v === 'on' || v === 'true' || v === 'yes');
23
+
24
+ /** The flags `chat rules` reads, as the office API's fields. */
25
+ export function chatFields(flags) {
26
+ const f = {};
27
+ const get = (k) => (flags.has(k) ? flags.get(k) : undefined);
28
+ for (const k of ['mode', 'who', 'react', 'links', 'swears']) if (get(k) !== undefined) f[k] = String(get(k));
29
+ for (const k of ['slow', 'max']) if (get(k) !== undefined) f[k] = Number(get(k));
30
+ for (const k of ['ai', 'bubbles', 'watchers', 'hub']) if (get(k) !== undefined) f[k] = onOff(get(k));
31
+ if (get('block') !== undefined) f.block = String(get('block')).split(',').map((w) => w.trim()).filter(Boolean);
32
+ if (get('allow') !== undefined) f.allow = String(get('allow')).split(',').map((w) => w.trim()).filter(Boolean);
33
+ return f;
34
+ }
35
+
36
+ export async function chatShow(root, { url, game } = {}) {
37
+ const r = await withKey(root, url, (call) => call(`/_studio/api/chat${game ? `?game=${encodeURIComponent(game)}` : ''}`));
38
+ if (!r.ok) return { ok: false, command: 'chat', why: r.why ?? r.message ?? 'the office did not answer' };
39
+ return { command: 'chat', ...r, ok: true };
40
+ }
41
+
42
+ export async function chatRulesSet(root, game, flags, { url } = {}) {
43
+ if (!GAME.test(String(game ?? ''))) return { ok: false, command: 'chat rules', why: 'usage: homie-studio chat rules <game> [--server <id>] --mode off|emoji|lines|text [--who …] [--slow <s>] … | --reset' };
44
+ const server = flags.get('server') ?? '';
45
+ const body = flags.has('reset') ? { game, server, reset: true } : { game, server, ...chatFields(flags) };
46
+ if (!body.reset && Object.keys(body).length <= 2) return { ok: false, command: 'chat rules', why: 'say what to change: --mode, --who, --react, --slow, --max, --links, --swears, --ai, --bubbles, --watchers, --hub, --block, --allow; or --reset' };
47
+ const r = await withKey(root, url, (call) => call('/_studio/api/chat/rules', body));
48
+ return askedFor(root, url, r, 'chat rules');
49
+ }
50
+
51
+ export async function chatRemove(root, game, room, id, { url, all = false } = {}) {
52
+ if (!GAME.test(String(game ?? '')) || !room || (!id && !all)) return { ok: false, command: 'chat remove', why: 'usage: homie-studio chat remove <game> <room> <line id> | --all (the ids are in `homie-studio chat`)' };
53
+ const r = await withKey(root, url, (call) => call('/_studio/api/chat/remove', { game, room, ...(all ? { all: true } : { id }) }));
54
+ return r.ok ? { ok: true, command: 'chat remove', removed: r.removed ?? 0, message: `Took ${r.removed ?? 0} message${r.removed === 1 ? '' : 's'} down in ${room}, on every screen.` } : { ok: false, command: 'chat remove', why: r.message ?? r.why };
55
+ }
56
+
57
+ export async function chatBudget(root, neurons, { url } = {}) {
58
+ const n = Math.floor(Number(neurons));
59
+ if (!(n >= 0 && n <= 1e7)) return { ok: false, command: 'chat budget', why: 'usage: homie-studio chat budget <neurons a day> (default 2000; Workers AI gives an account 10,000 a day free)' };
60
+ const r = await withKey(root, url, (call) => call('/_studio/api/chat/budget', { neurons: n }));
61
+ return askedFor(root, url, r, 'chat budget');
62
+ }
63
+
64
+ /** The built-in list, by group: what the floor always holds (swears only while a game holds swears). */
65
+ export function chatWords() {
66
+ const w = builtinWords();
67
+ return {
68
+ ok: true, command: 'chat words',
69
+ groups: {
70
+ slurs: [...w.slurs], slursInsideWords: w.slursAnywhere, sexual: [...w.sexual], sexualInsideWords: w.sexualAnywhere,
71
+ swears: [...w.swears], swearsInsideWords: w.swearsAnywhere, harm: w.harm, contact: w.contact,
72
+ },
73
+ note: 'Whole words unless "inside words". Shown as the floor reads them (lower case, letters only). A studio adds its own with chat rules --block, and lets one through with --allow.',
74
+ };
75
+ }
76
+
77
+ export function chatLines(r) {
78
+ const out = [];
79
+ const d = r.day;
80
+ if (d) out.push(`Review today: ${d.ai ? `${Math.round(d.used.neurons)} of ${d.budget.neurons} neurons, ${d.used.reviews} reviewed, ${d.used.held} held${d.used.errors ? `, ${d.used.errors} unanswered (the word list decided)` : ''}` : 'no Workers AI binding yet (npm run deploy binds it); the word list decides'}.`);
81
+ for (const g of r.games ?? []) {
82
+ const c = g.chat?.rules;
83
+ out.push(`${g.name} (${g.id}): ${c ? `${c.mode}${c.mode === 'text' ? `, typing ${c.who}` : ''}, emoji ${c.react}, slow ${c.slow} s${c.ai ? ', reviewed' : ''}${c.capped ? ` (capped: ${c.capped})` : ''}` : 'no rules read'} · from ${g.chat?.from ?? 'default'}`);
84
+ for (const sv of g.chat?.servers ?? []) if (sv.office) out.push(` server ${sv.id}: ${sv.rules.mode}, typing ${sv.rules.who}`);
85
+ for (const rm of g.rooms ?? []) for (const l of (rm.chat?.lines ?? []).slice(-8)) out.push(` ${rm.room} ${l.id} ${l.by === 'studio' ? 'Studio' : l.name}: ${l.kind === 'react' ? l.glyph : l.text}`);
86
+ for (const p of g.chat?.reports ?? []) out.push(` report ${p.id}: "${p.text}" by ${p.name} (${p.reasonText}), ${p.room}`);
87
+ }
88
+ return out;
89
+ }
@@ -218,7 +218,7 @@ export async function deploy(root, { log = () => {}, homie } = {}) {
218
218
  // Workers AI (0.17.0): bound only when a server's AI guides think with it.
219
219
  if (needsWorkersAi(root, w, cf.d1)) {
220
220
  writeFileSync(configPath(root), wranglerConfig({ worker: cf.worker, name: studio.name, d1: cf.d1, d1Id: db.uuid, r2, layout: layoutOf(root), ai: true }));
221
- step('Workers AI bound (AI): a server\'s AI guides think with it, within the day\'s budget (free allocation: 10,000 neurons a day)');
221
+ step('Workers AI bound (AI): a server\'s AI guides think with it and typed room chat is reviewed with it, each within its day\'s budget (free allocation: 10,000 neurons a day)');
222
222
  }
223
223
 
224
224
  const started = Date.now();
@@ -271,6 +271,9 @@ export function needsWorkersAi(root, w, db) {
271
271
  try {
272
272
  const cat = JSON.parse(readFileSync(join(root, 'site', 'dist', 'games.json'), 'utf8'));
273
273
  if ((cat.games ?? []).some((g) => (g.servers ?? []).some((s) => s.brain === 'workers-ai'))) return true;
274
+ // Room chat (0.23.0): a game whose players may type has its messages reviewed by the studio's own Workers AI
275
+ // (Cloudflare's Clef decision model, within a day's budget of the free allocation) unless its game.json says no.
276
+ if ((cat.games ?? []).some((g) => g.chat !== false && (g.chat?.mode ?? 'text') === 'text' && g.chat?.ai !== false)) return true;
274
277
  } catch { /* not built */ }
275
278
  const r = w(['d1', 'execute', db, '--remote', '--json', '--command', "SELECT COUNT(*) AS n FROM servers WHERE brain = 'workers-ai' AND state = 'open'"]);
276
279
  if (r.code !== 0) return false;
@@ -343,7 +346,7 @@ export async function ciDeploy(root, { log = () => {} } = {}) {
343
346
  try {
344
347
  const json = JSON.parse(readFileSync(configPath(root), 'utf8').replace(/^\s*\/\/.*$/gm, ''));
345
348
  if (!json.ai) { json.ai = { binding: 'AI' }; writeFileSync(configPath(root), `${JSON.stringify(json, null, 2)}\n`); }
346
- step('Workers AI bound (AI) for the AI guides');
349
+ step('Workers AI bound (AI): the AI guides and the review of typed room chat');
347
350
  } catch { step('could not add the Workers AI binding to wrangler.jsonc: the guides answer from the game\'s script'); }
348
351
  }
349
352
  const started = Date.now();
package/lib/scaffold.mjs CHANGED
@@ -28,6 +28,7 @@ import { STATS_MIGRATION, STATS_MIGRATION_FILE } from '../worker/stats.mjs';
28
28
  import { PLAYERS_MIGRATION, PLAYERS_MIGRATION_FILE } from '../worker/players.mjs';
29
29
  import { OFFICE_MIGRATION, OFFICE_MIGRATION_FILE } from '../worker/office-schema.mjs';
30
30
  import { SERVERS_MIGRATION, SERVERS_MIGRATION_FILE } from '../worker/servers.mjs';
31
+ import { CHAT_MIGRATION, CHAT_MIGRATION_FILE } from '../worker/chat-store.mjs';
31
32
  import { themeFile } from './site.mjs';
32
33
 
33
34
  export const COMPAT_DATE = '2026-06-01';
@@ -571,6 +572,7 @@ export { default, Table, Lobby } from '@homie-rocks/studio/worker';
571
572
  [`site/migrations/${PLAYERS_MIGRATION_FILE}`]: PLAYERS_MIGRATION,
572
573
  [`site/migrations/${OFFICE_MIGRATION_FILE}`]: OFFICE_MIGRATION,
573
574
  [`site/migrations/${SERVERS_MIGRATION_FILE}`]: SERVERS_MIGRATION,
575
+ [`site/migrations/${CHAT_MIGRATION_FILE}`]: CHAT_MIGRATION,
574
576
  'wrangler.jsonc': wranglerConfig({ worker, name, d1: studio.cloudflare.d1, r2: studio.cloudflare.r2, layout: 'root' }),
575
577
  '.claude/skills/.gitkeep': '',
576
578
  };
@@ -663,13 +665,22 @@ export function ensureServersMigration(root) {
663
665
  }
664
666
 
665
667
  /** Every migration the template owns that this studio lacks, added: the files written (deploy and dev say so). */
668
+ /** A studio made before 0.23.0 has no room chat tables: the owner's chat rules and players' reports. */
669
+ function ensureChatMigration(root) {
670
+ const file = join(root, 'site', 'migrations', CHAT_MIGRATION_FILE);
671
+ if (existsSync(file)) return null;
672
+ mkdirSync(dirname(file), { recursive: true });
673
+ writeFileSync(file, CHAT_MIGRATION);
674
+ return `site/migrations/${CHAT_MIGRATION_FILE}`;
675
+ }
676
+
666
677
  export function ensureMigrations(root) {
667
- return [ensureStatsMigration(root), ensurePlayersMigration(root), ensureOfficeMigration(root), ensureServersMigration(root)].filter(Boolean);
678
+ return [ensureStatsMigration(root), ensurePlayersMigration(root), ensureOfficeMigration(root), ensureServersMigration(root), ensureChatMigration(root)].filter(Boolean);
668
679
  }
669
680
 
670
681
  /** What a migration file the template added is for, in a few words (deploy and dev say it). */
671
682
  export function migrationWord(file) {
672
- return /players/.test(file) ? 'player accounts and cloud saves' : /servers/.test(file) ? 'servers and agent seats: room pools with their own rules, AI passes' : /office/.test(file) ? 'the back office: launch states, invites, the owner\'s controls' : 'the studio\'s own stats: counts, never tracks';
683
+ return /players/.test(file) ? 'player accounts and cloud saves' : /chat/.test(file) ? 'room chat: the owner\'s chat rules and players\' reports (never the chat itself)' : /servers/.test(file) ? 'servers and agent seats: room pools with their own rules, AI passes' : /office/.test(file) ? 'the back office: launch states, invites, the owner\'s controls' : 'the studio\'s own stats: counts, never tracks';
673
684
  }
674
685
 
675
686
  /**
@@ -962,6 +962,44 @@
962
962
  }
963
963
  }
964
964
  }
965
+ },
966
+ "0.23.0": {
967
+ "files": {
968
+ "CLAUDE.md": "ddf2b2a0376202f6",
969
+ "README.md": "684b3cca8001673c",
970
+ "HANDOFF.md": "ce7a8fd91c6bbee2",
971
+ "games/README.md": "5ad32fe80c61f117",
972
+ "music/README.md": "a3dc042dadd39df8",
973
+ "videos/README.md": "16babc3437d5545b",
974
+ "posts/README.md": "37841aae476bd540",
975
+ "site/README.md": "ca131cd0472464fb",
976
+ "site/src/worker.mjs": "ca3f1630f2420ad3"
977
+ },
978
+ "sections": {
979
+ "AGENTS.md": {
980
+ "(intro)": "214aa46eb3a895d5",
981
+ "## Layout": "8baf49e4291e7e88",
982
+ "## Commands (all through the pinned CLI in node_modules)": "d80ec56e4b4fc4c6",
983
+ "## The site": "ab922bb6bde23293",
984
+ "## Making games": "a8a893eeada33bf5",
985
+ "## The Game Codex and progress": "0738ef719cef3a4c",
986
+ "## Art direction and models": "124772c374de7390",
987
+ "## Running live games (the back office)": "4aa5f513a7cd2c3b",
988
+ "## Servers and AI seats": "0a332b07e22d3fce",
989
+ "## Continuing a build from the Claude app": "0d68081e924ca419",
990
+ "## Rules": "7e8458adc1b81ece",
991
+ "## Beta": "9d02638207a26e73"
992
+ }
993
+ },
994
+ "named": {
995
+ "Homie Arcade": {
996
+ "sections": {
997
+ "AGENTS.md": {
998
+ "## Commands (all through the pinned CLI in node_modules)": "b35df2c19cd4b534"
999
+ }
1000
+ }
1001
+ }
1002
+ }
965
1003
  }
966
1004
  }
967
1005
  }