144 lines
4.0 KiB
Python
Executable File
144 lines
4.0 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Hook handler for SessionEnd event.
|
|
Sends macOS notification and logs when a worktree session ends.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
|
|
def send_macos_notification(title: str, message: str, sound: str = "default"):
|
|
"""Send a macOS notification using osascript."""
|
|
script = f'''
|
|
display notification "{message}" with title "{title}" sound name "{sound}"
|
|
'''
|
|
subprocess.run(["osascript", "-e", script], capture_output=True)
|
|
|
|
|
|
def send_terminal_notifier(title: str, message: str, subtitle: str = None):
|
|
"""Send notification via terminal-notifier if available."""
|
|
try:
|
|
cmd = [
|
|
"terminal-notifier",
|
|
"-title", title,
|
|
"-message", message,
|
|
"-sound", "default",
|
|
"-group", "worktree-task"
|
|
]
|
|
if subtitle:
|
|
cmd.extend(["-subtitle", subtitle])
|
|
subprocess.run(cmd, capture_output=True, check=True)
|
|
return True
|
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
return False
|
|
|
|
|
|
def log_session_end(session_id: str, reason: str, cwd: str):
|
|
"""Log session end to a file for history tracking."""
|
|
log_dir = Path.home() / ".claude" / "plugins" / "worktree-task" / "logs"
|
|
log_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
log_file = log_dir / "session-history.log"
|
|
|
|
entry = {
|
|
"timestamp": datetime.now().isoformat(),
|
|
"session_id": session_id,
|
|
"reason": reason,
|
|
"cwd": cwd
|
|
}
|
|
|
|
with open(log_file, "a") as f:
|
|
f.write(json.dumps(entry) + "\n")
|
|
|
|
|
|
def get_session_info() -> dict:
|
|
"""Try to determine session context."""
|
|
cwd = os.getcwd()
|
|
|
|
info = {
|
|
"cwd": cwd,
|
|
"is_worktree": False,
|
|
"branch": None,
|
|
"project": None
|
|
}
|
|
|
|
# Get current branch
|
|
try:
|
|
branch_result = subprocess.run(
|
|
["git", "branch", "--show-current"],
|
|
capture_output=True, text=True, cwd=cwd
|
|
)
|
|
if branch_result.returncode == 0:
|
|
info["branch"] = branch_result.stdout.strip()
|
|
except Exception:
|
|
pass
|
|
|
|
# Check if worktree by path pattern
|
|
if "-" in os.path.basename(cwd) and "worktree" not in cwd.lower():
|
|
# Might be a worktree like "project-feature-branch"
|
|
info["is_worktree"] = True
|
|
|
|
# Also check git worktree list
|
|
try:
|
|
result = subprocess.run(
|
|
["git", "rev-parse", "--git-common-dir"],
|
|
capture_output=True, text=True, cwd=cwd
|
|
)
|
|
git_common = result.stdout.strip()
|
|
git_dir_result = subprocess.run(
|
|
["git", "rev-parse", "--git-dir"],
|
|
capture_output=True, text=True, cwd=cwd
|
|
)
|
|
git_dir = git_dir_result.stdout.strip()
|
|
|
|
# If git-dir != git-common-dir, we're in a worktree
|
|
if git_common != git_dir and ".git/worktrees" in git_dir:
|
|
info["is_worktree"] = True
|
|
except Exception:
|
|
pass
|
|
|
|
# Extract project name
|
|
info["project"] = os.path.basename(cwd)
|
|
|
|
return info
|
|
|
|
|
|
def main():
|
|
# Read hook input from stdin
|
|
try:
|
|
hook_input = json.load(sys.stdin)
|
|
except json.JSONDecodeError:
|
|
hook_input = {}
|
|
|
|
session_id = hook_input.get("session_id", "unknown")
|
|
reason = hook_input.get("reason", "unknown") # e.g., "clear", "logout", "exit"
|
|
|
|
session_info = get_session_info()
|
|
|
|
# Log all worktree session ends
|
|
if session_info.get("is_worktree"):
|
|
log_session_end(session_id, reason, session_info.get("cwd", ""))
|
|
|
|
branch = session_info.get("branch", "unknown")
|
|
project = session_info.get("project", "unknown")
|
|
|
|
title = "Worktree Session Ended"
|
|
message = f"Branch: {branch}"
|
|
subtitle = f"Reason: {reason}"
|
|
|
|
# Try terminal-notifier first, fallback to osascript
|
|
if not send_terminal_notifier(title, message, subtitle):
|
|
send_macos_notification(title, f"{message} ({reason})")
|
|
|
|
# Always exit 0 to not block Claude
|
|
sys.exit(0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|