@homie-rocks/studio 0.48.4 → 0.49.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.
Files changed (60) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/app-starters/gathering/README.md +4 -0
  3. package/app-starters/gathering/app.json +162 -0
  4. package/app-starters/gathering/index.html +3 -0
  5. package/app-starters/gathering/map/main.json +1 -0
  6. package/app-starters/gathering/src/move.ts +3 -0
  7. package/app-starters/gathering/src/rules.ts +21 -0
  8. package/app-starters/gathering/src/view.ts +74 -0
  9. package/app-starters/giving/README.md +5 -0
  10. package/app-starters/giving/app.json +162 -0
  11. package/app-starters/giving/index.html +3 -0
  12. package/app-starters/giving/map/main.json +1 -0
  13. package/app-starters/giving/src/move.ts +3 -0
  14. package/app-starters/giving/src/rules.ts +21 -0
  15. package/app-starters/giving/src/view.ts +74 -0
  16. package/app-starters/jobs/README.md +4 -0
  17. package/app-starters/jobs/app.json +162 -0
  18. package/app-starters/jobs/index.html +3 -0
  19. package/app-starters/jobs/map/main.json +1 -0
  20. package/app-starters/jobs/src/move.ts +3 -0
  21. package/app-starters/jobs/src/rules.ts +21 -0
  22. package/app-starters/jobs/src/view.ts +74 -0
  23. package/app-starters/quiz/README.md +4 -0
  24. package/app-starters/quiz/app.json +162 -0
  25. package/app-starters/quiz/index.html +3 -0
  26. package/app-starters/quiz/map/main.json +1 -0
  27. package/app-starters/quiz/src/move.ts +3 -0
  28. package/app-starters/quiz/src/rules.ts +21 -0
  29. package/app-starters/quiz/src/view.ts +74 -0
  30. package/app-starters/welcome/app.json +25 -2
  31. package/app-starters/welcome/index.html +1 -1
  32. package/app-starters/welcome/src/view.ts +20 -8
  33. package/bin/homie-studio.mjs +15 -11
  34. package/guides/app/SKILL.md +72 -0
  35. package/guides/parts/SKILL.md +158 -0
  36. package/guides/shop/SKILL.md +149 -0
  37. package/guides/studio-setup/SKILL.md +419 -0
  38. package/lib/authoring-guides.mjs +5 -4
  39. package/lib/build.mjs +5 -4
  40. package/lib/check-site.mjs +6 -6
  41. package/lib/check.mjs +8 -1
  42. package/lib/dev.mjs +4 -2
  43. package/lib/doctor.mjs +1 -1
  44. package/lib/handoff.mjs +13 -8
  45. package/lib/mcp-tools.mjs +10 -9
  46. package/lib/parts.mjs +6 -6
  47. package/lib/scaffold.mjs +35 -9
  48. package/lib/shop-links.mjs +2 -2
  49. package/lib/shop.mjs +5 -3
  50. package/lib/studio.mjs +5 -4
  51. package/lib/template-history.json +42 -0
  52. package/package.json +4 -4
  53. package/worker/app-format.mjs +1 -1
  54. package/worker/build-id.mjs +1 -1
  55. package/worker/index.mjs +2 -1
  56. package/worker/parts.mjs +4 -4
  57. package/worker/shop-page.mjs +21 -15
  58. package/worker/shop-rules.mjs +4 -3
  59. package/worker/site.mjs +4 -2
  60. package/worker/version.mjs +1 -1
@@ -1,4 +1,7 @@
1
1
  import * as THREE from 'three';
2
+ import app from '../app.json';
3
+ const words: Record<string, string> = app.words;
4
+ const word = (key: string, values: Record<string, string | number> = {}) => String(words[key] ?? '').replace(/\{(\w+)\}/g, (_, k: string) => String(values[k] ?? ''));
2
5
  import { damp1 } from '@homie-rocks/camera/spring.js';
3
6
  import { chamferBox } from '@homie-rocks/geom/chamfer.js';
4
7
  import { openRoom } from '@homie-rocks/studio/rules/view';
@@ -22,23 +25,28 @@ let loaded = false;
22
25
  let light = 0;
23
26
  let refreshing = false;
24
27
  join.hidden = role !== 'customer'; next.hidden = role !== 'staff';
25
- if (role === 'wall') { $('h1').innerHTML = 'You’re in<br>good company.'; $('#intro').textContent = 'A warm welcome, one person at a time. Scan to find your place.'; }
26
- if (role === 'staff') { $('h1').innerHTML = 'Make someone’s<br>day brighter.'; $('#intro').textContent = 'Your welcome desk. Call the next person when you are ready.'; }
28
+ $('h1').textContent = role === 'wall' ? words.wallTitle || app.name : role === 'staff' ? words.staffTitle || app.name : app.name;
29
+ $('#intro').textContent = app.blurb;
30
+ $('.eyebrow').textContent = words.eyebrow;
31
+ $('footer').textContent = words.footer;
32
+ join.textContent = words.joinAction; next.textContent = words.nextAction;
33
+ $('#waiting-word').textContent = words.waiting;
34
+ $('.panel').setAttribute('aria-label', words.panel);
27
35
  function paint() {
28
36
  const waiting = rows.filter((r) => r.data.status === 'waiting');
29
37
  const called = rows.filter((r) => r.data.status === 'called').at(-1);
30
38
  const own = rows.find((r) => r.id === mine);
31
39
  $('[data-count]').textContent = String(waiting.length);
32
- $('#panel-label').textContent = role === 'customer' ? (own ? 'Your ticket' : 'Welcome in') : 'Now welcoming';
33
- $('#ticket').textContent = role === 'customer' ? own ? `No. ${own.data.label}` : 'Make yourself at home.' : called ? `No. ${called.data.label}` : 'Ready when you are.';
34
- $('#detail').textContent = own?.data.status === 'called' ? 'It’s your turn. Come on over!' : own ? `You have a place. ${Math.max(0, waiting.findIndex((r) => r.id === mine))} ahead of you.` : role === 'customer' ? 'No account needed. Just a place in line.' : waiting.length ? `${waiting.length} ${waiting.length === 1 ? 'person is' : 'people are'} waiting for a welcome.` : 'The next arrival will appear here.';
40
+ $('#panel-label').textContent = role === 'customer' ? (own ? words.yourTicket : words.welcome) : words.panel;
41
+ $('#ticket').textContent = role === 'customer' ? own ? word('ticket', { label: own.data.label }) : words.home : called ? word('ticket', { label: called.data.label }) : words.ready;
42
+ $('#detail').textContent = own?.data.status === 'called' ? words.called : own ? word('ahead', { n: Math.max(0, waiting.findIndex((r) => r.id === mine)) }) : role === 'customer' ? words.guest : waiting.length ? word('waitingDetail', { n: waiting.length, people: waiting.length === 1 ? words.one : words.many }) : words.empty;
35
43
  $('.panel').classList.toggle('called', own?.data.status === 'called' || (role !== 'customer' && !!called));
36
44
  join.hidden = role !== 'customer' || !!own;
37
45
  join.disabled = pending || !loaded; next.disabled = pending || !waiting.length;
38
46
  }
