Skip to content

Commit 895f72c

Browse files
committed
Seems to work without being spammy
1 parent eca336e commit 895f72c

11 files changed

Lines changed: 210 additions & 149 deletions

File tree

.claude/hooks/notify_slack.py

Lines changed: 0 additions & 124 deletions
This file was deleted.

.claude/hooks/notify_slack.sh

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
#!/usr/bin/env bash
2+
3+
set -euo pipefail
4+
5+
# Get script directory for fallback env path
6+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7+
8+
# Prefer global env file, fall back to repo-local .claude/.env.claude
9+
PREFERRED_ENV_PATH="$HOME/.env/orchestra/.env.claude"
10+
FALLBACK_ENV_PATH="$(dirname "$SCRIPT_DIR")/.env.claude"
11+
12+
if [[ -f "$PREFERRED_ENV_PATH" ]]; then
13+
DOTENV_PATH="$PREFERRED_ENV_PATH"
14+
else
15+
DOTENV_PATH="$FALLBACK_ENV_PATH"
16+
fi
17+
18+
# Load env file if it exists
19+
if [[ -f "$DOTENV_PATH" ]]; then
20+
# Export variables from env file (handles quoted values and comments)
21+
set -a
22+
# shellcheck disable=SC1090
23+
source "$DOTENV_PATH"
24+
set +a
25+
echo "Loaded env from: $DOTENV_PATH" >&2
26+
else
27+
echo "No env file found at $PREFERRED_ENV_PATH or $FALLBACK_ENV_PATH" >&2
28+
fi
29+
30+
# Check for SLACK_WEBHOOK_URL
31+
if [[ -z "${SLACK_WEBHOOK_URL:-}" ]]; then
32+
echo "SLACK_WEBHOOK_URL is not set" >&2
33+
exit 1
34+
fi
35+
36+
# Read input JSON from stdin
37+
INPUT_JSON=$(cat)
38+
39+
if [[ -z "$INPUT_JSON" ]]; then
40+
echo "Failed to read input from stdin" >&2
41+
exit 1
42+
fi
43+
44+
# Parse JSON fields using jq
45+
EVENT=$(echo "$INPUT_JSON" | jq -r '.hook_event_name // ""')
46+
CWD=$(echo "$INPUT_JSON" | jq -r '.cwd // ""')
47+
SESSION_ID=$(echo "$INPUT_JSON" | jq -r '.session_id // ""')
48+
49+
# Get current timestamp
50+
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
51+
52+
# Function to extract final response from transcript JSONL file
53+
get_final_response() {
54+
local transcript_path="$1"
55+
local max_length="${2:-1500}"
56+
57+
# Expand ~ in path
58+
transcript_path="${transcript_path/#\~/$HOME}"
59+
60+
if [[ -z "$transcript_path" || ! -f "$transcript_path" ]]; then
61+
echo "(transcript not found)"
62+
return
63+
fi
64+
65+
# Extract final assistant response from JSONL
66+
# Look for entries with type=assistant and extract text content
67+
local final_response=""
68+
final_response=$(jq -rs '
69+
[.[] | select(.type == "assistant" and .message.role == "assistant" and .message.content)]
70+
| last
71+
| .message.content
72+
| if type == "array" then
73+
[.[] | if type == "object" and .type == "text" then .text elif type == "string" then . else empty end]
74+
| join("\n")
75+
elif type == "string" then
76+
.
77+
else
78+
""
79+
end
80+
// ""
81+
' "$transcript_path" 2>/dev/null || echo "")
82+
83+
if [[ -z "$final_response" ]]; then
84+
echo "(no response found)"
85+
return
86+
fi
87+
88+
# Truncate if too long
89+
if [[ ${#final_response} -gt $max_length ]]; then
90+
echo "${final_response:0:$max_length}...
91+
_(truncated)_"
92+
else
93+
echo "$final_response"
94+
fi
95+
}
96+
97+
# Build Slack message based on event type
98+
TEXT=""
99+
case "$EVENT" in
100+
"Notification")
101+
NOTIFICATION_TYPE=$(echo "$INPUT_JSON" | jq -r '.notification_type // ""')
102+
MESSAGE=$(echo "$INPUT_JSON" | jq -r '.message // ""')
103+
TEXT="🧠 Claude Code: *${NOTIFICATION_TYPE}*
104+
${MESSAGE}
105+
• time: \`${TIMESTAMP}\`
106+
• cwd: \`${CWD}\`
107+
• session: \`${SESSION_ID}\`"
108+
;;
109+
"Stop")
110+
STOP_HOOK_ACTIVE=$(echo "$INPUT_JSON" | jq -r '.stop_hook_active // false')
111+
TRANSCRIPT_PATH=$(echo "$INPUT_JSON" | jq -r '.transcript_path // ""')
112+
FINAL_RESPONSE=$(get_final_response "$TRANSCRIPT_PATH")
113+
TEXT="✅ Claude Code: *Stop*
114+
• time: \`${TIMESTAMP}\`
115+
• cwd: \`${CWD}\`
116+
• session: \`${SESSION_ID}\`
117+
• stop_hook_active: \`${STOP_HOOK_ACTIVE}\`
118+
119+
*Final Response:*
120+
\`\`\`
121+
${FINAL_RESPONSE}
122+
\`\`\`"
123+
;;
124+
*)
125+
TEXT="Claude Code hook: ${EVENT}
126+
• time: \`${TIMESTAMP}\`
127+
• cwd: ${CWD}
128+
• session: ${SESSION_ID}"
129+
;;
130+
esac
131+
132+
# Build Slack payload
133+
SLACK_PAYLOAD=$(jq -n --arg text "$TEXT" '{"text": $text}')
134+
135+
# Send to Slack
136+
HTTP_RESPONSE=$(curl -s -w "\n%{http_code}" \
137+
-X POST "$SLACK_WEBHOOK_URL" \
138+
-H "Content-Type: application/json" \
139+
-d "$SLACK_PAYLOAD" \
140+
--max-time 10 \
141+
2>&1) || {
142+
echo "Failed to send Slack notification: curl error" >&2
143+
exit 1
144+
}
145+
146+
# Extract HTTP status code (last line)
147+
HTTP_CODE=$(echo "$HTTP_RESPONSE" | tail -n1)
148+
149+
# Check for success (2xx status codes)
150+
if [[ ! "$HTTP_CODE" =~ ^2[0-9][0-9]$ ]]; then
151+
RESPONSE_BODY=$(echo "$HTTP_RESPONSE" | sed '$d')
152+
echo "Failed to send Slack notification: HTTP $HTTP_CODE - $RESPONSE_BODY" >&2
153+
exit 1
154+
fi

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@
1212
**/sandbox/db/*
1313
**/site
1414
**/docker/searxng/settings.yml.new
15+
**/*.pyc
16+
**/*.pyo
1517

16-
backend/src/public
18+
19+
**/backend/src/public
1720
**/.worktrees/
1821
**/.playwright*
1922
**/.backups/

.ralph/progress.txt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
- docker-compose.yml was NOT modified
1313
- Files changed: `backend/Makefile` (feature), plus 5 formatting fixes
1414
- **Learnings for future iterations:**
15-
- `watchdog` comes as a transitive dep, likely via uvicorn[standard] - no need to add it
16-
- TaskIQ's `--reload` flag uses watchdog under the hood
15+
- `watchdog` as transitive dep is NOT sufficient for TaskIQ reload - must install `taskiq[reload]` extra
16+
- `taskiq[reload]` belongs in dev dependencies since `--reload` is only for development
17+
- TaskIQ's `FileWatcher` is `None` without the reload extra, causing `TypeError: 'NoneType' object is not callable`
1718
- Ruff formatting may touch files unrelated to your changes - include them in the commit
1819
---

backend/.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
__pycache__/
2+
*.pyc
3+
*.pyo
4+
.pytest_cache/
5+
*.egg-info/
6+
.venv/

backend/Makefile

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
.PHONY: dev claude build run tag seeds test format
22

3+
ENV_FILE ?= $(HOME)/.env/orchestra/.env.backend
4+
35
dev:
4-
uv run uvicorn main:app --reload --host 0.0.0.0 --port 8000 --log-level debug --env-file ~/.env/orchestra/.env.backend
6+
uv run uvicorn main:app --reload --host 0.0.0.0 --port 8000 --log-level debug --env-file $(ENV_FILE)
57

68
dev.worker:
7-
@set -a && . ~/.env/orchestra/.env.backend && set +a && uv run taskiq worker src.workers.tasks:broker --reload --reload-dir src
9+
@set -a && . $(ENV_FILE) && set +a && uv run taskiq worker src.workers.tasks:broker --reload --reload-dir ./
810

9-
claude:
10-
uv run uvicorn main:app --reload --host 0.0.0.0 --port 8031 --log-level debug --env-file ~/secrets/.env.orchestra.backend
11+
dev.claude:
12+
uv run uvicorn main:app --reload --host 0.0.0.0 --port 8031 --log-level debug --env-file $(ENV_FILE)
1113

1214
build:
1315
bash scripts/build.sh
@@ -16,28 +18,28 @@ tag:
1618
bash scripts/tag.sh
1719

1820
seeds.user:
19-
uv run python -m seeds.user_seeder --env-file ~/.env/orchestra/.env.backend
21+
uv run python -m seeds.user_seeder --env-file $(ENV_FILE)
2022

2123
format:
2224
uvx ruff format
2325

2426
test:
25-
@set -a && . ~/.env/orchestra/.env.backend && set +a && uv run pytest
27+
@set -a && . $(ENV_FILE) && set +a && uv run pytest
2628

2729
migrate.up:
28-
ENV_FILE=~/.env/orchestra/.env.backend uv run alembic upgrade head
30+
ENV_FILE=$(ENV_FILE) uv run alembic upgrade head
2931

3032
migrate.down:
31-
ENV_FILE=~/.env/orchestra/.env.backend uv run alembic downgrade -1
33+
ENV_FILE=$(ENV_FILE) uv run alembic downgrade -1
3234

3335
migrate.history:
34-
ENV_FILE=~/.env/orchestra/.env.backend uv run alembic history
36+
ENV_FILE=$(ENV_FILE) uv run alembic history
3537

3638
migrate.revision:
37-
ENV_FILE=~/.env/orchestra/.env.backend uv run alembic revision -m "description_of_changes"
39+
ENV_FILE=$(ENV_FILE) uv run alembic revision -m "description_of_changes"
3840

3941
migrate.upgrade:
40-
ENV_FILE=~/.env/orchestra/.env.backend uv run alembic upgrade +1
42+
ENV_FILE=$(ENV_FILE) uv run alembic upgrade +1
4143

4244
migrate.downgrade:
43-
ENV_FILE=~/.env/orchestra/.env.backend uv run alembic downgrade -1
45+
ENV_FILE=$(ENV_FILE) uv run alembic downgrade -1

backend/pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ dependencies = [
5858
"fastmcp>=2.14.1",
5959
"taskiq-redis>=1.0.0",
6060
"redis>=5.0.0",
61+
"taskiq[reload]>=0.12.1",
6162
]
6263

6364
[dependency-groups]
@@ -68,7 +69,7 @@ dev = [
6869
"pytest>=8.4.1",
6970
"pytest-asyncio>=1.1.0",
7071
"pytest-mock>=3.14.0",
71-
"respx>=0.22.0",
72+
"respx>=0.22.0"
7273
]
7374

7475
[tool.pytest.ini_options]

0 commit comments

Comments
 (0)