# Claude Pet

**Codex Pets 相容的 Claude Code 桌面寵物。**
把 Codex `/hatch` 產出的寵物包原封不動拿來用，讓它站在桌面上，即時反映 Claude Code 正在做什麼：思考、等你授權、失敗、完成……還會盯著你的滑鼠游標看。

![狀態總覽](docs/states.png)

> 上圖由左至右：SessionStart 揮手、工作中、等待授權、完成時跳躍、檢視成果。氣泡會標出是哪個專案的 session。

---

## 目錄

- [特色](#特色)
- [需求](#需求)
- [快速開始](#快速開始)
- [日常使用](#日常使用)
- [行為對照表](#行為對照表)
- [寵物包格式（Codex V2）](#寵物包格式codex-v2)
- [新增 / 切換寵物](#新增--切換寵物)
- [設定檔](#設定檔)
- [架構與檔案](#架構與檔案)
- [能顯示什麼、不能顯示什麼](#能顯示什麼不能顯示什麼)
- [與 Codex 的對照](#與-codex-的對照)
- [HTTP API](#http-api)
- [打包成 exe](#打包成-exe)
- [發佈到 Gitea Releases](#發佈到-gitea-releases)
- [疑難排解](#疑難排解)
- [移除](#移除)

---

## 特色

- **完整實作 Codex V2 pet contract** — 8 欄 × 11 列、192×208 cell 的 spritesheet，9 個標準動作列 + 16 個看向方向，**每一幀的時長都照 Codex 規格**，動起來跟在 Codex app 裡一模一樣。
- **行為邏輯逐項對照 Codex** — 狀態優先序、各狀態到期時間、動畫時序、hover 版面、初次問候、拖曳方向判定……都是從 Codex app 的 `app.asar` 讀出來的數值（見[與 Codex 的對照](#與-codex-的對照)）。
- **16 方向追視（選配，預設關）** — 開啟後、且目前是待機狀態時，游標在哪她就看哪（000 = 正上方、順時針 22.5° 一格），追視時是靜態擺姿；Claude Code 在工作時以狀態動畫優先。Codex 的寵物**不看滑鼠**——它看的是 quick chat 的文字游標與 computer-use 的虛擬游標，所以預設關閉以求一致；喜歡的話在選單打開。
- **自動修正圖檔缺陷** — Codex 的 hatch 產生器有時會把整列 look 格子畫得比 idle 小（內附的小念第 10 列就小了 22%，游標一往下移人物就縮水）。以整列中位數偵測、用腳底當支點放大回正確身高；單格的姿勢差異不動。Codex 本身沒有做這件事。
- **與 Codex 同尺寸** — 100% 就是 Codex 桌面寵物的大小（126 × 137），高度用 Codex 的 `ceil(寬 × 208/192)` 算，兩邊並排看起來一樣大。
- **真正的桌面寵物** — 透明、無邊框、永遠置頂；人物方框以外的區域**點穿**，只有壓在人物或氣泡上滑鼠才會被吃掉，不擋你操作底下的視窗。
- **不搶焦點** — 視窗設為 non-focusable，點她不會讓終端機失焦。
- **拖曳 / 點擊 / 右鍵** — 拖曳時依方向播 running-left / running-right 並記住位置；點一下揮手；右鍵或系統匣開選單。
- **對話氣泡（兩行）** — 第一行是**對話名稱**（`/rename` 設的）或專案資料夾名，第二行才是訊息。同時開多個 session 時一眼就知道是哪一個在叫你。
- **工作中即時播報** — 氣泡會顯示正在用的工具與對象（`⚙️ 執行 npm run dist`、`📖 讀 renderer.js`），以及助理剛說的那句話（`💬 …`）。**注意：這不是思考過程**，原因見[能顯示什麼、不能顯示什麼](#能顯示什麼不能顯示什麼)。
- **多 session** — 同時開好幾個 Claude Code 時，以 `waiting > running > review > idle` 的優先序顯示最需要你注意的那個。
- **Codex 寵物自動出現** — 預設也掃描 `~/.codex/pets`，Codex hatch 出來的寵物直接在選單裡切換。
- **Hook 零負擔** — hook 腳本約 100 ms 完成、寵物沒開也靜默 exit 0，不會拖慢或卡住 Claude Code。

## 需求

| 項目 | 說明 |
|---|---|
| 作業系統 | Windows 10 / 11（透明視窗、點穿、系統匣、開機啟動皆以 Windows 實作） |
| Node.js | 18 以上（開發時為 24.19） |
| Claude Code | 2.1 以上（使用 `SessionStart`、`PermissionRequest`、`PostToolUseFailure`、`StopFailure`、`PreCompact` 等事件） |
| 寵物包 | 一個 Codex V2 寵物包（`pet.json` + `spritesheet.webp`）。本專案內附 `pets/xiao-nian`（小念） |

## 快速開始

### 直接用打包好的（一般使用）

三種都不需要系統管理員權限，選一種即可（見[打包成 exe](#打包成-exe)）：

| 方式 | 檔案 | 適合 |
|---|---|---|
| **免安裝資料夾**（推薦） | `Claude Pet-<版本>-win.zip` | 解壓到任何地方，點兩下裡面的 `Claude Pet.exe` 就跑。啟動快、寵物放旁邊的 `pets\`、適合開機自動啟動 |
| 安裝檔 | `Claude Pet-<版本>-setup.exe` | 裝到 `%LOCALAPPDATA%\Programs\Claude Pet`，有開始選單與桌面捷徑 |
| 免安裝單檔 | `Claude Pet-<版本>-portable.exe` | 只想帶一個檔案走。代價是**每次啟動都要重新解壓約 200 MB**，開得比較慢 |

開起來之後用系統匣選單的 **Claude Code hooks → 安裝** 接上 Claude Code，然後**重開一個新的 Claude Code session**。

### 從原始碼跑（開發）

```powershell
cd E:\Project\Test\AI\Claude\claude-pet
npm install
npm run hooks:install        # 把 13 個 hook 合併進 ~/.claude/settings.json（會先備份到 ~/.claude/backups）
npm start                    # 啟動寵物
npm run autostart:install    # （可選）開機自動啟動
```

> **注意**
> 1. 已開啟的 Claude Code session 不會重新讀 hooks，**重開一個新 session** 才會看到她反應。
> 2. 若 `npm install` 後 `node_modules\electron\dist\` 是空的，請看 [疑難排解](#electron-裝完沒有-electronexe)。

## 日常使用

| 操作 | 效果 |
|---|---|
| 左鍵點一下 | 揮手（若有非固定的氣泡，先關閉氣泡） |
| 左鍵拖曳 | 搬家；往左播 running-left、往右播 running-right；放開後位置寫入設定檔 |
| 右鍵 / 系統匣左鍵 | 開啟選單 |
| 滑鼠移到她身上 | 跳躍最多 3 次；游標一離開就立刻停止、回到原本狀態（Codex 的 hover 行為） |
| 滑鼠靠近 | 盯著游標看（選單開啟「跟著游標看」後才有） |

**選單功能**

- **切換寵物** — 列出所有找到的寵物；v1（9 列）寵物會標示「無追視」
- **大小** — 50% / 75% / 100% / 125% / 150%，以腳底為基準縮放（**100% = Codex 桌面寵物的大小**）
- **跟著游標看**、**永遠置頂**、**開機自動啟動** — 勾選即生效
- **動作測試** — 不用真的跑 Claude Code 也能看每個動畫（idle / running / waiting / review / failed / jumping / waving）
- **Claude Code hooks** — 安裝 / 移除 hooks
- **回到預設位置** — 跑到螢幕外面時用
- **開啟設定檔** / **開啟事件紀錄**
- **結束**

## 行為對照表

| Claude Code hook | 條件 | 動作 | 氣泡 |
|---|---|---|---|
| （程式啟動） | 該寵物第一次出現 | `waving` × 3 → 慢速 `idle` | 👋 嗨，我是〈寵物名〉（8 秒） |
| `SessionStart` | `source = startup` | `idle` | 👋 嗨！（專案） |
| `UserPromptSubmit` | | `running` | （關閉舊氣泡） |
| `PreToolUse` | 一般工具 | `running` | 有新的助理說明就顯示 `💬 …`，否則顯示 `⚙️ 執行 …` 之類的工具動作 |
| `PreToolUse` | `AskUserQuestion` | `waiting` | ❓ 有問題想問你 |
| `PreToolUse` | `ExitPlanMode` | `waiting` | 📋 計畫等你確認 |
| `PostToolUse` | | `running` | |
| `PostToolUseFailure` | | `running`（不變） | 😵 〈工具〉失敗了 |
| `PermissionRequest` | | `waiting` | 🔐 需要你授權：〈工具〉 |
| `Notification` | `permission_prompt` | `waiting` | 🔐 需要你授權 |
| `Notification` | `idle_prompt` | （不變） | 💤 在等你回覆 |
| `Notification` | `elicitation_dialog` | `waiting` | ❓ 有問題想問你 |
| `Notification` | 其他 | （不變） | 💬 訊息內容 |
| `Stop` | `stop_hook_active = false` | `review`，直到該 session 下一個動作／結束／7 天 | ✅ 完成！（專案） |
| `StopFailure` | | `failed`，直到該 session 下一個動作／結束／1 小時 | ❌ 錯誤訊息 |
| `SubagentStart` / `SubagentStop` | | `running` | |
| `PreCompact` | | `running` | 🗜️ 整理記憶中… |
| `SessionEnd` | | 移除該 session | 最後一個結束時 👋 掰掰 |
| （無事件） | 游標移動（追視開啟時、且目前為 idle） | 16 方向 `look` | |
| （無事件） | 拖曳 | 單次位移 ≥ 4 px 才決定 `running-left` / `running-right` | |
| （無事件） | 游標進入人物方框 | `jumping` 最多 × 3；離開即中斷 | |

規則：

- **待機時幾乎不動** — idle 每幀時長是合約值的 **6 倍**（一輪 6.6 秒，不是 1.1 秒），與 Codex 的 `codex-pet-assets` 一致。
- **非 idle 狀態播 3 次就沉澱** 成慢速 idle。動畫只在**狀態改變**時重新開始（Codex 的 effect 相依只有 state），工作中連續的工具呼叫不會讓她一直動。
- **一次性動作**（點擊揮手、初次問候）播完自動回到基礎狀態。
- **基礎狀態**依 session 彙整：`waiting(4) > failed(3) > review(2) > running(1) > idle(0)`，取最高者顯示——與 Codex 通知排序 `Ai()` 相同，注意 **review 高於 running**。
- **到期**與 Codex 的 `Ei()` 相同：`failed` 1 小時、`waiting` 24 小時、`review` 7 天後退回 idle；`running` 與 `idle` 不到期。另有一個 Codex 沒有的安全網：`running` 30 分鐘沒任何事件視為終端機已被關掉。
- **系統「減少動態效果」開啟時**只顯示每個狀態的第一格，與 Codex 相同。
- `waiting` 的氣泡是固定的，狀態離開 `waiting` 時自動消失。

## 寵物包格式（Codex V2）

與 Codex 完全相同，可直接互通。一個寵物是一個資料夾：

```
<pet-id>/
├── pet.json
└── spritesheet.webp     （或 .png）
```

`pet.json`：

```json
{
  "id": "xiao-nian",
  "displayName": "小念",
  "description": "一句話描述",
  "spriteVersionNumber": 2,
  "spritesheetPath": "spritesheet.webp"
}
```

Spritesheet：**1536 × 2288**，8 欄 × 11 列，每格 **192 × 208**，透明背景。

| 列 | 狀態 | 使用欄 | 每幀時長（ms） |
|---|---|---|---|
| 0 | idle | 0–5 | 280, 110, 110, 140, 140, 320 |
| 1 | running-right | 0–7 | 120 ×7, 220 |
| 2 | running-left | 0–7 | 120 ×7, 220 |
| 3 | waving | 0–3 | 140 ×3, 280 |
| 4 | jumping | 0–4 | 140 ×4, 280 |
| 5 | failed | 0–7 | 140 ×7, 240 |
| 6 | waiting | 0–5 | 150 ×5, 260 |
| 7 | running（工作中，不是跑步） | 0–5 | 120 ×5, 220 |
| 8 | review | 0–5 | 150 ×5, 280 |
| 9 | look A | 0–7 | 000, 022.5, 045, 067.5, 090, 112.5, 135, 157.5 度 |
| 10 | look B | 0–7 | 180, 202.5, 225, 247.5, 270, 292.5, 315, 337.5 度 |

- look 列的 **000 是正上方（12 點鐘）**，順時針。正面／中立不在 16 格裡，那是游標死區，退回 idle。
- 列 0 第 6 欄若有圖會被當作「中立正面」，拿來做系統匣圖示。
- 沒有 `spriteVersionNumber: 2` 的舊版（8×9、1536×1872）寵物也能用，只是沒有追視。

> 規格來源：Codex 的 `hatch-pet` skill（`~/.codex/skills/hatch-pet/references/codex-pet-contract.md`、`animation-rows.md`）。

## 新增 / 切換寵物

掃描順序：**執行目錄的 `pets\`**（portable 版是那顆 exe 旁邊的，其次才是內附的）→ `~/.codex/pets` → 設定檔 `petSources` 裡的額外路徑。同一個 id 以先掃到的為準。

1. **手動放入**（最直接）— 把寵物資料夾丟到 exe 旁邊的 `pets\<id>\`，選單「切換寵物 → 重新掃描」就會出現。選單的「開啟寵物資料夾」會直接開這一個。
2. **用 Codex hatch** — 在 Codex 裡 `/hatch`，完成後寵物在 `%USERPROFILE%\.codex\pets\<id>\`，本程式一律會掃這個資料夾。
3. **其他位置** — 在設定檔 `petSources` 加路徑。

切換後立即生效，不用重啟。

## 設定檔

位置：`%USERPROFILE%\.claude-pet\config.json`（第一次啟動後自動產生，選單「開啟設定檔」可直接開）。

| 欄位 | 預設 | 說明 |
|---|---|---|
| `activePetId` | `"xiao-nian"` | 目前使用的寵物 id |
| `petSources` | `[]` | **額外**的寵物資料夾。兩個隱含來源永遠會被掃到且不寫進這裡：**執行目錄的 `pets/`**（exe 旁邊，開發時是專案資料夾，排最前面）與 `~/.codex/pets`。不存在的路徑會在載入時自動剔除 |
| `scale` | `1` | 縮放：0.5 / 0.75 / 1 / 1.25 / 1.5。`1` 表示 126 × 137，與 Codex 桌面寵物同大小 |
| `position` | `null` | 視窗左上角座標；`null` 為主螢幕右下角。若座標落在所有螢幕之外會自動拉回 |
| `alwaysOnTop` | `true` | 永遠置頂 |
| `followCursor` | `false` | 追視游標（Codex 的寵物不看滑鼠，預設關） |
| `greetedPetIds` | `[]` | 已打過招呼的寵物 id；刪掉可讓該寵物下次啟動再揮手一次 |
| `port` | `17333` | 接收 hook 事件的本機埠（改了之後 hook 端要設環境變數 `CLAUDE_PET_PORT`） |

事件紀錄：`%USERPROFILE%\.claude-pet\events.log`（超過 2 MB 自動清空）。

氣泡的第一行是對話名稱或專案名，第二行是訊息；標題為空時整行會收起來，不會留空白。

## 架構與檔案

```
Claude Code ──hook（stdin JSON）──▶ hook/claude-pet-hook.js ──POST /event──▶ 127.0.0.1:17333
                                                                                   │  Electron main（main.js）
                                                                                   │  視窗 / 系統匣 / 游標輪詢 / 拖曳 / 設定
                                                                                   ▼  IPC
                                                                          renderer/renderer.js
                                                                          狀態機 + 幀動畫 + 追視 + 氣泡 + 矩形回報
```

| 檔案 | 職責 |
|---|---|
| `main.js` | Electron 主程序：透明置頂視窗、系統匣選單、HTTP 伺服器、游標輪詢（50 ms）、拖曳（60 fps）、設定讀寫、寵物掃描 |
| `preload.js` | 以 contextBridge 暴露白名單 IPC 頻道給 renderer（sandbox 模式） |
| `renderer/renderer.js` | Codex V2 幀時序播放器、session 狀態彙整、一次性動作、看向方向計算（含遲滯防抖）、氣泡、hover 判定與人物／氣泡矩形回報 |
| `renderer/index.html`、`style.css` | 版面與氣泡樣式（CSP 僅允許 self / data:） |
| `hook/claude-pet-hook.js` | Claude Code hook 端：讀 stdin → 擷取欄位 → POST；800 ms 逾時、永遠 exit 0 |
| `lib/hooks-installer.js` | 安全地合併 / 移除 `~/.claude/settings.json` 的 hooks（保留其他設定與其他 hooks，先備份） |
| `lib/autostart.js` | `HKCU\Software\Microsoft\Windows\CurrentVersion\Run\ClaudePet` 登錄值；開發時指向 `electron.exe + 專案路徑`，打包後直接指向那顆 exe |
| `lib/paths.js` | 開發／安裝版／portable 三種情境的路徑解析（`app.asar` → `app.asar.unpacked`、是否打包、portable 的真實 exe 位置） |
| `scripts/*.js` | 上述兩者的 CLI 包裝，以及 `release.js`（發佈到 Gitea Releases） |
| `pets/` | 內附寵物 |
| `docs/` | README 用圖 |
| `build/icon.ico` | exe 與安裝檔的圖示，取自小念 spritesheet 第 0 列第 6 欄（中立正面）的頭肩方形裁切 |

設計重點：

- **點穿由主程序決定**：視窗預設 `setIgnoreMouseEvents(true, { forward: true })`；主程序每 50 ms 自己輪詢游標（`screen.getCursorScreenPoint()`），對照 renderer 回報的**人物方框**與（顯示中的）氣泡矩形，游標在上面就接滑鼠、否則穿透（`lib/hit-test.js`，純邏輯可單元測試）。決策不經過 renderer：以前是 renderer 判定再用 IPC 叫主程序切，renderer 一旦例外、卡住或狀態錯亂（例如放開事件沒送到、`pressed` 一直是 true），點穿就永遠不會再更新，寵物就點不到了。renderer 沒回報矩形或掛掉時，主程序用版面公式算方框當備援；renderer 掛掉會記錄並自動重載。判定用方框而不是讀 canvas 該點的 alpha：人物的手腳與邊緣會隨動畫在透明／不透明之間切換（以小念為例，hover 時只有 44% 的人物像素每一幀都不透明），逐像素判定會讓點穿狀態跟著動畫高速抖動，按下去那一瞬間剛好是透明格，滑鼠事件就穿到底下的視窗去了。主程序輪詢也是必要的——視窗處於點穿狀態時 Electron 的 `forward` 不保證會把 mousemove 轉發進來（游標直接跳到寵物上常常收不到）。
- **拖曳結束一定通知 renderer**：視窗永遠不取得焦點，Windows 對背景視窗的滑鼠 capture 有限制，放開的那一下若落在視窗外 renderer 收不到 mouseup。主程序不管因為 mouseup、逾時（20 秒）或其他原因結束拖曳，都送 `pet:drag-ended` 讓 renderer 清掉按住／拖曳狀態。
- **右鍵選單開著時暫停置頂重宣告**：否則 `moveTop()` 會把寵物視窗抬到選單上面，人物方框整塊接滑鼠，被蓋住的選單項目就點不到。
- **診斷**：`GET /state` 除了動畫狀態，還會回 `bounds` 與 `mouse`（目前是否點穿、游標的視窗相對座標、是否在方框／氣泡內、矩形來源是 renderer 還是備援、切換次數、輪詢次數、renderer 錯誤數、最近幾次拖曳結束的原因）；renderer 的例外、拖曳開始／結束、選單開關都會寫進事件紀錄。
- **拖曳**：由主程序用 `screen.getCursorScreenPoint()` 每 16 ms 更新視窗位置，游標離開視窗也不會掉；方向由水平位移決定。
- **置頂要定期重新宣告**：Windows 有時候會把視窗踢出 topmost band，卻留著 `WS_EX_TOPMOST` 樣式位元——從外部檢查看到 `TOPMOST=true`，但 z-order 排在一般視窗（例如 VS Code）之下，寵物就被蓋住，而 Electron 這邊仍以為自己是置頂的，不會自己修好。所以每 2 秒重新宣告一次，並在螢幕配置變動時立刻補一次。`setAlwaysOnTop` 在狀態沒變時可能不會真的呼叫 `SetWindowPos`，因此再補一個 `moveTop()` 強制插回 topmost band 最上面；視窗是 `WS_EX_NOACTIVATE` + `focusable: false`，不會搶焦點。
- **行為邏輯以 Codex 為準**，細節見[與 Codex 的對照](#與-codex-的對照)。
- **顯示尺寸與 sprite 來源尺寸分開**：`CELL_W/CELL_H`（192 × 208）只用來從 spritesheet 取格子，畫到畫面上的是 `PET_W/PET_H`（126 × 137，Codex 的 mascot 尺寸）。視窗寬度另有 `MIN_WIN_W = 240` 的下限，免得寵物變小後氣泡被截掉。
- **Spritesheet 以 data URL 傳給 renderer**，避免 `file://` 跨來源污染 canvas 導致 `getImageData` 失敗。
- **hook 合併規則**：以指令中是否含 `claude-pet-hook` 辨識自己的項目，重裝只替換自己的、不動別人的。

## 能顯示什麼、不能顯示什麼

氣泡**顯示不了 Claude 的思考過程**。Claude Code 從 **2.1.238** 起就不再把 thinking 的文字寫進 transcript，只留加密簽章：

```json
{ "type": "thinking", "thinking": "", "signature": "CAQSwgQKEAgRGAI4AUII…" }
```

（2.1.237 以前有文字。這是 Claude Code 版本的差異，跟模型無關——同樣是 `claude-opus-5`，
2.1.237 的 session 存得到、2.1.241 的存不到。）Claude Code 也沒有對外的事件串流，
所以 hook 拿不到思考內容。這點跟 Codex 不同：Codex CLI 的 `--json` 會直接送出 `reasoning` 事件。

拿得到的是這些，氣泡就顯示這些：

| 來源 | 內容 | 從哪來 |
|---|---|---|
| 工具動作 | `⚙️ 執行 npm run dist`、`📖 讀 renderer.js` | hook 的 `tool_name` 與 `tool_input` |
| 助理說明 | 助理剛講那段話的第一句 | 由 `transcript_path` 讀 transcript 尾端的 `text` 區塊 |
| 氣泡標題 | 對話名稱，沒設就用專案資料夾名 | transcript 的 `custom-title`（`/rename` 寫進去的），退回 `cwd` 的最後一段 |

氣泡長這樣：

```
claude-pet
💬 兩行氣泡正確運作，這是真實事件。
```

同一段說明只播報一次（以 transcript 的 uuid 判斷），markdown 標記會先清掉，
等待授權時不播報以免蓋掉固定氣泡。讀 transcript 只讀最後 192 KB 並依檔案大小快取，
不會因為 transcript 長到幾 MB 就變慢。

## 與 Codex 的對照

所有數值都是從 Codex app（`WindowsApps\OpenAI.Codex_*\app\resources\app.asar`）的 `codex-pet-assets`、`avatar-overlay-native-frame` 與主程序讀出來的。

| 項目 | Codex | 本程式 |
|---|---|---|
| idle 速度 | 合約幀長 × 6（`_ = 6`） | 相同 |
| 非 idle 狀態 | 播 3 次 → 慢速 idle（`[...n,...n,...n]`） | 相同 |
| 動畫何時重播 | 只在狀態改變時（effect 相依 `[state, lookFrame, reducedMotion]`） | 相同 |
| 跨 thread 優先序 | `waiting 0 > failed 1 > review 2 > running 3 > idle 4`（`Ai()`） | 相同 |
| 狀態到期 | failed 1 h、waiting 24 h、review 7 d、running/idle 不到期（`Ei()`） | 相同，另加 running 30 分鐘無事件的安全網 |
| `failed` | 持續狀態（turn 失敗／取消） | 相同（`StopFailure`）；工具失敗不算 |
| `waiting` | 授權、`requestUserInput`、計畫待確認 | 相同；`idle_prompt` 不算 |
| `review` | turn 完成且未讀，直到使用者回到該 thread | 直到該 session 下一個動作 |
| `waving` | 只在 first-awake（每隻寵物一次，8 秒） | 相同（記在設定檔 `greetedPetIds`） |
| `jumping` | 定義在 `respondToHover ? "jumping" : state`，但整個 app 沒人開啟那個 prop | **選擇打開**：游標進入人物方框就播，最多 3 次 |
| hover 離開 | 顯示狀態立刻翻回 `state`，動畫重跑，不會把跳躍播完 | 相同（游標離開就中斷跳躍） |
| hover 版面 | 40 px 內進入、56 px 外退出；人物縮 0.9、下移 14 px、上方 24 px 控制鈕、200 ms | 未採用——控制鈕的功能就是選單，右鍵已經有了 |
| hover／點穿判定 | 用矩形（`pet-area-hover-changed` 由 rect 算） | 相同（逐像素會因每格輪廓不同而抖動；點穿若跟著抖，按下去的瞬間剛好透明就穿到底下去） |
| 追視來源 | quick chat 的文字游標／computer-use 游標 | 沒有對應來源；改為**選配**看滑鼠，預設關 |
| 追視優先序 | `lookFrame` 覆蓋所有狀態 | 只在 idle 時追視。Codex 的文字游標只有你在打字時才存在，滑鼠卻是一直都在，照搬會讓工作中的動畫永遠被靜態擺姿蓋掉 |
| 拖曳方向 | 單次取樣水平位移 ≥ 4 px | 相同 |
| 拖曳放開 | 彈簧回彈（spring 220 / damping 26） | 無（直接停在放開處） |
| 點擊 | 開 quick chat | 揮手（沒有對應功能） |
| reduced motion | 只顯示第一格 | 相同 |
| 預設位置 | 工作區右下各留 24 px | 相同 |
| 尺寸 | 寬 80–224 px 可調 | 65%–175%（82–221 px） |

刻意不同的地方都是因為 Claude Code 沒有對應的功能（quick chat、computer-use 游標）或對應的訊號（thread 是否已讀）。

## HTTP API

只監聽 `127.0.0.1`。

| 方法 | 路徑 | 說明 |
|---|---|---|
| `POST` | `/event` | 接收事件（JSON，≤ 64 KB），回 `204` |
| `GET` | `/state` | 目前狀態：`{"anim","base","sessions","pet","scale"}` |
| `GET` | `/health` | 回 `ok` |

`POST /event` 的 payload（hook 腳本送出的格式）：

```json
{
  "event": "Stop",
  "sessionId": "uuid",
  "cwd": "E:\\Project\\foo",
  "toolName": null,
  "notificationType": null,
  "message": null,
  "error": null,
  "source": null,
  "stopHookActive": false,
  "ts": 1787278568523
}
```

手動觸發範例：

```powershell
# 模擬需要授權
curl -X POST http://127.0.0.1:17333/event -H "content-type: application/json" `
  -d '{"event":"Notification","sessionId":"demo","cwd":"D:/demo","notificationType":"permission_prompt"}'

# 模擬完成
curl -X POST http://127.0.0.1:17333/event -H "content-type: application/json" `
  -d '{"event":"Stop","sessionId":"demo","cwd":"D:/demo"}'

# 查狀態
curl http://127.0.0.1:17333/state
```

也可以送 `{"event":"__test","anim":"waiting"}`（`idle|running|waiting|review|failed|jumping|waving|clear`）直接指定動畫，與選單「動作測試」相同。

## 打包成 exe

```powershell
npm run dist            # 三種都做
npm run dist:portable   # 只做免安裝單檔
npm run pack            # 只產 win-unpacked 資料夾，最快，改程式時驗證用
```

> **版號會自動遞增**
> `dist` 與 `dist:portable` 都掛了 `pre` 腳本執行 `npm version patch --no-git-tag-version`，
> 每次打包 patch 版號自動 +1。**產物檔名帶版號，同版號重包會讓人分不清手上是哪一份**，
> 所以不要繞過這個機制。要改 minor / major 就先手動 `npm version minor --no-git-tag-version`。
> `pack` 不遞增（它只產 `win-unpacked` 供開發驗證，不是散布用的檔案）。

產物在 `dist\`：

| 檔案 | 大小 | 說明 |
|---|---|---|
| `Claude Pet-<版本>-win.zip` | ~140 MB | **免安裝資料夾版**。解壓後點兩下 `Claude Pet.exe`。啟動最快，寵物就放旁邊的 `pets\` |
| `Claude Pet-<版本>-setup.exe` | ~100 MB | NSIS 安裝檔。per-user、可改安裝路徑、建立桌面與開始選單捷徑 |
| `Claude Pet-<版本>-portable.exe` | ~100 MB | **免安裝單檔**，點兩下直接跑 |
| `win-unpacked\` | ~200 MB | 上面 zip 的未壓縮版，整個資料夾複製走也能用 |

第一次建置時 electron-builder 會從 GitHub 抓 NSIS 與相關工具到 `%LOCALAPPDATA%\electron-builder\Cache`，需要網路，之後走快取。

### portable 單檔版的代價

NSIS 的 portable 外殼**每次啟動**都會把整包解壓到 `%TEMP%\ClaudePet`，**程式結束後又整個刪掉**（見 `app-builder-lib/templates/nsis/portable.nsi` 的 `RMDir /r $INSTDIR`）。所以：

- 每次開都要重新解壓約 200 MB，比資料夾版慢好幾秒。**要開機自動啟動的話建議用資料夾版**。
- 程式本體的位置是暫時的，因此 hook 腳本會另外複製一份到 `%USERPROFILE%\.claude-pet\hook\`，用那個穩定路徑去註冊。否則寵物沒開的時候 Claude Code 每次觸發 hook 都會找不到檔案而報錯。
- 使用者自己的寵物要放在**那顆 exe 旁邊**的 `pets\`（程式第一次跑會自己建），放進解壓目錄的會跟著被刪掉。同 id 時以 exe 旁邊那份為準。
- 程式被強制結束（工作管理員）時外殼來不及清理，`%TEMP%\ClaudePet` 會留下約 300 MB，可以手動刪。

### 打包後和原始碼跑不一樣的地方

都已經處理好，改程式時要留意：

| 項目 | 開發 | 安裝／資料夾版 | portable 單檔 |
|---|---|---|---|
| 內附寵物 | `<專案>\pets` | `<exe 旁>\pets` | `%TEMP%\ClaudePet\pets` |
| 使用者寵物 | 同上 | 同上 | `<exe 旁>\pets` |
| hook 腳本 | `<專案>\hook\` | `resources\app.asar.unpacked\hook\` | `%USERPROFILE%\.claude-pet\hook\` |
| 開機自動啟動 | `electron.exe` + 專案路徑 | 那顆 exe | `PORTABLE_EXECUTABLE_FILE` |

判斷邏輯都在 `lib/paths.js`：`isPackaged()` 用 `process.defaultApp`、`isPortable()` 用 `PORTABLE_EXECUTABLE_FILE`、`unpacked()` 把 `app.asar` 換成 `app.asar.unpacked`。

- **`pets/` 用 `extraFiles` 放到 exe 旁邊**（不是 `extraResources`，那會進 `resources\`），使用者直接把寵物資料夾丟進去就會出現，不用碰 asar。
- **`hook/` 用 `asarUnpack`**，因為 Claude Code 是用 `node <路徑>` 執行它，而 node 讀不到 asar 裡的檔案。
- **hook 指令仍然需要 `node` 在 PATH 裡**。路徑含空白時會自動加引號。

換圖示就換掉 `build/icon.ico`（要含 256×256）。目前這顆是從 `pets/xiao-nian/spritesheet.webp` 第 0 列第 6 欄（中立正面）取頭肩方形裁切產生的。

## 發佈到 Gitea Releases

```powershell
$env:GITEA_TOKEN = "<token>"
npm run release -- --dry-run   # 只檢查：產物、附件限制、release 是否已存在，不做任何寫入
npm run release                # 實際發佈
```

會建立 tag `v<版本>` 的 release（說明自動取自上一個 tag 之後的 commit 標題），
並上傳 `dist\` 裡的 `.exe` 與 `.zip`。同名附件會先刪再傳，所以可以重跑。
`.blockmap` 不上傳——那是 electron-updater 的差分更新才需要，本專案沒有用到。

**前置一：伺服器設定。** Gitea 預設的附件上限是 100 MB，而且 `ALLOWED_TYPES` 白名單沒有 `.exe`，
三個產物全部會被擋下。在 `app.ini` 調整後重啟 Gitea：

```ini
[attachment]
MAX_SIZE = 300
ALLOWED_TYPES = */*
```

腳本會先讀 `/api/v1/settings/attachment` 做預檢，設定不足時會直接列出哪個檔案卡在哪一條、
不會傳到一半才失敗。

**前置二：token。** 到 Gitea 的「設定 → 應用程式 → 產生新的權杖」，
勾選 `write:repository`（只有套件庫權限的 token 不行，repo API 會回 403）。
放環境變數就好，不要寫進檔案。

**其他環境變數**

| 變數 | 用途 |
|---|---|
| `GITEA_TOKEN` | 必填，需 `write:repository` |
| `GITEA_URL` | 網頁網址與 remote 主機不同時覆寫（預設 `https://<remote 主機>`）。SSH 走 8022 埠不影響，腳本只取主機名 |

## 疑難排解

### Electron 裝完沒有 `electron.exe`

`npm install` 成功但 `node_modules\electron\dist\` 是空的 → 你的 `~/.npmrc` 可能有 `allow-scripts=...`（npm 11 的供應鏈防護，只放行清單上套件的 postinstall）。兩種解法：

```powershell
node node_modules\electron\install.js                                # 手動跑一次下載
# 或把 electron 加進白名單
npm config set allow-scripts=@anthropic-ai/claude-code,electron --location=user
```

### 寵物對 Claude Code 沒反應

1. 確認 hooks 已安裝：`npm run hooks:install`，並**重開 Claude Code session**。
2. 確認寵物在跑：`curl http://127.0.0.1:17333/health` 應回 `ok`。
3. 看 `%USERPROFILE%\.claude-pet\events.log` 有沒有事件進來。有事件但沒動 → renderer 問題；沒事件 → hook 沒被呼叫。
4. hook 指令是 `node E:/.../claude-pet-hook.js`，Claude Code 執行 hook 的環境要找得到 `node`（PATH）。

### 啟動時跳出「無法監聽 127.0.0.1:17333」

已經有一隻在跑（本程式有單一實例鎖，通常不會發生），或埠被別的程式佔用。改設定檔 `port`，並在 Claude Code 的環境設 `CLAUDE_PET_PORT` 為同一個值。

### 她跑到螢幕外 / 換了螢幕配置看不到

選單「回到預設位置」，或刪掉設定檔的 `position`。啟動時若座標不在任何螢幕的工作區內會自動拉回。

### 專案搬家了

重新執行 `npm run hooks:install` 與 `npm run autostart:install`（兩者都以目前路徑重寫），寵物資料夾會自動跟著程式走。

## 移除

先在系統匣選單關掉 **開機自動啟動**、並用 **Claude Code hooks → 移除** 拆掉 hooks，然後結束程式。從原始碼跑的話也可以下指令：

```powershell
npm run hooks:uninstall        # 從 ~/.claude/settings.json 移除所有 Claude Pet hook（先備份）
npm run autostart:uninstall    # 移除開機啟動登錄值
```

裝過安裝檔的話，到「設定 → 應用程式 → 已安裝的應用程式」移除 **Claude Pet**（解除安裝不會動到 `%USERPROFILE%\.claude-pet\`）。最後刪掉專案資料夾與 `%USERPROFILE%\.claude-pet\` 即可。

---

## 致謝

- 寵物規格與動畫時序來自 OpenAI Codex 的 `hatch-pet` skill（Codex V2 pet contract）。
- 內附寵物「小念」由 Codex `/hatch` 產生。
