@homie-rocks/studio 0.51.3 → 0.51.5

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.
Files changed (91) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +10 -29
  3. package/apps/APPS.md +1 -1
  4. package/bin/homie-studio.mjs +28 -30
  5. package/docs/mcp-events.md +68 -0
  6. package/guides/app/SKILL.md +25 -1
  7. package/guides/art/SKILL.md +68 -140
  8. package/guides/art/scripts/art.mjs +6 -79
  9. package/guides/models/SKILL.md +73 -177
  10. package/guides/models/references/RIGHTS.md +1 -1
  11. package/guides/models/scripts/models.mjs +7 -288
  12. package/guides/music/SKILL.md +74 -183
  13. package/guides/music/references/COMPOSITION.md +21 -26
  14. package/guides/music/references/GAME-AUDIO.md +3 -3
  15. package/guides/music/references/RIGHTS.md +12 -15
  16. package/guides/music/scripts/music.mjs +11 -202
  17. package/guides/office/SKILL.md +24 -0
  18. package/guides/parts/SKILL.md +1 -1
  19. package/guides/servers/SKILL.md +24 -0
  20. package/guides/shop/SKILL.md +68 -77
  21. package/guides/sound/SKILL.md +70 -1
  22. package/guides/sound/scripts/player/sound.js +233 -0
  23. package/guides/studio-setup/SKILL.md +11 -27
  24. package/guides/studio-setup/references/OFFICIAL-MEDIA.md +66 -0
  25. package/guides/style/SKILL.md +66 -3
  26. package/guides/video/SKILL.md +83 -0
  27. package/guides/video/references/DELIVERY.md +73 -0
  28. package/guides/video/references/HONESTY.md +40 -0
  29. package/guides/video/references/METHOD.md +144 -0
  30. package/guides/video/references/RECORD.md +137 -0
  31. package/guides/video/references/TRAILER.md +154 -0
  32. package/guides/video/references/examples/site-walkthrough.json +14 -0
  33. package/guides/video/references/examples/studio-play-phone.json +15 -0
  34. package/guides/video/references/examples/studio-play.json +17 -0
  35. package/guides/video/scripts/capture-game.mjs +175 -0
  36. package/guides/video/scripts/clock.js +136 -0
  37. package/guides/video/scripts/drawover/film.js +58 -0
  38. package/guides/video/scripts/drawover/index.html +9 -0
  39. package/guides/video/scripts/drawover/look.js +115 -0
  40. package/guides/video/scripts/drawover/shots.json +12 -0
  41. package/guides/video/scripts/lib/browser.mjs +87 -0
  42. package/guides/video/scripts/lib/frames.mjs +170 -0
  43. package/guides/video/scripts/lib/remix.mjs +159 -0
  44. package/guides/video/scripts/qa.mjs +127 -0
  45. package/guides/video/scripts/record-fixed.mjs +403 -0
  46. package/guides/video/scripts/record-page.mjs +477 -0
  47. package/guides/video/scripts/tap.js +108 -0
  48. package/guides/video/scripts/video.mjs +888 -0
  49. package/lib/build.mjs +12 -9
  50. package/lib/cloudflare.mjs +6 -11
  51. package/lib/dev.mjs +1 -2
  52. package/lib/doctor.mjs +10 -54
  53. package/lib/functions-build.mjs +8 -1
  54. package/lib/mcp-tools.mjs +42 -21
  55. package/lib/mcp.mjs +1 -1
  56. package/lib/media-import.mjs +41 -0
  57. package/lib/media-workflows.mjs +13 -0
  58. package/lib/official-media.mjs +15 -0
  59. package/lib/rules-build.mjs +13 -5
  60. package/lib/scaffold.mjs +2 -2
  61. package/lib/shop-links.mjs +54 -18
  62. package/lib/shop-listen.mjs +43 -0
  63. package/lib/shop-local.mjs +1 -1
  64. package/lib/shop-mcp.mjs +58 -0
  65. package/lib/stripe-connect.mjs +36 -15
  66. package/lib/template-history.json +87 -0
  67. package/lib/upgrade-media.mjs +13 -0
  68. package/lib/upgrade.mjs +10 -0
  69. package/package.json +1 -1
  70. package/parts/PARTS.md +1 -1
  71. package/rules/view.ts +4 -0
  72. package/shop/SHOP.md +66 -34
  73. package/worker/build-id.mjs +1 -1
  74. package/worker/event-resources.mjs +12 -3
  75. package/worker/event-webhooks.mjs +14 -9
  76. package/worker/functions.mjs +11 -6
  77. package/worker/index.mjs +5 -1
  78. package/worker/mcp-event-streams.mjs +58 -0
  79. package/worker/mcp-events-wire.mjs +41 -0
  80. package/worker/mcp-events.mjs +132 -0
  81. package/worker/mcp.mjs +4 -1
  82. package/worker/purchase-reconciliation.mjs +16 -1
  83. package/worker/shop-subscriptions.mjs +2 -0
  84. package/worker/shop.mjs +7 -5
  85. package/worker/stripe-ownership.mjs +7 -0
  86. package/worker/version.mjs +1 -1
  87. package/guides/models/references/models.json +0 -175
  88. package/guides/music/scripts/lib/eleven.mjs +0 -260
  89. package/lib/projects-env.mjs +0 -68
  90. package/lib/projects.mjs +0 -311
  91. package/lib/provider-key.mjs +0 -21
