@homie-rocks/studio 0.24.3 → 0.24.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -1
- package/README.md +21 -5
- package/agents/GUIDES.md +13 -6
- package/bin/homie-studio.mjs +34 -10
- package/chat/CHAT.md +38 -5
- package/lib/agent-seat.mjs +60 -2
- package/lib/brain-probe.mjs +6 -5
- package/lib/build.mjs +6 -0
- package/lib/cloudflare.mjs +2 -0
- package/lib/doctor.mjs +14 -0
- package/lib/local-ai.mjs +66 -0
- package/lib/mcp-tools.mjs +10 -2
- package/lib/scaffold.mjs +17 -4
- package/lib/servers.mjs +31 -2
- package/lib/template-history.json +78 -0
- package/netplay/NETPLAY.md +95 -17
- package/netplay/netplay.ts +82 -0
- package/package.json +1 -1
- package/site/SITE.md +4 -0
- 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-page.mjs +173 -28
- package/worker/chat.mjs +17 -7
- package/worker/index.mjs +57 -2
- package/worker/office.mjs +63 -3
- package/worker/room.mjs +48 -3
- package/worker/version.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -17,9 +17,59 @@ 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.5] - 2026-10-03
|
|
21
|
+
|
|
22
|
+
**Plugin 0.25.5** · [#39](https://github.com/homie-rocks/homie/pull/39)
|
|
23
|
+
|
|
24
|
+
Room chat keeps off your game's HUD: on a phone the Chat pill is a round icon, as the room button is, and your game can say where the strip of new lines sits, or keep new lines in the Chat sheet, so a game with a busy HUD keeps typed chat.
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- game.json `"screen": { "chat": … }` says where room chat's strip of new lines sits on each screen: `at` (a corner, `top-center` or `bottom-center`), `x` / `y` to move it in from its side and its edge, and `"lines": "sheet-only"` to keep new lines in the Chat sheet while the pill counts them. `desk`, `phone`, `sideways` and `tv` (the big screen's corner of lines) each take their own, changing only what they name. A game whose HUD or controls sit where the strip shows no longer has to keep chat to emoji to stay clear. `chat/CHAT.md` has every field; the template's AGENTS.md and the game skill say when to use it.
|
|
29
|
+
- `homie-studio build` warns about a wrong field in `screen.chat` (the play page uses the default there), and says so when a place or `"sheet-only"` is written in game.json `"chat"` (the room's rules) instead.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- The Chat pill follows the room button: on a phone (either way up), and beside a room button your game keeps an icon (`screen.share` `"label": false`), it is a round icon with the count of new lines on its corner. On a computer it still says "Chat", unless the word would take the room button's band off the screen or into the middle third of its width, where games keep a clock or a title; then it stays the icon until the window changes size.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- On a phone, the Chat pill's word no longer covers the HUD beside the room button: the first digit of a clock, the last letter of a title.
|
|
38
|
+
|
|
39
|
+
### Upgrade notes
|
|
40
|
+
|
|
41
|
+
- Nothing to do: a game without `screen.chat` shows the strip where it did. If you kept a game's chat to emoji only so the strip stayed off its HUD, tell Claude: "Put room chat where my game has room, and let players type again." It places the strip with `screen.chat` and resets the game's chat rules (`homie-studio chat rules <game> --reset`).
|
|
42
|
+
|
|
43
|
+
## [0.24.4] - 2026-10-03
|
|
44
|
+
|
|
45
|
+
**Plugin 0.25.4** · [#37](https://github.com/homie-rocks/homie/pull/37) · [release-2026-10-03-studio-0.24.4](https://github.com/homie-rocks/homie/releases/tag/release-2026-10-03-studio-0.24.4)
|
|
46
|
+
|
|
47
|
+
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.
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
|
|
51
|
+
- **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.
|
|
52
|
+
- `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.
|
|
53
|
+
- **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.
|
|
54
|
+
- **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.
|
|
55
|
+
- 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.
|
|
56
|
+
- 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.
|
|
57
|
+
|
|
58
|
+
### Changed
|
|
59
|
+
|
|
60
|
+
- 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.
|
|
61
|
+
- 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.
|
|
62
|
+
- 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.
|
|
63
|
+
- The doctor's Workers AI check asks Clef one yes/no question (it does not take a chat message).
|
|
64
|
+
|
|
65
|
+
### Upgrade notes
|
|
66
|
+
|
|
67
|
+
- `npm run deploy` after upgrading: guides on Workers AI switch to Clef at their next decision. Nothing else to do.
|
|
68
|
+
- 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`.
|
|
69
|
+
|
|
20
70
|
## [0.24.3] - 2026-10-03
|
|
21
71
|
|
|
22
|
-
**Plugin 0.25.3** · [#36](https://github.com/homie-rocks/homie/pull/36)
|
|
72
|
+
**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)
|
|
23
73
|
|
|
24
74
|
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.
|
|
25
75
|
|
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
|
|
|
@@ -303,8 +316,9 @@ npx homie-studio chat remove night-rush pub-3 <line id> # take a
|
|
|
303
316
|
npx homie-studio chat words # the built-in word list the floor holds
|
|
304
317
|
```
|
|
305
318
|
|
|
306
|
-
- **The play page:** a Chat pill beside the room button (the corner `screen.share` keeps clear of the HUD
|
|
307
|
-
sheet (the last minutes, the reactions, the quick lines, typing, Report), a short
|
|
319
|
+
- **The play page:** a Chat pill beside the room button (the corner `screen.share` keeps clear of the HUD; a round
|
|
320
|
+
icon on a phone), its sheet (the last minutes, the reactions, the quick lines, typing, Report), a short strip of
|
|
321
|
+
new lines (game.json `screen.chat` puts it where the game has room, or keeps new lines in the sheet), and two
|
|
308
322
|
switches each player keeps: "Show my messages over my character" and "Show chat on this screen". The big screen
|
|
309
323
|
shows the lines in a corner; the watch page has a Chat button; homie.rocks shows a room's chat on its card.
|
|
310
324
|
- **Rules** per game (game.json `"chat"`) and per server, in the office or with `chat rules`: off, emoji, quick
|
|
@@ -314,7 +328,8 @@ npx homie-studio chat words # the bu
|
|
|
314
328
|
- **Moderation, in the studio's own Worker, before anyone sees a typed line:** a built-in word list and patterns
|
|
315
329
|
(slurs, sexual words, self-harm, personal questions and contact details, links, swears, your own words), then
|
|
316
330
|
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
|
|
331
|
+
neurons a message measured, 2,000 a day by default: inside the free allocation; under `dev`, Clef on this computer
|
|
332
|
+
when Ollama has it, free). Emoji and quick lines are never reviewed.
|
|
318
333
|
- **The owner:** every live room's last minutes in the office with Remove, Mute and Kick (from a line, with their
|
|
319
334
|
lines taken down), the same in the owner's own game, the reports players make, and Announce (a studio line).
|
|
320
335
|
- **Speech bubbles:** `net.on('say')` and the port kit's `createBubbles` / `paintBubbles`; Gem Rush, Ember Vale and
|
|
@@ -366,7 +381,8 @@ connector (the AI says whether its Homie tools are there; the directory is reach
|
|
|
366
381
|
with the studio's own Wrangler, and the account's email verified: a deploy that went through proves it, a
|
|
367
382
|
deploy refused for it is remembered), Chrome for the checks, ffmpeg, and the optional GitHub, ElevenLabs and
|
|
368
383
|
fal (and, in Claude Code, the status line); in a studio that sells (shop.json), a Stripe row: whether Stripe's own
|
|
369
|
-
agent plugin (its MCP server) is set up for this AI, with `stripe agent setup` as the fix (0.24.3)
|
|
384
|
+
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
|
|
385
|
+
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
|
|
370
386
|
unlocks, and gives the exact fix: a command the AI runs or a page the person taps. It only reads, answers in
|
|
371
387
|
seconds, and never prints a key, a token or an account's name or id. A studio the Claude app's setup card made
|
|
372
388
|
(`setup attach`) counts as connected to Cloudflare and GitHub there. `--json` gives the rows.
|
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,6 +123,11 @@
|
|
|
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)
|
|
@@ -245,7 +252,8 @@ import { statsKey, statsLink, statsRevoke, statsShare, statsShow } from '../lib/
|
|
|
245
252
|
import { playersOwner, playersShow } from '../lib/players.mjs';
|
|
246
253
|
import { officeAnnounce, officeClose, officeInvite, officeKey, officeKick, officeLaunch, officeLines, officeLink, officeMute, officeRevoke, officeShow } from '../lib/office.mjs';
|
|
247
254
|
import { chatBudget, chatLines, chatRemove, chatRulesSet, chatShow, chatWords } from '../lib/chat-cli.mjs';
|
|
248
|
-
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';
|
|
249
257
|
import { AgentSeat } from '../lib/agent-seat.mjs';
|
|
250
258
|
import { shopCheck, shopConnect, shopDisconnect, shopInit, shopLines, shopOrders, shopRefund, shopStatements, shopStatus } from '../lib/shop.mjs';
|
|
251
259
|
import { catalogLines, catalogPlan } from '../lib/shop-catalog.mjs';
|
|
@@ -263,7 +271,7 @@ import { formatHandoff, handoff } from '../lib/handoff.mjs';
|
|
|
263
271
|
|
|
264
272
|
const argv = process.argv.slice(2);
|
|
265
273
|
const flags = new Map();
|
|
266
|
-
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'];
|
|
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'];
|
|
267
275
|
const positional = [];
|
|
268
276
|
for (let i = 0; i < argv.length; i++) {
|
|
269
277
|
const a = argv[i];
|
|
@@ -825,8 +833,9 @@ async function main() {
|
|
|
825
833
|
if (sub === 'revoke') return agentsRevoke(root, positional[2], { url });
|
|
826
834
|
if (sub === 'brain' && positional[2] === 'key') return agentsBrainKey(root, { remove: flags.has('remove'), log });
|
|
827
835
|
if (sub === 'brain') return agentsBrain(root, positional[2], positional[3], positional[4], { url, budget: flags.get('budget') });
|
|
828
|
-
if (sub === 'sit') return agentsSit(root, positional[2], { url, server: flags.get('server'), pass: flags.get('pass'), label: flags.get('label') });
|
|
829
|
-
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)` };
|
|
830
839
|
}
|
|
831
840
|
if (cmd === 'media' && sub === 'put') return mediaPut(root, positional[2], flags.get('as'));
|
|
832
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 });
|
|
@@ -1030,9 +1039,17 @@ async function dev(root) {
|
|
|
1030
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)`);
|
|
1031
1040
|
const ai = devConfig(root, flags.has('remote-ai'));
|
|
1032
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
|
+
}
|
|
1033
1050
|
// --remote-ai: Wrangler's --local turns every remote binding off ("not supported"), so a dev with the real Workers AI
|
|
1034
1051
|
// (the guides' brains, room chat's review) runs without it; everything else stays local all the same.
|
|
1035
|
-
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' });
|
|
1036
1053
|
mkdirSync(dirname(devFile(root)), { recursive: true });
|
|
1037
1054
|
writeFileSync(devFile(root), `${JSON.stringify({ pid: process.pid, child: child.pid, port: Number(port), at: new Date().toISOString() })}\n`);
|
|
1038
1055
|
log(`Stop it with: npx --no-install homie-studio dev --stop (this studio's dev server only)`);
|
|
@@ -1063,13 +1080,20 @@ function devConfig(root, remoteAi) {
|
|
|
1063
1080
|
const copy = join(base, '.wrangler', 'homie-dev.wrangler.json');
|
|
1064
1081
|
mkdirSync(dirname(copy), { recursive: true });
|
|
1065
1082
|
writeFileSync(copy, `${JSON.stringify(json, null, 2)}\n`);
|
|
1066
|
-
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.' };
|
|
1067
1084
|
}
|
|
1068
1085
|
|
|
1069
1086
|
/** `agents sit`: an AI guide's seat from this terminal, for a demo or a test of a vocabulary. Lines on stdin act. */
|
|
1070
|
-
async function agentsSit(root, game, { url, server, pass, label }) {
|
|
1087
|
+
async function agentsSit(root, game, { url, server, pass, label, brain }) {
|
|
1071
1088
|
const site = url ?? siteUrl(root);
|
|
1072
|
-
|
|
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 });
|
|
1073
1097
|
const r = await seat.sit();
|
|
1074
1098
|
if (!r.ok) return { ok: false, command: 'agents sit', why: r.why };
|
|
1075
1099
|
log(`Seated as ${r.name} in ${r.room} (seat ${r.seat}). Type: look | do <goal> {"arg":…} | say <line> {"arg":…} with a goal | stand`);
|
package/chat/CHAT.md
CHANGED
|
@@ -14,9 +14,12 @@ studio's owner is responsible for, read [OWNERS.md](OWNERS.md).
|
|
|
14
14
|
|
|
15
15
|
- **The Chat pill** on the play page, beside the room button: the room's last minutes, the
|
|
16
16
|
reactions, the quick lines, typing where allowed, Report on a line, and two switches ("Show my
|
|
17
|
-
messages over my character", "Show chat on this screen").
|
|
17
|
+
messages over my character", "Show chat on this screen"). On a phone it is a round icon, as the
|
|
18
|
+
room button is, with the count of new lines on its corner; on a computer it says "Chat" unless
|
|
19
|
+
the word would reach the middle of the top edge, where games keep a clock or a title.
|
|
18
20
|
- **The float:** every reaction rises up every screen in the room.
|
|
19
|
-
- **The
|
|
21
|
+
- **The strip:** a new line shows for a moment where the play page's status chip sits (the bottom
|
|
22
|
+
left), or where the game says (below).
|
|
20
23
|
- **The big screen** (`/<game>/tv`) shows the room's lines in a corner; the **watch page** has a
|
|
21
24
|
Chat button; **homie.rocks** shows the room's chat on its card when the studio allows it.
|
|
22
25
|
|
|
@@ -49,9 +52,37 @@ studio's owner is responsible for, read [OWNERS.md](OWNERS.md).
|
|
|
49
52
|
| `block`, `allow` | the studio's own words to hold (`word*` inside words) or to let through | none |
|
|
50
53
|
|
|
51
54
|
`"chat": false` turns it off. A kids server, and every beginner server, keeps chat to emoji and
|
|
52
|
-
quick lines whatever the game says.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
+
quick lines whatever the game says.
|
|
56
|
+
|
|
57
|
+
**Where it sits on the screen, in game.json `"screen"`**, so a game with a busy HUD keeps typed
|
|
58
|
+
chat without anything covering its clock, its title or its controls. The Chat pill sits in the
|
|
59
|
+
room button's band, so `"screen": { "share": … }` places both (`"label": false` keeps both round
|
|
60
|
+
icons). `"screen": { "chat": … }` places the strip of new lines and the big screen's corner:
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
"screen": {
|
|
64
|
+
"share": { "phone": { "at": "top-left", "label": false } },
|
|
65
|
+
"chat": {
|
|
66
|
+
"desk": { "at": "top-left", "y": 130 },
|
|
67
|
+
"phone": { "lines": "sheet-only" },
|
|
68
|
+
"tv": { "at": "top-right" }
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
| Field | | Default |
|
|
74
|
+
|---|---|---|
|
|
75
|
+
| `at` | `top-left`, `top-center`, `top-right`, `bottom-left`, `bottom-center` or `bottom-right` | the bottom left (the bottom right when the room button is at the bottom left) |
|
|
76
|
+
| `x`, `y` | move it in from its side and its edge, in CSS pixels (0 to 600; in the middle of an edge, `x` moves it either way). Sharing the room button's place, it also sits past the button | 0 |
|
|
77
|
+
| `lines` | `strip` (new lines show for a moment), or `sheet-only` (they wait in the Chat sheet and the pill counts them) | `strip` |
|
|
78
|
+
| `desk`, `phone`, `sideways`, `tv` | a computer, a phone held upright, a phone turned sideways (else as `phone`), the big screen: each changes only what it names | |
|
|
79
|
+
|
|
80
|
+
A plain string (`"chat": "bottom-right"`) is a place for every device, and `false` is
|
|
81
|
+
`"lines": "sheet-only"` for every device. The big screen has no sheet: it takes a corner only
|
|
82
|
+
(the top level's when that is a corner, else the one its join card leaves free) and keeps its
|
|
83
|
+
lines unless its own `tv` entry says `"sheet-only"`, which leaves it the float. A wrong field is
|
|
84
|
+
the default on the page, and `homie-studio build` says which. Look at the play page on a phone
|
|
85
|
+
and a computer while a round is on and somebody says something, and at `/<id>/tv`.
|
|
55
86
|
|
|
56
87
|
**Speech bubbles over characters**, with the port kit (the starters draw them):
|
|
57
88
|
|
|
@@ -90,6 +121,8 @@ accuracy numbers for it, and the message it reads is a player's, so a player can
|
|
|
90
121
|
(less of a risk than with a model that writes, since it only scores fixed answers). That is why
|
|
91
122
|
**the word list always runs first and decides alone** whenever the review cannot (no binding, the
|
|
92
123
|
day's budget used, an error, a slow answer). `HOMIE_CHAT_MODEL` switches to Clef or Llama Guard.
|
|
124
|
+
Under `homie-studio dev` with no Workers AI binding, the same question goes to Clef on the person's
|
|
125
|
+
own computer when Ollama has `clef-flash` (0.24.4): free, nothing sent to Cloudflare.
|
|
93
126
|
Firewall for AI (now "AI Security for Apps") was ruled out: it reads HTTP requests at the zone, not
|
|
94
127
|
messages inside a Durable Object, and its detections are an Enterprise add-on.
|
|
95
128
|
|
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
|
@@ -47,6 +47,7 @@ import { STUDIO_VERSION } from './version.mjs';
|
|
|
47
47
|
import { licenseOf, remixRow } from '../worker/license.mjs';
|
|
48
48
|
import { SERVER_LIMITS, serverOf } from '../worker/servers.mjs';
|
|
49
49
|
import { chatProblems } from '../worker/chat.mjs';
|
|
50
|
+
import { screenChatProblems } from '../worker/chat-page.mjs';
|
|
50
51
|
import { vocabularyOf } from '../worker/brain.mjs';
|
|
51
52
|
import { shopForBuild } from './shop.mjs';
|
|
52
53
|
import { audienceOf } from '../worker/shop-rules.mjs';
|
|
@@ -321,6 +322,10 @@ export async function build(root, { only = null, log = () => {}, deploy = proces
|
|
|
321
322
|
// Room chat (NETPLAY.md section 19): game.json "chat", checked here (its quick lines pass the chat floor too).
|
|
322
323
|
const chatBad = chatProblems(g.chat);
|
|
323
324
|
if (chatBad.length) throw new Error(`games/${g.id}/game.json: ${chatBad.join('; ')}`);
|
|
325
|
+
// A game's own decisions (NETPLAY.md section 20): game.json "decide" is true or false (the Worker answers net.decide only then).
|
|
326
|
+
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`);
|
|
327
|
+
// Where chat sits on the screen (game.json "screen": { "chat" }, 0.24.5): a wrong field is the default on the page.
|
|
328
|
+
for (const p of screenChatProblems(g.screen?.chat)) log(`warning: games/${g.id}/game.json: ${p}; the play page uses the default there`);
|
|
324
329
|
// The game's own source, for other studios to remix (game.json "share": { "source": false } keeps it private).
|
|
325
330
|
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
331
|
// Its assets' licences, and the address and SHA-256 of each one a remix may carry (`game remix` fetches them).
|
|
@@ -368,6 +373,7 @@ export async function build(root, { only = null, log = () => {}, deploy = proces
|
|
|
368
373
|
// game.json `"chat"` (0.23.0, NETPLAY.md section 19): the room chat's defaults for this game (the owner's office
|
|
369
374
|
// overrides them), or false for none.
|
|
370
375
|
...(g.chat === false ? { chat: false } : g.chat && typeof g.chat === 'object' ? { chat: g.chat } : {}),
|
|
376
|
+
...(g.decide === true ? { decide: true } : {}),
|
|
371
377
|
// Servers (0.16.0): the seeds game.json ships, the netplay revision its build speaks (the office warns when it
|
|
372
378
|
// predates servers), and game.json "agents": { "vote": "game" | false } (the game draws its own vote card, or none).
|
|
373
379
|
...(seeds.length ? { servers: seeds } : {}),
|
package/lib/cloudflare.mjs
CHANGED
|
@@ -282,6 +282,8 @@ export function needsWorkersAi(root, w, db) {
|
|
|
282
282
|
// Room chat (0.23.0): a game whose players may type has its messages reviewed by the studio's own Workers AI
|
|
283
283
|
// (Cloudflare's Clef decision model, within a day's budget of the free allocation) unless its game.json says no.
|
|
284
284
|
if ((cat.games ?? []).some((g) => g.chat !== false && (g.chat?.mode ?? 'text') === 'text' && g.chat?.ai !== false)) return true;
|
|
285
|
+
// A game's own decisions (0.24.4): a game whose game.json says "decide": true asks the same decision model.
|
|
286
|
+
if ((cat.games ?? []).some((g) => g.decide === true)) return true;
|
|
285
287
|
} catch { /* not built */ }
|
|
286
288
|
const r = w(['d1', 'execute', db, '--remote', '--json', '--command', "SELECT COUNT(*) AS n FROM servers WHERE brain = 'workers-ai' AND state = 'open'"]);
|
|
287
289
|
if (r.code !== 0) return false;
|
package/lib/doctor.mjs
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* Stripe for selling only in a studio with shop.json (0.24.3): Stripe's own agent plugin (its MCP server
|
|
20
20
|
* and skills) for this AI, set up when the studio starts selling; the shop's key itself
|
|
21
21
|
* goes in through `homie-studio shop connect`, never here
|
|
22
|
+
* Clef locally optional Ollama with clef-flash: AI under dev for free (never downloaded without a yes)
|
|
22
23
|
* status line optional (Claude Code only) the build's progress under the prompt
|
|
23
24
|
*
|
|
24
25
|
* It is safe at any time, inside a studio or before one exists: it only reads (local files, `--version` of a few
|
|
@@ -31,6 +32,7 @@ import { spawn } from 'node:child_process';
|
|
|
31
32
|
import { existsSync, readFileSync } from 'node:fs';
|
|
32
33
|
import { join } from 'node:path';
|
|
33
34
|
import { workersAiRow } from './brain-probe.mjs';
|
|
35
|
+
import { detectLocalAi } from './local-ai.mjs';
|
|
34
36
|
import { findChrome } from './chrome.mjs';
|
|
35
37
|
import { whyFailed } from './net.mjs';
|
|
36
38
|
import { isOurs } from './statusline.mjs';
|
|
@@ -228,6 +230,18 @@ export async function setupStatus({
|
|
|
228
230
|
});
|
|
229
231
|
}
|
|
230
232
|
|
|
233
|
+
// Clef on this computer (0.24.4, lib/local-ai.mjs): Ollama with clef-flash. Optional, and never downloaded here: the
|
|
234
|
+
// person says yes to the size first. In a studio only (it serves `dev`).
|
|
235
|
+
if (root) {
|
|
236
|
+
const found = await detectLocalAi({ env, fetchFn });
|
|
237
|
+
rows.push({
|
|
238
|
+
id: 'local-ai', label: 'Clef on this computer', need: 'optional', state: found.ok ? 'ok' : 'optional',
|
|
239
|
+
detail: found.ok ? `${found.model} on Ollama ${found.version}` : found.why === 'no-model' ? `Ollama ${found.version} is here, without clef-flash` : found.why === 'old-ollama' ? `Ollama ${found.version} is too old for Clef (0.35.1 or later)` : 'no Ollama running here',
|
|
240
|
+
unlocks: 'AI guides, chat review and a game\'s own decisions under `dev` with no Cloudflare and no cost, and a guide seat that thinks on this computer (agents sit --brain local)',
|
|
241
|
+
fix: found.ok ? null : { who: 'person', say: found.say, ...(found.why === 'no-ollama' ? { open: 'https://ollama.com/download' } : {}) },
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
231
245
|
// GitHub.
|
|
232
246
|
rows.push({
|
|
233
247
|
id: 'github', label: 'GitHub', need: 'optional', state: gh.signedIn || gh.onGitHub ? 'ok' : 'optional',
|