初始提交: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>
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
// Claude Code hook → Claude Pet
|
||||
// 讀 stdin 的 hook JSON,POST 到本機的寵物 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);
|
||||
})();
|
||||
Reference in New Issue
Block a user