package/CHANGELOG.md CHANGED
@@ -17,6 +17,51 @@ 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.51.5] - 2026-10-14
21
+
22
+ **Plugin 0.51.5**
23
+
24
+ Connect your shop and media through official plugins, keep renewals and free bookings working, and see progress while you build.
25
+
26
+ ### Added
27
+
28
+ - **Shop setup:** Stripe's official plugin can configure and verify products, prices, item and menu links, paid bookings, monthly giving and the customer portal. Homie plans only the changes your shop needs. No API keys are requested; webhook installation checks the Stripe account and passes the signing secret privately to Wrangler, never through the model. Local webhook forwarding keeps its secret private too.
29
+ - **Build iteration:** Builds report progress, including during JSON output, and show a heartbeat while generated rules play. `build --quick` keeps type and guard checks while skipping play and snapshot smoke checks.
30
+ - **Media placement:** `media import` places completed media, checks scene references and records file hashes, rights, provenance and explicit cost status. Video guidance and local processing tools now ship with the toolkit.
31
+
32
+ ### Changed
33
+
34
+ - **Official media connections:** Art, models, music, sound, video and style use the official fal and ElevenLabs plugins in your AI client. Homie still handles art direction, local processing, placement and provenance. Its own provider generation clients, key-file readers and Stripe Projects credential bridge have been removed.
35
+ - **Connection setup:** Setup reports official connections verified in the current session and explains browser sign-in. Cloudflare inspection uses its official MCP; deployment continues through Wrangler's browser login.
36
+
37
+ ### Fixed
38
+
39
+ - **Recurring gifts and memberships:** Each renewal reaches your shop and notifies apps once, even when the studio also sells parts. Renewals keep the original purchase reference so apps can identify the cause or member. Subscription updates and cancellations reach the shop; parts payments retain their own route.
40
+ - **Free volunteer shifts and services:** Free bookings in a mixed free/paid app work before Stripe is connected. They confirm locally and enforce capacity; paid checkout still requires a connection.
41
+
42
+ ### Upgrade notes
43
+
44
+ - Update the toolkit and Homie plugin together, run upgrade, and connect the official provider plugins in your AI client. Existing media remains usable. Upgrade replaces retired credential instructions without reading or migrating old key files. Available ElevenLabs media capabilities depend on the connected server and are checked before generation.
45
+ - Shop setup starts with `stripe_shop_plan` / `shop plan`, then `stripe_webhook` / `shop webhook`. Private webhook setup may need a separate Stripe CLI browser approval. `shop connect` remains the full CLI alternative; `shop listen` forwards local events privately.
46
+ - Run a full build before release, even if you used `build --quick` while iterating.
47
+ - Remove any temporary subscription-routing postinstall workaround for 0.51.3–0.51.4 after upgrading.
48
+
49
+ ## [0.51.4] - 2026-10-14
50
+
51
+ **Plugin 0.51.4** · Studio events through experimental MCP Events.
52
+
53
+ Your studio’s declared events reach supporting AI clients through experimental MCP polling, SSE streams and signed webhooks.
54
+
55
+ ### Added
56
+
57
+ - Every declared studio feed speaks the Triggers & Events draft: role-filtered `events/list`, cursor/replay polling, SSE push with heartbeat and resume, and authenticated webhook subscribe/refresh/unsubscribe with negotiated TTLs and client-supplied Standard Webhooks secrets.
58
+ - Extension and configured webhooks share the durable delivery engine. Owners can see retries and errors; rotation dual-signs for five minutes. Existing resource subscriptions and cursor tools keep working.
59
+ - Servers, app and office guides explain subscription setup and Cloudflare's own Workers Issues events through the person's existing Cloudflare connection.
60
+
61
+ ### Upgrade notes
62
+
63
+ - Experimental wire format tracks the working-group draft as inspected on 2026-10-14. Declare exact trusted HTTPS callback URLs in each feed's `receivers` array for extension webhooks. Workers fetch cannot pin DNS with original TLS SNI, so arbitrary callbacks are refused; the guide documents this draft limitation. Poll and push need no callback setup.
64
+
20
65
  ## [0.51.3] - 2026-10-14
21
66
 
22
67
  **Plugin 0.51.3**
package/README.md CHANGED
@@ -513,17 +513,7 @@ npx homie-studio shop # open (test or live) or what is m
513
513
  npx homie-studio shop refund ord_… # an ASK: the owner taps once (or Refund in /_studio/office/shop)
