@rulemetric/hooks 0.7.3 → 0.7.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/scripts/_config.sh +11 -0
- package/scripts/_log.sh +9 -0
- package/scripts/_normalize.sh +23 -6
- package/scripts/assistant-response.sh +15 -3
- package/scripts/jq +1 -0
- package/scripts/post-tool-use.sh +20 -3
- package/scripts/session-end.sh +30 -14
- package/scripts/user-prompt.sh +20 -4
package/package.json
CHANGED
package/scripts/_config.sh
CHANGED
|
@@ -9,6 +9,17 @@
|
|
|
9
9
|
# Env var RULEMETRIC_ORG_ID always wins over the file. The variable is
|
|
10
10
|
# exported so child curl/jq invocations and downstream scripts can see it.
|
|
11
11
|
|
|
12
|
+
# npm packages a small jq-compatible fallback beside these scripts. Make it
|
|
13
|
+
# discoverable on hosts that do not have jq installed system-wide.
|
|
14
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
15
|
+
_rulemetric_scripts_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
16
|
+
if [ -x "$_rulemetric_scripts_dir/jq" ]; then
|
|
17
|
+
PATH="$_rulemetric_scripts_dir:$PATH"
|
|
18
|
+
export PATH
|
|
19
|
+
fi
|
|
20
|
+
unset _rulemetric_scripts_dir
|
|
21
|
+
fi
|
|
22
|
+
|
|
12
23
|
_rulemetric_config_dir() {
|
|
13
24
|
if [ -n "${RULEMETRIC_CONFIG_DIR:-}" ]; then
|
|
14
25
|
printf '%s' "$RULEMETRIC_CONFIG_DIR"
|
package/scripts/_log.sh
CHANGED
|
@@ -3,6 +3,15 @@
|
|
|
3
3
|
# Appends JSON lines to $TMPDIR/rulemetric/hook.log
|
|
4
4
|
# Never fails — all writes guarded with || true
|
|
5
5
|
|
|
6
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
7
|
+
_rulemetric_log_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
+
if [ -x "$_rulemetric_log_dir/jq" ]; then
|
|
9
|
+
PATH="$_rulemetric_log_dir:$PATH"
|
|
10
|
+
export PATH
|
|
11
|
+
fi
|
|
12
|
+
unset _rulemetric_log_dir
|
|
13
|
+
fi
|
|
14
|
+
|
|
6
15
|
_rulemetric_log() {
|
|
7
16
|
local script="${1:-unknown}"
|
|
8
17
|
local outcome="${2:-unknown}"
|
package/scripts/_normalize.sh
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
4
|
+
_rulemetric_normalize_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
|
+
if [ -x "$_rulemetric_normalize_dir/jq" ]; then
|
|
6
|
+
PATH="$_rulemetric_normalize_dir:$PATH"
|
|
7
|
+
export PATH
|
|
8
|
+
fi
|
|
9
|
+
unset _rulemetric_normalize_dir
|
|
10
|
+
fi
|
|
2
11
|
# RuleMetric: Normalize hook input across AI coding tools
|
|
3
12
|
# Detects Claude Code, VS Code Copilot, Copilot CLI, and Cursor input formats,
|
|
4
13
|
# then exports canonical HOOK_* variables for use by other scripts.
|
|
@@ -53,6 +62,13 @@ _rulemetric_normalize() {
|
|
|
53
62
|
if [ "${RULEMETRIC_HOOK_TOOL:-}" = "antigravity" ]; then
|
|
54
63
|
HOOK_TOOL="antigravity"
|
|
55
64
|
HOOK_SESSION_ID=$(echo "$input" | jq -r '.conversationId // empty' 2>/dev/null)
|
|
65
|
+
# The validation below only passes bare UUIDs — a non-UUID conversationId
|
|
66
|
+
# would be silently blanked, killing all Antigravity capture. Hash anything
|
|
67
|
+
# else into a stable synthetic ID (same defense as cursor/copilot_cli).
|
|
68
|
+
if [ -n "$HOOK_SESSION_ID" ] && \
|
|
69
|
+
! echo "$HOOK_SESSION_ID" | grep -qiE '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'; then
|
|
70
|
+
HOOK_SESSION_ID="antigravity-$(_rulemetric_short_hash "$HOOK_SESSION_ID")"
|
|
71
|
+
fi
|
|
56
72
|
HOOK_CWD=$(echo "$input" | jq -r '
|
|
57
73
|
.workspacePaths[0] //
|
|
58
74
|
.workspace_roots[0] //
|
|
@@ -75,9 +91,9 @@ _rulemetric_normalize() {
|
|
|
75
91
|
local cwd
|
|
76
92
|
cwd=$(echo "$input" | jq -r '.cwd // empty')
|
|
77
93
|
if [ -n "$cwd" ]; then
|
|
78
|
-
HOOK_SESSION_ID="copilot-cli-$(
|
|
94
|
+
HOOK_SESSION_ID="copilot-cli-$(_rulemetric_short_hash "$cwd")"
|
|
79
95
|
else
|
|
80
|
-
HOOK_SESSION_ID="copilot-cli-$(
|
|
96
|
+
HOOK_SESSION_ID="copilot-cli-$(_rulemetric_short_hash "unknown")"
|
|
81
97
|
fi
|
|
82
98
|
|
|
83
99
|
HOOK_CWD="$cwd"
|
|
@@ -124,9 +140,9 @@ _rulemetric_normalize() {
|
|
|
124
140
|
local conversation_id
|
|
125
141
|
conversation_id=$(echo "$input" | jq -r '.conversation_id // empty')
|
|
126
142
|
if [ -n "$conversation_id" ]; then
|
|
127
|
-
HOOK_SESSION_ID="cursor-$(
|
|
143
|
+
HOOK_SESSION_ID="cursor-$(_rulemetric_short_hash "$conversation_id")"
|
|
128
144
|
else
|
|
129
|
-
HOOK_SESSION_ID="cursor-$(
|
|
145
|
+
HOOK_SESSION_ID="cursor-$(_rulemetric_short_hash "unknown")"
|
|
130
146
|
fi
|
|
131
147
|
|
|
132
148
|
HOOK_CWD=$(echo "$input" | jq -r '.workspace_roots[0] // .cwd // empty')
|
|
@@ -191,9 +207,10 @@ _rulemetric_normalize() {
|
|
|
191
207
|
fi
|
|
192
208
|
|
|
193
209
|
# Validate HOOK_SESSION_ID format (prevent path traversal)
|
|
194
|
-
# Accepts: standard UUIDs (case-insensitive), copilot-cli-<hex>,
|
|
210
|
+
# Accepts: standard UUIDs (case-insensitive), plus copilot-cli-<hex>,
|
|
211
|
+
# cursor-<hex>, and antigravity-<hex> synthetic IDs
|
|
195
212
|
if [ -n "$HOOK_SESSION_ID" ]; then
|
|
196
|
-
if ! echo "$HOOK_SESSION_ID" | grep -qiE '^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|copilot-cli-[0-9a-f]{1,64}|cursor-[0-9a-f]{1,64})$'; then
|
|
213
|
+
if ! echo "$HOOK_SESSION_ID" | grep -qiE '^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|copilot-cli-[0-9a-f]{1,64}|cursor-[0-9a-f]{1,64}|antigravity-[0-9a-f]{1,64})$'; then
|
|
197
214
|
HOOK_SESSION_ID=""
|
|
198
215
|
fi
|
|
199
216
|
fi
|
|
@@ -71,16 +71,28 @@ TOKEN_USAGE=$(jq -n \
|
|
|
71
71
|
(if $cr != "" then {cache_read_input_tokens: ($cr | tonumber)} else {} end) +
|
|
72
72
|
(if $cw != "" then {cache_creation_input_tokens: ($cw | tonumber)} else {} end)')
|
|
73
73
|
|
|
74
|
-
# Increment per-session sequence (same locking pattern as post-tool-use)
|
|
74
|
+
# Increment per-session sequence (same locking pattern as post-tool-use):
|
|
75
|
+
# bounded wait + stale-lock break, so a hook killed mid-section doesn't leak a
|
|
76
|
+
# lock dir that wedges every later event hook for the session.
|
|
75
77
|
SEQ_FILE="$TEMP_DIR/$HOOK_SESSION_ID.seq"
|
|
76
78
|
LOCK_DIR="$TEMP_DIR/$HOOK_SESSION_ID.seqlock"
|
|
77
|
-
|
|
79
|
+
LOCK_TRIES=0
|
|
80
|
+
while ! mkdir "$LOCK_DIR" 2>/dev/null; do
|
|
81
|
+
sleep 0.01
|
|
82
|
+
LOCK_TRIES=$((LOCK_TRIES + 1))
|
|
83
|
+
if [ "$LOCK_TRIES" -ge 500 ]; then
|
|
84
|
+
{ rmdir "$LOCK_DIR" 2>/dev/null || rm -rf "$LOCK_DIR" 2>/dev/null; } || true
|
|
85
|
+
mkdir "$LOCK_DIR" 2>/dev/null || true
|
|
86
|
+
break
|
|
87
|
+
fi
|
|
88
|
+
done
|
|
78
89
|
SEQ=1
|
|
79
90
|
if [ -f "$SEQ_FILE" ]; then
|
|
80
91
|
SEQ=$(( $(cat "$SEQ_FILE") + 1 ))
|
|
81
92
|
fi
|
|
82
93
|
echo "$SEQ" > "$SEQ_FILE"
|
|
83
|
-
|
|
94
|
+
# Non-fatal: after a stale-lock break two hooks can briefly share the lock.
|
|
95
|
+
rmdir "$LOCK_DIR" 2>/dev/null || true
|
|
84
96
|
|
|
85
97
|
# Truncate text payload defensively (8 KB cap matches what the addon caps
|
|
86
98
|
# its captured snapshots at).
|
package/scripts/jq
CHANGED
package/scripts/post-tool-use.sh
CHANGED
|
@@ -48,16 +48,33 @@ if ! _rulemetric_ensure_session "post-tool-use"; then
|
|
|
48
48
|
exit 0
|
|
49
49
|
fi
|
|
50
50
|
|
|
51
|
-
# Increment sequence counter (atomic mkdir lock, works on macOS + Linux)
|
|
51
|
+
# Increment sequence counter (atomic mkdir lock, works on macOS + Linux).
|
|
52
|
+
# Bounded wait + stale-lock break (same pattern as _ensure-session.sh): a hook
|
|
53
|
+
# killed mid-section (run.ts's 10s timeout) leaks the lock dir, and an
|
|
54
|
+
# unbounded spin here would silently wedge every later event hook for the
|
|
55
|
+
# session.
|
|
52
56
|
SEQ_FILE="$TEMP_DIR/$HOOK_SESSION_ID.seq"
|
|
53
57
|
LOCK_DIR="$TEMP_DIR/$HOOK_SESSION_ID.seqlock"
|
|
54
|
-
|
|
58
|
+
LOCK_TRIES=0
|
|
59
|
+
while ! mkdir "$LOCK_DIR" 2>/dev/null; do
|
|
60
|
+
sleep 0.01
|
|
61
|
+
LOCK_TRIES=$((LOCK_TRIES + 1))
|
|
62
|
+
if [ "$LOCK_TRIES" -ge 500 ]; then
|
|
63
|
+
# Stale lock — force remove and take it
|
|
64
|
+
{ rmdir "$LOCK_DIR" 2>/dev/null || rm -rf "$LOCK_DIR" 2>/dev/null; } || true
|
|
65
|
+
mkdir "$LOCK_DIR" 2>/dev/null || true
|
|
66
|
+
break
|
|
67
|
+
fi
|
|
68
|
+
done
|
|
55
69
|
SEQ=1
|
|
56
70
|
if [ -f "$SEQ_FILE" ]; then
|
|
57
71
|
SEQ=$(( $(cat "$SEQ_FILE") + 1 ))
|
|
58
72
|
fi
|
|
59
73
|
echo "$SEQ" > "$SEQ_FILE"
|
|
60
|
-
|
|
74
|
+
# Non-fatal: after a stale-lock break two hooks can briefly share the lock, so
|
|
75
|
+
# the dir may already be gone. A failing rmdir here would trip the ERR trap and
|
|
76
|
+
# drop the event we just sequenced.
|
|
77
|
+
rmdir "$LOCK_DIR" 2>/dev/null || true
|
|
61
78
|
|
|
62
79
|
# Build payload — toolInput/toolOutput as JSON objects
|
|
63
80
|
PAYLOAD=$(jq -n \
|
package/scripts/session-end.sh
CHANGED
|
@@ -118,10 +118,14 @@ fi
|
|
|
118
118
|
"$AUTH_HEADER" "{\"sessionId\":\"$RW_SESSION_ID\"}" "session-end-link" 8 > /dev/null 2>&1) &
|
|
119
119
|
|
|
120
120
|
# ── Auto-enrich from local session-meta ──
|
|
121
|
-
# For Claude Code: read local session-meta file
|
|
121
|
+
# For Claude Code: read local session-meta file when present; nothing writes
|
|
122
|
+
# that dir anymore, so when it's missing fall back to deriving enrichment
|
|
123
|
+
# from the reimported events (claude_code enrichment was silently dead
|
|
124
|
+
# without this — with_meta dropped to ~0 from April on).
|
|
122
125
|
# For other tools: derive enrichment from stored session events via API
|
|
123
126
|
if [ "$HOOK_TOOL" = "claude_code" ]; then
|
|
124
127
|
SESSION_META_FILE="$HOME/.claude/usage-data/session-meta/$HOOK_SESSION_ID.json"
|
|
128
|
+
ENRICH_PAYLOAD=""
|
|
125
129
|
if [ -f "$SESSION_META_FILE" ]; then
|
|
126
130
|
# Build enrichment payload from local session-meta
|
|
127
131
|
ENRICH_PAYLOAD=$(SESSION_META_FILE="$SESSION_META_FILE" python3 -c "
|
|
@@ -156,13 +160,17 @@ try:
|
|
|
156
160
|
except:
|
|
157
161
|
pass
|
|
158
162
|
" 2>/dev/null)
|
|
163
|
+
fi
|
|
159
164
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
165
|
+
if [ -n "$ENRICH_PAYLOAD" ]; then
|
|
166
|
+
# Backgrounded retry-on-503 so a transient wedge doesn't drop enrichment
|
|
167
|
+
# (leaving insights stuck at the last-enriched session).
|
|
168
|
+
(_rulemetric_send PATCH "${RULEMETRIC_API_URL}/api/sessions/${RW_SESSION_ID}/enrich" \
|
|
169
|
+
"$AUTH_HEADER" "$ENRICH_PAYLOAD" "session-end-enrich" > /dev/null 2>&1) &
|
|
170
|
+
else
|
|
171
|
+
# Meta file missing or unreadable — derive from the reimported events.
|
|
172
|
+
(_rulemetric_send POST "${RULEMETRIC_API_URL}/api/sessions/${RW_SESSION_ID}/derive-enrichment" \
|
|
173
|
+
"$AUTH_HEADER" '{}' "session-end-derive" 8 > /dev/null 2>&1) &
|
|
166
174
|
fi
|
|
167
175
|
else
|
|
168
176
|
# Non-Claude tools: derive enrichment from stored session events
|
|
@@ -181,22 +189,30 @@ fi
|
|
|
181
189
|
# ── Auto-generate recommendations (daily debounce) ──
|
|
182
190
|
# Queue a recommendation job if one hasn't been generated today.
|
|
183
191
|
# The worker picks it up and runs 7 Opus prompts (~$5-15 per run).
|
|
192
|
+
# HOOK_CWD comes from the hook payload's cwd — JSON-encode it via jq; spliced
|
|
193
|
+
# raw, a path with a quote/backslash 400s the enqueue silently.
|
|
184
194
|
(curl -sf --noproxy '*' --max-time 4 -X POST "${RULEMETRIC_API_URL}/api/insights-jobs" \
|
|
185
195
|
-H "$AUTH_HEADER" \
|
|
186
196
|
-H "Content-Type: application/json" \
|
|
187
|
-
-d "{
|
|
197
|
+
-d "$(jq -nc --arg projectPath "$HOOK_CWD" '{dedup: "daily", projectPath: $projectPath}')" > /dev/null 2>&1) &
|
|
198
|
+
|
|
199
|
+
# Clean up temp files. The lock DIRS (.seqlock/.sesslock/.ptlock) are removed
|
|
200
|
+
# too — a hook killed mid-section leaks one, and a leaked lock outlives the
|
|
201
|
+
# session it belonged to.
|
|
202
|
+
_rulemetric_cleanup_session_temp() {
|
|
203
|
+
local key="$1"
|
|
204
|
+
[ -n "$key" ] || return 0
|
|
205
|
+
rm -f "$TEMP_DIR/$key" "$TEMP_DIR/$key.seq" "$TEMP_DIR/$key.ctxhash" 2>/dev/null || true
|
|
206
|
+
rm -rf "$TEMP_DIR/$key.seqlock" "$TEMP_DIR/$key.sesslock" "$TEMP_DIR/$key.ptlock" 2>/dev/null || true
|
|
207
|
+
}
|
|
188
208
|
|
|
189
|
-
# Clean up temp files
|
|
190
209
|
for registry_file in "$TEMP_DIR"/*; do
|
|
191
210
|
[ -f "$registry_file" ] || continue
|
|
192
211
|
case "$registry_file" in
|
|
193
212
|
*.log|*.seq|*.ctxhash|*.pid|*.lock) continue ;;
|
|
194
213
|
esac
|
|
195
214
|
if [ "$(sed -n '1p' "$registry_file" 2>/dev/null || true)" = "$RW_SESSION_ID" ]; then
|
|
196
|
-
|
|
197
|
-
rm -f "$registry_file.seq" 2>/dev/null || true
|
|
198
|
-
rm -f "$registry_file.ctxhash" 2>/dev/null || true
|
|
215
|
+
_rulemetric_cleanup_session_temp "$(basename "$registry_file")"
|
|
199
216
|
fi
|
|
200
217
|
done
|
|
201
|
-
|
|
202
|
-
rm -f "$TEMP_DIR/$HOOK_SESSION_ID.seq" 2>/dev/null || true
|
|
218
|
+
_rulemetric_cleanup_session_temp "$HOOK_SESSION_ID"
|
package/scripts/user-prompt.sh
CHANGED
|
@@ -36,16 +36,33 @@ if ! _rulemetric_ensure_session "user-prompt"; then
|
|
|
36
36
|
exit 0
|
|
37
37
|
fi
|
|
38
38
|
|
|
39
|
-
# Increment sequence counter (atomic mkdir lock, works on macOS + Linux)
|
|
39
|
+
# Increment sequence counter (atomic mkdir lock, works on macOS + Linux).
|
|
40
|
+
# Bounded wait + stale-lock break (same pattern as _ensure-session.sh): a hook
|
|
41
|
+
# killed mid-section (run.ts's 10s timeout) leaks the lock dir, and an
|
|
42
|
+
# unbounded spin here would silently wedge every later event hook for the
|
|
43
|
+
# session.
|
|
40
44
|
SEQ_FILE="$TEMP_DIR/$HOOK_SESSION_ID.seq"
|
|
41
45
|
LOCK_DIR="$TEMP_DIR/$HOOK_SESSION_ID.seqlock"
|
|
42
|
-
|
|
46
|
+
LOCK_TRIES=0
|
|
47
|
+
while ! mkdir "$LOCK_DIR" 2>/dev/null; do
|
|
48
|
+
sleep 0.01
|
|
49
|
+
LOCK_TRIES=$((LOCK_TRIES + 1))
|
|
50
|
+
if [ "$LOCK_TRIES" -ge 500 ]; then
|
|
51
|
+
# Stale lock — force remove and take it
|
|
52
|
+
rmdir "$LOCK_DIR" 2>/dev/null || rm -rf "$LOCK_DIR" 2>/dev/null
|
|
53
|
+
mkdir "$LOCK_DIR" 2>/dev/null || true
|
|
54
|
+
break
|
|
55
|
+
fi
|
|
56
|
+
done
|
|
43
57
|
SEQ=1
|
|
44
58
|
if [ -f "$SEQ_FILE" ]; then
|
|
45
59
|
SEQ=$(( $(cat "$SEQ_FILE") + 1 ))
|
|
46
60
|
fi
|
|
47
61
|
echo "$SEQ" > "$SEQ_FILE"
|
|
48
|
-
|
|
62
|
+
# Non-fatal: after a stale-lock break two hooks can briefly share the lock, so
|
|
63
|
+
# the dir may already be gone. A failing rmdir here would trip the ERR trap and
|
|
64
|
+
# drop the event we just sequenced.
|
|
65
|
+
rmdir "$LOCK_DIR" 2>/dev/null || true
|
|
49
66
|
|
|
50
67
|
# Fire-and-forget event POST, with bounded retry on a transient 503/429 so the
|
|
51
68
|
# admission gate (db-wedge plan Phase 1) sheds load without dropping the event.
|
|
@@ -58,4 +75,3 @@ _rulemetric_post_event \
|
|
|
58
75
|
_rulemetric_log "user-prompt" "success"
|
|
59
76
|
|
|
60
77
|
# Context capture is now handled by the proxy sniffer (`rulemetric proxy start`).
|
|
61
|
-
# File-based scanning via _context-scan.sh is available as a manual fallback.
|