open-design/docs/codex-pets.md
Tom Huang 6fa2077651
feat(web): add pet companion with Codex hatch-pet integration (#296)
* feat(web): add pet companion with Codex hatch-pet integration

Introduces a customizable floating pet companion (overlay + entry-view rail
+ composer menu + dedicated Settings → Pets section) that supports built-in
pets, user customization (glyph/image/spritesheet), and one-click adoption
of pets packaged by the upstream Codex `hatch-pet` skill via a new
`/api/codex-pets` daemon endpoint. Vendors the unmodified `hatch-pet`
skill under `skills/hatch-pet/` and adds i18n strings across all locales.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(scripts): sync community Codex pets from public catalogs

Adds `pnpm sync:community-pets` which fetches all pets from
codex-pet-share.pages.dev (paginated Supabase Functions API) and
j20.nz/hatchery (single-shot JSON), then writes each one as
`<id>/pet.json` + `<id>/spritesheet.webp` under
`\${CODEX_HOME:-\$HOME/.codex}/pets/`. The existing daemon
`codex-pets` registry already scans that folder, so synced pets
appear under Settings → Pets → Recently hatched and adopt with one
click — no manual upload. Supports --source/--out/--force/--limit
flags and validates magic bytes so HTML error pages never end up
masquerading as `.webp` files.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(daemon): tighten codex-pets validation and document vendoring

- sanitizeId now rejects ids that still contain `..` after collapsing,
  closing a defensive gap on the path-traversal guard for the
  `/api/codex-pets/:id/spritesheet` route.
- listCodexPets emits the sanitised folder name as the public id so the
  download route resolves directly against the on-disk folder, even when
  `manifest.id` differs (manual drops, sanitiser-touched manifests).
- Drop `@ts-nocheck` from `codex-pets.ts`; module is now strict-typed
  with explicit interfaces, an unknown-narrowed JSON.parse path, and a
  `pickString` helper guarding manifest fields one by one.
- Restrict the spritesheet response CORS header to sandboxed-iframe
  callers (Origin: null) instead of unconditional `*`, matching the
  existing raw-file route pattern. Same-origin web traffic does not
  need the header (web proxies `/api/*` through the daemon).
- Add `skills/hatch-pet/README.md` explaining the vendoring trade-off,
  provenance, and re-sync procedure.
- Add `docs/codex-pets.md` covering where pets live, how to populate the
  registry without Codex installed, and the manifest contract.

Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)

* fix(i18n): add pet.* keys to Hungarian locale

Hungarian locale was added on main after this branch diverged, so the new
pet.* dictionary keys never landed there and tsc -b reports hu's Dict as
incomplete once main is merged in.

Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)

* feat(web): atlas-driven pet animations + bundled community pets

