diff --git a/claude/.claude/scripts/ntfy-recap.py b/claude/.claude/scripts/ntfy-recap.py new file mode 100644 index 0000000..63db127 --- /dev/null +++ b/claude/.claude/scripts/ntfy-recap.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +import json +import sys +import time +import urllib.request + +NTFY_URL = "https://ntfy.unbl.ink/hr-agents" + + +def read_jsonl(path): + entries = [] + try: + with open(path) as f: + for line in f: + line = line.strip() + if not line: + continue + try: + entries.append(json.loads(line)) + except json.JSONDecodeError: + continue + except OSError: + pass + return entries + + +def load_jsonl(path, has_assistant_text=None): + if not path: + return [] + entries = read_jsonl(path) + if has_assistant_text is not None: + for _ in range(4): + if has_assistant_text(entries): + break + time.sleep(0.5) + entries = read_jsonl(path) + return entries + + +def text_blocks(entry): + content = entry.get("message", {}).get("content") + if isinstance(content, str): + yield content + elif isinstance(content, list): + for c in content: + if isinstance(c, dict) and c.get("type") == "text": + yield c.get("text", "") + + +def first_text(entries, role): + for e in entries: + if e.get("type") != role: + continue + for t in text_blocks(e): + if t.strip(): + return t.strip() + return "" + + +def last_text(entries, role): + for e in reversed(entries): + if e.get("type") != role: + continue + for t in text_blocks(e): + if t.strip(): + return t.strip() + return "" + + +def truncate(s, n): + s = " ".join(s.split()) + return s if len(s) <= n else s[: n - 1] + "…" + + +def post(title, message, tags, priority=None): + req = urllib.request.Request( + NTFY_URL, data=message.encode("utf-8"), method="POST" + ) + req.add_header("Title", title) + req.add_header("Tags", tags) + if priority: + req.add_header("Priority", priority) + urllib.request.urlopen(req, timeout=10) + + +def main(): + payload = json.load(sys.stdin) + is_subagent = bool(payload.get("agent_transcript_path")) or ( + payload.get("hook_event_name") == "SubagentStop" + ) + + if is_subagent: + agent_type = payload.get("agent_type", "subagent") + entries = load_jsonl( + payload.get("agent_transcript_path"), + has_assistant_text=lambda e: bool(last_text(e, "assistant")), + ) + task = truncate(first_text(entries, "user"), 200) + result = truncate(last_text(entries, "assistant"), 500) + message = ( + "\n\n".join(p for p in (f"Task: {task}" if task else "", f"Result: {result}" if result else "")) + or "(no transcript content found)" + ) + post(f"Subagent finished: {agent_type}", message, "robot", priority="low") + else: + entries = load_jsonl( + payload.get("transcript_path"), + has_assistant_text=lambda e: bool(last_text(e, "assistant")), + ) + request = truncate(last_text(entries, "user"), 200) + response = truncate(last_text(entries, "assistant"), 500) + message = ( + "\n\n".join(p for p in (f"Last request: {request}" if request else "", f"Last reply: {response}" if response else "")) + or "(no transcript content found)" + ) + reason = payload.get("reason", "") + title = f"Claude Code: session ended ({reason})" if reason else "Claude Code: session ended" + post(title, message, "white_check_mark") + + +if __name__ == "__main__": + try: + main() + except Exception: + pass diff --git a/claude/.claude/settings.json b/claude/.claude/settings.json new file mode 100644 index 0000000..5c5f57c --- /dev/null +++ b/claude/.claude/settings.json @@ -0,0 +1,46 @@ +{ + "env": { + "CLAUDE_CODE_SUBAGENT_MODEL": "claude-sonnet-4-6" + }, + "model": "sonnet", + "hooks": { + "SubagentStop": [ + { + "hooks": [ + { + "type": "command", + "command": "python3 ~/.claude/scripts/ntfy-recap.py 2>/dev/null || true", + "timeout": 15, + "async": true + } + ] + } + ], + "SessionEnd": [ + { + "hooks": [ + { + "type": "command", + "command": "python3 ~/.claude/scripts/ntfy-recap.py 2>/dev/null || true", + "timeout": 15, + "async": true + } + ] + } + ] + }, + "enabledPlugins": { + "pyright-lsp@claude-plugins-official": true, + "caveman@caveman": true + }, + "extraKnownMarketplaces": { + "caveman": { + "source": { + "source": "github", + "repo": "JuliusBrussee/caveman" + } + } + }, + "theme": "dark-daltonized", + "skipAutoPermissionPrompt": true +} diff --git a/git/.gitignore_global b/git/.gitignore_global index 5309ec7..1f44197 100644 --- a/git/.gitignore_global +++ b/git/.gitignore_global @@ -41,3 +41,5 @@ bin/.local/bin/agent bin/.local/bin/claude bin/.local/bin/cursor bin/.local/bin/cursor-agent + +**/.claude/settings.local.json