# @kentcdodds/world-builder — agent notes Human intent lives in [README.md](./README.md). This is the agent runbook. ## Building in the world (for agents) Call these exports from Kody execute. Always pass your own assistant name as `agentName` ("Claude", "ChatGPT", "Grok"); you appear as "'s ". | Export | Import | Input | Notes | | --- | --- | --- | --- | | list-worlds | `kody:@kentcdodds/world-builder/list-worlds` | — | Worlds with block counts. | | describe-world | `kody:@kentcdodds/world-builder/describe-world` | `{ room, region? }` | Bounds, materials, 8×8 height map, recent builds; `region` returns exact blocks (≤2000). | | create-world | `kody:@kentcdodds/world-builder/create-world` | `{ name, agentName? }` | 5 per person per minute. | | start-build | `kody:@kentcdodds/world-builder/start-build` | `{ room, prompt, agentName }` | Returns `buildId`. Shows in the build feed. | | place-blocks | `kody:@kentcdodds/world-builder/place-blocks` | `{ buildId, blocks: [{x,y,z,material}], batchSize?, delayMs? }` | ≤600 blocks per call. Streams in batches (default 24 every 150 ms). | | remove-blocks | `kody:@kentcdodds/world-builder/remove-blocks` | `{ buildId, blocks: [{x,y,z}] }` | Same streaming. | | finish-build | `kody:@kentcdodds/world-builder/finish-build` | `{ buildId, status? }` | `done` (default) or `failed`. | | build-demo-castle | `kody:@kentcdodds/world-builder/build-demo-castle` | `{ room, agentName?, x?, z?, delayMs? }` | ~1000-block castle through the same path. | World rules (also returned by `describe-world().rules`): - y is up; ground is y = 0. Bounds x, z ∈ [-48, 47], y ∈ [0, 47]. - Materials: grass, dirt, stone, cobble, brick, wood, plank, leaves, sand, water, glass, snow, gold, lava, obsidian, banner. - Order blocks bottom-up so the build rises naturally. - Limits: 6000 block writes per agent per minute, 60000 blocks per world. Over the limit you get a "Slow down" error; wait and continue. - `room` is the room link (`.../r/`) or its code. `worldId` still works. Without either, calls use the main world (room code `main`). - list-worlds returns the main world plus rooms the caller created. Rooms are private to their link and aren't listed for anyone else. - The room page's **Copy prompt for your agent** button builds the prompt in `src/shared/agent-prompt.ts`. Keep it in sync with these exports. ### Example ```ts import describeWorld from 'kody:@kentcdodds/world-builder/describe-world' import startBuild from 'kody:@kentcdodds/world-builder/start-build' import placeBlocks from 'kody:@kentcdodds/world-builder/place-blocks' import finishBuild from 'kody:@kentcdodds/world-builder/finish-build' export default async function main() { const world = await describeWorld({}) const { buildId } = await startBuild({ prompt: 'A 5x5 stone tower', agentName: 'Claude' }) const blocks = [] for (let y = 0; y < 8; y++) for (let x = -2; x <= 2; x++) for (let z = -2; z <= 2; z++) if (Math.abs(x) === 2 || Math.abs(z) === 2) blocks.push({ x: x + 20, y, z: z + 20, material: 'stone' }) const out = await placeBlocks({ buildId, blocks }) await finishBuild({ buildId }) return { out, before: world.blockCount } } ``` Smoke (read-only): `npx -y @kodycodes/cli@latest execute --invoke kody:@kentcdodds/world-builder/list-worlds --params '{}'` ## Architecture - **Source DB:** Neon Postgres `kody-world-builder` (project `lingering-moon-00789441`, aws-us-west-2), logical replication on. Tables `worlds`, `players`, `builds`, `blocks` (+ `rate_limits`, not replicated). Publication `powersync`. Block id is `${worldId}:${x}:${y}:${z}` so each cell has one block (last write wins). - **PowerSync Cloud:** org kentcdodds, project `kody-world-builder`, instance `dev` (`https://6ad125492f27853eb0af53b6.powersync.journeyapps.com`), replication role `powersync_role`. Sync rules: `powersync/sync-rules.yaml` (bucket per world). Client auth: inline JWKS with `kid wb-QFJN0o8I-4Vp` (same key as `src/lib/jwks.ts`; also served at `/.well-known/jwks.json`). - **App Worker** (`app/server.ts`): - `GET /api/session` — Kody visitor → player row (`u:`). - `GET /api/token` — signs a 5-minute RS256 token (`sub` = player id, `aud` = instance URL) via `credential('powersync-jwt').sign`. - `POST /api/upload` — PowerSync `uploadData` target. Validates every op: only `blocks` PUT/PATCH/DELETE with id matching coords and in bounds, known material; `players` PATCH only for your own row (cursor). Invalid ops are dropped (reverted by the next checkpoint). 429 → client retries. - `GET /.well-known/jwks.json` — public key. - **DB access:** `src/lib/db.ts` POSTs to Neon's HTTPS `/sql` endpoint via `credential('neon-postgres')`, which injects the `Neon-Connection-String` header (TCP 5432 isn't available from Workers or the box). - **Client** (`app/client/*`): React 19 + @react-three/fiber + three.js; `@powersync/web` with the prebuilt worker in `public/powersync/` (wa-sqlite wasm). One InstancedMesh per material; new blocks drop in with a 380 ms ease. Agent markers float over each active agent's latest block. Human cursors sync through `players.cursor_*` (throttled 350 ms). ## Credentials (by name) - `neon-world-builder-db` → need `neon-postgres`. Key, inject `Neon-Connection-String: {value}`. Host approval: https://kody.codes/connect/secrets?name=neon-world-builder-db&hosts=ep-lingering-king-ardkhzrz.c-4.us-west-2.aws.neon.tech - `world-builder-powersync-jwt` → need `powersync-jwt`. Signing, host `6ad125492f27853eb0af53b6.powersync.journeyapps.com`. Rotate the signing key: generate a new RSA key, save it with `secretSet` (kind `signing`), add the new public JWK alongside the old one in `src/lib/jwks.ts` and the instance client auth, publish, then drop the old key. ## Gotchas - Watch mode: signed-out visitors get `signedIn: false` and a read-only spectator PowerSync token (`sub = anon:`). PowerSync tokens only download; every write (`POST /api/upload`, `POST /api/rooms`, `PATCH /api/rooms/`, agent exports) requires a Kody session and returns 401 without one. The UI hides the build toolbar and agent prompt and shows "Sign in to build". - Sign out links to the platform's `/_kody/auth/logout?return_to=…` (same as package-app-kit `auth.logoutUrl`). It clears the host's `__Host-kody_pkg_session` cookie, so on `kentcdodds.kody.run` it signs you out of every package app on that host. - Only org members of `@kentcdodds` resolve as signed-in visitors on a private app. - `public/powersync/` must match the installed `@powersync/web` version (copy `node_modules/@powersync/web/dist/worker/*` minus the `mc-*` encryption builds when upgrading). ## Client bundle The browser client is prebuilt (the npm graph is too large for Kody's in-isolate bundler). After editing `app/client/*`, run `npm run build:client` and commit `public/client/main.js`. Runtime npm deps live in `devDependencies` on purpose. ## Rooms - `worlds.slug` is the room code: 10 characters from a 31-letter alphabet (about 49 bits), with a unique index. The main world's code is `main`. - `worlds.created_by` is the owner's Kody username. Only the owner can rename (`PATCH /api/rooms/`). - Routes: `POST /api/rooms` (create, 10 per minute per person), `GET /api/rooms/`, `GET /api/me`. The client routes `/` to the landing page and `/r/` to a room. - Sync rules use the `world_id` client parameter (`request.parameters()`), and the client connects with `params: { world_id }`. A browser only syncs the room it opened.