@homie-rocks/studio 0.14.0 → 0.14.2

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/README.md CHANGED
@@ -100,8 +100,8 @@ site. A game made with Homie's arcade controls that knocks for a Homie box (`/__
100
100
  A studio pins one version, so nothing changes until it asks. To take a newer one:
101
101
 
102
102
  ```sh
103
- npx -y @homie-rocks/studio@0.14.0 upgrade # the plan; changes nothing
104
- npx -y @homie-rocks/studio@0.14.0 upgrade --apply # after the person agrees
103
+ npx -y @homie-rocks/studio@0.14.2 upgrade # the plan; changes nothing
104
+ npx -y @homie-rocks/studio@0.14.2 upgrade --apply # after the person agrees
105
105
  npm install && npm run build
106
106
  ```
107
107
 
@@ -204,7 +204,9 @@ npx homie-studio office key # a key for the Homie MC
204
204
 
205
205
  Going private or invite-only never cuts a round short: each live room finishes its current round with a notice to
206
206
  the players, then everyone the new state leaves out is sent out with a thank-you (the owner, and in a beta the
207
- invited, play on). Mute drops a player's `say…`, `chat…` and `emote…` events, so a game that sends its chat and
207
+ invited, play on). A known edge: a room that opens in the same moment as the change can be missed by it, because the
208
+ office reaches the rooms its Lobby already knows. Each room therefore reads its game's launch state once, on its
209
+ first heartbeat after it opens, and re-gates the same way; nobody new can join a game that is not public meanwhile. Mute drops a player's `say…`, `chat…` and `emote…` events, so a game that sends its chat and
208
210
  emotes under those kinds needs nothing more (`netplay/NETPLAY.md` section 15). Kicking a whole network address is in
209
211
  the studio's API (`address: true`) and deliberately not in the office. A Preview (a branch's own unlisted address, with
210
212
  no D1) enforces no launch state and has no office.
@@ -230,7 +232,7 @@ device; the owner's own passkey account (`homie-studio players owner`) counts as
230
232
  ## Setup status
231
233
 
232
234
  ```sh
233
- npx -y @homie-rocks/studio@0.14.0 setup status --connector yes # before a studio exists
235
+ npx -y @homie-rocks/studio@0.14.2 setup status --connector yes # before a studio exists
234
236
  npx homie-studio setup status # in a studio (also: homie-studio doctor)
235
237
  ```
236
238
 
package/lib/mcp-tools.mjs CHANGED
@@ -981,6 +981,9 @@ export function toolDefs(ctx, avail = {}) {
981
981
  },
982
982
  });
983
983
  }
984
+ // Every tool says whose it is: a Claude app may also have the Homie house app's own MCP server (its room and TV
985
+ // tools), a different server with different tools.
986
+ for (const t of tools) if (!t.description.startsWith('Homie Studio')) t.description = `Homie Studio: ${t.description}`;
984
987
  return tools;
985
988
  }
986
989
 
package/lib/mcp.mjs CHANGED
@@ -27,7 +27,7 @@ import { STUDIO_VERSION } from './version.mjs';
27
27
  export const PROTOCOLS = ['2025-11-25', '2025-06-18', '2025-03-26', '2024-11-05'];
28
28
  export const MCP_APP_MIME = 'text/html;profile=mcp-app';
29
29
 
30
- export const INSTRUCTIONS = `Homie Studio, on this computer: make game studios and their multiplayer web games, music and videos, run them here, and put them online on the studio's own Cloudflare. These tools do the work in this chat; the person never types a command and never needs a terminal. A studio is a folder in the studios folder; the file tools (file_list, file_read, file_edit, file_write, file_search) work inside it.
30
+ export const INSTRUCTIONS = `Homie Studio, on this computer: make game studios and their multiplayer web games, music and videos, run them here, and put them online on the studio's own Cloudflare. (It is not the Homie house app: that app's own server, with its room and TV tools, is a different one.) These tools do the work in this chat; the person never types a command and never needs a terminal. A studio is a folder in the studios folder; the file tools (file_list, file_read, file_edit, file_write, file_search) work inside it.
31
31
 
