@homie-rocks/studio 0.28.0 → 0.30.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 +47 -2
- package/README.md +32 -3
- package/bin/homie-studio.mjs +30 -4
- package/chat/CHAT.md +5 -1
- package/chat/LOUNGE.md +114 -0
- package/chat/OWNERS.md +6 -2
- package/lib/build.mjs +5 -0
- package/lib/client.mjs +19 -0
- package/lib/doctor.mjs +4 -4
- package/lib/handoff.mjs +10 -8
- package/lib/lounge-cli.mjs +70 -0
- package/lib/mcp-tools.mjs +6 -0
- package/lib/scaffold.mjs +20 -8
- package/lib/setup.mjs +9 -6
- package/lib/template-history.json +80 -0
- package/mcp/ui/setup.js +2 -0
- package/netplay/NETPLAY.md +11 -1
- package/package.json +1 -1
- package/site/SITE.md +13 -0
- package/worker/chat-store.mjs +2 -1
- package/worker/chat.mjs +13 -1
- package/worker/discover.mjs +5 -0
- package/worker/index.mjs +39 -4
- package/worker/lounge-page.mjs +623 -0
- package/worker/lounge-store.mjs +281 -0
- package/worker/lounge.mjs +547 -0
- package/worker/office-page.mjs +99 -0
- package/worker/office.mjs +19 -1
- package/worker/players.mjs +5 -0
- package/worker/room.mjs +116 -12
- package/worker/site.mjs +6 -1
- package/worker/version.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,7 @@ What changed in each release of Homie's open parts, newest first:
|
|
|
5
5
|
- **`@homie-rocks/studio`**: the `homie-studio` command, your studio's site Worker, its multiplayer rooms (the
|
|
6
6
|
netplay contract), the port kit and the starter games. Every version is on npm, and Homie for Claude Desktop
|
|
7
7
|
(`homie-studio-<version>.mcpb` on each GitHub release) is built from it.
|
|
8
|
-
- **The Homie plugin** for Claude Code and
|
|
8
|
+
- **The Homie plugin** for Claude Code, Codex and Grok: its skills. It ships from this repository's marketplace with its
|
|
9
9
|
own version number, given beside each studio version below.
|
|
10
10
|
|
|
11
11
|
Each version says what you can now do (**Added**), what works differently (**Changed**), what was broken and is
|
|
@@ -17,9 +17,54 @@ 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.30.0] - 2026-10-03
|
|
21
|
+
|
|
22
|
+
**Plugin 0.31.0** · [#45](https://github.com/homie-rocks/homie/pull/45)
|
|
23
|
+
|
|
24
|
+
Grok can set up a studio and run it with the same holds as Claude Code and Codex.
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- `homie-studio setup attach <hs_…> --client grok` and `homie-studio handoff <hb_…> --client grok`. A Grok Bot in the studio folder checks the repository in. The directory is told the chat is Grok. Claude and Codex pass `--client claude` or `--client codex`; with no `--client` the words stay "the chat".
|
|
29
|
+
- Homie's hooks for Grok Build (`plugins/homie/.grok-plugin/plugin.json`, `hooks/grok.json`, `hooks/grok.mjs`). They ask `hooks/lib/holds.mjs`, the module the Claude Code mod and the Codex hooks already ask, so the three hold the same calls with the same words. A held call waits for your own `proceed <code>`. Secrets are taken out of what Grok reads.
|
|
30
|
+
- A new studio's home band says **Connect this chat**, for Claude, Codex or Grok. Grok has no Cloudflare connector: approving Cloudflare in the browser is still your step.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- The plugin manifests (Claude Code, Codex, Grok and `plugin.json`) name plugin 0.31.0 and say the same thing. `plugins/homie/test/manifests.test.mjs` checks the Grok manifest with the others.
|
|
35
|
+
|
|
36
|
+
## [0.29.0] - 2026-10-03
|
|
37
|
+
|
|
38
|
+
**Plugin 0.30.0** · [#44](https://github.com/homie-rocks/homie/pull/44) · [release-2026-10-03-studio-0.29.0](https://github.com/homie-rocks/homie/releases/tag/release-2026-10-03-studio-0.29.0)
|
|
39
|
+
|
|
40
|
+
The Lounge: a community room on your own studio, with play nights, moderators, live rooms and "show what you made" cards, and chat history you can turn on.
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
|
|
44
|
+
- The Lounge. `"lounge": true` in studio.json gives your studio a community room at `/lounge/`, in your studio's own look: one column of talk on a phone with "What's on" a tap away, the talk beside a sidebar on a computer. It is room chat in a room of its own, on your own Cloudflare: reactions and quick lines for anyone, typing for players signed in with a passkey (Sign in and Make a passkey are on the page itself), and every typed line through the word list and your Workers AI review (Clef) before anyone sees it. Slow mode is 3 s to start with. `chat/LOUNGE.md` has the whole of it.
|
|
45
|
+
- Play nights. Set them in the office (a name, a time in your own zone, how long, a game) or with `homie-studio lounge night "<title>" --at <time>`. Everyone sees the time in their own zone, a countdown, "On now", the game's Play, and Add to calendar (a small `.ics` the browser makes).
|
|
46
|
+
- Live rooms in the Lounge, each with Watch and Join: your studio's own, and with a directory the busiest across it (`"featured": "studio"` keeps it to yours, `"none"` shows none).
|
|
47
|
+
- Show what you made. A signed-in person pastes a link to a game and it becomes a card with the game's own title, pitch, picture and a Play button. Only a Homie studio's game can be one (yours, or a site that answers the studio manifest), its words pass the word list and the review like a typed line, and each person posts three an hour. Typed links stay held.
|
|
48
|
+
- Moderators. Name a player account as a moderator from one of their lines in the office (or `homie-studio lounge mod <player id>`): from the Lounge page they take a line down, mute or kick its sender for up to an hour (their lines come down with them) and set slow mode, never on your lines or another moderator's. You have the same from the page, for up to a day, and everything else in the office's new Lounge section: its rules, play nights, moderators, last lines and reports.
|
|
49
|
+
- Chat history you can turn on (`history`, a number of days, 1 to 90). In the office, "Keep what is said" for the Lounge, or `homie-studio lounge history <days>`; for a game, `"chat": { "history": 7 }` or the office. What is kept, in your studio's own D1: typed lines, quick lines, cards and your own studio lines, the name shown, when, and for a signed-in sender their account id. Never a reaction, an address or a browser key. It is deleted after that many days; keeping fewer days (or none) deletes the rest at once. The room gives kept lines back when it opens ("Earlier messages" pages further), a report still finds an older line, and a mute or kick from a kept line holds its sender's account.
|
|
50
|
+
- Anyone takes their own message down: Delete on their own line in the Lounge (a signed-in person's kept lines too, a guest's while it is in the room's last minutes). Deleting an account at `/account/` deletes every line it kept, and the account's download has them.
|
|
51
|
+
- `homie-studio lounge` (rules, play nights, moderators, last lines, reports), `lounge night`, `lounge history`, `lounge rules`, `lounge mod` and `lounge remove`. From your AI's office key, keeping more days, opening chat up, a new moderator, a mute or a kick only ASK (your one tap); a play night, slow mode, taking a line down and keeping less happen at once.
|
|
52
|
+
- `/lounge/api/now`: the Lounge's public facts (its rules, play nights and live rooms) as JSON, and the studio's manifest names its Lounge. homie.rocks can show a Lounge live when its rules allow it (`hub`): a visitor's own browser opens the Lounge's socket, as for a room's card; they react there and type on your site. The sitemap and llms.txt list the Lounge's page; robots.txt keeps its socket and APIs out.
|
|
53
|
+
- "Get Homie updates" on the setup card (Claude, Claude Desktop and the Claude Code mod): a link to homie.rocks's own sign-up for a short email when Homie ships something. The person signs up there themselves (double opt-in); the card never asks for an address.
|
|
54
|
+
|
|
55
|
+
### Changed
|
|
56
|
+
|
|
57
|
+
- Room chat's rules have one more field, `history`. It is 0 for every game and for the Lounge until an owner turns it on, and always 0 on a kids server, a beginner server and a kids Lounge, so nothing changes for a studio that does nothing: a room still keeps the last 50 lines of the last 15 minutes in memory and nothing else.
|
|
58
|
+
- A line on the wire may carry `card`, `mod`, `kept` and, on a page's own copy, `mine`; a page that knows none of them shows the line's words. A watching page may send `unsay` for its own line. The room's facts count watching pages (`counts.shells`). The relay stays revision 8.
|
|
59
|
+
|
|
60
|
+
### Upgrade notes
|
|
61
|
+
|
|
62
|
+
- `homie-studio upgrade` adds migration `0009_studio_lounge.sql` (kept chat, play nights, moderators; all empty until you use them) and `npm run deploy` applies it. Nothing else changes until you add `"lounge": true` to studio.json and deploy. A game already called `lounge` keeps its page, and the build says the Lounge is off.
|
|
63
|
+
- Before you turn history on, read `chat/OWNERS.md`: it says what is kept and what that means for children's data. The Lounge's page tells people what is kept, in its "How it works".
|
|
64
|
+
|
|
20
65
|
## [0.28.0] - 2026-10-03
|
|
21
66
|
|
|
22
|
-
**Plugin 0.29.0** · [#43](https://github.com/homie-rocks/homie/pull/43)
|
|
67
|
+
**Plugin 0.29.0** · [#43](https://github.com/homie-rocks/homie/pull/43) · [release-2026-10-03-studio-0.28.0](https://github.com/homie-rocks/homie/releases/tag/release-2026-10-03-studio-0.28.0)
|
|
23
68
|
|
|
24
69
|
Tell Homie: when something confuses you or gets in your way, Claude can offer to send the people who make Homie a short note about it. You see the note word for word first, and nothing is sent without your yes.
|
|
25
70
|
|
package/README.md
CHANGED
|
@@ -27,6 +27,7 @@ npx homie-studio stats # the studio's own number
|
|
|
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
29
|
npx homie-studio chat # room chat: each game's rules, the rooms' last minutes, reports
|
|
30
|
+
npx homie-studio lounge # the studio's Lounge: play nights, moderators, kept chat
|
|
30
31
|
npx homie-studio upgrade # what a newer template adds to this studio (--apply to take it)
|
|
31
32
|
```
|
|
32
33
|
|
|
@@ -82,8 +83,8 @@ The button copies the public template (`template/` in this repository, exactly w
|
|
|
82
83
|
writes) into the person's GitHub, makes the Worker, the D1 database and the rooms on their Cloudflare account,
|
|
83
84
|
and connects Workers Builds. The site goes live with its own home page ("First game coming soon": no starter game
|
|
84
85
|
is put in a studio unless the person asks for one), and its Home has a
|
|
85
|
-
**Connect
|
|
86
|
-
In the
|
|
86
|
+
**Connect this chat** band: one tap links it to the chat that set it up (the Homie MCP tool `studio_setup`).
|
|
87
|
+
In the session that works in the repository, `homie-studio setup attach <hs_…> [--client claude|codex|grok]` then gives the studio
|
|
87
88
|
the name chosen in the chat and removes the band.
|
|
88
89
|
|
|
89
90
|
**The site claims itself in the directory.** The homie.rocks directory lists a studio only when its site serves
|
|
@@ -353,10 +354,38 @@ npx homie-studio chat words # the bu
|
|
|
353
354
|
lines taken down), the same in the owner's own game, the reports players make, and Announce (a studio line).
|
|
354
355
|
- **Speech bubbles:** `net.on('say')` and the port kit's `createBubbles` / `paintBubbles`; Gem Rush, Ember Vale and
|
|
355
356
|
Gem Rush 3D draw them over the speaker.
|
|
356
|
-
- **Nothing is stored** but a report (the one message, 30 days): the room keeps 15 minutes in memory.
|
|
357
|
+
- **Nothing is stored** but a report (the one message, 30 days): the room keeps 15 minutes in memory. An owner may
|
|
358
|
+
turn a room's `history` on (0.29.0, a number of days; off for every game, never on a kids server): then what was
|
|
359
|
+
said (never a reaction) is kept in the studio's own D1 for that long, and people take their own lines down.
|
|
357
360
|
[chat/CHAT.md](chat/CHAT.md) is the short version and why Clef; [chat/OWNERS.md](chat/OWNERS.md) a plain note for
|
|
358
361
|
studio owners on chat and children's data; NETPLAY.md section 19 the wire.
|
|
359
362
|
|
|
363
|
+
## The Lounge: a room for the studio's community
|
|
364
|
+
|
|
365
|
+
`"lounge": true` in studio.json gives the studio a community room at `/lounge/` (0.29.0): room chat in a lasting room
|
|
366
|
+
of its own, on the studio's own Cloudflare like everything else. Reactions and quick lines for anyone, typing for
|
|
367
|
+
players signed in with a passkey (signing in is on the page), every typed line through the word list and the Clef
|
|
368
|
+
review first.
|
|
369
|
+
|
|
370
|
+
```sh
|
|
371
|
+
npx homie-studio lounge # its rules, play nights, moderators, lines, reports
|
|
372
|
+
npx homie-studio lounge night "Night Rush night" --at 2026-10-09T19:00:00-07:00 --game night-rush
|
|
373
|
+
npx homie-studio lounge history 14 # keep what is said for 14 days (asked: the owner confirms)
|
|
374
|
+
npx homie-studio lounge mod <player id> # a moderator (asked)
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
- **Play nights** the owner sets in the office: everyone sees the time in their own zone, a countdown and Add to
|
|
378
|
+
calendar. **Live rooms** with Watch and Join: the studio's, and with a directory the busiest across it.
|
|
379
|
+
- **Show what you made:** a signed-in person pastes a link to a Homie studio's game and it becomes a card with the
|
|
380
|
+
game's own title, pitch and picture (read from that studio's manifest; its words pass the word list and the
|
|
381
|
+
review). Typed links stay held.
|
|
382
|
+
- **Keeping it kind:** anyone reports a line or takes their own down; moderators the owner names Remove, Mute and
|
|
383
|
+
Kick (an hour at most) and set slow mode from the page; the owner has all of it and the office. A kids Lounge
|
|
384
|
+
keeps to emoji and quick lines and keeps nothing.
|
|
385
|
+
- **History is off** until the owner turns it on (1 to 90 days), and the page says what is kept.
|
|
386
|
+
- **homie.rocks** can show a Lounge live when its rules allow (`hub`): the visitor's browser opens the Lounge's own
|
|
387
|
+
socket, as for a room's card; they react there and type here. [chat/LOUNGE.md](chat/LOUNGE.md) has the whole of it.
|
|
388
|
+
|
|
360
389
|
## Selling things: the shop
|
|
361
390
|
|
|
362
391
|
A studio sells items for its games (0.24.0; Stripe's own agent tools since 0.24.3) with **its own Stripe account**: the studio is the seller, money goes
|
package/bin/homie-studio.mjs
CHANGED
|
@@ -106,6 +106,11 @@
|
|
|
106
106
|
* minutes, players' reports, the review's day; chat rules <game> [--server <id>]
|
|
107
107
|
* --mode off|emoji|lines|text --who anyone|signed-in|members … | --reset;
|
|
108
108
|
* chat remove <game> <room> <line id>; chat budget <neurons>; chat words
|
|
109
|
+
* homie-studio lounge the studio's Lounge (studio.json "lounge", chat/LOUNGE.md): its rules, play
|
|
110
|
+
* nights, moderators, last lines and reports; lounge night "<title>" --at <time>
|
|
111
|
+
* [--minutes 120] [--game <id>]; lounge night remove <id>; lounge history <days>;
|
|
112
|
+
* lounge rules [--slow <s>] [--who …] | --reset; lounge mod <player> [--remove];
|
|
113
|
+
* lounge remove <line id> | --all
|
|
109
114
|
* homie-studio servers [--game <id>] every server of every game (worker/servers.mjs): its policy, door, the AI's
|
|
110
115
|
* level, live rooms and AI, members, and builds that predate servers
|
|
111
116
|
* homie-studio servers new <game> "<Name>" --policy open|humans-only|hybrid|beginner [--ai <n>] [--guides <n>]
|
|
@@ -168,10 +173,12 @@
|
|
|
168
173
|
* into it (stage, each check going green, a preview picture) and stop
|
|
169
174
|
* when asked. Without one, nothing changes.
|
|
170
175
|
*
|
|
171
|
-
* homie-studio handoff <hb_…>
|
|
176
|
+
* homie-studio handoff <hb_…> [--client claude|codex|grok]
|
|
177
|
+
* (a session started from the chat with one line, "Continue building
|
|
172
178
|
* <Studio>: build hb_…": fetch the person's brief, check in for a new studio, take
|
|
173
179
|
* the build so the chat's card follows it, and print the steps; HANDOFF.md)
|
|
174
|
-
* homie-studio setup attach <hs_…>
|
|
180
|
+
* homie-studio setup attach <hs_…> [--client claude|codex|grok]
|
|
181
|
+
* (this repository is the studio the chat's setup card is making: say so, once,
|
|
175
182
|
* learn its live address, and give a template copy its real name)
|
|
176
183
|
* homie-studio setup --via stripe-projects [--with elevenlabs] [--accept-tos] [--dry-run]
|
|
177
184
|
* (a prototype, 0.24.3: Cloudflare, and ElevenLabs with --with, made or linked
|
|
@@ -258,6 +265,7 @@ import { statsKey, statsLink, statsRevoke, statsShare, statsShow } from '../lib/
|
|
|
258
265
|
import { playersOwner, playersShow } from '../lib/players.mjs';
|
|
259
266
|
import { officeAnnounce, officeClose, officeInvite, officeKey, officeKick, officeLaunch, officeLines, officeLink, officeMute, officeRevoke, officeShow } from '../lib/office.mjs';
|
|
260
267
|
import { chatBudget, chatLines, chatRemove, chatRulesSet, chatShow, chatWords } from '../lib/chat-cli.mjs';
|
|
268
|
+
import { loungeLines, loungeMod, loungeNight, loungeRemove, loungeRulesSet, loungeShow } from '../lib/lounge-cli.mjs';
|
|
261
269
|
import { detectLocalAi, localAiVars } from '../lib/local-ai.mjs';
|
|
262
270
|
import { agentsBrain, agentsBrainKey, agentsPass, agentsPasses, agentsRevoke, agentsTry, serversClose, serversLevel, serversLines, serversList, serversMember, serversNew, serversSet } from '../lib/servers.mjs';
|
|
263
271
|
import { AgentSeat } from '../lib/agent-seat.mjs';
|
|
@@ -479,8 +487,13 @@ function print(result) {
|
|
|
479
487
|
lines.push(result.note);
|
|
480
488
|
break;
|
|
481
489
|
case 'chat remove':
|
|
490
|
+
case 'lounge night':
|
|
491
|
+
case 'lounge remove':
|
|
482
492
|
lines.push(result.message);
|
|
483
493
|
break;
|
|
494
|
+
case 'lounge':
|
|
495
|
+
lines.push(...loungeLines(result));
|
|
496
|
+
break;
|
|
484
497
|
case 'shop':
|
|
485
498
|
case 'shop orders':
|
|
486
499
|
case 'shop check':
|
|
@@ -498,6 +511,8 @@ function print(result) {
|
|
|
498
511
|
break;
|
|
499
512
|
case 'chat rules':
|
|
500
513
|
case 'chat budget':
|
|
514
|
+
case 'lounge rules':
|
|
515
|
+
case 'lounge mod':
|
|
501
516
|
case 'shop refund':
|
|
502
517
|
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] : []));
|
|
503
518
|
break;
|
|
@@ -698,14 +713,14 @@ async function main() {
|
|
|
698
713
|
if (cmd === 'statusline') return installStatusLine(root, { remove: flags.has('remove'), replace: flags.has('replace'), project: flags.get('project') ?? null });
|
|
699
714
|
if (cmd === 'codex') return codexCommand(root, sub);
|
|
700
715
|
if (cmd === 'progress') return progressCommand(root, sub);
|
|
701
|
-
if (cmd === 'setup' && sub === 'attach') return setupAttach(root, positional[2], { homie: flags.get('homie') });
|
|
716
|
+
if (cmd === 'setup' && sub === 'attach') return setupAttach(root, positional[2], { homie: flags.get('homie'), client: flags.get('client') });
|
|
702
717
|
if (cmd === 'setup' && flags.has('via')) {
|
|
703
718
|
if (flags.get('via') !== 'stripe-projects') return { ok: false, command: 'setup', why: '--via stripe-projects is the one other way this version knows (the default is npx wrangler login)' };
|
|
704
719
|
const extra = String(flags.get('with') ?? '').split(',').map((x) => x.trim()).filter(Boolean);
|
|
705
720
|
if (extra.some((x) => x !== 'elevenlabs')) return { ok: false, command: 'setup', why: '--with takes elevenlabs (Cloudflare is always set up)' };
|
|
706
721
|
return setupViaProjects(root, { withElevenlabs: extra.includes('elevenlabs'), acceptTos: flags.has('accept-tos'), dryRun: flags.has('dry-run'), log });
|
|
707
722
|
}
|
|
708
|
-
if (cmd === 'handoff') return handoff(root, sub, { homie: flags.get('homie') });
|
|
723
|
+
if (cmd === 'handoff') return handoff(root, sub, { homie: flags.get('homie'), client: flags.get('client') });
|
|
709
724
|
if (cmd === 'port' && sub === 'import') return importPort(root, positional[2], flags.get('id'), { name: flags.get('name'), mode: flags.get('mode') });
|
|
710
725
|
if (cmd === 'port' && sub === 'check') {
|
|
711
726
|
const game = positional[2] ?? listGames(root)[0]?.id;
|
|
@@ -791,6 +806,17 @@ async function main() {
|
|
|
791
806
|
if (sub === 'close') return officeClose(root, positional[2], positional[3], { url, minutes: flags.get('minutes'), reopen: flags.has('reopen') });
|
|
792
807
|
if (sub === 'revoke') return officeRevoke(root, { url });
|
|
793
808
|
}
|
|
809
|
+
if (cmd === 'lounge') {
|
|
810
|
+
// The Lounge (0.29.0, chat/LOUNGE.md): its rules and history, play nights, moderators, lines.
|
|
811
|
+
const url = flags.get('url');
|
|
812
|
+
if (!sub) return loungeShow(root, { url });
|
|
813
|
+
if (sub === 'night') return loungeNight(root, positional.slice(2), flags, { url });
|
|
814
|
+
if (sub === 'history') return loungeRulesSet(root, flags, { url, history: positional[2] });
|
|
815
|
+
if (sub === 'rules') return loungeRulesSet(root, flags, { url });
|
|
816
|
+
if (sub === 'mod') return loungeMod(root, positional[2], { url, remove: flags.has('remove') });
|
|
817
|
+
if (sub === 'remove') return loungeRemove(root, positional[2], { url, all: flags.has('all') });
|
|
818
|
+
return { ok: false, command: 'lounge', why: `unknown: lounge ${sub} (night, history, rules, mod, remove)` };
|
|
819
|
+
}
|
|
794
820
|
if (cmd === 'chat') {
|
|
795
821
|
// Room chat (0.23.0, NETPLAY.md section 19): rules, the room's last minutes, reports, the review's budget.
|
|
796
822
|
const url = flags.get('url');
|
package/chat/CHAT.md
CHANGED
|
@@ -48,11 +48,15 @@ studio's owner is responsible for, read [OWNERS.md](OWNERS.md).
|
|
|
48
48
|
| `links`, `swears` | `block` or `allow` (a link is never clickable; slurs, sexual words, threats and contact details are always held) | `block` |
|
|
49
49
|
| `ai` | review typed messages with the studio's own Workers AI | `true` |
|
|
50
50
|
| `bubbles`, `watchers`, `hub` | over the speaker's character; watchers may send; homie.rocks may show it | `true` |
|
|
51
|
+
| `history` | days to keep what is said (typed lines, quick lines, cards; never a reaction) in the studio's own D1, 0 to 90 (0.29.0; never on a kids server) | `0`: nothing past the room's few minutes |
|
|
51
52
|
| `emoji`, `lines` | up to 3 more reactions; up to 12 quick lines (each passes the chat floor at build) | the five; 8 friendly lines |
|
|
52
53
|
| `block`, `allow` | the studio's own words to hold (`word*` inside words) or to let through | none |
|
|
53
54
|
|
|
54
55
|
`"chat": false` turns it off. A kids server, and every beginner server, keeps chat to emoji and
|
|
55
|
-
quick lines whatever the game says.
|
|
56
|
+
quick lines whatever the game says, and keeps nothing.
|
|
57
|
+
|
|
58
|
+
**A room for the studio's community** is the Lounge (0.29.0): room chat at `/lounge/` with play nights, moderators,
|
|
59
|
+
live rooms, "show what you made" cards and history the owner can turn on. Read [LOUNGE.md](LOUNGE.md).
|
|
56
60
|
|
|
57
61
|
**Where it sits on the screen, in game.json `"screen"`**, so a game with a busy HUD keeps typed
|
|
58
62
|
chat without anything covering its clock, its title or its controls. The Chat pill sits in the
|
package/chat/LOUNGE.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# The Lounge
|
|
2
|
+
|
|
3
|
+
A studio's own community room, at `/lounge/` on the studio's site (`@homie-rocks/studio` 0.29.0): play nights,
|
|
4
|
+
demos, "show what you made", and talking with the people who play your games. It runs on your own Cloudflare
|
|
5
|
+
account like everything else in the studio. Nobody else hosts it, reads it or moderates it for you.
|
|
6
|
+
|
|
7
|
+
It is room chat ([CHAT.md](CHAT.md), [NETPLAY.md section 19](../netplay/NETPLAY.md)) in a room of its own: the same
|
|
8
|
+
reactions floating up every screen, quick lines, typing for signed-in players, the same word list and Clef review
|
|
9
|
+
before anyone sees a typed line, and the same owner's tools. On top of that it has what a lasting room needs:
|
|
10
|
+
history (only if you turn it on), moderators, play nights, live rooms to jump into, and cards for games people made.
|
|
11
|
+
|
|
12
|
+
## Turn it on
|
|
13
|
+
|
|
14
|
+
In `studio.json`:
|
|
15
|
+
|
|
16
|
+
```json
|
|
17
|
+
"lounge": true
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
or, to say more:
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
"lounge": {
|
|
24
|
+
"name": "The Lounge",
|
|
25
|
+
"blurb": "Play nights, demos and what everybody is making. Come and say hi.",
|
|
26
|
+
"featured": "directory",
|
|
27
|
+
"kids": false,
|
|
28
|
+
"chat": { "slow": 5 }
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
| Field | | Default |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| `name`, `tab` | the page's title, and the word in the site's top line | "The Lounge", "Lounge" |
|
|
35
|
+
| `blurb` | one line under the title | the line above |
|
|
36
|
+
| `featured` | live rooms in the sidebar: `directory` (yours, then the busiest across your directory), `studio` (yours only), `none` | `directory` |
|
|
37
|
+
| `kids` | emoji and quick lines only, nothing kept | `true` for a studio with `"audience": "kids"` |
|
|
38
|
+
| `chat` | the Lounge's chat rules before you change them in the office (the fields of game.json `chat`) | below |
|
|
39
|
+
|
|
40
|
+
Then `npm run build` and `npm run deploy` (it applies migration `0009_studio_lounge.sql`). A game already called
|
|
41
|
+
`lounge` keeps its page, and the build says the Lounge is off.
|
|
42
|
+
|
|
43
|
+
## What it does, with no work
|
|
44
|
+
|
|
45
|
+
- **The talk.** Reactions and quick lines for anyone; typing for players signed in with a passkey (no password, no
|
|
46
|
+
email). Every typed line passes the word list and your Workers AI review first. Slow mode is 3 s by default.
|
|
47
|
+
- **Sign in in place.** "Sign in" and "Make a passkey" are on the Lounge page itself; the account is the same one your
|
|
48
|
+
games and `/account/` use.
|
|
49
|
+
- **Show what you made.** A signed-in person pastes a link to a game. If it is a Homie studio's game (yours, or any
|
|
50
|
+
site answering `/.well-known/homie-studio.json`), it becomes a card with the game's own title, pitch and picture, and
|
|
51
|
+
a Play button. Its words pass the word list and the review like a typed line. Three cards an hour each. Plain links
|
|
52
|
+
are never allowed in typed lines, so a card is the only way a link gets in.
|
|
53
|
+
- **Play nights.** You set them in the office (or with `homie-studio lounge night`). Everyone sees the time in their
|
|
54
|
+
own zone, a countdown, "On now", the game's link, and Add to calendar (a small `.ics` the browser makes; nothing
|
|
55
|
+
else is contacted).
|
|
56
|
+
- **Live now.** Your live rooms, and with a directory the busiest rooms across it, each with Watch and Join.
|
|
57
|
+
- **On a phone** it is one column of talk with "What's on" a tap away; on a computer, the talk and a sidebar.
|
|
58
|
+
- **On homie.rocks** (when your rules let it, `hub`), a visitor's own browser opens the Lounge's socket and shows it
|
|
59
|
+
live, like a room's card there. Those visitors react and send quick lines; typing stays on your site, where they can
|
|
60
|
+
sign in. homie.rocks keeps none of it.
|
|
61
|
+
|
|
62
|
+
## Keeping it kind
|
|
63
|
+
|
|
64
|
+
| Who | Can |
|
|
65
|
+
|---|---|
|
|
66
|
+
| Anyone | react, send quick lines, report a line (the studio sees the line and the reason, never who reported it), take their own line down |
|
|
67
|
+
| Signed in | type, show what they made, take down their own lines, kept ones too |
|
|
68
|
+
| A moderator | take any line down, mute or kick its sender for up to an hour (their lines come down with them), set slow mode; never on your lines or another moderator's |
|
|
69
|
+
| You, the owner | all of that, holds of up to a day, every rule, history, play nights, moderators, from the Lounge itself or the office |
|
|
70
|
+
|
|
71
|
+
Make someone a moderator from one of their lines in the office ("Make moderator"), or with
|
|
72
|
+
`homie-studio lounge mod <player id>`. A moderator needs an account with a passkey.
|
|
73
|
+
|
|
74
|
+
**Kids.** A kids Lounge keeps chat to emoji and quick lines and keeps nothing, whatever else it says. Nobody in the
|
|
75
|
+
Lounge is ever asked their age, where they live or for photos; the page says so, and the review holds it.
|
|
76
|
+
|
|
77
|
+
## History: what is kept, and only if you say so
|
|
78
|
+
|
|
79
|
+
By default the Lounge keeps what a room keeps: the last 50 lines of the last few minutes, in memory, and nothing
|
|
80
|
+
else. You can choose to keep what is said for 1 to 90 days (the office, or `homie-studio lounge history <days>`).
|
|
81
|
+
Then, in your D1 (`chat_history`):
|
|
82
|
+
|
|
83
|
+
- typed lines, quick lines, cards and your own studio lines; **never a reaction, an address or a browser key**;
|
|
84
|
+
- the name shown, when, and for a signed-in sender their account id (so they can take their lines down, and deleting
|
|
85
|
+
their account deletes them);
|
|
86
|
+
- deleted after that many days. Keeping fewer days (or none) deletes the rest at once.
|
|
87
|
+
|
|
88
|
+
A person sees what is kept on the page ("What's said is kept for 14 days"), takes their own lines down any time,
|
|
89
|
+
downloads theirs with everything else at `/account/`, and deletes them with their account. Reports stay as before:
|
|
90
|
+
one line, 30 days.
|
|
91
|
+
|
|
92
|
+
History is a room chat rule (`history`, in days), so a game can keep its chat too, but it is **off for every game**
|
|
93
|
+
and stays off unless you turn it on in the office for that game. Turning it on is the owner's one tap when your AI
|
|
94
|
+
asks for it.
|
|
95
|
+
|
|
96
|
+
## From the studio folder
|
|
97
|
+
|
|
98
|
+
```sh
|
|
99
|
+
npx --no-install homie-studio lounge # rules, play nights, moderators, last lines, reports
|
|
100
|
+
npx --no-install homie-studio lounge night "Night Rush night" --at 2026-10-09T19:00:00-07:00 --minutes 120 --game night-rush
|
|
101
|
+
npx --no-install homie-studio lounge night remove pn_0123456789ab
|
|
102
|
+
npx --no-install homie-studio lounge history 14 # asked: the owner confirms with one tap
|
|
103
|
+
npx --no-install homie-studio lounge rules --slow 10
|
|
104
|
+
npx --no-install homie-studio lounge mod pl_… [--remove] # asked
|
|
105
|
+
npx --no-install homie-studio lounge remove <line id>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The Lounge's public facts are at `/lounge/api/now` (its rules, play nights and live rooms; homie.rocks reads it).
|
|
109
|
+
|
|
110
|
+
## What it costs
|
|
111
|
+
|
|
112
|
+
A line or a reaction is one message on the Lounge's room (Durable Objects, billed 20:1); the fan-out is free. Typed
|
|
113
|
+
lines and cards use the review's daily budget, shared with your rooms (2,000 neurons a day by default, inside
|
|
114
|
+
Workers AI's free 10,000). History is a few D1 rows a line.
|
package/chat/OWNERS.md
CHANGED
|
@@ -41,7 +41,9 @@ What that means in practice:
|
|
|
41
41
|
|
|
42
42
|
| What | Where | How long |
|
|
43
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
|
|
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** unless you turn history on (below). An empty room forgets them a minute after the last person leaves. |
|
|
45
|
+
| **Kept chat**, only where you turn `history` on (off for every game and for the Lounge until you choose; never on a kids server) | Your D1 (`chat_history`) | Typed lines, quick lines, cards and your own lines (never a reaction, an address or a browser key): the words, the name shown, when, and for a signed-in sender their account id. For the number of days you chose (1 to 90), then deleted; choosing fewer days deletes the rest at once. A person takes their own lines down, downloads them at `/account/`, and deleting their account deletes them. |
|
|
46
|
+
| The Lounge's play nights and moderators | Your D1 (`lounge_nights`, `lounge_mods`) | A night until a day after it ends; a moderator (their account id) until you remove them. |
|
|
45
47
|
| 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
48
|
| 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
49
|
| Your chat rules and your words | Your D1 (`chat_rules`) | Until you change them. |
|
|
@@ -63,7 +65,9 @@ or that are directed at children. A typed message can contain personal informati
|
|
|
63
65
|
child types anything, and there is nothing personal in your chat to keep or lose.
|
|
64
66
|
- **Typing for signed-in players only** (the default) means a person behind each typed line has an
|
|
65
67
|
account you can act on.
|
|
66
|
-
- **Little is stored**, and only briefly (see above). Reports are the exception, kept 30 days
|
|
68
|
+
- **Little is stored**, and only briefly (see above). Reports are the exception, kept 30 days, and kept chat when
|
|
69
|
+
you turn history on (never on a kids server or a kids Lounge). If you keep chat, say so where people talk (the
|
|
70
|
+
Lounge does, on its page) and keep it as short as is useful.
|
|
67
71
|
- **You can delete**: a report with Dismiss, a message with Remove, a player's whole account in
|
|
68
72
|
their own account page (or `players` in the office).
|
|
69
73
|
|
package/lib/build.mjs
CHANGED
|
@@ -53,6 +53,7 @@ import { screenChatProblems } from '../worker/chat-page.mjs';
|
|
|
53
53
|
import { vocabularyOf } from '../worker/brain.mjs';
|
|
54
54
|
import { shopForBuild } from './shop.mjs';
|
|
55
55
|
import { audienceOf } from '../worker/shop-rules.mjs';
|
|
56
|
+
import { loungeConfig, loungeProblems } from '../worker/lounge-store.mjs';
|
|
56
57
|
|
|
57
58
|
const SOURCE_SKIP = new Set(['node_modules', 'dist', '.git', '.wrangler', '.port']);
|
|
58
59
|
/** Never copied into a static game's served folder. */
|
|
@@ -456,6 +457,9 @@ export async function build(root, { only = null, log = () => {}, deploy = proces
|
|
|
456
457
|
writeFileSync(join(dist, '_site', 'posts.json'), `${JSON.stringify({ v: 1, posts })}\n`);
|
|
457
458
|
const s = studio.site && typeof studio.site === 'object' ? studio.site : {};
|
|
458
459
|
const studioExtras = schemaExtras(s.schema, { where: 'studio.json site', types: 'Organization', log });
|
|
460
|
+
// The Lounge (0.29.0): studio.json "lounge", checked; off when a game already has /lounge/.
|
|
461
|
+
for (const p of loungeProblems(studio, rows, chatProblems)) log(`warning: studio.json: ${p}`);
|
|
462
|
+
const lounge = loungeConfig(studio, rows, { audience: audienceOf(studio) });
|
|
459
463
|
const catalogue = {
|
|
460
464
|
studio: {
|
|
461
465
|
name: studio.name, slug: studio.slug, version: STUDIO_VERSION,
|
|
@@ -481,6 +485,7 @@ export async function build(root, { only = null, log = () => {}, deploy = proces
|
|
|
481
485
|
// (this studio takes no referral statements as a referrer).
|
|
482
486
|
...(audienceOf(studio) !== 'general' ? { audience: audienceOf(studio) } : {}),
|
|
483
487
|
...(studio.referrals === false ? { referrals: false } : {}),
|
|
488
|
+
...(lounge ? { lounge } : {}),
|
|
484
489
|
build: buildInfo(root),
|
|
485
490
|
},
|
|
486
491
|
games: rows,
|
package/lib/client.mjs
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which chat is building the studio. Named only when the session says so (`--client`, or HOMIE_CLIENT).
|
|
3
|
+
* Anything else is "the chat": never a guess that it is Claude. Grok has no Cloudflare connector and no
|
|
4
|
+
* Claude Code window; `--client grok` is how a Grok Bot checks a repository in.
|
|
5
|
+
*/
|
|
6
|
+
const KNOWN = {
|
|
7
|
+
claude: { id: 'claude', card: "the Claude app's card", connect: 'Connect this chat' },
|
|
8
|
+
codex: { id: 'codex', card: "Codex's card", connect: 'Connect this chat' },
|
|
9
|
+
grok: { id: 'grok', card: "Grok's card", connect: 'Connect this chat' },
|
|
10
|
+
chat: { id: 'chat', card: 'the setup card', connect: 'Connect this chat' },
|
|
11
|
+
};
|
|
12
|
+
const ALIAS = { 'claude-code': 'claude', 'claude-app': 'claude', 'grok-bot': 'grok', grokbot: 'grok' };
|
|
13
|
+
|
|
14
|
+
/** @param {string | null | undefined} input `--client`, or null to read HOMIE_CLIENT */
|
|
15
|
+
export function studioClient(input, env = process.env) {
|
|
16
|
+
const raw = String(input ?? env.HOMIE_CLIENT ?? '').trim().toLowerCase();
|
|
17
|
+
const id = ALIAS[raw] ?? raw;
|
|
18
|
+
return KNOWN[id] ?? KNOWN.chat;
|
|
19
|
+
}
|
package/lib/doctor.mjs
CHANGED
|
@@ -152,7 +152,7 @@ export async function setupStatus({
|
|
|
152
152
|
{
|
|
153
153
|
const fixConnector = {
|
|
154
154
|
who: 'person',
|
|
155
|
-
say: 'Turn the Homie connector on. Claude Code: run /plugin, install or enable "homie" (marketplace homie-rocks/homie), then /mcp shows homie connected. Codex: install the Homie plugin from the same marketplace. The Claude app
|
|
155
|
+
say: 'Turn the Homie connector on. Claude Code: run /plugin, install or enable "homie" (marketplace homie-rocks/homie), then /mcp shows homie connected. Codex: install the Homie plugin from the same marketplace. Grok Build: the same plugin (.grok-plugin/plugin.json). The Claude app, or Grok: add the connector https://homie.rocks/mcp; its setup card (studio_setup) makes the studio on your own Cloudflare. Grok has no Cloudflare connector: you still approve Cloudflare in the browser. A Grok Bot on this computer runs the checklist and checks in with setup attach <hs_…> --client grok.',
|
|
156
156
|
};
|
|
157
157
|
let state; let detail;
|
|
158
158
|
if (net.blocked) { state = 'act'; detail = `the network proxy of this machine refused ${new URL(directory).host}`; }
|
|
@@ -162,7 +162,7 @@ export async function setupStatus({
|
|
|
162
162
|
else { state = 'unknown'; detail = net.ok ? `${new URL(directory).host} answers; your AI knows whether its Homie tools (studio_scaffold) are here` : `this computer's request to the directory failed: ${net.why}`; }
|
|
163
163
|
rows.push({
|
|
164
164
|
id: 'connector', label: 'Homie connector', need: 'required', state, detail,
|
|
165
|
-
unlocks: 'making the studio with the right toolkit version, listing games in the homie.rocks directory, and the cards
|
|
165
|
+
unlocks: 'making the studio with the right toolkit version, listing games in the homie.rocks directory, and the setup cards',
|
|
166
166
|
fix: state === 'ok' ? null : net.blocked
|
|
167
167
|
? { who: 'person', say: `In claude.ai/code, open this environment's settings, set Network access to Custom, add ${new URL(directory).host} (keep the default package managers), and start a new session.` }
|
|
168
168
|
: said === 'no' || net.ok ? fixConnector : null,
|
|
@@ -174,13 +174,13 @@ export async function setupStatus({
|
|
|
174
174
|
const local = root ? readLocal(root) : {};
|
|
175
175
|
const created = studio?.cloudflare?.created ?? [];
|
|
176
176
|
const deployed = Boolean(local.deployedAt || created.some((c) => String(c).startsWith('worker:')));
|
|
177
|
-
// A studio the
|
|
177
|
+
// A studio the setup card made (`setup attach` wrote connectedAt): Cloudflare's Deploy button made
|
|
178
178
|
// it on the person's account and Workers Builds deploys every merge, so no sign-in is needed on this computer.
|
|
179
179
|
const attached = Boolean(local.connectedAt);
|
|
180
180
|
const signedIn = attached || who.signedIn === true ? 'ok' : who.signedIn === false ? 'act' : 'later';
|
|
181
181
|
const verified = deployed || attached ? 'ok' : local.needs === 'cloudflare-verify-email' ? 'act' : 'later';
|
|
182
182
|
const parts = attached ? [
|
|
183
|
-
{ label: 'connected', state: 'ok', detail: 'through the
|
|
183
|
+
{ label: 'connected', state: 'ok', detail: 'through the setup card (Deploy to Cloudflare); Workers Builds deploys every merge' },
|
|
184
184
|
{ label: 'email verified', state: 'ok', detail: 'the site is live' },
|
|
185
185
|
] : [
|
|
186
186
|
{ label: 'signed in', state: signedIn, detail: who.signedIn === true ? (who.how ?? `${who.accounts ?? 1} account${who.accounts === 1 || who.accounts === null || who.accounts === undefined ? '' : 's'}`) : who.signedIn === false ? 'not signed in' : root ? (who.why ?? 'run npm install in the studio, then this again') : 'checked once the studio exists (it brings its own Wrangler)' },
|
package/lib/handoff.mjs
CHANGED
|
@@ -15,6 +15,7 @@ import { request } from './net.mjs';
|
|
|
15
15
|
import { startProgress } from './progress.mjs';
|
|
16
16
|
import { spawnSync } from 'node:child_process';
|
|
17
17
|
import { studioRepo } from './repo.mjs';
|
|
18
|
+
import { studioClient } from './client.mjs';
|
|
18
19
|
|
|
19
20
|
/** The repository this checkout IS (its origin remote), even Homie's engine one: the session must be in the studio's. */
|
|
20
21
|
function originRepo(root) {
|
|
@@ -36,13 +37,13 @@ const line = (v, max) => String(v ?? '').replace(/[\x00-\x1f\x7f]+/g, ' ').repla
|
|
|
36
37
|
const lines = (v, max) => String(v ?? '').split(/\r?\n/).map((l) => line(l, max)).filter(Boolean).join('\n').slice(0, max);
|
|
37
38
|
|
|
38
39
|
/** The steps for a kind of build, in this toolkit's commands (the brief says what; these say how). */
|
|
39
|
-
export function stepsFor(h, { games = [] } = {}) {
|
|
40
|
+
export function stepsFor(h, { games = [], client = 'chat' } = {}) {
|
|
40
41
|
const id = ID.test(String(h.id ?? '')) ? h.id : null;
|
|
41
42
|
const has = id && games.includes(id);
|
|
42
43
|
const q = (s) => JSON.stringify(String(s));
|
|
43
44
|
const work = {
|
|
44
45
|
setup: [
|
|
45
|
-
|
|
46
|
+
`This studio was just set up from ${client === 'grok' ? 'Grok' : client === 'codex' ? 'Codex' : client === 'claude' ? 'the Claude app' : 'this chat'}. Go through the new-studio checklist with the person, one step at a time and never ahead (the plugin's studio-setup skill): it has no game yet, and its home page says "First game coming soon".`,
|
|
46
47
|
'See a working game: `npx --no-install homie-studio demo` names a live one on Homie Arcade; copy a starter in only if they ask.',
|
|
47
48
|
'One small change from one sentence of theirs (the studio\'s colours in site/theme.json, a tagline in studio.json, or a first post in posts/), then plan their game into its Game Codex (`npx --no-install homie-studio codex new <id> --name "<Name>"`) before any game is made.',
|
|
48
49
|
],
|
|
@@ -67,16 +68,17 @@ export function stepsFor(h, { games = [] } = {}) {
|
|
|
67
68
|
];
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
export async function handoff(root, build, { homie, fetchFn = globalThis.fetch, attach = startProgress, checkIn = setupAttach, repoOf = originRepo } = {}) {
|
|
71
|
+
export async function handoff(root, build, { homie, client, fetchFn = globalThis.fetch, attach = startProgress, checkIn = setupAttach, repoOf = originRepo } = {}) {
|
|
71
72
|
const studio = readStudio(root);
|
|
73
|
+
const who = studioClient(client);
|
|
72
74
|
// "Continue building <Studio>: setup hs_…": a studio being set up, before any build: check in, then the checklist.
|
|
73
75
|
if (SETUP.test(String(build ?? ''))) {
|
|
74
|
-
const r = await checkIn(root, build, { homie });
|
|
76
|
+
const r = await checkIn(root, build, { homie, client: who.id === 'chat' ? null : who.id });
|
|
75
77
|
if (!r.ok) return { ok: false, command: 'handoff', setup: build, why: r.why, ...(r.needs ? { needs: r.needs } : {}) };
|
|
76
78
|
const h = { kind: 'setup', what: 'game', title: 'first steps', studio: r.name ?? studio.name, brief: '', setup: build };
|
|
77
|
-
return { ok: true, command: 'handoff', build: null, ...h, setup: { ok: true, message: r.message, renamed: r.renamed ?? [] }, attached: null, attachWhy: 'no build was opened in the chat yet (the setup card\'s Start building opens one)', steps: stepsFor(h) };
|
|
79
|
+
return { ok: true, command: 'handoff', build: null, ...h, client: who.id, setup: { ok: true, message: r.message, renamed: r.renamed ?? [] }, attached: null, attachWhy: 'no build was opened in the chat yet (the setup card\'s Start building opens one)', steps: stepsFor(h, { client: who.id }) };
|
|
78
80
|
}
|
|
79
|
-
if (!BUILD.test(String(build ?? ''))) return { ok: false, command: 'handoff', why:
|
|
81
|
+
if (!BUILD.test(String(build ?? ''))) return { ok: false, command: 'handoff', why: `the id from the one line the chat gave: hb_ (a build) or hs_ (a setup) and 32 hex digits` };
|
|
80
82
|
const directory = String(homie || studio.homie?.directory || 'https://homie.rocks').replace(/\/+$/, '');
|
|
81
83
|
// lib/net.mjs: through the environment's proxy, and an honest reason when it fails (the directory's own words, or
|
|
82
84
|
// the connection's error; only a refusal by the proxy names the network setting).
|
|
@@ -98,7 +100,7 @@ export async function handoff(root, build, { homie, fetchFn = globalThis.fetch,
|
|
|
98
100
|
};
|
|
99
101
|
let setup = null;
|
|
100
102
|
if (h.setup) {
|
|
101
|
-
const r = await checkIn(root, h.setup, { homie: directory });
|
|
103
|
+
const r = await checkIn(root, h.setup, { homie: directory, client: who.id === 'chat' ? null : who.id });
|
|
102
104
|
setup = { ok: r.ok, message: r.ok ? r.message : r.why, renamed: r.renamed ?? [] };
|
|
103
105
|
}
|
|
104
106
|
const taken = await attach(root, { attach: build, what: h.what, id: h.id, directory });
|
|
@@ -108,7 +110,7 @@ export async function handoff(root, build, { homie, fetchFn = globalThis.fetch,
|
|
|
108
110
|
ok: true, command: 'handoff', build, ...h, setup,
|
|
109
111
|
attached: taken.ok ? { feed: taken.build, shared: taken.shared?.build ?? build } : null,
|
|
110
112
|
attachWhy: taken.ok ? null : taken.why,
|
|
111
|
-
steps: stepsFor(h, { games }),
|
|
113
|
+
steps: stepsFor(h, { games, client: who.id }),
|
|
112
114
|
};
|
|
113
115
|
}
|
|
114
116
|
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `homie-studio lounge` — the studio's Lounge from the studio folder (worker/lounge.mjs says what it is; chat/LOUNGE.md
|
|
3
|
+
* the whole of it). The office key it uses is `office`'s: minted with the studio's own Cloudflare login, ten minutes
|
|
4
|
+
* long, dropped as soon as the answer is in.
|
|
5
|
+
*
|
|
6
|
+
* homie-studio lounge its rules, play nights, moderators, last lines and reports
|
|
7
|
+
* homie-studio lounge night "<title>" --at <time> [--minutes 120] [--game <id>|<https link>] [--note "…"]
|
|
8
|
+
* a play night; --at is an ISO time with its zone, or UTC (…Z)
|
|
9
|
+
* homie-studio lounge night remove <id>
|
|
10
|
+
* homie-studio lounge history <days> keep what is said for that many days (0 keeps nothing)
|
|
11
|
+
* homie-studio lounge rules [--slow <s>] [--who anyone|signed-in] [--mode …] [--hub on|off] [--ai on|off] | --reset
|
|
12
|
+
* homie-studio lounge mod <player id> [--remove] a moderator: Remove, Mute, Kick (an hour at most), slow mode
|
|
13
|
+
* homie-studio lounge remove <line id>|--all
|
|
14
|
+
*
|
|
15
|
+
* Keeping more (a longer history), opening chat up, a new moderator, a mute or a kick are only ASKED for here: the answer
|
|
16
|
+
* is the owner's one-tap link, as `office kick` gives. A play night, slow mode, taking a message down and keeping less
|
|
17
|
+
* happen at once.
|
|
18
|
+
*/
|
|
19
|
+
import { askedFor, withKey } from './office.mjs';
|
|
20
|
+
import { chatFields } from './chat-cli.mjs';
|
|
21
|
+
|
|
22
|
+
export async function loungeShow(root, { url } = {}) {
|
|
23
|
+
const r = await withKey(root, url, (call) => call('/_studio/api/lounge'));
|
|
24
|
+
if (!r.ok) return { ok: false, command: 'lounge', why: r.why ?? r.message ?? 'the office did not answer' };
|
|
25
|
+
return { command: 'lounge', ...r, ok: true };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function loungeNight(root, args, flags, { url } = {}) {
|
|
29
|
+
if (args[0] === 'remove') {
|
|
30
|
+
if (!/^pn_[a-f0-9]{12}$/.test(String(args[1] ?? ''))) return { ok: false, command: 'lounge night', why: 'usage: homie-studio lounge night remove <id> (the ids are in `homie-studio lounge`)' };
|
|
31
|
+
const r = await withKey(root, url, (call) => call('/_studio/api/lounge/night', { remove: args[1] }));
|
|
32
|
+
return r.ok ? { ok: true, command: 'lounge night', message: 'Play night removed.' } : { ok: false, command: 'lounge night', why: r.message ?? r.why };
|
|
33
|
+
}
|
|
34
|
+
const title = args.join(' ').trim();
|
|
35
|
+
if (!title || !flags.get('at')) return { ok: false, command: 'lounge night', why: 'usage: homie-studio lounge night "<title>" --at 2026-10-09T19:00:00-07:00 [--minutes 120] [--game <id>] [--note "…"]' };
|
|
36
|
+
const body = { title, at: String(flags.get('at')), ...(flags.has('minutes') ? { minutes: Number(flags.get('minutes')) } : {}), ...(flags.has('game') ? { game: String(flags.get('game')) } : {}), ...(flags.has('note') ? { note: String(flags.get('note')) } : {}) };
|
|
37
|
+
const r = await withKey(root, url, (call) => call('/_studio/api/lounge/night', body));
|
|
38
|
+
return r.ok ? { ok: true, command: 'lounge night', id: r.id, message: `Play night added (${r.id}): it shows in the Lounge in everyone's own time.` } : { ok: false, command: 'lounge night', why: r.message ?? r.why };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function loungeRulesSet(root, flags, { url, history = undefined } = {}) {
|
|
42
|
+
const body = flags.has('reset') ? { reset: true } : { ...chatFields(flags), ...(history !== undefined ? { history: Math.floor(Number(history)) } : flags.has('history') ? { history: Math.floor(Number(flags.get('history'))) } : {}) };
|
|
43
|
+
if (!body.reset && !Object.keys(body).length) return { ok: false, command: 'lounge rules', why: 'say what to change: --history <days>, --slow, --who, --mode, --hub, --ai, --block, --allow; or --reset' };
|
|
44
|
+
const r = await withKey(root, url, (call) => call('/_studio/api/lounge/rules', body));
|
|
45
|
+
return askedFor(root, url, r, 'lounge rules');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function loungeMod(root, player, { url, remove = false } = {}) {
|
|
49
|
+
if (!/^[A-Za-z0-9_-]{1,64}$/.test(String(player ?? ''))) return { ok: false, command: 'lounge mod', why: 'usage: homie-studio lounge mod <player id> [--remove] (`homie-studio lounge` shows the ids of signed-in senders)' };
|
|
50
|
+
const r = await withKey(root, url, (call) => call('/_studio/api/lounge/mod', { player, ...(remove ? { remove: true } : {}) }));
|
|
51
|
+
return askedFor(root, url, r, 'lounge mod');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function loungeRemove(root, id, { url, all = false } = {}) {
|
|
55
|
+
if (!id && !all) return { ok: false, command: 'lounge remove', why: 'usage: homie-studio lounge remove <line id> | --all' };
|
|
56
|
+
const r = await withKey(root, url, (call) => call('/_studio/api/lounge/remove', all ? { all: true } : { id }));
|
|
57
|
+
return r.ok ? { ok: true, command: 'lounge remove', message: `Took ${r.removed ?? 0} message${r.removed === 1 ? '' : 's'} down, on every screen.` } : { ok: false, command: 'lounge remove', why: r.message ?? r.why };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function loungeLines(r) {
|
|
61
|
+
const L = r.lounge;
|
|
62
|
+
if (!L) return ['This studio has no Lounge: add "lounge": true to studio.json and deploy.'];
|
|
63
|
+
const c = L.rules ?? {};
|
|
64
|
+
const out = [`${L.name} (${L.page}): ${c.mode}${c.mode === 'text' ? `, typing ${c.who}` : ''}, emoji ${c.react}, slow ${c.slow} s, ${c.history ? `kept ${c.history} days` : 'nothing kept'}${c.hub ? ', shown on homie.rocks' : ''}${L.kids ? ', kids' : ''} · rules from ${L.from === 'office' ? 'the office' : 'the defaults'} · ${L.here} here now`];
|
|
65
|
+
for (const n of L.nights ?? []) out.push(` night ${n.id}: ${n.title}, ${n.at} for ${n.minutes} min${n.game?.name ? ` (${n.game.name})` : ''}`);
|
|
66
|
+
for (const m of L.mods ?? []) out.push(` moderator ${m.player}: ${m.name ?? '?'}`);
|
|
67
|
+
for (const l of (L.lines ?? []).filter((x) => x.kind !== 'react').slice(-12)) out.push(` ${l.id} ${l.by === 'studio' ? 'Studio' : l.name}${l.player ? ` [${l.player}]` : ''}: ${l.card ? `card: ${l.card.title} by ${l.card.studio}` : l.text}`);
|
|
68
|
+
for (const p of L.reports ?? []) out.push(` report ${p.id}: "${p.text}" by ${p.name} (${p.reasonText})`);
|
|
69
|
+
return out;
|
|
70
|
+
}
|