Builds on the existing pet companion (#296) with a richer animation
loop, a curated set of community pets that ship with the repo, and a
one-click sync into ~/.codex/pets/.

- Atlas-mode rendering: PetSpriteFace can now play the full Codex 8x9
  sprite atlas and swap rows from a JS-driven frame index. PetOverlay
  classifies pointer interactions (idle / hover / drag-direction /
  long-idle waiting) and maps them to the matching atlas row, so the
  pet waves on hover, runs on drag, and falls into a waiting pose
  after 6s of stillness. Single-strip pets keep their existing CSS
  steps() animation, with the steps timing fixed to jump-none so frame
  cells line up on cell boundaries.
- Atlas adoption: PetSettings exposes both "Use full atlas (animated)"
  and "Freeze to this row" — full mode keeps every row for the
  interaction state machine, single-row mode crops one strip via the
  existing canvas helper. New prepareCodexAtlas downscales the atlas
  to a localStorage-friendly PNG while preserving the grid layout.
- Settings tabs: pet sources are now split into Built-in / Custom /
  Community tabs so each origin gets its own dedicated surface.
- Bundled pets: scripts/bake-community-pets.ts seeds a curated set
  (clippit, dario, nyako-shigure, slavik, trump, tux, yelling-dario,
  yorha-sit-2b) into assets/community-pets/. The daemon scans this
  alongside the user's ~/.codex/pets/ root, with user pets winning
  when ids collide. CodexPetSummary gains a `bundled` flag so the UI
  can tag those cards with a "Bundled" pill.
- One-click community sync: daemon-side port of sync-community-pets
  exposed via POST /api/codex-pets/sync. Returns the same
  wrote/skipped/failed/total summary the CLI prints. Web Pet settings
  surface this as a "Download community pets" button under the
  Community tab.
- Avatar dropdown + hide rail: EntryView's avatar button is now a
  small menu (mirrors the project-view AvatarMenu) with toggles for
  hiding/showing the pet rail and opening Settings. PetRail gets a
  matching × button for the same hide flow.
- Locales: 7 new pet.* keys for tabs, sync, hide/show, atlas full
  mode, and the Bundled pill — translated into all 13 supported
  locales.

Typechecks pass across all workspace packages; daemon + web vitest
suites stay green.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(web): bundled-pets built-in tab, ambient atlas animations, and community sync button

The Built-in tab now sources its catalog from the bundled spritesheets
at `assets/community-pets/` instead of the eight emoji placeholders that
felt boring next to the Codex hatch-pet atlases.

- Daemon: `listCodexPets` flags `bundled: true` by curated-set membership
  in `assets/community-pets/`, not by which folder the sprite happened to
  be read from. Previously a fully-synced user inbox preempted every
  bundled id and left the tab empty.
- Settings → Pets → Built-in renders the same sprite-card grid as
  Community, filtered by `bundled: true`, and reuses the existing
  `adoptCodexPet` flow. Community tab filters to non-bundled so the
  curated set never appears twice.
- Community tab gains the long-promised "Download community pets"
  trigger that calls `/api/codex-pets/sync` and shows an inline status
  line for the run summary. Strings already existed in every locale; we
  just plumbed the button.
- `PetOverlay` gets ambient atlas-row choreography — while idle, the
  overlay occasionally swaps `idle` for a random non-idle row (wave /
  hop / look) so the pet doesn't feel frozen. User gestures cancel the
  beat and take over instantly. `pickAmbientRow` lives next to
  `pickAtlasRow` so both row pickers share the fallback discipline.
- One-shot `migrateCustomPetAtlas` heals configs adopted before the
  overlay learned row switching by re-downloading the full spritesheet
  so hover / drag / ambient variety light up on next launch.
- `BUILT_IN_PETS` is now an empty array (the type stays for backwards
  compat); legacy configs whose `petId` still points at an emoji id
  (`mochi`, `pixel`, …) fall back to the user's custom slot in
  `resolveActivePet` so the overlay never renders blank.
- i18n: refresh `pet.tabBuiltInHint` (drop "emoji companions") and add
  `pet.builtInEmpty` across all locales.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-02 23:45:39 +08:00

3.6 KiB

Codex pets

The pet companion in the web app can adopt pets packaged by the upstream Codex hatch-pet skill. This doc explains where those pets live, how Open Design discovers them, and what to do if you do not have Codex installed.

Where pets live

The daemon scans this directory on every list request:

${CODEX_HOME:-$HOME/.codex}/pets/<pet-id>/
  pet.json          # { id, displayName, description, spritesheetPath }
  spritesheet.webp  # 1536x1872 8x9 atlas (.png / .gif also accepted)

CODEX_HOME is honoured if set; otherwise the daemon falls back to ~/.codex/pets/. Both paths follow the upstream Codex conventions.

The scan is implemented in apps/daemon/src/codex-pets.ts and surfaced through GET /api/codex-pets (list) and GET /api/codex-pets/:id/spritesheet (raw bytes). The web pet settings panel calls these endpoints from apps/web/src/components/pet/PetSettings.tsx under the "Recently hatched" section.

I do not have Codex installed

You do not need Codex to use Open Design. The pet companion ships with built-in pets that work out of the box. The "Recently hatched" section will simply stay empty until something appears under ${CODEX_HOME:-$HOME/.codex}/pets/.

You have three ways to populate it without running Codex:

  1. Sync the public catalogs. Run node --experimental-strip-types scripts/sync-community-pets.ts (see the script header for flags). It downloads pets from the community catalogs into the canonical Codex layout, then they show up under "Recently hatched" on the next refresh.
  2. Drop a pet folder in by hand. Create ~/.codex/pets/<your-pet>/ with a pet.json and a spritesheet.webp (8x9 atlas). The daemon does not require Codex to be installed — it only needs the directory.
  3. Run the vendored skill in any chat agent. The hatch-pet skill is vendored under skills/hatch-pet/. Any agent that can execute skills (Codex, or any other) can run it end-to-end and write into the same directory.

If ~/.codex/pets/ does not exist, the daemon does not auto-create it — empty list is returned and the UI shows "no recently hatched pets yet". Creating the directory is intentionally an explicit user step so the daemon never writes outside OD_DATA_DIR / project-owned paths without a user opting in.

Manifest shape

The pet.json manifest is read defensively — every field is treated as optional and validated as a string before use. The shape we honour:

{
  "id": "shiba-pomegranate",
  "displayName": "Shiba Pom",
  "description": "Friendly pixel-art shiba.",
  "spritesheetPath": "spritesheet.webp"
}

Notes:

  • The folder name is the on-disk identity. The list endpoint reports the sanitised folder name as the public id so that /api/codex-pets/:id/spritesheet can resolve it directly even when manifest.id differs from the folder name (e.g. the manifest declares spaces or punctuation that get sanitised away).
  • spritesheetPath is resolved relative to the pet folder and is rejected if it would escape the folder. If unset, we fall back to spritesheet.webp, then .png, then .gif.
  • Any field that is not a non-empty string is ignored and the UI falls back to a sensible default (folder name → display name, empty description, etc.).
  • Daemon registry + manifest validation: apps/daemon/src/codex-pets.ts
  • HTTP routes (list + spritesheet): apps/daemon/src/server.ts
  • Web list / adopt UI: apps/web/src/components/pet/PetSettings.tsx
  • Shared response types: packages/contracts/src/api/registry.ts
  • Vendored skill source: skills/hatch-pet/
  • Community catalog sync script: scripts/sync-community-pets.ts