Repository navigation
Expand file tree
/
Copy pathcc-hook.sh
More file actions
executable file
·276 lines (239 loc) · 9.57 KB
/
Copy pathcc-hook.sh
File metadata and controls
executable file
·276 lines (239 loc) · 9.57 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
#!/bin/bash
# Claude Code Hook
# Parse debug flag before sourcing logging utilities
if [[ "$1" == "--debug" ]]; then
export CLAUDE_DEBUG="true"
shift
fi
# Get project directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CC_TOOLS_DIR="$SCRIPT_DIR"
CONFIG_FILE="$CC_TOOLS_DIR/config/config.json"
RATE_LIMITED_STATE_FILE="$CC_TOOLS_DIR/logs/rate-limited-state.json"
# Source centralized logging utilities
source "$CC_TOOLS_DIR/utils/logging.sh"
log_debug "claude-code-tools hook started."
log_debug "Arguments: $@"
# Read JSON input from stdin
input=$(cat)
log_debug "Received input: $input"
# Extract event information
hook_event_name=$(echo "$input" | jq -r '.hook_event_name // empty')
message=$(echo "$input" | jq -r '.message // empty')
session_id=$(echo "$input" | jq -r '.session_id // empty')
transcript_path=$(echo "$input" | jq -r '.transcript_path // empty')
log_debug "Hook event: $hook_event_name"
log_debug "Session ID: $session_id"
log_debug "Transcript path: $transcript_path"
# Function to get local IANA timezone name
get_local_iana_timezone() {
# For macOS, readlink is often reliable
if [[ "$(uname)" == "Darwin" ]]; then
if [[ -h /etc/localtime ]]; then
local tz_path
tz_path=$(readlink /etc/localtime)
if [[ "$tz_path" == *"zoneinfo/"* ]]; then
echo "$tz_path" | sed 's|.*/zoneinfo/||'
return
fi
fi
# Fallback for macOS
systemsetup -gettimezone | awk -F': ' '{print $2}'
return
fi
# For many Linux systems with systemd
if command -v timedatectl &> /dev/null; then
timedatectl show --property=Timezone --value
return
fi
# Fallback for other Linux systems
if [[ -f /etc/timezone ]]; then
cat /etc/timezone
return
elif [[ -h /etc/localtime ]]; then
readlink /etc/localtime | sed 's|.*/zoneinfo/||'
return
fi
# If all else fails, return UTC as a safe default
echo "UTC"
}
# Function to extract rate limit expiration time from the last line of the transcript
extract_rate_limit_expire_at() {
local transcript_path="$1"
log_debug "Extracting rate limit expiration from transcript: $transcript_path"
if [ -z "$transcript_path" ] || [ ! -f "$transcript_path" ]; then
log_debug "Transcript path is empty or file does not exist."
echo "0"
return
fi
local last_line=$(tail -n 1 "$transcript_path")
if [ -z "$last_line" ]; then
log_debug "Transcript file is empty."
echo "0"
return
fi
local message_text=$(echo "$last_line" | jq -r '.message.content[0].text // empty')
log_debug "Last message text from transcript: $message_text"
# Check for new format: "5-hour limit reached ∙ resets 6pm"
if [[ "$message_text" =~ limit\ reached\ ∙\ resets\ ([0-9]{1,2})(am|pm) ]]; then
log_debug "New rate limit pattern found in message text."
local reset_hour="${BASH_REMATCH[1]}"
local reset_period="${BASH_REMATCH[2]}"
log_debug "Parsed reset time: ${reset_hour}${reset_period}"
# Convert to 24-hour format
local reset_hour_24
if [[ "$reset_period" == "am" ]]; then
if [[ "$reset_hour" == "12" ]]; then
reset_hour_24="0"
else
reset_hour_24="$reset_hour"
fi
else # pm
if [[ "$reset_hour" == "12" ]]; then
reset_hour_24="12"
else
reset_hour_24=$((reset_hour + 12))
fi
fi
log_debug "Converted to 24-hour format: ${reset_hour_24}:00"
# Get current time and target time for today
local current_timestamp=$(date +%s)
local today_reset_timestamp
if command -v gdate >/dev/null 2>&1; then
today_reset_timestamp=$(gdate -d "today ${reset_hour_24}:00" +%s)
else
today_reset_timestamp=$(date -j -f "%Y-%m-%d %H:%M" "$(date +%Y-%m-%d) ${reset_hour_24}:00" +%s 2>/dev/null || echo "0")
fi
# If today's reset time has passed, use tomorrow's reset time
local target_timestamp
if [[ "$today_reset_timestamp" -gt "$current_timestamp" ]]; then
target_timestamp="$today_reset_timestamp"
log_debug "Using today's reset time: $target_timestamp"
else
if command -v gdate >/dev/null 2>&1; then
target_timestamp=$(gdate -d "tomorrow ${reset_hour_24}:00" +%s)
else
target_timestamp=$(date -j -v+1d -f "%Y-%m-%d %H:%M" "$(date +%Y-%m-%d) ${reset_hour_24}:00" +%s 2>/dev/null || echo "0")
fi
log_debug "Using tomorrow's reset time: $target_timestamp"
fi
echo "$target_timestamp"
return
fi
log_debug "No rate limit pattern found in last message."
echo "0"
}
# Function to save rate limit state
save_rate_limit_state() {
local expires_at="$1"
log_debug "Saving rate limit state with expires_at: $expires_at"
local state_data=$(jq -n \
--arg expires_at "$expires_at" \
'{
expires_at: ($expires_at | tonumber)
}')
echo "$state_data" > "$RATE_LIMITED_STATE_FILE"
log_info "Rate limit state saved with expires_at: $expires_at"
}
# Function to check if SMS should be sent for this message
should_send_sms() {
local message="$1"
log_debug "Checking if SMS should be sent"
if [ ! -f "$CONFIG_FILE" ]; then
log_debug "Config file not found. Not sending SMS."
return 1
fi
local sms_enabled=$(jq -r '.sms_enabled // false' "$CONFIG_FILE" 2>/dev/null)
if [ "$sms_enabled" != "true" ]; then
log_debug "SMS is disabled in config. Not sending SMS."
return 1
fi
return 0
}
# Function to check if auto-resume is enabled
is_auto_resume_enabled() {
log_debug "Checking if auto-resume is enabled..."
if [ ! -f "$CONFIG_FILE" ]; then
log_debug "Config file not found. Auto-resume disabled."
return 1
fi
local auto_resume=$(jq -r '.auto_resume // false' "$CONFIG_FILE" 2>/dev/null)
if [ "$auto_resume" != "true" ]; then
log_debug "Auto-resume is false in config. Auto-resume disabled."
return 1
fi
log_debug "Auto-resume is enabled."
return 0
}
# Function to send SMS notification
send_sms() {
local message="$1"
local event_type="$2"
if should_send_sms "$message"; then
log_info "Sending SMS for event '$event_type': ${message:0:100}..."
"$CC_TOOLS_DIR/sms/sms-sender.sh" "$message" "$event_type" &
if [ "$event_type" = "notification" ]; then
local screenshot_enabled=$(jq -r '.screenshot_enabled // false' "$CONFIG_FILE" 2>/dev/null)
if [ "$screenshot_enabled" = "true" ]; then
log_info "Screenshot enabled, sending screenshot for notification event"
local user_phone=$(jq -r '.user_phone // ""' "$CONFIG_FILE" 2>/dev/null)
if [ -n "$user_phone" ]; then
"$CC_TOOLS_DIR/sms/screenshot.sh" "$user_phone" &
fi
fi
fi
else
log_info "Skipping SMS for event '$event_type'."
fi
}
# Main event processing logic
case "$hook_event_name" in
"Stop")
log_debug "Processing 'Stop' event."
local rate_limit_expires_at
rate_limit_expires_at=$(extract_rate_limit_expire_at "$transcript_path")
if [ "$rate_limit_expires_at" -gt 0 ]; then
if is_auto_resume_enabled; then
log_warn "Rate limit detected. Expires at: $rate_limit_expires_at"
save_rate_limit_state "$rate_limit_expires_at"
local local_tz
local_tz=$(get_local_iana_timezone)
log_debug "Determined local timezone: $local_tz"
local readable_time
local tz_abbr
if command -v gdate >/dev/null 2>&1; then
readable_time=$(TZ="$local_tz" gdate -d "@$rate_limit_expires_at" "+%-I%p")
tz_abbr=$(TZ="$local_tz" gdate -d "@$rate_limit_expires_at" "+%Z")
else
readable_time=$(TZ="$local_tz" date -r "$rate_limit_expires_at" "+%-I%p")
tz_abbr=$(TZ="$local_tz" date -r "$rate_limit_expires_at" "+%Z")
fi
local friendly_tz_name
friendly_tz_name=$(echo "$local_tz" | sed 's|.*/||' | sed 's|_| |g')
notification_msg="Claude AI usage limit reached, will be resumed automatically at ${readable_time} ${friendly_tz_name} time (${tz_abbr})"
send_sms "$notification_msg" "error"
else
log_info "Rate limit detected but auto-resume is disabled"
send_sms "Claude hit rate limit. Auto-resume is disabled." "error"
fi
else
stop_message="Claude completed its current task."
send_sms "$stop_message" "Stop"
fi
;;
"Notification")
log_debug "Processing 'Notification' event."
notification_message="$message"
log_debug "Final notification message: ${notification_message:0:200}..."
if [ -n "$notification_message" ]; then
send_sms "$notification_message" "notification"
else
log_debug "Notification message is empty. Nothing to do."
fi
;;
*)
log_debug "Hook event '$hook_event_name' is not handled by this hook. Ignoring."
;;
esac
log_debug "claude-code-tools hook finished."
exit 0