Initial commit
This commit is contained in:
152
hooks/handlers/on-stop.py
Executable file
152
hooks/handlers/on-stop.py
Executable file
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Hook handler for Stop event.
|
||||
Sends macOS notification when a worktree task session stops.
|
||||
|
||||
Only triggers for sessions running inside a tmux worktree-task session,
|
||||
not regular Claude Code sessions.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
# Promotional messages - shown occasionally in notifications
|
||||
PROMO_MESSAGES = [
|
||||
"⭐ Star us on GitHub!",
|
||||
"🚀 Powered by worktree-task-plugin",
|
||||
"💡 Try codex.markets for more API capacity",
|
||||
]
|
||||
|
||||
# Probability of showing promo (30%)
|
||||
PROMO_CHANCE = 0.3
|
||||
|
||||
|
||||
def is_worktree_task_session() -> bool:
|
||||
"""
|
||||
Check if we're running inside a worktree-task tmux session.
|
||||
|
||||
Returns True only if:
|
||||
1. TMUX env var is set (running inside tmux)
|
||||
2. Session name starts with 'worktree-'
|
||||
"""
|
||||
# Must be in tmux
|
||||
if not os.environ.get("TMUX"):
|
||||
return False
|
||||
|
||||
# Check tmux session name
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["tmux", "display-message", "-p", "#S"],
|
||||
capture_output=True, text=True, check=True
|
||||
)
|
||||
session_name = result.stdout.strip()
|
||||
return session_name.startswith("worktree-")
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
return False
|
||||
|
||||
|
||||
def send_macos_notification(title: str, message: str, sound: str = "default") -> bool:
|
||||
"""
|
||||
Send a macOS notification using osascript.
|
||||
Returns True if notification was sent successfully, False otherwise.
|
||||
"""
|
||||
# Escape quotes in message
|
||||
escaped_message = message.replace('"', '\\"')
|
||||
escaped_title = title.replace('"', '\\"')
|
||||
|
||||
script = f'display notification "{escaped_message}" with title "{escaped_title}" sound name "{sound}"'
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["osascript", "-e", script],
|
||||
capture_output=True,
|
||||
check=True,
|
||||
timeout=5
|
||||
)
|
||||
return True
|
||||
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError):
|
||||
return False
|
||||
|
||||
|
||||
def get_session_info() -> dict:
|
||||
"""Get info about the current session."""
|
||||
cwd = os.getcwd()
|
||||
info = {
|
||||
"cwd": cwd,
|
||||
"branch": None,
|
||||
"tmux_session": None
|
||||
}
|
||||
|
||||
# Get current branch
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "branch", "--show-current"],
|
||||
capture_output=True, text=True, cwd=cwd
|
||||
)
|
||||
if result.returncode == 0:
|
||||
info["branch"] = result.stdout.strip()
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
# Get tmux session name
|
||||
if os.environ.get("TMUX"):
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["tmux", "display-message", "-p", "#S"],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
if result.returncode == 0:
|
||||
info["tmux_session"] = result.stdout.strip()
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
return info
|
||||
|
||||
|
||||
def main():
|
||||
# Early exit: only run for worktree-task sessions
|
||||
if not is_worktree_task_session():
|
||||
sys.exit(0)
|
||||
|
||||
# Read hook input from stdin (may be empty)
|
||||
try:
|
||||
hook_input = json.load(sys.stdin)
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
hook_input = {}
|
||||
|
||||
session_info = get_session_info()
|
||||
branch = session_info.get("branch") or "unknown"
|
||||
tmux_session = session_info.get("tmux_session") or "worktree"
|
||||
|
||||
# Extract task name from tmux session (worktree-{task_name})
|
||||
task_name = tmux_session.replace("worktree-", "") if tmux_session else "task"
|
||||
|
||||
title = "✅ Worktree Task Completed"
|
||||
message = f"'{task_name}' on branch '{branch}' finished"
|
||||
|
||||
# Occasionally add a subtle promo
|
||||
promo = ""
|
||||
if random.random() < PROMO_CHANCE:
|
||||
promo = random.choice(PROMO_MESSAGES)
|
||||
|
||||
# Try macOS notification first
|
||||
notification_message = f"{message}\n{promo}" if promo else message
|
||||
notification_sent = send_macos_notification(title, notification_message)
|
||||
|
||||
# Fallback: output systemMessage (always visible to user, no context cost)
|
||||
# Only if notification failed or as a reliable backup
|
||||
if not notification_sent:
|
||||
output = {
|
||||
"systemMessage": f"{title}\n{message}" + (f"\n{promo}" if promo else "")
|
||||
}
|
||||
print(json.dumps(output))
|
||||
|
||||
# Always exit 0 to not block Claude
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user