@homie-rocks/studio 0.58.15 → 0.58.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -17,6 +17,27 @@ 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.58.18] - 2026-10-21
21
+
22
+ **Plugin 0.58.12**
23
+
24
+ Docs and tool descriptions describe the toolkit as it is today.
25
+
26
+ ### Changed
27
+
28
+ - Docs and tool descriptions describe the toolkit as it is today. The worker's adapter for a media binding a studio's host provides (`HOMIE_MEDIA`) is `worker/host-media.mjs`; it behaves as before.
29
+
30
+ ## [0.58.16] - 2026-10-21
31
+
32
+ **Plugin 0.58.10**
33
+
34
+ A failed deploy says what Cloudflare or the host actually refused.
35
+
36
+ ### Fixed
37
+
38
+ - A failed deploy whose Worker uses a queue no longer says the token needs Account > Queues > Edit unless Cloudflare refused a Queues request for authentication. Any other failure, an asset upload for one, now shows Wrangler's own error.
39
+ - When the host refuses a database call during a deploy, the reason it gives (for example that this deploy may not write to the live database) now shows after "D1 migrations failed" instead of being cut off by Wrangler's closing lines. The same goes for creating the database and registering the studio's functions.
40
+
20
41
  ## [0.58.15] - 2026-10-20
21
42
 
22
43
  **Plugin 0.58.9**
@@ -60,7 +81,7 @@ Every browser the toolkit opens can be a cloud browser, and a studio worked on a
60
81
  ### Added
61
82
 
62
83
  - Every command and skill that opens a browser (check, playtest, look, perf, shoot, lab check, port check, crowd load, 3D renders, art captures and video takes) can use a browser in the cloud (in 0.58.11 only check and the playtest script could): with `HOMIE_BROWSER_WS_ENDPOINT` set to a CDP WebSocket address (Cloudflare Browser Run, for example), they connect to it instead of starting Chrome on this computer. A platform target leaves queues and cron triggers to the platform.
63
- - A host that runs the toolkit away from the person's screen (Homie Managed's studio workspace, a cloud dev box) can name the address of the studio's private preview in `HOMIE_PREVIEW_URL`. For a studio whose studio.json has a preview target (`cloudflare.preview`), the local MCP server's `preview_run` then puts the build there (`deploy --preview`) and gives that address, `check` and `playtest` put the current build there and open it, and `preview_stop` has nothing to stop. Without it, the site runs on this computer as before.
84
+ - A host that runs the toolkit away from the person's screen (a hosted studio workspace or a cloud dev box) can name the address of the studio's private preview in `HOMIE_PREVIEW_URL`. For a studio whose studio.json has a preview target (`cloudflare.preview`), the local MCP server's `preview_run` then puts the build there (`deploy --preview`) and gives that address, `check` and `playtest` put the current build there and open it, and `preview_stop` has nothing to stop. Without it, the site runs on this computer as before.
64
85
 
65
86
  ## [0.58.11] - 2026-10-19
66
87
 
@@ -70,10 +91,10 @@ Host a studio on Workers for Platforms with a private preview, check it with a b
70
91
 
71
92
  ### Added
72
93
 
73
- - A studio can deploy to a Workers for Platforms dispatch namespace: `cloudflare.dispatchNamespace`, `worker`, `d1Id` and `domain` in studio.json name the preprovisioned Worker, and `homie-studio deploy` sends the build there with Wrangler's own `--dispatch-namespace`. `cloudflare.preview` names a second Worker and database, and `homie-studio deploy --preview` sends the same build to it. A platform that hosts studios this way (Homie Managed) enforces the target at its API boundary.
94
+ - A studio can deploy to a Workers for Platforms dispatch namespace: `cloudflare.dispatchNamespace`, `worker`, `d1Id` and `domain` in studio.json name the preprovisioned Worker, and `homie-studio deploy` sends the build there with Wrangler's own `--dispatch-namespace`. `cloudflare.preview` names a second Worker and database, and `homie-studio deploy --preview` sends the same build to it. A platform that deploys studios this way enforces the target at its own API boundary.
74
95
  - `homie-studio check` and the playtest script can use a browser in the cloud: with `HOMIE_BROWSER_WS_ENDPOINT` set to a CDP WebSocket address (Cloudflare Browser Run, for example), they connect to it instead of starting Chrome on this computer.
