@shumin13/claude-pet 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/server.js ADDED
@@ -0,0 +1,139 @@
1
+ import { createServer } from "node:http";
2
+ import { readFile } from "node:fs/promises";
3
+ import { extname, join, normalize } from "node:path";
4
+ import { port, root } from "./lib/config.js";
5
+
6
+ const publicDir = join(root, "public");
7
+ const clients = new Set();
8
+ const genericBashPermission = "Claude needs your permission to use Bash";
9
+
10
+ const types = {
11
+ ".html": "text/html; charset=utf-8",
12
+ ".css": "text/css; charset=utf-8",
13
+ ".js": "text/javascript; charset=utf-8",
14
+ ".svg": "image/svg+xml; charset=utf-8",
15
+ ".json": "application/json; charset=utf-8"
16
+ };
17
+
18
+ let lastEvent = {
19
+ type: "ready",
20
+ title: "Claude Pet is awake",
21
+ message: "Waiting for Claude Code notifications.",
22
+ createdAt: new Date().toISOString(),
23
+ replay: true
24
+ };
25
+
26
+ let permissionPromptTimer = null;
27
+
28
+ function send(client, event) {
29
+ client.write(`data: ${JSON.stringify(event)}\n\n`);
30
+ }
31
+
32
+ function broadcast(event) {
33
+ lastEvent = { ...event, createdAt: new Date().toISOString() };
34
+ for (const client of clients) send(client, lastEvent);
35
+
36
+ clearTimeout(permissionPromptTimer);
37
+ if (event.type === "permission_prompt") {
38
+ // Auto-clear if the user denies (no PostToolUse fires in that case)
39
+ permissionPromptTimer = setTimeout(() => broadcast(readyEvent()), 30_000);
40
+ }
41
+ }
42
+
43
+ function readyEvent(message = "") {
44
+ return {
45
+ type: "ready",
46
+ title: "Claude Pet is awake",
47
+ message,
48
+ createdAt: new Date().toISOString(),
49
+ replay: true
50
+ };
51
+ }
52
+
53
+ function normalizeEvent(incoming = {}) {
54
+ const type = incoming.notification_type || incoming.type || "notification";
55
+ return {
56
+ type,
57
+ title: incoming.title || "Claude Code",
58
+ message: incoming.message || "Claude Code needs your attention.",
59
+ replay: incoming.replay !== false,
60
+ sessionLabel: incoming.sessionLabel || incoming.session_label
61
+ };
62
+ }
63
+
64
+ function shouldIgnoreEvent(event) {
65
+ if (event.type === "auth_success") return true;
66
+ return event.type === "permission_prompt" && String(event.message || "").trim() === genericBashPermission;
67
+ }
68
+
69
+ async function readBody(req) {
70
+ let body = "";
71
+ for await (const chunk of req) body += chunk;
72
+ return body;
73
+ }
74
+
75
+ function json(res, status, payload) {
76
+ res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
77
+ res.end(JSON.stringify(payload));
78
+ }
79
+
80
+ const server = createServer(async (req, res) => {
81
+ try {
82
+ const url = new URL(req.url || "/", `http://${req.headers.host}`);
83
+
84
+ if (req.method === "GET" && url.pathname === "/events/stream") {
85
+ res.writeHead(200, {
86
+ "content-type": "text/event-stream",
87
+ "cache-control": "no-cache, no-transform",
88
+ connection: "keep-alive",
89
+ "access-control-allow-origin": "*"
90
+ });
91
+ clients.add(res);
92
+ send(res, lastEvent.replay === false ? readyEvent() : lastEvent);
93
+ req.on("close", () => clients.delete(res));
94
+ return;
95
+ }
96
+
97
+ if (req.method === "POST" && (url.pathname === "/events" || url.pathname === "/claude-notification")) {
98
+ const raw = await readBody(req);
99
+ const incoming = raw ? JSON.parse(raw) : {};
100
+ const event = normalizeEvent(incoming);
101
+ if (shouldIgnoreEvent(event)) {
102
+ json(res, 202, { ok: true, ignored: true, event: lastEvent });
103
+ return;
104
+ }
105
+ broadcast(event);
106
+ json(res, 202, { ok: true, event: lastEvent });
107
+ return;
108
+ }
109
+
110
+ if (req.method === "GET" && url.pathname === "/health") {
111
+ json(res, 200, { ok: true, clients: clients.size, lastEvent });
112
+ return;
113
+ }
114
+
115
+ if (req.method !== "GET") {
116
+ json(res, 405, { ok: false, error: "Method not allowed" });
117
+ return;
118
+ }
119
+
120
+ const requested = url.pathname === "/" || url.pathname === "/claude-notification" || url.pathname === "/desktop.html"
121
+ ? "/index.html"
122
+ : url.pathname;
123
+ const safePath = normalize(requested).replace(/^(\.\.[/\\])+/, "");
124
+ const filePath = join(publicDir, safePath);
125
+ const body = await readFile(filePath);
126
+ res.writeHead(200, { "content-type": types[extname(filePath)] || "application/octet-stream" });
127
+ res.end(body);
128
+ } catch (error) {
129
+ if (error?.code === "ENOENT") {
130
+ json(res, 404, { ok: false, error: "Not found" });
131
+ return;
132
+ }
133
+ json(res, 500, { ok: false, error: error?.message || "Server error" });
134
+ }
135
+ });
136
+
137
+ server.listen(Number(port), "127.0.0.1", () => {
138
+ console.log(`Claude Pet listening at http://127.0.0.1:${port}`);
139
+ });