[claude] Add claud settings and ntfy script
This commit is contained in:
125
claude/.claude/scripts/ntfy-recap.py
Normal file
125
claude/.claude/scripts/ntfy-recap.py
Normal file
@ -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
|
||||||
46
claude/.claude/settings.json
Normal file
46
claude/.claude/settings.json
Normal file
@ -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
|
||||||
|
}
|
||||||
@ -41,3 +41,5 @@ bin/.local/bin/agent
|
|||||||
bin/.local/bin/claude
|
bin/.local/bin/claude
|
||||||
bin/.local/bin/cursor
|
bin/.local/bin/cursor
|
||||||
bin/.local/bin/cursor-agent
|
bin/.local/bin/cursor-agent
|
||||||
|
|
||||||
|
**/.claude/settings.local.json
|
||||||
|
|||||||
Reference in New Issue
Block a user