@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rulemetric/hooks",
3
- "version": "0.7.3",
3
+ "version": "0.7.5",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -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}"
@@ -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-$(printf '%s' "$cwd" | shasum -a 256 | cut -c1-16)"
94
+ HOOK_SESSION_ID="copilot-cli-$(_rulemetric_short_hash "$cwd")"
79
95
  else
80
- HOOK_SESSION_ID="copilot-cli-$(printf '%s' "unknown" | shasum -a 256 | cut -c1-16)"
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-$(printf '%s' "$conversation_id" | shasum -a 256 | cut -c1-16)"
143
+ HOOK_SESSION_ID="cursor-$(_rulemetric_short_hash "$conversation_id")"
128
144
  else
129
- HOOK_SESSION_ID="cursor-$(printf '%s' "unknown" | shasum -a 256 | cut -c1-16)"
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>, and cursor-<hex> synthetic IDs
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
- while ! mkdir "$LOCK_DIR" 2>/dev/null; do sleep 0.01; done
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
- rmdir "$LOCK_DIR"
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
@@ -216,6 +216,7 @@ def build_known_n_filter(filter_, vars_):
216
216
  "gitBranch": vars_.get("gitBranch", ""),
217
217
  "metadata": vars_.get("metadata", {}),
218
218
  "orgId": vars_.get("orgId"),
219
+ "runId": vars_.get("runId"),
219
220
  }
220
221
 
221
222
  if 'eventType: "tool_call"' in compact:
@@ -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
- while ! mkdir "$LOCK_DIR" 2>/dev/null; do sleep 0.01; done
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
- rmdir "$LOCK_DIR"
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 \
@@ -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
- if [ -n "$ENRICH_PAYLOAD" ]; then
161
- # Backgrounded retry-on-503 so a transient wedge doesn't drop enrichment
162
- # (leaving insights stuck at the last-enriched session).
163
- (_rulemetric_send PATCH "${RULEMETRIC_API_URL}/api/sessions/${RW_SESSION_ID}/enrich" \
164
- "$AUTH_HEADER" "$ENRICH_PAYLOAD" "session-end-enrich" > /dev/null 2>&1) &
165
- fi
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 "{\"dedup\":\"daily\",\"projectPath\":\"${HOOK_CWD}\"}" > /dev/null 2>&1) &
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
- rm -f "$registry_file" 2>/dev/null || true
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
- rm -f "$TEMP_DIR/$HOOK_SESSION_ID" 2>/dev/null || true
202
- rm -f "$TEMP_DIR/$HOOK_SESSION_ID.seq" 2>/dev/null || true
218
+ _rulemetric_cleanup_session_temp "$HOOK_SESSION_ID"
@@ -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
- while ! mkdir "$LOCK_DIR" 2>/dev/null; do sleep 0.01; done
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
- rmdir "$LOCK_DIR"
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.