初始提交: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,50 @@
|
||||
"use strict";
|
||||
// Windows 開機自動啟動:HKCU\Software\Microsoft\Windows\CurrentVersion\Run\ClaudePet
|
||||
const path = require("node:path");
|
||||
const fs = require("node:fs");
|
||||
const { spawnSync } = require("node:child_process");
|
||||
|
||||
const APP_DIR = path.resolve(__dirname, "..");
|
||||
const RUN_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
|
||||
const VALUE_NAME = "ClaudePet";
|
||||
|
||||
function electronExe() {
|
||||
return path.join(APP_DIR, "node_modules", "electron", "dist", "electron.exe");
|
||||
}
|
||||
|
||||
function command() {
|
||||
return `"${electronExe()}" "${APP_DIR}"`;
|
||||
}
|
||||
|
||||
function reg(args) {
|
||||
const r = spawnSync("reg", args, { encoding: "utf8", windowsHide: true });
|
||||
return { ok: r.status === 0, stdout: r.stdout || "", stderr: r.stderr || "" };
|
||||
}
|
||||
|
||||
function isInstalled() {
|
||||
if (process.platform !== "win32") return false;
|
||||
return reg(["query", RUN_KEY, "/v", VALUE_NAME]).ok;
|
||||
}
|
||||
|
||||
function currentValue() {
|
||||
const r = reg(["query", RUN_KEY, "/v", VALUE_NAME]);
|
||||
if (!r.ok) return null;
|
||||
const m = r.stdout.match(/REG_SZ\s+(.+)$/m);
|
||||
return m ? m[1].trim() : null;
|
||||
}
|
||||
|
||||
function install() {
|
||||
if (process.platform !== "win32") throw new Error("只支援 Windows");
|
||||
if (!fs.existsSync(electronExe())) throw new Error(`找不到 Electron:${electronExe()}(先執行 npm install)`);
|
||||
const r = reg(["add", RUN_KEY, "/v", VALUE_NAME, "/t", "REG_SZ", "/d", command(), "/f"]);
|
||||
if (!r.ok) throw new Error(r.stderr || r.stdout || "reg add 失敗");
|
||||
return command();
|
||||
}
|
||||
|
||||
function uninstall() {
|
||||
if (process.platform !== "win32") return false;
|
||||
const r = reg(["delete", RUN_KEY, "/v", VALUE_NAME, "/f"]);
|
||||
return r.ok;
|
||||
}
|
||||
|
||||
module.exports = { install, uninstall, isInstalled, currentValue, command, electronExe, VALUE_NAME, RUN_KEY };
|
||||
@@ -0,0 +1,111 @@
|
||||
"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 };
|
||||
Reference in New Issue
Block a user