@kentcdodds/world-builder
Build a shared 3D voxel world with friends and your AI agents, live.
AGENTS.md
97 lines · 7.4 KB · Markdown@kentcdodds/world-builder — agent notes
Human intent lives in 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 "<person>'s <agentName>".
| 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.
roomis the room link (.../r/<code>) or its code.worldIdstill works. Without either, calls use the main world (room codemain).- 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
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(projectlingering-moon-00789441, aws-us-west-2), logical replication on. Tablesworlds,players,builds,blocks(+rate_limits, not replicated). Publicationpowersync. Block id is${worldId}:${x}:${y}:${z}so each cell has one block (last write wins). - PowerSync Cloud: org kentcdodds, project
kody-world-builder, instancedev(https://6ad125492f27853eb0af53b6.powersync.journeyapps.com), replication rolepowersync_role. Sync rules:powersync/sync-rules.yaml(bucket per world). Client auth: inline JWKS withkid wb-QFJN0o8I-4Vp(same key assrc/lib/jwks.ts; also served at/.well-known/jwks.json). - App Worker (
app/server.ts):GET /api/session— Kody visitor → player row (u:<username>).GET /api/token— signs a 5-minute RS256 token (sub= player id,aud= instance URL) viacredential('powersync-jwt').sign.POST /api/upload— PowerSyncuploadDatatarget. Validates every op: onlyblocksPUT/PATCH/DELETE with id matching coords and in bounds, known material;playersPATCH 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.tsPOSTs to Neon's HTTPS/sqlendpoint viacredential('neon-postgres'), which injects theNeon-Connection-Stringheader (TCP 5432 isn't available from Workers or the box). - Client (
app/client/*): React 19 + @react-three/fiber + three.js;@powersync/webwith the prebuilt worker inpublic/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 throughplayers.cursor_*(throttled 350 ms).
Credentials (by name)
neon-world-builder-db→ needneon-postgres. Key, injectNeon-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.techworld-builder-powersync-jwt→ needpowersync-jwt. Signing, host6ad125492f27853eb0af53b6.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: falseand a read-only spectator PowerSync token (sub = anon:<uuid>). PowerSync tokens only download; every write (POST /api/upload,POST /api/rooms,PATCH /api/rooms/<code>, 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-kitauth.logoutUrl). It clears the host's__Host-kody_pkg_sessioncookie, so onkentcdodds.kody.runit signs you out of every package app on that host. - Only org members of
@kentcdoddsresolve as signed-in visitors on a private app. public/powersync/must match the installed@powersync/webversion (copynode_modules/@powersync/web/dist/worker/*minus themc-*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.slugis the room code: 10 characters from a 31-letter alphabet (about 49 bits), with a unique index. The main world's code ismain.worlds.created_byis the owner's Kody username. Only the owner can rename (PATCH /api/rooms/<code>).- Routes:
POST /api/rooms(create, 10 per minute per person),GET /api/rooms/<code>,GET /api/me. The client routes/to the landing page and/r/<code>to a room. - Sync rules use the
world_idclient parameter (request.parameters()), and the client connects withparams: { world_id }. A browser only syncs the room it opened.