@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/README.md +274 -0
- package/bin/claude-pet.js +85 -0
- package/hooks/claude-pet-clear.js +17 -0
- package/hooks/claude-pet-notify.js +26 -0
- package/hooks/claude-pet-stop.js +24 -0
- package/lib/config.js +19 -0
- package/lib/lock.js +35 -0
- package/lib/overlay-binary.js +104 -0
- package/lib/runtime.js +49 -0
- package/lib/session-labels.js +86 -0
- package/macos/RobotPetOverlay.swift +101 -0
- package/package.json +36 -0
- package/prebuilt/macos/robot-pet-overlay +0 -0
- package/public/app.js +516 -0
- package/public/desktop.css +719 -0
- package/public/index.html +103 -0
- package/public/styles.css +34 -0
- package/scripts/close-desktop-if-last-session.js +73 -0
- package/scripts/install-claude-hook.js +78 -0
- package/scripts/launch-desktop-if-needed.js +77 -0
- package/scripts/launch-desktop-if-needed.sh +7 -0
- package/scripts/run-desktop.sh +7 -0
- package/scripts/setup.js +198 -0
- package/server.js +139 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { buildDir, root, sessionsFile } from "./config.js";
|
|
4
|
+
|
|
5
|
+
export { buildDir, root, sessionsFile };
|
|
6
|
+
|
|
7
|
+
const staleSessionMs = 24 * 60 * 60 * 1000;
|
|
8
|
+
|
|
9
|
+
export function sessionKey(event = {}) {
|
|
10
|
+
if (event.session_id) return `session:${event.session_id}`;
|
|
11
|
+
if (event.transcript_path) return `transcript:${event.transcript_path}`;
|
|
12
|
+
if (event.cwd) return `cwd:${event.cwd}`;
|
|
13
|
+
return "fallback:claude-session";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function hasSessionIdentity(event = {}) {
|
|
17
|
+
return Boolean(event.session_id || event.transcript_path || event.cwd);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function basename(path) {
|
|
21
|
+
return String(path || "")
|
|
22
|
+
.split(/[\\/]/)
|
|
23
|
+
.filter(Boolean)
|
|
24
|
+
.pop();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function sessionLabel(event = {}) {
|
|
28
|
+
const cwdName = basename(event.cwd);
|
|
29
|
+
if (cwdName) return cwdName;
|
|
30
|
+
|
|
31
|
+
const transcriptName = basename(event.transcript_path);
|
|
32
|
+
if (transcriptName) return transcriptName.replace(/\.jsonl$/, "");
|
|
33
|
+
|
|
34
|
+
const id = event.session_id ? String(event.session_id).slice(0, 8) : "";
|
|
35
|
+
return id ? `Session ${id}` : "Claude session";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function readSessions() {
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse(await readFile(sessionsFile, "utf8"));
|
|
41
|
+
} catch {
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function writeSessions(sessions) {
|
|
47
|
+
await mkdir(dirname(sessionsFile), { recursive: true });
|
|
48
|
+
await writeFile(sessionsFile, `${JSON.stringify(sessions, null, 2)}\n`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function pruneStaleSessions(sessions, now = Date.now()) {
|
|
52
|
+
return Object.fromEntries(
|
|
53
|
+
Object.entries(sessions).filter(([, session]) => {
|
|
54
|
+
const seenAt = Date.parse(session?.lastSeenAt || session?.startedAt || "");
|
|
55
|
+
return Number.isFinite(seenAt) && now - seenAt <= staleSessionMs;
|
|
56
|
+
})
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function recordSession(event = {}) {
|
|
61
|
+
if (!hasSessionIdentity(event)) return undefined;
|
|
62
|
+
const key = sessionKey(event) || `${Date.now()}`;
|
|
63
|
+
const sessions = pruneStaleSessions(await readSessions());
|
|
64
|
+
sessions[key] = {
|
|
65
|
+
cwd: event.cwd,
|
|
66
|
+
label: sessionLabel(event),
|
|
67
|
+
startedAt: sessions[key]?.startedAt || new Date().toISOString(),
|
|
68
|
+
lastSeenAt: new Date().toISOString()
|
|
69
|
+
};
|
|
70
|
+
await writeSessions(sessions);
|
|
71
|
+
return sessions[key];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function labelForEvent(event = {}) {
|
|
75
|
+
const key = sessionKey(event);
|
|
76
|
+
const sessions = await readSessions();
|
|
77
|
+
if (key && sessions[key]?.label) {
|
|
78
|
+
return sessions[key].label;
|
|
79
|
+
}
|
|
80
|
+
return sessionLabel(event);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function withSessionPrefix(message, label) {
|
|
84
|
+
const text = message || "Claude Code needs your attention.";
|
|
85
|
+
return `[${label}] ${text}`;
|
|
86
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import Cocoa
|
|
2
|
+
import Foundation
|
|
3
|
+
import WebKit
|
|
4
|
+
|
|
5
|
+
final class AppDelegate: NSObject, NSApplicationDelegate, WKScriptMessageHandler {
|
|
6
|
+
private var window: NSWindow!
|
|
7
|
+
private var webView: WKWebView!
|
|
8
|
+
private var dragStartMouse: NSPoint?
|
|
9
|
+
private var dragStartWindow: NSPoint?
|
|
10
|
+
|
|
11
|
+
func applicationDidFinishLaunching(_ notification: Notification) {
|
|
12
|
+
let screenFrame = NSScreen.main?.visibleFrame ?? NSRect(x: 0, y: 0, width: 1440, height: 900)
|
|
13
|
+
let size = NSSize(width: 390, height: 390)
|
|
14
|
+
let origin = NSPoint(
|
|
15
|
+
x: screenFrame.maxX - size.width - 28,
|
|
16
|
+
y: screenFrame.minY + 28
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
window = NSWindow(
|
|
20
|
+
contentRect: NSRect(origin: origin, size: size),
|
|
21
|
+
styleMask: [.borderless],
|
|
22
|
+
backing: .buffered,
|
|
23
|
+
defer: false
|
|
24
|
+
)
|
|
25
|
+
window.isOpaque = false
|
|
26
|
+
window.backgroundColor = .clear
|
|
27
|
+
window.hasShadow = false
|
|
28
|
+
window.level = .statusBar
|
|
29
|
+
window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
|
|
30
|
+
window.isMovableByWindowBackground = true
|
|
31
|
+
|
|
32
|
+
let configuration = WKWebViewConfiguration()
|
|
33
|
+
configuration.userContentController.add(self, name: "petDrag")
|
|
34
|
+
webView = WKWebView(frame: NSRect(origin: .zero, size: size), configuration: configuration)
|
|
35
|
+
webView.autoresizingMask = [.width, .height]
|
|
36
|
+
webView.setValue(false, forKey: "drawsBackground")
|
|
37
|
+
|
|
38
|
+
window.contentView = webView
|
|
39
|
+
window.orderFront(nil)
|
|
40
|
+
window.orderFrontRegardless()
|
|
41
|
+
|
|
42
|
+
let env = ProcessInfo.processInfo.environment
|
|
43
|
+
let port = env["CLAUDE_PET_PORT"] ?? "37421"
|
|
44
|
+
let page = env["CLAUDE_PET_DESKTOP_URL"] ?? "http://127.0.0.1:\(port)/desktop.html"
|
|
45
|
+
if let url = URL(string: page) {
|
|
46
|
+
webView.load(URLRequest(url: url))
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
|
51
|
+
guard message.name == "petDrag",
|
|
52
|
+
let body = message.body as? [String: Any],
|
|
53
|
+
let type = body["type"] as? String
|
|
54
|
+
else { return }
|
|
55
|
+
|
|
56
|
+
switch type {
|
|
57
|
+
case "start":
|
|
58
|
+
dragStartMouse = NSEvent.mouseLocation
|
|
59
|
+
dragStartWindow = window.frame.origin
|
|
60
|
+
case "move":
|
|
61
|
+
guard let startMouse = dragStartMouse,
|
|
62
|
+
let startWindow = dragStartWindow
|
|
63
|
+
else { return }
|
|
64
|
+
let mouse = NSEvent.mouseLocation
|
|
65
|
+
let nextOrigin = NSPoint(
|
|
66
|
+
x: startWindow.x + mouse.x - startMouse.x,
|
|
67
|
+
y: startWindow.y + mouse.y - startMouse.y
|
|
68
|
+
)
|
|
69
|
+
window.setFrameOrigin(nextOrigin)
|
|
70
|
+
case "end":
|
|
71
|
+
dragStartMouse = nil
|
|
72
|
+
dragStartWindow = nil
|
|
73
|
+
case "close":
|
|
74
|
+
let env = ProcessInfo.processInfo.environment
|
|
75
|
+
if let buildDir = env["CLAUDE_PET_BUILD_DIR"] {
|
|
76
|
+
let pidPath = URL(fileURLWithPath: buildDir)
|
|
77
|
+
.appendingPathComponent("robot-pet-overlay.pid")
|
|
78
|
+
try? FileManager.default.removeItem(at: pidPath)
|
|
79
|
+
}
|
|
80
|
+
if let projectRoot = env["CLAUDE_PET_ROOT"] {
|
|
81
|
+
let pidPath = URL(fileURLWithPath: projectRoot)
|
|
82
|
+
.appendingPathComponent(".build")
|
|
83
|
+
.appendingPathComponent("robot-pet-overlay.pid")
|
|
84
|
+
let legacyPidPath = URL(fileURLWithPath: projectRoot)
|
|
85
|
+
.appendingPathComponent(".build")
|
|
86
|
+
.appendingPathComponent("claude-pet.pid")
|
|
87
|
+
try? FileManager.default.removeItem(at: pidPath)
|
|
88
|
+
try? FileManager.default.removeItem(at: legacyPidPath)
|
|
89
|
+
}
|
|
90
|
+
NSApp.terminate(nil)
|
|
91
|
+
default:
|
|
92
|
+
break
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let app = NSApplication.shared
|
|
98
|
+
let delegate = AppDelegate()
|
|
99
|
+
app.delegate = delegate
|
|
100
|
+
app.setActivationPolicy(.accessory)
|
|
101
|
+
app.run()
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@shumin13/claude-pet",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "A lightweight macOS desktop companion for Claude Code hook events.",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/shumin13/claude-pet.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/shumin13/claude-pet#readme",
|
|
11
|
+
"bin": {
|
|
12
|
+
"claude-pet": "bin/claude-pet.js"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"bin",
|
|
16
|
+
"hooks",
|
|
17
|
+
"lib",
|
|
18
|
+
"macos",
|
|
19
|
+
"prebuilt",
|
|
20
|
+
"public",
|
|
21
|
+
"scripts",
|
|
22
|
+
"server.js",
|
|
23
|
+
"README.md"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"setup": "node scripts/setup.js",
|
|
27
|
+
"start": "node server.js",
|
|
28
|
+
"hook": "node hooks/claude-pet-notify.js",
|
|
29
|
+
"install:hooks": "node scripts/install-claude-hook.js",
|
|
30
|
+
"build:overlay:local": "rm -rf .build/module-cache && mkdir -p .build/module-cache && CLANG_MODULE_CACHE_PATH=.build/module-cache swiftc macos/RobotPetOverlay.swift -framework Cocoa -framework WebKit -o .build/robot-pet-overlay",
|
|
31
|
+
"build:overlay:package": "rm -rf .build/module-cache && mkdir -p prebuilt/macos .build/module-cache && CLANG_MODULE_CACHE_PATH=.build/module-cache swiftc macos/RobotPetOverlay.swift -framework Cocoa -framework WebKit -o prebuilt/macos/robot-pet-overlay && rm -rf .build/module-cache",
|
|
32
|
+
"launch": "scripts/run-desktop.sh",
|
|
33
|
+
"test": "node tests/pet-integration.test.js",
|
|
34
|
+
"package:zip": "mkdir -p .build && zip -qr .build/claude-pet-source.zip .gitignore README.md package.json server.js bin lib hooks scripts public docs tests macos prebuilt"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
Binary file
|