@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 +30 -1
- package/README.md +37 -0
- package/bin/homie-studio.mjs +34 -1
- package/chat/CHAT.md +101 -0
- package/chat/OWNERS.md +78 -0
- package/lib/build.mjs +7 -0
- package/lib/chat-cli.mjs +89 -0
- package/lib/cloudflare.mjs +5 -2
- package/lib/scaffold.mjs +13 -2
- package/lib/template-history.json +38 -0
- package/netplay/NETPLAY.md +191 -3
- package/netplay/netplay.ts +150 -3
- package/package.json +2 -1
- package/port/index.ts +3 -2
- package/port/view.ts +238 -0
- package/site/SITE.md +11 -1
- package/starters/ember-vale/src/main.ts +22 -2
- package/starters/gem-rush/src/main.ts +21 -1
- package/starters/gem-rush-3d/src/main.ts +22 -1
- package/worker/agents.mjs +3 -3
- package/worker/chat-page.mjs +556 -0
- package/worker/chat-store.mjs +161 -0
- package/worker/chat-words.mjs +46 -0
- package/worker/chat.mjs +419 -0
- package/worker/index.mjs +148 -5
- package/worker/office-page.mjs +115 -0
- package/worker/office.mjs +137 -6
- package/worker/pages.mjs +21 -6
- package/worker/room.mjs +288 -10
- package/worker/servers.mjs +5 -1
- package/worker/site.mjs +4 -1
- package/worker/stats.mjs +4 -1
- package/worker/version.mjs +1 -1
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
|
package/bin/homie-studio.mjs
CHANGED
|
@@ -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
|
-
|
|
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 } : {}),
|
package/lib/chat-cli.mjs
ADDED
|
@@ -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
|
+
}
|
package/lib/cloudflare.mjs
CHANGED
|
@@ -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
|
|
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)
|
|
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
|
}
|