32
32
  A new studio follows one checklist, in order and never ahead. Show it in your first reply and again, ticked, as each step ends:
33
33
  0. Setup status (setup_status): what this computer and their accounts have; optional rows never block.
@@ -299,6 +299,60 @@
299
299
  "## Beta": "9d02638207a26e73"
300
300
  }
301
301
  }
302
+ },
303
+ "0.14.1": {
304
+ "files": {
305
+ "CLAUDE.md": "ddf2b2a0376202f6",
306
+ "README.md": "684b3cca8001673c",
307
+ "HANDOFF.md": "ce7a8fd91c6bbee2",
308
+ "games/README.md": "5ad32fe80c61f117",
309
+ "music/README.md": "b8a828de9311bd28",
310
+ "videos/README.md": "70614388cbb203c7",
311
+ "posts/README.md": "37841aae476bd540",
312
+ "site/README.md": "ca131cd0472464fb",
313
+ "site/src/worker.mjs": "ca3f1630f2420ad3"
314
+ },
315
+ "sections": {
316
+ "AGENTS.md": {
317
+ "(intro)": "214aa46eb3a895d5",
318
+ "## Layout": "f14d641159480e6c",
319
+ "## Commands (all through the pinned CLI in node_modules)": "e579cd6ce3ce342a",
320
+ "## The site": "7c1618431bae6324",
321
+ "## Making games": "63069b31c87a2aa2",
322
+ "## The Game Codex and progress": "0738ef719cef3a4c",
323
+ "## Running live games (the back office)": "f0566f07cff04113",
324
+ "## Continuing a build from the Claude app": "0d68081e924ca419",
325
+ "## Rules": "969318425c99f5a5",
326
+ "## Beta": "9d02638207a26e73"
327
+ }
328
+ }
329
+ },
330
+ "0.14.2": {
331
+ "files": {
332
+ "CLAUDE.md": "ddf2b2a0376202f6",
333
+ "README.md": "684b3cca8001673c",
334
+ "HANDOFF.md": "ce7a8fd91c6bbee2",
335
+ "games/README.md": "5ad32fe80c61f117",
336
+ "music/README.md": "b8a828de9311bd28",
337
+ "videos/README.md": "70614388cbb203c7",
338
+ "posts/README.md": "37841aae476bd540",
339
+ "site/README.md": "ca131cd0472464fb",
340
+ "site/src/worker.mjs": "ca3f1630f2420ad3"
341
+ },
342
+ "sections": {
343
+ "AGENTS.md": {
344
+ "(intro)": "214aa46eb3a895d5",
345
+ "## Layout": "f14d641159480e6c",
346
+ "## Commands (all through the pinned CLI in node_modules)": "e579cd6ce3ce342a",
347
+ "## The site": "7c1618431bae6324",
348
+ "## Making games": "63069b31c87a2aa2",
349
+ "## The Game Codex and progress": "0738ef719cef3a4c",
350
+ "## Running live games (the back office)": "f0566f07cff04113",
351
+ "## Continuing a build from the Claude app": "0d68081e924ca419",
352
+ "## Rules": "969318425c99f5a5",
353
+ "## Beta": "9d02638207a26e73"
354
+ }
355
+ }
302
356
  }
303
357
  }
304
358
  }
@@ -660,7 +660,7 @@ tools). The room's relay enforces five controls:
660
660
  | `announce` | `text` (one line, at most 280 characters; empty takes it down), `seconds` (5–3600, default 30) | Everyone and every watching shell gets `announce { id, text, at, until, from: 'studio' }`; a joiner gets it in `welcome.announce` until it ends. |
661
661
  | `close` | `minutes`, or `seconds` (10 s up to a day), `message?`, or `reopen: true` | Every socket gets `error room-closed { until, message }` and is closed; the room forgets its play as an empty room does (section 4); watchers get `closed`. Until `until` every `hello` is refused with `room-closed`, and the Lobby sends nobody there. `reopen` opens it at once. |
662
662
  | `seats` | `max` | The room's seats while it runs (the owner's "players per room"). Players already seated above it keep their seat; nobody new is seated there. Never above the room's own cap (the game's manifest). |
