2026-08-21 10:32:39 +08:00
|
|
|
|
#!/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 {}; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-24 10:16:43 +08:00
|
|
|
|
// 從 tool_input 挑一個最有資訊量的欄位當成「正在對什麼做事」。
|
|
|
|
|
|
// 整包 tool_input 可能很大(例如 Write 帶整份檔案內容),只取一小段。
|
|
|
|
|
|
const TARGET_KEYS = ["command", "file_path", "pattern", "path", "url", "query", "notebook_path", "description", "prompt"];
|
|
|
|
|
|
|
|
|
|
|
|
function toolTarget(toolInput) {
|
|
|
|
|
|
if (!toolInput || typeof toolInput !== "object") return undefined;
|
|
|
|
|
|
for (const key of TARGET_KEYS) {
|
|
|
|
|
|
const v = toolInput[key];
|
|
|
|
|
|
if (typeof v === "string" && v.trim()) return trim(v.trim().replace(/\s+/g, " "), 160);
|
|
|
|
|
|
}
|
|
|
|
|
|
return undefined;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-21 10:32:39 +08:00
|
|
|
|
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,
|
2026-08-24 10:16:43 +08:00
|
|
|
|
toolTarget: toolTarget(input.tool_input),
|
|
|
|
|
|
agentType: input.agent_type ?? null,
|
|
|
|
|
|
// 助理的說明文字要從 transcript 撈;寵物那邊才讀,hook 只負責把路徑帶過去
|
|
|
|
|
|
transcriptPath: input.transcript_path ?? null,
|
2026-08-21 10:32:39 +08:00
|
|
|
|
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);
|
|
|
|
|
|
})();
|