@rulemetric/hooks 0.2.4 → 0.4.0

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.2.4",
3
+ "version": "0.4.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -160,7 +160,7 @@ _rulemetric_ensure_session() {
160
160
  '{tool: $tool, externalSessionId: $externalSessionId, projectPath: $projectPath, gitBranch: $gitBranch, metadata: $metadata, orgId: $orgId, runId: $runId}')
161
161
 
162
162
  local response
163
- response=$(curl -sf -X POST "${RULEMETRIC_API_URL}/api/sessions" \
163
+ response=$(curl -sf --noproxy '*' --max-time 4 -X POST "${RULEMETRIC_API_URL}/api/sessions" \
164
164
  -H "$AUTH_HEADER" \
165
165
  -H "Content-Type: application/json" \
166
166
  -d "$payload" \
@@ -16,7 +16,9 @@ trap 'echo "rulemetric assistant-response failed at line $LINENO: $BASH_COMMAND"
16
16
  # Load config (env vars, ~/.config/rulemetric/env, or .env.local)
17
17
  source "$SCRIPT_DIR/_config.sh" 2>/dev/null || true
18
18
 
19
- INPUT=$(cat)
19
+ # Bound the read so a hook runner that leaves stdin open cannot wedge the agent.
20
+ INPUT=""
21
+ IFS= read -r -d '' -t 2 INPUT || true
20
22
  source "$SCRIPT_DIR/_normalize.sh"
21
23
 
22
24
  if [ -z "$HOOK_SESSION_ID" ] || [ -z "${RULEMETRIC_API_URL:-}" ]; then
@@ -74,7 +76,15 @@ TOKEN_USAGE=$(jq -n \
74
76
  # Increment per-session sequence (same locking pattern as post-tool-use)
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
+ SEQ_LOCK_TRIES=0
80
+ while ! mkdir "$LOCK_DIR" 2>/dev/null; do
81
+ sleep 0.01
82
+ SEQ_LOCK_TRIES=$((SEQ_LOCK_TRIES + 1))
83
+ if [ "$SEQ_LOCK_TRIES" -ge 500 ]; then
84
+ _rulemetric_log "assistant-response" "skip" "seq_lock_timeout"
85
+ exit 0
86
+ fi
87
+ done
78
88
  SEQ=1
79
89
  if [ -f "$SEQ_FILE" ]; then
80
90
  SEQ=$(( $(cat "$SEQ_FILE") + 1 ))
@@ -115,7 +125,7 @@ PAYLOAD=$(jq -n \
115
125
  _rulemetric_log "assistant-response" "success"
116
126
 
117
127
  # Fire-and-forget (same error-logging pattern as post-tool-use)
118
- (CURL_OUT=$(curl -s -w "\n%{http_code}" -X POST "${RULEMETRIC_API_URL}/api/sessions/${RW_SESSION_ID}/events" \
128
+ (CURL_OUT=$(curl -s --noproxy '*' --max-time 4 -w "\n%{http_code}" -X POST "${RULEMETRIC_API_URL}/api/sessions/${RW_SESSION_ID}/events" \
119
129
  -H "$AUTH_HEADER" \
120
130
  -H "Content-Type: application/json" \
121
131
  -d "$PAYLOAD" 2>&1)
@@ -9,8 +9,10 @@ trap 'echo "rulemetric post-tool-use failed at line $LINENO: $BASH_COMMAND" >&2;
9
9
  # Load config (env vars, ~/.config/rulemetric/env, or .env.local)
10
10
  source "$SCRIPT_DIR/_config.sh" 2>/dev/null || true
11
11
 
12
- # Read hook input from stdin and normalize across tools
13
- INPUT=$(cat)
12
+ # Read hook input from stdin and normalize across tools. Bound the read so a
13
+ # hook runner that leaves stdin open cannot wedge the agent.
14
+ INPUT=""
15
+ IFS= read -r -d '' -t 2 INPUT || true
14
16
  source "$SCRIPT_DIR/_normalize.sh"
15
17
 
16
18
  # Session paper-trail: snapshot the full working tree to a shadow git ref for
@@ -51,7 +53,15 @@ fi
51
53
  # Increment sequence counter (atomic mkdir lock, works on macOS + Linux)
52
54
  SEQ_FILE="$TEMP_DIR/$HOOK_SESSION_ID.seq"
53
55
  LOCK_DIR="$TEMP_DIR/$HOOK_SESSION_ID.seqlock"
54
- while ! mkdir "$LOCK_DIR" 2>/dev/null; do sleep 0.01; done
56
+ SEQ_LOCK_TRIES=0
57
+ while ! mkdir "$LOCK_DIR" 2>/dev/null; do
58
+ sleep 0.01
59
+ SEQ_LOCK_TRIES=$((SEQ_LOCK_TRIES + 1))
60
+ if [ "$SEQ_LOCK_TRIES" -ge 500 ]; then
61
+ _rulemetric_log "post-tool-use" "skip" "seq_lock_timeout"
62
+ exit 0
63
+ fi
64
+ done
55
65
  SEQ=1
56
66
  if [ -f "$SEQ_FILE" ]; then
57
67
  SEQ=$(( $(cat "$SEQ_FILE") + 1 ))
@@ -71,7 +81,7 @@ PAYLOAD=$(jq -n \
71
81
  _rulemetric_log "post-tool-use" "success"
72
82
 
73
83
  # Fire-and-forget (capture HTTP errors for debugging)
74
- (CURL_OUT=$(curl -s -w "\n%{http_code}" -X POST "${RULEMETRIC_API_URL}/api/sessions/${RW_SESSION_ID}/events" \
84
+ (CURL_OUT=$(curl -s --noproxy '*' --max-time 4 -w "\n%{http_code}" -X POST "${RULEMETRIC_API_URL}/api/sessions/${RW_SESSION_ID}/events" \
75
85
  -H "$AUTH_HEADER" \
76
86
  -H "Content-Type: application/json" \
77
87
  -d "$PAYLOAD" 2>&1)
@@ -9,8 +9,10 @@ trap 'echo "rulemetric session-end failed at line $LINENO: $BASH_COMMAND" >&2; _
9
9
  # Load config (env vars, ~/.config/rulemetric/env, or .env.local)
10
10
  source "$SCRIPT_DIR/_config.sh" 2>/dev/null || true
11
11
 
12
- # Read hook input from stdin and normalize across tools
13
- INPUT=$(cat)
12
+ # Read hook input from stdin and normalize across tools. Bound the read so a
13
+ # hook runner that leaves stdin open cannot wedge the agent.
14
+ INPUT=""
15
+ IFS= read -r -d '' -t 2 INPUT || true
14
16
  source "$SCRIPT_DIR/_normalize.sh"
15
17
 
16
18
  if [ -z "$HOOK_SESSION_ID" ] || [ -z "${RULEMETRIC_API_URL:-}" ]; then
@@ -39,20 +41,20 @@ fi
39
41
  # Try to reimport with full transcript, fall back to close
40
42
  if [ -n "$HOOK_TRANSCRIPT_PATH" ] && [ -f "$HOOK_TRANSCRIPT_PATH" ]; then
41
43
  CONTENT=$(cat "$HOOK_TRANSCRIPT_PATH")
42
- curl -sf -X POST "${RULEMETRIC_API_URL}/api/sessions/${RW_SESSION_ID}/reimport" \
44
+ curl -sf --noproxy '*' --max-time 8 -X POST "${RULEMETRIC_API_URL}/api/sessions/${RW_SESSION_ID}/reimport" \
43
45
  -H "$AUTH_HEADER" \
44
46
  -H "Content-Type: application/json" \
45
47
  -d "{\"content\":$(echo "$CONTENT" | jq -Rs .),\"tool\":\"$HOOK_TOOL\"}" \
46
48
  > /dev/null 2>&1 || {
47
49
  _rulemetric_log "session-end" "fallback" "reimport_failed"
48
- curl -sf -X POST "${RULEMETRIC_API_URL}/api/sessions/${RW_SESSION_ID}/close" \
50
+ curl -sf --noproxy '*' --max-time 4 -X POST "${RULEMETRIC_API_URL}/api/sessions/${RW_SESSION_ID}/close" \
49
51
  -H "$AUTH_HEADER" \
50
52
  > /dev/null 2>&1 || \
51
53
  _rulemetric_log "session-end" "error" "close_failed"
52
54
  }
53
55
  else
54
56
  # No transcript available — just close the session
55
- curl -sf -X POST "${RULEMETRIC_API_URL}/api/sessions/${RW_SESSION_ID}/close" \
57
+ curl -sf --noproxy '*' --max-time 4 -X POST "${RULEMETRIC_API_URL}/api/sessions/${RW_SESSION_ID}/close" \
56
58
  -H "$AUTH_HEADER" \
57
59
  > /dev/null 2>&1 || \
58
60
  _rulemetric_log "session-end" "error" "close_failed"
@@ -60,9 +62,26 @@ fi
60
62
 
61
63
  _rulemetric_log "session-end" "success"
62
64
 
65
+ # ── Emit a synthetic instruction snapshot (hooks-first, zero-proxy linking) ──
66
+ # The proxy classifies the rendered system prompt into context_items that
67
+ # link-instructions matches on. Hooks-only sessions have no proxy, so build an
68
+ # equivalent snapshot from on-disk CLAUDE.md files and POST it SYNCHRONOUSLY —
69
+ # it must land before the backgrounded link-instructions call below. Best-effort:
70
+ # a failure never breaks session-end (see the ERR trap guard). Claude Code only
71
+ # for now (the snapshot uses claude_md items).
72
+ if [ "$HOOK_TOOL" = "claude_code" ]; then
73
+ INSTR_SNAPSHOT=$(rulemetric hooks emit-instructions --project-dir "${HOOK_CWD:-$(pwd)}" 2>/dev/null || true)
74
+ if [ -n "$INSTR_SNAPSHOT" ]; then
75
+ curl -sf --noproxy '*' --max-time 5 -X POST "${RULEMETRIC_API_URL}/api/sessions/${RW_SESSION_ID}/context-snapshots" \
76
+ -H "$AUTH_HEADER" \
77
+ -H "Content-Type: application/json" \
78
+ -d "$INSTR_SNAPSHOT" > /dev/null 2>&1 || _rulemetric_log "session-end" "warn" "instruction_snapshot_failed"
79
+ fi
80
+ fi
81
+
63
82
  # ── Auto-link instructions from context snapshots ──
64
83
  # Fire-and-forget: link any captured context to instruction records
65
- (curl -sf -X POST "${RULEMETRIC_API_URL}/api/sessions/link-instructions" \
84
+ (curl -sf --noproxy '*' --max-time 8 -X POST "${RULEMETRIC_API_URL}/api/sessions/link-instructions" \
66
85
  -H "$AUTH_HEADER" \
67
86
  -H "Content-Type: application/json" \
68
87
  -d '{}' > /dev/null 2>&1) &
@@ -108,7 +127,7 @@ except:
108
127
  " 2>/dev/null)
109
128
 
110
129
  if [ -n "$ENRICH_PAYLOAD" ]; then
111
- (curl -sf -X PATCH "${RULEMETRIC_API_URL}/api/sessions/${RW_SESSION_ID}/enrich" \
130
+ (curl -sf --noproxy '*' --max-time 4 -X PATCH "${RULEMETRIC_API_URL}/api/sessions/${RW_SESSION_ID}/enrich" \
112
131
  -H "$AUTH_HEADER" \
113
132
  -H "Content-Type: application/json" \
114
133
  -d "$ENRICH_PAYLOAD" > /dev/null 2>&1) &
@@ -116,7 +135,7 @@ except:
116
135
  fi
117
136
  else
118
137
  # Non-Claude tools: derive enrichment from stored session events
119
- (curl -sf -X POST "${RULEMETRIC_API_URL}/api/sessions/${RW_SESSION_ID}/derive-enrichment" \
138
+ (curl -sf --noproxy '*' --max-time 8 -X POST "${RULEMETRIC_API_URL}/api/sessions/${RW_SESSION_ID}/derive-enrichment" \
120
139
  -H "$AUTH_HEADER" \
121
140
  -H "Content-Type: application/json" \
122
141
  -d '{}' > /dev/null 2>&1) &
@@ -133,11 +152,22 @@ fi
133
152
  # ── Auto-generate recommendations (daily debounce) ──
134
153
  # Queue a recommendation job if one hasn't been generated today.
135
154
  # The worker picks it up and runs 7 Opus prompts (~$5-15 per run).
136
- (curl -sf -X POST "${RULEMETRIC_API_URL}/api/insights-jobs" \
155
+ (curl -sf --noproxy '*' --max-time 4 -X POST "${RULEMETRIC_API_URL}/api/insights-jobs" \
137
156
  -H "$AUTH_HEADER" \
138
157
  -H "Content-Type: application/json" \
139
158
  -d "{\"dedup\":\"daily\",\"projectPath\":\"${HOOK_CWD}\"}" > /dev/null 2>&1) &
140
159
 
141
160
  # Clean up temp files
161
+ for registry_file in "$TEMP_DIR"/*; do
162
+ [ -f "$registry_file" ] || continue
163
+ case "$registry_file" in
164
+ *.log|*.seq|*.ctxhash|*.pid|*.lock) continue ;;
165
+ esac
166
+ if [ "$(sed -n '1p' "$registry_file" 2>/dev/null || true)" = "$RW_SESSION_ID" ]; then
167
+ rm -f "$registry_file" 2>/dev/null || true
168
+ rm -f "$registry_file.seq" 2>/dev/null || true
169
+ rm -f "$registry_file.ctxhash" 2>/dev/null || true
170
+ fi
171
+ done
142
172
  rm -f "$TEMP_DIR/$HOOK_SESSION_ID" 2>/dev/null || true
143
173
  rm -f "$TEMP_DIR/$HOOK_SESSION_ID.seq" 2>/dev/null || true
@@ -10,8 +10,10 @@ trap 'echo "rulemetric session-start failed at line $LINENO: $BASH_COMMAND" >&2;
10
10
  # Load config (env vars, ~/.config/rulemetric/env, or .env.local)
11
11
  source "$SCRIPT_DIR/_config.sh" 2>/dev/null || true
12
12
 
13
- # Read hook input from stdin and normalize across tools
14
- INPUT=$(cat)
13
+ # Read hook input from stdin and normalize across tools. Bound the read so a
14
+ # hook runner that leaves stdin open cannot wedge the agent.
15
+ INPUT=""
16
+ IFS= read -r -d '' -t 2 INPUT || true
15
17
  source "$SCRIPT_DIR/_normalize.sh"
16
18
 
17
19
  if [ -z "$HOOK_SESSION_ID" ]; then
@@ -38,7 +40,7 @@ _rulemetric_check_auth() {
38
40
 
39
41
  # Quick auth check — GET /api/sessions?limit=1 with a 3s timeout
40
42
  local http_code
41
- http_code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 3 \
43
+ http_code=$(curl -s --noproxy '*' -o /dev/null -w "%{http_code}" --max-time 3 \
42
44
  "$api_url/api/sessions?limit=1" \
43
45
  -H "Authorization: Bearer $auth_token" 2>/dev/null) || http_code="000"
44
46
 
@@ -120,7 +122,7 @@ _rulemetric_suggest_instructions() {
120
122
  # suggestions arrive with the actual rule body. The body is rendered to
121
123
  # stdout, which Claude Code surfaces to the model as session context.
122
124
  # Low-confidence suggestions still render as name+reason only.
123
- response="$(curl -s --max-time 4 \
125
+ response="$(curl -s --noproxy '*' --max-time 4 \
124
126
  "${api_url}/api/instruction-suggestions?projectPath=${encoded_path}&limit=3&includeContent=true" \
125
127
  -H "Authorization: Bearer ${auth_token}" 2>/dev/null)" || return 0
126
128
 
@@ -172,7 +174,7 @@ _rulemetric_suggest_instructions() {
172
174
  else
173
175
  event_type="surfaced"
174
176
  fi
175
- curl -s --max-time 3 -X POST \
177
+ curl -s --noproxy '*' --max-time 3 -X POST \
176
178
  "${api_url}/api/instruction-suggestions/${sid}/events" \
177
179
  -H "Authorization: Bearer ${auth_token}" \
178
180
  -H "Content-Type: application/json" \
@@ -9,8 +9,10 @@ trap 'echo "rulemetric user-prompt failed at line $LINENO: $BASH_COMMAND" >&2; _
9
9
  # Load config (env vars, ~/.config/rulemetric/env, or .env.local)
10
10
  source "$SCRIPT_DIR/_config.sh" 2>/dev/null || true
11
11
 
12
- # Read hook input from stdin and normalize across tools
13
- INPUT=$(cat)
12
+ # Read hook input from stdin and normalize across tools. Bound the read so a
13
+ # hook runner that leaves stdin open cannot wedge the agent.
14
+ INPUT=""
15
+ IFS= read -r -d '' -t 2 INPUT || true
14
16
  source "$SCRIPT_DIR/_normalize.sh"
15
17
 
16
18
  if [ -z "$HOOK_SESSION_ID" ] || [ -z "${RULEMETRIC_API_URL:-}" ]; then
@@ -39,7 +41,15 @@ fi
39
41
  # Increment sequence counter (atomic mkdir lock, works on macOS + Linux)
40
42
  SEQ_FILE="$TEMP_DIR/$HOOK_SESSION_ID.seq"
41
43
  LOCK_DIR="$TEMP_DIR/$HOOK_SESSION_ID.seqlock"
42
- while ! mkdir "$LOCK_DIR" 2>/dev/null; do sleep 0.01; done
44
+ SEQ_LOCK_TRIES=0
45
+ while ! mkdir "$LOCK_DIR" 2>/dev/null; do
46
+ sleep 0.01
47
+ SEQ_LOCK_TRIES=$((SEQ_LOCK_TRIES + 1))
48
+ if [ "$SEQ_LOCK_TRIES" -ge 500 ]; then
49
+ _rulemetric_log "user-prompt" "skip" "seq_lock_timeout"
50
+ exit 0
51
+ fi
52
+ done
43
53
  SEQ=1
44
54
  if [ -f "$SEQ_FILE" ]; then
45
55
  SEQ=$(( $(cat "$SEQ_FILE") + 1 ))
@@ -48,7 +58,7 @@ echo "$SEQ" > "$SEQ_FILE"
48
58
  rmdir "$LOCK_DIR"
49
59
 
50
60
  # Fire-and-forget: send user message event (capture HTTP errors for debugging)
51
- (CURL_OUT=$(curl -s -w "\n%{http_code}" -X POST "${RULEMETRIC_API_URL}/api/sessions/${RW_SESSION_ID}/events" \
61
+ (CURL_OUT=$(curl -s --noproxy '*' --max-time 4 -w "\n%{http_code}" -X POST "${RULEMETRIC_API_URL}/api/sessions/${RW_SESSION_ID}/events" \
52
62
  -H "$AUTH_HEADER" \
53
63
  -H "Content-Type: application/json" \
54
64
  -d "{\"sequence\":$SEQ,\"eventType\":\"user_message\",\"content\":$(printf '%s' "$HOOK_PROMPT" | jq -Rs .),\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%S.000Z)\"}" 2>&1)