diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 1f0d4e2..8eea591 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -11,26 +11,26 @@ npm install
npm start # your pet appears
```
-The [Development section of the README](README.md#development) covers the day-to-day commands. The short version:
+The [development guide](docs/development.md) covers the day-to-day commands. The short version:
| Command | What it does |
|---------|--------------|
| `npm test` | Smoke tests (node --test). Must be green before a PR. |
-| `npx eslint src/ tests/` | Lint. Lint these folders, not `.` (local scratch dirs are noisy). |
-| `npm run sheet` | Renders every pose x coat into `previews/contact-sheet.png` for visual QA. |
+| `npm run lint` | Lint (eslint, cached). CI runs exactly this. |
+| `npm run poses:cat` | Renders every activity x coat into `previews/cat-poses.png` for visual QA. Headless: no Electron, no GPU. |
| `npm run test:boot` | Boots the app headlessly and renders one frame. |
## Before you open a PR
1. **Tests green:** `npm test` (CI runs the same thing).
-2. **Visual changes need visual proof.** If you touch `src/renderer.js`, `src/cat-sprite.js`, or anything that draws, run `npm run sheet` before and after. For pure refactors the sha256 of the contact sheet should not change; for intentional changes, include a before/after crop in the PR.
+2. **Visual changes need visual proof.** If you touch `src/renderer.js`, `src/cat-sprite.js`, or anything that draws, run `npm run poses:cat` before and after. For pure refactors the sha256 of the sheet should not change; for intentional changes, include a before/after crop in the PR.
3. **Keep the site cat in sync.** `site/cat-sprite.js` must stay a byte-identical copy of `src/cat-sprite.js` (a test enforces this). If you change the sprite, re-copy it: `node -e "fs.copyFileSync('src/cat-sprite.js','site/cat-sprite.js')"`.
4. **Respect line endings.** Files in this repo are a mix of LF and CRLF for historical reasons. Do not normalize files you are not otherwise changing; keep each file's existing endings.
5. **Commit style:** conventional commits (`feat:`, `fix:`, `docs:`, `test:`, `chore:`), imperative mood, like the existing history.
## Good places to start
-- **Own a Mac?** The macOS port is code-complete but untested on real hardware. Running the [beta checklist](README.md#build-a-standalone-app) and reporting what happened is the single most useful contribution right now.
+- **Own a Mac?** The macOS port is code-complete but untested on real hardware. Running the [beta checklist](docs/development.md#macos-beta-checklist) and reporting what happened is the single most useful contribution right now.
- **Design a coat** and share it (Settings → Custom coats → Export). Great ones can become built-ins.
- Anything labeled [good first issue](https://github.com/JOhnsonKC201/pixelpets/labels/good%20first%20issue) or [help wanted](https://github.com/JOhnsonKC201/pixelpets/labels/help%20wanted).
diff --git a/README.md b/README.md
index 664162b..ea9c525 100644
--- a/README.md
+++ b/README.md
@@ -40,18 +40,13 @@ scratch: nearly every sprite, animation, and sound is original and procedural.
- Two pets ·
- In action ·
- Features ·
+ In action ·
+ What it does ·
Quick start ·
- Meet the cat ·
- Stay on track ·
Controls ·
AI agents ·
- Custom coats ·
- How it works ·
Privacy ·
- Development ·
+ Docs ·
Contributing
@@ -59,36 +54,6 @@ scratch: nearly every sprite, animation, and sound is original and procedural.
---
-## Two pets, one app
-
-Pick your species from the tray (**Pet → Cat / Dog**). Each one keeps its own
-coat choice, so switching back and forth never loses your pick.
-
-| | Cat | Dog |
-|---|---|---|
-| Coats | 14 coats, from Orange Tabby to Russian Blue | the Black Lab |
-| Resting | loafs into a "cat bread" | curls nose-to-tail into a ring |
-| Excited | hunting crouch, ears back | **play bow**: chest down, rump up, tail flagged |
-| Tail | slow rolling S-curve, tip flicks | fast wag from the base, shaped per breed (curl / plume / feather / stub / straight) |
-| Play | bats a butterfly | **fetch**: chases the ball down, carries it home, drops it |
-| Left alone | a butterfly flutters in | starts its own game of fetch |
-| After exertion | grooms | pants, tongue out |
-| Scroll | climbs a yarn rope, or swipes at a blowing leaf, depending on the coat | rears into a beg and swipes at the leaf |
-| Reward | a fish treat | a tennis ball |
-
-The dog is not a recoloured cat. It has its own sprite module with a muzzle that
-protrudes past the skull line, floppy ears, a broader chest, and a straight
-otter tail. Markings are coat *structures* rather than palette swaps, and the
-build archetypes behind them (spotted, saddled, tricolour, masked, merle, and
-the short-legged dwarf silhouette) are still in the sprite module for any breed
-added back later.
-
-```bash
-npm run poses:dog # previews/dog-poses.png - every breed x every ACTIVITY
-npm run poses:cat # previews/cat-poses.png - 14 coats x 11 activities
-npm run sheet:dog # previews/dog-sheet.png - the five base poses only
-```
-
## See it in action
@@ -135,13 +100,42 @@ Nothing is a screen recording.
+### Cat or dog
+
+Pick your species from the tray (**Pet > Cat / Dog**); each keeps its own coat.
+The dog is not a recoloured cat. It has its own sprite module, with a muzzle that
+protrudes past the skull line, floppy ears, a broader chest, and a straight otter
+tail.
+
+| | Cat | Dog |
+|---|---|---|
+| Excited | hunting crouch, ears back | **play bow**: chest down, rump up, tail flagged |
+| Play | bats a butterfly | **fetch**: chases the ball down, carries it home, drops it |
+| After exertion | grooms | pants, tongue out |
+| Reward | a fish treat | a tennis ball |
+
+[Full comparison, and every interaction, in the feature guide.](docs/features.md#cat-or-dog)
+
+### It keeps you on track
+
+The pet doubles as a quiet productivity companion, and it works out when to leave
+you alone. **Focus Guard** reads "busy" from a calendar event that is actually
+happening now, from Quiet Hours, or from Work mode, then parks the pet and holds
+email, reminders and agent messages back rather than meowing into your
+screen-share. Nothing is thrown away: when you are free it sums the lot up in one
+line. On top of that sit break and Pomodoro timers, repeating reminders, a pinned
+note, IMAP mail alerts, `.ics` calendar nudges, and a live lo-fi jam.
+
+[All of it, in detail.](docs/features.md#stay-on-track)
+
## Quick start
The Windows installer is the easy path. No git, no Node, just run it:
[](https://github.com/JOhnsonKC201/pixelpets/releases/latest)
-You can also [play with the cat in your browser](https://pixelcat-jet.vercel.app) before installing anything.
+You can also [play with the cat in your browser](https://pixelcat-jet.vercel.app)
+before installing anything.
> [!IMPORTANT]
> **Your OS will warn you the first time, and that is expected.** The builds are
@@ -162,7 +156,7 @@ You can also [play with the cat in your browser](https://pixelcat-jet.vercel.app
> [Privacy](#privacy) and [SECURITY.md](SECURITY.md): the keyboard hook forwards
> a single "a key was pressed" boolean and never what you typed.
-To run from source (for development, or on macOS) you need git and Node 20 or newer:
+To run from source, on any platform, you need git and Node 20 or newer:
```powershell
git clone https://github.com/JOhnsonKC201/pixelpets.git
@@ -173,226 +167,9 @@ npm start
Either way your pet appears in the corner and registers itself to start at login.
`npm run autostart:off` turns that off, and the installed version uninstalls like
-any other program, from Windows Settings > Apps.
-
-> [!TIP]
-> **Windows, running from source:** `npm start` leaves a console window open behind
-> the pet. `launch-pixelpets.vbs` starts the same thing silently, so it works as a
-> desktop shortcut (point one at `wscript.exe "\launch-pixelpets.vbs"`). It
-> locates the project from its own path, so moving the folder does not break it.
-
-> [!NOTE]
-> **macOS (beta):** same commands. On first run, grant Accessibility permission
-> (System Settings > Privacy & Security > Accessibility) so your pet can react to your
-> typing and cursor. Input is only detected, never logged or sent anywhere. The mac
-> port is code-complete but not yet smoke-tested on real hardware; see the
-> [build checklist](#build-a-standalone-app). Issues welcome.
-
----
-
-## Meet the cat
-
-The whole point: a pet that *feels* alive on your desktop. It reacts to touch,
-to your cursor, to your typing, and to its own internal mood.
-
-| Interaction | What the cat does |
-|-------------|-------------------|
-| **Drag it** | Stretches like mochi (head and feet stay solid while the body thins), then squashes and bounces back. Shake it side to side and it wobbles like jello with a startled mrrp. It stays where you drop it. |
-| **Pet its head** | Squeezes its eyes shut, wiggles, floats little hearts, and purrs. The squint holds through the whole stroke, not just when your hand stops moving. |
-| **Touch its body** | Squints just as happily, leans and arches into your hand, tail up, trilling. |
-| **Tap it** | A quick pet: happy eyes, hearts, a chirp. |
-| **Move your cursor** | The cat watches it and blinks now and then. Flick the cursor fast and it crouches, stalks, and pounces. A sudden jolt startles it: it puffs up, freezes, then bolts or creeps back. |
-| **Type in any app** | It leans onto two big keys and kneads them with its paws. Type fast enough and it overheats, turning red with steam, then cools down. |
-| **Scroll anywhere** | Coats with hand-painted climb art grab a yarn rope and haul themselves up it, hand over hand, with a ball of yarn on the floor below. Every other coat, and every dog, rears up and swipes at a leaf blowing past in the direction you are scrolling, faster the harder you flick the wheel. |
-| **Wait for a visitor** | Once in a while a butterfly flutters in. The cat tracks it, swats at it, and occasionally pounces and catches it between its paws before it flutters off. Step away from the keyboard and one comes out on its own, so the pet always has something to play with. Keep a dog instead and it starts its own game of fetch rather than waiting on a butterfly. |
-| **Leave it be** | Left alone it keeps itself busy: it bats a drifting leaf with a paw, washes its face, loafs, and its whiskers twitch. Lively without ever getting in your way. |
-| **Come back** | Return after being away and the cat notices you: happy eyes, a little heart, and a friendly chirp hello. |
-| **Give it a treat** | Pick "Give a treat" from the tray and a little fish drops in. The cat trots over and noms it with hearts and a happy chirp. |
-| **Late at night** | After 23:00 the cat winds down. It settles to calm faster and loafs or dozes more, though a nudge still rouses it. |
-
-
-Coats and pixel art: 14 built-in patterns, custom coats, a polished sticker look
-
-- 14 coat patterns: Orange, Mackerel, and Brown tabby, Siamese, Tuxedo, Black,
- Gray, White, Cream, Tortoiseshell, Calico, Slate, Chocolate (a solid warm-brown
- Havana with green eyes), and Russian Blue (cool blue-grey with green eyes). It
- ships as Mackerel Tabby; right-click the cat to cycle, and your choice is remembered.
-- Custom coats: design your own (see [Custom coats](#custom-coats)).
-- The pixel art has a white sticker outline that pops on any wallpaper, soft
- top-lit shading, whiskers, a ground shadow, and sparkly eyes.
-- The cat is one role-coded sprite recolored per pattern at draw time, so a
- dozen cats come from one shape, and shading, outline, and the overheat tint
- apply to every coat for free.
-
-
-
-
-Moods and energy: calm, playful, or zoomies, driven by what you do
-
-The cat tracks an internal energy value (0 to 100) that decays over time and is
-bumped by stimuli: typing, scrolling, fast-mouse play, petting, an AI agent
-finishing. Energy maps to three mood bands that gate and scale every reaction:
-
-| Band | Energy | Behaviour |
-|------|--------|-----------|
-| **Calm** | 0-50 | Mellow, with small and infrequent idle moves (loafs, grooms) |
-| **Playful** | 51-80 | Full reactions (the original feel) |
-| **Zoomies** | 81-100 | Frantic and fast, then a hard crash back toward calm |
-
-Keep it busy and it gets the zoomies, then settles back down. Startle fires on
-an abrupt cursor jump (no microphone, fully local). The whole system turns off
-with the Mood reactions toggle (Settings or tray) for the classic always-playful
-behaviour, and the tray Mood submenu has "Zoomies!" and "Calm down" to drive the
-model on demand.
-
-
-
-
-Sound: synthesized meow, purr, chirp, and mrrp, with no audio files
-
-The meow is a voiced sawtooth shaped by a moving mouth formant (it opens into
-the "ee" and closes through the "ow"), with a breath of air on the onset and
-gentle vibrato. Each meow randomly comes out as a short mew, a two-syllable
-meow, or a drawn-out meeow, so it never sounds canned, and pitch and length
-also vary by cat species. There is also a purr, a rolled chirrup trill (the
-flutter cats greet you with), and a startled mrrp. All of it is synthesized
-with Web Audio in code, so there are no audio assets to ship. Toggle it in
-Settings.
-
-
-
-
-Desktop-pet overlay: floats over everything, never blocks your clicks
-
-A full-screen, transparent, click-through layer: the cat floats over everything
-but only the cat itself is interactive. It stays on top of every app (it
-re-asserts top-most, even over fullscreen windows), and you can confine it to a
-play area by picking a tray preset or drawing one with the mouse (tray > Set
-play area). It starts at login by registering itself in Windows startup.
-
-
-
-## Stay on track
-
-pixelpets doubles as a quiet productivity companion. Every alert comes through
-your pet, as a meow or a bark and a speech bubble, with an optional real
-desktop notification.
-
-
-Focus Guard: the pet works out when to leave you alone
-
-A desktop pet that meows into the middle of a screen-share is a desktop pet you
-uninstall. Focus Guard is the pet noticing you are busy without being told.
-
-It reads "busy" from a calendar event that is actually happening now (an all-day
-block is explicitly not a meeting, so *Vacation* cannot silence it for a whole
-day), from Quiet Hours, or from Work mode. While you are busy the pet parks in
-its rest corner and stops chasing things, and email, reminders and agent
-messages **wait** rather than interrupt.
-
-Nothing is thrown away. Held messages still land in the tray recap as they
-arrive, and when you are free the pet sums them up in one line - *"While you
-were busy: 3 new emails and 1 reminder."* Calendar nudges always come through,
-because being told about the meeting you are about to miss is the opposite of an
-interruption, and you can name senders under **Always tell me about** so the
-people who matter reach you anyway.
-
-
-
-
-Quiet hours: a nightly do-not-disturb window
-
-Set a From and To time and the pet goes silent between them - no meow, no
-desktop notification. The speech bubble still appears, so a reminder that lands
-at 3am is waiting for you in the morning rather than lost.
-
-
-
-
-Timers: break reminders and Pomodoro
-
-- Break timer: pick an interval and the cat grows big to stretch with you and
- meows, on a schedule. Or "Start break now."
-- Pomodoro timer: set focus and break loops, and a pixel timer floats next to
- the cat (a tomato dot for focus, green for break). At the end of each focus
- block the cat stretches with you; when the break ends it meows "Back to
- focus!". Toggle in Settings or the tray.
-
-
-
-
-Lobby Jam: synthesized lo-fi study music the cat plays live
-
-Flip on Lobby Jam (Settings or the tray submenu) and the cat picks up a little
-guitar and plays an endlessly improvising lo-fi loop: plucked Karplus-Strong
-guitar over lazy jazz voicings, soft bass, brushed percussion, and tape warmth.
-It is all Web Audio, generated live, with no audio files. Pick a mood:
-
-| Mood | For |
-|------|-----|
-| **Cozy café** | A warm, easy background loop |
-| **Dreamy** | Slow and washed out, with lots of reverb |
-| **Upbeat lounge** | Brighter and a touch faster |
-| **Deep focus** | Steady and minimal, almost no flourishes, so it stays out of the way while you work |
-| **Rainy study** | A cozy loop over a soft, gusting rain bed |
-| **Sleepy night** | Very slow, warm, and dark, for late-night wind-down |
-
-The music mixes through the same Volume control as the meow and purr, and the
-floating notes and the cat's strumming bob along in time with the beat.
-
-
-
-
-Reminders and notes: timed, repeating, snoozable
-
-- Reminders: set a time and a message and the cat meows and shows a speech
- bubble. Reminders can repeat (daily, weekdays, specific weekdays, or once),
- can be snoozed from the tray, and support `{name}`, `{time}`, and `{date}`
- placeholders.
-- Pinned note: pin an important message and it stays in a bubble above the
- cat's head until you clear it. Reminders briefly take over, then it returns.
-- It calls you by name: tell the cat your name in Settings (or use `{name}`)
- and it greets you with it.
-- Desktop alerts: every reminder can also raise a real Windows notification and
- a sound, so you never miss one when you are not looking at the cat.
-
-
-
-
-Mail and calendar: IMAP unread alerts and .ics event nudges
-
-- Unread-mail alerts: point the cat at your IMAP inbox (Gmail, Outlook, anything
- IMAP) and it tells you who the mail is from - *"Alice: Budget review"* - so you
- can decide from the bubble instead of going to look. Name the senders you never
- want to miss under **Always tell me about**, and they reach you even while
- Focus Guard is holding everything else back; a bare domain like `@acme.com`
- covers everyone there. Your app password is stored encrypted at rest (Electron
- `safeStorage`, which is DPAPI on Windows), never in `settings.json`, and the
- IMAP connection runs in an isolated worker process.
-- Calendar nudges: paste your calendar's secret `.ics` URL (Google and Outlook
- both provide one) and the cat nudges you a few minutes before each event. The
- feed is fetched and parsed in an isolated worker.
-
-
-
-
-Notify the cat: push any message from a script, CI, or cron job
-
-Any script or tool can make the cat deliver an arbitrary message: a speech
-bubble, a Windows toast, and a meow.
-
-```bash
-node scripts/notify.js "Build finished" --title CI --level success
-node scripts/notify.js "Coffee break ☕" --ttl 8000
-echo "anything" | node scripts/notify.js "Deploy done" # hook-safe (drains stdin)
-```
-
-Flags: `--title `, `--level info|success|warn|alert`, `--ttl `,
-`--no-sound`. The script appends one JSON line to `%TEMP%/pixelcat-notify.jsonl`
-and the running pet tails the file. Lines written before the pet launched are
-ignored (no backlog replay), and calling it while the pet is closed is harmless.
-
-
+any other program, from Windows Settings > Apps. Running from source has two
+platform notes (a silent Windows launcher, and the macOS Accessibility grant)
+covered in the [development guide](docs/development.md).
## Controls
@@ -410,275 +187,90 @@ ignored (no backlog replay), and calling it while the pet is closed is harmless.
Settings persist to `settings.json` in your per-user app-data folder
(`%APPDATA%/pixelpets/` on Windows, `~/Library/Application Support/pixelpets/` on
-macOS). An install that predates the rename is migrated across on first launch. Timers and reminders only fire while
-pixelpets is running, and reminder times use your local clock.
+macOS). An install that predates the rename is migrated across on first launch.
+Timers and reminders only fire while pixelpets is running, and reminder times use
+your local clock.
## AI agent reactions
-The cat reacts to a coding agent's work status, and it uses its paws to do it.
-It raises a paw to its chin to ponder (with a "…" bubble) while an agent like
-Claude Code, Codex, or Cursor thinks, taps a paw along with a spinner while it
-works, and does a happy hop and meow when it finishes. Any tool can signal it by
-running the bundled helper, which writes a tiny status file the cat watches
+The cat reacts to a coding agent's work status, and it uses its paws to do it. It
+raises a paw to its chin to ponder while an agent like Claude Code, Codex, or
+Cursor thinks, taps a paw along with a spinner while it works, and does a happy
+hop and meow when it finishes. Any tool can signal it by running the bundled
+helper, which writes a tiny status file the cat watches
(`%TEMP%/pixelcat-agent.state`):
```bash
node agent-hook.js thinking # ponders, paw to chin + "…" bubble
-node agent-hook.js editing # taps a paw + "working" spinner (also: writing/testing/building/running)
+node agent-hook.js editing # taps a paw + "working" spinner
node agent-hook.js error # the cat startles (flinch)
node agent-hook.js done # happy hop + meow
node agent-hook.js idle # back to normal
```
-Ready-to-use hook configs for five agents live in [`integrations/`](integrations/).
-Copy the one for your agent and replace the path with your checkout:
-
-| Agent | Setup |
-|-------|-------|
-| **Claude Code** | merge [`integrations/claude-code/settings.hooks.json`](integrations/claude-code/) into `~/.claude/settings.json` |
-| **Codex CLI** | merge [`integrations/codex/config.toml`](integrations/codex/) into `~/.codex/config.toml` |
-| **Cursor** | copy [`integrations/cursor/hooks.json`](integrations/cursor/) to `/.cursor/hooks.json` |
-| **Antigravity** | add hooks to `.agents/hooks.json`, per [`integrations/antigravity/`](integrations/antigravity/) |
-| **Kiro** | add hooks via the Agent Hooks UI, per [`integrations/kiro/`](integrations/kiro/) |
-
-The mapping: prompt and submit events signal `thinking`, tool calls and file
-edits signal `working`, and stop or complete signals `done`. Use the absolute
-path to `agent-hook.js`, since hooks run from varying directories (forward
-slashes work on Windows too). The helper is hook-safe: it drains stdin and
-replies `{"continue": true}`, so it never blocks or alters your agent.
-
-> Tip: `node scripts/install-hook.js ` (or `npm run hook -- `)
-> prints the config with the absolute path already filled in.
+Ready-to-use configs for **Claude Code, Codex CLI, Cursor, Antigravity and Kiro**
+live in [`integrations/`](integrations/), and `npm run hook -- ` prints
+yours with the absolute path already filled in. The helper is hook-safe: it
+drains stdin and replies `{"continue": true}`, so it never blocks or alters your
+agent.
The richer status reactions were inspired by the open-source AI desktop pets
-[openpets](https://github.com/alvinunreal/openpets) (MIT) and
-[clawd-on-desk](https://github.com/rullerzhou-afk/clawd-on-desk) (AGPL-3.0).
+openpets (MIT) and
+clawd-on-desk (AGPL-3.0).
Ideas only; all code here is original to pixelpets.
-## Custom coats
-
-Design your own under Settings > Pet > Custom coats > "+ Add a custom coat": pick
-a name, a body build (standard, slender, stocky, or fluffy), optional tabby
-stripes, and eight colours (coat, marks, white, patch, eyes, nose, inner ear,
-outline). Your coat shows up in the Coat dropdown and the tray menu next to the
-14 built-ins. Custom coats are built from the cat's geometry, so they apply to
-cats only; the Pet tab says so when a dog is selected.
-
-Custom coats live in `themes.json` in your app-data folder
-(`%APPDATA%/pixelpets/themes.json`) and can be hand-edited too:
-
-```json
-{ "themes": [
- { "name": "Galaxy", "build": "fluffy", "tabby": false,
- "coat": "#3b2f63", "mark": "#2a2147", "white": "#c9c0e8", "patch": "#7a5cc0",
- "eye": "#7fd6ff", "nose": "#e0a0c0", "inner": "#9a7ad0", "outline": "#15101f" } ] }
-```
-
-Every colour role is required and must be a `#rrggbb` hex (invalid themes are
-skipped). Preview one without the overlay: `npx electron . --shot --pattern=galaxy`.
-Share coats with Export and Import in the same panel; they write and read a JSON
-file, and imported coats merge into your set by name.
-
-## Build a standalone app
-
-```powershell
-npm run pack # portable build -> dist/win-unpacked/pixelpets.exe (no installer)
-npm run dist # Windows installer -> dist/pixelpets Setup .exe
-```
-
-`pack` works out of the box. `dist` (the NSIS installer) needs permission to
-create symlinks while electron-builder unpacks its bundled signing tools:
-enable Windows Developer Mode (Settings > Privacy & security > For developers)
-or run the build once from an Administrator terminal. The native `uiohook-napi`
-module ships N-API prebuilds, so `npmRebuild` is disabled in the build config
-and no Visual Studio is needed. `npm run icon` regenerates the procedural app
-and tray icons.
-
-**macOS:** `npm run dist:mac` (on a Mac) builds a dmg and zip for Apple Silicon
-and Intel; the release workflow also builds them in CI on every version tag.
-Builds are ad-hoc signed rather than notarized, so Gatekeeper still asks before
-the first launch: open it once, then **System Settings > Privacy & Security >
-Open Anyway**. (The ad-hoc seal matters for more than the warning - macOS keys the
-Accessibility grant and the Keychain entry to a code signature, so without it both
-would be thrown away on every update.) The mac port
-is code-complete but not yet smoke-tested on real hardware. If you have a Mac,
-the checklist is: the overlay shows over all apps and Spaces (including
-fullscreen), clicks pass through except on the cat, the typing reaction works
-after granting Accessibility, the cat rests on the Dock edge (not the menu
-bar), the tray menu works in the menu bar, and login launch works.
-
-## Drive it from an iPad
-
-```
-npm run ipad:lan
-```
-
-That prints a URL. Open it in Safari on the iPad and you have a real terminal - `vim`,
-`top`, tab completion, colours - on **this** machine. Share → Add to Home Screen gives
-it an icon and a full-screen window.
-
-The honest limitation first, because it is the reason the tool works this way: nothing
-running on an iPad can control other iPad apps. iPadOS sandboxes every app, Apple
-exposes no API for cross-app control, and no terminal on the App Store gets around it -
-Shortcuts is the only sanctioned path, and only for apps that publish App Intents. So
-this puts the shell on the computer, where a shell is worth having, and lets the iPad be
-the screen and the keyboard.
-
-It is built for a tablet: a key bar supplies the Esc, Tab, Ctrl and arrows the soft
-keyboard lacks, and because Safari suspends a backgrounded tab, the shell outlives the
-connection and replays exactly the output you missed when you come back.
-
-It is a shell running as you, over plain HTTP. Fine on your own Wi-Fi, not fine anywhere
-else - see [`tools/ipad-terminal/`](tools/ipad-terminal/) for the full notes.
-
-## How it works
-
-- The cat is one role-coded sprite (outline, coat, markings, white, patch, eye,
- nose, inner ear) built procedurally, then recolored per pattern at draw time,
- so a dozen cats come from one shape.
-- Every pose is composed into that same grid, including the limbs. A raised paw
- is made of the same cells as the rest of the pet, so it picks up the coat's
- shading, outline halo, markings and breathing scale for free. Nothing is
- painted on top afterwards. That is why washing, pondering, batting a leaf,
- boxing at the butterfly and swiping at the scroll leaf all work in all 15 coats
- and both species without a single extra sprite asset.
-- Poses that vary continuously (how high a paw is raised, how far it reaches,
- which paw is mid-swipe) are quantised to a handful of steps and memoised, and
- built only for the coat currently on screen. Pixel art wants stepped limbs
- anyway, so the cheap thing and the right-looking thing are the same thing.
-- Rendering happens on an HTML canvas in a full-screen transparent Electron
- window. Ordinary screenshots cannot capture it (it is GPU-composited), so
- previews use a self-capture:
- `electron . --shot --pattern= [--species=cat|dog] [--state=] [--at=]`.
-- The mochi drag is a spring system (a pinned handle plus a trailing body
- point) with a three-band stretch that keeps the head and feet rigid.
-- Break timers and reminders are scheduled in the main process (the renderer
- throttles and pauses when idle), which pushes fire events to the cat over
- IPC. Sound is synthesized with Web Audio in the renderer, so there are no
- audio assets to ship: every meow, bark, purr, pant, swipe and thud is
- generated live, and so is the lo-fi jam.
-
-### Project layout
-
-```
-pixelpets/
- src/
- main.js # overlay window, global input hooks, tray + menu,
- # scheduler (breaks, reminders, Pomodoro), config IPC
- renderer.js # the pet: sprites, palettes, physics, reactions, sound
- preload.js # safe IPC bridge for the overlay
- cat-sprite.js # the role-coded cat, mirrored to site/ for the browser demo
- dog-sprite.js # the Black Lab, and the poses composed from it
- pets.js # per-species registry: menu labels, IPC channels, copy
- patterns.js # shared coat list, recolored at draw time
- art-frames.js # baked hand-painted poses that win over the composer
- climb-frames.js # the yarn-rope climb, per coat
- audio.js # synthesized voice: meow, purr, bark, pant, chirp
- jam.js # the live lo-fi jam, generated bar by bar
- effects.js / bubble.js # hearts and leaves, and the speech bubble
- focus.js # when to leave you alone: meetings, work mode, quiet hours
- quiet-hours.js # the nightly do-not-disturb window
- mail.js / mail-worker.js # IMAP unread-mail checks (isolated worker)
- cal.js / cal-worker.js # .ics calendar feed (isolated worker)
- config.js # settings.json load/save/normalize (per-user app data)
- datadir.js # one-time rescue of that folder after the rename
- themes.js # custom-coat load/validate
- template.js # message placeholders: {name}, {time}, {date}, {count}
- index.html # the overlay page
- settings*.{html,js} # settings window + its IPC bridge
- tests/ # node:test suites, no Electron and no GPU required
- scripts/ # the vm harness, icon and demo generators, notify.js,
- # install-hook.js, the boot check
- tools/ipad-terminal/ # browser terminal for this machine, driven from an iPad
- integrations/ # ready-made agent hook configs (5 agents)
- assets/ # generated icons, showcase, hero + gallery clips
- site/ # the browser demo deployed to Vercel
- docs/ # frame-pack guide, and the early coat studies
- extras/ # the standalone Lobby Jam page
- .github/ # CI and release workflows, issue and PR templates
-```
-
## Privacy
Your pet reacts to your typing and scrolling, which means it listens to global
input events, so here is the plain statement: input is used only to trigger
-animations, in the moment, on your machine. Keystrokes are never logged,
-stored, or sent anywhere. There is no telemetry and no auto-update. The app
-makes no network connections at all unless you set up the optional mail or
-calendar alerts, and those talk only to the servers you point them at, from
-isolated worker processes. Your IMAP app password is stored encrypted at rest
-(Electron `safeStorage`) and never written to `settings.json`.
+animations, in the moment, on your machine. Keystrokes are never logged, stored,
+or sent anywhere. There is no telemetry and no auto-update. The app makes no
+network connections at all unless you set up the optional mail or calendar
+alerts, and those talk only to the servers you point them at, from isolated
+worker processes. Your IMAP app password is stored encrypted at rest (Electron
+`safeStorage`) and never written to `settings.json`.
+
+## Documentation
+
+| Guide | What is in it |
+|---|---|
+| [Features](docs/features.md) | Every interaction, coat, mood, sound, and productivity feature |
+| [Custom coats](docs/custom-coats.md) | Designing, hand-editing, and sharing your own coat |
+| [How it works](docs/architecture.md) | One sprite covering 15 coats, and the project layout |
+| [Development](docs/development.md) | Running from source, building installers, visual QA |
+| [Frame pack](docs/frame-pack.md) | Painting a pose by hand and importing it |
+| [Agent hooks](integrations/) | Wiring the pet to Claude Code, Codex, Cursor, Antigravity, Kiro |
+| [iPad terminal](tools/ipad-terminal/) | A real terminal for *this* machine, driven from an iPad |
## Development
```powershell
-npm start # run the app
-npm test # 200+ tests: config and data migration, poses, interactions,
- # audio, focus and quiet hours, mail and calendar, site drift
-npm run lint # eslint over src, tests, scripts and site (what CI runs)
-npm run test:boot # launch the real app and assert it renders a frame
-npm run check:hook # does the global input hook load on this Electron?
-npm run poses:cat # previews/cat-poses.png (every activity x every coat)
-npm run poses:dog # the same for the Black Lab
-npm run frames:import -- # import painted PNGs as baked poses
-npm run demo:all # regenerate the README media (hero, gallery, carousel)
-npm run hook -- cursor # print a path-filled agent hook config
-npm run icon # regenerate the tray + app-tile icons
+npm start # run the app
+npm test # 200+ tests, no Electron and no GPU required
+npm run lint # what CI runs, alongside the tests and a real boot check
+npm run poses:cat # contact sheet: every activity x every coat, for visual QA
```
-CI runs `npm run lint`, `npm test`, and the boot check on Windows and macOS for
-every push and pull request.
-
-**Painting a pose by hand.** Every pose is composed in code, which is why one
-`sit` covers 15 coats and a new coat costs nine hex values instead of an art pass.
-The trade is that changing how the pet looks means editing geometry.
-`npm run frames:import` is the escape hatch: paint a pose against a placeholder
-palette, import it, and it wins over the composer for exactly the coats you name
-while everything else keeps composing. Five held poses can be baked (`sit`,
-`type`, `loaf`, `rear`, `hunt`); the six raised-limb activities are parameterised
-rigs whose limbs sweep through quantised frames, so a still would freeze them.
-Palette, naming and the checks the importer runs are in
-[docs/frame-pack.md](docs/frame-pack.md).
-
-**Visual QA.** The overlay is GPU-composited, so ordinary screenshots cannot
-capture it. Poses are reviewed with a one-command contact sheet instead:
-`npm run poses:cat` renders **every activity across every coat** into one image,
-bottom-aligned on a shared floor line so silhouettes can be compared down a
-column. It runs headlessly, with no Electron and no GPU, by loading the overlay's
-script stack in a vm (`scripts/overlay-vm.js`) and reading the pose grids back
-out. That is also what the pose tests drive, so the sheet and the suite are
-looking at exactly the same sprites.
-
-**README media.** The hero banner, the gallery above, and the coat carousel are
-all rendered from the same sprite geometry by `scripts/make-demo-gif.js`, in
-pure Node with no browser or GPU. Regenerate any one with
-`node scripts/make-demo-gif.js [mp4]`, or all of them
-with `npm run demo:all`.
-
-Single poses preview via
-`npx electron . --shot --state= --pattern=`.
-Add `--at=` to capture an animated pose at a chosen phase (a typing
-key-press, say): `npx electron . --shot --state=typing --at=760`. Add
-`--note=""` to pin a speech bubble open in the capture, which is how bubble
-wrapping and screen-edge clamping get checked against a real font rather than only
-in unit tests: `npx electron . --shot --note="a long reminder that has to wrap"`.
-
-### Tech
-
-Electron · HTML canvas · Web Audio · [`uiohook-napi`](https://github.com/SnosMe/uiohook-napi)
-(system-wide keyboard hook) · [`imapflow`](https://github.com/postalsys/imapflow) ·
-[`node-ical`](https://github.com/jens-maus/node-ical).
+The overlay is GPU-composited, so ordinary screenshots cannot capture it. Visual
+changes are reviewed with those contact sheets instead. The full command list,
+build instructions, and the macOS beta checklist are in the
+[development guide](docs/development.md).
## Contributing
Bug reports, ideas, and PRs are all welcome. Start with the
-[contributing guide](CONTRIBUTING.md); the [security policy](SECURITY.md)
-covers reporting a vulnerability privately. If you own a Mac, running the
-[beta checklist](#build-a-standalone-app) and opening an issue with whatever you
-see is the single most useful contribution right now: the port is code-complete
-and the builds are ad-hoc signed, but nobody has run one on real Apple hardware. Custom coats and desk setups
-belong in [Discussions](https://github.com/JOhnsonKC201/pixelpets/discussions),
-and release history lives in the [changelog](CHANGELOG.md).
+[contributing guide](CONTRIBUTING.md); the [security policy](SECURITY.md) covers
+reporting a vulnerability privately.
+
+If you own a Mac, running the
+[beta checklist](docs/development.md#macos-beta-checklist) and opening an issue
+with whatever you see is the single most useful contribution right now: the port
+is code-complete and the builds are ad-hoc signed, but nobody has run one on real
+Apple hardware.
+
+Custom coats and desk setups belong in
+[Discussions](https://github.com/JOhnsonKC201/pixelpets/discussions), and release
+history lives in the [changelog](CHANGELOG.md).
---
diff --git a/assets/pixelcat-demo.gif b/assets/pixelcat-demo.gif
deleted file mode 100644
index 526aef3..0000000
Binary files a/assets/pixelcat-demo.gif and /dev/null differ
diff --git a/assets/pixelcat-demo.mp4 b/assets/pixelcat-demo.mp4
deleted file mode 100644
index 5f2104c..0000000
Binary files a/assets/pixelcat-demo.mp4 and /dev/null differ
diff --git a/assets/pixelcat.ico b/assets/pixelcat.ico
deleted file mode 100644
index 357904c..0000000
Binary files a/assets/pixelcat.ico and /dev/null differ
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 0000000..bde69d1
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,25 @@
+# pixelpets documentation
+
+The [README](../README.md) is the tour. These are the details it links out to.
+
+| Document | What is in it |
+|---|---|
+| [features.md](features.md) | Every interaction, coat, mood, sound, and productivity feature |
+| [custom-coats.md](custom-coats.md) | Designing, hand-editing, and sharing your own coat |
+| [architecture.md](architecture.md) | How one sprite covers 15 coats, and the project layout |
+| [development.md](development.md) | Running from source, building installers, visual QA |
+| [frame-pack.md](frame-pack.md) | Painting a pose by hand and importing it |
+| [reference-art/](reference-art/) | The original hand-drawn coat studies, kept as history |
+
+Elsewhere in the repo:
+
+| Location | What is in it |
+|---|---|
+| [`integrations/`](../integrations/) | Agent hook configs: Claude Code, Codex, Cursor, Antigravity, Kiro |
+| [`scripts/`](../scripts/) | What each script does and which are safe to move |
+| [`assets/climb/`](../assets/climb/) | Adding hand-painted rope-climb frames for a coat |
+| [`tools/ipad-terminal/`](../tools/ipad-terminal/) | Driving this machine from an iPad browser |
+| [`tools/lobby-jam/`](../tools/lobby-jam/) | The lo-fi jam generator as a standalone page |
+| [CONTRIBUTING.md](../CONTRIBUTING.md) | How to open a good PR here |
+| [SECURITY.md](../SECURITY.md) | What the app touches, and reporting a vulnerability |
+| [CHANGELOG.md](../CHANGELOG.md) | Release history |
diff --git a/docs/architecture.md b/docs/architecture.md
new file mode 100644
index 0000000..19348c4
--- /dev/null
+++ b/docs/architecture.md
@@ -0,0 +1,94 @@
+# How it works
+
+Why one `sit` pose covers fifteen coats and two species, and why the repo ships
+no sprite sheets and no audio files.
+
+## One sprite, recolored
+
+The cat is one role-coded sprite (outline, coat, markings, white, patch, eye,
+nose, inner ear) built procedurally, then recolored per pattern at draw time, so
+a dozen cats come from one shape.
+
+Every pose is composed into that same grid, including the limbs. A raised paw is
+made of the same cells as the rest of the pet, so it picks up the coat's shading,
+outline halo, markings and breathing scale for free. Nothing is painted on top
+afterwards. That is why washing, pondering, batting a leaf, boxing at the
+butterfly and swiping at the scroll leaf all work in all 15 coats and both
+species without a single extra sprite asset.
+
+Poses that vary continuously (how high a paw is raised, how far it reaches, which
+paw is mid-swipe) are quantised to a handful of steps and memoised, and built
+only for the coat currently on screen. Pixel art wants stepped limbs anyway, so
+the cheap thing and the right-looking thing are the same thing.
+
+## Rendering
+
+Rendering happens on an HTML canvas in a full-screen transparent Electron window.
+Ordinary screenshots cannot capture it (it is GPU-composited), so previews use a
+self-capture:
+
+```powershell
+electron . --shot --pattern= [--species=cat|dog] [--state=] [--at=]
+```
+
+The mochi drag is a spring system (a pinned handle plus a trailing body point)
+with a three-band stretch that keeps the head and feet rigid.
+
+## Scheduling and sound
+
+Break timers and reminders are scheduled in the main process (the renderer
+throttles and pauses when idle), which pushes fire events to the cat over IPC.
+
+Sound is synthesized with Web Audio in the renderer, so there are no audio assets
+to ship: every meow, bark, purr, pant, swipe and thud is generated live, and so
+is the lo-fi jam.
+
+## Project layout
+
+```
+pixelpets/
+ src/
+ main.js # overlay window, global input hooks, tray + menu,
+ # scheduler (breaks, reminders, Pomodoro), config IPC
+ renderer.js # the pet: sprites, palettes, physics, reactions, sound
+ preload.js # safe IPC bridge for the overlay
+ cat-sprite.js # the role-coded cat, mirrored to site/ for the browser demo
+ dog-sprite.js # the Black Lab, and the poses composed from it
+ pets.js # per-species registry: menu labels, IPC channels, copy
+ patterns.js # shared coat list, recolored at draw time
+ art-frames.js # baked hand-painted poses that win over the composer
+ climb-frames.js # the yarn-rope climb, per coat
+ audio.js # synthesized voice: meow, purr, bark, pant, chirp
+ jam.js # the live lo-fi jam, generated bar by bar
+ effects.js / bubble.js # hearts and leaves, and the speech bubble
+ focus.js # when to leave you alone: meetings, work mode, quiet hours
+ quiet-hours.js # the nightly do-not-disturb window
+ mail.js / mail-worker.js # IMAP unread-mail checks (isolated worker)
+ cal.js / cal-worker.js # .ics calendar feed (isolated worker)
+ config.js # settings.json load/save/normalize (per-user app data)
+ datadir.js # one-time rescue of that folder after the rename
+ themes.js # custom-coat load/validate
+ template.js # message placeholders: {name}, {time}, {date}, {count}
+ index.html # the overlay page
+ settings*.{html,js} # settings window + its IPC bridge
+
+ tests/ # node:test suites, no Electron and no GPU required
+ scripts/ # the vm harness, icon and demo generators, notify.js,
+ # install-hook.js, the boot check (see scripts/README.md)
+ tools/ # standalone extras: the iPad terminal, the Lobby Jam page
+ integrations/ # ready-made agent hook configs (5 agents)
+ assets/ # generated icons, showcase, hero + gallery clips
+ site/ # the browser demo deployed to Vercel
+ docs/ # this folder: features, architecture, development, frame pack
+ .github/ # CI and release workflows, issue and PR templates
+```
+
+## Entry points that are part of the public surface
+
+These paths appear in other people's config files, so they do not move:
+
+| Path | Who depends on it |
+|---|---|
+| `agent-hook.js` | every user's agent hook config (see [`integrations/`](../integrations/)) |
+| `scripts/notify.js` | scripts, CI jobs and cron entries that push a message to the pet |
+| `launch-pixelpets.vbs` | Windows desktop shortcuts that start the pet without a console |
diff --git a/docs/custom-coats.md b/docs/custom-coats.md
new file mode 100644
index 0000000..2d2028b
--- /dev/null
+++ b/docs/custom-coats.md
@@ -0,0 +1,52 @@
+# Custom coats
+
+Design your own coat under **Settings > Pet > Custom coats > "+ Add a custom
+coat"**: pick a name, a body build (standard, slender, stocky, or fluffy),
+optional tabby stripes, and eight colours (coat, marks, white, patch, eyes, nose,
+inner ear, outline). Your coat shows up in the Coat dropdown and the tray menu
+next to the 14 built-ins.
+
+Custom coats are built from the cat's geometry, so they apply to cats only. The
+Pet tab says so when a dog is selected.
+
+## Where they live
+
+`themes.json` in your app-data folder, which you can hand-edit too:
+
+| Platform | Path |
+|---|---|
+| Windows | `%APPDATA%/pixelpets/themes.json` |
+| macOS | `~/Library/Application Support/pixelpets/themes.json` |
+
+```json
+{ "themes": [
+ { "name": "Galaxy", "build": "fluffy", "tabby": false,
+ "coat": "#3b2f63", "mark": "#2a2147", "white": "#c9c0e8", "patch": "#7a5cc0",
+ "eye": "#7fd6ff", "nose": "#e0a0c0", "inner": "#9a7ad0", "outline": "#15101f" } ] }
+```
+
+Every colour role is required and must be a `#rrggbb` hex. Invalid themes are
+skipped rather than crashing the app.
+
+## Preview one without the overlay
+
+```powershell
+npx electron . --shot --pattern=galaxy
+```
+
+## Share them
+
+Export and Import live in the same Settings panel. They write and read a plain
+JSON file, and imported coats merge into your set by name, so sharing a coat is
+sending one small file.
+
+Great coats can become built-ins. Open a
+[Discussion](https://github.com/JOhnsonKC201/pixelpets/discussions) with the
+export and a screenshot.
+
+## Painting a pose instead
+
+Custom coats change *colour*. Changing the pet's *shape* for a given pose is a
+different job, and it has its own escape hatch: paint the pose, import it, and it
+wins over the composer for exactly the coats you name. See
+[frame-pack.md](frame-pack.md).
diff --git a/docs/development.md b/docs/development.md
new file mode 100644
index 0000000..23b5c8e
--- /dev/null
+++ b/docs/development.md
@@ -0,0 +1,137 @@
+# Development
+
+Run from source, build an installer, and check visual changes on a project whose
+output is GPU-composited and cannot be screenshotted.
+
+Requires git and Node 20 or newer.
+
+```powershell
+git clone https://github.com/JOhnsonKC201/pixelpets.git
+cd pixelpets
+npm install
+npm start
+```
+
+## Commands
+
+```powershell
+npm start # run the app
+npm test # 200+ tests: config and data migration, poses, interactions,
+ # audio, focus and quiet hours, mail and calendar, site drift
+npm run lint # eslint over src, tests and scripts (what CI runs)
+npm run test:boot # launch the real app and assert it renders a frame
+npm run check:hook # does the global input hook load on this Electron?
+npm run poses:cat # previews/cat-poses.png (every activity x every coat)
+npm run poses:dog # the same for the Black Lab
+npm run frames:import -- # import painted PNGs as baked poses
+npm run demo:all # regenerate the README media (hero, gallery, carousel)
+npm run hook -- cursor # print a path-filled agent hook config
+npm run icon # regenerate the tray + app-tile icons
+```
+
+CI runs `npm run lint`, `npm test`, and the boot check on Windows and macOS for
+every push and pull request.
+
+> [!TIP]
+> **Windows:** `npm start` leaves a console window open behind the pet.
+> `launch-pixelpets.vbs` starts the same thing silently, so it works as a desktop
+> shortcut (point one at `wscript.exe "\launch-pixelpets.vbs"`). It locates
+> the project from its own path, so moving the folder does not break it.
+
+> [!NOTE]
+> **macOS:** same commands. On first run, grant Accessibility permission (System
+> Settings > Privacy & Security > Accessibility) so your pet can react to your
+> typing and cursor. Input is only detected, never logged or sent anywhere.
+
+## Build a standalone app
+
+```powershell
+npm run pack # portable build -> dist/win-unpacked/pixelpets.exe (no installer)
+npm run dist # Windows installer -> dist/pixelpets Setup .exe
+```
+
+`pack` works out of the box. `dist` (the NSIS installer) needs permission to
+create symlinks while electron-builder unpacks its bundled signing tools: enable
+Windows Developer Mode (Settings > Privacy & security > For developers) or run
+the build once from an Administrator terminal. The native `uiohook-napi` module
+ships N-API prebuilds, so `npmRebuild` is disabled in the build config and no
+Visual Studio is needed.
+
+**macOS:** `npm run dist:mac` (on a Mac) builds a dmg and zip for Apple Silicon
+and Intel; the release workflow also builds them in CI on every version tag.
+Builds are ad-hoc signed rather than notarized, so Gatekeeper still asks before
+the first launch: open it once, then **System Settings > Privacy & Security >
+Open Anyway**. The ad-hoc seal matters for more than the warning: macOS keys the
+Accessibility grant and the Keychain entry to a code signature, so without it both
+would be thrown away on every update.
+
+### macOS beta checklist
+
+The mac port is code-complete but has not been smoke-tested on real hardware. If
+you own a Mac, running this and opening an issue with whatever you see is the
+single most useful contribution to the project right now.
+
+- [ ] The overlay shows over all apps and Spaces, including fullscreen.
+- [ ] Clicks pass through everywhere except on the pet.
+- [ ] The typing reaction works after granting Accessibility.
+- [ ] The pet rests on the Dock edge, not the menu bar.
+- [ ] The tray menu works in the menu bar.
+- [ ] Login launch works.
+
+## Visual QA
+
+The overlay is GPU-composited, so ordinary screenshots cannot capture it. Poses
+are reviewed with a one-command contact sheet instead:
+
+```powershell
+npm run poses:cat
+```
+
+That renders **every activity across every coat** into one image, bottom-aligned
+on a shared floor line so silhouettes can be compared down a column. It runs
+headlessly, with no Electron and no GPU, by loading the overlay's script stack in
+a vm (`scripts/overlay-vm.js`) and reading the pose grids back out. That is also
+what the pose tests drive, so the sheet and the suite are looking at exactly the
+same sprites.
+
+Single poses preview via:
+
+```powershell
+npx electron . --shot --state= --pattern=
+```
+
+Add `--at=` to capture an animated pose at a chosen phase (a typing key-press,
+say): `npx electron . --shot --state=typing --at=760`. Add `--note=""` to
+pin a speech bubble open in the capture, which is how bubble wrapping and
+screen-edge clamping get checked against a real font rather than only in unit
+tests: `npx electron . --shot --note="a long reminder that has to wrap"`.
+
+## Painting a pose by hand
+
+Every pose is composed in code, which is why one `sit` covers 15 coats and a new
+coat costs nine hex values instead of an art pass. The trade is that changing how
+the pet looks means editing geometry.
+
+`npm run frames:import` is the escape hatch: paint a pose against a placeholder
+palette, import it, and it wins over the composer for exactly the coats you name
+while everything else keeps composing. Five held poses can be baked (`sit`,
+`type`, `loaf`, `rear`, `hunt`); the six raised-limb activities are parameterised
+rigs whose limbs sweep through quantised frames, so a still would freeze them.
+
+Palette, naming and the checks the importer runs are in
+[frame-pack.md](frame-pack.md).
+
+## README media
+
+The hero banner, the gallery, and the coat carousel are all rendered from the
+same sprite geometry by `scripts/make-demo-gif.js`, in pure Node with no browser
+or GPU. Regenerate any one with
+`node scripts/make-demo-gif.js [mp4]`, or all three with
+`npm run demo:all`.
+
+## Tech
+
+Electron, HTML canvas, Web Audio,
+[`uiohook-napi`](https://github.com/SnosMe/uiohook-napi) (system-wide keyboard
+hook), [`imapflow`](https://github.com/postalsys/imapflow), and
+[`node-ical`](https://github.com/jens-maus/node-ical).
diff --git a/docs/features.md b/docs/features.md
new file mode 100644
index 0000000..32d26dc
--- /dev/null
+++ b/docs/features.md
@@ -0,0 +1,215 @@
+# Features
+
+Everything pixelpets does, in detail. The [README](../README.md) has the short
+version; this page is the full catalogue.
+
+- [Meet the pet](#meet-the-pet)
+- [Coats and pixel art](#coats-and-pixel-art)
+- [Moods and energy](#moods-and-energy)
+- [Sound](#sound)
+- [The overlay](#the-overlay)
+- [Stay on track](#stay-on-track)
+
+## Meet the pet
+
+The whole point: a pet that *feels* alive on your desktop. It reacts to touch,
+to your cursor, to your typing, and to its own internal mood.
+
+| Interaction | What the cat does |
+|-------------|-------------------|
+| **Drag it** | Stretches like mochi (head and feet stay solid while the body thins), then squashes and bounces back. Shake it side to side and it wobbles like jello with a startled mrrp. It stays where you drop it. |
+| **Pet its head** | Squeezes its eyes shut, wiggles, floats little hearts, and purrs. The squint holds through the whole stroke, not just when your hand stops moving. |
+| **Touch its body** | Squints just as happily, leans and arches into your hand, tail up, trilling. |
+| **Tap it** | A quick pet: happy eyes, hearts, a chirp. |
+| **Move your cursor** | The cat watches it and blinks now and then. Flick the cursor fast and it crouches, stalks, and pounces. A sudden jolt startles it: it puffs up, freezes, then bolts or creeps back. |
+| **Type in any app** | It leans onto two big keys and kneads them with its paws. Type fast enough and it overheats, turning red with steam, then cools down. |
+| **Scroll anywhere** | Coats with hand-painted climb art grab a yarn rope and haul themselves up it, hand over hand, with a ball of yarn on the floor below. Every other coat, and every dog, rears up and swipes at a leaf blowing past in the direction you are scrolling, faster the harder you flick the wheel. |
+| **Wait for a visitor** | Once in a while a butterfly flutters in. The cat tracks it, swats at it, and occasionally pounces and catches it between its paws before it flutters off. Step away from the keyboard and one comes out on its own, so the pet always has something to play with. Keep a dog instead and it starts its own game of fetch rather than waiting on a butterfly. |
+| **Leave it be** | Left alone it keeps itself busy: it bats a drifting leaf with a paw, washes its face, loafs, and its whiskers twitch. Lively without ever getting in your way. |
+| **Come back** | Return after being away and the cat notices you: happy eyes, a little heart, and a friendly chirp hello. |
+| **Give it a treat** | Pick "Give a treat" from the tray and a little fish drops in. The cat trots over and noms it with hearts and a happy chirp. |
+| **Late at night** | After 23:00 the cat winds down. It settles to calm faster and loafs or dozes more, though a nudge still rouses it. |
+
+### Cat or dog
+
+Pick your species from the tray (**Pet > Cat / Dog**). Each one keeps its own
+coat choice, so switching back and forth never loses your pick.
+
+| | Cat | Dog |
+|---|---|---|
+| Coats | 14 coats, from Orange Tabby to Russian Blue | the Black Lab |
+| Resting | loafs into a "cat bread" | curls nose-to-tail into a ring |
+| Excited | hunting crouch, ears back | **play bow**: chest down, rump up, tail flagged |
+| Tail | slow rolling S-curve, tip flicks | fast wag from the base, shaped per breed (curl / plume / feather / stub / straight) |
+| Play | bats a butterfly | **fetch**: chases the ball down, carries it home, drops it |
+| Left alone | a butterfly flutters in | starts its own game of fetch |
+| After exertion | grooms | pants, tongue out |
+| Scroll | climbs a yarn rope, or swipes at a blowing leaf, depending on the coat | rears into a beg and swipes at the leaf |
+| Reward | a fish treat | a tennis ball |
+
+The dog is not a recoloured cat. It has its own sprite module with a muzzle that
+protrudes past the skull line, floppy ears, a broader chest, and a straight
+otter tail. Markings are coat *structures* rather than palette swaps, and the
+build archetypes behind them (spotted, saddled, tricolour, masked, merle, and
+the short-legged dwarf silhouette) are still in the sprite module for any breed
+added back later.
+
+```bash
+npm run poses:dog # previews/dog-poses.png - every breed x every ACTIVITY
+npm run poses:cat # previews/cat-poses.png - 14 coats x 11 activities
+npm run sheet:dog # previews/dog-sheet.png - the five base poses only
+```
+
+## Coats and pixel art
+
+- 14 coat patterns: Orange, Mackerel, and Brown tabby, Siamese, Tuxedo, Black,
+ Gray, White, Cream, Tortoiseshell, Calico, Slate, Chocolate (a solid warm-brown
+ Havana with green eyes), and Russian Blue (cool blue-grey with green eyes). It
+ ships as Mackerel Tabby; right-click the cat to cycle, and your choice is
+ remembered.
+- Custom coats: design your own, documented in [custom-coats.md](custom-coats.md).
+- The pixel art has a white sticker outline that pops on any wallpaper, soft
+ top-lit shading, whiskers, a ground shadow, and sparkly eyes.
+- The cat is one role-coded sprite recolored per pattern at draw time, so a
+ dozen cats come from one shape, and shading, outline, and the overheat tint
+ apply to every coat for free.
+
+## Moods and energy
+
+The cat tracks an internal energy value (0 to 100) that decays over time and is
+bumped by stimuli: typing, scrolling, fast-mouse play, petting, an AI agent
+finishing. Energy maps to three mood bands that gate and scale every reaction:
+
+| Band | Energy | Behaviour |
+|------|--------|-----------|
+| **Calm** | 0-50 | Mellow, with small and infrequent idle moves (loafs, grooms) |
+| **Playful** | 51-80 | Full reactions (the original feel) |
+| **Zoomies** | 81-100 | Frantic and fast, then a hard crash back toward calm |
+
+Keep it busy and it gets the zoomies, then settles back down. Startle fires on
+an abrupt cursor jump (no microphone, fully local). The whole system turns off
+with the Mood reactions toggle (Settings or tray) for the classic always-playful
+behaviour, and the tray Mood submenu has "Zoomies!" and "Calm down" to drive the
+model on demand.
+
+## Sound
+
+The meow is a voiced sawtooth shaped by a moving mouth formant (it opens into
+the "ee" and closes through the "ow"), with a breath of air on the onset and
+gentle vibrato. Each meow randomly comes out as a short mew, a two-syllable
+meow, or a drawn-out meeow, so it never sounds canned, and pitch and length
+also vary by cat species. There is also a purr, a rolled chirrup trill (the
+flutter cats greet you with), and a startled mrrp. All of it is synthesized
+with Web Audio in code, so there are no audio assets to ship. Toggle it in
+Settings.
+
+## The overlay
+
+A full-screen, transparent, click-through layer: the cat floats over everything
+but only the cat itself is interactive. It stays on top of every app (it
+re-asserts top-most, even over fullscreen windows), and you can confine it to a
+play area by picking a tray preset or drawing one with the mouse (tray > Set
+play area). It starts at login by registering itself in Windows startup.
+
+## Stay on track
+
+pixelpets doubles as a quiet productivity companion. Every alert comes through
+your pet, as a meow or a bark and a speech bubble, with an optional real
+desktop notification.
+
+### Focus Guard
+
+A desktop pet that meows into the middle of a screen-share is a desktop pet you
+uninstall. Focus Guard is the pet noticing you are busy without being told.
+
+It reads "busy" from a calendar event that is actually happening now (an all-day
+block is explicitly not a meeting, so *Vacation* cannot silence it for a whole
+day), from Quiet Hours, or from Work mode. While you are busy the pet parks in
+its rest corner and stops chasing things, and email, reminders and agent
+messages **wait** rather than interrupt.
+
+Nothing is thrown away. Held messages still land in the tray recap as they
+arrive, and when you are free the pet sums them up in one line: *"While you were
+busy: 3 new emails and 1 reminder."* Calendar nudges always come through,
+because being told about the meeting you are about to miss is the opposite of an
+interruption, and you can name senders under **Always tell me about** so the
+people who matter reach you anyway.
+
+### Quiet hours
+
+Set a From and To time and the pet goes silent between them: no meow, no desktop
+notification. The speech bubble still appears, so a reminder that lands at 3am is
+waiting for you in the morning rather than lost.
+
+### Timers
+
+- Break timer: pick an interval and the cat grows big to stretch with you and
+ meows, on a schedule. Or "Start break now."
+- Pomodoro timer: set focus and break loops, and a pixel timer floats next to the
+ cat (a tomato dot for focus, green for break). At the end of each focus block
+ the cat stretches with you; when the break ends it meows "Back to focus!".
+ Toggle in Settings or the tray.
+
+### Lobby Jam
+
+Flip on Lobby Jam (Settings or the tray submenu) and the cat picks up a little
+guitar and plays an endlessly improvising lo-fi loop: plucked Karplus-Strong
+guitar over lazy jazz voicings, soft bass, brushed percussion, and tape warmth.
+It is all Web Audio, generated live, with no audio files. Pick a mood:
+
+| Mood | For |
+|------|-----|
+| **Cozy cafe** | A warm, easy background loop |
+| **Dreamy** | Slow and washed out, with lots of reverb |
+| **Upbeat lounge** | Brighter and a touch faster |
+| **Deep focus** | Steady and minimal, almost no flourishes, so it stays out of the way while you work |
+| **Rainy study** | A cozy loop over a soft, gusting rain bed |
+| **Sleepy night** | Very slow, warm, and dark, for late-night wind-down |
+
+The music mixes through the same Volume control as the meow and purr, and the
+floating notes and the cat's strumming bob along in time with the beat. The same
+generator also runs as a standalone page, with no Electron, in
+[`tools/lobby-jam/`](../tools/lobby-jam/).
+
+### Reminders and notes
+
+- Reminders: set a time and a message and the cat meows and shows a speech
+ bubble. Reminders can repeat (daily, weekdays, specific weekdays, or once), can
+ be snoozed from the tray, and support `{name}`, `{time}`, and `{date}`
+ placeholders.
+- Pinned note: pin an important message and it stays in a bubble above the cat's
+ head until you clear it. Reminders briefly take over, then it returns.
+- It calls you by name: tell the cat your name in Settings (or use `{name}`) and
+ it greets you with it.
+- Desktop alerts: every reminder can also raise a real Windows notification and a
+ sound, so you never miss one when you are not looking at the cat.
+
+### Mail and calendar
+
+- Unread-mail alerts: point the cat at your IMAP inbox (Gmail, Outlook, anything
+ IMAP) and it tells you who the mail is from, as *"Alice: Budget review"*, so
+ you can decide from the bubble instead of going to look. Name the senders you
+ never want to miss under **Always tell me about**, and they reach you even
+ while Focus Guard is holding everything else back; a bare domain like
+ `@acme.com` covers everyone there. Your app password is stored encrypted at
+ rest (Electron `safeStorage`, which is DPAPI on Windows), never in
+ `settings.json`, and the IMAP connection runs in an isolated worker process.
+- Calendar nudges: paste your calendar's secret `.ics` URL (Google and Outlook
+ both provide one) and the cat nudges you a few minutes before each event. The
+ feed is fetched and parsed in an isolated worker.
+
+### Notify the pet from a script
+
+Any script or tool can make the cat deliver an arbitrary message: a speech
+bubble, a Windows toast, and a meow.
+
+```bash
+node scripts/notify.js "Build finished" --title CI --level success
+node scripts/notify.js "Coffee break" --ttl 8000
+echo "anything" | node scripts/notify.js "Deploy done" # hook-safe (drains stdin)
+```
+
+Flags: `--title `, `--level info|success|warn|alert`, `--ttl `,
+`--no-sound`. The script appends one JSON line to `%TEMP%/pixelcat-notify.jsonl`
+and the running pet tails the file. Lines written before the pet launched are
+ignored (no backlog replay), and calling it while the pet is closed is harmless.
diff --git a/docs/frame-pack.md b/docs/frame-pack.md
index 5f0f4a1..064f8e1 100644
--- a/docs/frame-pack.md
+++ b/docs/frame-pack.md
@@ -1,8 +1,9 @@
# Frame pack: painting poses by hand
Every pose the pet holds is composed in code from grid primitives, which is why one
-`sit` covers 28 coats and why a new coat costs nine hex values instead of an art
-pass. The trade is that improving how the pet *looks* means editing geometry.
+`sit` covers 15 coats across two species, and why a new coat costs nine hex
+values instead of an art pass. The trade is that improving how the pet *looks*
+means editing geometry.
This is the escape hatch. Paint a pose, import it, and it wins over the composer
for exactly the coats you name. Everything else keeps composing, so a pack of one
diff --git a/scripts/README.md b/scripts/README.md
new file mode 100644
index 0000000..02d6089
--- /dev/null
+++ b/scripts/README.md
@@ -0,0 +1,45 @@
+# scripts/
+
+Everything here is a Node or Electron entry point. Nothing in this folder ships
+inside the app except `notify.js`, which is bundled deliberately so an installed
+copy can still be driven from a shell.
+
+## Public surface (do not move or rename)
+
+These paths appear in other people's config files and shortcuts.
+
+| Script | Run via | What it does |
+|---|---|---|
+| `notify.js` | `node scripts/notify.js "msg"` | Pushes a message to the running pet: speech bubble, toast, meow. Hook-safe (drains stdin). |
+| `install-hook.js` | `npm run hook -- ` | Prints an agent hook config with the absolute path already filled in. |
+
+## Build and release
+
+| Script | Run via | What it does |
+|---|---|---|
+| `adhoc-sign.js` | electron-builder `afterPack` | Ad-hoc signs the macOS bundle. Without the seal, macOS discards the Accessibility grant and Keychain entry on every update. |
+| `bootcheck.js` | `npm run test:boot` | Launches the real app and asserts it renders a frame. Runs in CI on Windows and macOS. |
+| `hook-check.js` | `npm run check:hook` | Answers whether the global input hook loads on this Electron build. |
+
+## Art and media generators
+
+All of these render from the same sprite geometry the app draws with, in pure
+Node, with no browser and no GPU.
+
+| Script | Run via | Output |
+|---|---|---|
+| `make-demo-gif.js` | `npm run demo:all` | `assets/hero-banner.*`, `assets/gallery/*`, `assets/coat-carousel.gif`. The `demo` recipe is legacy and is not part of `all`. |
+| `make-reel.js` | `npm run reel` | `assets/pixelpets-reel.mp4`, the launch reel. Films the real renderer. |
+| `make-social-card.js` | `npm run social` | `assets/social-card.png`, the GitHub and link-preview card. |
+| `make-logo-icons.js` | `npm run icon` | Every app and tray icon, from the master `assets/logo.png`. |
+| `logo-source.js` | library | Loads and rescales `logo.png`, and decodes PNGs for the climb slicer. |
+
+## Sprite tooling
+
+| Script | Run via | What it does |
+|---|---|---|
+| `overlay-vm.js` | library | Loads the overlay's script stack in a vm so poses can be read back headlessly. The pose tests and the contact sheets both drive this. |
+| `pet-sheet.js` | `npm run poses:cat` / `poses:dog` | Renders every activity across every coat into one contact sheet for visual QA. |
+| `import-frames.js` | `npm run frames:import -- ` | Imports hand-painted PNGs as baked poses that win over the composer. See [docs/frame-pack.md](../docs/frame-pack.md). |
+| `slice-climb-sheet.js` | `node scripts/slice-climb-sheet.js ` | Slices a 5-panel climb contact sheet into per-coat frames. See [assets/climb/](../assets/climb/). |
+| `embed-climb-frames.js` | `npm run climb-frames` | Bakes those frames into `src/climb-frames.js`. |
diff --git a/scripts/logo-glyph.js b/scripts/logo-glyph.js
deleted file mode 100644
index 9c3c53b..0000000
--- a/scripts/logo-glyph.js
+++ /dev/null
@@ -1,95 +0,0 @@
-// Shared "pixelcat" logo mark - a bold, friendly FRONT-FACING cat face that reads
-// at 16px and scales up crisply. Brand orange on a dark outline + light halo so it
-// stays visible on a dark Windows taskbar (the old dark cat-head vanished there).
-//
-// renderCatFace(S, { halo }) returns an S*S RGBA buffer (transparent background).
-// The face geometry is normalized (0..1), classified per pixel with supersampling
-// for clean edges, then outline/halo are grown around the silhouette. Used by both
-// scripts/make-tray-icon.js (tray) and scripts/make-app-icon.js (app tile).
-
-const COL = {
- 1: [232, 148, 60], // coat - brand orange
- 2: [181, 100, 29], // mark - tabby stripes
- 3: [247, 241, 230], // cream - muzzle / chin
- 4: [240, 170, 165], // earin - inner ear
- 5: [38, 40, 48], // eye
- 6: [255, 255, 255], // shine - eye sparkle
- 7: [224, 136, 143], // nose
- 8: [60, 36, 18], // line - dark outline
- 9: [255, 252, 246], // halo - light contrast ring
-};
-
-function sign(px, py, ax, ay, bx, by) { return (px - bx) * (ay - by) - (ax - bx) * (py - by); }
-function inTri(px, py, a, b, c) {
- const d1 = sign(px, py, a[0], a[1], b[0], b[1]);
- const d2 = sign(px, py, b[0], b[1], c[0], c[1]);
- const d3 = sign(px, py, c[0], c[1], a[0], a[1]);
- return !((d1 < 0 || d2 < 0 || d3 < 0) && (d1 > 0 || d2 > 0 || d3 > 0));
-}
-
-// Classify a normalized point (nx, ny in 0..1) into a role code, or 0 (empty).
-function classify(nx, ny) {
- // eyes (+ a bright sparkle in the upper-right of each)
- for (const ex of [0.355, 0.645]) {
- const dx = (nx - ex) / 0.090, dy = (ny - 0.55) / 0.125;
- if (dx * dx + dy * dy <= 1) {
- const sx = (nx - (ex + 0.028)) / 0.028, sy = (ny - (0.55 - 0.040)) / 0.028;
- return (sx * sx + sy * sy <= 1) ? 6 : 5;
- }
- }
- // nose
- { const dx = (nx - 0.5) / 0.045, dy = (ny - 0.665) / 0.036; if (dx * dx + dy * dy <= 1) return 7; }
- // inner ears
- if (inTri(nx, ny, [0.215, 0.15], [0.15, 0.37], [0.33, 0.30])) return 4;
- if (inTri(nx, ny, [0.785, 0.15], [0.85, 0.37], [0.67, 0.30])) return 4;
- const inHead = ((nx - 0.5) / 0.40) ** 2 + ((ny - 0.57) / 0.36) ** 2 <= 1;
- // cream muzzle / chin (lower-centre of the head)
- if (inHead) { const dx = (nx - 0.5) / 0.185, dy = (ny - 0.72) / 0.125; if (dx * dx + dy * dy <= 1) return 3; }
- // forehead "M" tabby stripes on the coat
- if (inHead && ny > 0.27 && ny < 0.40) { for (const mx of [0.415, 0.50, 0.585]) if (Math.abs(nx - mx) < 0.017) return 2; }
- const inEarL = inTri(nx, ny, [0.20, 0.05], [0.07, 0.42], [0.40, 0.30]);
- const inEarR = inTri(nx, ny, [0.80, 0.05], [0.93, 0.42], [0.60, 0.30]);
- return (inHead || inEarL || inEarR) ? 1 : 0;
-}
-
-function renderCatFace(S, opts = {}) {
- const halo = opts.halo !== false;
- const ss = S <= 64 ? 4 : 2, W = S * ss;
- const role = new Int8Array(W * W);
- for (let y = 0; y < W; y++) for (let x = 0; x < W; x++) role[y * W + x] = classify((x + 0.5) / W, (y + 0.5) / W);
- // grow a dark outline (ss thick) around the silhouette, then a light halo
- const grow = (from, to, steps) => {
- for (let s = 0; s < steps; s++) {
- const add = [];
- for (let y = 0; y < W; y++) for (let x = 0; x < W; x++) {
- if (role[y * W + x] !== 0) continue;
- const n = (xx, yy) => xx >= 0 && yy >= 0 && xx < W && yy < W && role[yy * W + xx] >= from && role[yy * W + xx] <= to;
- if (n(x - 1, y) || n(x + 1, y) || n(x, y - 1) || n(x, y + 1)) add.push(y * W + x);
- }
- for (const i of add) role[i] = to + 1; // 8 (line) when growing 1..7, 9 (halo) when growing 1..8
- }
- };
- grow(1, 7, ss); // outline
- if (halo) grow(1, 8, ss); // halo ring (only useful on transparent/dark backgrounds)
- // hi-res RGBA, then box-downsample (premultiplied) to S for clean alpha edges
- const hi = new Uint8ClampedArray(W * W * 4);
- for (let i = 0; i < W * W; i++) { const c = COL[role[i]]; if (!c) continue; hi[i * 4] = c[0]; hi[i * 4 + 1] = c[1]; hi[i * 4 + 2] = c[2]; hi[i * 4 + 3] = 255; }
- return downsample(hi, W, S);
-}
-
-function downsample(src, srcSize, size) {
- if (size === srcSize) return src;
- const out = new Uint8ClampedArray(size * size * 4), ratio = srcSize / size;
- for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) {
- let r = 0, g = 0, b = 0, a = 0, n = 0;
- for (let sy = 0; sy < ratio; sy++) for (let sx = 0; sx < ratio; sx++) {
- const px = Math.floor(x * ratio + sx), py = Math.floor(y * ratio + sy), i = (py * srcSize + px) * 4, af = src[i + 3] / 255;
- r += src[i] * af; g += src[i + 1] * af; b += src[i + 2] * af; a += src[i + 3]; n++;
- }
- const o = (y * size + x) * 4, av = a / n;
- out[o] = av ? r / n / (av / 255) : 0; out[o + 1] = av ? g / n / (av / 255) : 0; out[o + 2] = av ? b / n / (av / 255) : 0; out[o + 3] = av;
- }
- return out;
-}
-
-module.exports = { renderCatFace, COL };
diff --git a/scripts/make-app-icon.js b/scripts/make-app-icon.js
deleted file mode 100644
index 097efc3..0000000
--- a/scripts/make-app-icon.js
+++ /dev/null
@@ -1,51 +0,0 @@
-// Generates assets/icon.png (256) and assets/icon.ico (16/32/48/64/128/256) - the
-// "pixelcat" logo: a bold ORANGE cat face (the shared logo mark) centred on a vibrant
-// indigo rounded app-tile. The same face is used for the tray glyph (make-tray-icon.js)
-// so the brand is consistent and instantly recognizable. Run: node scripts/make-app-icon.js
-const fs = require('fs');
-const zlib = require('zlib');
-const path = require('path');
-const { renderCatFace } = require('./logo-glyph.js');
-
-const TILE_TOP = [126, 86, 214], TILE_BOT = [56, 38, 138]; // vibrant indigo gradient
-const lerp = (a, b, t) => Math.round(a + (b - a) * t);
-
-function render(SZ) {
- const buf = new Uint8ClampedArray(SZ * SZ * 4);
- const radius = Math.round(SZ * 0.22);
- for (let y = 0; y < SZ; y++) for (let x = 0; x < SZ; x++) { // rounded gradient tile
- const rx = Math.max(0, radius - x, x - (SZ - 1 - radius)), ry = Math.max(0, radius - y, y - (SZ - 1 - radius));
- if (rx > 0 && ry > 0 && rx * rx + ry * ry > radius * radius) continue;
- const i = (y * SZ + x) * 4;
- buf[i] = lerp(TILE_TOP[0], TILE_BOT[0], y / SZ); buf[i + 1] = lerp(TILE_TOP[1], TILE_BOT[1], y / SZ); buf[i + 2] = lerp(TILE_TOP[2], TILE_BOT[2], y / SZ); buf[i + 3] = 255;
- }
- const fSz = Math.round(SZ * 0.80), face = renderCatFace(fSz, { halo: false }); // cat face centred (~80%)
- const off = Math.floor((SZ - fSz) / 2);
- for (let y = 0; y < fSz; y++) for (let x = 0; x < fSz; x++) { // alpha-over composite
- const fi = (y * fSz + x) * 4, a = face[fi + 3] / 255; if (!a) continue;
- const X = off + x, Y = off + y; if (X < 0 || Y < 0 || X >= SZ || Y >= SZ) continue;
- const i = (Y * SZ + X) * 4;
- buf[i] = Math.round(face[fi] * a + buf[i] * (1 - a)); buf[i + 1] = Math.round(face[fi + 1] * a + buf[i + 1] * (1 - a)); buf[i + 2] = Math.round(face[fi + 2] * a + buf[i + 2] * (1 - a)); buf[i + 3] = 255;
- }
- return buf;
-}
-function downsample(src, srcSize, size) {
- if (size === srcSize) return src;
- const out = new Uint8ClampedArray(size * size * 4), ratio = srcSize / size;
- for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) { let r = 0, g = 0, b = 0, a = 0, n = 0; for (let sy = 0; sy < ratio; sy++) for (let sx = 0; sx < ratio; sx++) { const pxn = Math.floor(x * ratio + sx), py = Math.floor(y * ratio + sy), i = (py * srcSize + pxn) * 4, af = src[i + 3] / 255; r += src[i] * af; g += src[i + 1] * af; b += src[i + 2] * af; a += src[i + 3]; n++; } const o = (y * size + x) * 4, av = a / n; out[o] = av ? r / n / (av / 255) : 0; out[o + 1] = av ? g / n / (av / 255) : 0; out[o + 2] = av ? b / n / (av / 255) : 0; out[o + 3] = av; }
- return out;
-}
-function crc(b) { let c = ~0; for (let i = 0; i < b.length; i++) { c ^= b[i]; for (let k = 0; k < 8; k++) c = (c >>> 1) ^ (0xEDB88320 & -(c & 1)); } return ~c >>> 0; }
-function chunk(t, d) { const l = Buffer.alloc(4); l.writeUInt32BE(d.length, 0); const b = Buffer.concat([Buffer.from(t), d]); const cc = Buffer.alloc(4); cc.writeUInt32BE(crc(b), 0); return Buffer.concat([l, b, cc]); }
-function encodePng(rgba, s) { const ih = Buffer.alloc(13); ih.writeUInt32BE(s, 0); ih.writeUInt32BE(s, 4); ih[8] = 8; ih[9] = 6; const raw = Buffer.alloc(s * (s * 4 + 1)); for (let y = 0; y < s; y++) { raw[y * (s * 4 + 1)] = 0; for (let x = 0; x < s * 4; x++) raw[y * (s * 4 + 1) + 1 + x] = rgba[y * s * 4 + x]; } return Buffer.concat([Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]), chunk('IHDR', ih), chunk('IDAT', zlib.deflateSync(raw, { level: 9 })), chunk('IEND', Buffer.alloc(0))]); }
-function buildIco(pngs) { const head = Buffer.alloc(6); head.writeUInt16LE(0, 0); head.writeUInt16LE(1, 2); head.writeUInt16LE(pngs.length, 4); let off = 6 + 16 * pngs.length; const e = pngs.map((p) => { const b = Buffer.alloc(16); b.writeUInt8(p.size >= 256 ? 0 : p.size, 0); b.writeUInt8(p.size >= 256 ? 0 : p.size, 1); b.writeUInt16LE(1, 4); b.writeUInt16LE(32, 6); b.writeUInt32LE(p.data.length, 8); b.writeUInt32LE(off, 12); off += p.data.length; return b; }); return Buffer.concat([head, ...e, ...pngs.map((p) => p.data)]); }
-
-const hi = render(512);
-const big = render(256);
-const outDir = path.join(__dirname, '..', 'assets');
-fs.mkdirSync(outDir, { recursive: true });
-const pngs = [256, 128, 64, 48, 32, 16].map((s) => ({ size: s, data: encodePng(s === 256 ? big : downsample(hi, 512, s), s) }));
-fs.writeFileSync(path.join(outDir, 'icon.png'), pngs[0].data);
-fs.writeFileSync(path.join(outDir, 'icon.ico'), buildIco(pngs));
-fs.writeFileSync(path.join(outDir, 'icon-512.png'), encodePng(hi, 512)); // macOS app icon (electron-builder needs >= 512px)
-console.log('wrote assets/icon.png (256), assets/icon.ico (16-256), and assets/icon-512.png (mac)');
diff --git a/scripts/make-demo-gif.js b/scripts/make-demo-gif.js
index 2a3796b..c29ef11 100644
--- a/scripts/make-demo-gif.js
+++ b/scripts/make-demo-gif.js
@@ -1,10 +1,11 @@
-// Generates assets/pixelcat-demo.gif - a short looping clip that shows what the
-// desktop pet DOES: it sits and follows your cursor, naps when idle, taps its
-// paws when you type, pounces to hunt, and purrs when you pet it. Pure JS, no
-// browser / no native deps: the sprite geometry comes from src/cat-sprite.js
-// (single source of truth) plus the pose composers copied from renderer.js, all
-// rasterised by hand into RGBA frames and encoded with gifenc.
-// Run: node scripts/make-demo-gif.js
+// Renders the README media - short looping clips that show what the desktop pet
+// DOES: it sits and follows your cursor, naps when idle, taps its paws when you
+// type, pounces to hunt, and purrs when you pet it. Pure JS, no browser / no
+// native deps: the sprite geometry comes from src/cat-sprite.js (single source of
+// truth) plus the pose composers copied from renderer.js, all rasterised by hand
+// into RGBA frames and encoded with gifenc.
+// Run: node scripts/make-demo-gif.js [demo|hero|gallery|carousel|all] [mp4]
+// npm run demo:all # the three the README embeds
const fs = require('fs');
const path = require('path');
const { GIFEncoder, quantize, applyPalette } = require('gifenc');
@@ -652,7 +653,8 @@ function maybeDump(frames, tag) {
// ---- recipes ----------------------------------------------------------------
function setCanvas(w, h, px, baseY) { W = w; H = h; PX = px; BASE_Y = baseY; }
-// the original 5-scene desktop demo (480x340) -> assets/pixelcat-demo.{gif,mp4}
+// the original 5-scene desktop demo (480x340) -> assets/pixelpets-demo.{gif,mp4}.
+// Not README media (the hero, gallery and carousel below are), so `all` leaves it out.
function recipeDemo(wantMp4) {
setCanvas(480, 340, 6, 250); SP = buildPoses(0); PAL = paletteFor(S.PATTERNS[0]);
const frames = [];
@@ -662,8 +664,8 @@ function recipeDemo(wantMp4) {
sceneHunt(frames, { caption: 'POUNCES TO HUNT' });
scenePet(frames, { caption: 'PURRS WHEN YOU PET IT' });
maybeDump(frames, 'demo');
- encodeGif(frames, A('pixelcat-demo.gif'));
- if (wantMp4) encodeMp4(frames, A('pixelcat-demo.mp4'));
+ encodeGif(frames, A('pixelpets-demo.gif'));
+ if (wantMp4) encodeMp4(frames, A('pixelpets-demo.mp4'));
}
// wide cinematic banner (960x360) -> assets/hero-banner.{gif,mp4}
@@ -730,7 +732,7 @@ const RECIPES = {
hero: () => recipeHero(wantMp4),
gallery: () => recipeGallery(wantMp4),
carousel: () => recipeCarousel(),
- all: () => { recipeDemo(wantMp4); recipeHero(wantMp4); recipeGallery(wantMp4); recipeCarousel(); },
+ all: () => { recipeHero(wantMp4); recipeGallery(wantMp4); recipeCarousel(); },
};
if (!RECIPES[recipe]) { console.error(`unknown recipe: ${recipe} (expected demo|hero|gallery|carousel|all)`); process.exit(1); }
RECIPES[recipe]();
diff --git a/scripts/make-ico.js b/scripts/make-ico.js
deleted file mode 100644
index b297498..0000000
--- a/scripts/make-ico.js
+++ /dev/null
@@ -1,30 +0,0 @@
-// Bundle the generated tray PNGs into assets/pixelcat.ico (PNG-embedded ICO,
-// supported on modern Windows). Run after make-tray-icon.js.
-const fs = require('fs');
-const path = require('path');
-
-const dir = path.join(__dirname, '..', 'assets');
-const imgs = [
- { size: 16, file: 'tray.png' },
- { size: 32, file: 'tray@2x.png' },
-].map((e) => ({ size: e.size, data: fs.readFileSync(path.join(dir, e.file)) }));
-
-const HEADER = 6, ENTRY = 16;
-const head = Buffer.alloc(HEADER);
-head.writeUInt16LE(0, 0); head.writeUInt16LE(1, 2); head.writeUInt16LE(imgs.length, 4);
-
-let offset = HEADER + ENTRY * imgs.length;
-const entries = [];
-for (const im of imgs) {
- const e = Buffer.alloc(ENTRY);
- e.writeUInt8(im.size === 256 ? 0 : im.size, 0);
- e.writeUInt8(im.size === 256 ? 0 : im.size, 1);
- e.writeUInt8(0, 2); e.writeUInt8(0, 3);
- e.writeUInt16LE(1, 4); e.writeUInt16LE(32, 6);
- e.writeUInt32LE(im.data.length, 8);
- e.writeUInt32LE(offset, 12);
- offset += im.data.length;
- entries.push(e);
-}
-fs.writeFileSync(path.join(dir, 'pixelcat.ico'), Buffer.concat([head, ...entries, ...imgs.map((i) => i.data)]));
-console.log('wrote assets/pixelcat.ico');
diff --git a/scripts/make-logo-icons.js b/scripts/make-logo-icons.js
index 45e1e89..81fd88d 100644
--- a/scripts/make-logo-icons.js
+++ b/scripts/make-logo-icons.js
@@ -163,11 +163,10 @@ fs.writeFileSync(path.join(outDir, 'icon-512.png'), encodePng(onTile(512), 512))
// Transparent 512 mascot mark for the README hero (no tile).
fs.writeFileSync(path.join(outDir, 'logo-mark.png'), encodePng(rgbaAt(512), 512));
-// Tray glyph (16 + retina 32) and the legacy tray .ico - on the same warm tile as the app
-// icon so the dark mascot is clearly visible on a dark system tray.
+// Tray glyph (16 + retina 32) - on the same warm tile as the app icon so the dark
+// mascot is clearly visible on a dark system tray.
fs.writeFileSync(path.join(outDir, 'tray.png'), encodePng(onTile(16), 16));
fs.writeFileSync(path.join(outDir, 'tray@2x.png'), encodePng(onTile(32), 32));
-fs.writeFileSync(path.join(outDir, 'pixelcat.ico'), buildIco([32, 16].map(tileEntry)));
// macOS menu-bar glyph. A template image is pure BLACK plus an alpha channel: macOS
// reads only the alpha and paints it itself, which is what lets it invert on a dark
diff --git a/scripts/make-tray-icon.js b/scripts/make-tray-icon.js
deleted file mode 100644
index 6207b3b..0000000
--- a/scripts/make-tray-icon.js
+++ /dev/null
@@ -1,40 +0,0 @@
-// Generates assets/tray.png (16px) and assets/tray@2x.png (32px) - the "pixelcat"
-// logo mark: a bold ORANGE cat face with a dark outline and light halo, so it stays
-// visible on a dark Windows taskbar (the previous dark cat-head was invisible there).
-// Artwork comes from the shared scripts/logo-glyph.js. Run: node scripts/make-tray-icon.js
-const fs = require('fs');
-const zlib = require('zlib');
-const path = require('path');
-const { renderCatFace } = require('./logo-glyph.js');
-
-// Minimal PNG encoder (truecolor + alpha).
-function crc32(buf) {
- let c = ~0;
- for (let i = 0; i < buf.length; i++) { c ^= buf[i]; for (let k = 0; k < 8; k++) c = (c >>> 1) ^ (0xEDB88320 & -(c & 1)); }
- return ~c >>> 0;
-}
-function chunk(type, data) {
- const len = Buffer.alloc(4); len.writeUInt32BE(data.length, 0);
- const tb = Buffer.from(type, 'ascii'), body = Buffer.concat([tb, data]);
- const crc = Buffer.alloc(4); crc.writeUInt32BE(crc32(body), 0);
- return Buffer.concat([len, body, crc]);
-}
-function encodePng(rgba, size) {
- const sig = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
- const ihdr = Buffer.alloc(13);
- ihdr.writeUInt32BE(size, 0); ihdr.writeUInt32BE(size, 4);
- ihdr[8] = 8; ihdr[9] = 6; ihdr[10] = 0; ihdr[11] = 0; ihdr[12] = 0;
- const raw = Buffer.alloc(size * (size * 4 + 1));
- for (let y = 0; y < size; y++) {
- raw[y * (size * 4 + 1)] = 0;
- for (let x = 0; x < size * 4; x++) raw[y * (size * 4 + 1) + 1 + x] = rgba[y * size * 4 + x];
- }
- const idat = zlib.deflateSync(raw, { level: 9 });
- return Buffer.concat([sig, chunk('IHDR', ihdr), chunk('IDAT', idat), chunk('IEND', Buffer.alloc(0))]);
-}
-
-const outDir = path.join(__dirname, '..', 'assets');
-fs.mkdirSync(outDir, { recursive: true });
-fs.writeFileSync(path.join(outDir, 'tray.png'), encodePng(renderCatFace(16, { halo: true }), 16));
-fs.writeFileSync(path.join(outDir, 'tray@2x.png'), encodePng(renderCatFace(32, { halo: true }), 32));
-console.log('wrote assets/tray.png (16) and assets/tray@2x.png (32)');
diff --git a/site/cat-sprite.js b/site/cat-sprite.js
index 9363f18..361f26c 100644
--- a/site/cat-sprite.js
+++ b/site/cat-sprite.js
@@ -3,7 +3,7 @@
// by the overlay (index.html) and the settings window (settings.html) - its
// top-level const/function declarations live in the shared global lexical scope,
// so renderer.js / cat-preview.js use them as bare identifiers - and required as a
-// CommonJS module by scripts/make-app-icon.js (Node). Pose-specific composers
+// CommonJS module by scripts/pet-sheet.js (Node). Pose-specific composers
// (hunt/type/sleep) and the animated drawCat stay in renderer.js.
const CELL = 4; // px per sprite cell - sets the cat's overall size (was 5; 4 ≈ 20% smaller)
@@ -163,7 +163,7 @@ function shadeStr(rgb, f) { const c = (v) => Math.max(0, Math.min(255, Math.roun
const HALO = '#fbfdff';
const BODY = new Set(['C', 'K', 'W', 'X', 'I']);
-// CommonJS export (Node, for scripts/make-app-icon.js). In a browser classic
+// CommonJS export (Node, for scripts/pet-sheet.js). In a browser classic
// script `module` is undefined, so this is skipped and the declarations above
// remain available in the shared global scope.
if (typeof module !== 'undefined' && module.exports) {
diff --git a/src/cat-sprite.js b/src/cat-sprite.js
index 9363f18..361f26c 100644
--- a/src/cat-sprite.js
+++ b/src/cat-sprite.js
@@ -3,7 +3,7 @@
// by the overlay (index.html) and the settings window (settings.html) - its
// top-level const/function declarations live in the shared global lexical scope,
// so renderer.js / cat-preview.js use them as bare identifiers - and required as a
-// CommonJS module by scripts/make-app-icon.js (Node). Pose-specific composers
+// CommonJS module by scripts/pet-sheet.js (Node). Pose-specific composers
// (hunt/type/sleep) and the animated drawCat stay in renderer.js.
const CELL = 4; // px per sprite cell - sets the cat's overall size (was 5; 4 ≈ 20% smaller)
@@ -163,7 +163,7 @@ function shadeStr(rgb, f) { const c = (v) => Math.max(0, Math.min(255, Math.roun
const HALO = '#fbfdff';
const BODY = new Set(['C', 'K', 'W', 'X', 'I']);
-// CommonJS export (Node, for scripts/make-app-icon.js). In a browser classic
+// CommonJS export (Node, for scripts/pet-sheet.js). In a browser classic
// script `module` is undefined, so this is skipped and the declarations above
// remain available in the shared global scope.
if (typeof module !== 'undefined' && module.exports) {
diff --git a/tests/bridge-paths.test.js b/tests/bridge-paths.test.js
index 7d8eac1..095fc41 100644
--- a/tests/bridge-paths.test.js
+++ b/tests/bridge-paths.test.js
@@ -64,11 +64,23 @@ test('the Windows app identity stays frozen too', () => {
'the installer appId must match the app user model id, or NSIS installs alongside the old copy');
});
-test('the README documents the same bridge paths the code uses', () => {
- // The README is how anyone sets these hooks up, so a stale path there sends people
- // to a file nothing reads.
- const readme = src('README.md');
+test('the docs quote the same bridge paths the code uses', () => {
+ // The docs are how anyone sets these hooks up, so a stale path there sends people
+ // to a file nothing reads. Which page carries which path may move as the docs are
+ // reorganised; what must not happen is a path going undocumented, or documented
+ // wrong, so this asserts over the whole user-facing set rather than one file.
+ const PAGES = ['README.md', 'docs/features.md', 'integrations/README.md'];
+ const docs = PAGES.map((p) => [p, src(p)]);
for (const name of [constTmpName('src/main.js', 'AGENT_FILE'), constTmpName('src/main.js', 'NOTIFY_FILE')]) {
- assert.ok(readme.includes(name), `README.md should document the bridge path ${name}`);
+ assert.ok(docs.some(([, text]) => text.includes(name)),
+ `the bridge path ${name} is documented nowhere in ${PAGES.join(', ')}`);
+ }
+ // A page that names the wrong file is worse than one that stays quiet.
+ const STALE = /%TEMP%\/([\w.-]+\.(?:state|jsonl))/g;
+ const live = [constTmpName('src/main.js', 'AGENT_FILE'), constTmpName('src/main.js', 'NOTIFY_FILE')];
+ for (const [page, text] of docs) {
+ for (const m of text.matchAll(STALE)) {
+ assert.ok(live.includes(m[1]), `${page} documents ${m[1]}, which nothing reads`);
+ }
}
});
diff --git a/tools/lobby-jam/README.md b/tools/lobby-jam/README.md
new file mode 100644
index 0000000..54b24b2
--- /dev/null
+++ b/tools/lobby-jam/README.md
@@ -0,0 +1,17 @@
+# Lobby Jam, standalone
+
+`index.html` is the pet's lo-fi jam generator with the pet taken out: the same
+Karplus-Strong guitar, jazz voicings, brushed percussion and tape warmth, in one
+self-contained page with no Electron, no build step and no audio files.
+
+Open it directly in a browser:
+
+```powershell
+start tools/lobby-jam/index.html # Windows
+open tools/lobby-jam/index.html # macOS
+```
+
+Useful for tuning the generator without launching the app, and for hearing what
+the moods sound like back to back. The version that ships inside pixelpets lives
+in [`src/jam.js`](../../src/jam.js) and is toggled from Settings or the tray; see
+[docs/features.md](../../docs/features.md#lobby-jam).
diff --git a/extras/lobby-jam.html b/tools/lobby-jam/index.html
similarity index 100%
rename from extras/lobby-jam.html
rename to tools/lobby-jam/index.html