@homie-rocks/studio 0.24.2 → 0.24.4
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 +56 -1
- package/README.md +51 -5
- package/agents/GUIDES.md +13 -6
- package/bin/homie-studio.mjs +77 -15
- package/chat/CHAT.md +2 -0
- package/lib/agent-seat.mjs +60 -2
- package/lib/brain-probe.mjs +6 -5
- package/lib/build.mjs +3 -0
- package/lib/cloudflare.mjs +13 -3
- package/lib/doctor.mjs +64 -5
- package/lib/local-ai.mjs +66 -0
- package/lib/mcp-tools.mjs +10 -2
- package/lib/projects-env.mjs +68 -0
- package/lib/projects.mjs +311 -0
- package/lib/scaffold.mjs +20 -2
- package/lib/servers.mjs +31 -2
- package/lib/shop-catalog.mjs +163 -0
- package/lib/shop.mjs +151 -60
- package/lib/template-history.json +78 -0
- package/netplay/NETPLAY.md +86 -12
- package/netplay/netplay.ts +82 -0
- package/package.json +1 -1
- package/shop/SELLING.md +16 -0
- package/shop/SHOP.md +65 -16
- package/starters/ember-vale/game.json +1 -0
- package/starters/ember-vale/src/main.ts +80 -6
- package/worker/agents.mjs +93 -47
- package/worker/brain.mjs +356 -5
- package/worker/chat.mjs +13 -6
- package/worker/index.mjs +57 -2
- package/worker/office.mjs +65 -4
- package/worker/room.mjs +48 -3
- package/worker/shop-rules.mjs +17 -1
- package/worker/shop.mjs +32 -9
- package/worker/stripe.mjs +30 -0
- package/worker/version.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -17,9 +17,64 @@ 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.24.4] - 2026-10-03
|
|
21
|
+
|
|
22
|
+
**Plugin 0.25.4** · [#37](https://github.com/homie-rocks/homie/pull/37)
|
|
23
|
+
|
|
24
|
+
Your AI guides think with Cloudflare's Clef decision model, which does what a player asks; Clef runs on your own computer while you build; and your games can ask it for their own decisions (tactics, a director's call, a turn) in about a quarter of a second.
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- **Clef thinks for your AI guides.** On a server whose guides use Workers AI, the default model is now `@cf/cloudflare/clef-flash`, Cloudflare's open-source decision model (Apache 2.0). It never writes text: each decision is asked as typed Choices of your `agents.json` (which goal, which quest or place or player, which line or none), and its answer is checked by the same rules as before. On 64 recorded Ember Vale moments it did what an open ask asked 36 times out of 40 by itself (the old Llama 3.1 8B: 0 of 40), and blind judges preferred its decisions in 51 of 64 (Llama's in none). In a live room it answered all 9 asks itself.
|
|
29
|
+
- `homie-studio agents try <game> --view <file.json> [--ask ask_help:quest=king-slime --from 0] [--model <id>]`: what your guides' brain would decide in one moment, with no seat taken: the decision, the brain's own pick before the fixed rules, how sure it was, how long it took and what it cost. Use it to test an `agents.json` or to compare a model before setting it.
|
|
30
|
+
- **Clef on your own computer.** With [Ollama](https://ollama.com) 0.35.1 or later and `clef-flash` on this computer, `homie-studio dev` runs the guides, chat review and your game's decisions on it: free, nothing sent to Cloudflare (`--no-local-ai` turns it off). `homie-studio agents sit <game> --brain local` (the MCP's `agent_sit { brain: "local" }`) seats a guide that thinks on your computer every few seconds. `setup status` has a "Clef on this computer" row. Nothing ever downloads the model by itself: `ollama pull clef-flash` is about 11 GB, and it is your call.
|
|
31
|
+
- **Your game's own decisions** (NETPLAY.md section 20): `net.decide(state, questions, { floor })` on the host, for a game whose `game.json` says `"decide": true`. Ask a Choice (2 to 26 options), a yes/no or a Score about your game's state; the room asks Clef and answers with option ids and numbers, within the same daily budget as the guides, at most one ask every 3 s per room. It always answers: with your own floor when there is no AI, no budget, or no answer in time. Measured: about a quarter of a second there and back, about 4 neurons for three questions. Ask per beat or per turn, never per frame.
|
|
32
|
+
- Ember Vale's slimes can think (opt-in: set `"decide": true` in its `game.json`): every 6 s the slimes' director picks how they hunt (rush, surround, gang up on the most hurt, regroup at the King), whether a wave comes and how hard to push, and every screen shows what the slimes are up to. On a kids server they never gang up on the most hurt and never push past steady.
|
|
33
|
+
- The office shows each room's game decisions (how many, who answered, the model's time, the neurons), and each guide decision's model, how sure it was and, when a rule overruled it, what the brain itself chose.
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- A player's ask is now answered with the values they asked for: a brain that picks the asked goal with another quest or place is overruled, like one that picks another goal.
|
|
38
|
+
- House guides remember the lines they said and who is new to them: they greet a newcomer once and otherwise stay quiet unless asked or something changes.
|
|
39
|
+
- Clef costs more neurons a decision than Llama (about 9 against 4: it reads its questions and answers, and writes nothing), so the default 8,000 a day is about 900 guide decisions. Set `HOMIE_BRAIN_MODEL` to `@cf/meta/llama-3.1-8b-instruct-fp8-fast` in `wrangler.jsonc` to keep the old model.
|
|
40
|
+
- The doctor's Workers AI check asks Clef one yes/no question (it does not take a chat message).
|
|
41
|
+
|
|
42
|
+
### Upgrade notes
|
|
43
|
+
|
|
44
|
+
- `npm run deploy` after upgrading: guides on Workers AI switch to Clef at their next decision. Nothing else to do.
|
|
45
|
+
- To try local Clef, install Ollama (0.35.1 or later), say yes to the download, and run `ollama pull clef-flash`; then `npm run dev`.
|
|
46
|
+
|
|
47
|
+
## [0.24.3] - 2026-10-03
|
|
48
|
+
|
|
49
|
+
**Plugin 0.25.3** · [#36](https://github.com/homie-rocks/homie/pull/36) · [release-2026-10-03-studio-0.24.3](https://github.com/homie-rocks/homie/releases/tag/release-2026-10-03-studio-0.24.3)
|
|
50
|
+
|
|
51
|
+
Set up your shop with Stripe's own tools for AI: your AI makes the products, checks tax, makes the webhook and answers "how are sales?"; you make the account, approve Stripe's pages and paste one key.
|
|
52
|
+
|
|
53
|
+
### Added
|
|
54
|
+
|
|
55
|
+
- Stripe's agent plugin is how your AI works with your Stripe (`npm install -g @stripe/cli@latest && stripe agent setup`: Stripe's own MCP server and skills for Claude Code and Codex, or Stripe's connector in the Claude app). It is set up when your studio starts selling, never before: `setup status` shows a Stripe row once `shop.json` exists. You sign in once on Stripe's page and give it a sandbox first.
|
|
56
|
+
- `homie-studio shop catalog`: your items as Products in Stripe, made by your AI through Stripe's MCP. It names the one read to make, then (`--have <Stripe's answer>`) exactly the writes still needed: a Product per item with a tax code eligible for Managed Payments and a default Price of `shop.json`'s amount. A new price becomes a new default Price; an item you removed is archived, never deleted; products you made yourself are never touched. In sync, `shop.json` records `"catalog": ["test"]` and checkouts name each item's Product, so Stripe's Dashboard and reports see sales by product. `shop.json`'s price is still what is charged.
|
|
57
|
+
- `homie-studio shop connect` now makes the webhook itself: you paste one restricted key (with Webhook Endpoints: Write added to its permissions), the page makes the endpoint with it, and Stripe's signing secret goes straight to your Worker without anyone seeing it. An older endpoint the kit made for the same address is turned off, not deleted. A key without that permission still works with a webhook secret you made yourself.
|
|
58
|
+
- With Managed Payments chosen in test mode, the connect page tries one test checkout with it (expired at once) and tells you whether Stripe has it on for your account.
|
|
59
|
+
- A refund Stripe holds for approval (when the shop's key is an Agent key) is shown as held, not failed: approve it in Stripe and the item leaves the player's account when Stripe refunds. Refunds made in Stripe's Dashboard or through Stripe's MCP take the item back the same way.
|
|
60
|
+
- `homie-studio setup --via stripe-projects [--with elevenlabs]` (a prototype): for a creator with no Cloudflare account, Stripe Projects makes or links one (and ElevenLabs) on their own Stripe sign-in, on free plans. It stops at each step that is yours (Stripe's sign-in, the providers' terms, Cloudflare's approval page), puts the account id in `studio.json`, keeps the token in Projects' vault and its git-ignored `.env`, and every Wrangler run in the studio uses it. `npx wrangler login` stays the default.
|
|
61
|
+
- The plugin's shop skill now walks a creator with a fresh Stripe account through it: what you click, what to tell your AI, and what stays yours. The studio-setup skill has the Stripe Projects option.
|
|
62
|
+
|
|
63
|
+
### Changed
|
|
64
|
+
|
|
65
|
+
- The Homie mod refuses a write through Stripe's MCP that would make a webhook endpoint (or an event destination with its secret): Stripe answers the signing secret in that call, and it would land in the conversation. The connect page makes the webhook instead.
|
|
66
|
+
- The Homie mod now takes Stripe webhook signing secrets (`whsec_…`) out of tool output, as it does Stripe keys.
|
|
67
|
+
- `providers.json` lists Stripe: its agent plugin and MCP server for the shop skill, and Stripe Projects for studio-setup. The shop skill declares Stripe's MCP server for Codex. The Homie mod holds `stripe projects upgrade`, `billing add` / `billing update` and anything with `--confirm-paid-service` as paid calls whose cost cannot be read first.
|
|
68
|
+
- A new studio's `.gitignore` also keeps `.env.*`, `.projects/vault/` and `.projects/cache/` out of git; `upgrade` adds them to yours.
|
|
69
|
+
|
|
70
|
+
### Upgrade notes
|
|
71
|
+
|
|
72
|
+
- Stripe's MCP accepts only its sign-in page (OAuth) or keys tagged "Agent" from 2026-10-31. If you gave an AI a plain Stripe key for the MCP, replace it with an Agent key or sign in again. The shop's own key in your Worker is not affected, and must stay a plain restricted key.
|
|
73
|
+
- To have the connect page make your webhook, add Webhook Endpoints: Write to the shop's restricted key (or make a new key) and run `homie-studio shop connect` again. Afterwards you can set that permission back to None.
|
|
74
|
+
|
|
20
75
|
## [0.24.2] - 2026-10-03
|
|
21
76
|
|
|
22
|
-
**Plugin 0.25.2** · [#35](https://github.com/homie-rocks/homie/pull/35)
|
|
77
|
+
**Plugin 0.25.2** · [#35](https://github.com/homie-rocks/homie/pull/35) · [release-2026-10-03-studio-0.24.2](https://github.com/homie-rocks/homie/releases/tag/release-2026-10-03-studio-0.24.2)
|
|
23
78
|
|
|
24
79
|
Homie's skills work through each provider's own CLI, plugin and MCP server, declared so they load only when a skill needs them, and the Homie mod holds the paid calls and Cloudflare changes those tools can make.
|
|
25
80
|
|
package/README.md
CHANGED
|
@@ -284,8 +284,21 @@ npx homie-studio agents pass night-rush --label Claude
|
|
|
284
284
|
day); with none, the game's scripted floor. The local MCP's `agent_sit` puts the owner's own Claude in a guide's
|
|
285
285
|
seat. The AI never types: it picks ids; the room drops anything else.
|
|
286
286
|
[agents/GUIDES.md](agents/GUIDES.md) is the short path for an existing RPG.
|
|
287
|
+
- **Clef thinks for the guides** (0.24.4): Workers AI's default for them is Cloudflare's decision model
|
|
288
|
+
(`@cf/cloudflare/clef-flash`), which answers a decision as typed Choices (goal, values, line or none) and never
|
|
289
|
+
writes text. On 64 recorded Ember Vale moments it answered 36 of 40 open asks itself (Llama 3.1 8B: none) and blind
|
|
290
|
+
judges preferred it in 51 of 64, at about 9 neurons a decision (Llama: 4). `HOMIE_BRAIN_MODEL` still picks Llama or
|
|
291
|
+
the 27B Clef; `agents try <game> --view <file> [--ask …] [--model …]` shows what the brain would decide in a moment.
|
|
292
|
+
- **Clef on your own computer** (0.24.4): with Ollama and `clef-flash` here, `dev` runs the guides, chat review and
|
|
293
|
+
game decisions on it, free and with no Cloudflare, and `agents sit --brain local` (the MCP's `agent_sit { brain:
|
|
294
|
+
"local" }`) seats a guide that thinks here. Nothing downloads it: `ollama pull clef-flash` is about 11 GB, the
|
|
295
|
+
person's choice (doctor's "Clef on this computer" row says what is there).
|
|
296
|
+
- **A game's own decisions** (0.24.4, NETPLAY.md section 20): `net.decide(state, questions, { floor })` on the host,
|
|
297
|
+
for a game whose game.json says `"decide": true`: tactics for its opponents, an NPC's reaction, a director's call,
|
|
298
|
+
a turn's move, answered by Clef in the room in about a quarter of a second, per beat (never per frame), within the
|
|
299
|
+
AI brains' day; the floor answers whenever it cannot. Ember Vale's slimes pick their tactics this way (opt-in).
|
|
287
300
|
- Everything is in the studio's own Worker and D1 (migration `0006_studio_servers.sql`). The site's side is in
|
|
288
|
-
[site/SITE.md](site/SITE.md); the room's in [netplay/NETPLAY.md](netplay/NETPLAY.md) sections 17 and
|
|
301
|
+
[site/SITE.md](site/SITE.md); the room's in [netplay/NETPLAY.md](netplay/NETPLAY.md) sections 17, 18 and 20.
|
|
289
302
|
|
|
290
303
|
## Room chat
|
|
291
304
|
|
|
@@ -314,7 +327,8 @@ npx homie-studio chat words # the bu
|
|
|
314
327
|
- **Moderation, in the studio's own Worker, before anyone sees a typed line:** a built-in word list and patterns
|
|
315
328
|
(slurs, sexual words, self-harm, personal questions and contact details, links, swears, your own words), then
|
|
316
329
|
Cloudflare's **Clef** decision model on the studio's own Workers AI (`@cf/cloudflare/clef-flash`, about 2.3
|
|
317
|
-
neurons a message measured, 2,000 a day by default: inside the free allocation
|
|
330
|
+
neurons a message measured, 2,000 a day by default: inside the free allocation; under `dev`, Clef on this computer
|
|
331
|
+
when Ollama has it, free). Emoji and quick lines are never reviewed.
|
|
318
332
|
- **The owner:** every live room's last minutes in the office with Remove, Mute and Kick (from a line, with their
|
|
319
333
|
lines taken down), the same in the owner's own game, the reports players make, and Announce (a studio line).
|
|
320
334
|
- **Speech bubbles:** `net.on('say')` and the port kit's `createBubbles` / `paintBubbles`; Gem Rush, Ember Vale and
|
|
@@ -325,7 +339,7 @@ npx homie-studio chat words # the bu
|
|
|
325
339
|
|
|
326
340
|
## Selling things: the shop
|
|
327
341
|
|
|
328
|
-
A studio sells items for its games (0.24.0) with **its own Stripe account**: the studio is the seller, money goes
|
|
342
|
+
A studio sells items for its games (0.24.0; Stripe's own agent tools since 0.24.3) with **its own Stripe account**: the studio is the seller, money goes
|
|
329
343
|
straight from players to its Stripe, and homie.rocks never sees, holds or moves it (Homie takes no cut; no shared
|
|
330
344
|
currency; not a Connect platform). Real-money prices only, nothing random, the kids rules built in (no shop on a kids
|
|
331
345
|
server, spending off until a neutral age question says adult, nothing for under-13s, 13-17 through a parent's own
|
|
@@ -334,11 +348,20 @@ checkout, a monthly cap), and the television only shows a code to buy on a phone
|
|
|
334
348
|
```sh
|
|
335
349
|
npx homie-studio shop init --supporter # shop.json with a US$5 Supporter pack, and SELLING.md (the owner's plain words)
|
|
336
350
|
npx homie-studio shop check # the kit's rules; every build checks them too
|
|
337
|
-
npx homie-studio shop
|
|
351
|
+
npx homie-studio shop catalog [--have <file>] # the items as Products in Stripe: the read, then the exact writes (Stripe's MCP)
|
|
352
|
+
npx homie-studio shop connect # a page on THIS computer: the owner pastes one restricted key; it makes the webhook
|
|
338
353
|
npx homie-studio shop # open (test or live) or what is missing
|
|
339
354
|
npx homie-studio shop refund ord_… # an ASK: the owner taps once (or Refund in /_studio/office/shop)
|
|
340
355
|
```
|
|
341
356
|
|
|
357
|
+
Since 0.24.3 the owner's AI sets it up with **Stripe's own agent tools** (`npm install -g @stripe/cli@latest && stripe
|
|
358
|
+
agent setup`: Stripe's MCP server and skills; the owner signs in once on Stripe's page, a sandbox first): it makes the
|
|
359
|
+
catalog through the MCP (`shop catalog` says exactly what to write), reads the tax settings, and answers "how are
|
|
360
|
+
sales?" read-only. The owner makes the account, approves Stripe's pages and pastes **one** restricted key on the
|
|
361
|
+
connect page, which makes the webhook with it, so its signing secret goes straight to the Worker and never through a
|
|
362
|
+
chat. A webhook is never made through Stripe's MCP (its answer carries the secret). From 2026-10-31 Stripe's MCP takes
|
|
363
|
+
only OAuth or Agent-tagged keys; the shop's own key is a plain restricted key and is not affected.
|
|
364
|
+
|
|
342
365
|
In a game: `createShop()` from `@homie-rocks/studio/shop` (`has`, `entitlements`, `on('change')`, `open`, `used`); a
|
|
343
366
|
supporter's badge rides on their seat (`peer.badge`). Stripe Checkout (hosted), Stripe Tax on, or Stripe Managed
|
|
344
367
|
Payments (Stripe as seller of record, 3.5% more) as one switch; a signed, idempotent webhook; refunds from the
|
|
@@ -356,7 +379,9 @@ One checklist of what this computer and the person's accounts have for a studio:
|
|
|
356
379
|
connector (the AI says whether its Homie tools are there; the directory is reached), Cloudflare (signed in
|
|
357
380
|
with the studio's own Wrangler, and the account's email verified: a deploy that went through proves it, a
|
|
358
381
|
deploy refused for it is remembered), Chrome for the checks, ffmpeg, and the optional GitHub, ElevenLabs and
|
|
359
|
-
fal (and, in Claude Code, the status line)
|
|
382
|
+
fal (and, in Claude Code, the status line); in a studio that sells (shop.json), a Stripe row: whether Stripe's own
|
|
383
|
+
agent plugin (its MCP server) is set up for this AI, with `stripe agent setup` as the fix (0.24.3); and in a studio, whether Clef is on this
|
|
384
|
+
computer (Ollama with `clef-flash`, optional, never downloaded without the person's yes) (0.24.4). Each row is green, missing or "do this now", says what it
|
|
360
385
|
unlocks, and gives the exact fix: a command the AI runs or a page the person taps. It only reads, answers in
|
|
361
386
|
seconds, and never prints a key, a token or an account's name or id. A studio the Claude app's setup card made
|
|
362
387
|
(`setup attach`) counts as connected to Cloudflare and GitHub there. `--json` gives the rows.
|
|
@@ -613,6 +638,27 @@ the home folder's `.cache/homie-studio` (from storage.googleapis.com, which a cl
|
|
|
613
638
|
renders with SwiftShader. `check` reports how fast each browser drew the game and on what renderer; on SwiftShader
|
|
614
639
|
it says the frame rate is not a person's, and judges only seats, rooms and rounds.
|
|
615
640
|
|
|
641
|
+
## Another way to Cloudflare: Stripe Projects (a prototype, 0.24.3)
|
|
642
|
+
|
|
643
|
+
`npx wrangler login` stays the way. For a creator with no Cloudflare account (and no ElevenLabs), Stripe Projects
|
|
644
|
+
(Stripe's CLI plugin, docs.stripe.com/projects) can make or link both on the person's own Stripe sign-in, on free
|
|
645
|
+
plans, and hand the credentials to the studio folder:
|
|
646
|
+
|
|
647
|
+
```sh
|
|
648
|
+
npx homie-studio setup --via stripe-projects [--with elevenlabs] --dry-run # what it would do; changes nothing
|
|
649
|
+
npx homie-studio setup --via stripe-projects [--with elevenlabs] --accept-tos # after the person accepted the providers' terms
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
It stops with a `needs` step at each thing that is the person's (the Stripe CLI and its Projects plugin, Stripe's
|
|
653
|
+
sign-in, the providers' terms, Cloudflare's own approval page for an existing account); it never adds a paid plan or
|
|
654
|
+
a payment method. The Cloudflare account id goes in studio.json with `"auth": "stripe-projects"`; the token stays in
|
|
655
|
+
Projects' vault and the git-ignored `.env` it syncs, and every Wrangler run in the studio takes it from there
|
|
656
|
+
(`deploy`, `dev`, secrets, the office). It checks read-only what the token can do (Wrangler signed in, D1, Workers AI;
|
|
657
|
+
R2 is optional) and that `.gitignore` keeps `.env`, `.env.*`, `.projects/vault/` and `.projects/cache/` out of git.
|
|
658
|
+
With `--with elevenlabs` the music skill finds the ElevenLabs key there too (ElevenLabs' free plan has no commercial
|
|
659
|
+
licence). Unverified until a real run: the exact JSON of the Projects commands, and the Cloudflare token's permission
|
|
660
|
+
groups, which the checks measure instead of assuming.
|
|
661
|
+
|
|
616
662
|
## No payment method needed
|
|
617
663
|
|
|
618
664
|
`deploy` creates only what Cloudflare's free Workers plan gives a new account with no
|
package/agents/GUIDES.md
CHANGED
|
@@ -10,8 +10,8 @@ Vale (`homie-studio game new <id> --from ember-vale`) is the worked example.
|
|
|
10
10
|
- A beginner server whose AI guide seats (always marked "· AI") follow new heroes, lead them to places, take on a
|
|
11
11
|
quest with them, and pull back when asked.
|
|
12
12
|
- **Hands** are your game's own bot code, every frame, at the party's skill dial. **The brain** picks a goal every
|
|
13
|
-
few seconds: the studio's own Workers AI, the owner's own key, or the owner's Claude in a seat. With none
|
|
14
|
-
(or over the day's budget) your scripted `decide` plays.
|
|
13
|
+
few seconds: the studio's own Workers AI (Clef), the owner's own key, or the owner's Claude in a seat. With none
|
|
14
|
+
of them (or over the day's budget) your scripted `decide` plays.
|
|
15
15
|
- The AI never types. It chooses ids from your `agents.json`; your game draws your own words.
|
|
16
16
|
|
|
17
17
|
## 1. Upgrade
|
|
@@ -86,10 +86,17 @@ const agents = useAgents(room.net, vocab as unknown as Vocabulary, {
|
|
|
86
86
|
|
|
87
87
|
1. `homie-studio servers new <id> "First Steps" --policy beginner --guides 2` (add `--kids` for children).
|
|
88
88
|
2. `homie-studio agents brain <id> first-steps workers-ai`: the first time is an ASK the owner taps; then
|
|
89
|
-
`npm run deploy` once (it binds Workers AI). The
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
`npm run deploy` once (it binds Workers AI). The guides think with Cloudflare's Clef decision model
|
|
90
|
+
(`@cf/cloudflare/clef-flash`, the default since 0.24.4): it answers each decision as typed Choices of your
|
|
91
|
+
goals, values and lines, so it does what an ask asks far more often than a chat model did. About 9 neurons a
|
|
92
|
+
decision: the default budget of 8,000 a day is about 900 decisions (the free allocation is 10,000). Or
|
|
93
|
+
`owner-key` with `agents brain key` on the owner's own computer.
|
|
94
|
+
3. Try it: `homie-studio agents try <id> --view view.json --ask ask_help:quest=<quest> --from 0` shows what the
|
|
95
|
+
brain would decide in that moment (with `--model` to compare one). Then two browsers on the server's page, walk
|
|
96
|
+
up to a guide, tap "Help me with ...". In a terminal or the local MCP, `agent_sit { game: "<id>", server:
|
|
97
|
+
"first-steps" }` puts your own Claude in a guide's seat; `brain: "local"` lets Clef on your computer decide.
|
|
98
|
+
Under `npm run dev` the guides think with Clef on your own computer when Ollama has `clef-flash` (free; it is
|
|
99
|
+
about 11 GB and only ever downloaded when you say so: `ollama pull clef-flash`).
|
|
93
100
|
|
|
94
101
|
## 5. Do not
|
|
95
102
|
|
package/bin/homie-studio.mjs
CHANGED
|
@@ -18,8 +18,10 @@
|
|
|
18
18
|
* homie-studio games
|
|
19
19
|
* homie-studio build [<id>] [--maps] (--maps: also keep each bundle's source map and module sizes in .studio/maps/<id>/,
|
|
20
20
|
* never in site/dist: what `perf` reads a CPU profile through)
|
|
21
|
-
* homie-studio dev [--port 8787] [--remote-ai] (--stop: stop exactly this studio's dev server, nothing else;
|
|
22
|
-
* AI guides
|
|
21
|
+
* homie-studio dev [--port 8787] [--remote-ai] [--no-local-ai] (--stop: stop exactly this studio's dev server, nothing else;
|
|
22
|
+
* AI guides, chat review and game decisions think with Clef on this computer
|
|
23
|
+
* when Ollama has clef-flash (free; dev never downloads it), else scripted;
|
|
24
|
+
* --remote-ai: the real Workers AI, billed)
|
|
23
25
|
* homie-studio check <id> [--url <site>] [--shots <dir>]
|
|
24
26
|
* homie-studio perf <id> [--url <site>] [--device computer,phone] [--runs 1] [--seconds 15] [--warm 3] [--profile]
|
|
25
27
|
* [--cpu 4] [--out <dir>] [--max-load 0.8] [--pair <label>]
|
|
@@ -121,12 +123,21 @@
|
|
|
121
123
|
* time AI guides may talk are ASKED for, like office kick; --budget is
|
|
122
124
|
* Workers AI neurons a day, or dollars a day for the owner's key)
|
|
123
125
|
* homie-studio agents brain key [--remove] (the owner's AI key, typed into a page on this computer only)
|
|
126
|
+
* homie-studio agents sit <game> [--server <id>] [--brain local] (a guide's seat from this terminal; --brain local:
|
|
127
|
+
* Clef on this computer decides every few seconds, free; never downloads it)
|
|
128
|
+
* homie-studio agents try <game> --view <file.json> [--ask <ask>[:<arg>=<value>] --from <seat>] [--model <id>]
|
|
129
|
+
* (what the guides' brain would decide in that moment, with no seat taken;
|
|
130
|
+
* spent from the guides' day; --model compares a model before setting it)
|
|
124
131
|
* homie-studio shop is the studio's shop selling (its own Stripe), and if not, what is missing
|
|
125
132
|
* homie-studio shop init [--supporter] [--currency usd] [--price 500] [--managed] shop.json and SELLING.md
|
|
126
133
|
* homie-studio shop check shop.json against the kit's rules (real money, nothing random, the kids rules)
|
|
127
134
|
* homie-studio shop connect [--managed] [--live] a page on THIS computer where the owner pastes the studio's
|
|
128
|
-
* restricted Stripe key
|
|
129
|
-
*
|
|
135
|
+
* restricted Stripe key; with it this makes the webhook (0.24.3), and the key
|
|
136
|
+
* and the webhook's secret go straight to the Worker secrets, never a chat or a
|
|
137
|
+
* file (test keys only unless --live)
|
|
138
|
+
* homie-studio shop catalog [--have <file>|-] [--mode test|live] the items as Products in the studio's Stripe,
|
|
139
|
+
* made by the AI through Stripe's own MCP: the read first, then (with what it
|
|
140
|
+
* answered) the exact writes; in sync, shop.json "catalog" records the mode
|
|
130
141
|
* homie-studio shop disconnect both secrets gone: the shop closes
|
|
131
142
|
* homie-studio shop orders the latest orders (never a card or an email)
|
|
132
143
|
* homie-studio shop refund <order> [--reason …] [--note "<why>"] ASKS the owner (a one-tap link)
|
|
@@ -162,6 +173,13 @@
|
|
|
162
173
|
* the build so the chat's card follows it, and print the steps; HANDOFF.md)
|
|
163
174
|
* homie-studio setup attach <hs_…> (this repository is the studio the Claude app's setup card is making: say so, once,
|
|
164
175
|
* learn its live address, and give a template copy its real name)
|
|
176
|
+
* homie-studio setup --via stripe-projects [--with elevenlabs] [--accept-tos] [--dry-run]
|
|
177
|
+
* (a prototype, 0.24.3: Cloudflare, and ElevenLabs with --with, made or linked
|
|
178
|
+
* through Stripe Projects on their free plans; the person accepts the providers'
|
|
179
|
+
* terms (then --accept-tos) and signs in on Stripe's and the provider's own
|
|
180
|
+
* pages; the account id goes into studio.json, the token stays in Projects'
|
|
181
|
+
* vault and its git-ignored .env, and Wrangler runs with it here. The usual
|
|
182
|
+
* way, npx wrangler login, stays the default. lib/projects.mjs)
|
|
165
183
|
* homie-studio setup status [--connector yes|no] (also: homie-studio doctor)
|
|
166
184
|
* what this computer and the person's accounts have for a studio: Node, the Homie
|
|
167
185
|
* connector, Cloudflare (signed in, email verified), Chrome, ffmpeg, GitHub,
|
|
@@ -234,9 +252,13 @@ import { statsKey, statsLink, statsRevoke, statsShare, statsShow } from '../lib/
|
|
|
234
252
|
import { playersOwner, playersShow } from '../lib/players.mjs';
|
|
235
253
|
import { officeAnnounce, officeClose, officeInvite, officeKey, officeKick, officeLaunch, officeLines, officeLink, officeMute, officeRevoke, officeShow } from '../lib/office.mjs';
|
|
236
254
|
import { chatBudget, chatLines, chatRemove, chatRulesSet, chatShow, chatWords } from '../lib/chat-cli.mjs';
|
|
237
|
-
import {
|
|
255
|
+
import { detectLocalAi, localAiVars } from '../lib/local-ai.mjs';
|
|
256
|
+
import { agentsBrain, agentsBrainKey, agentsPass, agentsPasses, agentsRevoke, agentsTry, serversClose, serversLevel, serversLines, serversList, serversMember, serversNew, serversSet } from '../lib/servers.mjs';
|
|
238
257
|
import { AgentSeat } from '../lib/agent-seat.mjs';
|
|
239
258
|
import { shopCheck, shopConnect, shopDisconnect, shopInit, shopLines, shopOrders, shopRefund, shopStatements, shopStatus } from '../lib/shop.mjs';
|
|
259
|
+
import { catalogLines, catalogPlan } from '../lib/shop-catalog.mjs';
|
|
260
|
+
import { projectsLines, setupViaProjects } from '../lib/projects.mjs';
|
|
261
|
+
import { projectsCloudflareEnv } from '../lib/projects-env.mjs';
|
|
240
262
|
import { Feed, currentFeed, currentId, flushProgress, publicFeed, readFeed, recordChange, startProgress } from '../lib/progress.mjs';
|
|
241
263
|
import { formatStatus, setupStatus } from '../lib/doctor.mjs';
|
|
242
264
|
import { codexTarget, newCodex, writeCodexPage } from '../lib/codex.mjs';
|
|
@@ -249,7 +271,7 @@ import { formatHandoff, handoff } from '../lib/handoff.mjs';
|
|
|
249
271
|
|
|
250
272
|
const argv = process.argv.slice(2);
|
|
251
273
|
const flags = new Map();
|
|
252
|
-
const BOOL_FLAGS = ['off', 'revoke', 'json', 'yes', 'detach', 'no-install', 'plan', 'stop', 'share', 'apply', 'diff', 'template', 'ci', 'fresh', 'install', 'remove', 'replace', 'artifact', 'open', 'reopen', 'kids', 'remote-ai', 'dry-run', 'verify', 'maps', 'profile', 'hands-on', 'automatic', 'unlock', 'confirm', 'no-library', 'no-validate', 'rigged', 'supporter', 'managed', 'live', 'send'];
|
|
274
|
+
const BOOL_FLAGS = ['off', 'revoke', 'json', 'yes', 'detach', 'no-install', 'plan', 'stop', 'share', 'apply', 'diff', 'template', 'ci', 'fresh', 'install', 'remove', 'replace', 'artifact', 'open', 'reopen', 'kids', 'remote-ai', 'dry-run', 'verify', 'maps', 'profile', 'hands-on', 'automatic', 'unlock', 'confirm', 'no-library', 'no-validate', 'rigged', 'supporter', 'managed', 'live', 'send', 'accept-tos', 'quiet', 'no-local-ai'];
|
|
253
275
|
const positional = [];
|
|
254
276
|
for (let i = 0; i < argv.length; i++) {
|
|
255
277
|
const a = argv[i];
|
|
@@ -461,6 +483,12 @@ function print(result) {
|
|
|
461
483
|
case 'shop disconnect':
|
|
462
484
|
lines.push(...shopLines(result));
|
|
463
485
|
break;
|
|
486
|
+
case 'shop catalog':
|
|
487
|
+
lines.push(...catalogLines(result));
|
|
488
|
+
break;
|
|
489
|
+
case 'setup':
|
|
490
|
+
lines.push(...projectsLines(result));
|
|
491
|
+
break;
|
|
464
492
|
case 'chat rules':
|
|
465
493
|
case 'chat budget':
|
|
466
494
|
case 'shop refund':
|
|
@@ -662,6 +690,12 @@ async function main() {
|
|
|
662
690
|
if (cmd === 'codex') return codexCommand(root, sub);
|
|
663
691
|
if (cmd === 'progress') return progressCommand(root, sub);
|
|
664
692
|
if (cmd === 'setup' && sub === 'attach') return setupAttach(root, positional[2], { homie: flags.get('homie') });
|
|
693
|
+
if (cmd === 'setup' && flags.has('via')) {
|
|
694
|
+
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)' };
|
|
695
|
+
const extra = String(flags.get('with') ?? '').split(',').map((x) => x.trim()).filter(Boolean);
|
|
696
|
+
if (extra.some((x) => x !== 'elevenlabs')) return { ok: false, command: 'setup', why: '--with takes elevenlabs (Cloudflare is always set up)' };
|
|
697
|
+
return setupViaProjects(root, { withElevenlabs: extra.includes('elevenlabs'), acceptTos: flags.has('accept-tos'), dryRun: flags.has('dry-run'), log });
|
|
698
|
+
}
|
|
665
699
|
if (cmd === 'handoff') return handoff(root, sub, { homie: flags.get('homie') });
|
|
666
700
|
if (cmd === 'port' && sub === 'import') return importPort(root, positional[2], flags.get('id'), { name: flags.get('name'), mode: flags.get('mode') });
|
|
667
701
|
if (cmd === 'port' && sub === 'check') {
|
|
@@ -775,10 +809,22 @@ async function main() {
|
|
|
775
809
|
if (sub === 'check') return shopCheck(root);
|
|
776
810
|
if (sub === 'connect') return shopConnect(root, { managed: flags.has('managed') ? true : null, live: flags.has('live'), log });
|
|
777
811
|
if (sub === 'disconnect') return shopDisconnect(root);
|
|
812
|
+
if (sub === 'catalog') {
|
|
813
|
+
const from = flags.get('have');
|
|
814
|
+
let have = null;
|
|
815
|
+
if (from === '-' || from === true) have = readFileSync(0, 'utf8');
|
|
816
|
+
else if (typeof from === 'string') {
|
|
817
|
+
if (!existsSync(from)) return { ok: false, command: 'shop catalog', why: `no such file: ${from}` };
|
|
818
|
+
have = readFileSync(from, 'utf8');
|
|
819
|
+
}
|
|
820
|
+
const mode = flags.get('mode');
|
|
821
|
+
if (mode !== undefined && mode !== 'test' && mode !== 'live') return { ok: false, command: 'shop catalog', why: '--mode is test or live' };
|
|
822
|
+
return catalogPlan(root, { have, mode: mode ?? null });
|
|
823
|
+
}
|
|
778
824
|
if (sub === 'orders') return shopOrders(root, { url });
|
|
779
825
|
if (sub === 'refund') return shopRefund(root, positional[2], { url, reason: flags.get('reason'), note: flags.get('note') });
|
|
780
826
|
if (sub === 'statements') return shopStatements(root, { url, period: flags.get('period'), send: flags.has('send') });
|
|
781
|
-
return { ok: false, command: 'shop', why: `unknown: shop ${sub} (init, check, connect, disconnect, orders, refund, statements)` };
|
|
827
|
+
return { ok: false, command: 'shop', why: `unknown: shop ${sub} (init, check, connect, disconnect, catalog, orders, refund, statements)` };
|
|
782
828
|
}
|
|
783
829
|
if (cmd === 'agents') {
|
|
784
830
|
const url = flags.get('url');
|
|
@@ -787,8 +833,9 @@ async function main() {
|
|
|
787
833
|
if (sub === 'revoke') return agentsRevoke(root, positional[2], { url });
|
|
788
834
|
if (sub === 'brain' && positional[2] === 'key') return agentsBrainKey(root, { remove: flags.has('remove'), log });
|
|
789
835
|
if (sub === 'brain') return agentsBrain(root, positional[2], positional[3], positional[4], { url, budget: flags.get('budget') });
|
|
790
|
-
if (sub === 'sit') return agentsSit(root, positional[2], { url, server: flags.get('server'), pass: flags.get('pass'), label: flags.get('label') });
|
|
791
|
-
return {
|
|
836
|
+
if (sub === 'sit') return agentsSit(root, positional[2], { url, server: flags.get('server'), pass: flags.get('pass'), label: flags.get('label'), brain: flags.get('brain') });
|
|
837
|
+
if (sub === 'try') return agentsTry(root, positional[2], { url, view: flags.get('view'), ask: flags.get('ask'), from: flags.get('from'), model: flags.get('model'), mode: flags.get('mode'), quiet: flags.has('quiet'), avoid: flags.get('avoid'), players: flags.get('players'), readFile: (f) => readFileSync(resolve(f), 'utf8') });
|
|
838
|
+
return { ok: false, command: 'agents', why: `unknown: agents ${sub ?? ''} (pass, passes, revoke, brain, brain key, sit, try)` };
|
|
792
839
|
}
|
|
793
840
|
if (cmd === 'media' && sub === 'put') return mediaPut(root, positional[2], flags.get('as'));
|
|
794
841
|
if (cmd === 'media' && sub === 'move') return mediaMove(root, { paths: positional.slice(2), dryRun: flags.has('dry-run') || flags.has('plan'), verify: flags.has('verify'), log });
|
|
@@ -983,7 +1030,7 @@ async function dev(root) {
|
|
|
983
1030
|
const bin = wranglerBin(root);
|
|
984
1031
|
if (!bin) return { ok: false, command: 'dev', why: 'run npm install in the studio first' };
|
|
985
1032
|
const studio = readStudio(root);
|
|
986
|
-
const env = { ...process.env, WRANGLER_SEND_METRICS: 'false', CI: '1' };
|
|
1033
|
+
const env = { ...process.env, ...projectsCloudflareEnv(root), WRANGLER_SEND_METRICS: 'false', CI: '1' };
|
|
987
1034
|
for (const added of ensureMigrations(root)) log(`added ${added} (${migrationWord(added)})`);
|
|
988
1035
|
await new Promise((done) => {
|
|
989
1036
|
const m = spawn(bin, ['d1', 'migrations', 'apply', 'DB', '--local'], { cwd: workerDir(root), env, stdio: ['ignore', 'ignore', 'inherit'] });
|
|
@@ -992,9 +1039,17 @@ async function dev(root) {
|
|
|
992
1039
|
log(`Local site: http://127.0.0.1:${port}/ (each game: http://127.0.0.1:${port}/<id>/play — open it in two browsers)`);
|
|
993
1040
|
const ai = devConfig(root, flags.has('remote-ai'));
|
|
994
1041
|
if (ai.note) log(ai.note);
|
|
1042
|
+
// The person's own Clef (0.24.4, lib/local-ai.mjs): with Ollama and clef-flash on this computer, the local Worker's
|
|
1043
|
+
// guides, chat review and game decisions think here, free. Never a download: dev only says what one would cost.
|
|
1044
|
+
let localVars = [];
|
|
1045
|
+
if (!flags.has('remote-ai') && !flags.has('no-local-ai')) {
|
|
1046
|
+
const found = await detectLocalAi();
|
|
1047
|
+
localVars = localAiVars(found);
|
|
1048
|
+
log(found.ok ? `AI guides, chat review and game decisions think with ${found.model} on this computer (Ollama ${found.version}): free, nothing sent to Cloudflare.` : found.say);
|
|
1049
|
+
}
|
|
995
1050
|
// --remote-ai: Wrangler's --local turns every remote binding off ("not supported"), so a dev with the real Workers AI
|
|
996
1051
|
// (the guides' brains, room chat's review) runs without it; everything else stays local all the same.
|
|
997
|
-
const child = spawn(bin, ['dev', ...(flags.has('remote-ai') ? [] : ['--local']), '--ip', '127.0.0.1', '--port', port, ...ai.args], { cwd: workerDir(root), env, stdio: 'inherit' });
|
|
1052
|
+
const child = spawn(bin, ['dev', ...(flags.has('remote-ai') ? [] : ['--local']), '--ip', '127.0.0.1', '--port', port, ...ai.args, ...localVars], { cwd: workerDir(root), env, stdio: 'inherit' });
|
|
998
1053
|
mkdirSync(dirname(devFile(root)), { recursive: true });
|
|
999
1054
|
writeFileSync(devFile(root), `${JSON.stringify({ pid: process.pid, child: child.pid, port: Number(port), at: new Date().toISOString() })}\n`);
|
|
1000
1055
|
log(`Stop it with: npx --no-install homie-studio dev --stop (this studio's dev server only)`);
|
|
@@ -1025,13 +1080,20 @@ function devConfig(root, remoteAi) {
|
|
|
1025
1080
|
const copy = join(base, '.wrangler', 'homie-dev.wrangler.json');
|
|
1026
1081
|
mkdirSync(dirname(copy), { recursive: true });
|
|
1027
1082
|
writeFileSync(copy, `${JSON.stringify(json, null, 2)}\n`);
|
|
1028
|
-
return { args: ['--config', copy, '--persist-to', join(base, '.wrangler', 'state')], note: remoteAi ? 'AI guides think with Workers AI (remote, billed to the signed-in account).' : '
|
|
1083
|
+
return { args: ['--config', copy, '--persist-to', join(base, '.wrangler', 'state')], note: remoteAi ? 'AI guides think with Workers AI (remote, billed to the signed-in account).' : 'No Workers AI under dev (--remote-ai for the real one, billed): AI guides think with Clef on this computer when Ollama has it, else scripted.' };
|
|
1029
1084
|
}
|
|
1030
1085
|
|
|
1031
1086
|
/** `agents sit`: an AI guide's seat from this terminal, for a demo or a test of a vocabulary. Lines on stdin act. */
|
|
1032
|
-
async function agentsSit(root, game, { url, server, pass, label }) {
|
|
1087
|
+
async function agentsSit(root, game, { url, server, pass, label, brain }) {
|
|
1033
1088
|
const site = url ?? siteUrl(root);
|
|
1034
|
-
|
|
1089
|
+
// --brain local: Clef on this computer decides (lib/local-ai.mjs); never a download, only what one would cost.
|
|
1090
|
+
let local = null;
|
|
1091
|
+
if (brain === 'local') {
|
|
1092
|
+
local = await detectLocalAi();
|
|
1093
|
+
if (!local.ok) return { ok: false, command: 'agents sit', why: local.say };
|
|
1094
|
+
log(`This seat thinks with ${local.model} on this computer (Ollama ${local.version}) every few seconds. Type stand to leave.`);
|
|
1095
|
+
} else if (brain !== undefined) return { ok: false, command: 'agents sit', why: '--brain is local (Clef on this computer), or leave it out to choose yourself' };
|
|
1096
|
+
const seat = new AgentSeat({ site, game, server: server ?? null, pass: pass ?? null, label: label ?? (local ? 'Clef' : 'Claude'), root, brain: local ? 'local' : null, local });
|
|
1035
1097
|
const r = await seat.sit();
|
|
1036
1098
|
if (!r.ok) return { ok: false, command: 'agents sit', why: r.why };
|
|
1037
1099
|
log(`Seated as ${r.name} in ${r.room} (seat ${r.seat}). Type: look | do <goal> {"arg":…} | say <line> {"arg":…} with a goal | stand`);
|
|
@@ -1065,7 +1127,7 @@ function mediaPut(root, file, as) {
|
|
|
1065
1127
|
const key = as ?? (/^(music|videos)\//.test(rel) && !rel.includes('..') ? rel : `${folder}/${basename(file)}`);
|
|
1066
1128
|
const bin = wranglerBin(root);
|
|
1067
1129
|
return new Promise((done) => {
|
|
1068
|
-
const p = spawn(bin, ['r2', 'object', 'put', `${r2}/${key}`, '--file', resolve(file), '--content-type', typeOf(file), '--remote'], { cwd: workerDir(root), env: { ...process.env, CI: '1' }, stdio: ['ignore', 'ignore', 'inherit'] });
|
|
1130
|
+
const p = spawn(bin, ['r2', 'object', 'put', `${r2}/${key}`, '--file', resolve(file), '--content-type', typeOf(file), '--remote'], { cwd: workerDir(root), env: { ...process.env, ...projectsCloudflareEnv(root), CI: '1' }, stdio: ['ignore', 'ignore', 'inherit'] });
|
|
1069
1131
|
p.on('close', (code) => {
|
|
1070
1132
|
if (code !== 0) return done({ ok: false, command: 'media put', why: 'wrangler r2 object put failed' });
|
|
1071
1133
|
// The manifest is committed: it names the file by its path on the site, never the workers.dev address.
|
package/chat/CHAT.md
CHANGED
|
@@ -90,6 +90,8 @@ accuracy numbers for it, and the message it reads is a player's, so a player can
|
|
|
90
90
|
(less of a risk than with a model that writes, since it only scores fixed answers). That is why
|
|
91
91
|
**the word list always runs first and decides alone** whenever the review cannot (no binding, the
|
|
92
92
|
day's budget used, an error, a slow answer). `HOMIE_CHAT_MODEL` switches to Clef or Llama Guard.
|
|
93
|
+
Under `homie-studio dev` with no Workers AI binding, the same question goes to Clef on the person's
|
|
94
|
+
own computer when Ollama has `clef-flash` (0.24.4): free, nothing sent to Cloudflare.
|
|
93
95
|
Firewall for AI (now "AI Security for Apps") was ruled out: it reads HTTP requests at the zone, not
|
|
94
96
|
messages inside a Durable Object, and its detections are an Enterprise add-on.
|
|
95
97
|
|
package/lib/agent-seat.mjs
CHANGED
|
@@ -8,8 +8,13 @@
|
|
|
8
8
|
*
|
|
9
9
|
* The pass: one the owner gave (`hap_…`), or, from a studio folder, a one-day guide pass minted with the owner's office
|
|
10
10
|
* key and revoked when the AI stands. The secret stays in this process; it is never printed or returned.
|
|
11
|
+
*
|
|
12
|
+
* BRAIN 'local' (0.24.4): instead of an AI's turns, Cloudflare's Clef decision model on this computer (Ollama with
|
|
13
|
+
* clef-flash, lib/local-ai.mjs) decides every few seconds, the way a house guide does (worker/agents.mjs think: the same
|
|
14
|
+
* questions, the same fixed rules), for free and with nothing sent to Cloudflare. agent_do still overrides it.
|
|
11
15
|
*/
|
|
12
|
-
import { parseDecision, renderLine, vocabularyOf } from '../worker/brain.mjs';
|
|
16
|
+
import { clefDecide, parseDecision, renderLine, vocabularyOf } from '../worker/brain.mjs';
|
|
17
|
+
import { think } from '../worker/agents.mjs';
|
|
13
18
|
import { withKey } from './office.mjs';
|
|
14
19
|
|
|
15
20
|
const GAME = /^[a-z0-9][a-z0-9-]{0,39}$/;
|
|
@@ -26,7 +31,7 @@ export class AgentSeat {
|
|
|
26
31
|
* @param {string} [o.pass] an agent pass (hap_…); else one is minted from `root` with the office key
|
|
27
32
|
* @param {string} [o.root] the studio folder (for a pass of its own)
|
|
28
33
|
*/
|
|
29
|
-
constructor({ site, game, server = null, pass = null, label = 'Claude', root = null, WebSocketImpl = globalThis.WebSocket, fetchImpl = globalThis.fetch }) {
|
|
34
|
+
constructor({ site, game, server = null, pass = null, label = 'Claude', root = null, WebSocketImpl = globalThis.WebSocket, fetchImpl = globalThis.fetch, brain = null, local = null }) {
|
|
30
35
|
this.site = String(site ?? '').replace(/\/+$/, '');
|
|
31
36
|
this.game = game;
|
|
32
37
|
this.server = server;
|
|
@@ -52,6 +57,10 @@ export class AgentSeat {
|
|
|
52
57
|
this.errors = [];
|
|
53
58
|
this.vocab = null;
|
|
54
59
|
this.ping = null;
|
|
60
|
+
/** brain 'local': Clef on this computer ({ base, model }) decides; its last decisions, and who said no thanks. */
|
|
61
|
+
this.brain = brain === 'local' && local ? 'local' : null;
|
|
62
|
+
this.local = local;
|
|
63
|
+
this.auto = { timer: null, busy: false, at: 0, decisions: [], avoid: new Map(), said: [], goalState: null };
|
|
55
64
|
}
|
|
56
65
|
|
|
57
66
|
async sit() {
|
|
@@ -101,9 +110,56 @@ export class AgentSeat {
|
|
|
101
110
|
this.ping = setInterval(() => { try { ws.send(JSON.stringify({ t: 'ping', c: Date.now() })); } catch { /* closed */ } }, 2000);
|
|
102
111
|
// The host shows a guide the game every 2 s: wait for the first look (up to 6 s).
|
|
103
112
|
for (let i = 0; i < 30 && !this.view && !this.closed; i += 1) await wait(200);
|
|
113
|
+
if (this.brain === 'local') this.autopilot();
|
|
104
114
|
return { ok: true, seat: this.seat, name: this.name, room: this.room, server: this.server, ...this.look() };
|
|
105
115
|
}
|
|
106
116
|
|
|
117
|
+
/**
|
|
118
|
+
* BRAIN 'local': a decision on a new ask (at once), when the goal in force ends, else every 12 s while the room has a
|
|
119
|
+
* view; never more than one at a time, and do() keeps the 3 s pace and the 8 s quiet. No thanks holds that player off
|
|
120
|
+
* for 10 minutes.
|
|
121
|
+
*/
|
|
122
|
+
autopilot() {
|
|
123
|
+
const tick = async () => {
|
|
124
|
+
if (this.closed) { this.stopAuto(); return; }
|
|
125
|
+
const now = Date.now();
|
|
126
|
+
const a = this.auto;
|
|
127
|
+
const asks = this.asks.filter((x) => now - x.at < 30_000);
|
|
128
|
+
const state = this.view?.goal && typeof this.view.goal === 'object' ? this.view.goal.state ?? null : null;
|
|
129
|
+
const ended = (state === 'done' || state === 'failed') && a.goalState !== state;
|
|
130
|
+
a.goalState = state;
|
|
131
|
+
if (a.busy || !this.view || !(asks.length || ended || now - a.at >= 12_000)) return;
|
|
132
|
+
a.busy = true;
|
|
133
|
+
a.at = now;
|
|
134
|
+
for (const [s, until] of a.avoid) if (until <= now) a.avoid.delete(s);
|
|
135
|
+
for (const x of asks) if (this.vocab?.asks?.[x.k]?.leave) a.avoid.set(x.from, now + 10 * 60_000);
|
|
136
|
+
try {
|
|
137
|
+
const party = Array.isArray(this.view.party) ? this.view.party.map((p) => p?.seat).filter(Number.isInteger) : [];
|
|
138
|
+
const ctx = {
|
|
139
|
+
view: this.view, me: { seat: this.seat }, goal: this.view.goal ?? null, asks, party: this.party.filter((p) => now - p.at < 60_000),
|
|
140
|
+
speech: this.policy?.speech ?? 'lines', kids: this.policy?.kids === true, quiet: !this.talking() || now - this.sayAt < 8000,
|
|
141
|
+
players: party, avoid: [...a.avoid.keys()], said: a.said.map((x) => ({ ...x, ago: now - x.at })),
|
|
142
|
+
};
|
|
143
|
+
const local = this.local;
|
|
144
|
+
const t = await think(this.vocab, { mode: 'local', brain: { engine: 'local', run: (args) => clefDecide({}, { vocab: args.vocab, ctx: args.ctx, model: local.model, ms: 8000, local, fetch: this.fetch }) }, ctx });
|
|
145
|
+
const d = t.decision;
|
|
146
|
+
let sent = null;
|
|
147
|
+
if (d?.goal && !this.closed) {
|
|
148
|
+
const say = d.say && this.talking() && Date.now() - this.sayAt >= 8000 ? d.say : null;
|
|
149
|
+
sent = await this.do({ goal: d.goal, args: d.args, say, sayArgs: say ? d.sayArgs : {} });
|
|
150
|
+
if (sent.ok && say) a.said = [...a.said, { line: say, args: d.sayArgs, at: Date.now() }].slice(-4);
|
|
151
|
+
}
|
|
152
|
+
a.decisions = [...a.decisions, { at: new Date(now).toISOString(), provider: t.provider, goal: d?.goal ?? null, args: d?.args ?? null, say: d?.say ?? null, ms: t.ms, ...(t.why ? { why: t.why } : {}), ...(sent && !sent.ok ? { refused: sent.why } : {}) }].slice(-20);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
a.decisions = [...a.decisions, { at: new Date(now).toISOString(), provider: 'none', why: String(error?.message ?? error).slice(0, 120) }].slice(-20);
|
|
155
|
+
} finally { a.busy = false; }
|
|
156
|
+
};
|
|
157
|
+
this.auto.timer = setInterval(() => { void tick(); }, 1000);
|
|
158
|
+
void tick();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
stopAuto() { if (this.auto.timer) clearInterval(this.auto.timer); this.auto.timer = null; }
|
|
162
|
+
|
|
107
163
|
onFrame(m) {
|
|
108
164
|
switch (m.t) {
|
|
109
165
|
case 'welcome': this.seat = m.seat; this.name = m.name; this.policy = m.policy ?? null; return;
|
|
@@ -135,6 +191,7 @@ export class AgentSeat {
|
|
|
135
191
|
asks: this.asks.filter((a) => now - a.at < 60_000).map((a) => ({ ask: a.k, args: a.args, seat: a.from, text: v ? renderLine(v, a.k, a.args, { kind: 'asks', nameOf: (s) => `seat ${s}` }) : null, secondsAgo: Math.round((now - a.at) / 1000) })),
|
|
136
192
|
party: this.party.filter((p) => now - p.at < 60_000).map((p) => ({ seat: p.seat, line: p.line, args: p.args })),
|
|
137
193
|
lastDo: this.lastDo,
|
|
194
|
+
...(this.brain === 'local' ? { brain: { local: this.local.model, decisions: this.auto.decisions.slice(-5) } } : {}),
|
|
138
195
|
choices: v ? { goals: describe(v.goals), lines: describe(v.lines) } : null,
|
|
139
196
|
errors: this.errors.slice(-3),
|
|
140
197
|
};
|
|
@@ -162,6 +219,7 @@ export class AgentSeat {
|
|
|
162
219
|
talking() { return Boolean(this.policy && ['workers-ai', 'owner-key'].includes(this.policy.brain) && this.policy.speech !== 'off'); }
|
|
163
220
|
|
|
164
221
|
close() {
|
|
222
|
+
this.stopAuto();
|
|
165
223
|
if (this.ping) clearInterval(this.ping);
|
|
166
224
|
this.ping = null;
|
|
167
225
|
if (this.ws) { try { this.ws.send(JSON.stringify({ t: 'bye' })); } catch { /* closed */ } try { this.ws.close(1000, 'bye'); } catch { /* closed */ } }
|
package/lib/brain-probe.mjs
CHANGED
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
* doctor asks once.
|
|
7
7
|
*
|
|
8
8
|
* The check is ONE tiny call to the model through Cloudflare's REST API (`POST /accounts/:id/ai/run/:model`, a
|
|
9
|
-
* one-word prompt, max_tokens 1:
|
|
9
|
+
* one-word prompt, max_tokens 1, or for a Clef decision model one yes/no question: a few of the 10,000 free neurons a day), with the studio's own Wrangler login
|
|
10
10
|
* (`wrangler auth token`, held in this process only) or CLOUDFLARE_API_TOKEN. It never prints the token, the account's
|
|
11
11
|
* id or its name. A call that cannot be made (not signed in, several accounts and none named) says so and spends
|
|
12
12
|
* nothing.
|
|
13
13
|
*/
|
|
14
14
|
import { existsSync, readFileSync } from 'node:fs';
|
|
15
15
|
import { join } from 'node:path';
|
|
16
|
-
import { DEFAULT_MODEL } from '../worker/brain.mjs';
|
|
16
|
+
import { CLEF, DEFAULT_MODEL } from '../worker/brain.mjs';
|
|
17
17
|
import { configPath, workerDir } from './studio.mjs';
|
|
18
18
|
|
|
19
19
|
const MODELS = 'https://developers.cloudflare.com/workers-ai/models/';
|
|
@@ -96,7 +96,8 @@ export async function probeModel({ model, accountId, headers, fetchFn = globalTh
|
|
|
96
96
|
const res = await fetchFn(`${String(base).replace(/\/+$/, '')}/accounts/${encodeURIComponent(accountId)}/ai/run/${model}`, {
|
|
97
97
|
method: 'POST',
|
|
98
98
|
headers: { ...headers, 'content-type': 'application/json', accept: 'application/json', 'user-agent': 'homie-studio-doctor' },
|
|
99
|
-
|
|
99
|
+
// A decision model (Clef) takes a state and a question, not a chat message: one yes/no, a few hundred tokens.
|
|
100
|
+
body: JSON.stringify(CLEF.models[model] ? { model: CLEF.models[model], state: 'OK', questions: { ok: { type: 'noul', instructions: 'Does the state say OK?' } } } : { messages: [{ role: 'user', content: 'Say OK.' }], max_tokens: 1 }),
|
|
100
101
|
signal: AbortSignal.timeout(ms),
|
|
101
102
|
});
|
|
102
103
|
return { status: res.status, body: await res.json().catch(() => null) };
|
|
@@ -122,7 +123,7 @@ export async function workersAiRow({ root, studio, env = process.env, exec, fetc
|
|
|
122
123
|
const where = set ? 'HOMIE_BRAIN_MODEL in wrangler.jsonc' : 'the toolkit\'s default';
|
|
123
124
|
const pick = (lead) => ({
|
|
124
125
|
who: 'ai', open: MODELS,
|
|
125
|
-
say: `${lead}Pick a model the account can run: set HOMIE_BRAIN_MODEL in wrangler.jsonc "vars" to a
|
|
126
|
+
say: `${lead}Pick a model the account can run: set HOMIE_BRAIN_MODEL in wrangler.jsonc "vars" to a model from ${MODELS} that is not marked Workers Paid (Clef, @cf/cloudflare/clef-flash, or a text-generation model)${set && model !== DEFAULT_MODEL ? ` (or remove it for the default, ${DEFAULT_MODEL})` : ''}, run \`${cli} doctor\` again, then \`npm run deploy\`. Until then the guides answer from the game's script.`,
|
|
126
127
|
});
|
|
127
128
|
const money = ' Moving the account to Workers Paid ($5 a month) also works, but that is the person\'s money: ask them, never decide it.';
|
|
128
129
|
if (!MODEL_ID.test(model)) return { ...row, state: 'act', detail: `${where} is "${model.slice(0, 80)}", which is not a Workers AI model id (@cf/<maker>/<model>)`, fix: pick('') };
|
|
@@ -145,7 +146,7 @@ export async function workersAiRow({ root, studio, env = process.env, exec, fetc
|
|
|
145
146
|
case 'missing': return { ...row, state: 'act', detail: `Cloudflare has no model ${model} (renamed or retired)${said}`, fix: pick('') };
|
|
146
147
|
case 'not-allowed': return { ...row, state: 'act', detail: `this account may not use ${model} (a private model)${said}`, fix: pick('') };
|
|
147
148
|
case 'agreement': return { ...row, state: 'act', detail: `${model} asks for its maker's licence to be agreed once before it is used${said}`, fix: pick('Agreeing to a licence is the person\'s to do, never yours; or simply: ') };
|
|
148
|
-
case 'input': return { ...row, state: 'act', detail: `${model} did not take a chat message: the guides need a text-generation model${said}`, fix: pick('') };
|
|
149
|
+
case 'input': return { ...row, state: 'act', detail: `${model} did not take a chat message: the guides need Clef or a text-generation model${said}`, fix: pick('') };
|
|
149
150
|
case 'allowance': return { ...row, state: 'later', detail: `today's free Workers AI allowance (10,000 neurons for the account) is used up, so ${model} could not be checked; the guides answer from the script until 00:00 UTC`, fix: { who: 'person', say: `Nothing to do now: run doctor again after 00:00 UTC. The studio's daily budget (\`${cli} agents brain <game> <server> workers-ai --budget <neurons>\`) keeps the guides under the allowance.` } };
|
|
150
151
|
case 'busy': return { ...row, state: 'unknown', detail: `Workers AI was busy just now (status ${answer.status})${said}; run doctor again in a minute`, fix: null };
|
|
151
152
|
case 'auth': return { ...row, state: 'unknown', detail: `Cloudflare did not let this sign-in run Workers AI (status ${answer.status})${said}`, fix: { who: 'ai', run: 'npx wrangler login', say: 'Sign in to Cloudflare again (the login includes Workers AI), then run doctor again.' } };
|
package/lib/build.mjs
CHANGED
|
@@ -321,6 +321,8 @@ export async function build(root, { only = null, log = () => {}, deploy = proces
|
|
|
321
321
|
// Room chat (NETPLAY.md section 19): game.json "chat", checked here (its quick lines pass the chat floor too).
|
|
322
322
|
const chatBad = chatProblems(g.chat);
|
|
323
323
|
if (chatBad.length) throw new Error(`games/${g.id}/game.json: ${chatBad.join('; ')}`);
|
|
324
|
+
// A game's own decisions (NETPLAY.md section 20): game.json "decide" is true or false (the Worker answers net.decide only then).
|
|
325
|
+
if (g.decide !== undefined && typeof g.decide !== 'boolean') throw new Error(`games/${g.id}/game.json: "decide" is true (the host may ask the studio's decision model, within the AI brains' day) or false`);
|
|
324
326
|
// The game's own source, for other studios to remix (game.json "share": { "source": false } keeps it private).
|
|
325
327
|
if (g.share?.source !== false) writeFileSync(join(out, 'source.json'), `${JSON.stringify(sourceOf(g.dir, g.id, { studio: studio.name ?? null, game: g.name ?? g.id, license: g.license }))}\n`);
|
|
326
328
|
// Its assets' licences, and the address and SHA-256 of each one a remix may carry (`game remix` fetches them).
|
|
@@ -368,6 +370,7 @@ export async function build(root, { only = null, log = () => {}, deploy = proces
|
|
|
368
370
|
// game.json `"chat"` (0.23.0, NETPLAY.md section 19): the room chat's defaults for this game (the owner's office
|
|
369
371
|
// overrides them), or false for none.
|
|
370
372
|
...(g.chat === false ? { chat: false } : g.chat && typeof g.chat === 'object' ? { chat: g.chat } : {}),
|
|
373
|
+
...(g.decide === true ? { decide: true } : {}),
|
|
371
374
|
// Servers (0.16.0): the seeds game.json ships, the netplay revision its build speaks (the office warns when it
|
|
372
375
|
// predates servers), and game.json "agents": { "vote": "game" | false } (the game draws its own vote card, or none).
|
|
373
376
|
...(seeds.length ? { servers: seeds } : {}),
|