663
- | `regate` | `allow` (holder kinds: `o` the owner, `i` an invite, `p` a player account), `notice?`, `message?`; no `allow` calls a waiting one off | A game's launch state narrowed (private, or an invite-only beta). The current round finishes first: the `notice` goes up as an announcement, and 5 s after the host's `round` with `phase: 'over'` (at the latest 20 s after its `endsAt`, or 30 s when no round is running; never later than 15 minutes) every socket whose ticket names no allowed holder gets `error room-closed { message }` (no `until`: they come back through the game's door if they have access) and is closed; its seat is freed and the others play on. A room with nobody to send out does nothing. |
663
+ | `regate` | `allow` (holder kinds: `o` the owner, `i` an invite, `p` a player account), `notice?`, `message?`; no `allow` calls a waiting one off | A game's launch state narrowed (private, or an invite-only beta). The current round finishes first: the `notice` goes up as an announcement, and 5 s after the host's `round` with `phase: 'over'` (at the latest 20 s after its `endsAt`, or 30 s when no round is running; never later than 15 minutes) every socket whose ticket names no allowed holder gets `error room-closed { message }` (no `until`: they come back through the game's door if they have access) and is closed; its seat is freed and the others play on. A room with nobody to send out does nothing. The studio's Worker sends it to every live room the game's Lobby knows; a room that opens in the same moment reads its game's launch state once on its first heartbeat and re-gates itself the same way. |
664
664
 
665
665
  **Signed by the studio, checked by the room.** A control reaches the relay only from the
666
666
  studio's own Worker, as
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homie-rocks/studio",
3
- "version": "0.14.0",
3
+ "version": "0.14.2",
4
4
  "description": "A studio for games, music and video on its own Cloudflare: scaffold, multiplayer starters on the netplay contract, a port toolkit that makes existing single-player web games multiplayer, the site Worker with public rooms (Table/Lobby Durable Objects and D1, all on the free Workers plan, no payment method), a site with the hub's sections (Home, Games, Music, Videos, Rooms, Posts with feeds) in the studio's own look, an epic landing page for every game, optional R2 storage for large media, deploy, and the homie.rocks directory.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
package/worker/index.mjs CHANGED
@@ -66,7 +66,7 @@ import { count, countVisit, counter, isQa, onlyOf, ownerAllowed, playedByGame, p
66
66
  import { ownerRoutes } from './stats-page.mjs';
67
67
  import { playerRoutes, players as playerAccounts } from './players.mjs';
68
68
  import {
69
- accessOf, accountSub, gatePage, holders, isOwner, joinHolders, launchOf, officeRoutes, publicCatalogue, redeemInvite, remixOf, seatsFor, settingsOf, ticketAllows, ticketFor,
69
+ accessOf, accountSub, gatePage, holders, isOwner, joinHolders, launchOf, regateArgs, officeRoutes, publicCatalogue, redeemInvite, remixOf, seatsFor, settingsOf, ticketAllows, ticketFor,
70
70
  ticketSub, usePlayers, verifyControl,
71
71
  } from './office.mjs';
72
72
 
@@ -815,6 +815,9 @@ export class Table {
815
815
  const room = this.room;
816
816
  if (!room) return;
817
817
  room.tick();
818
+ // A room that just opened reads its game's launch state once: a change made in the moment it opened (before the
819
+ // Lobby knew of it) still reaches it, after the current round as always.
820
+ if (room.openedAt && this.launchReadFor !== room.openedAt) { this.launchReadFor = room.openedAt; this.ctx.waitUntil(this.rereadLaunch().catch(() => {})); }
818
821
  // A launch change the room applied by itself (after its round): its stored state says so too.
819
822
  if (room.officeDirty) { room.officeDirty = false; this.ctx.storage.put('office', room.officeSaved()).catch(() => {}); }
820
823
  n += 1;
@@ -824,6 +827,22 @@ export class Table {
824
827
  }, 250);
825
828
  }
826
829
 
830
+ /**
831
+ * The game's launch state, read once when the room opens (one D1 read): a private or invite-only game whose room
832
+ * holds anyone the state leaves out re-gates after the current round. Never in a Preview (no launch states there).
833
+ */
834
+ async rereadLaunch() {
835
+ const room = this.room;
836
+ if (!room || !this.game || !this.env.DB || this.env.HOMIE_PREVIEW === '1' || room.regate) return null;
837
+ let launch = null;
838
+ try { launch = (await this.env.DB.prepare('SELECT launch FROM office_games WHERE game = ?1').bind(this.game).first())?.launch ?? null; } catch { return null; }
839
+ const args = regateArgs(launch);
840
+ if (!args.allow) return null;
841
+ const res = room.control('regate', args);
842
+ if (res.ok && res.leaving) await this.ctx.storage.put('office', room.officeSaved()).catch(() => {});
843
+ return res;
844
+ }
845
+
827
846
  /** Tell the Lobby this room closed (until when; 0 when it opened again), so it sends nobody here meanwhile. */
828
847
  tellLobby(extra) {
829
848
  if (!this.game || !this.code) return;
package/worker/office.mjs CHANGED
@@ -57,6 +57,21 @@ const TICKET_TTL_MS = 12 * 3600_000;
57
57
  /** Who may stay in a live room after a launch change, by the kinds of holder a ticket names (o, i-…, p-…). */
58
58
  const ALLOW = Object.freeze({ private: ['o'], invite: ['o', 'i'], public: null });
59
59
 
60
+ /**
61
+ * The room control for a launch state (NETPLAY.md section 15 `regate`): who may stay, the notice everyone sees while
62
+ * the round finishes, and the words for whoever then leaves. Public calls a waiting change off.
63
+ */
64
+ export function regateArgs(launch, name = 'This game') {
65
+ if (!ALLOW[launch]) return {};
66
+ return {
67
+ allow: ALLOW[launch],
68
+ ...{
69
+ private: { notice: `${name} goes private after this round. Thanks for playing!`, message: `${name} is private now. Thanks for playing!` },
70
+ invite: { notice: `${name} becomes an invite-only beta after this round. Invited players keep playing.`, message: `${name} is an invite-only beta now. Have an invite? Enter it on the game's page. Thanks for playing!` },
71
+ }[launch],
72
+ };
73
+ }
74
+
60
75
  const json = (body, status = 200, extra = {}) => new Response(`${JSON.stringify(body)}\n`, {
61
76
  status, headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store, private', 'x-robots-tag': 'noindex', ...extra },
62
77
  });
@@ -578,12 +593,7 @@ export async function perform(env, cat, a) {
578
593
  // state leaves out is sent out of it (the owner, and in an invite-only beta the invited, play on). Back to a
579
594
  // wider state, a change still waiting is called off.
580
595
  if (launch !== prev) {
581
- const words = {
582
- private: { notice: `${meta.name} goes private after this round. Thanks for playing!`, message: `${meta.name} is private now. Thanks for playing!` },
583
- invite: { notice: `${meta.name} becomes an invite-only beta after this round. Invited players keep playing.`, message: `${meta.name} is an invite-only beta now. Have an invite? Enter it on the game's page. Thanks for playing!` },
584
- public: {},
585
- }[launch];
586
- const res = await Promise.all(rooms.map((r) => roomControl(env, meta, fresh, r.room, 'regate', ALLOW[launch] ? { allow: ALLOW[launch], ...words } : {})));
596
+ const res = await Promise.all(rooms.map((r) => roomControl(env, meta, fresh, r.room, 'regate', regateArgs(launch, meta.name))));
587
597
  done.regating = res.filter((r) => r.ok && r.leaving > 0).length;
588
598
  }
589
599
  if (a.maxPlayers !== undefined) {
@@ -1,2 +1,2 @@
1
1
  /** The @homie-rocks/studio version this Worker code is (package.json's; a test keeps them equal). Cache-busts /_homie/site.js. */
2
- export const STUDIO_VERSION_TAG = '0.14.0';
2
+ export const STUDIO_VERSION_TAG = '0.14.2';