mirror of
https://github.com/galoryber/daedalus
synced 2026-06-21 13:51:16 +00:00
813a6a185e
A framework for running Claude Code as a fully autonomous agent via cron, with persistent memory, task management, and session handoff. Core features: - Checkpoint-based session handoff (save game system) - File-based state: tasks, worklog, questions, lessons-learned - Full autonomy with human-in-the-loop via questions + Pushover - PID management with stale session detection - 11-step autonomous workflow prompt - Customizable for any project via CLAUDE.md + ADAPTING.md guide Named for the myth: Daedalus, the master craftsman who built autonomous machines that worked tirelessly without supervision.
140 lines
4.2 KiB
Python
Executable File
140 lines
4.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""
|
||
Pushover notification script for Daedalus.
|
||
|
||
Sends push notifications when Claude needs human input,
|
||
resolves questions, or reports session status.
|
||
|
||
Environment variables required:
|
||
PUSHOVER_USER_KEY — Your Pushover user key
|
||
PUSHOVER_APP_TOKEN — Your Pushover application token
|
||
|
||
Usage:
|
||
notify.py blocking "Title" "Message body" # High priority, red alert
|
||
notify.py question "Title" "Message body" # Normal priority, question
|
||
notify.py resolved "Title" "Message body" # Low priority, question resolved
|
||
notify.py info "Title" "Message body" # Lowest priority, FYI only
|
||
notify.py error "Title" "Message body" # High priority, session error
|
||
"""
|
||
|
||
import sys
|
||
import os
|
||
import urllib.request
|
||
import urllib.parse
|
||
import json
|
||
from datetime import datetime
|
||
|
||
PUSHOVER_API_URL = "https://api.pushover.net/1/messages.json"
|
||
|
||
# Priority levels map to Pushover priority values:
|
||
# -2 = lowest, -1 = low, 0 = normal, 1 = high, 2 = emergency (requires ack)
|
||
NOTIFICATION_TYPES = {
|
||
"blocking": {
|
||
"priority": 1,
|
||
"sound": "persistent",
|
||
"prefix": "🛑 BLOCKING",
|
||
"description": "Work is stopped — response needed",
|
||
},
|
||
"question": {
|
||
"priority": 0,
|
||
"sound": "pushover",
|
||
"prefix": "💬 QUESTION",
|
||
"description": "Claude has a non-blocking question",
|
||
},
|
||
"resolved": {
|
||
"priority": -1,
|
||
"sound": "none",
|
||
"prefix": "✅ RESOLVED",
|
||
"description": "A previous question is no longer needed",
|
||
},
|
||
"info": {
|
||
"priority": -2,
|
||
"sound": "none",
|
||
"prefix": "ℹ️ INFO",
|
||
"description": "Status update, no action needed",
|
||
},
|
||
"error": {
|
||
"priority": 1,
|
||
"sound": "siren",
|
||
"prefix": "❌ ERROR",
|
||
"description": "Session error or unexpected failure",
|
||
},
|
||
}
|
||
|
||
|
||
def send_notification(notif_type: str, title: str, message: str) -> bool:
|
||
"""Send a Pushover notification.
|
||
|
||
Returns True on success, False on failure (prints error to stderr).
|
||
"""
|
||
user_key = os.environ.get("PUSHOVER_USER_KEY")
|
||
app_token = os.environ.get("PUSHOVER_APP_TOKEN")
|
||
|
||
if not user_key or not app_token:
|
||
print(
|
||
"ERROR: PUSHOVER_USER_KEY and PUSHOVER_APP_TOKEN must be set.",
|
||
file=sys.stderr,
|
||
)
|
||
return False
|
||
|
||
if notif_type not in NOTIFICATION_TYPES:
|
||
print(
|
||
f"ERROR: Unknown notification type '{notif_type}'. "
|
||
f"Valid types: {', '.join(NOTIFICATION_TYPES.keys())}",
|
||
file=sys.stderr,
|
||
)
|
||
return False
|
||
|
||
config = NOTIFICATION_TYPES[notif_type]
|
||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||
|
||
full_title = f"{config['prefix']}: {title}"
|
||
full_message = f"{message}\n\n🕐 {timestamp}"
|
||
|
||
data = urllib.parse.urlencode({
|
||
"token": app_token,
|
||
"user": user_key,
|
||
"title": full_title,
|
||
"message": full_message,
|
||
"priority": config["priority"],
|
||
"sound": config["sound"],
|
||
}).encode("utf-8")
|
||
|
||
try:
|
||
req = urllib.request.Request(PUSHOVER_API_URL, data=data)
|
||
with urllib.request.urlopen(req, timeout=10) as response:
|
||
result = json.loads(response.read().decode("utf-8"))
|
||
if result.get("status") == 1:
|
||
print(f"Notification sent: {full_title}")
|
||
return True
|
||
else:
|
||
print(
|
||
f"ERROR: Pushover API error: {result}",
|
||
file=sys.stderr,
|
||
)
|
||
return False
|
||
except Exception as e:
|
||
print(f"ERROR: Failed to send notification: {e}", file=sys.stderr)
|
||
return False
|
||
|
||
|
||
def main():
|
||
if len(sys.argv) < 4:
|
||
print("Usage: notify.py <type> \"Title\" \"Message\"")
|
||
print(f"Types: {', '.join(NOTIFICATION_TYPES.keys())}")
|
||
print("\nDescriptions:")
|
||
for name, config in NOTIFICATION_TYPES.items():
|
||
print(f" {name:10s} — {config['description']}")
|
||
sys.exit(1)
|
||
|
||
notif_type = sys.argv[1]
|
||
title = sys.argv[2]
|
||
message = sys.argv[3]
|
||
|
||
success = send_notification(notif_type, title, message)
|
||
sys.exit(0 if success else 1)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|