Files
claude-pet/hook/claude-pet-hook.js
T
JianMiauandClaude Fable 5 b1d57249f8 初始提交:Codex Pets 相容的 Claude Code 桌面寵物
摘要:
Electron 桌面寵物,透過 Claude Code hooks 即時反映工作狀態(思考、等待授權、失敗、完成),並支援 16 方向追視游標。

根本原因:
專案尚未納入版控,需要建立初始 repo 以便後續追蹤與協作。

影響:
建立 main 分支的第一個版本,包含完整可執行的程式碼與內附寵物「小念」。

修法:
納入以下內容:
- main.js / preload.js:Electron 主程序、透明置頂視窗、系統匣選單、HTTP 事件伺服器、游標輪詢與拖曳
- renderer/:Codex V2 spritesheet 幀動畫播放器、session 狀態機、氣泡、alpha 點穿判定
- hook/claude-pet-hook.js:Claude Code hook 端,stdin → POST /event,永遠 exit 0
- lib/、scripts/:hooks 安裝/移除、Windows 開機自動啟動
- pets/xiao-nian:內附寵物包(pet.json + spritesheet.webp)
- README.md、docs/states.png:使用說明與狀態總覽圖

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 10:32:39 +08:00

84 lines
2.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
"use strict";
// Claude Code hook → Claude Pet
// 讀 stdin 的 hook JSONPOST 到本機的寵物 app。任何失敗都靜默結束(exit 0),絕不阻塞 Claude Code。
const http = require("node:http");
const PORT = Number(process.env.CLAUDE_PET_PORT || 17333);
const TIMEOUT_MS = 800;
function trim(value, max) {
if (typeof value !== "string") return undefined;
return value.length > max ? `${value.slice(0, max)}…` : value;
}
function readStdin() {
return new Promise((resolve) => {
if (process.stdin.isTTY) return resolve({});
let data = "";
const timer = setTimeout(() => resolve(parse(data)), TIMEOUT_MS);
process.stdin.setEncoding("utf8");
process.stdin.on("data", (chunk) => { data += chunk; });
process.stdin.on("end", () => { clearTimeout(timer); resolve(parse(data)); });
process.stdin.on("error", () => { clearTimeout(timer); resolve(parse(data)); });
});
}
function parse(text) {
try { return text.trim() ? JSON.parse(text) : {}; } catch { return {}; }
}
function buildPayload(input) {
const error = typeof input.error === "string" ? input.error : input.error?.message;
return {
event: input.hook_event_name || process.argv[2] || "Unknown",
sessionId: input.session_id ?? null,
cwd: input.cwd ?? null,
permissionMode: input.permission_mode ?? null,
toolName: input.tool_name ?? null,
notificationType: input.notification_type ?? null,
title: trim(input.title, 160),
message: trim(input.message, 240),
error: trim(error, 240),
source: input.source ?? null,
reason: input.reason ?? null,
trigger: input.trigger ?? null,
stopHookActive: input.stop_hook_active ?? null,
lastAssistantMessage: trim(input.last_assistant_message, 300),
prompt: trim(input.prompt, 120),
ts: Date.now(),
};
}
function post(payload) {
return new Promise((resolve) => {
const body = Buffer.from(JSON.stringify(payload), "utf8");
const req = http.request(
{
host: "127.0.0.1",
port: PORT,
method: "POST",
path: "/event",
headers: { "content-type": "application/json", "content-length": body.length },
},
(res) => { res.resume(); res.on("end", resolve); res.on("error", resolve); },
);
req.setTimeout(TIMEOUT_MS, () => req.destroy());
req.on("error", resolve);
req.end(body);
});
}
(async () => {
const guard = setTimeout(() => process.exit(0), TIMEOUT_MS * 2 + 500);
guard.unref();
try {
const input = await readStdin();
await post(buildPayload(input));
} catch {
// ignore
}
process.exit(0);
})();