514
514
  ```
515
515
 
516
- Connect with `stripe_login` or `shop connect`: the owner approves Stripe's official browser page, sandbox first.
517
- Where the CLI supplies a transferable legacy test key, the toolkit creates the webhook and installs both Worker
518
- secrets privately. It reports the 90-day expiry; `shop connect --renew` obtains approval again. Current CLI OAuth
519
- and live mode cannot export an independent Worker's key, so the result names that limitation. Only if the owner
520
- chooses the fallback does `shop connect --manual` open the old local key page (`--manual --live` for live mode).
521
- Never request a key in chat. Login is not a verified purchase; complete the test purchase, webhook and refund check.
522
-
523
- Stripe's optional agent tools (`stripe agent setup`) provide MCP and skills for catalog work (`shop catalog`),
524
- tax reads and sales questions. They are separate from the Worker's credentials. Never create a webhook through
525
- MCP because its response contains a secret. From 2026-10-31 Stripe MCP accepts only OAuth or Agent-tagged keys;
526
- that change does not affect the shop's ordinary restricted API key. [SHOP.md](shop/SHOP.md) describes the flow.
516
+ Connect through Stripe’s official OAuth plugin: `stripe_shop_plan` gives exact catalog, menu, booking and recurring operations for Stripe MCP and verifies its fresh reads. `stripe_webhook` privately installs the signing secret using one separate Stripe CLI browser approval if needed. No owner handles a key. `shop connect` remains the CLI alternative. [SHOP.md](shop/SHOP.md) describes the flow.
527
517
 
528
518
  In a game: `createShop()` from `@homie-rocks/studio/shop` (`has`, `entitlements`, `on('change')`, `open`, `used`, `add`, `checkout`, `buy`); a
529
519
  supporter's badge rides on their seat (`peer.badge`). Stripe Checkout (hosted), optional Stripe Tax, or Stripe Managed
@@ -1185,26 +1175,17 @@ no Wrangler, no rooms, the game alone and offline with its bots) and shoots that
1185
1175
  there (it needs rooms: use `dev` and `--url`). The result names the build the frames are of, by the hash `build`
1186
1176
  printed and the hashed bundle the page loaded.
1187
1177
 
1188
- ## Another way to Cloudflare: Stripe Projects (a prototype, 0.24.3)
1178
+ ## Official media and hosting connections
1189
1179
 
1190
- `npx wrangler login` stays the way. For a creator with no Cloudflare account (and no ElevenLabs), Stripe Projects
1191
- (Stripe's CLI plugin, docs.stripe.com/projects) can make or link both on the person's own Stripe sign-in, on free
1192
- plans, and hand the credentials to the studio folder:
1180
+ Connect fal and ElevenLabs through their official plugins in your AI client and approve
1181
+ one browser sign-in per provider. Homie never handles their credentials. Cloudflare's
1182
+ plugin provides account inspection; deployment keeps pinned Wrangler's own browser login.
1183
+ See [connection and upgrade guidance](guides/studio-setup/references/OFFICIAL-MEDIA.md).
1193
1184
 
1194
- ```sh
1195
- npx homie-studio setup --via stripe-projects [--with elevenlabs] --dry-run # what it would do; changes nothing
1196
- npx homie-studio setup --via stripe-projects [--with elevenlabs] --accept-tos # after the person accepted the providers' terms
1197
- ```
1198
-
1199
- It stops with a `needs` step at each thing that is the person's (the Stripe CLI and its Projects plugin, Stripe's
1200
- sign-in, the providers' terms, Cloudflare's own approval page for an existing account); it never adds a paid plan or
1201
- a payment method. The Cloudflare account id goes in studio.json with `"auth": "stripe-projects"`; the token stays in
1202
- Projects' vault and the git-ignored `.env` it syncs, and every Wrangler run in the studio takes it from there
1203
- (`deploy`, `dev`, secrets, the office). It checks read-only what the token can do (Wrangler signed in, D1, Workers AI;
1204
- R2 is optional) and that `.gitignore` keeps `.env`, `.env.*`, `.projects/vault/` and `.projects/cache/` out of git.
1205
- With `--with elevenlabs` the music skill finds the ElevenLabs key there too (ElevenLabs' free plan has no commercial
1206
- licence). Unverified until a real run: the exact JSON of the Projects commands, and the Cloudflare token's permission
1207
- groups, which the checks measure instead of assuming.
1185
+ `homie-studio media import --file <download> --receipt <receipt.json> --out <studio-path>
1186
+ --used-by <scene-file>` places a completed asset and records its hash, rights, style and
1187
+ cost status in `media/provenance.json`. It makes no provider calls. The scene file must
1188
+ reference the delivered path; also verify the asset in the running scene.
1208
1189
 
1209
1190
  ## No payment method needed
1210
1191
 
package/apps/APPS.md CHANGED
@@ -114,7 +114,7 @@ information in a separate private collection. Public roles must not have write
114
114
  capabilities for slots or bookings. Staff movement is a versioned record update
115
115
  containing only `slot`; confirmation and release follow verified Checkout state.
116
116
  The default keyless connection uses one Payment Link per published slot. Call
117
- `stripe_bookings_sync` after publishing/changing slots. Pending checkout does not
117
+ `stripe_shop_plan` with the complete slot manifest after publishing/changing slots. Pending checkout does not
118
118
  block others; payment secures the seat. Use record.booking.full/available/synced
119
119
  for slot display. Read SHOP.md for the complete manifest and limitations.
120
120
 
@@ -17,9 +17,9 @@
17
17
  * homie-studio app role <id> <role> [--player <account>] [--revoke] [--url <site>]
18
18
  * homie-studio game new <id> [--from gem-rush] [--name "<Game Name>"]
19
19
  * homie-studio games
20
- * homie-studio build [<id>] [--maps] [--types] [--long-check] (--maps: also keep each bundle's source map and
20
+ * homie-studio build [<id>] [--maps] [--types] [--long-check] [--quick] (--maps: also keep each bundle's source map and
21
21
  * module sizes in .studio/maps/<id>/, never in site/dist: what `perf` reads a
22
- * CPU profile through; --types: check the games' TypeScript first, with the
22
+ * CPU profile through; --quick: skip rules play and smoke checks; --types: check the games' TypeScript first, with the
23
23
  * studio's own `typescript`; a type error stops the build; --long-check: play
24
24
  * each rules game eight times as long before it is written (a rules game is
25
25
  * always type-checked and played; this is more of the same play, for a
@@ -183,10 +183,11 @@
183
183
  * homie-studio shop is the studio's shop selling (its own Stripe), and if not, what is missing
184
184
  * homie-studio shop init [--supporter|--giving] [--currency <code>] [--price 500] [--managed] shop.json and SELLING.md
185
185
  * homie-studio shop check shop.json against studio settings and provider requirements
186
- * homie-studio shop connect [--live] [--renew] [--manual] sync Payment Links and webhook through Stripe browser approval; --manual chooses the local page for the studio's
187
- * restricted Stripe key; with it this makes the webhook (0.24.3), and the key
188
- * and the webhook's secret go straight to the Worker secrets, never a chat or a
189
- * file (test keys only unless --live)
186
+ * homie-studio shop listen --profile <sandbox> [--port 8787] private local webhook forwarding
187
+ * homie-studio shop plan --have <file> exact next operation for Stripe's official MCP (public reads only)
188
+ * homie-studio shop verify --have <file> verify fresh MCP reads and save the public link configuration
189
+ * homie-studio shop webhook [--live] [--renew] privately install webhook using one Stripe CLI browser approval
190
+ * homie-studio shop connect [--live] [--renew] alternative: full sync through Stripe CLI approval
190
191
  * homie-studio shop catalog [--have <file>|-] [--mode test|live] the items as Products in the studio's Stripe,
191
192
  * made by the AI through Stripe's own MCP: the read first, then (with what it
192
193
  * answered) the exact writes; in sync, shop.json "catalog" records the mode
@@ -226,14 +227,9 @@
226
227
  * homie-studio setup attach <hs_…> [--client claude|codex|grok]
227
228
  * (this repository is the studio the chat's setup card is making: say so, once,
228
229
  * learn its live address, and give a template copy its real name)
229
- * homie-studio setup --via stripe-projects [--with elevenlabs] [--accept-tos] [--dry-run]
230
- * (a prototype, 0.24.3: Cloudflare, and ElevenLabs with --with, made or linked
231
- * through Stripe Projects on their free plans; the person accepts the providers'
232
- * terms (then --accept-tos) and signs in on Stripe's and the provider's own
233
- * pages; the account id goes into studio.json, the token stays in Projects'
234
- * vault and its git-ignored .env, and Wrangler runs with it here. The usual
235
- * way, npx wrangler login, stays the default. lib/projects.mjs)
236
230
  * homie-studio domain <hostname> AI configures DNS/TLS for the next deploy
231
+ * homie-studio media import --file <download> --receipt <receipt.json> --out <studio-path> --used-by <scene-file>
232
+ * Place completed official MCP output with provenance (no provider calls)
237
233
  * homie-studio setup prepare AI installs a private Node/npm if missing
238
234
  * homie-studio setup status [--connector yes|no] [--client claude|codex|grok] (also: homie-studio doctor)
239
235
  * what this computer and the person's accounts have for a studio: Node, the Homie
@@ -325,6 +321,7 @@
325
321
  *
326
322
  * Every command prints a few lines for a person; --json prints the result.
327
323
  */
324
+ import { importMedia } from '../lib/media-import.mjs';
328
325
  // FIRST, before any other file of the toolkit is evaluated: a Node.js that is too old ends here, with the reason.
329
326
  import '../lib/node-version.mjs';
330
327
  import { spawn, spawnSync } from 'node:child_process';
@@ -362,8 +359,6 @@ import { agentsBrain, agentsBrainKey, agentsPass, agentsPasses, agentsRevoke, ag
362
359
  import { AgentSeat } from '../lib/agent-seat.mjs';
363
360
  import { shopCheck, shopConnect, shopDisconnect, shopInit, shopLines, shopOrders, shopRefund, shopStatus } from '../lib/shop.mjs';
364
361
  import { catalogLines, catalogPlan } from '../lib/shop-catalog.mjs';
365
- import { projectsLines, setupViaProjects } from '../lib/projects.mjs';
366
- import { projectsCloudflareEnv } from '../lib/projects-env.mjs';
367
362
  import { Feed, currentFeed, currentId, flushProgress, publicFeed, readFeed, recordChange, startProgress } from '../lib/progress.mjs';
368
363
  import { formatStatus, setupStatus } from '../lib/doctor.mjs';
369
364
  import { codexTarget, newCodex, writeCodexPage } from '../lib/codex.mjs';
@@ -384,22 +379,23 @@ import { standaloneDeployNotes } from '../lib/standalone.mjs';
384
379
 
385
380
  const argv = process.argv.slice(2);
386
381
  const flags = new Map();
387
- const BOOL_FLAGS = ['public', 'lan', 'off', 'revoke', 'json', 'yes', 'detach', 'no-install', 'plan', 'stop', 'share', 'apply', 'diff', 'template', 'ci', 'fresh', 'install', 'remove', 'replace', 'artifact', 'open', 'reopen', 'kids', 'remote-ai', 'dry-run', 'verify', 'maps', 'profile', 'hands-on', 'automatic', 'unlock', 'confirm', 'no-library', 'no-validate', 'rigged', 'no-rig', 'supporter', 'giving', 'managed', 'live', 'manual', 'renew', 'send', 'accept-tos', 'quiet', 'no-local-ai', 'timestamps', 'overwrite', 'own-route', 'before', 'release', 'device'];
382
+ // build --quick keeps compilation and type checks, skips rules generated play/smoke checks.
383
+ const BOOL_FLAGS = ['quick', 'public', 'lan', 'off', 'revoke', 'json', 'yes', 'detach', 'no-install', 'plan', 'stop', 'share', 'apply', 'diff', 'template', 'ci', 'fresh', 'install', 'remove', 'replace', 'artifact', 'open', 'reopen', 'kids', 'remote-ai', 'dry-run', 'verify', 'maps', 'profile', 'hands-on', 'automatic', 'unlock', 'confirm', 'no-library', 'no-validate', 'rigged', 'no-rig', 'supporter', 'giving', 'managed', 'live', 'manual', 'renew', 'send', 'accept-tos', 'quiet', 'no-local-ai', 'timestamps', 'overwrite', 'own-route', 'before', 'release', 'device'];
388
384
  const positional = [];
389
385
  for (let i = 0; i < argv.length; i++) {
390
386
  const a = argv[i];
391
387
  if (a.startsWith('--')) {
392
388
  const [k, v] = a.slice(2).split(/=(.*)/s, 2);
393
389
  if (v !== undefined) flags.set(k, v);
394
- else if (argv[i + 1] !== undefined && !argv[i + 1].startsWith('--') && !BOOL_FLAGS.includes(k)) flags.set(k, argv[++i]);
390
+ else if (argv[i + 1] !== undefined && !argv[i + 1].startsWith('--') && (!BOOL_FLAGS.includes(k) || k === 'profile' && argv[0] === 'shop')) flags.set(k, argv[++i]);
395
391
  else flags.set(k, true);
396
392
  } else positional.push(a);
397
393
  }
398
394
  const asJson = flags.has('json');
399
- const log = asJson ? () => {} : (line) => process.stderr.write(`${line}\n`);
395
+ const log = asJson && positional[0] !== 'build' ? () => {} : (line) => process.stderr.write(`${line}\n`);
400
396
 
401
397
  function print(result) {
402
- if (asJson) { process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); return; }
398
+ if (asJson || /^shop (plan|verify)$/.test(result.command ?? '')) { process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); return; }
403
399
  // A standalone plan or build says every target, built or not, and what a standalone copy does not have.
404
400
  if (/^standalone( |$)/.test(String(result.command ?? ''))) { process.stdout.write(`${standaloneLines(result).join('\n')}\n`); return; }
405
401
  // A check that found problems still prints them (its rows say what to fix), never a bare "failed".
@@ -635,9 +631,6 @@ function print(result) {
635
631
  case 'shop catalog':
636
632
  lines.push(...catalogLines(result));
637
633
  break;
638
- case 'setup':
639
- lines.push(...projectsLines(result));
640
- break;
641
634
  case 'chat rules':
642
635
  case 'chat budget':
643
636
  case 'lounge rules':
@@ -846,6 +839,8 @@ const OUTWARD_FLAGS = {
846
839
  'chat remove': ['url', 'all'], 'chat budget': ['url'],
847
840
  'servers close': ['url', 'reopen'], 'servers level': ['url'], 'servers member': ['url', 'role', 'remove'],
848
841
  'parts reissue': ['claim-hash', 'receipt-verified', 'buyer'], 'parts retire': [], 'parts keys': [], 'parts refund': ['game'],
842
+ 'shop listen': ['profile','port'],
843
+ 'shop webhook': ['live','renew','profile'],
849
844
  'shop connect': ['local', 'profile', 'url', 'managed', 'live', 'manual', 'renew', 'from-clipboard'], 'shop disconnect': ['url'], 'shop refund': ['url', 'reason', 'note', 'manual-transaction'],
850
845
  'agents pass': ['url', 'label', 'server', 'hands', 'role', 'days'], 'agents revoke': ['url'], 'agents brain': ['url', 'budget', 'remove'],
851
846
  'agents sit': ['url', 'server', 'pass', 'label', 'brain'],
@@ -936,12 +931,7 @@ async function main() {
936
931
  if (cmd === 'progress') return progressCommand(root, sub);
937
932
  if (cmd === 'domain') return studioDomain(root, sub);
938
933
  if (cmd === 'setup' && sub === 'attach') return setupAttach(root, positional[2], { homie: flags.get('homie'), client: flags.get('client') });
939
- if (cmd === 'setup' && flags.has('via')) {
940
- 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)' };
941
- const extra = String(flags.get('with') ?? '').split(',').map((x) => x.trim()).filter(Boolean);
942
- if (extra.some((x) => x !== 'elevenlabs')) return { ok: false, command: 'setup', why: '--with takes elevenlabs (Cloudflare is always set up)' };
943
- return setupViaProjects(root, { withElevenlabs: extra.includes('elevenlabs'), acceptTos: flags.has('accept-tos'), dryRun: flags.has('dry-run'), log });
944
- }
934
+ if (cmd === 'media' && sub === 'import') return importMedia(root, { file: flags.get('file'), receipt: flags.get('receipt'), out: flags.get('out'), usedBy: flags.get('used-by') });
945
935
  if (cmd === 'handoff') return handoff(root, sub, { homie: flags.get('homie'), client: flags.get('client') });
946
936
  if (cmd === 'port' && sub === 'import') return importPort(root, positional[2], flags.get('id'), { name: flags.get('name'), mode: flags.get('mode') });
947
937
  if (cmd === 'port' && sub === 'check') {
@@ -963,7 +953,7 @@ async function main() {
963
953
  // go, so an old note, an old card or an agent's memory of the toolkit does not end at "unknown command".
964
954
  if (cmd === 'game' && sub === 'remix') return { ok: false, command: 'game remix', why: 'remix was retired: a game is no longer handed over whole, and no studio serves one. Games build on each other through parts, the pieces of a game its studio chose to share: find one with `homie-studio parts find <words>` and bring it in with `homie-studio parts add` (in chat: parts_find, part_add; parts/PARTS.md)' };
965
955
  if (cmd === 'games') return { ok: true, command: 'games', games: listGames(root).map(({ dir, ...g }) => ({ ...g, dir: relative(root, dir) })) };
966
- if (cmd === 'build') return tracked(root, 'build', () => build(root, { only: positional[1] ?? null, log, maps: flags.has('maps'), types: flags.has('types'), longCheck: flags.has('long-check') }), 'build');
956
+ if (cmd === 'build') return tracked(root, 'build', () => build(root, { only: positional[1] ?? null, log, maps: flags.has('maps'), types: flags.has('types'), longCheck: flags.has('long-check'), quick: flags.has('quick') }), 'build');
967
957
  if (cmd === 'preview') return preview(root, positional[1] ?? (listGames(root).length === 1 ? listGames(root)[0].id : null));
968
958
  if (cmd === 'perf' && sub === 'sizes') return perfSizes(root, positional[2] ?? listGames(root)[0]?.id);
969
959
  if (cmd === 'lab') return labCommand(root, sub);
@@ -1114,7 +1104,15 @@ async function main() {
1114
1104
  const url = flags.get('url');
1115
1105
  if (!sub) return shopStatus(root, { url });
1116
1106
  if (sub === 'init') return shopInit(root, { supporter: flags.has('supporter'), giving: flags.has('giving'), currency: flags.get('currency') ?? null, price: flags.get('price') ?? 500, managed: flags.has('managed') });
1107
+ if(sub==='listen')return (await import('../lib/shop-listen.mjs')).shopListen(root,{profile:flags.get('profile'),port:flags.get('port')??8787,log});
1117
1108
  if (sub === 'check') return shopCheck(root);
1109
+ if (sub === 'plan' || sub === 'verify') {
1110
+ const {shopMcpPlan,saveMcpVerification}=await import('../lib/shop-mcp.mjs');
1111
+ const file=flags.get('have');
1112
+ const input=file?JSON.parse(readFileSync(file==='-'?0:file,'utf8')):{};
1113
+ return (sub==='verify'?saveMcpVerification:shopMcpPlan)(root,input);
1114
+ }
1115
+ if (sub === 'webhook') return shopConnect(root,{webhookOnly:true,live:flags.has('live'),renew:flags.has('renew'),profile:flags.get('profile'),log:line=>process.stderr.write(`${line}\n`)});
1118
1116
  if (sub === 'connect' && flags.has('local')) {
1119
1117
  if(flags.has('live'))throw new Error('Local checkout requires test mode');
1120
1118
  return (await import('../lib/shop-local.mjs')).shopConnectLocal(root,{url:flags.get('url'),profile:flags.get('profile')});
@@ -1362,7 +1360,7 @@ function mediaPut(root, file, as) {
1362
1360
  const key = as ?? (/^(music|videos)\//.test(rel) && !rel.includes('..') ? rel : `${folder}/${basename(file)}`);
1363
1361
  const bin = wranglerBin(root);
1364
1362
  return new Promise((done) => {
1365
- const p = spawn(bin, ['r2', 'object', 'put', `${r2}/${key}`, '--file', resolve(file), '--content-type', typeOf(file), '--remote'], { cwd: workerDir(root), env: { ...process.env, ...projectsCloudflareEnv(root), CI: '1' }, stdio: ['ignore', 'ignore', 'inherit'] });
1363
+ const p = spawn(bin, ['r2', 'object', 'put', `${r2}/${key}`, '--file', resolve(file), '--content-type', typeOf(file), '--remote'], { cwd: workerDir(root), env: { ...process.env, CI: '1' }, stdio: ['ignore', 'ignore', 'inherit'] });
1366
1364
  p.on('close', (code) => {
1367
1365
  if (code !== 0) return done({ ok: false, command: 'media put', why: 'wrangler r2 object put failed' });
1368
1366
  // The manifest is committed: it names the file by its path on the site, never the workers.dev address.
@@ -0,0 +1,68 @@
1
+ # Experimental MCP Events
2
+
3
+ Studio MCP implements the [Triggers & Events working group's design sketch](https://github.com/modelcontextprotocol/experimental-ext-triggers-events/blob/13e0a59749cde947374aaa8e3a65f0b424468d5a/docs/design-sketch-proposal.md) as inspected on 2026-10-14. This is an experimental draft, not an adopted MCP standard. Method names, envelopes, schemas, metadata keys and provisional error codes live in `worker/mcp-events-wire.mjs` so draft changes have one wire-format boundary. The studio's existing `resources/subscribe`, event resources and cursor-read tool continue to work.
4
+
5
+ Declare feeds in `studio.json` under `mcp.events`. Each map key is the extension's event `name`. For example:
6
+
7
+ ```json
8
+ {
9
+ "mcp": {
10
+ "events": {
11
+ "bookings": {
12
+ "event": "record.changed",
13
+ "app": "cafe",
14
+ "collection": "bookings",
15
+ "audience": { "app": "cafe", "role": "staff" },
16
+ "receivers": ["https://your-ai-receiver.example/hooks/studio"]
17
+ }
18
+ }
19
+ }
20
+ }
21
+ ```
22
+
23
+ `receivers` is an owner-controlled list of exact HTTPS callback URLs, without credentials or fragments. Include only endpoints whose operators have agreed to receive events. A feed's existing configured `webhook.url` is also accepted. Rebuild and deploy the declaration through the studio's usual authorized workflow. Poll and push need no receiver declaration. `maxSubscriptions`, when explicitly declared on a feed, limits its active extension webhooks; otherwise there is no subscription quota. Event arguments are currently `{}`: the declared feed already supplies the app, collection and audience filter.
24
+
25
+ ## An owner's AI connects and subscribes
26
+
27
+ 1. Use an MCP Events-capable client and connect to `<studio-site>/mcp/auth`. Approve with the person's existing studio account in the browser. Staff approve their own account. Do not request an office key. Initialization advertises `capabilities.events.listChanged: true`.
28
+ 2. Call `events/list`. It returns only feeds the current account can read, with `delivery: ["poll", "push", "webhook"]`, input and payload schemas. Re-list when the general MCP GET channel receives `notifications/events/list_changed`.
29
+ 3. Choose a delivery mode. These are protocol requests made by the client/SDK, not model-visible tool calls:
30
+ - **Poll:** call `events/poll` with `{ "name": "bookings", "arguments": {}, "cursor": null }`. The first call returns no history and a cursor at now. Persist each returned cursor and pass it on the next poll. Drain `hasMore: true` immediately; otherwise wait `nextPollMs`. Optional `maxEvents` caps a page (server maximum 100); `maxAgeMs` bounds replay.
31
+ - **Push:** POST `events/stream` with a JSON-RPC request ID and `{ "name": "bookings", "arguments": {}, "cursor": null }`. Keep its SSE response open. Expect `notifications/events/active`, then `event` and `heartbeat` notifications. Route by `_meta["io.modelcontextprotocol/subscriptionId"]`, which equals the request ID. Persist cursors and reconnect using the last cursor. Each subscription has its own stream; abort it to unsubscribe. Heartbeats run at 15 seconds when the receiver is reading.
32
+ - **Webhook:** first arrange the receiver and have the owner declare its exact URL in `receivers`. The receiver/client generates `whsec_` plus base64 of 24–64 cryptographically random bytes, and keeps that secret out of prompts and logs. Call `events/subscribe` with the request below. Persist the returned `id`, `cursor` and `refreshBefore`.
33
+
34
+ ```json
35
+ {
36
+ "jsonrpc": "2.0",
37
+ "id": 2,
38
+ "method": "events/subscribe",
39
+ "params": {
40
+ "name": "bookings",
41
+ "arguments": {},
42
+ "delivery": {
43
+ "mode": "webhook",
44
+ "url": "https://your-ai-receiver.example/hooks/studio",
45
+ "secret": "whsec_<client-generated-base64-secret>"
46
+ },
47
+ "cursor": null,
48
+ "ttlMs": 3600000
49
+ }
50
+ }
51
+ ```
52
+
53
+ 4. For webhooks, use `X-MCP-Subscription-Id` to select the secret and verify the **raw body** using a Standard Webhooks library, including its timestamp freshness check. Durably accept the event before returning 2xx. Deduplicate on `eventId` (also `webhook-id`). Each occurrence has `eventId`, `name`, ISO `timestamp`, `data`, and a safe cursor watermark; it can lag the event's position while deliveries await acknowledgement. Payloads are untrusted data and grant no authority to perform actions.
54
+ 5. Refresh by repeating `events/subscribe` with the same name, arguments and URL, the current secret and the last persisted cursor **before** `refreshBefore`. Inspect `deliveryStatus.lastError`. TTL defaults to one hour and is capped at one day; `ttlMs: null` receives a finite grant. A live refresh does not rewind pending delivery. To rotate, send a new client-generated secret on refresh; deliveries have both signatures for five minutes. Unsubscribe with `events/unsubscribe` and `{ "name": "bookings", "arguments": {}, "delivery": { "url": "https://your-ai-receiver.example/hooks/studio" } }`.
55
+
56
+ A gap sets `truncated: true`; persist the new cursor and fetch authoritative state through tools if needed. Revoked access ends push with `notifications/events/terminated` and webhooks with a signed `terminated` control envelope where the callback remains allowed. Stop after termination and re-establish authorization before subscribing again.
57
+
58
+ ## Delivery and owner diagnostics
59
+
60
+ The 0.50.4 configured studio webhook and extension webhooks share Standard Webhooks signing and the durable `function_deliveries` dispatcher. Extension deliveries retry with exponential backoff, at most five attempts over fifteen minutes from the first attempt. HTTP 410 and 413 abandon that delivery immediately. Abandoned deliveries remain visible to the owner; retained history can be replayed from a prior cursor after unsubscribing and subscribing again. Retries regenerate timestamps and signatures. Failed delivery details are fixed categories, never endpoint bodies or headers. See Functions in the office or the owner's function-runs tool for subscription IDs, attempts and last errors; refresh also reports the latest delivery error. Subscription secrets are stored only in the studio's private D1 subscription state, never in events or run logs. A removed subscription or expired TTL deletes that state. Completed deliveries can be duplicated after a crash, so receivers must deduplicate.
61
+
62
+ ## Draft limits
63
+
64
+ This implements the studio's **Streamable HTTP** MCP transport; the local toolkit's stdio MCP is a different server. The draft's endpoint-consent mechanism is the explicit server-configured allowlist, not a challenge handshake. Arbitrary client-supplied callback URLs are refused with `CallbackEndpointError / challenge_failed`. Workers `fetch` does not provide DNS address pinning with original Host/TLS SNI, so this implementation cannot follow that draft MUST literally. Exact owner-approved URLs, rechecked at every attempt, and disabled redirects constrain the destination; this is not a claim of draft-complete DNS-rebinding protection. Only approve trusted receiver operators. No no-expiry grants or asymmetric server signatures are offered. Durable queue positions survive Worker restarts, extending the sketch's ephemeral in-flight bookkeeping while preserving client-owned replay cursors. A webhook's conservative safe watermark may lag the event being delivered by one event.
65
+
66
+ ## Production failures from Cloudflare
67
+
68
+ An owner's AI can also subscribe through the person's **existing Cloudflare connection** to Cloudflare's own API MCP at `https://mcp.cloudflare.com/mcp`: discover `cloudflare.alert.workers_observability_real_time_issue` with `events/list`, then use `events/subscribe` with that descriptor's arguments scoped to the studio's Worker and the client's webhook. Enable Workers Issues on that Worker and grant **Notifications Write** and **Workers Observability Write**. Cloudflare posts signed events for new or recurring production issues, so reliability problems reach the AI without polling. Homie hosts nothing for this connection. See [Cloudflare's 2026-10-13 announcement](https://developers.cloudflare.com/changelog/post/2026-10-13-cloudflare-mcp-events/).
@@ -183,7 +183,7 @@ Scaffold businesses with `new <folder> --kind business`; causes with `--kind cau
183
183
 
184
184
  For giving, set the studio currency and run `shop init --giving --currency CAD` (use the organisation’s currency). It creates one-time/monthly suggested gifts. The owner supplies `giving.receiptText` in shop.json; Homie supplies no legal or tax text. Before Stripe is connected, local /shop/ previews items and prices with checkout disabled. Run the connection step yourself; the owner approves their Stripe account in the browser.
185
185
 
186
- Use `shop.book` and the booking collection declaration in SHOP.md. Default keyless booking links show pending while paying; an unpaid guest does not block others. Stripe limits completed sessions to capacity. The owner’s AI syncs published/changed slots with `stripe_bookings_sync`. Say “Pay to secure your seat.” Exclusive unpaid holds require the explicitly selected optional keyed connection.
186
+ Use `shop.book` and the booking collection declaration in SHOP.md. Default keyless booking links show pending while paying; an unpaid guest does not block others. Stripe limits completed sessions to capacity. The owner’s AI syncs published/changed slots with `stripe_shop_plan` and the complete slot manifest. Say “Pay to secure your seat.” Exclusive unpaid holds require the explicitly selected optional keyed connection.
187
187
 
188
188
 
189
189
  ## Tell my AI when a booking comes in
@@ -288,3 +288,27 @@ Upgrade compares copied app presets with recorded toolkit versions. Unchanged
288
288
  preset files and default values take the new version; custom scene files, names and
289
289
  edited labels remain the studio’s. An app wall uses its join label rather than game
290
290
  instructions. Rebuild after upgrade and inspect both the wall and giving page.
291
+
292
+ ## Experimental MCP Events
293
+
294
+ For an owner's AI that should hear a studio's declared events, read
295
+ `node_modules/@homie-rocks/studio/docs/mcp-events.md`. Connect the client to
296
+ `<site>/mcp/auth` through the person's existing studio approval, discover feeds
297
+ with `events/list`, and use `events/poll`, an SSE `events/stream`, or
298
+ `events/subscribe` with a client-generated `whsec_` secret and an exact
299
+ owner-declared `mcp.events.<feed>.receivers` URL. Refresh before `refreshBefore`,
300
+ persist cursors, verify Standard Webhooks signatures, deduplicate event IDs, and
301
+ use `events/unsubscribe` to stop a webhook. The feeds keep their current roles;
302
+ resources/subscribe and the cursor tool still work. This follows the experimental
303
+ Triggers & Events working-group draft, with the Workers DNS-pinning limitation
304
+ and receiver allowlist explained in that guide. Inspect Functions in the office
305
+ and refresh's `deliveryStatus` for failed deliveries; never put secrets in logs.
306
+
307
+ For production failures, the owner's AI can also use the person's existing
308
+ Cloudflare connection to `https://mcp.cloudflare.com/mcp`, discover
309
+ `cloudflare.alert.workers_observability_real_time_issue` with `events/list`, and
310
+ subscribe with `events/subscribe` scoped to the studio's Worker. Enable Workers
311
+ Issues and grant Notifications Write and Workers Observability Write. Cloudflare
312
+ posts signed events to the client's webhook so new or recurring reliability
313
+ problems reach the AI without polling; Homie hosts nothing for this. See
314
+ [Cloudflare's announcement](https://developers.cloudflare.com/changelog/post/2026-10-13-cloudflare-mcp-events/).