@homie-rocks/studio 0.29.0 → 0.30.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,7 +5,7 @@ What changed in each release of Homie's open parts, newest first:
5
5
  - **`@homie-rocks/studio`**: the `homie-studio` command, your studio's site Worker, its multiplayer rooms (the
6
6
  netplay contract), the port kit and the starter games. Every version is on npm, and Homie for Claude Desktop
7
7
  (`homie-studio-<version>.mcpb` on each GitHub release) is built from it.
8
- - **The Homie plugin** for Claude Code and Codex: its skills. It ships from this repository's marketplace with its
8
+ - **The Homie plugin** for Claude Code, Codex and Grok: its skills. It ships from this repository's marketplace with its
9
9
  own version number, given beside each studio version below.
10
10
 
11
11
  Each version says what you can now do (**Added**), what works differently (**Changed**), what was broken and is
@@ -17,9 +17,52 @@ To bring a studio up to date, tell Claude: "Upgrade my studio to the newest Homi
17
17
  `npx -y @homie-rocks/studio@latest upgrade`, which shows what's new since the version your studio pins (from this
18
18
  file) and what the upgrade would change, and changes nothing until you agree.
19
19
 
20
+ ## [0.30.1] - 2026-10-04
21
+
22
+ **Plugin 0.31.1** · [#47](https://github.com/homie-rocks/homie/pull/47)
23
+
24
+ Fixes from a filmed first run in the Claude desktop app: Claude tells you once to choose Always allow and what to do when a step sits on a spinner, the deploy plan's card fills in, and a starter is changed in a few large edits instead of twenty small ones.
25
+
26
+ ### Added
27
+
28
+ - `file_edit` (Homie for Claude Desktop, and any client of `homie-studio mcp`) takes every change to a file in one call: `edits`, a list of `{ old, new }` applied in order, all of them or none. A miss says which edit it was and leaves the file as it was. One change with `old` and `new` works as before.
29
+ - The build card draws a deploy plan: what going online creates on your own Cloudflare, what is there already and what it costs, before anything is made.
30
+
31
+ ### Changed
32
+
33
+ - In the Claude desktop app, Claude says once, in its first words to you: the app asks before each Homie Studio tool, **Always allow** lets a build run without a click at every step (the filmed first build made about fifty tool calls), and if a step sits on a spinner with nothing to click, a request is waiting out of sight: ⌘ Return (Ctrl+Enter on Windows) allows it, and scrolling the chat can bring it into view. Claude also writes a line of text between a card and its next tool call, because a request that came straight after a card is the one the app has failed to draw. Only the desktop extension says any of this (its manifest starts the server with `HOMIE_STUDIO_EXTENSION=1`): in Claude Code, Codex, Grok and other MCP clients it is not true, and nothing is said.
34
+ - The README's desktop section, `desktop/README.md` and the extension's install screen say the same in a sentence.
35
+ - Fewer, larger edits. The `game` skill, the note above every guide in `homie-studio mcp`, `game_make` and `game_plan` ask the AI to read a file once and make every change to it in one call. A first build spent about twenty single edits on one starter file and ran into the app's limit of tool calls in a turn, so the person had to say "keep going".
36
+ - `setup_status` takes the name of a studio that is not made yet as `studio`: the card and the checklist are then the new studio's.
37
+
38
+ ### Fixed
39
+
40
+ - The deploy plan's card stayed on a grey "Loading" skeleton for good. `studio_deploy` with `plan: true` answers with a plan, not a build, and the build card knew only builds. A card whose tool answered with something else (a plan, a job still running, plain words) now shows that answer; "Loading" is only for a tool that has not answered yet.
41
+ - With one studio already in the Studios folder, the setup card for a second studio led with the first one's name and checklist.
42
+
43
+ ### Upgrade notes
44
+
45
+ - Homie for Claude Desktop: install this version's `homie-studio.mcpb` over the one you have. A studio needs no change.
46
+
47
+ ## [0.30.0] - 2026-10-03
48
+
49
+ **Plugin 0.31.0** · [#45](https://github.com/homie-rocks/homie/pull/45) · [release-2026-10-03-studio-0.30.0](https://github.com/homie-rocks/homie/releases/tag/release-2026-10-03-studio-0.30.0)
50
+
51
+ Grok can set up a studio and run it with the same holds as Claude Code and Codex.
52
+
53
+ ### Added
54
+
55
+ - `homie-studio setup attach <hs_…> --client grok` and `homie-studio handoff <hb_…> --client grok`. A Grok Bot in the studio folder checks the repository in. The directory is told the chat is Grok. Claude and Codex pass `--client claude` or `--client codex`; with no `--client` the words stay "the chat".
56
+ - Homie's hooks for Grok Build (`plugins/homie/.grok-plugin/plugin.json`, `hooks/grok.json`, `hooks/grok.mjs`). They ask `hooks/lib/holds.mjs`, the module the Claude Code mod and the Codex hooks already ask, so the three hold the same calls with the same words. A held call waits for your own `proceed <code>`. Secrets are taken out of what Grok reads.
57
+ - A new studio's home band says **Connect this chat**, for Claude, Codex or Grok. Grok has no Cloudflare connector: approving Cloudflare in the browser is still your step.
58
+
59
+ ### Changed
60
+
61
+ - The plugin manifests (Claude Code, Codex, Grok and `plugin.json`) name plugin 0.31.0 and say the same thing. `plugins/homie/test/manifests.test.mjs` checks the Grok manifest with the others.
62
+
20
63
  ## [0.29.0] - 2026-10-03
21
64
 
22
- **Plugin 0.30.0** · [#44](https://github.com/homie-rocks/homie/pull/44)
65
+ **Plugin 0.30.0** · [#44](https://github.com/homie-rocks/homie/pull/44) · [release-2026-10-03-studio-0.29.0](https://github.com/homie-rocks/homie/releases/tag/release-2026-10-03-studio-0.29.0)
23
66
 
24
67
  The Lounge: a community room on your own studio, with play nights, moderators, live rooms and "show what you made" cards, and chat history you can turn on.
25
68
 
package/README.md CHANGED
@@ -83,8 +83,8 @@ The button copies the public template (`template/` in this repository, exactly w
83
83
  writes) into the person's GitHub, makes the Worker, the D1 database and the rooms on their Cloudflare account,
84
84
  and connects Workers Builds. The site goes live with its own home page ("First game coming soon": no starter game
85
85
  is put in a studio unless the person asks for one), and its Home has a
86
- **Connect to Claude** band: one tap links it to the Claude chat that set it up (the Homie MCP tool `studio_setup`).
87
- In the Claude Code session that works in the repository, `homie-studio setup attach <hs_…>` then gives the studio
86
+ **Connect this chat** band: one tap links it to the chat that set it up (the Homie MCP tool `studio_setup`).
87
+ In the session that works in the repository, `homie-studio setup attach <hs_…> [--client claude|codex|grok]` then gives the studio
88
88
  the name chosen in the chat and removes the band.
89
89
 
90
90
  **The site claims itself in the directory.** The homie.rocks directory lists a studio only when its site serves
@@ -173,10 +173,12 @@
173
173
  * into it (stage, each check going green, a preview picture) and stop
174
174
  * when asked. Without one, nothing changes.
175
175
  *
176
- * homie-studio handoff <hb_…> (a Claude Code session started from the Claude app with one line, "Continue building
176
+ * homie-studio handoff <hb_…> [--client claude|codex|grok]
177
+ * (a session started from the chat with one line, "Continue building
177
178
  * <Studio>: build hb_…": fetch the person's brief, check in for a new studio, take
178
179
  * the build so the chat's card follows it, and print the steps; HANDOFF.md)
179
- * homie-studio setup attach <hs_…> (this repository is the studio the Claude app's setup card is making: say so, once,
180
+ * homie-studio setup attach <hs_…> [--client claude|codex|grok]
181
+ * (this repository is the studio the chat's setup card is making: say so, once,
180
182
  * learn its live address, and give a template copy its real name)
181
183
  * homie-studio setup --via stripe-projects [--with elevenlabs] [--accept-tos] [--dry-run]
182
184
  * (a prototype, 0.24.3: Cloudflare, and ElevenLabs with --with, made or linked
@@ -711,14 +713,14 @@ async function main() {
711
713
  if (cmd === 'statusline') return installStatusLine(root, { remove: flags.has('remove'), replace: flags.has('replace'), project: flags.get('project') ?? null });
712
714
  if (cmd === 'codex') return codexCommand(root, sub);
713
715
  if (cmd === 'progress') return progressCommand(root, sub);
714
- if (cmd === 'setup' && sub === 'attach') return setupAttach(root, positional[2], { homie: flags.get('homie') });
716
+ if (cmd === 'setup' && sub === 'attach') return setupAttach(root, positional[2], { homie: flags.get('homie'), client: flags.get('client') });
715
717
  if (cmd === 'setup' && flags.has('via')) {
716
718
  if (flags.get('via') !== 'stripe-projects') return { ok: false, command: 'setup', why: '--via stripe-projects is the one other way this version knows (the default is npx wrangler login)' };
717
719
  const extra = String(flags.get('with') ?? '').split(',').map((x) => x.trim()).filter(Boolean);
718
720
  if (extra.some((x) => x !== 'elevenlabs')) return { ok: false, command: 'setup', why: '--with takes elevenlabs (Cloudflare is always set up)' };
719
721
  return setupViaProjects(root, { withElevenlabs: extra.includes('elevenlabs'), acceptTos: flags.has('accept-tos'), dryRun: flags.has('dry-run'), log });
720
722
  }
721
- if (cmd === 'handoff') return handoff(root, sub, { homie: flags.get('homie') });
723
+ if (cmd === 'handoff') return handoff(root, sub, { homie: flags.get('homie'), client: flags.get('client') });
722
724
  if (cmd === 'port' && sub === 'import') return importPort(root, positional[2], flags.get('id'), { name: flags.get('name'), mode: flags.get('mode') });
723
725
  if (cmd === 'port' && sub === 'check') {
724
726
  const game = positional[2] ?? listGames(root)[0]?.id;
package/lib/client.mjs ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Which chat is building the studio. Named only when the session says so (`--client`, or HOMIE_CLIENT).
3
+ * Anything else is "the chat": never a guess that it is Claude. Grok has no Cloudflare connector and no
4
+ * Claude Code window; `--client grok` is how a Grok Bot checks a repository in.
5
+ */
6
+ const KNOWN = {
7
+ claude: { id: 'claude', card: "the Claude app's card", connect: 'Connect this chat' },
8
+ codex: { id: 'codex', card: "Codex's card", connect: 'Connect this chat' },
9
+ grok: { id: 'grok', card: "Grok's card", connect: 'Connect this chat' },
10
+ chat: { id: 'chat', card: 'the setup card', connect: 'Connect this chat' },
11
+ };
12
+ const ALIAS = { 'claude-code': 'claude', 'claude-app': 'claude', 'grok-bot': 'grok', grokbot: 'grok' };
13
+
14
+ /** @param {string | null | undefined} input `--client`, or null to read HOMIE_CLIENT */
15
+ export function studioClient(input, env = process.env) {
16
+ const raw = String(input ?? env.HOMIE_CLIENT ?? '').trim().toLowerCase();
17
+ const id = ALIAS[raw] ?? raw;
18
+ return KNOWN[id] ?? KNOWN.chat;
19
+ }
package/lib/doctor.mjs CHANGED
@@ -152,7 +152,7 @@ export async function setupStatus({
152
152
  {
153
153
  const fixConnector = {
154
154
  who: 'person',
155
- say: 'Turn the Homie connector on. Claude Code: run /plugin, install or enable "homie" (marketplace homie-rocks/homie), then /mcp shows homie connected. Codex: install the Homie plugin from the same marketplace. The Claude app: Settings, Connectors, Add custom connector, https://homie.rocks/mcp; then its setup card (studio_setup) makes the studio on your own Cloudflare and GitHub in three taps.',
155
+ say: 'Turn the Homie connector on. Claude Code: run /plugin, install or enable "homie" (marketplace homie-rocks/homie), then /mcp shows homie connected. Codex: install the Homie plugin from the same marketplace. Grok Build: the same plugin (.grok-plugin/plugin.json). The Claude app, or Grok: add the connector https://homie.rocks/mcp; its setup card (studio_setup) makes the studio on your own Cloudflare. Grok has no Cloudflare connector: you still approve Cloudflare in the browser. A Grok Bot on this computer runs the checklist and checks in with setup attach <hs_…> --client grok.',
156
156
  };
157
157
  let state; let detail;
158
158
  if (net.blocked) { state = 'act'; detail = `the network proxy of this machine refused ${new URL(directory).host}`; }
@@ -162,7 +162,7 @@ export async function setupStatus({
162
162
  else { state = 'unknown'; detail = net.ok ? `${new URL(directory).host} answers; your AI knows whether its Homie tools (studio_scaffold) are here` : `this computer's request to the directory failed: ${net.why}`; }
163
163
  rows.push({
164
164
  id: 'connector', label: 'Homie connector', need: 'required', state, detail,
165
- unlocks: 'making the studio with the right toolkit version, listing games in the homie.rocks directory, and the cards in the Claude app',
165
+ unlocks: 'making the studio with the right toolkit version, listing games in the homie.rocks directory, and the setup cards',
166
166
  fix: state === 'ok' ? null : net.blocked
167
167
  ? { who: 'person', say: `In claude.ai/code, open this environment's settings, set Network access to Custom, add ${new URL(directory).host} (keep the default package managers), and start a new session.` }
168
168
  : said === 'no' || net.ok ? fixConnector : null,
@@ -174,13 +174,13 @@ export async function setupStatus({
174
174
  const local = root ? readLocal(root) : {};
175
175
  const created = studio?.cloudflare?.created ?? [];
176
176
  const deployed = Boolean(local.deployedAt || created.some((c) => String(c).startsWith('worker:')));
177
- // A studio the Claude app's setup card made (`setup attach` wrote connectedAt): Cloudflare's Deploy button made
177
+ // A studio the setup card made (`setup attach` wrote connectedAt): Cloudflare's Deploy button made
178
178
  // it on the person's account and Workers Builds deploys every merge, so no sign-in is needed on this computer.
179
179
  const attached = Boolean(local.connectedAt);
180
180
  const signedIn = attached || who.signedIn === true ? 'ok' : who.signedIn === false ? 'act' : 'later';
181
181
  const verified = deployed || attached ? 'ok' : local.needs === 'cloudflare-verify-email' ? 'act' : 'later';
182
182
  const parts = attached ? [
183
- { label: 'connected', state: 'ok', detail: 'through the Claude app\'s setup card (Deploy to Cloudflare); Workers Builds deploys every merge' },
183
+ { label: 'connected', state: 'ok', detail: 'through the setup card (Deploy to Cloudflare); Workers Builds deploys every merge' },
184
184
  { label: 'email verified', state: 'ok', detail: 'the site is live' },
185
185
  ] : [
186
186
  { label: 'signed in', state: signedIn, detail: who.signedIn === true ? (who.how ?? `${who.accounts ?? 1} account${who.accounts === 1 || who.accounts === null || who.accounts === undefined ? '' : 's'}`) : who.signedIn === false ? 'not signed in' : root ? (who.why ?? 'run npm install in the studio, then this again') : 'checked once the studio exists (it brings its own Wrangler)' },
package/lib/files.mjs CHANGED
@@ -10,7 +10,7 @@ import { dirname, extname, join, relative, resolve, sep } from 'node:path';
10
10
  import { PICTURE_MAX, PICTURE_TYPES, pictureFor } from './pictures.mjs';
11
11
 
12
12
  // A picture is at most PICTURE_MAX raw in an answer (lib/pictures.mjs): a bigger one goes as a smaller JPEG copy.
13
- export const FILE_LIMITS = Object.freeze({ write: 2 * 1024 * 1024, read: 400 * 1024, image: PICTURE_MAX, lines: 2000, list: 600, matches: 200 });
13
+ export const FILE_LIMITS = Object.freeze({ write: 2 * 1024 * 1024, read: 400 * 1024, image: PICTURE_MAX, lines: 2000, list: 600, matches: 200, edits: 100 });
14
14
  const IMAGES = PICTURE_TYPES;
15
15
  const SKIP_DIRS = new Set(['node_modules', '.git', '.wrangler', '.studio', 'dist']);
16
16
 
@@ -112,19 +112,33 @@ export function writeStudioFile(root, path, content) {
112
112
  return { rel, bytes: Buffer.byteLength(text), created: !existed };
113
113
  }
114
114
 
115
- export function editStudioFile(root, path, oldText, newText, { all = false } = {}) {
115
+ /**
116
+ * Replace exact text in a file. One change (`oldText`, `newText`, `all`), or several in one call (`edits`: a list of
117
+ * { old, new, all? }), applied in order to the text in memory and written once: all of them, or none. A change that
118
+ * does not match says which one it was, and the file is left as it was.
119
+ */
120
+ export function editStudioFile(root, path, oldText, newText, { all = false, edits = null } = {}) {
116
121
  const { abs, rel } = studioPath(root, path, { write: true });
117
122
  if (!existsSync(abs)) throw new Error(`${rel} does not exist (file_write makes a new file)`);
118
- const was = readFileSync(abs, 'utf8');
119
- const from = String(oldText ?? '');
120
- if (!from) throw new Error('old is the exact text to replace (it cannot be empty)');
121
- const count = was.split(from).length - 1;
122
- if (!count) throw new Error(`the text to replace is not in ${rel} (it must match exactly, spaces and line breaks too)`);
123
- if (count > 1 && !all) throw new Error(`the text to replace is in ${rel} ${count} times: give more of it so it is unique, or all: true`);
124
- const next = all ? was.split(from).join(String(newText ?? '')) : was.replace(from, () => String(newText ?? ''));
123
+ const list = Array.isArray(edits) && edits.length ? edits : [{ old: oldText, new: newText, all }];
124
+ if (list.length > FILE_LIMITS.edits) throw new Error(`at most ${FILE_LIMITS.edits} edits in one call`);
125
+ const many = list.length > 1;
126
+ let next = readFileSync(abs, 'utf8');
127
+ let replaced = 0;
128
+ list.forEach((e, i) => {
129
+ const from = String(e?.old ?? '');
130
+ const which = many ? `edit ${i + 1} of ${list.length} (${JSON.stringify(from.slice(0, 60))}${from.length > 60 ? '…' : ''}): ` : '';
131
+ const none = many ? '; nothing was changed' : '';
132
+ if (!from) throw new Error(`${which}old is the exact text to replace (it cannot be empty)${none}`);
133
+ const count = next.split(from).length - 1;
134
+ if (!count) throw new Error(`${which}the text to replace is not in ${rel} (it must match exactly, spaces and line breaks too${many ? '; an earlier edit in this call may have changed it' : ''})${none}`);
135
+ if (count > 1 && e.all !== true) throw new Error(`${which}the text to replace is in ${rel} ${count} times: give more of it so it is unique, or all: true${none}`);
136
+ next = e.all === true ? next.split(from).join(String(e.new ?? '')) : next.replace(from, () => String(e.new ?? ''));
137
+ replaced += e.all === true ? count : 1;
138
+ });
125
139
  if (Buffer.byteLength(next) > FILE_LIMITS.write) throw new Error(`at most ${FILE_LIMITS.write / 1024 / 1024} MB a file`);
126
140
  writeFileSync(abs, next);
127
- return { rel, replaced: all ? count : 1 };
141
+ return { rel, replaced, edits: list.length };
128
142
  }
129
143
 
130
144
  /** Lines matching a regular expression, in the studio's text files (never node_modules, git, builds or .studio). */
package/lib/handoff.mjs CHANGED
@@ -15,6 +15,7 @@ import { request } from './net.mjs';
15
15
  import { startProgress } from './progress.mjs';
16
16
  import { spawnSync } from 'node:child_process';
17
17
  import { studioRepo } from './repo.mjs';
18
+ import { studioClient } from './client.mjs';
18
19
 
19
20
  /** The repository this checkout IS (its origin remote), even Homie's engine one: the session must be in the studio's. */
20
21
  function originRepo(root) {
@@ -36,13 +37,13 @@ const line = (v, max) => String(v ?? '').replace(/[\x00-\x1f\x7f]+/g, ' ').repla
36
37
  const lines = (v, max) => String(v ?? '').split(/\r?\n/).map((l) => line(l, max)).filter(Boolean).join('\n').slice(0, max);
37
38
 
38
39
  /** The steps for a kind of build, in this toolkit's commands (the brief says what; these say how). */
39
- export function stepsFor(h, { games = [] } = {}) {
40
+ export function stepsFor(h, { games = [], client = 'chat' } = {}) {
40
41
  const id = ID.test(String(h.id ?? '')) ? h.id : null;
41
42
  const has = id && games.includes(id);
42
43
  const q = (s) => JSON.stringify(String(s));
43
44
  const work = {
44
45
  setup: [
45
- 'This studio was just set up from the Claude app. Go through the new-studio checklist with the person, one step at a time and never ahead (the plugin\'s studio-setup skill): it has no game yet, and its home page says "First game coming soon".',
46
+ `This studio was just set up from ${client === 'grok' ? 'Grok' : client === 'codex' ? 'Codex' : client === 'claude' ? 'the Claude app' : 'this chat'}. Go through the new-studio checklist with the person, one step at a time and never ahead (the plugin's studio-setup skill): it has no game yet, and its home page says "First game coming soon".`,
46
47
  'See a working game: `npx --no-install homie-studio demo` names a live one on Homie Arcade; copy a starter in only if they ask.',
47
48
  'One small change from one sentence of theirs (the studio\'s colours in site/theme.json, a tagline in studio.json, or a first post in posts/), then plan their game into its Game Codex (`npx --no-install homie-studio codex new <id> --name "<Name>"`) before any game is made.',
48
49
  ],
@@ -67,16 +68,17 @@ export function stepsFor(h, { games = [] } = {}) {
67
68
  ];
68
69
  }
69
70
 
70
- export async function handoff(root, build, { homie, fetchFn = globalThis.fetch, attach = startProgress, checkIn = setupAttach, repoOf = originRepo } = {}) {
71
+ export async function handoff(root, build, { homie, client, fetchFn = globalThis.fetch, attach = startProgress, checkIn = setupAttach, repoOf = originRepo } = {}) {
71
72
  const studio = readStudio(root);
73
+ const who = studioClient(client);
72
74
  // "Continue building <Studio>: setup hs_…": a studio being set up, before any build: check in, then the checklist.
73
75
  if (SETUP.test(String(build ?? ''))) {
74
- const r = await checkIn(root, build, { homie });
76
+ const r = await checkIn(root, build, { homie, client: who.id === 'chat' ? null : who.id });
75
77
  if (!r.ok) return { ok: false, command: 'handoff', setup: build, why: r.why, ...(r.needs ? { needs: r.needs } : {}) };
76
78
  const h = { kind: 'setup', what: 'game', title: 'first steps', studio: r.name ?? studio.name, brief: '', setup: build };
77
- return { ok: true, command: 'handoff', build: null, ...h, setup: { ok: true, message: r.message, renamed: r.renamed ?? [] }, attached: null, attachWhy: 'no build was opened in the chat yet (the setup card\'s Start building opens one)', steps: stepsFor(h) };
79
+ return { ok: true, command: 'handoff', build: null, ...h, client: who.id, setup: { ok: true, message: r.message, renamed: r.renamed ?? [] }, attached: null, attachWhy: 'no build was opened in the chat yet (the setup card\'s Start building opens one)', steps: stepsFor(h, { client: who.id }) };
78
80
  }
79
- if (!BUILD.test(String(build ?? ''))) return { ok: false, command: 'handoff', why: 'the id from the one line the Claude app gave: hb_ (a build) or hs_ (a setup) and 32 hex digits' };
81
+ if (!BUILD.test(String(build ?? ''))) return { ok: false, command: 'handoff', why: `the id from the one line the chat gave: hb_ (a build) or hs_ (a setup) and 32 hex digits` };
80
82
  const directory = String(homie || studio.homie?.directory || 'https://homie.rocks').replace(/\/+$/, '');
81
83
  // lib/net.mjs: through the environment's proxy, and an honest reason when it fails (the directory's own words, or
82
84
  // the connection's error; only a refusal by the proxy names the network setting).
@@ -98,7 +100,7 @@ export async function handoff(root, build, { homie, fetchFn = globalThis.fetch,
98
100
  };
99
101
  let setup = null;
100
102
  if (h.setup) {
101
- const r = await checkIn(root, h.setup, { homie: directory });
103
+ const r = await checkIn(root, h.setup, { homie: directory, client: who.id === 'chat' ? null : who.id });
102
104
  setup = { ok: r.ok, message: r.ok ? r.message : r.why, renamed: r.renamed ?? [] };
103
105
  }
104
106
  const taken = await attach(root, { attach: build, what: h.what, id: h.id, directory });
@@ -108,7 +110,7 @@ export async function handoff(root, build, { homie, fetchFn = globalThis.fetch,
108
110
  ok: true, command: 'handoff', build, ...h, setup,
109
111
  attached: taken.ok ? { feed: taken.build, shared: taken.shared?.build ?? build } : null,
110
112
  attachWhy: taken.ok ? null : taken.why,
111
- steps: stepsFor(h, { games }),
113
+ steps: stepsFor(h, { games, client: who.id }),
112
114
  };
113
115
  }
114
116
 
package/lib/mcp-tools.mjs CHANGED
@@ -54,6 +54,20 @@ export const HOMIE_UPDATES = 'https://homie.rocks/updates/?from=plugin';
54
54
  const inside = (base, p) => p === base || p.startsWith(base.endsWith(sep) ? base : `${base}${sep}`);
55
55
  const realOr = (p) => { try { return realpathSync(p); } catch { return resolve(p); } };
56
56
 
57
+ /** In Homie for Claude Desktop: the extension's manifest starts the server with HOMIE_STUDIO_EXTENSION=1 (desktop/manifest.json). */
58
+ export const inDesktopApp = (env = process.env) => env.HOMIE_STUDIO_EXTENSION === '1';
59
+
60
+ /*
61
+ * A FIRST RUN IN THE CLAUDE DESKTOP APP (0.30.1). The app asks the person before every tool call of an extension:
62
+ * about fifty requests in one filmed first build, each a click, and nothing told the person that "Always allow"
63
+ * exists. Twice the app raised a request it did not draw (it came straight after a card, with no text between):
64
+ * the chat showed a spinner until the request timed out four minutes later, while the request's own key still
65
+ * answered it. That is the app's to fix; Homie's part is that the person is told, once, by the model, in its first
66
+ * reply. Only the desktop extension says this (inDesktopApp): in Claude Code, Codex, Grok or any other MCP client
67
+ * it would not be true.
68
+ */
69
+ export const DESKTOP_APPROVALS = 'This is the Claude desktop app: it asks the person before each Homie Studio tool call. ONCE in a chat, in your first words to the person (skip it if you already said it in this chat), tell them in two short sentences of your own: (1) Claude will ask before it uses each Homie Studio tool, and choosing "Always allow" lets the build run without a click at every step; (2) if a step ever sits on a spinner for more than a few seconds with nothing to click, a request is waiting out of sight: Cmd+Return on a Mac (Ctrl+Enter on Windows) allows it once, and scrolling the chat can bring it into view (a request shows its keys beside its buttons). Do not repeat it later. And after a tool that shows a card (setup_status, studio_scaffold, game_codex, build_open, check, playtest, studio_deploy, …), write one short line of text before your next tool call: a request that came straight after a card, with no text between, is the one the app has failed to draw.';
70
+
57
71
  /** A folder as the Claude desktop app's settings give it: `${HOME}/Studios` arrives as written, so expand it here. */
58
72
  export function expandHome(value, home = homedir()) {
59
73
  const v = String(value ?? '').trim();
@@ -65,8 +79,10 @@ export function expandHome(value, home = homedir()) {
65
79
  /* ------------------------------------------------------------------ which studio */
66
80
 
67
81
  export class StudioContext {
68
- constructor({ studiosDir = null, cwd = process.cwd(), skillsDir = null, waitMs = 40_000, directory = null, install = true } = {}) {
82
+ constructor({ studiosDir = null, cwd = process.cwd(), skillsDir = null, waitMs = 40_000, directory = null, install = true, desktop = inDesktopApp() } = {}) {
69
83
  this.install = install;
84
+ // Homie for Claude Desktop (the .mcpb): the one host these tools know asks the person before every call.
85
+ this.desktop = desktop === true;
70
86
  this.studiosDir = studiosDir ? expandHome(studiosDir) : null;
71
87
  this.cwdStudio = findStudio(cwd);
72
88
  this.current = this.cwdStudio;
@@ -110,13 +126,24 @@ export class StudioContext {
110
126
  return Boolean((this.studiosDir && inside(realOr(this.studiosDir), real)) || (this.cwdStudio && inside(realOr(this.cwdStudio), real)));
111
127
  }
112
128
 
129
+ /** Where `a` (a folder name, a studio's name, or a path inside the studios folder) would be a studio, or null. */
130
+ pathOf(a, list = this.studios()) {
131
+ const byName = list.find((s) => s.folder === a || s.name.toLowerCase() === a.toLowerCase() || slugify(s.name) === slugify(a));
132
+ return byName?.root ?? (isAbsolute(expandHome(a) ?? '') && (a.includes('/') || a.includes('\\') || a.startsWith('~')) ? expandHome(a) : this.studiosDir ? join(this.studiosDir, a) : null);
133
+ }
134
+
135
+ /** Whether `arg` names a studio that exists (as root() would find it). */
136
+ knows(arg) {
137
+ const path = this.pathOf(String(arg ?? '').trim());
138
+ return Boolean(path && existsSync(join(path, 'studio.json')));
139
+ }
140
+
113
141
  /** The studio a tool works in: `arg` (a folder name, a studio's name, or a path inside the studios folder). */
114
142
  root(arg, { need = true } = {}) {
115
143
  if (arg !== undefined && arg !== null && String(arg).trim()) {
116
144
  const a = String(arg).trim();
117
145
  const list = this.studios();
118
- const byName = list.find((s) => s.folder === a || s.name.toLowerCase() === a.toLowerCase() || slugify(s.name) === slugify(a));
119
- const path = byName?.root ?? (isAbsolute(expandHome(a) ?? '') && (a.includes('/') || a.includes('\\') || a.startsWith('~')) ? expandHome(a) : this.studiosDir ? join(this.studiosDir, a) : null);
146
+ const path = this.pathOf(a, list);
120
147
  if (!path || !existsSync(join(path, 'studio.json'))) throw new Error(`no studio "${a}"${list.length ? ` (studios here: ${list.map((s) => s.folder).join(', ')})` : ''}`);
121
148
  if (!this.allowed(path)) throw new Error(`${path} is outside the studios folder (${this.studiosDir ?? 'none is set'}); these tools only work in it`);
122
149
  this.current = realOr(path);
@@ -203,7 +230,7 @@ function checklist(ctx, root) {
203
230
  return steps.map((label, n) => ({ n, label, state: done[n] ? 'done' : n === now ? 'now' : 'todo' }));
204
231
  }
205
232
 
206
- async function setupCard(ctx, root, { fresh = false, made = null, install = null } = {}) {
233
+ async function setupCard(ctx, root, { fresh = false, made = null, install = null, wanted = null } = {}) {
207
234
  const status = await statusOf(ctx, root, { fresh });
208
235
  const studios = ctx.studios().map(({ root: r, ...s }) => s);
209
236
  const current = root ? ctx.describe(root) : null;
@@ -212,6 +239,8 @@ async function setupCard(ctx, root, { fresh = false, made = null, install = null
212
239
  const steps = checklist(ctx, root);
213
240
  const data = {
214
241
  kind: 'setup', version: STUDIO_VERSION, studiosDir: ctx.studiosDir,
242
+ // The name of a studio that is not made yet (setup_status asked for it by name): the card is that new studio's.
243
+ wanted: current ? null : wanted,
215
244
  current: current ? { folder: current.folder, name: current.name, games: current.games, installed: current.installed, site: siteUrl(root) } : null,
216
245
  studios, checklist: steps,
217
246
  status: { rows: status.rows.map(({ parts, ...r }) => r), features: status.features, next: status.next, meanwhile: status.meanwhile, blocking: status.blocking },
@@ -220,16 +249,22 @@ async function setupCard(ctx, root, { fresh = false, made = null, install = null
220
249
  // homie.rocks). The card never asks for an address, and an agent never types one in for the person.
221
250
  updates: HOMIE_UPDATES,
222
251
  };
252
+ const others = studios.map((s) => s.name);
253
+ const first = (steps.find((s) => s.state === 'now')?.n ?? steps.length) <= 2;
223
254
  const text = [
224
- current ? `Studio: ${current.name} (${root})${current.installed ? '' : job ? ' (installing its toolkit)' : ''}` : `No studio yet${ctx.studiosDir ? ` in ${ctx.studiosDir}` : ''}.`,
255
+ current ? `Studio: ${current.name} (${root})${current.installed ? '' : job ? ' (installing its toolkit)' : ''}`
256
+ : wanted ? `No studio called "${wanted}" yet${ctx.studiosDir ? ` in ${ctx.studiosDir}` : ''}${others.length ? ` (the studios here: ${others.join(', ')}; this checklist is the new studio's, not theirs)` : ''}: studio_scaffold makes it.`
257
+ : `No studio yet${ctx.studiosDir ? ` in ${ctx.studiosDir}` : ''}.`,
225
258
  '',
226
- `New studio${current ? `: ${current.name}` : ''}`,
259
+ `New studio${current ? `: ${current.name}` : wanted ? `: ${wanted}` : ''}`,
227
260
  ...steps.map((s) => ` ${s.state === 'done' ? '✓' : s.state === 'now' ? '→' : ' '} ${s.n}. ${s.label}`),
228
261
  '',
229
262
  formatStatus(status),
230
263
  '',
231
264
  `Homie updates by email (optional): ${HOMIE_UPDATES} (the person signs up there themselves; never type an address in for them).`,
232
265
  'The card on screen shows this; it follows the install by itself.',
266
+ // The first minutes of a new studio in the Claude desktop app: what the person is told once (DESKTOP_APPROVALS).
267
+ ...(ctx.desktop && first ? ['', DESKTOP_APPROVALS] : []),
233
268
  ].join('\n');
234
269
  return ok(text, data);
235
270
  }
@@ -703,7 +738,7 @@ function guideTopics(ctx) {
703
738
  try { return readdirSync(ctx.skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory() && existsSync(join(ctx.skillsDir, d.name, 'SKILL.md'))).map((d) => d.name).sort(); } catch { return []; }
704
739
  }
705
740
 
706
- const DESKTOP_NOTE = 'In this app, where a guide says to run `npx --no-install homie-studio <command>`, call the tool of the same job (build, check, preview_run, studio_deploy, …) or studio_run with that command\'s words; where it says to edit a file, use file_read, file_edit and file_write; a guide\'s own script (music, sound, art, video) is the tool of that name.';
741
+ const DESKTOP_NOTE = 'In this app, where a guide says to run `npx --no-install homie-studio <command>`, call the tool of the same job (build, check, preview_run, studio_deploy, …) or studio_run with that command\'s words; where it says to edit a file, use file_read, file_edit and file_write, in few, large calls: read a file once, then make every change it needs in ONE file_edit (its edits list takes many replacements) or one file_write of a new or short file, never a call per change (every call is a step the person waits on, and a turn has only so many); a guide\'s own script (music, sound, art, video) is the tool of that name.';
707
742
 
708
743
  /* ------------------------------------------------------------------ the tools */
709
744
 
@@ -717,10 +752,15 @@ export function toolDefs(ctx, avail = {}) {
717
752
  const tools = [
718
753
  {
719
754
  name: 'setup_status', title: 'Setup status',
720
- description: 'Step 0 of a new studio, and any time the person asks what they need or whether they are set up: what this computer and their accounts have (Node, Cloudflare signed in and email verified, Chrome for the checks, ffmpeg, and the optional GitHub, ElevenLabs and fal), what each unlocks, and the exact fix; the studios in the studios folder; and the new-studio checklist with where they are on it. Read-only, a few seconds. Optional rows never block anything.',
721
- inputSchema: { type: 'object', properties: { ...STUDIO_ARG, fresh: { type: 'boolean', description: 'Check again now instead of the last 20 s' } } },
755
+ description: 'Step 0 of a new studio, and any time the person asks what they need or whether they are set up: what this computer and their accounts have (Node, Cloudflare signed in and email verified, Chrome for the checks, ffmpeg, and the optional GitHub, ElevenLabs and fal), what each unlocks, and the exact fix; the studios in the studios folder; and the new-studio checklist with where they are on it. For a studio that is not made yet, pass its name as studio: the checklist is then the new studio\'s, never another studio\'s that happens to be in the folder. Read-only, a few seconds. Optional rows never block anything.',
756
+ inputSchema: { type: 'object', properties: { studio: str('Optional: which studio (its folder name or name), or the name of a new studio that is not made yet; else the one in use'), fresh: { type: 'boolean', description: 'Check again now instead of the last 20 s' } } },
722
757
  annotations: { title: 'Setup status', ...RO }, _meta: ui(UI.setup),
723
- run: async (a) => setupCard(ctx, ctx.root(a.studio, { need: false }), { fresh: a.fresh === true }),
758
+ run: async (a) => {
759
+ // A name with no studio of it yet is the studio about to be made: its own checklist, from step 1.
760
+ const named = String(a.studio ?? '').trim().slice(0, 60);
761
+ if (named && !ctx.knows(named)) return setupCard(ctx, null, { fresh: a.fresh === true, wanted: named });
762
+ return setupCard(ctx, ctx.root(a.studio, { need: false }), { fresh: a.fresh === true });
763
+ },
724
764
  },
725
765
  {
726
766
  name: 'studio_scaffold', title: 'Make a studio',
@@ -840,7 +880,7 @@ export function toolDefs(ctx, avail = {}) {
840
880
  }
841
881
  const m = r.result?.models;
842
882
  const got = m ? ` Its models: ${m.fetched} from the starter library${m.missing.length ? `; ${m.missing.length} could not be fetched (${m.why ?? m.missing[0].why}), and it draws stand-ins for those` : ''}.` : '';
843
- return ok(`games/${a.id} is ${a.name}, from the ${a.from ?? 'gem-rush'} starter (${(r.result?.files ?? []).length} files).${got}${install} Change it in games/${a.id}/src/main.ts (file_read, file_edit), then build and preview_run; check proves two browsers finish a round.`, { kind: 'game', ...r.result });
883
+ return ok(`games/${a.id} is ${a.name}, from the ${a.from ?? 'gem-rush'} starter (${(r.result?.files ?? []).length} files).${got}${install} Change it in games/${a.id}/src/main.ts: file_read it once (a long file in two or three reads), decide every change, and make them in ONE file_edit with an edits list (never a call per change: twenty single edits use up a turn before the game is checked); then build and preview_run; check proves two browsers finish a round.`, { kind: 'game', ...r.result });
844
884
  },
845
885
  },
846
886
  {
@@ -888,7 +928,7 @@ export function toolDefs(ctx, avail = {}) {
888
928
  },
889
929
  {
890
930
  name: 'game_plan', title: 'Plan a game (its Game Codex)',
891
- description: 'Step 4: start the game\'s Game Codex (games/<id>/CODEX.md, every section) and get the plan interview to run with the person: two or three questions a message, each with options and your pick, about game type and genre, style, devices, players and rooms, art and film, music and sound, and scope. A game is planned before it is made: with no game <id> yet, this starts its folder with only the codex. Fill CODEX.md from their answers with file_edit, then game_codex shows it.',
931
+ description: 'Step 4: start the game\'s Game Codex (games/<id>/CODEX.md, every section) and get the plan interview to run with the person: two or three questions a message, each with options and your pick, about game type and genre, style, devices, players and rooms, art and film, music and sound, and scope. A game is planned before it is made: with no game <id> yet, this starts its folder with only the codex. Fill CODEX.md from their answers in one call (file_write of the whole file, or one file_edit with an edits list), then game_codex shows it.',
892
932
  inputSchema: { type: 'object', properties: { id: str('The game\'s id (lowercase, digits, hyphens)'), name: str('Its name, when the game is not made yet'), ...STUDIO_ARG }, required: ['id'] },
893
933
  annotations: { title: 'Plan a game', ...RW },
894
934
  run: async (a) => {
@@ -902,7 +942,7 @@ export function toolDefs(ctx, avail = {}) {
902
942
  const guide = ctx.skillsDir && existsSync(join(ctx.skillsDir, 'plan', 'references', 'INTERVIEW.md')) ? readFileSync(join(ctx.skillsDir, 'plan', 'references', 'INTERVIEW.md'), 'utf8') : null;
903
943
  return ok([
904
944
  made ? `Wrote games/${a.id}/CODEX.md (every section${made.planned ? '; the game is planned here before it is made' : ''}).` : `games/${a.id}/CODEX.md is there already: change it, never replace it.`,
905
- 'Now the interview: two or three questions a message, each with concrete options and your pick, so "yes" is an answer. Say back what you heard in one line before the next. Stop after three or four rounds; "just build it" means fill the rest with your own choices and list them under Open questions. Then fill CODEX.md (file_read it, file_edit each section) and show it with game_codex.',
945
+ 'Now the interview: two or three questions a message, each with concrete options and your pick, so "yes" is an answer. Say back what you heard in one line before the next. Stop after three or four rounds; "just build it" means fill the rest with your own choices and list them under Open questions. Then fill CODEX.md in one call (file_read it once; then file_write the whole file, or one file_edit whose edits list has every section) and show it with game_codex.',
906
946
  ...(guide ? ['', guide] : []),
907
947
  ].join('\n'), { kind: 'plan', id: a.id, file: `games/${a.id}/CODEX.md`, created: Boolean(made) });
908
948
  },
@@ -1062,7 +1102,7 @@ export function toolDefs(ctx, avail = {}) {
1062
1102
  if (!r.ended) return stillRunning(r.job, 'The deploy plan');
1063
1103
  if (r.job.code !== 0) return fail(whyOf(r.job));
1064
1104
  const p = r.result;
1065
- return ok([`What going online does for ${p.studio}:`, ...p.cloudflare.map((x) => ` ${x.kind}${x.name ? ` ${x.name}` : ''}: ${x.what} [${x.state}]`), `Cost: ${p.cost}`, `Sign-in: ${p.login}`, `The directory stores: ${p.directory?.stores ?? ''}`].join('\n'), { kind: 'deploy-plan', ...p });
1105
+ return ok([`What going online does for ${p.studio}:`, ...p.cloudflare.map((x) => ` ${x.kind}${x.name ? ` ${x.name}` : ''}: ${x.what} [${x.state}]`), `Cost: ${p.cost}`, `Sign-in: ${p.login}`, `The directory stores: ${p.directory?.stores ?? ''}`, 'The card on screen shows this plan (nothing is made yet): tell the person its gist in two or three lines, then go on.'].join('\n'), { kind: 'deploy-plan', ...p });
1066
1106
  }
1067
1107
  const studio = readStudio(root);
1068
1108
  return startRun(ctx, root, { kind: 'deploy', game: null, title: `${studio.name}: online`, steps: [{ label: 'deploy', args: ['deploy'] }] });
@@ -1294,14 +1334,18 @@ export function toolDefs(ctx, avail = {}) {
1294
1334
  },
1295
1335
  {
1296
1336
  name: 'file_edit', title: 'Edit a studio file',
1297
- description: 'Replace exact text in a studio file: old must match exactly once (spaces and line breaks too), or all: true for every match. Read the file first.',
1298
- inputSchema: { type: 'object', properties: { path: str('The file, relative to the studio'), old: str('The exact text to replace'), new: str('What replaces it'), all: { type: 'boolean' }, ...STUDIO_ARG }, required: ['path', 'old', 'new'] },
1337
+ description: 'Replace exact text in a studio file: old must match exactly once (spaces and line breaks too), or all: true for every match. Read the file first. Several changes to one file go in ONE call: edits is a list of { old, new, all? } applied in order, all of them or none (a miss says which one, and the file is left as it was). Prefer one call with every change over a call per change.',
1338
+ inputSchema: { type: 'object', properties: { path: str('The file, relative to the studio'), old: str('The exact text to replace (one change)'), new: str('What replaces it'), all: { type: 'boolean' }, edits: { type: 'array', description: 'Several changes to this file in one call, applied in order, all or none', items: { type: 'object', properties: { old: str('The exact text to replace'), new: str('What replaces it'), all: { type: 'boolean' } }, required: ['old', 'new'] } }, ...STUDIO_ARG }, required: ['path'] },
1299
1339
  annotations: { title: 'Edit a file', readOnlyHint: false, destructiveHint: true, openWorldHint: false },
1300
1340
  run: async (a) => {
1301
1341
  const root = ctx.root(a.studio);
1302
- const r = editStudioFile(root, a.path, a.old, a.new, { all: a.all === true });
1342
+ const list = Array.isArray(a.edits) && a.edits.length ? a.edits : null;
1343
+ if (!list && (a.old === undefined || a.new === undefined)) return fail('give old and new (one change), or edits: [{ "old": "…", "new": "…" }, …] for several changes to this file in one call');
1344
+ const r = list
1345
+ ? editStudioFile(root, a.path, null, null, { edits: [...(a.old !== undefined && a.new !== undefined ? [{ old: a.old, new: a.new, all: a.all === true }] : []), ...list] })
1346
+ : editStudioFile(root, a.path, a.old, a.new, { all: a.all === true });
1303
1347
  ctx.note(root).changed = true;
1304
- return ok(`Changed ${r.rel} (${r.replaced} place${r.replaced === 1 ? '' : 's'}).`);
1348
+ return ok(`Changed ${r.rel} (${r.replaced} place${r.replaced === 1 ? '' : 's'}${r.edits > 1 ? `, ${r.edits} edits in one call` : ''}).`);
1305
1349
  },
1306
1350
  },
1307
1351
  {
@@ -1333,7 +1377,7 @@ export function toolDefs(ctx, avail = {}) {
1333
1377
  return ok(readFileSync(f, 'utf8'));
1334
1378
  }
1335
1379
  const refs = (() => { try { return readdirSync(join(dir, 'references')); } catch { return []; } })();
1336
- return ok(`${DESKTOP_NOTE}\n\n${readFileSync(join(dir, 'SKILL.md'), 'utf8')}${refs.length ? `\n\nReferences (file): ${refs.join(', ')}` : ''}`);
1380
+ return ok(`${DESKTOP_NOTE}${ctx.desktop ? `\n\n${DESKTOP_APPROVALS}` : ''}\n\n${readFileSync(join(dir, 'SKILL.md'), 'utf8')}${refs.length ? `\n\nReferences (file): ${refs.join(', ')}` : ''}`);
1337
1381
  },
1338
1382
  });
1339
1383
  }
package/lib/mcp.mjs CHANGED
@@ -12,6 +12,9 @@
12
12
  * --no-install never run npm install in a studio (tests, and a studio whose node_modules are linked by hand)
13
13
  * --homie the directory a new studio names (default https://homie.rocks)
14
14
  *
15
+ * HOMIE_STUDIO_EXTENSION=1 (the desktop extension's manifest sets it) says the server runs in the Claude desktop app,
16
+ * which asks the person before every tool call: only there, the model is told to say so once (DESKTOP_APPROVALS).
17
+ *
15
18
  * The transport is the MCP stdio transport: one JSON-RPC 2.0 message per line on stdin and stdout; logs go to
16
19
  * stderr. No SDK. MCP Apps cards (spec 2026-01-26, `text/html;profile=mcp-app`) are served as ui:// resources from
17
20
  * mcp/ui/ (real files, read as they are: a card's script is never built from a function's source), and every tool
@@ -20,7 +23,7 @@
20
23
  import { existsSync, readFileSync } from 'node:fs';
21
24
  import { join } from 'node:path';
22
25
  import { PACKAGE_ROOT } from './studio.mjs';
23
- import { StudioContext, UI, availability, toolDefs } from './mcp-tools.mjs';
26
+ import { DESKTOP_APPROVALS, StudioContext, UI, availability, inDesktopApp, toolDefs } from './mcp-tools.mjs';
24
27
  import { stopAllJobs, toolPath } from './jobs.mjs';
25
28
  import { shrinkPictureData } from './pictures.mjs';
26
29
  import { STUDIO_VERSION } from './version.mjs';
@@ -31,12 +34,12 @@ export const MCP_APP_MIME = 'text/html;profile=mcp-app';
31
34
  export const INSTRUCTIONS = `Homie Studio, on this computer: make game studios and their multiplayer web games, music and videos, run them here, and put them online on the studio's own Cloudflare. (It is not the Homie house app: that app's own server, with its room and TV tools, is a different one.) These tools do the work in this chat; the person never types a command and never needs a terminal. A studio is a folder in the studios folder; the file tools (file_list, file_read, file_edit, file_write, file_search) work inside it.
32
35
 
33
36
  A new studio follows one checklist, in order and never ahead. Show it in your first reply and again, ticked, as each step ends:
34
- 0. Setup status (setup_status): what this computer and their accounts have; optional rows never block.
37
+ 0. Setup status (setup_status; for a studio that is not made yet, pass its name as studio, so the checklist is the new studio's and never another's): what this computer and their accounts have; optional rows never block.
35
38
  1. The studio (studio_scaffold): it has NO game; its home page says "First game coming soon".
36
39
  2. See a working game (game_demo): a live game on Homie Arcade with its Play link, nothing copied in. Copy a starter in (game_make) only if they ask.
37
40
  3. One small change from one sentence of theirs: to the copied game, or to the studio's home (site/theme.json colours, a tagline in studio.json, a first post in posts/). Then build and preview_run, and they reload.
38
41
  4. Plan their game (game_plan): a short interview, two or three questions a message with options and your pick; then fill games/<id>/CODEX.md and show it (game_codex).
39
- 5. Build it: build_open, then game_make under the planned id (the codex stays), file edits, build, preview_run, check. The card follows every build.
42
+ 5. Build it: build_open, then game_make under the planned id (the codex stays), file edits (few and large: one file_edit carries every change to a file in its edits list, never a call per change), build, preview_run, check. The card follows every build.
40
43
  6. Playtest it (playtest), then put it online: studio_deploy with plan: true first (say what it creates and costs, free), cloudflare_login when not signed in (they approve once in their browser), studio_deploy, then studio_publish.
41
44
  If they ask for everything at once, show the list, make your own choices for steps 2 to 4 in one line each, and go on.
42
45
 
@@ -54,6 +57,9 @@ Telling Homie (homie_feedback): when the person is stuck, confused or frustrated
54
57
 
55
58
  Long work (npm install, check, playtest, deploy, renders) runs in the background: the tool answers at once with a card that follows it, and build_progress or studio_job reads where it is. studio_guide has Homie's full guide for each job (game, plan, port, playtest, publish, music, sound, art, video). Never put a key or password in a file or the chat. If Homie's homie.rocks connector is connected too, its tools of the same names say what to run; these run it.`;
56
59
 
60
+ /** What the server tells the model as it connects: the checklist, and in the Claude desktop app what it asks the person. */
61
+ export const instructionsFor = ({ desktop = false } = {}) => (desktop ? `${INSTRUCTIONS}\n\n${DESKTOP_APPROVALS}` : INSTRUCTIONS);
62
+
57
63
  const CARD_FILES = { [UI.setup]: 'setup.js', [UI.build]: 'build.js', [UI.studio]: 'studio.js', [UI.codex]: 'codex.js', [UI.lab]: 'lab.js', [UI.style]: 'style.js', [UI.decision]: 'decision.js', [UI.cast]: 'cast.js', [UI.lineup]: 'lineup.js', [UI.rights]: 'rights.js', [UI.animation]: 'animation.js', [UI.feedback]: 'feedback.js' };
58
64
  const CARD_TITLES = { [UI.setup]: 'Studio setup', [UI.build]: 'Build progress', [UI.studio]: 'Studio', [UI.codex]: 'Game Codex', [UI.lab]: 'Game Lab', [UI.style]: 'Style board', [UI.decision]: 'Look decision', [UI.cast]: 'Cast', [UI.lineup]: 'Lineup', [UI.rights]: 'Rights', [UI.animation]: 'Clips', [UI.feedback]: 'Tell Homie' };
59
65
  const UI_DIR = join(PACKAGE_ROOT, 'mcp', 'ui');
@@ -136,11 +142,11 @@ function promptText(name, args = {}) {
136
142
  /**
137
143
  * The server. `input`/`output` are streams (stdin/stdout by default). Returns a promise that ends with the input.
138
144
  */
139
- export async function serveMcp({ studios = null, skills = null, cwd = process.cwd(), input = process.stdin, output = process.stdout, waitMs, directory = null, install = true, log = (line) => process.stderr.write(`[homie-studio mcp] ${line}\n`) } = {}) {
145
+ export async function serveMcp({ studios = null, skills = null, cwd = process.cwd(), input = process.stdin, output = process.stdout, waitMs, directory = null, install = true, desktop = inDesktopApp(), log = (line) => process.stderr.write(`[homie-studio mcp] ${line}\n`) } = {}) {
140
146
  // A GUI app starts this with a short PATH: everything it runs sees the usual places Node, npm and ffmpeg live.
141
147
  process.env.PATH = toolPath();
142
148
  const skillsDir = skills ?? [join(PACKAGE_ROOT, '..', '..', 'plugins', 'homie', 'skills'), join(PACKAGE_ROOT, '..', 'skills')].find((d) => existsSync(join(d, 'studio-setup', 'SKILL.md'))) ?? null;
143
- const ctx = new StudioContext({ studiosDir: studios, cwd, skillsDir, waitMs: waitMs ?? Number(process.env.HOMIE_MCP_WAIT_MS || 40_000), directory, install });
149
+ const ctx = new StudioContext({ studiosDir: studios, cwd, skillsDir, waitMs: waitMs ?? Number(process.env.HOMIE_MCP_WAIT_MS || 40_000), directory, install, desktop });
144
150
  let avail = availability();
145
151
  let names = '';
146
152
  const send = (message) => output.write(`${JSON.stringify(message)}\n`);
@@ -168,7 +174,7 @@ export async function serveMcp({ studios = null, skills = null, cwd = process.cw
168
174
  protocolVersion: PROTOCOLS.includes(asked) ? asked : PROTOCOLS[0],
169
175
  capabilities: { tools: { listChanged: true }, resources: { listChanged: false }, prompts: { listChanged: false } },
170
176
  serverInfo: { name: 'homie-studio', title: 'Homie Studio', version: STUDIO_VERSION },
171
- instructions: INSTRUCTIONS,
177
+ instructions: instructionsFor({ desktop: ctx.desktop }),
172
178
  });
173
179
  }
174
180
  case 'ping': return reply({});
package/lib/scaffold.mjs CHANGED
@@ -666,14 +666,14 @@ export { default, Table, Lobby } from '@homie-rocks/studio/worker';
666
666
  return files;
667
667
  }
668
668
 
669
- /** The template's first-run band on Home: one link that connects this studio to the Claude chat that set it up. */
670
- export const CONNECT_BAND = `<!-- A new studio from the Homie template. This band goes away once Claude works in the studio
669
+ /** The template's first-run band on Home: one link that connects this studio to the chat that set it up. */
670
+ export const CONNECT_BAND = `<!-- A new studio from the Homie template. This band goes away once a chat works in the studio
671
671
  (homie-studio setup attach removes it); delete it by hand any time. -->
672
672
  <div class="band-in" style="text-align:center">
673
673
  <p class="kicker">New studio</p>
674
674
  <h2>{{studio.name}} is live</h2>
675
- <p>It runs on your own Cloudflare account. Connect it to the Claude chat that set it up, and Claude takes it from here.</p>
676
- <p><a class="btn" href="/_studio/connect">Connect to Claude</a></p>
675
+ <p>It runs on your own Cloudflare account. Connect it to the chat that set it up, and that chat takes it from here.</p>
676
+ <p><a class="btn" href="/_studio/connect">Connect this chat</a></p>
677
677
  </div>
678
678
  `;
679
679
 
@@ -689,8 +689,9 @@ The button copies this studio into your GitHub, creates its Worker, database and
689
689
  deploys it with Workers Builds: every push to \`main\` goes live, and every other branch gets its own Preview.
690
690
  The site goes live at once with its own home page ("first game coming soon"); the games come next.
691
691
 
692
- Then open the site and tap **Connect to Claude**, or ask Claude in the Claude app to set up your studio with the
693
- Homie connector: it makes games, songs and videos here, in a pull request you merge with one tap.
692
+ Then open the site and tap **Connect this chat**, or ask Claude, Codex or Grok (with the Homie connector,
693
+ https://homie.rocks/mcp) to set up your studio: it makes games, songs and videos here, in a pull request you merge
694
+ with one tap. Grok has no Cloudflare connector. Approving Cloudflare in the browser is still your step.
694
695
 
695
696
  \`AGENTS.md\` says how everything here works.
696
697
  `;
package/lib/setup.mjs CHANGED
@@ -6,7 +6,7 @@
6
6
  * and answers with the studio's live address once its site is connected (the card's Cloudflare step);
7
7
  * - a copy of the public template (studio.json `template: true`) gets the name the person chose in the chat:
8
8
  * studio.json, the Worker's STUDIO_NAME, and AGENTS.md and README.md while they are untouched; its first-run
9
- * "Connect to Claude" band goes; studio.json takes the Worker and database names Cloudflare's form chose;
9
+ * "Connect this chat" band goes; studio.json takes the Worker and database names Cloudflare's form chose;
10
10
  * - the live address goes into .studio/local.json (git-ignored), so `check --url` and the card know it.
11
11
  *
12
12
  * It sends the repository's owner/name and the studio's name and slug, nothing else: no key, no path, no code.
@@ -16,6 +16,7 @@
16
16
  import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
17
17
  import { join } from 'node:path';
18
18
  import { configNames } from './cloudflare.mjs';
19
+ import { studioClient } from './client.mjs';
19
20
  import { request } from './net.mjs';
20
21
  import { ENGINE_REPO, repoOf } from './repo.mjs';
21
22
  import { CONNECT_BAND, agentsMd, readme, slugify, templateReadme } from './scaffold.mjs';
@@ -24,8 +25,9 @@ import { STUDIO_VERSION } from './version.mjs';
24
25
 
25
26
  const SETUP_ID = /^hs_[a-f0-9]{32}$/;
26
27
 
27
- export async function setupAttach(root, id, { homie } = {}) {
28
- if (!SETUP_ID.test(String(id ?? ''))) return { ok: false, command: 'setup attach', why: 'attach to the setup id the Claude app\'s setup card showed: hs_ and 32 hex digits' };
28
+ export async function setupAttach(root, id, { homie, client } = {}) {
29
+ if (!SETUP_ID.test(String(id ?? ''))) return { ok: false, command: 'setup attach', why: 'attach to the setup id the setup card showed: hs_ and 32 hex digits' };
30
+ const who = studioClient(client);
29
31
  const studio = readStudio(root);
30
32
  const directory = String(homie || studio.homie?.directory || 'https://homie.rocks').replace(/\/+$/, '');
31
33
  const repo = repoOf(root);
@@ -34,7 +36,7 @@ export async function setupAttach(root, id, { homie } = {}) {
34
36
  }
35
37
  const sent = await request(`${directory}/api/studio/setup/${id}/attach`, {
36
38
  method: 'POST', headers: { 'content-type': 'application/json', 'user-agent': `homie-studio/${STUDIO_VERSION}` },
37
- body: JSON.stringify({ repo, studio: { name: studio.name, slug: studio.slug, template: studio.template === true }, version: STUDIO_VERSION }),
39
+ body: JSON.stringify({ repo, studio: { name: studio.name, slug: studio.slug, template: studio.template === true }, version: STUDIO_VERSION, ...(who.id === 'chat' ? {} : { client: who.id }) }),
38
40
  });
39
41
  // What went wrong, as it is: the directory's own answer, or the connection's error. Never a guess at the network.
40
42
  if (!sent.ok) return { ok: false, command: 'setup attach', repo, ...(sent.status ? { status: sent.status } : {}), ...(sent.code ? { code: sent.code } : {}), ...(sent.needs ? { needs: sent.needs } : {}), why: sent.why };
@@ -72,10 +74,11 @@ export async function setupAttach(root, id, { homie } = {}) {
72
74
  if (now.github !== repo) { writeStudio(root, { ...now, github: repo }); if (!renamed.includes('studio.json')) renamed.push('studio.json'); }
73
75
  if (body.site) writeLocal(root, { url: body.site, connectedAt: new Date().toISOString() });
74
76
  return {
75
- ok: true, command: 'setup attach', setup: id, repo, site: body.site ?? null, name: name ?? studio.name, renamed,
76
- message: body.site ? `This repository is ${name ?? studio.name}, live at ${body.site}. The Claude app's card shows it.` : `This repository is ${name ?? studio.name}. Its site is not connected yet: ${body.connect ?? 'open the site and tap Connect to Claude'}.`,
77
+ ok: true, command: 'setup attach', setup: id, repo, client: who.id, site: body.site ?? null, name: name ?? studio.name, renamed,
78
+ message: body.site ? `This repository is ${name ?? studio.name}, live at ${body.site}. ${who.card[0].toUpperCase()}${who.card.slice(1)} shows it.` : `This repository is ${name ?? studio.name}. Its site is not connected yet: ${body.connect ?? `open the site and tap ${who.connect}`}.`,
77
79
  next: [
78
80
  ...(renamed.length ? ['commit the renamed files on a branch and open a pull request (merging it names the live site)'] : []),
81
+ ...(who.id === 'grok' ? ['a Grok Bot in this folder runs the checklist from here (there is no second session to start)'] : []),
79
82
  'npm install, then npx --no-install homie-studio progress attach <hb_…> when the chat opened a build',
80
83
  ],
81
84
  };
@@ -1431,6 +1431,86 @@
1431
1431
  }
1432
1432
  }
1433
1433
  }
1434
+ },
1435
+ "0.30.0": {
1436
+ "files": {
1437
+ "CLAUDE.md": "ddf2b2a0376202f6",
1438
+ "README.md": "684b3cca8001673c",
1439
+ "HANDOFF.md": "ce7a8fd91c6bbee2",
1440
+ "games/README.md": "5ad32fe80c61f117",
1441
+ "music/README.md": "a3dc042dadd39df8",
1442
+ "videos/README.md": "16babc3437d5545b",
1443
+ "posts/README.md": "37841aae476bd540",
1444
+ "site/README.md": "2928ca4f079ab76b",
1445
+ "site/src/worker.mjs": "ca3f1630f2420ad3"
1446
+ },
1447
+ "sections": {
1448
+ "AGENTS.md": {
1449
+ "(intro)": "214aa46eb3a895d5",
1450
+ "## Layout": "8baf49e4291e7e88",
1451
+ "## Commands (all through the pinned CLI in node_modules)": "d80ec56e4b4fc4c6",
1452
+ "## The site": "366ecce1a424837a",
1453
+ "## Making games": "abf2f0d59341512d",
1454
+ "## The Game Codex and progress": "0738ef719cef3a4c",
1455
+ "## Art direction and models": "aaea65236db52126",
1456
+ "## Running live games (the back office)": "4aa5f513a7cd2c3b",
1457
+ "## Servers and AI seats": "98d08705087a33ec",
1458
+ "## Selling things (the shop)": "b04b0a2b25f524f2",
1459
+ "## Continuing a build from the Claude app": "0d68081e924ca419",
1460
+ "## Rules": "7e8458adc1b81ece",
1461
+ "## Telling Homie": "80b46fdd9e578db8",
1462
+ "## Beta": "9d02638207a26e73"
1463
+ }
1464
+ },
1465
+ "named": {
1466
+ "Homie Arcade": {
1467
+ "sections": {
1468
+ "AGENTS.md": {
1469
+ "## Commands (all through the pinned CLI in node_modules)": "b35df2c19cd4b534"
1470
+ }
1471
+ }
1472
+ }
1473
+ }
1474
+ },
1475
+ "0.30.1": {
1476
+ "files": {
1477
+ "CLAUDE.md": "ddf2b2a0376202f6",
1478
+ "README.md": "684b3cca8001673c",
1479
+ "HANDOFF.md": "ce7a8fd91c6bbee2",
1480
+ "games/README.md": "5ad32fe80c61f117",
1481
+ "music/README.md": "a3dc042dadd39df8",
1482
+ "videos/README.md": "16babc3437d5545b",
1483
+ "posts/README.md": "37841aae476bd540",
1484
+ "site/README.md": "2928ca4f079ab76b",
1485
+ "site/src/worker.mjs": "ca3f1630f2420ad3"
1486
+ },
1487
+ "sections": {
1488
+ "AGENTS.md": {
1489
+ "(intro)": "214aa46eb3a895d5",
1490
+ "## Layout": "8baf49e4291e7e88",
1491
+ "## Commands (all through the pinned CLI in node_modules)": "d80ec56e4b4fc4c6",
1492
+ "## The site": "366ecce1a424837a",
1493
+ "## Making games": "abf2f0d59341512d",
1494
+ "## The Game Codex and progress": "0738ef719cef3a4c",
1495
+ "## Art direction and models": "aaea65236db52126",
1496
+ "## Running live games (the back office)": "4aa5f513a7cd2c3b",
1497
+ "## Servers and AI seats": "98d08705087a33ec",
1498
+ "## Selling things (the shop)": "b04b0a2b25f524f2",
1499
+ "## Continuing a build from the Claude app": "0d68081e924ca419",
1500
+ "## Rules": "7e8458adc1b81ece",
1501
+ "## Telling Homie": "80b46fdd9e578db8",
1502
+ "## Beta": "9d02638207a26e73"
1503
+ }
1504
+ },
1505
+ "named": {
1506
+ "Homie Arcade": {
1507
+ "sections": {
1508
+ "AGENTS.md": {
1509
+ "## Commands (all through the pinned CLI in node_modules)": "b35df2c19cd4b534"
1510
+ }
1511
+ }
1512
+ }
1513
+ }
1434
1514
  }
1435
1515
  }
1436
1516
  }
package/mcp/ui/bridge.js CHANGED
@@ -75,13 +75,26 @@ var Card = (function () {
75
75
  });
76
76
  }
77
77
  function textOf(r) { return (r && r.content && r.content[0] && r.content[0].text) || ''; }
78
+ // An answer's own words, for a card that has nothing of its own to draw from it: short enough to stay a card.
79
+ function words(r) {
80
+ var j = r && r.structuredContent;
81
+ if (j && j.kind === 'job' && j.label) return j.label + (j.state === 'running' ? ' is still running (' + (j.seconds || 0) + ' s so far). Ask again in a moment.' : ': ' + (j.state || 'done') + '.');
82
+ var t = textOf(r).trim(); return t.length > 1500 ? t.slice(0, 1500).replace(/\s+\S*$/, '') + ' …' : t;
83
+ }
78
84
  function render() {
79
85
  if (!spec) return;
80
- var card;
86
+ var card = null;
81
87
  if (!S.data) {
82
- card = el('main', 'card');
83
- if (S.result && S.result.isError) add(card, top(spec.label, pill('Not done', 'bad')), el('p', 'note bad', textOf(S.result) || 'That did not work.'));
84
- else add(card, top(spec.label, pill('Loading', 'live', true)), el('div', 'skel'), el('div', 'skel'));
88
+ // The tool answered, but not with this card's own data (a plan, a job still running, plain words). The card
89
+ // may draw that answer itself (spec.other); else it shows the answer's words. "Loading" is only for a tool
90
+ // that has not answered yet: an answered card never stays on the skeleton.
91
+ if (S.result && !S.result.isError && spec.other) { try { card = spec.other(S.result.structuredContent || {}, S.result) || null; } catch (e) { card = null; } }
92
+ if (!card) {
93
+ card = el('main', 'card');
94
+ if (S.result && S.result.isError) add(card, top(spec.label, pill('Not done', 'bad')), el('p', 'note bad', textOf(S.result) || 'That did not work.'));
95
+ else if (S.result) add(card, top(spec.label), el('p', 'note words', words(S.result) || 'Done.'));
96
+ else add(card, top(spec.label, pill('Loading', 'live', true)), el('div', 'skel'), el('div', 'skel'));
97
+ }
85
98
  } else {
86
99
  try { card = spec.render(S.data, S.result); } catch (e) { card = add(el('main', 'card'), top(spec.label), el('p', 'note bad', String(e && e.message || e))); }
87
100
  if (S.result && S.result.isError && S.data) card.appendChild(el('p', 'note bad', textOf(S.result)));
@@ -131,7 +144,10 @@ var Card = (function () {
131
144
  size: size, render: render, take: take, fullscreenButton: fullscreenButton, textOf: textOf,
132
145
  ctx: function () { return ctx; },
133
146
  state: S,
134
- /** spec: { kind, label, render(data, result) -> element, poll(data) -> { tool, args, every } | null } */
147
+ /**
148
+ * spec: { kind, label, render(data, result) -> element, poll(data) -> { tool, args, every } | null,
149
+ * other(structured, result) -> element | null: an answer of another kind this card draws itself }
150
+ */
135
151
  start: function (s) {
136
152
  spec = s;
137
153
  rpc('ui/initialize', {
package/mcp/ui/build.js CHANGED
@@ -1,5 +1,6 @@
1
1
  /* The build card: one build's stages, each check going green, a picture and where to play it, money against the
2
- * budget, and Stop. It follows the build every 2 s while it runs and tells the model once when it ends. */
2
+ * budget, and Stop. It follows the build every 2 s while it runs and tells the model once when it ends. A deploy's
3
+ * plan (studio_deploy with plan: true) is drawn here too: what going online makes and costs, before it is made. */
3
4
  (function () {
4
5
  var C = Card;
5
6
  var ICON = { pass: '✓', done: '✓', fail: '✕', failed: '✕', running: '●', pending: '·', skip: '–', skipped: '–', stopped: '■' };
@@ -56,8 +57,28 @@
56
57
  }
57
58
  return card;
58
59
  }
60
+ // The deploy plan (studio_deploy with plan: true): what going online makes on the person's own Cloudflare and what
61
+ // it costs, before anything is made. It is the same tool as the deploy, so the app draws this card for it too.
62
+ function plan(p) {
63
+ var card = C.el('main', 'card');
64
+ C.add(card, C.top('Build', C.pill('Plan', '')), C.el('h1', '', (p.studio || 'The studio') + ': going online'),
65
+ C.el('p', 'sub', 'On your own Cloudflare account. Nothing has been made or changed yet.'));
66
+ var ul = C.el('ul', 'list');
67
+ (p.cloudflare || []).filter(function (x) { return x && x.name; }).forEach(function (x) {
68
+ var made = /^exists/.test(x.state || '');
69
+ ul.appendChild(C.add(C.el('li', made ? 'state-ok' : ''), C.el('span', 'cn', made ? '✓' : '+'),
70
+ C.add(C.el('div', 'cb'), C.el('b', '', x.kind + ' ' + x.name), C.el('span', '', x.state + (x.plan ? ' · ' + x.plan : '')))));
71
+ });
72
+ card.appendChild(ul);
73
+ if (p.cost) card.appendChild(C.el('p', 'note', p.cost));
74
+ card.appendChild(C.el('p', 'fine', 'Cloudflare asks you to approve once in your browser if this computer is not signed in (a free account works).'));
75
+ if (p.directory && p.directory.stores) card.appendChild(C.el('p', 'fine', 'The Homie directory stores ' + p.directory.stores));
76
+ if (p.never) card.appendChild(C.el('p', 'fine', p.never));
77
+ return card;
78
+ }
59
79
  C.start({
60
80
  kind: 'build', label: 'Build', fullscreen: true, render: render,
81
+ other: function (sc) { return sc && sc.kind === 'deploy-plan' ? plan(sc) : null; },
61
82
  poll: function (sc) { return sc.feed && sc.feed.state === 'running' ? { tool: 'build_progress', args: { build: sc.build }, every: 2000 } : null; },
62
83
  });
63
84
  })();
package/mcp/ui/card.css CHANGED
@@ -20,6 +20,7 @@ h2{margin:16px 0 8px;font:600 13px/1.2 var(--f-sans);color:var(--c-fg2);text-tra
20
20
  .sub{margin:0 0 12px;color:var(--c-fg2);overflow-wrap:anywhere}
21
21
  .note{margin:10px 0 0;padding:10px 12px;border-radius:var(--r-md);background:var(--c-bg2);color:var(--c-fg2)}
22
22
  .note.bad{background:var(--c-badbg);color:var(--c-bad)}.note.ok{background:var(--c-okbg);color:var(--c-ok);font-weight:600}
23
+ .note.words{margin-top:0;white-space:pre-wrap;overflow-wrap:anywhere}
23
24
  .fine{margin:10px 0 0;font-size:12px;color:var(--c-fg3)}
24
25
  .pill{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:var(--r-full);font:600 12px/1.2 var(--f-sans);background:var(--c-bg2);color:var(--c-fg2);white-space:nowrap}
25
26
  .pill.ok{background:var(--c-okbg);color:var(--c-ok)}.pill.bad{background:var(--c-badbg);color:var(--c-bad)}.pill.warn{background:var(--c-warnbg);color:var(--c-warn)}
package/mcp/ui/setup.js CHANGED
@@ -21,8 +21,8 @@
21
21
  var install = sc.install;
22
22
  var head = install && install.state === 'running' ? C.pill('Installing · ' + install.seconds + ' s', 'live', true)
23
23
  : cur ? C.pill(cur.games.length ? cur.games.length + (cur.games.length === 1 ? ' game' : ' games') : 'No game yet', cur.games.length ? 'ok' : '') : C.pill('New studio', '');
24
- C.add(card, C.top('Studio setup', head), C.el('h1', '', cur ? cur.name : 'Set up a studio'),
25
- C.el('p', 'sub', cur ? (cur.games.length ? 'On this computer, in ' + cur.folder : 'On this computer, in ' + cur.folder + '. Its home page says "First game coming soon".') : 'Studios live in ' + (sc.studiosDir || 'the folder set in the Homie extension') + '.'));
24
+ C.add(card, C.top('Studio setup', head), C.el('h1', '', cur ? cur.name : sc.wanted || 'Set up a studio'),
25
+ C.el('p', 'sub', cur ? (cur.games.length ? 'On this computer, in ' + cur.folder : 'On this computer, in ' + cur.folder + '. Its home page says "First game coming soon".') : (sc.wanted ? 'Not made yet. ' : '') + 'Studios live in ' + (sc.studiosDir || 'the folder set in the Homie extension') + '.'));
26
26
  var ol = C.el('ol', 'list');
27
27
  (sc.checklist || []).forEach(function (s) { ol.appendChild(step(s)); });
28
28
  card.appendChild(ol);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homie-rocks/studio",
3
- "version": "0.29.0",
3
+ "version": "0.30.1",
4
4
  "description": "A studio for games, music and video on its own Cloudflare: scaffold, multiplayer starters on the netplay contract, a port toolkit that makes existing single-player web games multiplayer, the site Worker with public rooms (Table/Lobby Durable Objects and D1, all on the free Workers plan, no payment method), a site with the hub's sections (Home, Games, Music, Videos, Rooms, Posts with feeds) in the studio's own look, an epic landing page for every game, big songs and videos in the studio's own R2 once it has storage (uploaded, checked by SHA-256, served at the same addresses), deploy, and the homie.rocks directory.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
package/worker/index.mjs CHANGED
@@ -589,7 +589,7 @@ async function route(request, env, ctx) {
589
589
  const getAll = () => (catP ??= catalogue(env, url.origin).then((cat) => named(cat, env)));
590
590
  const getCat = () => (pubP ??= Promise.all([getAll(), settingsOf(env)]).then(([cat, settings]) => publicCatalogue(cat, settings, env)));
591
591
 
592
- // "Connect to Claude" (the template's first-run band): the directory's setup page, for this site's address.
592
+ // "Connect this chat" (the template's first-run band): the directory's setup page, for this site's address.
593
593
  if (path === '/_studio/connect' && read) {
594
594
  const directory = directoryOf(await getCat());
595
595
  if (!directory || env.HOMIE_PREVIEW === '1') return json({ ok: false, error: 'no-directory', message: 'This studio is not connected to a directory.' }, 404);
@@ -1,2 +1,2 @@
1
1
  /** The @homie-rocks/studio version this Worker code is (package.json's; a test keeps them equal). Cache-busts /_homie/site.js. */
2
- export const STUDIO_VERSION_TAG = '0.29.0';
2
+ export const STUDIO_VERSION_TAG = '0.30.1';