39
47
  async function refresh() {
40
48
  if (refreshing) return; refreshing = true;
41
- try { rows = await records.list(); loaded = true; $('#status').textContent = ''; paint(); }
49
+ try { rows = await records.list(); loaded = true; net.playable(); $('#status').textContent = ''; paint(); }
42
50
  catch (e) { $('#status').textContent = (e as Error).message; }
43
51
  finally { refreshing = false; }
44
52
  }
@@ -55,7 +63,7 @@ join.onclick = () => action(async () => {
55
63
  // Build a customer link from a clean address, never from a private staff URL.
56
64
  const origin = new URL((window as any).HOMIE_NET.url.replace(/^ws/, 'http')).origin;
57
65
  const url = appLink(`/${(window as any).HOMIE_NET.url.split('/')[3]}/open`, { ticket: mine, room: (window as any).HOMIE_NET.room }, origin);
58
- $('#ticket-qr').innerHTML = qrSvg(url, { title: 'Keep your ticket' });
66
+ $('#ticket-qr').innerHTML = qrSvg(url, { title: words.keepTicket });
59
67
  parent.postMessage({ type: 'homie-app-ticket', ticket: mine }, '*');
60
68
  });
61
69
  next.onclick = () => action(async () => {
@@ -65,11 +73,12 @@ next.onclick = () => action(async () => {
65
73
  // The room carries scene state and change notices; records always come from the authorized database.
66
74
  room.on('refresh', () => { void refresh(); });
67
75
  net.on('link', () => { void refresh(); });
68
- setInterval(() => { $('#connection').textContent = net.link === 'online' ? 'Here together · live' : 'Reconnecting…'; }, 500);
76
+ setInterval(() => { $('#connection').textContent = net.link === 'online' ? words.live : words.reconnecting; }, 500);
69
77
  setInterval(() => void refresh(), 2000); // recover a missed notice; no dependency on a trusted browser host
70
78
  void refresh(); paint();
71
79
 
72
80
  // A single sculptural scene. Queue arrivals unfold the lanterns; the camera follows your context.
81
+ function sceneStart() {
73
82
  const renderer = new THREE.WebGLRenderer({ canvas: $('canvas'), antialias: true, alpha: false });
74
83
  renderer.setPixelRatio(Math.min(devicePixelRatio, 1.5)); renderer.setClearColor('#122f38');
75
84
  renderer.outputColorSpace = THREE.SRGBColorSpace;
@@ -107,3 +116,6 @@ function draw(now: number) {
107
116
  renderer.render(scene, camera);
108
117
  }
109
118
  requestAnimationFrame(draw);
119
+
120
+ }
121
+ try { sceneStart(); } catch { $('canvas').hidden = true; const fallback = document.createElement('p'); fallback.dataset.webglFallback = ''; fallback.textContent = words.fallback; $('header').append(fallback); net.playable(); }
@@ -13,7 +13,7 @@
13
13
  * this toolkit as a local MCP server (stdio): the Claude desktop app's Homie
14
14
  * extension, Claude Code or any MCP client builds in the same chat that shows
15
15
  * the cards (lib/mcp.mjs); --studios: the folder the studios live in
16
- * homie-studio app new <id> [--name "<App Name>"]
16
+ * homie-studio app new <id> [--from welcome|gathering|giving|quiz|jobs] [--name "<App Name>"]
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
@@ -178,7 +178,7 @@
178
178
  * (what the guides' brain would decide in that moment, with no seat taken;
179
179
  * spent from the guides' day; --model compares a model before setting it)
180
180
  * homie-studio shop is the studio's shop selling (its own Stripe), and if not, what is missing
181
- * homie-studio shop init [--supporter] [--currency usd] [--price 500] [--managed] shop.json and SELLING.md
181
+ * homie-studio shop init [--supporter|--giving] [--currency <code>] [--price 500] [--managed] shop.json and SELLING.md
182
182
  * homie-studio shop check shop.json against studio settings and provider requirements
183
183
  * 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
184
184
  * restricted Stripe key; with it this makes the webhook (0.24.3), and the key
@@ -326,7 +326,7 @@
326
326
  // FIRST, before any other file of the toolkit is evaluated: a Node.js that is too old ends here, with the reason.
327
327
  import '../lib/node-version.mjs';
328
328
  import { spawn, spawnSync } from 'node:child_process';
329
- import { existsSync, readFileSync, statSync } from 'node:fs';
329
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, statSync } from 'node:fs';
330
330
  import { basename, join, relative, resolve } from 'node:path';
331
331
  import { build } from '../lib/build.mjs';
332
332
  import { PREVIEW_PORT, previewServer } from '../lib/preview.mjs';
@@ -347,7 +347,7 @@ import { R2_COST, lineOf, mediaPlan, r2OverOf, recordUpload, resolveMedia, sizeO
347
347
  import { newStudio } from '../lib/scaffold.mjs';
348
348
  import { lineDiff, upgradeApply, upgradePlan } from '../lib/upgrade.mjs';
349
349
  import { whatsNewLines } from '../lib/changelog.mjs';
350
- import { listGames, newGame, newApp, readStudio, requireStudio, siteUrl, starters, workerDir } from '../lib/studio.mjs';
350
+ import { listExperiences, listGames, newGame, newApp, readStudio, requireStudio, siteUrl, starters, workerDir } from '../lib/studio.mjs';
351
351
  import { STUDIO_VERSION } from '../lib/version.mjs';
352
352
  import { statsKey, statsLink, statsRevoke, statsShare, statsShow } from '../lib/stats.mjs';
353
353
  import { playersOwner, playersShow } from '../lib/players.mjs';
@@ -382,7 +382,7 @@ import { standaloneDeployNotes } from '../lib/standalone.mjs';
382
382
 
383
383
  const argv = process.argv.slice(2);
384
384
  const flags = new Map();
385
- 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', 'managed', 'live', 'manual', 'renew', 'send', 'accept-tos', 'quiet', 'no-local-ai', 'timestamps', 'overwrite', 'own-route', 'before', 'release', 'device'];
385
+ 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'];
386
386
  const positional = [];
387
387
  for (let i = 0; i < argv.length; i++) {
388
388
  const a = argv[i];
@@ -436,7 +436,7 @@ function print(result) {
436
436
  lines.push(`Landing /${l.id}/: hero from ${l.hero === 'footage' ? 'its footage' : l.hero === 'art' ? 'its art (moving)' : 'the studio\'s colours (it has no picture or footage yet)'}${l.credits ? ', credits' : ''}`);
437
437
  // Which files, and what shape: the names the landing looks for (lib/site.mjs HERO_NAMES). Any size is shown,
438
438
  // cropped to fill; these are the sizes that fill a computer and an upright phone without waste.
439
- if (l.hero === 'colours') lines.push(` give it one: games/${l.id}/hero/wide.jpg (16:9, 1280×720 or larger) and games/${l.id}/hero/tall.jpg (9:16, 720×1280, for an upright phone); footage beside them as hero/wide.mp4 and hero/tall.mp4 (the same shapes, a silent loop of 8 to 15 s, under 3 MB each); or "cover" in its game.json`);
439
+ if (l.hero === 'colours') lines.push(` give it one: ${l.kind === 'app' ? 'apps' : 'games'}/${l.id}/hero/wide.jpg (16:9, 1280×720 or larger) and ${l.kind === 'app' ? 'apps' : 'games'}/${l.id}/hero/tall.jpg (9:16, 720×1280, for an upright phone); footage beside them as hero/wide.mp4 and hero/tall.mp4 (the same shapes, a silent loop of 8 to 15 s, under 3 MB each); or "cover" in its ${l.kind === 'app' ? 'app' : 'game'}.json`);
440
440
  }
441
441
  if (result.posts?.length) lines.push(`Posts: ${result.posts.map((x) => `/posts/${x}/`).join(', ')} (feeds: /posts/feed.xml, /posts/feed.json)`);
442
442
  if (result.pages?.length || result.partials?.length) lines.push(`The studio's own: ${[...(result.pages ?? []).map((x) => `page ${x}`), ...(result.partials ?? []).map((x) => `partial ${x}`)].join(', ')}`);
@@ -785,6 +785,7 @@ function print(result) {
785
785
  `Written: ${result.file}`);
786
786
  break;
787
787
  case 'check':
788
+ if (result.kind === 'app') { lines.push(`PASS: app ${result.game ?? ''}: wall and two phones shared an action; reconnect ${result.reconnect ? 'passed' : 'failed'}.`, ` ${result.before} → ${result.after}`); break; }
788
789
  if (result.results) { for (const r of result.results) lines.push(`${r.game}: PASS at ${r.site}: room ${r.room}, round ${r.round?.n} (${r.round?.humans} humans, ${r.round?.bots} bots), connection ${r.uninterrupted === true ? 'uninterrupted' : r.uninterrupted === false ? 'INTERRUPTED' : 'unknown'}`); break; }
789
790
  if (result.site) lines.push(`Checked ${result.local ? 'this studio\'s own build' : 'the site'} at ${result.site}.`);
790
791
  lines.push(`PASS: two fresh browsers in room ${result.room} finished round ${result.round.n} (${result.round.humans} humans, ${result.round.bots} bots) in ${Math.round(result.totalMs / 1000)} s.`,
@@ -874,7 +875,7 @@ async function main() {
874
875
  if (cmd === 'version' || flags.has('version')) return { ok: true, command: 'version', version: STUDIO_VERSION };
875
876
  const strange = unknownFlags(positional, flags);
876
877
  if (strange) return strange;
877
- if (cmd === 'new') return newStudio(positional[1], { name: flags.get('name'), homie: flags.get('homie'), slug: flags.get('slug'), install: !flags.has('no-install'), template: flags.has('template') });
878
+ if (cmd === 'new') return newStudio(positional[1], { name: flags.get('name'), homie: flags.get('homie'), slug: flags.get('slug'), install: !flags.has('no-install'), template: flags.has('template'), kind: flags.get('kind') ?? 'game' });
878
879
  if (cmd === 'starters') return { ok: true, command: 'starters', starters: starters() };
879
880
  // The floor's built-in words need no studio: anyone can read what a studio's chat always holds.
880
881
  if (cmd === 'chat' && sub === 'words') return chatWords();
@@ -905,7 +906,7 @@ async function main() {
905
906
  const root = requireStudio();
906
907
  // Keep guidance visible to AI callers using --json as well as human output.
907
908
  if (['build', 'check', 'upgrade'].includes(cmd)) {
908
- for (const line of (await import('../lib/authoring-guides.mjs')).authoringWords()) process.stderr.write(`${line}\n`);
909
+ for (const line of (await import('../lib/authoring-guides.mjs')).authoringWords({ kind: ['app', 'business', 'cause'].includes(readStudio(root).kind) || listExperiences(root).some(g => g.kind === 'app') ? 'app' : 'game' })) process.stderr.write(`${line}\n`);
909
910
  }
910
911
  if (cmd === 'function') return (await import('../lib/functions-cli.mjs')).functionCommand(root, sub, positional, flags, { log });
911
912
  if (cmd === 'tool') return (await import('../lib/tools-cli.mjs')).toolCommand(root, sub, positional, flags, { log });
@@ -946,7 +947,7 @@ async function main() {
946
947
  if (!/^[a-z0-9][a-z0-9-]{0,39}$/.test(id ?? '') || !/^[a-z][a-z0-9-]{0,39}$/.test(role ?? '')) throw new Error('app role <id> <role> [--player <account>] [--revoke] [--url <site>]');
947
948
  return withKey(root, flags.get('url'), (call) => call(`/${id}/api/app/roles`, { role, ...(flags.get('player') ? { player: flags.get('player') } : {}), revoke: flags.has('revoke') }));
948
949
  }
949
- if (cmd === 'app' && sub === 'new') return newApp(root, positional[2], { name: flags.get('name') });
950
+ if (cmd === 'app' && sub === 'new') return newApp(root, positional[2], { name: flags.get('name'), from: flags.get('from') ?? 'welcome' });
950
951
  if (cmd === 'game' && sub === 'new') return newGame(root, positional[2], { from: flags.get('from') ?? 'gem-rush', name: flags.get('name') });
951
952
  // Remix was retired (a game is never handed over whole): the command is refused in a sentence that says where to
952
953
  // go, so an old note, an old card or an agent's memory of the toolkit does not end at "unknown command".
@@ -996,6 +997,9 @@ async function main() {
996
997
  const shots = flags.get('shots') ? resolve(flags.get('shots'), ...(games.length > 1 ? [game] : [])) : null;
997
998
  const r = await tracked(root, 'checks', (report) => check({ url: site.url, game, shots, log, report }), 'check');
998
999
  results.push({ ...r, game, site: site.url, local: site.local });
1000
+ const receiptDir = join(root, '.studio', 'checks');
1001
+ mkdirSync(receiptDir, { recursive: true });
1002
+ writeFileSync(join(receiptDir, 'latest.json'), JSON.stringify({ ...r, game, site: site.url, at: new Date().toISOString() }, null, 2));
999
1003
  if (!r.ok) break;
1000
1004
  }
1001
1005
  return results.length === 1 ? results[0] : { ok: results.every((r) => r.ok), command: 'check', site: site.url, local: site.local, why: results.find((r) => !r.ok)?.why, results };
@@ -1099,7 +1103,7 @@ async function main() {
1099
1103
  if (cmd === 'shop') {
1100
1104
  const url = flags.get('url');
1101
1105
  if (!sub) return shopStatus(root, { url });
1102
- if (sub === 'init') return shopInit(root, { supporter: flags.has('supporter'), currency: flags.get('currency') ?? 'usd', price: flags.get('price') ?? 500, managed: flags.has('managed') });
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') });
1103
1107
  if (sub === 'check') return shopCheck(root);
1104
1108
  if (sub === 'connect') return shopConnect(root, { managed: flags.has('managed') ? true : null, live: flags.has('live'), manual: flags.has('manual'), fromClipboard: flags.has('from-clipboard'), renew: flags.has('renew'), log: (line) => process.stderr.write(`${line}\n`) });
1105
1109
  if (sub === 'disconnect') return shopDisconnect(root);
@@ -1222,7 +1226,7 @@ async function tracked(root, stage, run, command) {
1222
1226
  const note = stage === 'build' ? `Built ${(result.games ?? []).map((g) => `${g.id} (${Math.round(g.bytes / 1024)} KB)`).join(', ') || 'the site'}`
1223
1227
  : stage === 'deploy' ? `Live: ${result.url ?? 'deployed'}`
1224
1228
  : result.command === 'port check' ? `${result.passed.length} passed${result.skipped.length ? `, ${result.skipped.length} skipped` : ''} in ${Math.round(result.totalMs / 1000)} s`
1225
- : `Room ${result.room}: round ${result.round?.n} finished with ${result.round?.humans} people in ${Math.round(result.totalMs / 1000)} s`;
1229
+ : result.kind === 'app' ? `App ${result.game ?? ''}: wall and two phones, shared action and reconnect passed` : `Room ${result.room}: round ${result.round?.n} finished with ${result.round?.humans} people in ${Math.round(result.totalMs / 1000)} s`;
1226
1230
  feed.stage(stage, 'done', note);
1227
1231
  if (stage === 'deploy') {
1228
1232
  const doc = feed.doc;
@@ -0,0 +1,72 @@
1
+ ---
2
+ name: app
3
+ description: "Make or change a Homie app: one morphing screen, roles and reusable parts, live across phones, wall screens, kiosks and staff tablets. Use for customer apps, venue screens, queues, workshop boards and cause dashboards in a studio with studio.json. Apps use the same engines, rooms, parts and standalone builds as games, without compulsory rounds, scores, bots or players."
4
+ compatibility: Node 22 and Chrome; the studio's pinned toolkit and Wrangler.
5
+ ---
6
+
7
+ # Make an app
8
+
9
+ An app is **one screen, its roles, and its parts**. The screen transforms with the person and the work: a camera moves, a ticket unfolds, controls appear in context. Do not build page-to-page navigation, a sidebar of links, or a conventional admin website inside it. A studio's public website can still have pages around the app.
10
+
11
+ Read the person's brief and existing `apps/<id>/` first. Choose sensible engineering defaults from the brief; never ask the person how to implement shared state. A business or cause needs no game demo, mandatory entertainment, invented customers, scores or rounds. Never impose a shop, directory listing, sign-in for public visitors, or paid media. Make the fewest human steps possible; run commands yourself.
12
+
13
+ ## Start and compose
14
+
15
+ - Run `homie-studio app new <id> --from <starter> --name "<Name>"`, or `app_make`. Starters: `welcome` (queue), `gathering` (check-in/task choice), `giving` (goal and giving intentions linked to the shop), `quiz` (casual quiz night), `jobs` (service booking requests). The welcome starter is one 3D scene with customer, wall and staff roles. Install its reported `needsAdded`, then build.
16
+ - `apps/<id>/app.json` describes roles (`signIn`, `can`), surfaces (`phone`, `wall`, `kiosk`, `tablet`), wording, record collections, parts and the real UI action used by check. The app's entry and assets live beside it, just like a game.
17
+ - Before inventing each component, inspect engine packages for the mechanism and `parts_find` for the piece. A queue, quiz, menu board, live total, music loop or video intro can be a part. Record its licence and credit. Sharing happens only when requested; selling parts is not part of this workflow.
18
+ - Use the same engine packages as a game: `@homie-rocks/geom` and `props` for original scenery; `render`, `camera`, `postfx`, `fx` for light, composition and motion; `input`, `ui`, `ui-world`, `audio`, `device`, `loop` for interaction and feel. An app can be fully 3D and wild. Legibility and the actual task come first.
19
+ - Reach for the `style`, `art`, `models`, `animate`, `sound`, `music`, `video` and `perf` skills where they help. It should look like a game studio made it: a coherent world with a useful HUD, not generic cards and navigation. Media cost and licensing decisions remain the person's.
20
+ - Keep native buttons at least 44 px, keyboard focus visible, text as text, motion reducible, and the wall QR clear of useful information. Pointer gesture guards belong on the canvas, not native click buttons.
21
+
22
+ ## Roles and records
23
+
24
+ For transient shared live state, use the same server rules rooms as games. Read
25
+ [the rules reference](../game/RULES.md). Declare `src/rules.ts`, `src/move.ts`, `src/view.ts`, and app.json `room: { host: 'server' }`
26
+ with `entry: 'src/view.ts'`. Each interactive visitor has a player entity containing
27
+ its cursor/selection; commands validate actions in that entity, then sendRoom to
28
+ update shared bounded board/poll/scene state. Walls watch without a seat. Omit bots,
29
+ scores and automatic rounds; use `rounds: { seconds: 0, breakSeconds: 0 }`. The current contract requires a body for each participant: use a circle with radius 0.1,
30
+ maxSpeed 0 and a no-op defineMove handler for a stationary board or poll. This is a
31
+ logical seat anchor, not a visible avatar; never add fake movement to pass a test. Include a small map/main.json with bounds even for stationary seats. Draw through openRoom, and acknowledge a button
32
+ locally while waiting for shared outcomes. Keep animations and camera in the view.
33
+
34
+ Do not add rules to an app that only needs the records service and local UI.
35
+ The welcome starter demonstrates authorized records plus a server-rules scene with
36
+ stationary participants, change effects and a shared revision. Existing apps keep working. Use the manual extraction steps in [REWRITE.md](../game/REWRITE.md)
37
+ for existing live state, adapting game-only tests to the app's workflow.
38
+
39
+ Transport host/replica/screen roles are not staff permission. Never trust a role
40
+ parameter or hidden button to authorize an operational change. Rules snapshots
41
+ are public, rooms have at most 32 seats, and end after the last person leaves;
42
+ they are neither private business storage nor an authorization service.
43
+
44
+ Use `createAppRecords` from `@homie-rocks/studio/apps` for lasting records. Declare each collection's fields and every role's read/create/update/delete capabilities. The Worker validates writes and checks the signed-in account on every privileged request. Updates use a version; reload and resolve a conflict instead of overwriting somebody else's edit. A room may empty without deleting these records. Transient effects and camera state remain in netplay.
45
+
46
+ Public collections reach everyone allowed to read them. A ticket number or filtering the HUD is not privacy. Keep personal/private records in a different collection whose read permission excludes customers and walls. Do not put contact details in the starter's public queue.
47
+
48
+ `homie-studio app role <id> staff --player <account-id> --url <site>` grants an existing signed-in account and returns its private role address. Without `--player` it returns the owner's role address. The existing owner check protects issuance; the link alone grants nothing. `--revoke` removes a named account's grant. Staff use the studio's existing account/passkey sign-in. Keep these links private.
49
+
50
+ The complete helper and limits are in `node_modules/@homie-rocks/studio/apps/APPS.md`. Import `appLink`, `qrSvg`, and `randomId` from `@homie-rocks/studio/links` for a ticket URL with parameters. Build customer links from a clean public address, never by copying a staff URL. `randomId` works on plain HTTP.
51
+
52
+ ## Prove and deliver
53
+
54
+ 1. `homie-studio build --types` and `homie-studio dev --lan`. Open the printed Wi-Fi address for phones and the wall. Localhost QR codes cannot reach a phone. Passkeys need HTTPS or localhost; do not bypass staff authorization for HTTP.
55
+ 2. `homie-studio check <id> --url <origin> --shots .checks`: a wall and two phones connect, a declared real UI action changes all screens, and a reload retains it. In `app.json`, `check.action` is its CSS selector and `check.observe` is a shared text value that changes as a consequence. Use an actual workflow, no fake QA-only action.
56
+ 3. Also prove the app's own role workflow with staff signed in, unauthorized writes refused, records after all screens close, and layout in phone portrait/landscape, tablet and wall. Use the `playtest` skill for visual and interaction review, adapting game-only criteria to the app's purpose.
57
+ 4. Customer store builds use the existing `standalone` skill/command with the app id. The same bundle and public records work through the wrapper; its existing account, billing and store-upload limitations still apply. Never promise store approval.
58
+ 5. Give the person `/<id>/open`, `/<id>/tv`, a real screenshot and the measured proof. Publish only within their authorization through the existing publish workflow. Do not turn a request for an app into an unwanted deployment or directory listing.
59
+
60
+ ## Connect an AI to this studio
61
+
62
+ Use Studio functions (`functions/FUNCTIONS.md` in the toolkit) for durable reactions to `record.changed`, `order.paid`, refunds, schedules and tool calls. `homie-studio function new <name> --event <type>` creates a plain TypeScript file; local `function fire` and the office delivery table make it testable. Deduplicate external effects by event ID. Public or priced tools can expose app services to customer AIs using the same purchase engine.
63
+
64
+ Every studio serves its own MCP at `<site>/mcp`. Use the `tools` skill for studio-defined business actions, signed integrations and staff AI access. Reuse office and app-record built-ins first. After an authorized deploy, hand over the MCP address and `<site>/_studio/office/connections`: the person approves with the studio account they already have, with no key to copy. Staff connect their own account and retain only their app grants. Client connector setup is separate from the studio approval. Never deploy merely to set up a local tool test.
65
+
66
+ ## Public pages and giving
67
+
68
+ Scaffold businesses with `new <folder> --kind business`; causes with `--kind cause` (MCP `studio_scaffold` uses `kind`). The header links custom `site/pages/<slug>/index.html` pages and an existing Shop. A page at `site/pages/<app-id>/index.html` takes over only the landing: use `/<id>/open` for phones and `/<id>/tv` for the wall. Build says when this happens. In-app links to public pages should use `target="_blank" rel="noopener"` because the app is sandboxed. App assets and hero files belong under `apps/<id>/`, including `apps/<id>/hero/`.
69
+
70
+ 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.
71
+
72
+ Booking requests are not capacity reservations. Calendar availability and seat capacity are the next app capability; do not promise them.
@@ -0,0 +1,158 @@
1
+ ---
2
+ name: parts
3
+ description: Studios build on each other with parts from games, apps, music and video that their owners choose to share or sell (a creature, a level generator, a chase camera, a bot brain, a pickup mechanic, an audio pack). Use this when planning or making games, apps, music or video (look for pieces before writing one from scratch), when the person asks what parts exist, says "use the X from that game", "make this reusable" or "share this", and before a game that uses parts goes online.
4
+ compatibility: Node 22. Paid purchases need @homie-rocks/studio 0.37.2 or later. A studio made with @homie-rocks/studio; npm installs the packages a part builds on.
5
+ ---
6
+
7
+ **Apps:** For a business, venue, cause or customer app, follow the `app` skill: `apps/<id>/app.json`, one morphing screen, roles and parts. Reuse these engines and workflows; do not impose game rounds, scores, bots, a game demo or page navigation. The app check proves shared actions and reconnect; app stores use the same standalone command.
8
+
9
+
10
+ # Studio parts: games, apps, music and video
11
+
12
+ A studio is the folder with `studio.json`. Studios build on each other with **parts**: reusable pieces of games, apps, music and video that their owners choose to share or sell. A creature from one game, a level from
13
+ another, a bot brain from a third. Without parts you would have to come up with everything on your own.
14
+
15
+ The rules, in full in `node_modules/@homie-rocks/studio/parts/PARTS.md`:
16
+
17
+ 1. Game and app lifting extracts a piece, **never the whole experience**. App components can also be lifted from `apps/<id>/`; music loops or tracks and video may be authored directly in `parts/<id>/`; their licence controls permitted use, including venue use.
18
+ 2. A part can take **any form** that suits the piece: code, assets, data, a JSON description, a tuned config, a mix.
19
+ 3. **Private until shared**: on purpose, with a licence (an SPDX identifier) and an attribution. A part records the
20
+ game and studio it came from.
21
+ 4. **Homie packages are not parts.** `@homie-rocks/*` packages are general mechanisms from npm. A part may say
22
+ which packages it builds on; npm installs them.
23
+ 5. **The person works in chat** and never types a command. Four tools: `parts_find`, `part_add`, `part_new`,
24
+ `part_share`.
25
+ 6. **You look for parts first.**
26
+
27
+ There is no mash-up command: mashing up is making a game with parts from several games.
28
+
29
+ ## 1. Look first
30
+
31
+ When planning or making a game, app, music or video, for each system it needs (camera, movement, bots, pickups, effects, sound, UI,
32
+ environment, characters), before writing it from scratch:
33
+
34
+ 1. Check the `@homie-rocks/*` packages the studio has for the general mechanism.
35
+ 2. `parts_find` with the words for the piece (`"chase camera"`, `"cave"`, `"pickups"`), and `kind`, `tag`,
36
+ `license` or `builds` (a package or a skeleton it builds on) to narrow it.
37
+ 3. Tell the person what you found, a line each, **naming the source work and studio each piece is from**, with your pick.
38
+
39
+ If the catalogue cannot be reached, the tool says so. That is not "nothing exists": say it could not be read, go on
40
+ with what the studio has, and look again later. If it answered and nothing fits, write the piece.
41
+
42
+ Record in the project notes or CODEX, under **Built from**, what came from where: the packages, each part with its game,
43
+ studio and licence, and what you wrote from scratch and why.
44
+
45
+ ## 2. Use a part
46
+
47
+ `part_add` with `"<studio site>/<part id>"` (as `parts_find` gave it; `@1.2.0` for one version) and the `game` option (a game or app id; omit it for standalone media). It
48
+ fetches one exact version, checks every file's integrity before anything is written, copies it into the studio
49
+ (the studio's to tune from then on), records where it came from, adds the credit to the game, and lets npm install
50
+ the packages it builds on. Read what it answers:
51
+
52
+ - **The import line**: `import … from '@parts/<studio site>/<part id>'`. Its `part.json` says what the piece takes
53
+ (`contract`) and who decides in a room; its `tuning.json` holds the values to tune.
54
+ - **Packages**: npm's own words. If a package the studio already names does not fit, nothing was changed: tell the
55
+ person which part wants what.
56
+ - **Other parts it names**: each with what adds it.
57
+ - **Licences that cannot be combined** are said plainly. Tell the person before building on it.
58
+ - **The credit** in the game's `credits.json`: keep it, whatever else changes.
59
+
60
+ Adding the same part again later brings the newer version: it shows what changed and keeps `tuning.json`. A file
61
+ the studio edited is not replaced unless you pass `overwrite`: ask the person first.
62
+
63
+ Then build, and try it on two devices.
64
+
65
+ ## 3. Make a part
66
+
67
+ After building a piece another game could use, offer it in one line: "The chase camera turned out reusable. Shall
68
+ I make it a part, so your next game can use it? It stays private unless you ask to share it."
69
+
70
+ `part_new` with `id`, `from` (the game or app) and `files` (the module or folder of that piece). The files are lifted out
71
+ into `parts/<id>/`, the game imports them from there, and **the game still builds and plays the same**: build it
72
+ and check. A file that reaches into the rest of the game is refused by name: pass what it needs in as an argument
73
+ (a collision function, a palette, the room), then lift it. The game's own entry cannot be lifted: a part is never
74
+ the whole game.
75
+
76
+ Then fill `parts/<id>/part.json`: a one-sentence `summary`, tags, what it takes from a game (`contract`), the
77
+ packages it builds on (`requires.packages`), its scale and skeleton if it has them, costs only if measured, a small
78
+ `preview/index.html` that shows it, and its tunables in `tuning.json`.
79
+
80
+ When the preview is something to play with, declare its shape in `preview`: `"aspect": "4:3"` and
81
+ `"phoneAspect": "3:4"` (whole numbers 1 to 32, no flatter than 3:1, no taller than 1:2). The default is 16:9, which
82
+ on a 390 px phone is a frame about 197 px tall: the controls fit and the demo under them is cut off, with no error.
83
+
84
+ ## 4. Share a part, only when the person asks
85
+
86
+ Ask which licence (`CC-BY-4.0` for "use it, credit me", `CC0-1.0` for "no strings", `MIT` for code) and who to
87
+ credit. `part_share` with `share: true`, `license` and `attribution`. It refuses in plain words when there is no
88
+ SPDX licence, credit is owed and nobody is named, the part does not say where it came from, or the rights to a
89
+ file are unknown (for example a model the studio bought): leave that file out or replace it, never work around it.
90
+ `part_share` with no `share` only checks.
91
+
92
+ It is **live after the studio's next deploy** (`studio_deploy`), not before: say so. Then the studio's site lists
93
+ it and the catalogue on homie.rocks shows it if the studio is listed there. `share: false` stops it at the next
94
+ deploy. A shared version never changes: a change needs a new `version`, then share again.
95
+
96
+ ## 5. Before a game that uses parts goes online
97
+
98
+ The deploy plan (`studio_deploy` with `plan: true`) and the listing (`studio_publish`, and its `before: true`)
99
+ show each part's licence and attribution and say plainly
100
+ when licences cannot be combined: two share-alike licences in one game, a non-commercial part in a studio with a
101
+ shop, a no-derivatives part that was edited. Tell the person, and settle it before deploying.
102
+
103
+ ## Never
104
+
105
+ - Share a part the person did not ask to share, or pick its licence for them.
106
+ - Copy a piece from another studio by hand: `part_add` is what checks it and writes the credit.
107
+ - Remove or reword a credit a part wrote.
108
+ - Treat a package as a part, or a whole game as one.
109
+
110
+ ## Paid parts: use the person's wallet
111
+
112
+ Needs studio **0.36.0** or later. Read the full settings and recovery procedure in
113
+ `node_modules/@homie-rocks/studio/parts/PARTS.md`.
114
+
115
+ Show the exact quote, currency, quantity, recurring interval, licence, refund and update
116
+ terms from `part_add`. Ask first. Spending authority means the person's yes to this price in this conversation,
117
+ or a standing instruction that names a spending limit and covers these terms. A quote for a new release must show its new price. Never approve a
118
+ hash beside an earlier purchase's price.
119
+
120
+ Use the person's installed open wallet client: `HOMIE_PARTS_WALLET=link-cli` or `purl`. `part_add` invokes it after exact-quote approval. Money goes directly to the
121
+ selling studio's Stripe account. Wallet authority and limits belong to that wallet;
122
+ never create another spending mechanism or disable its controls. Link currently requires
123
+ the person to approve each card spend in their wallet app. Give them the returned wallet
124
+ approval link, then retry `part_add` after approval. This is not a seller checkout page.
125
+
126
+ Checkout is the fallback when there is no compatible wallet, for recurring purchases,
127
+ and where the protocol cannot quote the final total. On this fallback the person must
128
+ accept the licence and operate Pay themselves. Never submit licence acceptance through
129
+ a browser tool. A base price before tax is not Stripe's final total.
130
+
131
+ Keep private purchase records and verified backups private. Never give a catalogue the
132
+ claim or download token. A proof is evidence, not a download credential. First delivery
133
+ and updates depend on the seller; an existing verified backup restores offline.
134
+
135
+ Use `parts refund` only when requested. Never pass `--receipt-verified` on the assistant's
136
+ own word: the seller must confirm matching the Stripe receipt and payer to the order.
137
+ Never approve an unknown buyer's test order. Test downloads require owner approval of
138
+ the exact claim, and do not become paid licences when the shop goes live.
139
+
140
+
141
+ Machine purchases use the seller's Stripe account. Only studios with paid releases need the
142
+ additional payment permissions. Connect and scheduled readiness use read-only provider calls;
143
+ readiness does not prove a funded settlement. Tempo is disabled; the offered methods are card
144
+ shared payment tokens and Base, subject to the seller's configuration and Stripe eligibility.
145
+ Respect the wallet owner's limits, including x402's default payment cap. A decline can use a
146
+ fresh approved token or the hosted fallback. An uncertain outcome must be reconciled first.
147
+
148
+ The office lists unresolved orders and their provider identifiers. Use Reconcile payments now;
149
+ a five-minute scheduled job also reads existing payments. After 23 hours it stops replaying
150
+ charges but continues reading. If paid files cannot be restored, the owner must refund manually.
151
+ For an unrecorded Base payment, send the exact refund from the seller address to the original
152
+ payer after the purchase, then record its hash with `shop refund <order> --manual-transaction
153
+ <hash>`. The chain must confirm the sender, recipient, amount and time before access ends.
154
+
155
+
156
+ For app, music and video parts, use `part_new` without `from`, choose any suitable kind label, put the licensed files in its folder, and supply the source as `{kind, id, studio}` in `from`. Preserve existing game manifests and imports. Hash and share through the usual tools. No new app build framework is implied.
157
+
158
+ Selling defaults to the keyless shop: `shop connect` uses the official Stripe CLI approval, creates Payment Links and the webhook, and installs the signing key privately. One approval if needed, zero for an already connected profile. Machine MPP/SPT and MCP charges and the Stripe-recorded x402 path need the fuller Worker credential. Read the payment-path table in PARTS.md before setup. With browser access, operate Stripe's key page for the owner, let them complete only provider verification prompts, click Copy, and run `shop connect --from-clipboard`. Never ask the owner to find or paste a key when this alternative is available. The command reads and clears the clipboard privately. Do not print its contents. No real-account verification is claimed by the release's stand-in tests.
@@ -0,0 +1,149 @@
1
+ ---
2
+ name: shop
3
+ description: Sell things in a Homie studio's games with the studio's OWN Stripe - a supporter pack, cosmetics, a season pass, a one-time unlock, a tip - in real money, with Stripe Checkout, Stripe Tax or Stripe Managed Payments, refunds from the office, and optional studio policies with an open default; set up with Stripe's own agent tools (Stripe's MCP server and skills) with one browser approval and Payment Links by default, no human-handled key and no API key in the Worker. Use when someone asks to sell something, add a shop or store, take payments or donations, make money from a game, set up or connect Stripe, make the products in Stripe, asks "how are sales?", about tax or Managed Payments, to add a supporter badge, refund a player, or about chargebacks, referral shares or affiliate links between studios.
4
+ compatibility: The studio's own current toolkit with stripe_login or shop connect --renew, Wrangler, and the official Stripe CLI. Stripe's own agent plugin (MCP and skills) is optional for catalog work and analytics.
5
+ metadata:
6
+ providers: stripe
7
+ ---
8
+
9
+ Customer AIs can buy through the studio's own `/mcp`. Read the toolkit's `tools/TOOLS.md`
10
+ and use the `tools` skill for `studio_catalogue`, cart quotes, standard agent payments,
11
+ private purchase claims, and human Stripe checkout approval. Priced studio tools sell
12
+ services through that same purchase engine; `shop connect` synchronizes their Payment Links.
13
+ Keep orders, grants and refunds in the existing shop. For order/refund follow-up code, use
14
+ Studio functions (`functions/FUNCTIONS.md`), with the event ID as the idempotency key.
15
+
16
+
17
+ # The shop
18
+
19
+ Check that the toolkit exposes `stripe_login`, or that `homie-studio --help` lists `shop connect` with `--renew` and `--manual`. An older toolkit's default is the paste page: upgrade before using this flow; do not send the owner to that old default.
20
+ The guide is `node_modules/@homie-rocks/studio/shop/SHOP.md`; the owner's plain words are `SELLING.md`.
21
+
22
+ **The model, said plainly to the person.** The studio sells with **its own Stripe account**. The studio is the
23
+ seller: its prices, its refunds, its disputes, its tax. Money goes straight from players to the studio's Stripe;
24
+ **homie.rocks never sees it, holds it or moves it, and Homie takes no cut.** The studio is responsible for the law where it sells and for Stripe's terms.
25
+
26
+ In a rules game, keep shop UI in the view. Do not turn a client character save, an
27
+ entity field or a command into proof of a purchase: grants still come from the shop's
28
+ verified server records. Room state is replicated to players; keep payment and private
29
+ customer data out of it. Read the game skill's RULES.md before changing game outcomes.
30
+
31
+ ## Connect with Stripe's own browser approval
32
+
33
+ Use `stripe_login` (MCP) or `npx --no-install homie-studio shop connect`. Test mode and a
34
+ sandbox first. If the CLI is missing, run the one installation command the result gives:
35
+ `npm install -g @stripe/cli@latest`. The toolkit opens Stripe's page and completes its official
36
+ non-interactive login. Give the person the pairing code; they select their own account and approve.
37
+ Use `studio_job` for the background result. Do not call a raw CLI config command: it may print secrets.
38
+
39
+ The default is **keyless**: the toolkit uses the CLI's approved account to sync Products, Prices
40
+ (including custom amounts for tips), Payment Links and the webhook. It captures the signing secret
41
+ privately and installs it with the link configuration; it never exports the CLI credentials.
42
+ The Worker redirects buyers to Stripe and grants purchases from signed events. No desktop agent,
43
+ Homie service or API key is needed when someone pays. Run connect again after editing shop.json;
44
+ it updates changed objects and archives removed ones. Existing connections need no new approval.
45
+
46
+ Only when the person asks for combined carts, spending reservations, custom checkout expiry,
47
+ automatic recovery of lost webhooks or office refunds, explain one sentence:
48
+ “The fuller connection adds combined carts, spending limits, automatic recovery and refunds here.”
49
+ A pre-existing Worker key selects that path automatically. Never silently select `--manual` or ask
50
+ for a key: explain its remaining Dashboard step only if the person explicitly chooses that route.
51
+ Before relying on spending restrictions after switching, deactivate old Payment Links with the approved CLI and resolve in-flight checkouts; a credential change does not cancel them.
52
+ `shop connect` selects keyless again and removes an existing Worker API key after saving the links.
53
+
54
+ ## Never
55
+
56
+ - Never ask a person for a Stripe key, a webhook secret or a password in chat. Never display a CLI
57
+ config, credential response, environment value or raw login-completion output. The toolkit captures
58
+ these privately. Keys belong to the studio; no Homie account or service takes payments.
59
+ - **Never make a webhook endpoint or an event destination through Stripe's MCP**: its signing secret
60
+ would enter the conversation. `shop connect` handles endpoint creation and secret storage privately.
61
+ - Never silently select `--manual`. Only when the owner chooses the fallback, run
62
+ `shop connect --manual` (or `--manual --live`) and give the local page link. The page explains the
63
+ remaining Dashboard restricted-key step and creates the webhook automatically. This fallback
64
+ requires copying a key and does not meet the one-approval target; say so plainly.
65
+ - Live mode only when the owner says go live. Account activation, tax, bank details and Stripe
66
+ approval links are the owner's. Never approve a Stripe confirmation link for them.
67
+ - The studio chooses its settings. Omitted policies are open; choose a preset only when requested.
68
+
69
+ ## The owner's steps, for a creator with a fresh Stripe account
70
+
71
+ With an existing Stripe account and deployed studio, there is **one Stripe approval** for the CLI;
72
+ if that CLI profile is already connected, there are **zero new approvals**. An existing MCP-only
73
+ consent is not a CLI login and is not exported or borrowed. Account creation/activation, an administrator
74
+ enabling CLI access, and an initial Cloudflare sign-in are separate Stripe/hosting requirements.
75
+
76
+ | The owner | You |
77
+ |---|---|
78
+ | Approves Stripe's browser page, selecting their studio account and sandbox. | Create/check shop.json; run stripe_login / shop connect; follow studio_job to completion. |
79
+ | Nothing. | Sync all items and the webhook, install the signing secret and links privately, then verify a test purchase and grant. |
80
+ | Requests a refund in Stripe or asks their connected AI. | Use the official Stripe connection, honor any confirmation link, verify the signed refund event removes the item. |
81
+ | Says “go live” and completes Stripe activation if needed. | shop connect --live in the authorized account; verify only an authorized real purchase. |
82
+
83
+ `shop connect --renew` restarts the approval flow when needed. CLI OAuth refresh is the CLI's job;
84
+ there is no runtime API credential to expire. `shop catalog` remains the optional older catalog planner
85
+ for keyed shops; keyless shops use connect for the complete catalog, links and webhook sync.
86
+
87
+ ## Optional Stripe agent tools
88
+
89
+ For catalog work, analytics and advice, `stripe agent setup` installs Stripe's own agent plugin and
90
+ skills; its OAuth MCP is `https://mcp.stripe.com`. This is optional and separate from Worker credentials.
91
+ Check `get_stripe_account_info` before writes; confirm the account and mode match the studio.
92
+ For CLI refunds or event resends, use the non-secret `profile` returned by connect with `stripe --project-name <profile>`; preserve its test/live context. Do not read credentials.
93
+ The tools include `stripe_api_read`, `stripe_api_write`, API discovery and `stripe_analytics`.
94
+ From **2026-10-31**, MCP rejects API keys without the Agent tag; OAuth remains supported. Agent keys
95
+ can put refunds behind Stripe approval. A returned confirmation link belongs to the owner; wait
96
+ for approval before retrying. Do not silently replace the payment runtime with MCP; a separate persistent MCP client would need its own production verification.
97
+
98
+ ## Do
99
+
100
+ | The person says | Run | What happens |
101
+ |---|---|---|
102
+ | "Sell a supporter pack for $5" | `npx --no-install homie-studio shop init --supporter` (then `shop check`) | `shop.json` with a US$5 Supporter pack (a badge on their profile and beside their name in rooms, for a year; it changes nothing about play) and `SELLING.md`. Commit both. |
103
+ | "Sell a skin / a season pass / the full game" | edit `shop.json` `items` (`kind`: `cosmetic`, `pass`, `unlock`; `price` in Stripe currency units; `gives`: the keys the game reads), `shop check`, then `shop connect` again | Kinds are studio labels, without a fixed list. `"advantage": true` follows the studio policy for beginner and kids servers. Homie sets no price ceiling. |
104
+ | "Let people tip" | an item `{ "kind": "tip", "price": "choose", "min": 200, "max": 5000 }` | Pay what you want; amounts follow Stripe currency requirements, and max is optional and chosen by the studio. |
105
+ | "Make the products in Stripe" | `shop connect` | Idempotent Product, Price, Payment Link and webhook sync from shop.json; no secret enters tool output. |
106
+ | "Connect my Stripe" / "turn the shop on" | `stripe_login` or `shop connect` | Approve Stripe once if needed; the Worker receives links and the signing secret only. Follow the single next step returned. Live only when requested. |
107
+ | "Is tax set up?" | `stripe_api_read` `GET /v1/tax/settings`, and `GET /v1/tax/registrations` | Seller "stripe": Stripe Tax needs `status: active` (the business address in Settings, Tax), or checkouts fail; with no registrations it collects no tax anywhere: say so, the accountant decides where to register. Seller "stripe-managed": Stripe files the tax; the connect page's test checkout said whether Managed Payments is on. Change nothing yourself. |
108
+ | "Is the shop working?" | `npx --no-install homie-studio shop` | Open (test or live) or exactly what is missing, the last 30 days, the webhook address. |
109
+ | "How are sales?" / "Show me the sales" | `shop` and `shop orders` first (the studio's own books); with Stripe's MCP, read-only: `stripe_analytics`, or `stripe_api_read` on `/v1/balance`, `/v1/payouts`, `/v1/checkout/sessions` | Counts, money and payouts in a few lines; never a buyer's name, email or card. Never a write to answer a question. The owner's `/_studio/office/shop` links every Stripe page and gives the accountant a CSV. |
110
+ | "Refund that player" | The owner’s official Stripe connection, or link their Stripe Dashboard payment | Honor Stripe’s confirmation link; signed events update the books and revoke fully refunded items. The keyed way still offers office refunds. |
111
+ | "Someone charged back" | nothing to undo: the owner answers it in Stripe (the office links it) | While open nothing changes; lost: that one item goes; won: it stays. **The account is never locked or deleted over a dispute.** |
112
+ | "Show the item in the game" / "the supporter badge" | in the game: `createShop()` from `@homie-rocks/studio/shop`; `shop.has('skin:ember')`, `shop.on('change', …)`, `shop.open('ember-skin')` from a button the player pressed, `shop.used(key)` when equipped | The play shell answers for the signed-in player; a badge rides on their seat as `peer.badge` (the Worker sets it, never a hello). |
113
+ | "Pay studios that send us players" / "affiliate links" | `shop.json` `referrals` (rate, window, hold), `shop statements [--send]` | A `?via=<host>` link from another site (homie.rocks is one more referrer, on the same terms) counts for a new player's purchases; statements are signed with the studio's key; the referrer invoices the studio; the owner pays and marks it paid (an ASK from you). Nothing moves through Homie. |
114
+
115
+ ## Your shop, your choices
116
+
117
+ The default is open: guests buy repeatedly without an age question, with any wording or kind, from any page.
118
+ `shop init` writes no policy. A released shop file without a preset now uses the open default.
119
+ Write `"policy": { "preset": "protective" }` to retain earlier account and age behavior.
120
+ `adults-only` is another optional bundle. Neither scans content. SHOP.md lists individual overrides.
121
+
122
+ The studio can set items, quantities, prices, tip ranges, entitlements, sale dates, currency, optional
123
+ `automaticTax`, `capPerPlayerMonth`, `refundDays`, referral terms and configurable flood protection.
124
+ No toolkit amount or duration ceiling applies. Stripe currency constraints and safe integer arithmetic apply.
125
+ A refund window includes used items unless `policy.refundUsedItems` is false. A player cannot refund a tip;
126
+ the owner can refund through Stripe; keyed shops also offer office refunds. Guest purchases stay owned when the buyer signs in later.
127
+
128
+ For keyed shops, carts: `shop.add(item, quantity, amount?)`, then `shop.checkout()`; `shop.buy(item)` buys directly.
129
+ Free and paid lines share one order and one Stripe Session. The studio chooses where to open the shop.
130
+
131
+ ## The first sale (the acceptance)
132
+
133
+ In test mode: buy the supporter pack on a phone with Stripe's test card `4242 4242 4242 4242`, see "It's yours",
134
+ see the badge on the account page and beside the name in a room (from the next room the player joins), refund it
135
+ in Stripe (or through the connected AI), and see the badge go. With the protective preset, a kids server's room shows no shop and `/<game>/tv` shows only a code.
136
+
137
+ Keyless purchases buy one item type at a time, with quantity confirmed on Stripe. Tips choose the
138
+ amount on Stripe and always have quantity one. Free orders grant locally without Stripe.
139
+ Refund and dispute snapshots are stored by provider object ID and tolerate retries and reordering.
140
+ A missing webhook stays pending: ask Stripe to resend the event (Dashboard or official CLI
141
+ `stripe events resend <event> --webhook-endpoint <endpoint>`). Never grant based on the return URL.
142
+ Payment Links are public and reusable; do not promise that local sale dates, repeat-purchase or
143
+ eligibility checks revoke a previously opened link. Deactivation stops new sessions after re-sync;
144
+ already opened sessions may still complete. Strict pre-payment enforcement needs the keyed way.
145
+ Stripe always enables Adaptive Pricing on Payment Links; the buyer can use the original currency.
146
+ Keyless has no per-session expiry or provider-read reconciliation. The keyed path retains both,
147
+ carts, cap reservations, local refunds. Managed Payments is supported on Payment Links when Stripe enables it for the account. The studio chooses its needs.
148
+
149
+ For a cause, use `shop init --giving --currency <organisation currency>`: one-time and monthly suggested gifts, no badge or game access. Read `giving.receiptText` from the organisation; leave it empty until provided. Local /shop/ previews before Stripe. Monthly gifts use the existing keyless Payment Links connection and Stripe handles renewals.