75
96
  - `homie-studio drain on|off|status` pauses new room admissions without deploying or closing existing sockets. A move waits for empty rooms and delivered records before freezing the source.
76
- - Managed media bindings preserve streamed media, range requests and large player saves.
97
+ - A media binding the studio's host provides (`HOMIE_MEDIA`) preserves streamed media, range requests and large player saves.
77
98
  - A read-only switch for a studio: `homie-studio read-only on|off|status` (and the local MCP tool `studio_read_only`) freezes the live studio for a few minutes, for a move to another host or maintenance, without losing anything. It sets the Worker secret `STUDIO_READ_ONLY` with the studio's own Cloudflare sign-in, as a new Worker version. While on, changes answer 503 with `Retry-After` and `x-studio-read-only: 1`; pages, games, live rooms and the Stripe webhook keep working, D1 takes no other write, room records wait in each room, scheduled and queued work waits, and `/api/games` reports `readOnly`.
78
99
 
79
100
  ### Fixed
@@ -46,13 +46,13 @@ Cloudflare or physical-phone capacity guarantees.
46
46
  ## Start it
47
47
 
48
48
  - **Look for pieces before writing them.** Games build on each other by sharing parts: a creature from one game,
49
- Search by system (touch controls, feedback, sound, scoring, title and results UI, rooms), not only by genre. Add a fitting result with `part_add` before implementing a replacement; import and use it in the game. A starter or npm package is not a catalog search. Record part version, source game/studio, licence and integration in CODEX **Built from**, or the search and concrete gap if nothing fits. In managed studios these same tools work; wait for the committed preview through `build_progress` after adding a part.
50
-
51
49
  a level from another, a bot brain from a third. For each system the game needs (camera, movement, bots, pickups,
52
50
  effects, sound, UI, environment), before writing it from scratch:
53
51
  - check the `@homie-rocks/*` packages (npm) for the general mechanism: camera, input, audio, effects;
54
52
  - run `parts_find` for pieces other studios shared, and bring one in with `part_add`.
55
53
 
54
+ Search by system (touch controls, feedback, sound, scoring, title and results UI, rooms), not only by genre. Add a fitting result with `part_add` before implementing a replacement; import and use it in the game. A starter or npm package is not a catalog search. Record part version, source game/studio, licence and integration in CODEX **Built from**, or the search and concrete gap if nothing fits. In a hosted workspace these same tools work; after `part_add`, wait for the committed preview through `build_progress`.
55
+
56
56
  Record both in the game's CODEX (Built from) and its credits: what came from where, and what you wrote here and
57
57
  why. After building a piece another game could use, offer to make it a part (`part_new`) and, only if the person
58
58
  asks, to share it (`part_share`). The `parts` skill has the method.
