# dsh-plugin-pet A desktop virtual pet for DeepSeek Harness **0.2.1+** (Web and Desktop): mood animations driven by live session state, built-in sprites, custom images, Codex-style multi-frame spritesheets, and a local companionship growth system. [中文](README.md) [![dsh-plugin topic](https://img.shields.io/badge/topic-dsh--plugin-blue)](https://github.com/topics/dsh-plugin) [![license](https://img.shields.io/badge/license-MIT-green)](LICENSE) The pet lives in the bottom-right corner and reacts to session activity (thinking / running tools / errors / completion / waiting for you). Settings are no longer an embedded panel: they are a **standalone page** (the keyed `main` slot) opened from the **account avatar menu → Pet**, using the same shell as the Gold Board and News Board pages. - **One bundle for Web and Desktop**: relative `/api` routes through the authenticated bridge, `--dsw-alias-*` theme tokens only. - **Standalone page**: fixed header, three tabs (Overview / Appearance / Growth), save actions inside the tab row, one scroll area, fixed footer. Switching tabs never moves the header. - **Avatar-menu entry**: a "Pet" item is injected into the bottom-left account popup (0.2.1 exposes no menu-item slot, so the plugin augments the rendered portal defensively; the item detects the account trigger through its `data-signed-out` marker plus zh/en aria-label fallbacks). - **14 built-in sprites** (blob / cat / duck / robot / axolotl / ghost / panda / shiba / fox / bunny / penguin / frog / owl / slime): inline SVG, pure CSS animation, blinking, and mood-driven eyes/mouths. - **Custom images** (PNG / JPEG / GIF / WebP; animated images play as-is). - **Codex spritesheets**: `pet.json` + `spritesheet.webp` (V1 1536×1872, V2 1536×2288, see [Codex pet package spec](https://codexpet.xyz/spec/)), or a generic grid (columns / rows / fps / state rows / frames per row). - **Local growth**: intimacy, level, EXP, mood distribution, achievements, daily online time and completed tasks — all stored on this machine. ## Structure | File | Role | | --- | --- | | `lib/index.js` | Host plugin: config + growth persistence, authenticated routes via `ctx.connection.fetch` | | `lib/connection-routes.js` | Dependency-free Fetch adapter: 405s, body caps, error envelopes, `Request` / `Response` translation | | `lib/client.js` | Browser bundle (hand-written factory-CJS, no build step): overlay, standalone page, account-menu entry | | `cordis.patch.yml` | Bundle-layer loader row | | `test/` | `node --test`: host Fetch routes, config migration, revisions, client slots / page render / menu augmentation | This is a **dual-face package**: the host half is a plain Cordis plugin; the client half is declared with `"dsh": { "client": { "platform": "web", "inject": [], "immediately": true } }` and the `"./client"` export, so client-modules loads it eagerly at boot. ## Compatibility - Target runtime: DeepSeek Harness **0.2.1+** (`engines.dsh: >=0.2.1-rc.2 <0.3.0-0`); the same code runs on Desktop and the Web profile. - The 1.x (DSH 1.0.7) `settingsScope` / `currentProvideInfo` / raw `webServer` routes are gone; a legacy flat `config.json` is migrated on first read. ## Install Bundle mode is recommended (`dsh.bundle` + `cordis.patch.yml` inserts the loader row): ```sh # Web (recommended bundle mode; inserts the loader row automatically) npx @deepseek-ai/dsh plugin --profile web add "github:c-ling/dsh-plugin-pet#v2.0.0" dsh plugin --profile web add "github:c-ling/dsh-plugin-pet#v2.0.0" # Desktop (managed by the Desktop app's bundled CLI or the Plugins page) dsh plugin --profile desktop add "github:c-ling/dsh-plugin-pet#v2.0.0" ``` > This package declares `dsh.bundle` + `cordis.patch.yml`, so `dsh plugin add` installs it as a > bundle layer and inserts the loader row automatically; restart the host afterwards. Manual mode installs a plain dependency and requires the same row in the profile's `cordis.patch.yml` (**never both**): ```sh cd ~/.dsh/profiles/web corepack pnpm add "github:c-ling/dsh-plugin-pet#v2.0.0" ``` ```yaml - insert: - id: dsh-plugin-pet name: 'dsh-plugin-pet' config: maxImageBytes: 5242880 # optional: image upload cap (default 5 MB) maxSheetBytes: 10485760 # optional: spritesheet cap (default 10 MB) interactionCooldownMs: 1500 # optional: interaction cooldown (default 1500, 0 disables) maxDailyInteractions: 500 # optional: daily interaction cap (default 500) directory: "" # optional: custom state directory (default storages/dsh-plugin-pet) ``` **Restart the host process** after installing (client-modules caches package verdicts per process): restart `dsh web`, or quit and reopen the Desktop app. A page refresh alone is not enough for a new package. ## Usage ### Open the Pet page - Click the **account avatar** in the bottom-left → **Pet** (directly below Settings). Four tabs: | Tab | Content | Saving | | --- | --- | --- | | Overview | Current sprite and mood, level / intimacy, name / size / visibility, quick feed / play / pet | Save in the tab row; only this tab's fields are posted | | Appearance | Built-in sprites, custom image, Codex / generic spritesheet, mood preview | Save in the tab row; only the sprite is posted | | Growth | Companion time, interactions, daily online / tasks, mood distribution, achievements | Read-only (the tab row keeps its 44px height) | Header, tab row and footer stay fixed; only the content scrolls. Switching tabs resets the scroll position, and re-entering the page returns to Overview and refetches config. ### Overlay - **Drag** the pet anywhere (position stored per browser); it plays a directional run animation while dragged. - **Click** to pet it (hearts + EXP); **double-click** to hide it, leaving a paw button in the corner to bring it back. Keyboard: Tab to focus, Enter / Space to pet. - Mood is derived from live state: idle, thinking, working (the bubble names the running tool), sad on errors, waiting for your confirmation, and a celebration when a turn completes. Long idle stretches doze off; long working stretches remind you to drink water / rest / look away. - Achievement unlocks and level-ups appear as toasts above the pet. ## Data and storage State directory: `$DSH_HOME/storages/dsh-plugin-pet/` (override with `config.directory`). | File | Content | | --- | --- | | `config.json` | `{ version: 1, revision, config: { name, size, visible, sprite } }`; directory 0700, file 0600 | | `config.json.bak` | Synchronous backup of every successful write | | `stats.json` / `stats.json.bak` | Growth stats; a corrupt main file recovers from `.bak` | | `image.` / `sheet.` | Uploaded image / spritesheet bytes | - **Revision**: `GET /api/dsh-plugin-pet/config` returns `revision`; `POST` may send `expectedRevision`. A mismatch returns `409 { code: "stale-config", config, revision }`; the client adopts the newest config and asks you to retry, so two tabs cannot overwrite each other silently. - **1.x migration**: a legacy flat `config.json` (top-level `name/size/visible/sprite`) keeps its sprite descriptor and is rewritten into the 2.x envelope on first read; `image.*` / `sheet.*` and `stats.json` keep working unchanged. ## HTTP endpoints (authenticated `/api` bridge) | Method | Path | Purpose | | --- | --- | --- | | GET | `/api/dsh-plugin-pet/config` | Current config + `revision` | | POST | `/api/dsh-plugin-pet/config` | Patch name / size / visibility / builtin / sheet fps, state rows, frames; accepts `expectedRevision` | | GET/POST/DELETE | `/api/dsh-plugin-pet/image` | Read / upload raw bytes / delete the custom image | | GET/POST/DELETE | `/api/dsh-plugin-pet/sheet` | Read / upload (`?source=codex&version=1\|2\|auto` or `?source=custom&cols=&rows=&fps=`) / delete the spritesheet | | POST | `/api/dsh-plugin-pet/petjson` | Import Codex `pet.json` (requires a dimension-matching sheet) | | GET | `/api/dsh-plugin-pet/stats` | Growth stats | | POST | `/api/dsh-plugin-pet/stats/heartbeat` | Page-presence heartbeat (companion time, online time, mood samples) | | POST | `/api/dsh-plugin-pet/stats/interaction` | Feed / play / pet; returns EXP and intimacy gains | | POST | `/api/dsh-plugin-pet/stats/task` | Completed-task growth | | POST | `/api/dsh-plugin-pet/stats/event` | Client-detected achievement events (continuous work / error fixed) | Every write is validated on the host: magic-byte sniffing (PNG / JPEG / GIF / WebP), size caps, exact Codex dimensions, and even generic-grid division. Config and stats writes are atomic and keep a `.bak` copy. Errors carry stable machine codes (`{ ok: false, code, error, params? }`, e.g. `bad_json` / `too_large` / `codex_size` / `grid_uneven` / `stale-config`) translated by the client. Growth responses also carry `unlockedKeys` and `levelUp`; a throttled interaction returns `throttled: true` and grants nothing. > Plain browser `` requests to these `/api` assets include the signed session cookie, so Web and Desktop behave identically. ## Codex spritesheets 1. **Pet page → Appearance → Spritesheet → Import a Codex pet pack**. 2. Upload `spritesheet.webp` / `spritesheet.png` first: it must be **1536×1872 (V1, 9 rows)** or **1536×2288 (V2, 11 rows)** — an 8-column grid of 192×208 cells. The version is auto-detected. 3. Optionally import `pet.json` to apply `displayName` and validate `spriteVersionNumber` against the uploaded sheet. Codex row → DSH mood: idle→0, run_right→1, run_left→2, pet→3 (waving), happy→4 (jumping), sad→5 (failed), waiting→6, working→7 (running), thinking→8 (review), sleeping→0 (reuses idle). All nine V1 action rows are used; only the V2 look-direction rows (9/10) stay unused. ## Generic spritesheets **Appearance → Spritesheet → Generic spritesheet**: any PNG / WebP whose width and height divide evenly by the grid works. Set columns, rows and fps, upload, then assign a row and frame count per mood; the page previews the animation live. ## Development and verification ```sh node --check lib/index.js lib/client.js lib/connection-routes.js node --test # Node >= 24: plain `node --test` python3 -m json.tool package.json git diff --check ``` - The client bundle is the shell's factory-CJS format (`window.__ModuleLoader__.load({ id, factory })`); `require("react")` / `require("react-dom")` come from the shell registry, and `ui-primitives` degrades to native elements when absent. - Page UI uses `--dsw-alias-*` tokens and official density (20/28 title, 14/22 body, 12/18 hints, 32px inputs, 36px buttons, 44px tab row); the macOS Desktop header reserves `--dsh-frame-top-clearance`. - Host tests call registered Fetch handlers with `new Request(...)` and assert the registry key starts with `/api`; client tests load the bundle in a `new Function` sandbox and assert the factory id, `inject`, slots, three tabs, per-tab action rows, live-state projection and account-menu injection. ## Known limitations - One global pet per page (root overlay); it is not per-session. - No Codex “trailing blank frames” detection; frame counts come from the spec defaults or explicit config. - V2 look-direction rows (9/10) are still unused. - Pet position is stored per browser in `localStorage`; it does not sync across devices. - The avatar-menu entry relies on defensive augmentation of the shell popup DOM; if an official menu slot ships, the plugin will switch to it. ## License [MIT](LICENSE)