@homie-rocks/studio 0.51.3 → 0.51.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/README.md +10 -29
- package/apps/APPS.md +1 -1
- package/bin/homie-studio.mjs +28 -30
- package/docs/mcp-events.md +68 -0
- package/guides/app/SKILL.md +25 -1
- package/guides/art/SKILL.md +68 -140
- package/guides/art/scripts/art.mjs +6 -79
- package/guides/models/SKILL.md +73 -177
- package/guides/models/references/RIGHTS.md +1 -1
- package/guides/models/scripts/models.mjs +7 -288
- package/guides/music/SKILL.md +74 -183
- package/guides/music/references/COMPOSITION.md +21 -26
- package/guides/music/references/GAME-AUDIO.md +3 -3
- package/guides/music/references/RIGHTS.md +12 -15
- package/guides/music/scripts/music.mjs +11 -202
- package/guides/office/SKILL.md +24 -0
- package/guides/parts/SKILL.md +1 -1
- package/guides/servers/SKILL.md +24 -0
- package/guides/shop/SKILL.md +68 -77
- package/guides/sound/SKILL.md +70 -1
- package/guides/sound/scripts/player/sound.js +233 -0
- package/guides/studio-setup/SKILL.md +11 -27
- package/guides/studio-setup/references/OFFICIAL-MEDIA.md +66 -0
- package/guides/style/SKILL.md +66 -3
- package/guides/video/SKILL.md +83 -0
- package/guides/video/references/DELIVERY.md +73 -0
- package/guides/video/references/HONESTY.md +40 -0
- package/guides/video/references/METHOD.md +144 -0
- package/guides/video/references/RECORD.md +137 -0
- package/guides/video/references/TRAILER.md +154 -0
- package/guides/video/references/examples/site-walkthrough.json +14 -0
- package/guides/video/references/examples/studio-play-phone.json +15 -0
- package/guides/video/references/examples/studio-play.json +17 -0
- package/guides/video/scripts/capture-game.mjs +175 -0
- package/guides/video/scripts/clock.js +136 -0
- package/guides/video/scripts/drawover/film.js +58 -0
- package/guides/video/scripts/drawover/index.html +9 -0
- package/guides/video/scripts/drawover/look.js +115 -0
- package/guides/video/scripts/drawover/shots.json +12 -0
- package/guides/video/scripts/lib/browser.mjs +87 -0
- package/guides/video/scripts/lib/frames.mjs +170 -0
- package/guides/video/scripts/lib/remix.mjs +159 -0
- package/guides/video/scripts/qa.mjs +127 -0
- package/guides/video/scripts/record-fixed.mjs +403 -0
- package/guides/video/scripts/record-page.mjs +477 -0
- package/guides/video/scripts/tap.js +108 -0
- package/guides/video/scripts/video.mjs +888 -0
- package/lib/build.mjs +12 -9
- package/lib/cloudflare.mjs +6 -11
- package/lib/dev.mjs +1 -2
- package/lib/doctor.mjs +10 -54
- package/lib/functions-build.mjs +8 -1
- package/lib/mcp-tools.mjs +42 -21
- package/lib/mcp.mjs +1 -1
- package/lib/media-import.mjs +41 -0
- package/lib/media-workflows.mjs +13 -0
- package/lib/official-media.mjs +15 -0
- package/lib/rules-build.mjs +13 -5
- package/lib/scaffold.mjs +2 -2
- package/lib/shop-links.mjs +54 -18
- package/lib/shop-listen.mjs +43 -0
- package/lib/shop-local.mjs +1 -1
- package/lib/shop-mcp.mjs +58 -0
- package/lib/stripe-connect.mjs +36 -15
- package/lib/template-history.json +87 -0
- package/lib/upgrade-media.mjs +13 -0
- package/lib/upgrade.mjs +10 -0
- package/package.json +1 -1
- package/parts/PARTS.md +1 -1
- package/rules/view.ts +4 -0
- package/shop/SHOP.md +66 -34
- package/worker/build-id.mjs +1 -1
- package/worker/event-resources.mjs +12 -3
- package/worker/event-webhooks.mjs +14 -9
- package/worker/functions.mjs +11 -6
- package/worker/index.mjs +5 -1
- package/worker/mcp-event-streams.mjs +58 -0
- package/worker/mcp-events-wire.mjs +41 -0
- package/worker/mcp-events.mjs +132 -0
- package/worker/mcp.mjs +4 -1
- package/worker/purchase-reconciliation.mjs +16 -1
- package/worker/shop-subscriptions.mjs +2 -0
- package/worker/shop.mjs +7 -5
- package/worker/stripe-ownership.mjs +7 -0
- package/worker/version.mjs +1 -1
- package/guides/models/references/models.json +0 -175
- package/guides/music/scripts/lib/eleven.mjs +0 -260
- package/lib/projects-env.mjs +0 -68
- package/lib/projects.mjs +0 -311
- package/lib/provider-key.mjs +0 -21
|
@@ -1,31 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
*
|
|
4
|
-
* Music through the creator's own account, mastered, looped on bar lines, and
|
|
5
|
-
* published as a song page on the studio's site.
|
|
6
|
-
*
|
|
7
|
-
* check the provider road, the plan, credits left, the rights, ffmpeg, the studio
|
|
8
|
-
* quote --seconds <s> [--vocals] the credit cost before anything is rendered
|
|
9
|
-
* budget <slug> --cap <credits> the cap the person approved for this song (refuses anything past it)
|
|
10
|
-
* plan <slug> --spec <spec.json> a composition plan with bar-exact sections (free, local)
|
|
11
|
-
* render <slug> [--dry-run] [--yes] ONE paid render of the plan (or --prompt/--seconds); --dry-run sends nothing
|
|
12
|
-
* lyrics <slug> [--render <n>] the transcription check: is every planned line actually sung?
|
|
13
|
-
* reconcile <slug> settle receipts the account had not reported yet (its balance can lag)
|
|
14
|
-
* master <slug> [--lufs -14] [--tp -1] two-pass loudness to a target, measured after
|
|
15
|
-
* loop <slug> --bars <n> [--from-bar <k>] a seamless loop cut on bar lines (WAV + OGG), seam measured
|
|
16
|
-
* stems <slug> [--yes] stems from the provider (paid; priced by measurement)
|
|
17
|
-
* analyze <file> duration, loudness, tempo and beat grid of any audio file
|
|
18
|
-
* add <slug> --title "<t>" [--blurb] [--kind song|score|loop] [--for-game <id>] [--publish]
|
|
19
|
-
* the entry in music/manifest.json from what is in music/<slug>/
|
|
20
|
-
* publish <slug> media to R2 when the studio has it, rebuild and redeploy, check the page
|
|
21
|
-
*
|
|
22
|
-
* Everything prints a few lines; --json prints the result. Nothing prints a key.
|
|
23
|
-
*/
|
|
2
|
+
/** music.mjs: local asset preparation only. Generation, schemas and pricing belong to the official provider MCP.
|
|
3
|
+
* Read ../SKILL.md for the current workflow. */
|
|
24
4
|
import { spawnSync } from 'node:child_process';
|
|
25
5
|
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
26
6
|
import { join, resolve } from 'node:path';
|
|
27
7
|
import { beatsOf, decodeMono, loudness, need, probe, run } from './lib/audio.mjs';
|
|
28
|
-
|
|
8
|
+
|
|
29
9
|
import { SLUG, checkBudget, getEntry, readBudget, readJson, receipt, rel, requireStudio, setBudget, studioHasMediaPages, upsertEntry, writeJson } from './lib/studio.mjs';
|
|
30
10
|
|
|
31
11
|
const argv = process.argv.slice(2);
|
|
@@ -55,12 +35,7 @@ function jobDir(root, slug) {
|
|
|
55
35
|
/* ---------------------------------------------------------------- check / quote / budget */
|
|
56
36
|
|
|
57
37
|
async function check() {
|
|
58
|
-
const
|
|
59
|
-
const out = { ok: true, command: 'check', road: r.road, cli: r.cli ?? null, ffmpeg: need('ffmpeg') && need('ffprobe') };
|
|
60
|
-
if (!r.road) { out.ok = false; out.why = r.why; }
|
|
61
|
-
else {
|
|
62
|
-
try { const s = await subscription(r); out.plan = s; out.rights = rightsFor(s.tier); } catch (error) { out.ok = false; out.why = error.message; }
|
|
63
|
-
}
|
|
38
|
+
const out = { ok: true, command: 'check', ffmpeg: need('ffmpeg') && need('ffprobe'), generation: 'Use the official ElevenLabs MCP in your AI client' };
|
|
64
39
|
let root = null;
|
|
65
40
|
try { root = requireStudio(); } catch (error) { out.studio = { ok: false, why: error.message }; }
|
|
66
41
|
if (root) out.studio = { ok: true, root, pages: studioHasMediaPages(root) };
|
|
@@ -68,18 +43,6 @@ async function check() {
|
|
|
68
43
|
return out;
|
|
69
44
|
}
|
|
70
45
|
|
|
71
|
-
async function quote() {
|
|
72
|
-
const seconds = Number(flags.get('seconds'));
|
|
73
|
-
if (!(seconds >= 3 && seconds <= 600)) throw new Error('--seconds must be 3..600 (ElevenLabs Music renders 3 s to 10 min)');
|
|
74
|
-
const q = quoteCredits(seconds, { vocals: flags.has('vocals') });
|
|
75
|
-
let plan = null;
|
|
76
|
-
try { plan = await subscription(); } catch { /* quote without the account */ }
|
|
77
|
-
const fits = plan ? q.total <= plan.remaining : null;
|
|
78
|
-
const cap = flags.has('budget') ? Number(flags.get('budget')) : null;
|
|
79
|
-
const longest = cap ? Math.floor(cap / (q.total / seconds)) : null;
|
|
80
|
-
return { ok: true, command: 'quote', seconds, credits: q, plan, fitsInPlan: fits, budget: cap, longestWithinBudget: longest };
|
|
81
|
-
}
|
|
82
|
-
|
|
83
46
|
function budget(root) {
|
|
84
47
|
const slug = pos[1];
|
|
85
48
|
const cap = Number(flags.get('cap'));
|
|
@@ -143,7 +106,7 @@ function plan(root) {
|
|
|
143
106
|
if (specFile !== join(dir, 'spec.json')) writeJson(join(dir, 'spec.json'), spec);
|
|
144
107
|
const p = buildPlan(spec);
|
|
145
108
|
writeJson(join(dir, 'plan.json'), { title: spec.title ?? slug, bpm: p.bpm, key: spec.key ?? null, beatsPerBar: p.beatsPerBar, bars: p.bars, totalMs: p.totalMs, vocals: p.vocals, lyrics: p.lyrics, composition_plan: p.plan });
|
|
146
|
-
const q =
|
|
109
|
+
const q = { status: 'unknown', next: 'Quote through the official ElevenLabs MCP before generation' };
|
|
147
110
|
return { ok: true, command: 'plan', slug, seconds: p.totalMs / 1000, bars: p.bars, bpm: p.bpm, vocals: p.vocals, sections: p.plan.chunks.map((c) => ({ text: c.text.split('\n')[0], ms: c.duration_ms })), warnings: p.warnings, quote: q, file: rel(root, join(dir, 'plan.json')) };
|
|
148
111
|
}
|
|
149
112
|
|
|
@@ -154,76 +117,9 @@ function plan(root) {
|
|
|
154
117
|
* +445 a minute later). Wait up to ~40 s for it to move; null when it has not (the receipt then holds
|
|
155
118
|
* the quote and is marked pending, and `reconcile` settles it later).
|
|
156
119
|
*/
|
|
157
|
-
async function settle(r, beforeUsed) {
|
|
158
|
-
for (let i = 0; i < 10; i++) {
|
|
159
|
-
const s = await subscription(r).catch(() => null);
|
|
160
|
-
if (s && s.used > beforeUsed) return { delta: s.used - beforeUsed, after: s };
|
|
161
|
-
await new Promise((ok) => setTimeout(ok, Number(process.env.MUSIC_SETTLE_MS ?? 4000)));
|
|
162
|
-
}
|
|
163
|
-
return { delta: null, after: await subscription(r).catch(() => null) };
|
|
164
|
-
}
|
|
165
120
|
|
|
166
|
-
function nextRender(dir) {
|
|
167
|
-
const ns = readdirSync(join(dir, 'work')).map((f) => /^render-(\d+)\./.exec(f)?.[1]).filter(Boolean).map(Number);
|
|
168
|
-
return ns.length ? Math.max(...ns) + 1 : 1;
|
|
169
|
-
}
|
|
170
121
|
|
|
171
|
-
|
|
172
|
-
const slug = pos[1];
|
|
173
|
-
const dir = jobDir(root, slug);
|
|
174
|
-
const p = readJson(join(dir, 'plan.json'), null);
|
|
175
|
-
let body; let seconds; let vocals = false;
|
|
176
|
-
if (flags.has('prompt')) {
|
|
177
|
-
seconds = Number(flags.get('seconds'));
|
|
178
|
-
if (!(seconds >= 3 && seconds <= 600)) throw new Error('--seconds 3..600 with --prompt');
|
|
179
|
-
if (!flags.has('instrumental')) throw new Error('a prompt-only render is for instrumentals (--instrumental): words go through a composition plan (`plan`), because a prompt with lyrics has come back with nothing sung');
|
|
180
|
-
body = { prompt: String(flags.get('prompt')), music_length_ms: Math.round(seconds * 1000), force_instrumental: true, model_id: 'music_v2' };
|
|
181
|
-
} else {
|
|
182
|
-
if (!p) throw new Error(`no plan for ${slug}: run \`plan ${slug} --spec <spec.json>\` first (or --prompt ... --instrumental --seconds n)`);
|
|
183
|
-
seconds = p.totalMs / 1000; vocals = p.vocals;
|
|
184
|
-
body = { model_id: 'music_v2', composition_plan: p.composition_plan, with_timestamps: true };
|
|
185
|
-
}
|
|
186
|
-
const q = quoteCredits(seconds, { vocals });
|
|
187
|
-
if (flags.has('dry-run')) {
|
|
188
|
-
const d = dryRun(body, { outputFormat: OUTPUT_FORMAT });
|
|
189
|
-
return { ok: d.ok, command: 'render', dryRun: true, slug, seconds, quote: q, ...d };
|
|
190
|
-
}
|
|
191
|
-
const b = checkBudget(dir, q.music, { provider: 'elevenlabs', unit: 'credits' });
|
|
192
|
-
if (!flags.has('yes')) {
|
|
193
|
-
return { ok: false, command: 'render', slug, needs: 'approval', seconds, quote: q, budget: { cap: b.cap, spent: b.spent }, why: `This render costs about ${q.music} credits (${q.basis}). Tell the person that, and render with --yes once they say go.` };
|
|
194
|
-
}
|
|
195
|
-
const n = nextRender(dir);
|
|
196
|
-
const pending = join(dir, 'work', `render-${n}.pending`);
|
|
197
|
-
const stale = readdirSync(join(dir, 'work')).find((f) => f.endsWith('.pending'));
|
|
198
|
-
if (stale && !flags.has('again')) throw new Error(`work/${stale} says an earlier render was sent and never came back; it may have been billed. Check the account's history, then run again with --again if the person agrees`);
|
|
199
|
-
if (stale) rmSync(join(dir, 'work', stale));
|
|
200
|
-
const r = road();
|
|
201
|
-
const before = await subscription(r);
|
|
202
|
-
if (before.remaining < q.music) throw new Error(`the account has ${before.remaining} credits left and this needs about ${q.music}; nothing was sent (ElevenLabs does not go over on this plan: ${before.canGoOver ? 'it can' : 'it cannot'})`);
|
|
203
|
-
writeJson(pending, { at: new Date().toISOString(), seconds, quote: q.music });
|
|
204
|
-
say(`rendering ${seconds} s (about ${q.music} credits)…`);
|
|
205
|
-
const started = Date.now();
|
|
206
|
-
const res = await composeDetailed(body, { outputFormat: OUTPUT_FORMAT });
|
|
207
|
-
rmSync(pending, { force: true });
|
|
208
|
-
if (!res.audio?.length) throw new Error('the answer had no audio in it');
|
|
209
|
-
const audio = join(dir, 'work', `render-${n}.mp3`);
|
|
210
|
-
writeFileSync(audio, res.audio);
|
|
211
|
-
writeJson(join(dir, 'work', `render-${n}.json`), { songId: res.songId, composition_plan: res.json?.composition_plan ?? null, song_metadata: res.json?.song_metadata ?? null, words: res.json?.words_timestamps ?? null });
|
|
212
|
-
const { delta: measured, after } = await settle(r, before.used);
|
|
213
|
-
const facts = probe(audio);
|
|
214
|
-
const rights = rightsFor(before.tier);
|
|
215
|
-
// The budget counts the larger of the quote and the measurement until the account has reported the real cost.
|
|
216
|
-
const line = receipt(root, dir, {
|
|
217
|
-
provider: 'elevenlabs', model: 'music_v2', requestId: res.songId, cost: measured ?? q.music, unit: 'credits', pending: measured === null,
|
|
218
|
-
quoted: q.music, measured, balanceBefore: before.used, seconds: facts.duration, artifact: rel(root, audio), plan: before.tier, rights,
|
|
219
|
-
});
|
|
220
|
-
const out = { n, file: rel(root, audio), seconds: facts.duration, askedSeconds: seconds, songId: res.songId, words: res.json?.words_timestamps?.length ?? 0, creditsQuoted: q.music, creditsMeasured: measured, creditsLeft: after?.remaining ?? null, tier: before.tier, rights, ms: Date.now() - started, receiptAt: line.at };
|
|
221
|
-
writeJson(join(dir, 'render.json'), { ...out, at: new Date().toISOString() });
|
|
222
|
-
const note = measured === null
|
|
223
|
-
? 'the account had not reported this render\'s cost yet: the quote is counted against the budget; run `reconcile` in a minute for the real number'
|
|
224
|
-
: Math.abs(measured - q.music) > q.music * 0.5 ? 'the measured cost is far from the quote: another job on the same account may have spent at the same time' : undefined;
|
|
225
|
-
return { ok: true, command: 'render', slug, ...out, note };
|
|
226
|
-
}
|
|
122
|
+
|
|
227
123
|
|
|
228
124
|
/* ---------------------------------------------------------------- lyrics check */
|
|
229
125
|
|
|
@@ -264,32 +160,6 @@ export function matchLyrics(lines, heardWords, { pass = 0.75 } = {}) {
|
|
|
264
160
|
return rows;
|
|
265
161
|
}
|
|
266
162
|
|
|
267
|
-
async function lyricsCheck(root) {
|
|
268
|
-
const slug = pos[1];
|
|
269
|
-
const dir = jobDir(root, slug);
|
|
270
|
-
const p = readJson(join(dir, 'plan.json'), null);
|
|
271
|
-
if (!p?.vocals) throw new Error('this song has no sung lines in its plan');
|
|
272
|
-
const last = readJson(join(dir, 'render.json'), null);
|
|
273
|
-
const n = flags.has('render') ? Number(flags.get('render')) : last?.n;
|
|
274
|
-
const file = flags.get('file') ? resolve(String(flags.get('file'))) : join(dir, 'work', `render-${n}.mp3`);
|
|
275
|
-
if (!existsSync(file)) throw new Error(`no audio at ${rel(root, file)}`);
|
|
276
|
-
const seconds = probe(file).duration;
|
|
277
|
-
const cost = Math.ceil(seconds * 0.5);
|
|
278
|
-
const b = readBudget(dir);
|
|
279
|
-
if (b) checkBudget(dir, cost, { provider: 'elevenlabs', unit: 'credits' });
|
|
280
|
-
const r = road();
|
|
281
|
-
const before = await subscription(r).catch(() => null);
|
|
282
|
-
const t = await transcribe(file, { language: flags.get('language') ?? null });
|
|
283
|
-
const measured = before ? (await settle(r, before.used)).delta : null;
|
|
284
|
-
receipt(root, dir, { provider: 'elevenlabs', model: 'scribe_v2', requestId: t.transcription_id ?? null, cost: measured ?? cost, unit: 'credits', pending: measured === null, quoted: cost, measured, balanceBefore: before?.used ?? null, seconds, artifact: rel(root, file) });
|
|
285
|
-
const words = (t.words ?? []).filter((w) => (w.type ?? 'word') === 'word');
|
|
286
|
-
const lines = p.lyrics.flatMap((s) => s.lines);
|
|
287
|
-
const rows = matchLyrics(lines, words);
|
|
288
|
-
const out = { ok: rows.every((x) => x.ok), command: 'lyrics', slug, file: rel(root, file), heard: t.text ?? '', lines: rows, missing: rows.filter((x) => !x.ok).map((x) => x.line), creditsMeasured: measured };
|
|
289
|
-
writeJson(join(dir, `lyrics-check-${n ?? 'file'}.json`), out);
|
|
290
|
-
return out;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
163
|
/* ---------------------------------------------------------------- master */
|
|
294
164
|
|
|
295
165
|
function master(root) {
|
|
@@ -396,58 +266,13 @@ function loop(root) {
|
|
|
396
266
|
|
|
397
267
|
/* ---------------------------------------------------------------- stems */
|
|
398
268
|
|
|
399
|
-
async function stems(root) {
|
|
400
|
-
const slug = pos[1];
|
|
401
|
-
const dir = jobDir(root, slug);
|
|
402
|
-
const src = flags.get('in') ? resolve(String(flags.get('in'))) : join(dir, `${slug}-master.wav`);
|
|
403
|
-
if (!existsSync(src)) throw new Error('stems of what? master first, or --in <file>');
|
|
404
|
-
const est = Math.ceil(probe(src).duration * 10);
|
|
405
|
-
checkBudget(dir, est, { provider: 'elevenlabs', unit: 'credits' });
|
|
406
|
-
if (!flags.has('yes')) return { ok: false, command: 'stems', needs: 'approval', why: `Stem separation is paid and its price is not published per second; this job's budget allows it (a guess of ${est} credits is held against the cap). Ask the person, then run with --yes.` };
|
|
407
|
-
const r = road();
|
|
408
|
-
const before = await subscription(r);
|
|
409
|
-
const zip = join(dir, 'work', 'stems.zip');
|
|
410
|
-
await separateStems(src, zip);
|
|
411
|
-
const after = await subscription(r).catch(() => null);
|
|
412
|
-
const measured = after ? after.used - before.used : null;
|
|
413
|
-
receipt(root, dir, { provider: 'elevenlabs', model: 'stem-separation', cost: measured ?? est, unit: 'credits', estimated: measured === null, artifact: rel(root, zip) });
|
|
414
|
-
const outDir = join(dir, 'stems');
|
|
415
|
-
mkdirSync(outDir, { recursive: true });
|
|
416
|
-
const u = spawnSync('unzip', ['-o', '-q', zip, '-d', outDir]);
|
|
417
|
-
if (u.status !== 0) throw new Error('could not unzip the stems');
|
|
418
|
-
return { ok: true, command: 'stems', slug, files: readdirSync(outDir).map((f) => rel(root, join(outDir, f))), creditsMeasured: measured };
|
|
419
|
-
}
|
|
420
|
-
|
|
421
269
|
/* ---------------------------------------------------------------- reconcile */
|
|
422
270
|
|
|
423
271
|
/**
|
|
424
272
|
* Settle pending receipts: the account's usage now, minus its usage before this song's first pending call,
|
|
425
273
|
* is what those calls cost (plus anything else the account did meanwhile, which is said, not hidden).
|
|
426
274
|
*/
|
|
427
|
-
|
|
428
|
-
const slug = pos[1];
|
|
429
|
-
const dir = jobDir(root, slug);
|
|
430
|
-
const b = readJson(join(dir, 'budget.json'), null);
|
|
431
|
-
if (!b) throw new Error('this song has no budget.json');
|
|
432
|
-
const pendingIdx = b.calls.map((c, i) => (c.pending ? i : -1)).filter((i) => i >= 0);
|
|
433
|
-
const now = await subscription();
|
|
434
|
-
if (!pendingIdx.length) return { ok: true, command: 'reconcile', slug, pending: 0, spent: b.spent, creditsLeft: now.remaining };
|
|
435
|
-
const first = b.calls[pendingIdx[0]];
|
|
436
|
-
if (first.balanceBefore == null) throw new Error('the pending call has no balance reading to settle from');
|
|
437
|
-
const settledBefore = b.calls.slice(pendingIdx[0]).filter((c) => !c.pending).reduce((a, c) => a + (c.cost || 0), 0);
|
|
438
|
-
const delta = now.used - first.balanceBefore - settledBefore;
|
|
439
|
-
const quoted = pendingIdx.reduce((a, i) => a + (b.calls[i].quoted ?? b.calls[i].cost), 0);
|
|
440
|
-
if (delta <= 0) return { ok: false, command: 'reconcile', slug, why: 'the account still shows no change since the render; try again in a minute' };
|
|
441
|
-
for (const i of pendingIdx) {
|
|
442
|
-
const c = b.calls[i];
|
|
443
|
-
const share = Math.round(delta * ((c.quoted ?? c.cost) / quoted));
|
|
444
|
-
c.measured = share; c.cost = share; c.pending = false; c.reconciledAt = new Date().toISOString();
|
|
445
|
-
}
|
|
446
|
-
b.spent = b.calls.reduce((a, c) => a + (c.cost || 0), 0);
|
|
447
|
-
writeJson(join(dir, 'budget.json'), b);
|
|
448
|
-
receipt(root, dir, { provider: 'elevenlabs', model: 'reconcile', cost: 0, unit: 'credits', note: `settled ${pendingIdx.length} pending call(s): ${delta} credits measured against ${quoted} quoted (anything else the account did in between is included)`, measuredTotal: delta });
|
|
449
|
-
return { ok: true, command: 'reconcile', slug, settled: pendingIdx.length, measured: delta, quoted, spent: b.spent, cap: b.cap, creditsLeft: now.remaining };
|
|
450
|
-
}
|
|
275
|
+
|
|
451
276
|
|
|
452
277
|
/* ---------------------------------------------------------------- analyze */
|
|
453
278
|
|
|
@@ -573,27 +398,16 @@ function print(r) {
|
|
|
573
398
|
const L = [];
|
|
574
399
|
switch (r.command) {
|
|
575
400
|
case 'check':
|
|
576
|
-
L.push(`ElevenLabs: ${r.road ? `connected (${r.road === 'cli' ? `the elevenlabs CLI, ${r.cli}` : r.via === 'stripe-projects' ? 'the key Stripe Projects keeps for this studio' : r.source ?? 'ELEVENLABS_API_KEY'})` : 'not connected'}`);
|
|
577
401
|
if (r.plan) L.push(`Plan: ${r.plan.tier} (${r.plan.status}); ${r.plan.remaining} credits left of ${r.plan.limit}; resets ${r.plan.resetAt ?? '?'}; ${r.plan.canGoOver ? 'CAN go over (usage-based billing)' : 'cannot go over'}`);
|
|
578
402
|
if (r.rights) L.push(`Rights: ${r.rights.words}`);
|
|
579
403
|
L.push(`ffmpeg: ${r.ffmpeg ? 'yes' : 'NO'}`, `Studio: ${r.studio?.ok ? r.studio.root : r.studio?.why}${r.studio?.pages && !r.studio.pages.ok ? ` (song pages: ${r.studio.pages.why})` : ''}`);
|
|
580
404
|
break;
|
|
581
|
-
|
|
582
|
-
L.push(`${r.seconds} s: about ${r.credits.total} credits (${r.credits.music} music${r.credits.lyricCheck ? ` + ${r.credits.lyricCheck} lyric check` : ''}); ${r.credits.basis}.`);
|
|
583
|
-
if (r.plan) L.push(`The account has ${r.plan.remaining} credits left (${r.plan.tier}).`);
|
|
584
|
-
if (r.budget) L.push(`Within a budget of ${r.budget} credits the longest render is about ${r.longestWithinBudget} s.`);
|
|
585
|
-
break;
|
|
405
|
+
|
|
586
406
|
case 'plan':
|
|
587
|
-
L.push(`Plan: ${r.seconds} s, ${r.bars} bars at ${r.bpm} BPM${r.vocals ? ', with vocals' : ', instrumental'};
|
|
588
|
-
break;
|
|
589
|
-
case 'render':
|
|
590
|
-
if (r.dryRun) L.push(`Dry run (nothing sent): ${r.ok ? 'the request is valid' : r.why}; it would cost about ${r.quote.music} credits.`);
|
|
591
|
-
else if (r.needs) L.push(r.why);
|
|
592
|
-
else L.push(`Rendered ${r.file}: ${r.seconds.toFixed(2)} s (asked ${r.askedSeconds} s). Credits: quoted ${r.creditsQuoted}, measured ${r.creditsMeasured ?? 'unknown'}; ${r.creditsLeft ?? '?'} left. Plan ${r.tier}: ${r.rights.commercial ? 'commercial licence' : 'NO commercial licence'}.`, ...(r.note ? [r.note] : []));
|
|
593
|
-
break;
|
|
594
|
-
case 'lyrics':
|
|
595
|
-
L.push(`${r.ok ? 'Every planned line is sung.' : `NOT every line is sung: ${r.missing.length} missing.`}`, ...r.lines.map((x) => ` ${x.ok ? 'ok ' : 'MISS'} ${Math.round(x.matched * 100)}% "${x.line}" heard: "${x.heard}"`));
|
|
407
|
+
L.push(`Plan: ${r.seconds} s, ${r.bars} bars at ${r.bpm} BPM${r.vocals ? ', with vocals' : ', instrumental'}; quote through the official ElevenLabs MCP.`, ...r.sections.map((s) => ` ${s.text} ${s.ms} ms`), ...r.warnings.map((w) => ` warning: ${w}`), `Wrote ${r.file}`);
|
|
596
408
|
break;
|
|
409
|
+
|
|
410
|
+
|
|
597
411
|
case 'master':
|
|
598
412
|
L.push(`Mastered to ${r.master.lufs} LUFS, ${r.master.truePeak} dBTP (target ${r.target.lufs} / ${r.target.truePeak}); the mp3 reads ${r.mp3.lufs} LUFS, ${r.mp3.truePeak} dBTP. ${r.master.file}, ${r.mp3.file}`);
|
|
599
413
|
break;
|
|
@@ -616,17 +430,12 @@ async function main() {
|
|
|
616
430
|
return null;
|
|
617
431
|
}
|
|
618
432
|
if (cmd === 'check') return check();
|
|
619
|
-
if (cmd === 'quote') return quote();
|
|
620
433
|
if (cmd === 'analyze') return analyze();
|
|
621
434
|
const root = requireStudio();
|
|
622
435
|
if (cmd === 'budget') return budget(root);
|
|
623
436
|
if (cmd === 'plan') return plan(root);
|
|
624
|
-
if (cmd === 'render') return render(root);
|
|
625
|
-
if (cmd === 'lyrics') return lyricsCheck(root);
|
|
626
|
-
if (cmd === 'reconcile') return reconcile(root);
|
|
627
437
|
if (cmd === 'master') return master(root);
|
|
628
438
|
if (cmd === 'loop') return loop(root);
|
|
629
|
-
if (cmd === 'stems') return stems(root);
|
|
630
439
|
if (cmd === 'add') return add(root);
|
|
631
440
|
if (cmd === 'publish') return publishEntry(root, 'music', pos[1], { deploy: !flags.has('no-deploy') });
|
|
632
441
|
return { ok: false, command: cmd, why: `unknown command "${cmd}" (music.mjs help)` };
|
package/guides/office/SKILL.md
CHANGED
|
@@ -156,3 +156,27 @@ in and continues there. A plain link-preview GET spends nothing; the loaded page
|
|
|
156
156
|
submits the same-origin sign-in form. Keep the link private.
|
|
157
157
|
Local office and role commands reuse the running dev server’s D1 configuration even
|
|
158
158
|
after deploy records the production database id.
|
|
159
|
+
|
|
160
|
+
## Experimental MCP Events
|
|
161
|
+
|
|
162
|
+
For an owner's AI that should hear a studio's declared events, read
|
|
163
|
+
`node_modules/@homie-rocks/studio/docs/mcp-events.md`. Connect the client to
|
|
164
|
+
`<site>/mcp/auth` through the person's existing studio approval, discover feeds
|
|
165
|
+
with `events/list`, and use `events/poll`, an SSE `events/stream`, or
|
|
166
|
+
`events/subscribe` with a client-generated `whsec_` secret and an exact
|
|
167
|
+
owner-declared `mcp.events.<feed>.receivers` URL. Refresh before `refreshBefore`,
|
|
168
|
+
persist cursors, verify Standard Webhooks signatures, deduplicate event IDs, and
|
|
169
|
+
use `events/unsubscribe` to stop a webhook. The feeds keep their current roles;
|
|
170
|
+
resources/subscribe and the cursor tool still work. This follows the experimental
|
|
171
|
+
Triggers & Events working-group draft, with the Workers DNS-pinning limitation
|
|
172
|
+
and receiver allowlist explained in that guide. Inspect Functions in the office
|
|
173
|
+
and refresh's `deliveryStatus` for failed deliveries; never put secrets in logs.
|
|
174
|
+
|
|
175
|
+
For production failures, the owner's AI can also use the person's existing
|
|
176
|
+
Cloudflare connection to `https://mcp.cloudflare.com/mcp`, discover
|
|
177
|
+
`cloudflare.alert.workers_observability_real_time_issue` with `events/list`, and
|
|
178
|
+
subscribe with `events/subscribe` scoped to the studio's Worker. Enable Workers
|
|
179
|
+
Issues and grant Notifications Write and Workers Observability Write. Cloudflare
|
|
180
|
+
posts signed events to the client's webhook so new or recurring reliability
|
|
181
|
+
problems reach the AI without polling; Homie hosts nothing for this. See
|
|
182
|
+
[Cloudflare's announcement](https://developers.cloudflare.com/changelog/post/2026-10-13-cloudflare-mcp-events/).
|
package/guides/parts/SKILL.md
CHANGED
|
@@ -155,4 +155,4 @@ payer after the purchase, then record its hash with `shop refund <order> --manua
|
|
|
155
155
|
|
|
156
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
157
|
|
|
158
|
-
Selling defaults to
|
|
158
|
+
Selling defaults to Stripe’s official plugin for shop catalog and links, with Homie’s private webhook installation. Read the shop skill. `shop connect` remains the CLI alternative for priced tool links. Never ask an owner for an API key. Machine payment paths that require runtime credentials are developer integrations, not an owner setup fallback.
|
package/guides/servers/SKILL.md
CHANGED
|
@@ -157,3 +157,27 @@ For local function and tool proof, run `dev` and use its localhost URL. The priv
|
|
|
157
157
|
dev credential permits headless owner tool calls; production still uses OAuth.
|
|
158
158
|
App need sessions are scoped to the declared role and fresh records. See the app
|
|
159
159
|
and functions guides for tablet/kiosk proof and atomic function storage.
|
|
160
|
+
|
|
161
|
+
## Experimental MCP Events
|
|
162
|
+
|
|
163
|
+
For an owner's AI that should hear a studio's declared events, read
|
|
164
|
+
`node_modules/@homie-rocks/studio/docs/mcp-events.md`. Connect the client to
|
|
165
|
+
`<site>/mcp/auth` through the person's existing studio approval, discover feeds
|
|
166
|
+
with `events/list`, and use `events/poll`, an SSE `events/stream`, or
|
|
167
|
+
`events/subscribe` with a client-generated `whsec_` secret and an exact
|
|
168
|
+
owner-declared `mcp.events.<feed>.receivers` URL. Refresh before `refreshBefore`,
|
|
169
|
+
persist cursors, verify Standard Webhooks signatures, deduplicate event IDs, and
|
|
170
|
+
use `events/unsubscribe` to stop a webhook. The feeds keep their current roles;
|
|
171
|
+
resources/subscribe and the cursor tool still work. This follows the experimental
|
|
172
|
+
Triggers & Events working-group draft, with the Workers DNS-pinning limitation
|
|
173
|
+
and receiver allowlist explained in that guide. Inspect Functions in the office
|
|
174
|
+
and refresh's `deliveryStatus` for failed deliveries; never put secrets in logs.
|
|
175
|
+
|
|
176
|
+
For production failures, the owner's AI can also use the person's existing
|
|
177
|
+
Cloudflare connection to `https://mcp.cloudflare.com/mcp`, discover
|
|
178
|
+
`cloudflare.alert.workers_observability_real_time_issue` with `events/list`, and
|
|
179
|
+
subscribe with `events/subscribe` scoped to the studio's Worker. Enable Workers
|
|
180
|
+
Issues and grant Notifications Write and Workers Observability Write. Cloudflare
|
|
181
|
+
posts signed events to the client's webhook so new or recurring reliability
|
|
182
|
+
problems reach the AI without polling; Homie hosts nothing for this. See
|
|
183
|
+
[Cloudflare's announcement](https://developers.cloudflare.com/changelog/post/2026-10-13-cloudflare-mcp-events/).
|
package/guides/shop/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: shop
|
|
3
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, a round of several items to a table, a paid booking with slot capacity, monthly giving or a membership - 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, take paid bookings, set up monthly giving or memberships, 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.
|
|
4
|
-
compatibility:
|
|
4
|
+
compatibility: Stripe's official OAuth MCP/plugin, Homie's stripe_shop_plan and stripe_webhook tools, and Wrangler. Stripe CLI is used privately for webhook setup and local forwarding.
|
|
5
5
|
metadata:
|
|
6
6
|
providers: stripe
|
|
7
7
|
---
|
|
@@ -16,7 +16,7 @@ Studio functions (`functions/FUNCTIONS.md`), with the event ID as the idempotenc
|
|
|
16
16
|
|
|
17
17
|
# The shop
|
|
18
18
|
|
|
19
|
-
Check that the toolkit exposes `
|
|
19
|
+
Check that the toolkit exposes `stripe_shop_plan` and `stripe_webhook`; upgrade older toolkits first.
|
|
20
20
|
The guide is `node_modules/@homie-rocks/studio/shop/SHOP.md`; the owner's plain words are `SELLING.md`.
|
|
21
21
|
|
|
22
22
|
**The model, said plainly to the person.** The studio sells with **its own Stripe account**. The studio is the
|
|
@@ -28,87 +28,78 @@ entity field or a command into proof of a purchase: grants still come from the s
|
|
|
28
28
|
verified server records. Room state is replicated to players; keep payment and private
|
|
29
29
|
customer data out of it. Read the game skill's RULES.md before changing game outcomes.
|
|
30
30
|
|
|
31
|
-
##
|
|
32
|
-
|
|
33
|
-
Use
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
`
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
For catalog work, analytics and advice, `stripe agent setup` installs Stripe's own agent plugin and
|
|
95
|
-
skills; its OAuth MCP is `https://mcp.stripe.com`. This is optional and separate from Worker credentials.
|
|
96
|
-
Check `get_stripe_account_info` before writes; confirm the account and mode match the studio.
|
|
97
|
-
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.
|
|
98
|
-
The tools include `stripe_api_read`, `stripe_api_write`, API discovery and `stripe_analytics`.
|
|
99
|
-
From **2026-10-31**, MCP rejects API keys without the Agent tag; OAuth remains supported. Agent keys
|
|
100
|
-
can put refunds behind Stripe approval. A returned confirmation link belongs to the owner; wait
|
|
101
|
-
for approval before retrying. Do not silently replace the payment runtime with MCP; a separate persistent MCP client would need its own production verification.
|
|
31
|
+
## Default: Stripe's official plugin
|
|
32
|
+
|
|
33
|
+
Use Stripe's official plugin, authenticated with its browser OAuth approval, for Products,
|
|
34
|
+
Prices, Payment Links, menu links, per-slot booking links, monthly giving and the customer
|
|
35
|
+
portal. Install with `stripe agent setup` if needed; the MCP URL is
|
|
36
|
+
`https://mcp.stripe.com`. Current tools are `stripe_api_search`, `stripe_api_details`,
|
|
37
|
+
`stripe_api_read` and `stripe_api_write`. Discover the operation and its schema before
|
|
38
|
+
calling it; tool argument shapes can change. See https://docs.stripe.com/mcp.
|
|
39
|
+
|
|
40
|
+
1. Create/check `shop.json`. Read the app's published slot records and supply the complete
|
|
41
|
+
slot manifest described below. Select the owner's account and sandbox in Stripe's plugin;
|
|
42
|
+
read `/v1/account` to record its account ID. Live mode only after the owner requests it.
|
|
43
|
+
2. Call Homie's `stripe_shop_plan` with `account`, `mode: "test"`, `slots` and
|
|
44
|
+
`observations: []`. CLI equivalent: `homie-studio shop plan --have plan-input.json`.
|
|
45
|
+
That JSON file contains the same fields. A returned `next` is the exact operation:
|
|
46
|
+
method, API path, parameters, API version and (for creates) idempotency key. Updates set desired fields; read before retrying. Metadata
|
|
47
|
+
scopes objects to this studio and origin; prices and links have content revisions.
|
|
48
|
+
3. Perform each `next` through Stripe's official MCP. For GET, append
|
|
49
|
+
`{ "path": "…", "params": {…}, "body": <Stripe response> }` to `observations`
|
|
50
|
+
and plan again. Follow every pagination request. After a write, discard observations
|
|
51
|
+
and read fresh. Replanning resolves real Stripe IDs; never invent IDs or substitute
|
|
52
|
+
a different price. Reuse the supplied idempotency key where the tool supports it;
|
|
53
|
+
otherwise read before retrying and match the stable product ID/revision metadata.
|
|
54
|
+
4. When no writes remain, use fresh reads with `stripe_shop_plan` `verify: true`
|
|
55
|
+
(CLI `homie-studio shop verify --have plan-input.json`). It checks the catalog,
|
|
56
|
+
actual link fields and line items, menus, slot limits and portal settings against
|
|
57
|
+
shop.json and the slot manifest. A mismatch blocks verification. It saves only public
|
|
58
|
+
configuration to `.studio/stripe-mcp.json`; local test configuration goes to `.dev.vars`.
|
|
59
|
+
5. For a deployed HTTPS studio, run `stripe_webhook` (CLI `homie-studio shop webhook`).
|
|
60
|
+
Follow `studio_job`. This uses Stripe CLI only for private webhook installation,
|
|
61
|
+
checks the same account as the MCP, and installs the signing secret and verified
|
|
62
|
+
links through Wrangler stdin. The CLI needs one separate browser approval if its
|
|
63
|
+
studio profile is not already connected. MCP approval cannot be reused or exported.
|
|
64
|
+
Cloudflare authorization and an existing deployment are prerequisites.
|
|
65
|
+
6. After installation, perform the returned `retire` operations through Stripe MCP to
|
|
66
|
+
archive replaced catalog objects, then verify fresh reads again. Test a sandbox
|
|
67
|
+
purchase and its signed event, order grant, booking capacity or recurring invoice;
|
|
68
|
+
a browser return is never proof. Stripe CLI `trigger` can send a test event, and
|
|
69
|
+
`events resend <event> --webhook-endpoint <endpoint>` retries a delivery. Check the
|
|
70
|
+
endpoint's delivery result and the app's verified records. Do not type a card into
|
|
71
|
+
a page for an automated proof; use Stripe's documented test PaymentMethods/fixtures.
|
|
72
|
+
|
|
73
|
+
Never create a webhook through model-visible Stripe MCP: its response exposes the signing secret.
|
|
74
|
+
|
|
75
|
+
The owner never handles an API key or signing secret. No Homie backend handles payments.
|
|
76
|
+
The Worker needs only public link configuration and the signing secret; buyers pay
|
|
77
|
+
Stripe directly even when the AI and the owner's computer are offline. The old
|
|
78
|
+
`stripe_login` / `shop connect` flow remains an alternative that syncs everything
|
|
79
|
+
through Stripe CLI browser approval. `--renew` renews that approval; `--live` requires
|
|
80
|
+
the owner's go-live request. Never read CLI config or print login-completion output.
|
|
81
|
+
|
|
82
|
+
For local checkout, pass `url: "http://localhost:8787"` (or the private Wi-Fi address)
|
|
83
|
+
and `mode: "test"` to the same plan/verify flow; the official plugin makes the links.
|
|
84
|
+
Run `homie-studio shop listen --profile <sandbox-profile>` in a background terminal.
|
|
85
|
+
Stripe CLI is required for forwarding; one browser approval is enough if needed.
|
|
86
|
+
The command captures and saves the listener secret privately. Start/restart
|
|
87
|
+
`homie-studio dev --lan` once it reports ready and keep the listener running.
|
|
88
|
+
Never copy the listener's raw output into the conversation.
|
|
89
|
+
|
|
90
|
+
For a faster local iteration, `homie-studio build --quick` skips generated rules play
|
|
91
|
+
and the snapshot smoke-run, while retaining compilation, guards and type checks.
|
|
92
|
+
Run a full build before release; quick output is not evidence that play checks passed.
|
|
102
93
|
|
|
103
94
|
## Do
|
|
104
95
|
|
|
105
96
|
| The person says | Run | What happens |
|
|
106
97
|
|---|---|---|
|
|
107
98
|
| "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. |
|
|
108
|
-
| "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
|
|
99
|
+
| "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 the official MCP plan/verify flow 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. |
|
|
109
100
|
| "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. |
|
|
110
|
-
| "Make the products in Stripe" | `
|
|
111
|
-
| "Connect my Stripe" / "turn the shop on" | `
|
|
101
|
+
| "Make the products in Stripe" | `stripe_shop_plan` | Idempotent Product, Price, Payment Link and webhook sync from shop.json; no secret enters tool output. |
|
|
102
|
+
| "Connect my Stripe" / "turn the shop on" | `stripe_shop_plan`, then `stripe_webhook` | Approve Stripe once if needed; the Worker receives links and the signing secret only. Follow the single next step returned. Live only when requested. |
|
|
112
103
|
| "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. |
|
|
113
104
|
| "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. |
|
|
114
105
|
| "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. |
|
|
@@ -158,7 +149,7 @@ Read SHOP.md for a round to table 7, `shop.book` and $20 monthly giving.
|
|
|
158
149
|
Menus let guests choose several quantities on Stripe; signed invoices provide actual
|
|
159
150
|
paid lines and the app reference. Slot Payment Links enforce completed-session
|
|
160
151
|
capacity. Pending guests do not block others. The owner's AI syncs published slots
|
|
161
|
-
with `
|
|
152
|
+
with `stripe_shop_plan` and the complete slot manifest; the app shows Full from verified availability.
|
|
162
153
|
Recurring Prices provide monthly preset gifts and memberships. Connect enables the
|
|
163
154
|
no-code customer portal login through Stripe's API, without a Dashboard toggle.
|
|
164
155
|
Paid invoice periods govern membership; failed renewal never extends it.
|