@homie-rocks/studio 0.58.20 → 0.58.23
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 +48 -0
- package/bin/homie-studio.mjs +1 -1
- package/guides/playtest/SKILL.md +2 -2
- package/guides/servers/SKILL.md +5 -5
- package/guides/studio-setup/SKILL.md +11 -11
- package/lib/doctor.mjs +1 -1
- package/lib/job-runner.mjs +133 -0
- package/lib/jobs.mjs +200 -37
- package/lib/local-ai.mjs +2 -2
- package/lib/mcp-tools.mjs +41 -31
- package/lib/parts-store.mjs +46 -2
- package/lib/parts.mjs +13 -1
- package/lib/scaffold.mjs +13 -12
- package/lib/template-history.json +150 -6
- package/package.json +20 -5
- package/parts/PARTS.md +27 -0
- package/parts/README.md +1 -0
- package/parts/catalogue.json +138 -0
- package/parts/venue-desk/LICENSE +202 -0
- package/parts/venue-desk/NOTICE +3 -0
- package/parts/venue-desk/README.md +34 -0
- package/parts/venue-desk/part.json +138 -0
- package/parts/venue-desk/preview/index.html +7 -0
- package/parts/venue-desk/src/index.ts +228 -0
- package/parts/venue-desk/tools/venue-booking-cancel.ts +2 -0
- package/parts/venue-desk/tools/venue-booking-confirm.ts +2 -0
- package/parts/venue-desk/tools/venue-booking-details.ts +2 -0
- package/parts/venue-desk/tools/venue-booking-status.ts +2 -0
- package/parts/venue-desk/tools/venue-desk.ts +2 -0
- package/parts/venue-desk/tools/venue-info-set.ts +2 -0
- package/parts/venue-desk/tools/venue-info.ts +2 -0
- package/parts/venue-desk/tools/venue-waitlist-join.ts +2 -0
- package/parts/venue-desk/tools/venue-waitlist-offer.ts +2 -0
- package/parts/venue-desk/tools/venue-waitlist-status.ts +2 -0
- package/parts/venue-desk/tuning.json +1 -0
- package/tools/TOOLS.md +14 -0
- package/worker/booking-availability.mjs +1 -1
- package/worker/booking-holds.mjs +48 -21
- package/worker/booking-options.mjs +1 -1
- package/worker/build-id.mjs +1 -1
- package/worker/customer-mcp.mjs +15 -1
- package/worker/mcp-room.mjs +3 -2
- package/worker/mcp-tools.mjs +9 -1
- package/worker/mcp.mjs +6 -1
- package/worker/room.mjs +3 -2
- package/worker/version.mjs +1 -1
package/lib/mcp-tools.mjs
CHANGED
|
@@ -26,7 +26,8 @@ import { renderCodex, newCodex } from './codex.mjs';
|
|
|
26
26
|
import { demoGames, formatDemo } from './demo.mjs';
|
|
27
27
|
import { formatStatus, setupStatus } from './doctor.mjs';
|
|
28
28
|
import { editStudioFile, listFiles, readStudioFile, searchStudio, writeStudioFile } from './files.mjs';
|
|
29
|
-
import { cliJob, findNode, getJob, installJob, installed, jobView, runningJobs, startJob, toolEnv, waitJob, which } from './jobs.mjs';
|
|
29
|
+
import { cliJob, cliOf, findNode, getJob, installJob, installed, jobView, runningJobs, startJob, stepJobs, toolEnv, waitJob, which } from './jobs.mjs';
|
|
30
|
+
import { endFeed } from './job-runner.mjs';
|
|
30
31
|
import { Feed, currentId, publicFeed, readFeed, startProgress } from './progress.mjs';
|
|
31
32
|
import { newStudio, slugify } from './scaffold.mjs';
|
|
32
33
|
import { repoFromUrl, studioRepo } from './repo.mjs';
|
|
@@ -179,6 +180,13 @@ async function cli(ctx, root, label, args, { wait = ctx.waitMs, onEnd = null, cl
|
|
|
179
180
|
return { ended, job, result: job.result };
|
|
180
181
|
}
|
|
181
182
|
|
|
183
|
+
/** Where a job an earlier server started may be: the studios, and where a clone or a sign-in runs. */
|
|
184
|
+
function jobRoots(ctx) {
|
|
185
|
+
let studios = [];
|
|
186
|
+
try { studios = ctx.studios().map((s) => s.root); } catch { /* none */ }
|
|
187
|
+
return [ctx.current, ...studios, ctx.studiosDir, homedir()];
|
|
188
|
+
}
|
|
189
|
+
|
|
182
190
|
function stillRunning(job, what) {
|
|
183
191
|
return ok(`${what} is still running (job ${job.id}, ${jobView(job).seconds} s so far). studio_job { "job": "${job.id}" } says when it is done; tell the person what is happening meanwhile.`, { kind: 'job', ...jobView(job) });
|
|
184
192
|
}
|
|
@@ -404,13 +412,7 @@ async function feedForRun(ctx, root, { id, title }) {
|
|
|
404
412
|
|
|
405
413
|
function endRun(root, run, state, note) {
|
|
406
414
|
if (!run.auto) return;
|
|
407
|
-
|
|
408
|
-
const feed = new Feed(root, run.build);
|
|
409
|
-
const doc = feed.doc;
|
|
410
|
-
if (!doc || doc.state !== 'running') return;
|
|
411
|
-
feed.change((d) => { for (const s of d.stages) if (s.state === 'pending') s.state = 'skipped'; });
|
|
412
|
-
feed.end(state, note);
|
|
413
|
-
} catch { /* the feed stays as it was */ }
|
|
415
|
+
endFeed(Feed, root, run.build, state, note);
|
|
414
416
|
}
|
|
415
417
|
|
|
416
418
|
function buildCard(ctx, root, found) {
|
|
@@ -439,23 +441,30 @@ function buildCard(ctx, root, found) {
|
|
|
439
441
|
return ok(runLine ? `${runLine}\n${text}` : text, { kind: 'build', build: found.id, feed, jobs, run: runView, now: new Date().toISOString() });
|
|
440
442
|
}
|
|
441
443
|
|
|
442
|
-
/**
|
|
444
|
+
/**
|
|
445
|
+
* Start a check / playtest / deploy: its feed first, then the work, and the card at once. The steps run as one job
|
|
446
|
+
* (lib/jobs.mjs), which outlives this server and ends the feed itself; while this server is here it also keeps the
|
|
447
|
+
* run's own words (run.state, run.why, run.look) for the card.
|
|
448
|
+
*/
|
|
443
449
|
async function startRun(ctx, root, { kind, game, title, steps }) {
|
|
444
|
-
const run = { kind, title, state: 'running', ...(await feedForRun(ctx, root, { id: game ?? null, title })),
|
|
450
|
+
const run = { kind, title, state: 'running', ...(await feedForRun(ctx, root, { id: game ?? null, title })), job: null };
|
|
451
|
+
Object.defineProperty(run, 'jobs', { get: () => (run.job ? stepJobs(run.job) : []) });
|
|
445
452
|
ctx.runs.set(run.build, run);
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
453
|
+
const node = findNode();
|
|
454
|
+
if (!node) throw new Error('Node.js is missing. AI: call setup_prepare, then retry this tool.');
|
|
455
|
+
run.job = startJob({
|
|
456
|
+
root, label: title, cmd: node.bin, json: true, env: toolEnv(),
|
|
457
|
+
steps: steps.map((step) => ({ label: step.label, args: [cliOf(root), ...step.args, '--json'], json: true, soft: step.soft })),
|
|
458
|
+
feed: run.auto ? { build: run.build, title } : null,
|
|
459
|
+
onEnd: (job) => {
|
|
460
|
+
const done = stepJobs(job);
|
|
461
|
+
done.forEach((j, n) => { try { steps[n]?.after?.(j, run); } catch { /* the run's result stands */ } });
|
|
462
|
+
const failed = done.find((j, n) => j.code !== 0 && !steps[n]?.soft);
|
|
463
|
+
if (failed || job.code !== 0) { run.state = 'failed'; run.why = failed ? `${failed.label}: ${whyOf(failed)}` : job.why ?? `exited with ${job.code}`; endRun(root, run, 'failed', run.why); return; }
|
|
464
|
+
run.state = 'passed'; endRun(root, run, 'passed', `${title}: done.`);
|
|
465
|
+
if (kind === 'check' || kind === 'playtest') ctx.note(root).checked = true;
|
|
466
|
+
},
|
|
467
|
+
});
|
|
459
468
|
await new Promise((r) => setTimeout(r, 1500));
|
|
460
469
|
return buildCard(ctx, root, feedFor(root, run.build));
|
|
461
470
|
}
|
|
@@ -1090,7 +1099,7 @@ export function toolDefs(ctx, avail = {}) {
|
|
|
1090
1099
|
},
|
|
1091
1100
|
{
|
|
1092
1101
|
name: 'preview_run', title: 'Run the site here',
|
|
1093
|
-
description: 'Run the studio\'s whole site on this computer (pages, public rooms, the database) and give the addresses to open: the home page, and each game\'s Play page. Two browser tabs on a game are two players in the same room. It keeps running until preview_stop
|
|
1102
|
+
description: 'Run the studio\'s whole site on this computer (pages, public rooms, the database) and give the addresses to open: the home page, and each game\'s Play page. Two browser tabs on a game are two players in the same room. It keeps running until preview_stop.',
|
|
1094
1103
|
inputSchema: { type: 'object', properties: { game: str('Optional: the game to give the Play address of'), port: { type: 'number', description: 'Optional port (default: 8787, or the next free one)' }, ...STUDIO_ARG } },
|
|
1095
1104
|
annotations: { title: 'Run the site here', ...RW },
|
|
1096
1105
|
run: async (a) => {
|
|
@@ -1221,7 +1230,7 @@ export function toolDefs(ctx, avail = {}) {
|
|
|
1221
1230
|
},
|
|
1222
1231
|
{
|
|
1223
1232
|
name: 'studio_deploy', title: 'Put the studio online',
|
|
1224
|
-
description: 'Put the studio\'s site online on the studio\'s OWN Cloudflare account: one Worker
|
|
1233
|
+
description: 'Put the studio\'s site online on the studio\'s OWN Cloudflare account: one Worker (with its Durable Objects and two Queues) and one D1 database, with its own resources. Call it with plan: true first and tell the person in two or three lines what it creates and costs. If Cloudflare is not signed in, cloudflare_login opens it in their browser to approve once. Runs in the background with the build card.',
|
|
1225
1234
|
inputSchema: { type: 'object', properties: { plan: { type: 'boolean', description: 'Only say what it will create and what it costs; change nothing' }, ownRoute: { type: 'boolean', description: 'Only after the plan or a deploy warned that another site\'s route on the studio\'s custom domain covers its hostname, and the person agreed: add the studio\'s own exact-host route (the one line the warning gives) and deploy. It never edits or removes any other route' }, ...STUDIO_ARG } },
|
|
1226
1235
|
annotations: { title: 'Put the studio online', readOnlyHint: false, destructiveHint: false, openWorldHint: true }, _meta: ui(UI.build),
|
|
1227
1236
|
run: async (a) => {
|
|
@@ -1255,7 +1264,7 @@ export function toolDefs(ctx, avail = {}) {
|
|
|
1255
1264
|
inputSchema:{type:'object',properties:{...STUDIO_ARG,live:{type:'boolean'},renew:{type:'boolean'}}},
|
|
1256
1265
|
annotations:{readOnlyHint:false,destructiveHint:false,openWorldHint:true},
|
|
1257
1266
|
run:async a=>{
|
|
1258
|
-
const job=cliJob(ctx.root(a.studio),'Private Stripe webhook',['shop','webhook',...(a.live?['--live']:[]),...(a.renew?['--renew']:[])],{cli:join(PACKAGE_ROOT,'bin','homie-studio.mjs')
|
|
1267
|
+
const job=cliJob(ctx.root(a.studio),'Private Stripe webhook',['shop','webhook',...(a.live?['--live']:[]),...(a.renew?['--renew']:[])],{cli:join(PACKAGE_ROOT,'bin','homie-studio.mjs')});
|
|
1259
1268
|
await waitJob(job,Math.min(ctx.waitMs,1000));
|
|
1260
1269
|
return ok('Follow studio_job. Its events contain any required Stripe pairing; a saved login needs no new approval.',{kind:'job',...jobView(job)});
|
|
1261
1270
|
},
|
|
@@ -1269,7 +1278,7 @@ export function toolDefs(ctx, avail = {}) {
|
|
|
1269
1278
|
const root=ctx.root(a.studio);
|
|
1270
1279
|
mkdirSync(join(root,'.studio'),{recursive:true});
|
|
1271
1280
|
writeFileSync(join(root,'.studio','booking-slots.json'),JSON.stringify(a.slots,null,2));
|
|
1272
|
-
const job=cliJob(root,'Sync Stripe booking links',['shop','connect',...(a.live?['--live']:[])],{cli:join(PACKAGE_ROOT,'bin','homie-studio.mjs')
|
|
1281
|
+
const job=cliJob(root,'Sync Stripe booking links',['shop','connect',...(a.live?['--live']:[])],{cli:join(PACKAGE_ROOT,'bin','homie-studio.mjs')});
|
|
1273
1282
|
await waitJob(job,Math.min(ctx.waitMs,1000));
|
|
1274
1283
|
return ok('Booking link sync started. Check studio_job for completion.',{kind:'job',...jobView(job)});
|
|
1275
1284
|
},
|
|
@@ -1282,7 +1291,7 @@ export function toolDefs(ctx, avail = {}) {
|
|
|
1282
1291
|
run: async (a) => {
|
|
1283
1292
|
const root = ctx.root(a.studio);
|
|
1284
1293
|
const args = ['shop', 'connect', ...(a.live ? ['--live'] : []), ...(a.renew ? ['--renew'] : [])];
|
|
1285
|
-
const job = cliJob(root, 'Stripe connection', args, { cli: join(PACKAGE_ROOT, 'bin', 'homie-studio.mjs')
|
|
1294
|
+
const job = cliJob(root, 'Stripe connection', args, { cli: join(PACKAGE_ROOT, 'bin', 'homie-studio.mjs') });
|
|
1286
1295
|
const ended = await waitJob(job, Math.min(ctx.waitMs, 1000));
|
|
1287
1296
|
ctx.status = null;
|
|
1288
1297
|
if (ended && job.code !== 0) return fail(whyOf(job), { kind: 'job', ...jobView(job) });
|
|
@@ -1298,7 +1307,7 @@ export function toolDefs(ctx, avail = {}) {
|
|
|
1298
1307
|
const root = ctx.root(a.studio);
|
|
1299
1308
|
const need = needsInstall(ctx, root); if (need) return need;
|
|
1300
1309
|
const bin = join(root, 'node_modules', '.bin', process.platform === 'win32' ? 'wrangler.cmd' : 'wrangler');
|
|
1301
|
-
const job = startJob({ root, label: 'Cloudflare sign-in (wrangler login)', cmd: bin, args: ['login']
|
|
1310
|
+
const job = startJob({ root, label: 'Cloudflare sign-in (wrangler login)', cmd: bin, args: ['login'] });
|
|
1302
1311
|
const ended = await waitJob(job, Math.min(ctx.waitMs, 40_000));
|
|
1303
1312
|
ctx.status = null;
|
|
1304
1313
|
if (!ended) return ok(`Cloudflare opened in the browser: approve it there (job ${job.id}). setup_status says when it is done.`, { kind: 'job', ...jobView(job) });
|
|
@@ -1461,8 +1470,9 @@ export function toolDefs(ctx, avail = {}) {
|
|
|
1461
1470
|
inputSchema: { type: 'object', properties: { job: str('The job id (j_…)') }, required: ['job'] },
|
|
1462
1471
|
annotations: { title: 'A job', ...RO },
|
|
1463
1472
|
run: async (a) => {
|
|
1464
|
-
|
|
1465
|
-
|
|
1473
|
+
// A job outlives the server that started it: one an earlier server started is read from its studio.
|
|
1474
|
+
const job = getJob(a.job, { roots: jobRoots(ctx) });
|
|
1475
|
+
if (!job) return fail(`no job ${a.job} in these studios (build_progress shows a build)`);
|
|
1466
1476
|
await waitJob(job, Math.min(ctx.waitMs, 30_000));
|
|
1467
1477
|
const v = jobView(job, { lines: 20 });
|
|
1468
1478
|
if (job.endedAt) ctx.status = null;
|
package/lib/parts-store.mjs
CHANGED
|
@@ -21,6 +21,7 @@ import { priceWords } from "../worker/purchase-pricing.mjs";
|
|
|
21
21
|
import { spawnSync } from 'node:child_process';
|
|
22
22
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
23
23
|
import { dirname, join } from 'node:path';
|
|
24
|
+
import { fileURLToPath } from 'node:url';
|
|
24
25
|
import {
|
|
25
26
|
LIMITS, TUNING_FILE, TUNING_UPSTREAM, assetRecords, checkPart, compareVersions, isVersion, licenceIssues, licenseOfPart, listOwnParts, packPart, packedDir,
|
|
26
27
|
packedVersions, parseRef, partDir, readOrigins, readPart, requiredParts, sha256, shareProblems, syncPartCredits, vendorDir, verifyFiles, writeHashes,
|
|
@@ -32,6 +33,45 @@ export const HUB = 'https://homie.rocks';
|
|
|
32
33
|
const isObj = (v) => Boolean(v) && typeof v === 'object' && !Array.isArray(v);
|
|
33
34
|
const line = (v, n = 200) => (typeof v === 'string' ? v.replace(/[\x00-\x1f\x7f]+/g, ' ').trim().slice(0, n) : '');
|
|
34
35
|
|
|
36
|
+
/* ------------------------------------------------------------------ the toolkit's own parts */
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The parts that come WITH the toolkit (parts/catalogue.json in this package, e.g. venue-desk): found and added from the
|
|
40
|
+
* installed toolkit, with no network. "venue-desk" alone, or "homie.rocks/venue-desk", names one; every file is checked
|
|
41
|
+
* against the catalogue's SHA-256 and size exactly as a fetched part's are.
|
|
42
|
+
*/
|
|
43
|
+
export const TOOLKIT_HOST = 'homie.rocks';
|
|
44
|
+
const TOOLKIT_PARTS = fileURLToPath(new URL('../parts/', import.meta.url));
|
|
45
|
+
export function bundledCatalogue() {
|
|
46
|
+
try {
|
|
47
|
+
const c = JSON.parse(readFileSync(join(TOOLKIT_PARTS, 'catalogue.json'), 'utf8'));
|
|
48
|
+
return Array.isArray(c?.parts) ? c.parts.filter((p) => isObj(p) && typeof p.id === 'string' && p.share === true && Array.isArray(p.files)) : [];
|
|
49
|
+
} catch { return []; }
|
|
50
|
+
}
|
|
51
|
+
/** A bundled part named by `refText`, read and checked; null when it is not one of the toolkit's. */
|
|
52
|
+
export function bundledPart(refText) {
|
|
53
|
+
const text = String(refText ?? '').trim();
|
|
54
|
+
const bare = /^([a-z0-9][a-z0-9-]{0,47})(?:@(.{1,60}))?$/.exec(text);
|
|
55
|
+
const ref = bare ? { host: TOOLKIT_HOST, id: bare[1], version: bare[2] ?? null, ref: `${TOOLKIT_HOST}/${bare[1]}` } : parseRef(text);
|
|
56
|
+
if (!ref || ref.host !== TOOLKIT_HOST) return null;
|
|
57
|
+
const part = bundledCatalogue().find((p) => p.id === ref.id);
|
|
58
|
+
if (!part) return null;
|
|
59
|
+
if (ref.version && ref.version !== part.version) throw new Error(`The toolkit has ${ref.id} ${part.version}, not ${ref.version}. Upgrade the toolkit for a newer one.`);
|
|
60
|
+
const shape = checkPart(part);
|
|
61
|
+
if (!shape.ok) throw new Error(`${ref.ref} ${part.version} is not a part this toolkit can read: ${shape.problems.filter((p) => p.level === 'refuse').map((p) => `${p.field}: ${p.problem}`).slice(0, 3).join('; ')}`);
|
|
62
|
+
const files = new Map();
|
|
63
|
+
for (const f of part.files) {
|
|
64
|
+
const at = join(TOOLKIT_PARTS, part.id, f.path);
|
|
65
|
+
if (!at.startsWith(join(TOOLKIT_PARTS, part.id) + '/')) throw new Error(`${ref.ref}: ${f.path} is outside the part`);
|
|
66
|
+
let bytes;
|
|
67
|
+
try { bytes = readFileSync(at); } catch { throw new Error(`${ref.ref}: ${f.path} is missing from the installed toolkit. Reinstall it (npm ci).`); }
|
|
68
|
+
if (bytes.length !== f.bytes || sha256(bytes) !== f.sha256) throw new Error(`${ref.ref} ${part.version}: ${f.path} in the installed toolkit is not the file its catalogue describes. Reinstall the toolkit (npm ci).`);
|
|
69
|
+
files.set(f.path, bytes);
|
|
70
|
+
}
|
|
71
|
+
const entry = { ...part, from: { ...(isObj(part.from) ? part.from : {}), studio: 'Homie toolkit' }, studio: { name: 'Homie toolkit' } };
|
|
72
|
+
return { ref: { ...ref, version: part.version }, part, files, entry, base: baseOf(TOOLKIT_HOST), version: part.version, versions: [part.version], access: null, bundled: true };
|
|
73
|
+
}
|
|
74
|
+
|
|
35
75
|
/* ------------------------------------------------------------------ the one fetch */
|
|
36
76
|
|
|
37
77
|
/** A studio's site from its host: https, except a site on this computer (the local preview), which has no certificate. */
|
|
@@ -196,7 +236,7 @@ export async function addPart(root, refText, { game = null, fetch, npm = runNpm,
|
|
|
196
236
|
if (game && !existsSync(experienceFile(root, game))) return fail(`there is no game "${game}" in this studio (${listGames(root).map((g) => g.id).join(', ') || 'it has none yet'})`);
|
|
197
237
|
let got;
|
|
198
238
|
try {
|
|
199
|
-
got = offline ? await offlinePurchase(root, refText, game) : await fetchPart(refText, { fetch, onPaid: (ref, part, offer) => paidAccess(root, ref, part, { game, quantity, approve, fetch, offer, wallet, walletPay }) });
|
|
239
|
+
got = offline ? await offlinePurchase(root, refText, game) : bundledPart(refText) ?? await fetchPart(refText, { fetch, onPaid: (ref, part, offer) => paidAccess(root, ref, part, { game, quantity, approve, fetch, offer, wallet, walletPay }) });
|
|
200
240
|
if (got.access) {
|
|
201
241
|
savePurchaseBackup(root, got, got.access, game);
|
|
202
242
|
const c = got.access.claims;
|
|
@@ -345,6 +385,10 @@ export async function findParts(root, query = '', { kind = null, tag = null, lic
|
|
|
345
385
|
if (part) here.push(row({ ...part, from: e.from ?? part.from }, { where: 'here', ref, add: ref, import: part.entry ? `@parts/${ref}` : null, games: e.games ?? [], say: `Already in this studio (${e.version}): import it from '@parts/${ref}'.` }));
|
|
346
386
|
}
|
|
347
387
|
}
|
|
388
|
+
for (const p of bundledCatalogue()) {
|
|
389
|
+
const ref = `${TOOLKIT_HOST}/${p.id}`;
|
|
390
|
+
if (!here.some((h) => h.ref === ref)) here.push(row({ ...p, from: { ...(isObj(p.from) ? p.from : {}), studio: 'Homie toolkit' } }, { where: 'toolkit', ref, add: ref, import: null, say: `Comes with the Homie toolkit (no download): part_add { "part": "${ref}" }.` }));
|
|
391
|
+
}
|
|
348
392
|
let base = hub;
|
|
349
393
|
if (!base && root) { try { const d = readStudio(root).homie?.directory; base = d === false || d === null ? null : d; } catch { base = null; } }
|
|
350
394
|
base = /^https?:\/\/[a-z0-9.:-]+$/i.test(String(base ?? '')) ? base : HUB;
|
|
@@ -353,7 +397,7 @@ export async function findParts(root, query = '', { kind = null, tag = null, lic
|
|
|
353
397
|
try {
|
|
354
398
|
const index = await getJson(url, { fetch, what: 'the catalogue of shared parts' });
|
|
355
399
|
if (!Array.isArray(index?.parts)) throw new Error(`${url} is not a parts catalogue`);
|
|
356
|
-
const have = new Set(here.filter((h) => h.where === 'here').map((h) => h.ref));
|
|
400
|
+
const have = new Set(here.filter((h) => h.where === 'here' || h.where === 'toolkit').map((h) => h.ref));
|
|
357
401
|
found = index.parts.filter((p) => isObj(p) && typeof p.id === 'string' && (typeof p.host === 'string' || typeof p.add === 'string')).map((p) => {
|
|
358
402
|
const ref = parseRef(typeof p.add === 'string' ? p.add : `${p.host}/${p.id}`)?.ref;
|
|
359
403
|
return ref ? row(p, { where: 'hub', ref, add: ref, studio: line(p.studio?.name, 80) || null, page: /^https:\/\//.test(String(p.page ?? '')) ? p.page : null, here: have.has(ref), say: `${p.sale ? `Paid: ${priceWords(p.sale)}. Ask for approval of the exact quote before requesting payment. ` : 'Free. '}Add it: part_add { "part": "${ref}" }.` }) : null;
|
package/lib/parts.mjs
CHANGED
|
@@ -30,7 +30,7 @@ import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, r
|
|
|
30
30
|
import { dirname, join, relative } from 'node:path';
|
|
31
31
|
|
|
32
32
|
export const PART_ID = /^[a-z0-9][a-z0-9-]{0,47}$/;
|
|
33
|
-
export const PART_KINDS = Object.freeze(['character', 'rig', 'clips', 'environment', 'effect', 'sound', 'ui', 'mechanic', 'shader', 'level-generator', 'bot-brain', 'audio-pack', 'set-piece', 'quiz', 'menu-board', 'waitlist', 'live-total', 'loop', 'stem', 'track', 'video-intro', 'video-template', 'app', 'music', 'video']);
|
|
33
|
+
export const PART_KINDS = Object.freeze(['character', 'rig', 'clips', 'environment', 'effect', 'sound', 'ui', 'mechanic', 'shader', 'level-generator', 'bot-brain', 'audio-pack', 'set-piece', 'quiz', 'menu-board', 'waitlist', 'live-total', 'loop', 'stem', 'track', 'video-intro', 'video-template', 'app', 'music', 'video', 'agent']);
|
|
34
34
|
export const NETPLAY_KINDS = Object.freeze(['host-authoritative', 'replicated', 'local']);
|
|
35
35
|
export const PROVENANCE = Object.freeze(['original', 'generated', 'imported']);
|
|
36
36
|
export const PIVOTS = Object.freeze(['feet', 'centre', 'base', 'origin']);
|
|
@@ -321,6 +321,17 @@ export function checkPart(part, { id = null } = {}) {
|
|
|
321
321
|
if (part.contract.netplay !== undefined && !NETPLAY_KINDS.includes(part.contract.netplay)) bad('contract.netplay', `"${String(part.contract.netplay).slice(0, 40)}" is not a netplay contract`, `one of: ${NETPLAY_KINDS.join(', ')}`);
|
|
322
322
|
}
|
|
323
323
|
}
|
|
324
|
+
// An agent part is a template for a studio agent (the chat's sidebar offers it under New agent): who it is, not code.
|
|
325
|
+
if (part.kind === 'agent') {
|
|
326
|
+
const a = part.agent, text = (v, max) => typeof v === 'string' && v.length <= max;
|
|
327
|
+
if (!isObj(a)) bad('agent', 'an agent part has no agent block', '{ "name": "Bookings", "persona": "what it does, in a line", "instructions": "how it works" }');
|
|
328
|
+
else {
|
|
329
|
+
if (!(text(a.name, 40) && a.name.trim())) bad('agent.name', 'it is not a name of 1 to 40 characters', '"Bookings"');
|
|
330
|
+
if (a.persona !== undefined && !text(a.persona, 400)) bad('agent.persona', 'it is not text of up to 400 characters', 'one line on what the agent does');
|
|
331
|
+
if (a.instructions !== undefined && !text(a.instructions, 8000)) bad('agent.instructions', 'it is not text of up to 8000 characters', 'how the agent should work');
|
|
332
|
+
if (a.tools !== undefined && a.tools !== null && !(Array.isArray(a.tools) && a.tools.length <= 40 && a.tools.every((t) => typeof t === 'string' && /^[\w:.-]{1,80}$/.test(t)))) bad('agent.tools', 'it is not a list of up to 40 tool names', 'leave it out to let the agent use every tool');
|
|
333
|
+
}
|
|
334
|
+
}
|
|
324
335
|
if (part.kind === 'mechanic' && !NETPLAY_KINDS.includes(part.contract?.netplay)) warn('contract.netplay', 'a mechanic does not say how it behaves in a room', `${NETPLAY_KINDS.join(', ')}: another game needs to know who decides`);
|
|
325
336
|
if (part.cost !== undefined) {
|
|
326
337
|
if (!isObj(part.cost)) bad('cost', 'it is not an object', '{ "triangles": 0, "drawCalls": 0, "textureMB": 0, "bytes": 0, "measuredOn": "…" }');
|
|
@@ -646,6 +657,7 @@ export function newPart(root, id, { kind = 'mechanic', name = null, from = null,
|
|
|
646
657
|
requires: {},
|
|
647
658
|
...(['character', 'rig', 'clips', 'environment', 'set-piece'].includes(kind) ? { physical: { units: 'metres', scale: 1, pivot: kind === 'environment' || kind === 'set-piece' ? 'origin' : 'feet', collision: null } } : {}),
|
|
648
659
|
...(['character', 'rig', 'clips'].includes(kind) ? { skeleton: { rig: null, clips: [] } } : {}),
|
|
660
|
+
...(kind === 'agent' ? { agent: { name: title, persona: '', instructions: '' } } : {}),
|
|
649
661
|
...(kind === 'mechanic' || kind === 'bot-brain' ? { contract: { inputs: {}, state: {}, netplay: 'host-authoritative' } } : {}),
|
|
650
662
|
cost: {},
|
|
651
663
|
provenance: { source: 'original', notes: '' },
|
package/lib/scaffold.mjs
CHANGED
|
@@ -188,7 +188,7 @@ Cloudflare creates DNS and TLS during deploy with the existing sign-in.
|
|
|
188
188
|
|
|
189
189
|
## Apps: one screen, roles and parts
|
|
190
190
|
|
|
191
|
-
Plan apps with the same Codex mechanism as games: \`codex new <id> --kind app\` writes \`apps/<id>/CODEX.md\`. Read the packaged plan, app and playtest guides. Preserve the person’s words, scene, people, offers and testable needs; build that scene with engines, style, parts and media
|
|
191
|
+
Plan apps with the same Codex mechanism as games: \`codex new <id> --kind app\` writes \`apps/<id>/CODEX.md\`. Read the packaged plan, app and playtest guides. Preserve the person’s words, scene, people, offers and testable needs; build that scene with engines, style, parts and media. Starters are plumbing, not completion.
|
|
192
192
|
|
|
193
193
|
An app lives in \`apps/<id>/app.json\` and \`src/\`. Make one with \`homie-studio app new <id>\` (or \`app_make\`). Use the Homie plugin's \`app\` skill for businesses, venues, causes and customer apps. No game demo, rounds, scores, bots or mandatory fun: build what the person needs with the fewest human steps.
|
|
194
194
|
|
|
@@ -196,7 +196,7 @@ An app is ONE screen that morphs with context: camera moves, panels unfold, its
|
|
|
196
196
|
|
|
197
197
|
Declare roles, surfaces, words and lasting record collections in app.json. Netplay's host is not a staff permission. Use \`@homie-rocks/studio/apps\` for authorized lasting records and \`@homie-rocks/studio/links\` for ticket links, QR and HTTP-safe IDs. Staff use private role links plus existing account grants; keep private records out of public collections. Look for engine mechanisms and shared parts before writing a component. Credit licences; sharing is optional and separate from selling.
|
|
198
198
|
|
|
199
|
-
Your studio also serves MCP at \`/mcp\`. Staff connect their own AI with their existing account and one browser approval; never ask them for a key. Add business actions with \`homie-studio tool new <name> --app <id>\`, keep their declared role narrow, and reuse the app's existing record IDs, field formats and versions. Customers can browse and buy through this same endpoint, and tools may declare a price using the studio's existing payments. Add event handlers with \`homie-studio function new <name> --event order.paid\`; read \`node_modules/@homie-rocks/studio/functions/FUNCTIONS.md\`.
|
|
199
|
+
Your studio also serves MCP at \`/mcp\`. Staff connect their own AI with their existing account and one browser approval; never ask them for a key. Add business actions with \`homie-studio tool new <name> --app <id>\`, keep their declared role narrow, and reuse the app's existing record IDs, field formats and versions. Customers can browse and buy through this same endpoint, and tools may declare a price using the studio's existing payments. Add event handlers with \`homie-studio function new <name> --event order.paid\`; read \`node_modules/@homie-rocks/studio/functions/FUNCTIONS.md\`. Read \`node_modules/@homie-rocks/studio/tools/TOOLS.md\` and the Homie plugin's \`tools\` skill.
|
|
200
200
|
|
|
201
201
|
Build normally, run \`homie-studio dev --lan\`, then \`homie-studio check <id> --url <origin>\`: the app check proves an actual action across a wall and two phones plus reconnect. The public screen is \`/<id>/open\`, the wall \`/<id>/tv\`. Customer apps use the same \`standalone\` build for desktop/iOS/Android; its existing sign-in and store limitations still apply. See \`node_modules/@homie-rocks/studio/apps/APPS.md\`.
|
|
202
202
|
|
|
@@ -263,7 +263,7 @@ studio's pinned copy, never a registry lookup of the bare name.
|
|
|
263
263
|
bucket on the studio's own Cloudflare account. Cloudflare asks for a payment method on the account
|
|
264
264
|
before R2 works, so this is a separate step the person agrees to; nothing else needs it. R2 has no
|
|
265
265
|
egress fees; storage is free up to 10 GB-month, then US$0.015 per GB-month.
|
|
266
|
-
- \`npx --no-install homie-studio media move\`
|
|
266
|
+
- \`npx --no-install homie-studio media move\` — with storage, the big files of
|
|
267
267
|
published songs and videos (over 1 MiB, or left out of git; studio.json \`media.r2Over\` changes the
|
|
268
268
|
size) go to R2: each is uploaded, read back and checked by SHA-256, and only then does the site stop
|
|
269
269
|
carrying it; it keeps its address, and the file stays in this folder. Every \`npm run deploy\` does
|
|
@@ -376,8 +376,7 @@ studio's pinned copy, never a registry lookup of the bare name.
|
|
|
376
376
|
the game's own: on a phone it raises copy/paste and the thumb's touch is lost. Call
|
|
377
377
|
\`guardGestures({ touch: 'canvas, [data-action]' })\` from \`@homie-rocks/studio/netplay\` once, early: no
|
|
378
378
|
selection, no callout, no pan or pinch on the gameplay surfaces; text fields, links and ordinary buttons keep the
|
|
379
|
-
browser's own behaviour (\`NETPLAY.md\` section 24).
|
|
380
|
-
does not raise the callout.
|
|
379
|
+
browser's own behaviour (\`NETPLAY.md\` section 24). Emulated touch does not raise the callout.
|
|
381
380
|
- **The frame has no storage of its own.** The game runs in a sandboxed frame without \`allow-same-origin\` (a
|
|
382
381
|
stranger's game must never read this site's storage or the owner's session), so \`localStorage\` throws inside it.
|
|
383
382
|
A setting or a personal best goes in \`net.prefs\` (\`net.prefs.string('quality', 'high')\`, \`net.prefs.number('volume', 0.8, { min: 0, max: 1 })\` after \`await net.prefs.ready\`: the typed readers give your fallback for a missing key, never a null that turns into 0,
|
|
@@ -491,7 +490,7 @@ studio's pinned copy, never a registry lookup of the bare name.
|
|
|
491
490
|
- **A server** is a named, lasting pool of rooms for one game, with its own policy and door. Strangers are matched
|
|
492
491
|
only inside one server. Every game's \`pub-N\` rooms are its \`public\` server (Quick play), so old links keep
|
|
493
492
|
working. A server's page is \`/<id>/s/<server>/\`; its rooms are \`s-<server>-<n>\`; \`/<id>/servers/\` lists them.
|
|
494
|
-
- **Policies:** \`open\` (anyone; an AI
|
|
493
|
+
- **Policies:** \`open\` (anyone; an AI may sit, always marked AI), \`humans-only\` (no AI of any
|
|
495
494
|
kind; the game's bots are off unless \`--bots fill\`), \`hybrid\` (N seats in every room are AI companions),
|
|
496
495
|
\`beginner\` (accounts under 30 days, AI guides, quick lines only, optionally \`--kids\`: handles only, the AI's
|
|
497
496
|
level at most 3). **Doors:** \`open\`, \`accounts\` (a passkey account) or \`invite\` (\`office invite\` with
|
|
@@ -509,8 +508,9 @@ studio's pinned copy, never a registry lookup of the bare name.
|
|
|
509
508
|
snippet is in \`NETPLAY.md\` section 17; \`BotBrain\` from the port kit reads it with a rebuild), and declare
|
|
510
509
|
\`caps: ['skill', 'agents']\` in \`createNetplay\` (rules rooms provide the agent runtime). A build
|
|
511
510
|
from before 0.16.0 still plays on every server; the office says it predates servers until it is rebuilt.
|
|
512
|
-
- **
|
|
513
|
-
|
|
511
|
+
- **An AI in a seat:** a signed-in person's own AI sits through this studio's \`/mcp\` (\`agent_sit\`), and the
|
|
512
|
+
owner's own through the local MCP's \`agent_sit\`. For an AI with no MCP connection,
|
|
513
|
+
\`npx --no-install homie-studio agents pass <id> --label Claude [--server <server>]\` gives it a seat (shown once; \`agents revoke <pass>\` ends it). It sits with \`POST /<id>/api/agent\`
|
|
514
514
|
(Bearer pass), only in a room with people in it, and never on a humans-only server.
|
|
515
515
|
- **AI guides that talk (0.17.0):** a beginner server's guides get a brain. A game's own words for them are
|
|
516
516
|
\`games/<id>/agents.json\` (its vocabulary: goals, lines, the asks a player taps; the build checks it), and
|
|
@@ -526,7 +526,7 @@ studio's pinned copy, never a registry lookup of the bare name.
|
|
|
526
526
|
picks among the vocabulary's goals, values and lines; it never writes text; about 9 neurons a decision).
|
|
527
527
|
\`agents try <id> --view <file> [--ask …] [--model …]\` shows what the brain would decide in one moment. Under
|
|
528
528
|
\`npm run dev\`, Clef on this computer (Ollama with \`clef-flash\`) runs the guides, chat review and game
|
|
529
|
-
decisions for free when it is here;
|
|
529
|
+
decisions for free when it is here; pull it only when the person wants Clef, and say its size in one line (\`ollama pull clef-flash\` is
|
|
530
530
|
about 11 GB). A game whose game.json says \`"decide": true\` may ask \`net.decide(state, questions, { floor })\`
|
|
531
531
|
on the host (\`NETPLAY.md\` section 20): per beat or per turn, never per frame.
|
|
532
532
|
|
|
@@ -553,7 +553,7 @@ where it sells and for Stripe's terms. Homie takes no cut.
|
|
|
553
553
|
The Worker stores only links and a signing secret; buying needs no API key or running AI.
|
|
554
554
|
Rerun \`shop connect\` after shop.json changes; use \`shop connect --renew\` if approval needs renewal.
|
|
555
555
|
Verify a test purchase, signed webhook, item grant, and a refund in Stripe or through the connected AI.
|
|
556
|
-
|
|
556
|
+
An existing Worker key selects the fuller cart/cap/refund path;
|
|
557
557
|
explain it only when those features are requested. Never silently choose \`--manual\`. Live only on request.
|
|
558
558
|
A lost webhook stays pending: ask Stripe to resend it. Free orders need no Stripe.
|
|
559
559
|
Never create secret-returning webhooks through conversation MCP tools; connect captures secrets privately.
|
|
@@ -778,7 +778,7 @@ export function studioFiles({ name, slug, homie, template = false, kind = 'game'
|
|
|
778
778
|
stats: { share: false },
|
|
779
779
|
};
|
|
780
780
|
const pkg = {
|
|
781
|
-
name: `${slug}-studio`,
|
|
781
|
+
name: /(^|-)studio$/.test(slug) ? slug : `${slug}-studio`, // "Playtest Studio" stays playtest-studio, not playtest-studio-studio
|
|
782
782
|
private: true,
|
|
783
783
|
type: 'module',
|
|
784
784
|
scripts: { dev: 'homie-studio dev', build: 'homie-studio build', deploy: 'homie-studio deploy', check: 'homie-studio check', studio: 'homie-studio' },
|
|
@@ -1167,6 +1167,7 @@ export function newStudio(folder, { name, homie, slug: askedSlug, install = true
|
|
|
1167
1167
|
writeFileSync(path, content);
|
|
1168
1168
|
wrote.push(rel);
|
|
1169
1169
|
}
|
|
1170
|
+
wrote.sort(); // migrations (and every other file) in name order
|
|
1170
1171
|
let git = 'already inside a git repository';
|
|
1171
1172
|
if (!insideGit(dir)) {
|
|
1172
1173
|
try { execFileSync('git', ['init', '-q'], { cwd: dir, stdio: 'ignore' }); git = 'initialised'; wrote.push('.git/'); } catch { git = 'git not found'; }
|
|
@@ -1196,7 +1197,7 @@ export function newStudio(folder, { name, homie, slug: askedSlug, install = true
|
|
|
1196
1197
|
'only when the person asks for a game: start from a rules starter and implement the requested mechanic: npx --no-install homie-studio game new <id> --from gem-rush --name "<Game Name>"',
|
|
1197
1198
|
'npm run dev (the home page, "first game coming soon", at http://127.0.0.1:8787/; with a game: npx --no-install homie-studio check <id> --url http://127.0.0.1:8787)',
|
|
1198
1199
|
'when the owner asks to publish: npm run deploy (then studio_publish, or: npx --no-install homie-studio publish)',
|
|
1199
|
-
], online: 'Going online creates one Worker
|
|
1200
|
+
], online: 'Going online creates one Worker (with its Durable Objects and two Queues) and one D1 database on your own Cloudflare account: free plan, no payment method, no R2. `npx --no-install homie-studio deploy --plan` says exactly what, and changes nothing.' };
|
|
1200
1201
|
}
|
|
1201
1202
|
|
|
1202
1203
|
export const _test = { relative };
|
|
@@ -4782,15 +4782,30 @@
|
|
|
4782
4782
|
"(intro)": "af7f2f732505d379",
|
|
4783
4783
|
"## Version-matched authoring guides": "f6261a0e5801b96b",
|
|
4784
4784
|
"## Layout": "f3a280b8c96cd068",
|
|
4785
|
-
"## Apps: one screen, roles and parts":
|
|
4786
|
-
|
|
4785
|
+
"## Apps: one screen, roles and parts": [
|
|
4786
|
+
"2ecf713305fd26f5",
|
|
4787
|
+
"c10aa0441ea89702"
|
|
4788
|
+
],
|
|
4789
|
+
"## Commands (all through the pinned CLI in node_modules)": [
|
|
4790
|
+
"661cbdf5d6a9dbae",
|
|
4791
|
+
"c20d90de21cde0c7"
|
|
4792
|
+
],
|
|
4787
4793
|
"## The site": "0cb8dd9a6f8a0096",
|
|
4788
|
-
"## Making games":
|
|
4794
|
+
"## Making games": [
|
|
4795
|
+
"644c1e7608bb9740",
|
|
4796
|
+
"bb715272fd1a615b"
|
|
4797
|
+
],
|
|
4789
4798
|
"## The Game Codex and progress": "0738ef719cef3a4c",
|
|
4790
4799
|
"## Art direction and models": "a48fff7bf0a5f2fc",
|
|
4791
4800
|
"## Running live games (the back office)": "51ebf2ab756f6e4d",
|
|
4792
|
-
"## Servers and AI seats":
|
|
4793
|
-
|
|
4801
|
+
"## Servers and AI seats": [
|
|
4802
|
+
"1904f45b599a33e4",
|
|
4803
|
+
"1ccfd549d2369a60"
|
|
4804
|
+
],
|
|
4805
|
+
"## Selling things (the shop)": [
|
|
4806
|
+
"0df5a5fe1833c624",
|
|
4807
|
+
"e5bd380cb77f2b11"
|
|
4808
|
+
],
|
|
4794
4809
|
"## Continuing a build from the Claude app": "0d68081e924ca419",
|
|
4795
4810
|
"## Rules": "b2234d80f6cbe71d",
|
|
4796
4811
|
"## Telling Homie": "80b46fdd9e578db8",
|
|
@@ -4801,7 +4816,136 @@
|
|
|
4801
4816
|
"Homie Arcade": {
|
|
4802
4817
|
"sections": {
|
|
4803
4818
|
"AGENTS.md": {
|
|
4804
|
-
"## Commands (all through the pinned CLI in node_modules)":
|
|
4819
|
+
"## Commands (all through the pinned CLI in node_modules)": [
|
|
4820
|
+
"0a7b2c36414a29a0",
|
|
4821
|
+
"8364e05cebefcba4"
|
|
4822
|
+
]
|
|
4823
|
+
}
|
|
4824
|
+
}
|
|
4825
|
+
}
|
|
4826
|
+
}
|
|
4827
|
+
},
|
|
4828
|
+
"0.58.21": {
|
|
4829
|
+
"files": {
|
|
4830
|
+
"CLAUDE.md": "ddf2b2a0376202f6",
|
|
4831
|
+
"README.md": "684b3cca8001673c",
|
|
4832
|
+
"HANDOFF.md": "29337e2ace95b2e7",
|
|
4833
|
+
"games/README.md": "5ad32fe80c61f117",
|
|
4834
|
+
"music/README.md": "a3dc042dadd39df8",
|
|
4835
|
+
"videos/README.md": "16babc3437d5545b",
|
|
4836
|
+
"posts/README.md": "37841aae476bd540",
|
|
4837
|
+
"site/README.md": "1dbdf22c03a1e505",
|
|
4838
|
+
"site/src/worker.mjs": "6cc5c6131eaf6fd0"
|
|
4839
|
+
},
|
|
4840
|
+
"sections": {
|
|
4841
|
+
"AGENTS.md": {
|
|
4842
|
+
"(intro)": "af7f2f732505d379",
|
|
4843
|
+
"## Version-matched authoring guides": "f6261a0e5801b96b",
|
|
4844
|
+
"## Layout": "f3a280b8c96cd068",
|
|
4845
|
+
"## Apps: one screen, roles and parts": "2ecf713305fd26f5",
|
|
4846
|
+
"## Commands (all through the pinned CLI in node_modules)": "661cbdf5d6a9dbae",
|
|
4847
|
+
"## The site": "0cb8dd9a6f8a0096",
|
|
4848
|
+
"## Making games": "644c1e7608bb9740",
|
|
4849
|
+
"## The Game Codex and progress": "0738ef719cef3a4c",
|
|
4850
|
+
"## Art direction and models": "a48fff7bf0a5f2fc",
|
|
4851
|
+
"## Running live games (the back office)": "51ebf2ab756f6e4d",
|
|
4852
|
+
"## Servers and AI seats": "1904f45b599a33e4",
|
|
4853
|
+
"## Selling things (the shop)": "e5bd380cb77f2b11",
|
|
4854
|
+
"## Continuing a build from the Claude app": "0d68081e924ca419",
|
|
4855
|
+
"## Rules": "b2234d80f6cbe71d",
|
|
4856
|
+
"## Telling Homie": "80b46fdd9e578db8",
|
|
4857
|
+
"## Beta": "9d02638207a26e73"
|
|
4858
|
+
}
|
|
4859
|
+
},
|
|
4860
|
+
"named": {
|
|
4861
|
+
"Homie Arcade": {
|
|
4862
|
+
"sections": {
|
|
4863
|
+
"AGENTS.md": {
|
|
4864
|
+
"## Commands (all through the pinned CLI in node_modules)": "0a7b2c36414a29a0"
|
|
4865
|
+
}
|
|
4866
|
+
}
|
|
4867
|
+
}
|
|
4868
|
+
}
|
|
4869
|
+
},
|
|
4870
|
+
"0.58.22": {
|
|
4871
|
+
"files": {
|
|
4872
|
+
"CLAUDE.md": "ddf2b2a0376202f6",
|
|
4873
|
+
"README.md": "684b3cca8001673c",
|
|
4874
|
+
"HANDOFF.md": "29337e2ace95b2e7",
|
|
4875
|
+
"games/README.md": "5ad32fe80c61f117",
|
|
4876
|
+
"music/README.md": "a3dc042dadd39df8",
|
|
4877
|
+
"videos/README.md": "16babc3437d5545b",
|
|
4878
|
+
"posts/README.md": "37841aae476bd540",
|
|
4879
|
+
"site/README.md": "1dbdf22c03a1e505",
|
|
4880
|
+
"site/src/worker.mjs": "6cc5c6131eaf6fd0"
|
|
4881
|
+
},
|
|
4882
|
+
"sections": {
|
|
4883
|
+
"AGENTS.md": {
|
|
4884
|
+
"(intro)": "af7f2f732505d379",
|
|
4885
|
+
"## Version-matched authoring guides": "f6261a0e5801b96b",
|
|
4886
|
+
"## Layout": "f3a280b8c96cd068",
|
|
4887
|
+
"## Apps: one screen, roles and parts": "2ecf713305fd26f5",
|
|
4888
|
+
"## Commands (all through the pinned CLI in node_modules)": "661cbdf5d6a9dbae",
|
|
4889
|
+
"## The site": "0cb8dd9a6f8a0096",
|
|
4890
|
+
"## Making games": "644c1e7608bb9740",
|
|
4891
|
+
"## The Game Codex and progress": "0738ef719cef3a4c",
|
|
4892
|
+
"## Art direction and models": "a48fff7bf0a5f2fc",
|
|
4893
|
+
"## Running live games (the back office)": "51ebf2ab756f6e4d",
|
|
4894
|
+
"## Servers and AI seats": "1904f45b599a33e4",
|
|
4895
|
+
"## Selling things (the shop)": "e5bd380cb77f2b11",
|
|
4896
|
+
"## Continuing a build from the Claude app": "0d68081e924ca419",
|
|
4897
|
+
"## Rules": "b2234d80f6cbe71d",
|
|
4898
|
+
"## Telling Homie": "80b46fdd9e578db8",
|
|
4899
|
+
"## Beta": "9d02638207a26e73"
|
|
4900
|
+
}
|
|
4901
|
+
},
|
|
4902
|
+
"named": {
|
|
4903
|
+
"Homie Arcade": {
|
|
4904
|
+
"sections": {
|
|
4905
|
+
"AGENTS.md": {
|
|
4906
|
+
"## Commands (all through the pinned CLI in node_modules)": "0a7b2c36414a29a0"
|
|
4907
|
+
}
|
|
4908
|
+
}
|
|
4909
|
+
}
|
|
4910
|
+
}
|
|
4911
|
+
},
|
|
4912
|
+
"0.58.23": {
|
|
4913
|
+
"files": {
|
|
4914
|
+
"CLAUDE.md": "ddf2b2a0376202f6",
|
|
4915
|
+
"README.md": "684b3cca8001673c",
|
|
4916
|
+
"HANDOFF.md": "29337e2ace95b2e7",
|
|
4917
|
+
"games/README.md": "5ad32fe80c61f117",
|
|
4918
|
+
"music/README.md": "a3dc042dadd39df8",
|
|
4919
|
+
"videos/README.md": "16babc3437d5545b",
|
|
4920
|
+
"posts/README.md": "37841aae476bd540",
|
|
4921
|
+
"site/README.md": "1dbdf22c03a1e505",
|
|
4922
|
+
"site/src/worker.mjs": "6cc5c6131eaf6fd0"
|
|
4923
|
+
},
|
|
4924
|
+
"sections": {
|
|
4925
|
+
"AGENTS.md": {
|
|
4926
|
+
"(intro)": "af7f2f732505d379",
|
|
4927
|
+
"## Version-matched authoring guides": "f6261a0e5801b96b",
|
|
4928
|
+
"## Layout": "f3a280b8c96cd068",
|
|
4929
|
+
"## Apps: one screen, roles and parts": "2ecf713305fd26f5",
|
|
4930
|
+
"## Commands (all through the pinned CLI in node_modules)": "661cbdf5d6a9dbae",
|
|
4931
|
+
"## The site": "0cb8dd9a6f8a0096",
|
|
4932
|
+
"## Making games": "644c1e7608bb9740",
|
|
4933
|
+
"## The Game Codex and progress": "0738ef719cef3a4c",
|
|
4934
|
+
"## Art direction and models": "a48fff7bf0a5f2fc",
|
|
4935
|
+
"## Running live games (the back office)": "51ebf2ab756f6e4d",
|
|
4936
|
+
"## Servers and AI seats": "1904f45b599a33e4",
|
|
4937
|
+
"## Selling things (the shop)": "e5bd380cb77f2b11",
|
|
4938
|
+
"## Continuing a build from the Claude app": "0d68081e924ca419",
|
|
4939
|
+
"## Rules": "b2234d80f6cbe71d",
|
|
4940
|
+
"## Telling Homie": "80b46fdd9e578db8",
|
|
4941
|
+
"## Beta": "9d02638207a26e73"
|
|
4942
|
+
}
|
|
4943
|
+
},
|
|
4944
|
+
"named": {
|
|
4945
|
+
"Homie Arcade": {
|
|
4946
|
+
"sections": {
|
|
4947
|
+
"AGENTS.md": {
|
|
4948
|
+
"## Commands (all through the pinned CLI in node_modules)": "0a7b2c36414a29a0"
|
|
4805
4949
|
}
|
|
4806
4950
|
}
|
|
4807
4951
|
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homie-rocks/studio",
|
|
3
|
-
"version": "0.58.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.58.23",
|
|
4
|
+
"description": "Open-source tools that let your AI build and run a multiplayer game studio on its own Cloudflare: an MCP server, multiplayer starters, Workers and Durable Objects.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
|
-
"homepage": "https://homie.rocks/studio/",
|
|
7
|
+
"homepage": "https://homie.rocks/make/studio/",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"mcp",
|
|
10
|
+
"mcp-server",
|
|
11
|
+
"ai-agents",
|
|
12
|
+
"multiplayer",
|
|
13
|
+
"multiplayer-games",
|
|
14
|
+
"game-studio",
|
|
15
|
+
"game-engine",
|
|
16
|
+
"cloudflare-workers",
|
|
17
|
+
"durable-objects",
|
|
18
|
+
"cloudflare",
|
|
19
|
+
"netplay",
|
|
20
|
+
"open-source"
|
|
21
|
+
],
|
|
8
22
|
"repository": {
|
|
9
23
|
"type": "git",
|
|
10
24
|
"url": "git+https://github.com/homie-rocks/homie.git",
|
|
@@ -168,11 +182,12 @@
|
|
|
168
182
|
"zod": "4.3.6"
|
|
169
183
|
},
|
|
170
184
|
"scripts": {
|
|
185
|
+
"pretest": "tsc --build ../../tsconfig.json",
|
|
171
186
|
"test": "node --test --test-concurrency=2 --test-timeout=1800000 test/*.test.mjs",
|
|
172
|
-
"prepack": "node lib/build-identity.mjs --check && node ../../scripts/authoring-guides.mjs --check"
|
|
187
|
+
"prepack": "node lib/build-identity.mjs --check && node ../../scripts/authoring-guides.mjs --check && node ../../scripts/studio-template-history.mjs --check"
|
|
173
188
|
},
|
|
174
189
|
"peerDependencies": {
|
|
175
|
-
"@homie-rocks/physics": "0.1.
|
|
190
|
+
"@homie-rocks/physics": "0.1.3"
|
|
176
191
|
},
|
|
177
192
|
"peerDependenciesMeta": {
|
|
178
193
|
"@homie-rocks/physics": {
|