Files
claude-pet/lib/hooks-installer.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

112 lines
3.5 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.
"use strict";
// 把 Claude Pet 的 hook 合併進 ~/.claude/settings.json(保留使用者原本的設定與其他 hooks)
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const APP_DIR = path.resolve(__dirname, "..");
const CLAUDE_DIR = path.join(os.homedir(), ".claude");
const SETTINGS_PATH = path.join(CLAUDE_DIR, "settings.json");
const BACKUP_DIR = path.join(CLAUDE_DIR, "backups");
const HOOK_SCRIPT = path.join(APP_DIR, "hook", "claude-pet-hook.js");
const MARKER = "claude-pet-hook";
// Claude Code 2.1.x 支援的 hook 事件(對應到寵物狀態)
const EVENTS = [
"SessionStart",
"SessionEnd",
"UserPromptSubmit",
"PreToolUse",
"PostToolUse",
"PostToolUseFailure",
"PermissionRequest",
"Notification",
"Stop",
"StopFailure",
"SubagentStart",
"SubagentStop",
"PreCompact",
];
function hookCommand() {
const p = HOOK_SCRIPT.replace(/\\/g, "/");
return /\s/.test(p) ? `node "${p}"` : `node ${p}`;
}
function readSettings() {
try {
const text = fs.readFileSync(SETTINGS_PATH, "utf8");
return text.trim() ? JSON.parse(text) : {};
} catch (err) {
if (err.code === "ENOENT") return {};
throw new Error(`無法解析 ${SETTINGS_PATH}${err.message}`);
}
}
function writeSettings(settings) {
fs.mkdirSync(CLAUDE_DIR, { recursive: true });
const tmp = `${SETTINGS_PATH}.${process.pid}.tmp`;
fs.writeFileSync(tmp, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
fs.renameSync(tmp, SETTINGS_PATH);
}
function backup() {
if (!fs.existsSync(SETTINGS_PATH)) return null;
fs.mkdirSync(BACKUP_DIR, { recursive: true });
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
const dest = path.join(BACKUP_DIR, `settings.claude-pet.${stamp}.json`);
fs.copyFileSync(SETTINGS_PATH, dest);
return dest;
}
function isPetHook(entry) {
try { return JSON.stringify(entry).includes(MARKER); } catch { return false; }
}
function normalize(list) {
if (Array.isArray(list)) return list;
if (list && typeof list === "object") return [list];
return [];
}
function install() {
const settings = readSettings();
const backupPath = backup();
settings.hooks = settings.hooks && typeof settings.hooks === "object" ? settings.hooks : {};
for (const event of EVENTS) {
const kept = normalize(settings.hooks[event]).filter((e) => !isPetHook(e));
kept.push({ hooks: [{ type: "command", command: hookCommand(), timeout: 5 }] });
settings.hooks[event] = kept;
}
writeSettings(settings);
return { events: EVENTS, settingsPath: SETTINGS_PATH, backupPath, command: hookCommand() };
}
function uninstall() {
const settings = readSettings();
if (!settings.hooks || typeof settings.hooks !== "object") return { removed: 0 };
const backupPath = backup();
let removed = 0;
for (const event of Object.keys(settings.hooks)) {
const before = normalize(settings.hooks[event]);
const after = before.filter((e) => !isPetHook(e));
removed += before.length - after.length;
if (after.length) settings.hooks[event] = after;
else delete settings.hooks[event];
}
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
writeSettings(settings);
return { removed, settingsPath: SETTINGS_PATH, backupPath };
}
function isInstalled() {
try {
const settings = readSettings();
return normalize(settings.hooks?.Stop).some(isPetHook);
} catch {
return false;
}
}
module.exports = { install, uninstall, isInstalled, hookCommand, EVENTS, SETTINGS_PATH };