@@ -57,12 +57,26 @@ import { LOCAL_STATE, configPath, domainOrigin, isRulesGame, isWorkersDev, layou
57
57
  const ANSI = /\u001b\[[0-9;]*m/g;
58
58
 
59
59
  /** Keep Wrangler's error and its context; progress/footer lines must not hide it. */
60
- export function wranglerDeployError(out) {
60
+ export function wranglerError(out) {
61
61
  const clean = redactText(String(out ?? '').replace(ANSI, '')).text;
62
62
  const error = clean.search(/(?:^|\n)[^\n]*\[ERROR\]/);
63
63
  return (error >= 0 ? clean.slice(error) : clean).trim();
64
64
  }
65
65
 
66
+ /**
67
+ * Each Cloudflare API request Wrangler reports as failed, with the codes Cloudflare answered it with. Wrangler prints
68
+ * "[ERROR] A request to the Cloudflare API (/accounts/<id>/queues?name=…) failed." and then each of Cloudflare's errors
69
+ * as "<message> [code: <n>]" (workers-utils cfetch throwFetchError).
70
+ */
71
+ export function failedRequests(out) {
72
+ const text = String(out ?? '').replace(ANSI, '');
73
+ return [...text.matchAll(/\[ERROR\] A request to the Cloudflare API \(([^)\s]+)\) failed\.([\s\S]*?)(?=\n[^\n]*\[ERROR\]|$)/g)]
74
+ .map(([, resource, notes]) => ({ resource, codes: [...notes.matchAll(/\[code: (\d+)\]/g)].map((m) => Number(m[1])) }));
75
+ }
76
+ // The codes Wrangler itself treats as an authentication error (wrangler's AUTHENTICATION_ERROR_CODES): a token
77
+ // without the permission the request needs is answered with 10000, "Authentication error".
78
+ const AUTH_CODES = new Set([9106, 10000]);
79
+
66
80
  export function experienceAddresses(root, id, url) {
67
81
  const app = listGames(root).find(g => g.id === id)?.kind === 'app';
68
82
  return { [app ? 'open' : 'play']: url ? `${url}/${id}/${app ? 'open' : 'play'}` : null, wall: url ? `${url}/${id}/tv` : null };
@@ -132,7 +146,9 @@ function parseJson(text) {
132
146
  export function explainCloudflare(out, accountId = null) {
133
147
  const text = String(out ?? '');
134
148
  const dash = accountId ? `https://dash.cloudflare.com/${accountId}` : 'https://dash.cloudflare.com';
135
- if (/queue.*(?:permission|authentication|forbidden|unauthori)|(?:permission|authentication|forbidden|unauthori).*queue/is.test(text)) {
149
+ // Only a Queues API request Cloudflare refused for authentication: a failed deploy that merely binds a queue keeps
150
+ // Wrangler's own error.
151
+ if (failedRequests(text).some((r) => /\/accounts\/[^/]+\/queues(?:[/?]|$)/.test(r.resource) && r.codes.some((c) => AUTH_CODES.has(c)))) {
136
152
  return {needs:'queues-permission',why:'The deployment token needs Account > Queues > Edit on this account. Update the CI/Workers Builds token permissions, then retry deploy.'};
137
153
  }
138
154
  if (/(?:cron|schedule).*?(?:limit|maximum|exceed)|(?:limit|maximum|exceed).*?(?:cron|schedule)/i.test(text)) {
@@ -388,7 +404,7 @@ async function deployLocked(root, { log = () => {}, homie, fetchFn = null, ownRo
388
404
  if (exists.code === 0) {
389
405
  return { ok: false, command: 'deploy', why: `A Worker named "${cf.worker}" already exists on this Cloudflare account and this studio did not create it, so nothing was deployed. Pick another name in studio.json (cloudflare.worker) and site/wrangler.jsonc (name).` };
390
406
  }
391
- if (!/10007|does not exist/i.test(exists.out)) return refuse(`could not check the Worker name: ${exists.out.trim().split('\n').slice(-3).join(' ')}`, exists.out);
407
+ if (!/10007|does not exist/i.test(exists.out)) return refuse(`could not check the Worker name: ${wranglerError(exists.out)}`, exists.out);
392
408
  }
393
409
 
394
410
  // D1.
@@ -401,7 +417,7 @@ async function deployLocked(root, { log = () => {}, homie, fetchFn = null, ownRo
401
417
  if (!db) {
402
418
  const made = w(['d1', 'create', cf.d1]);
403
419
  const id = /"?database_id"?\s*[:=]\s*"([0-9a-f-]{36})"/.exec(made.out)?.[1];
404
- if (made.code !== 0 || !id) return refuse(`could not create the D1 database "${cf.d1}": ${made.out.trim().split('\n').slice(-4).join(' ')}`, made.out);
420
+ if (made.code !== 0 || !id) return refuse(`could not create the D1 database "${cf.d1}": ${wranglerError(made.out)}`, made.out);
405
421
  db = { name: cf.d1, uuid: id };
406
422
  remember(`d1:${cf.d1}`);
407
423
  step(`created D1 ${cf.d1}`);
@@ -431,11 +447,11 @@ async function deployLocked(root, { log = () => {}, homie, fetchFn = null, ownRo
431
447
  for (const added of ensureMigrations(root)) step(`added ${added} (${migrationWord(added)})`);
432
448
  try {await ensureBackgroundQueues(root,w,{accountId,...(fetchFn?{fetchFn}:{})});for(const key of readStudio(root).cloudflare.created??[])created.add(key);} catch(error) {return refuse(error.message);}
433
449
  const migrate = w(['d1', 'migrations', 'apply', cf.d1, '--remote']);
434
- if (migrate.code !== 0) return refuse(`D1 migrations failed: ${migrate.out.trim().split('\n').slice(-4).join(' ')}`, migrate.out);
450
+ if (migrate.code !== 0) return refuse(`D1 migrations failed: ${wranglerError(migrate.out)}`, migrate.out);
435
451
  try {const replication=await enableReadReplication(root,w,{accountId,databaseId:db.uuid,...(fetchFn?{fetchFn}:{})});step(replication.enabled?'D1 read replication enabled':`D1 uses the primary: ${replication.reason}`);} catch(error) {step(`D1 uses the primary: ${error.message}`);}
436
452
  step('D1 migrations applied');
437
453
  const subscriptions=w(['d1','execute',cf.d1,'--remote','--command',(await import('./functions-build.mjs')).functionDeploymentSQL(root)]);
438
- if(subscriptions.code!==0)return refuse('Could not register studio functions',subscriptions.out);
454
+ if(subscriptions.code!==0)return refuse(`Could not register studio functions: ${wranglerError(subscriptions.out)}`,subscriptions.out);
439
455
 
440
456
  const started = Date.now();
441
457
  let dep = w(['deploy', ...(workerEntry(root) ? [workerEntry(root)] : []), ...repoVar(root)]);
@@ -447,7 +463,7 @@ async function deployLocked(root, { log = () => {}, homie, fetchFn = null, ownRo
447
463
  step(address.created ? 'registered the account’s workers.dev address automatically' : 'kept the account’s existing workers.dev address');
448
464
  dep = w(['deploy', ...(workerEntry(root) ? [workerEntry(root)] : []), ...repoVar(root)]);
449
465
  }
450
- if (dep.code !== 0) return refuse(`wrangler deploy failed: ${wranglerDeployError(dep.out)}`, dep.out);
466
+ if (dep.code !== 0) return refuse(`wrangler deploy failed: ${wranglerError(dep.out)}`, dep.out);
451
467
  try{await ensureStudioSigningKey(readStudio(root),w);}catch(error){return refuse(error.message);}
452
468
  if (paidReleases(root).length) {
453
469
  const bin = wranglerBin(root);
@@ -658,11 +674,11 @@ export async function ciDeploy(root, { log = () => {}, wrangler = null, preview
658
674
  const apply = () => w(['d1', 'migrations', 'apply', names.binding, '--remote']);
659
675
  let migrate = apply();
660
676
  const first = !target && migrate.code !== 0 && /not found|could(?:n't| not) find|does not exist|no database|database_id/i.test(migrate.out);
661
- if (migrate.code !== 0 && !first) return refuse(`D1 migrations failed: ${migrate.out.trim().split('\n').slice(-4).join(' ')}`, migrate.out);
677
+ if (migrate.code !== 0 && !first) return refuse(`D1 migrations failed: ${wranglerError(migrate.out)}`, migrate.out);
662
678
  if (!first) step('D1 migrations applied');
663
679
  const register=()=>w(['d1','execute',names.binding,'--remote','--command',functionDeploymentSQL(root)]);
664
680
  const {functionDeploymentSQL}=await import('./functions-build.mjs');
665
- if(!first) {const registered=register();if(registered.code!==0)return refuse('Could not register studio functions',registered.out);}
681
+ if(!first) {const registered=register();if(registered.code!==0)return refuse(`Could not register studio functions: ${wranglerError(registered.out)}`,registered.out);}
666
682
  step(`Cron triggers: ${(readConfig(root)?.triggers?.crons??[]).join('; ')||'none (remove toolkit triggers)'}`);
667
683
  // Workers AI (0.17.0): bound only when a server's AI guides think with it. The checkout is thrown away after.
668
684
  if (!first && needsWorkersAi(root, w, names.binding)) {
@@ -674,16 +690,16 @@ export async function ciDeploy(root, { log = () => {}, wrangler = null, preview
674
690
  }
675
691
  const started = Date.now();
676
692
  const dep = w(['deploy', ...(workerEntry(root) ? [workerEntry(root)] : []), ...repoVar(root)]);
677
- if (dep.code !== 0) return refuse(`wrangler deploy failed: ${wranglerDeployError(dep.out)}`, dep.out);
693
+ if (dep.code !== 0) return refuse(`wrangler deploy failed: ${wranglerError(dep.out)}`, dep.out);
678
694
  try{await ensureStudioSigningKey(readStudio(root),w);}catch(error){return refuse(error.message);}
679
695
  step(`deployed ${names.worker ?? 'the Worker'} in ${Math.round((Date.now() - started) / 1000)} s`);
680
696
  const repo = repoOf(root);
681
697
  step(repo ? `the live site knows its GitHub repository (${repo}), for the Claude app's hand-off` : 'no GitHub repository found here (no HOMIE_REPO, studio.json "github" or git remote): the Claude app\'s card asks the person which it is');
682
698
  if (first) {
683
699
  migrate = apply();
684
- if (migrate.code !== 0) return refuse(`D1 migrations failed after the first deploy: ${migrate.out.trim().split('\n').slice(-4).join(' ')}`, migrate.out);
700
+ if (migrate.code !== 0) return refuse(`D1 migrations failed after the first deploy: ${wranglerError(migrate.out)}`, migrate.out);
685
701
  step('D1 migrations applied (the database was created with this first deploy)');
686
- const registered=register();if(registered.code!==0)return refuse('Could not register studio functions',registered.out);
702
+ const registered=register();if(registered.code!==0)return refuse(`Could not register studio functions: ${wranglerError(registered.out)}`,registered.out);
687
703
  }
688
704
  try {const replication=await enableReadReplication(root,w);step(replication.enabled?'D1 read replication enabled':`D1 uses the primary: ${replication.reason}`);} catch(error) {step(`D1 uses the primary: ${error.message}`);}
689
705
  // The live address, from what Wrangler printed (a custom domain in studio.json wins).
@@ -735,7 +751,7 @@ export async function storageAdd(root, { log = () => {} } = {}) {
735
751
  const buckets = w(['r2', 'bucket', 'list']);
736
752
  if (buckets.code !== 0) {
737
753
  if (/enable R2|R2 is not enabled|10042|purchase|subscription|payment/i.test(buckets.out)) return enable;
738
- return { ok: false, command: 'storage add', ...(explainCloudflare(buckets.out, accountId) ?? { why: `could not list R2 buckets: ${buckets.out.trim().split('\n').slice(-3).join(' ')}` }) };
754
+ return { ok: false, command: 'storage add', ...(explainCloudflare(buckets.out, accountId) ?? { why: `could not list R2 buckets: ${wranglerError(buckets.out)}` }) };
739
755
  }
740
756
  const names = [...buckets.out.matchAll(/^name:\s+(\S+)/gm)].map((m) => m[1]);
741
757
  if (names.includes(bucket)) {
@@ -744,7 +760,7 @@ export async function storageAdd(root, { log = () => {} } = {}) {
744
760
  const made = w(['r2', 'bucket', 'create', bucket]);
745
761
  if (made.code !== 0) {
746
762
  if (/enable R2|R2 is not enabled|10042|purchase|subscription|payment/i.test(made.out)) return enable;
747
- return { ok: false, command: 'storage add', why: `could not create the R2 bucket "${bucket}": ${made.out.trim().split('\n').slice(-4).join(' ')}` };
763
+ return { ok: false, command: 'storage add', why: `could not create the R2 bucket "${bucket}": ${wranglerError(made.out)}` };
748
764
  }
749
765
  created.add(`r2:${bucket}`);
750
766
  const next = { ...readStudio(root), cloudflare: { ...readStudio(root).cloudflare, accountId, r2: bucket, created: [...created].sort() } };
@@ -844,7 +860,7 @@ export async function mediaMove(root, { paths = null, dryRun = false, verify = f
844
860
  // Where the file is meanwhile: on the site when it fits; a bigger one keeps an older R2 copy, or waits.
845
861
  const meanwhile = r.bytes <= MAX_ASSET_BYTES ? ' It stays on the site.' : r.held ? ' Its page keeps playing the older copy in R2.' : ' At this size the site cannot carry it, so its page waits until it is in R2.';
846
862
  if (put.code !== 0) {
847
- failed.push({ path: r.path, bytes: r.bytes, why: `${explainCloudflare(put.out, account)?.why ?? `the upload failed: ${put.out.trim().split('\n').slice(-3).join(' ')}`}${meanwhile}` });
863
+ failed.push({ path: r.path, bytes: r.bytes, why: `${explainCloudflare(put.out, account)?.why ?? `the upload failed: ${wranglerError(put.out)}`}${meanwhile}` });
848
864
  continue;
849
865
  }
850
866
  const got = await hashRemote(bin, `${bucket}/${key}`, opts);
package/lib/mcp-tools.mjs CHANGED
@@ -462,7 +462,7 @@ async function startRun(ctx, root, { kind, game, title, steps }) {
462
462
 
463
463
  /**
464
464
  * WHERE "HERE" IS, FOR A HOST AWAY FROM THE PERSON'S SCREEN. A platform that runs this toolkit for the person in a
465
- * workspace of its own (Homie Managed's studio workspace, a cloud dev box) has no browser beside it that can open
465
+ * workspace of its own (a hosted studio workspace or a cloud dev box) has no browser beside it that can open
466
466
  * this computer's addresses. It names the address its studio's private preview is reachable at, HOMIE_PREVIEW_URL,
467
467
  * and the studio's studio.json names that preview's Workers for Platforms target (cloudflare.preview). Then the
468
468
  * studio's site "here" is that preview: preview_run puts the build there (deploy --preview), and check and playtest
@@ -4638,6 +4638,90 @@
4638
4638
  }
4639
4639
  }
4640
4640
  }
4641
+ },
4642
+ "0.58.16": {
4643
+ "files": {
4644
+ "CLAUDE.md": "ddf2b2a0376202f6",
4645
+ "README.md": "684b3cca8001673c",
4646
+ "HANDOFF.md": "29337e2ace95b2e7",
4647
+ "games/README.md": "5ad32fe80c61f117",
4648
+ "music/README.md": "a3dc042dadd39df8",
4649
+ "videos/README.md": "16babc3437d5545b",
4650
+ "posts/README.md": "37841aae476bd540",
4651
+ "site/README.md": "1dbdf22c03a1e505",
4652
+ "site/src/worker.mjs": "6cc5c6131eaf6fd0"
4653
+ },
4654
+ "sections": {
4655
+ "AGENTS.md": {
4656
+ "(intro)": "af7f2f732505d379",
4657
+ "## Version-matched authoring guides": "f6261a0e5801b96b",
4658
+ "## Layout": "f3a280b8c96cd068",
4659
+ "## Apps: one screen, roles and parts": "c10aa0441ea89702",
4660
+ "## Commands (all through the pinned CLI in node_modules)": "c20d90de21cde0c7",
4661
+ "## The site": "0cb8dd9a6f8a0096",
4662
+ "## Making games": "bb715272fd1a615b",
4663
+ "## The Game Codex and progress": "0738ef719cef3a4c",
4664
+ "## Art direction and models": "a48fff7bf0a5f2fc",
4665
+ "## Running live games (the back office)": "51ebf2ab756f6e4d",
4666
+ "## Servers and AI seats": "1ccfd549d2369a60",
4667
+ "## Selling things (the shop)": "0df5a5fe1833c624",
4668
+ "## Continuing a build from the Claude app": "0d68081e924ca419",
4669
+ "## Rules": "b2234d80f6cbe71d",
4670
+ "## Telling Homie": "80b46fdd9e578db8",
4671
+ "## Beta": "9d02638207a26e73"
4672
+ }
4673
+ },
4674
+ "named": {
4675
+ "Homie Arcade": {
4676
+ "sections": {
4677
+ "AGENTS.md": {
4678
+ "## Commands (all through the pinned CLI in node_modules)": "8364e05cebefcba4"
4679
+ }
4680
+ }
4681
+ }
4682
+ }
4683
+ },
4684
+ "0.58.18": {
4685
+ "files": {
4686
+ "CLAUDE.md": "ddf2b2a0376202f6",
4687
+ "README.md": "684b3cca8001673c",
4688
+ "HANDOFF.md": "29337e2ace95b2e7",
4689
+ "games/README.md": "5ad32fe80c61f117",
4690
+ "music/README.md": "a3dc042dadd39df8",
4691
+ "videos/README.md": "16babc3437d5545b",
4692
+ "posts/README.md": "37841aae476bd540",
4693
+ "site/README.md": "1dbdf22c03a1e505",
4694
+ "site/src/worker.mjs": "6cc5c6131eaf6fd0"
4695
+ },
4696
+ "sections": {
4697
+ "AGENTS.md": {
4698
+ "(intro)": "af7f2f732505d379",
4699
+ "## Version-matched authoring guides": "f6261a0e5801b96b",
4700
+ "## Layout": "f3a280b8c96cd068",
4701
+ "## Apps: one screen, roles and parts": "c10aa0441ea89702",
4702
+ "## Commands (all through the pinned CLI in node_modules)": "c20d90de21cde0c7",
4703
+ "## The site": "0cb8dd9a6f8a0096",
4704
+ "## Making games": "bb715272fd1a615b",
4705
+ "## The Game Codex and progress": "0738ef719cef3a4c",
4706
+ "## Art direction and models": "a48fff7bf0a5f2fc",
4707
+ "## Running live games (the back office)": "51ebf2ab756f6e4d",
4708
+ "## Servers and AI seats": "1ccfd549d2369a60",
4709
+ "## Selling things (the shop)": "0df5a5fe1833c624",
4710
+ "## Continuing a build from the Claude app": "0d68081e924ca419",
4711
+ "## Rules": "b2234d80f6cbe71d",
4712
+ "## Telling Homie": "80b46fdd9e578db8",
4713
+ "## Beta": "9d02638207a26e73"
4714
+ }
4715
+ },
4716
+ "named": {
4717
+ "Homie Arcade": {
4718
+ "sections": {
4719
+ "AGENTS.md": {
4720
+ "## Commands (all through the pinned CLI in node_modules)": "8364e05cebefcba4"
4721
+ }
4722
+ }
4723
+ }
4724
+ }
4641
4725
  }
4642
4726
  }
4643
4727
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homie-rocks/studio",
3
- "version": "0.58.15",
3
+ "version": "0.58.18",
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, big songs and videos in the studio's own R2 once it has storage (uploaded, checked by SHA-256, served at the same addresses), deploy, and the homie.rocks directory.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -1,2 +1,2 @@
1
1
  // Generated by lib/build-identity.mjs; identifies the bundled runtime sources.
2
- export const STUDIO_BUILD = Object.freeze({"version":"0.58.15","sha256":"21d2ff598c3349a401492759f1ee0ce077752b2e3e00b8078973d3eac14197fc"});
2
+ export const STUDIO_BUILD = Object.freeze({"version":"0.58.18","sha256":"333254a4a029de3f655b1689480b580903801f14cc0d9e6ea99f2bac4b9e5c7f"});
@@ -1,4 +1,4 @@
1
- /** Hydrate the R2-compatible metadata returned by the studio-scoped managed binding.
1
+ /** Hydrate the R2-compatible metadata returned by the studio-scoped media binding its host provides (HOMIE_MEDIA).
2
2
  * Streams cross RPC without buffering songs, videos or saves in the Worker heap. */
3
3
  function object(value) {
4
4
  if(!value)return null;
@@ -9,7 +9,7 @@ function object(value) {
9
9
  json:()=>new Response(value.body).json(),
10
10
  };
11
11
  }
12
- export function managedMedia(env) {
12
+ export function hostMedia(env) {
13
13
  if(!env.HOMIE_MEDIA)return env;
14
14
  const binding=env.HOMIE_MEDIA;
15
15
  return {...env,MEDIA:{get:async(key,options)=>object(await binding.get(key,options)),head:async key=>object(await binding.head(key)),put:async(key,body,options)=>object(await binding.put(key,body,options)),delete:keys=>binding.delete(keys)}};
package/worker/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import {drainSince,isAdmission,drainResponse} from './move-drain.mjs';
2
- import {managedMedia} from './managed-media.mjs';
2
+ import {hostMedia} from './host-media.mjs';
3
3
  /** Studio Worker event boundaries and public toolkit exports. */
4
4
  import { catalogue } from './catalogue.mjs';
5
5
  import {trackBackground,requestDatabase,MUTATION_VERSION} from './request-db.mjs';
@@ -66,7 +66,7 @@ export default {
66
66
  await Promise.all([maintenance,analytics]);
67
67
  },
68
68
  async fetch(request, env, ctx) {
69
- env=managedMedia(env);
69
+ env=hostMedia(env);
70
70
  if(isAdmission(request)&&await drainSince(env))return drainResponse();
71
71
  const staticPath=new URL(request.url).pathname;
72
72
  if(['GET','HEAD'].includes(request.method)&&staticPath==='/_homie/site.js')return finish(await route(request,env,ctx),staticPath);
@@ -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.58.15';
2
+ export const STUDIO_VERSION_TAG = '0.58.18';