@rulemetric/hooks 0.1.1
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 +21 -0
- package/scripts/_config.sh +60 -0
- package/scripts/_ensure-session.sh +103 -0
- package/scripts/_log.sh +32 -0
- package/scripts/_normalize.sh +170 -0
- package/scripts/post-tool-use.sh +70 -0
- package/scripts/session-end.sh +135 -0
- package/scripts/session-start.sh +61 -0
- package/scripts/user-prompt.sh +64 -0
- package/scripts-dir.js +3 -0
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rulemetric/hooks",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"type": "module",
|
|
8
|
+
"exports": {
|
|
9
|
+
"./scripts-dir": "./scripts-dir.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"scripts",
|
|
13
|
+
"scripts-dir.js"
|
|
14
|
+
],
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"vitest": "^3.2.4"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"test": "vitest run"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# RuleMetric: Shared config loader for hook scripts
|
|
3
|
+
# Sources credentials from multiple locations (first match wins):
|
|
4
|
+
# 1. Already-set environment variables (e.g. from shell profile)
|
|
5
|
+
# 2. ~/.config/rulemetric/env (global user config, created by `rulemetric login`)
|
|
6
|
+
# 3. .env.local in project root (development)
|
|
7
|
+
|
|
8
|
+
_rulemetric_load_config() {
|
|
9
|
+
# If already configured, nothing to do
|
|
10
|
+
if [ -n "${RULEMETRIC_API_URL:-}" ] && { [ -n "${RULEMETRIC_API_KEY:-}" ] || [ -n "${RULEMETRIC_ACCESS_TOKEN:-}" ]; }; then
|
|
11
|
+
return 0
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
local config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/rulemetric"
|
|
15
|
+
|
|
16
|
+
# Try global env file (shell-sourceable, written by `rulemetric auth login`)
|
|
17
|
+
if [ -f "$config_dir/env" ]; then
|
|
18
|
+
set -a
|
|
19
|
+
# shellcheck source=/dev/null
|
|
20
|
+
source "$config_dir/env"
|
|
21
|
+
set +a
|
|
22
|
+
if [ -n "${RULEMETRIC_API_URL:-}" ] && { [ -n "${RULEMETRIC_API_KEY:-}" ] || [ -n "${RULEMETRIC_ACCESS_TOKEN:-}" ]; }; then
|
|
23
|
+
return 0
|
|
24
|
+
fi
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# Try auth.json (written by `rulemetric auth login`)
|
|
28
|
+
if [ -f "$config_dir/auth.json" ] && command -v jq >/dev/null 2>&1; then
|
|
29
|
+
local token
|
|
30
|
+
token=$(jq -r '.accessToken // empty' "$config_dir/auth.json" 2>/dev/null)
|
|
31
|
+
if [ -n "$token" ]; then
|
|
32
|
+
export RULEMETRIC_ACCESS_TOKEN="$token"
|
|
33
|
+
# Default API URL if not set
|
|
34
|
+
export RULEMETRIC_API_URL="${RULEMETRIC_API_URL:-https://api.rulemetric.com}"
|
|
35
|
+
return 0
|
|
36
|
+
fi
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
# Try project-local .env.local (development)
|
|
40
|
+
if [ -f .env.local ]; then
|
|
41
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
42
|
+
case "$line" in
|
|
43
|
+
RULEMETRIC_*=*)
|
|
44
|
+
key="${line%%=*}"
|
|
45
|
+
value="${line#*=}"
|
|
46
|
+
# Only export valid variable names
|
|
47
|
+
case "$key" in
|
|
48
|
+
*[!A-Za-z0-9_]*) continue ;;
|
|
49
|
+
esac
|
|
50
|
+
export "$key=$value"
|
|
51
|
+
;;
|
|
52
|
+
esac
|
|
53
|
+
done < .env.local
|
|
54
|
+
return 0
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
return 0
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
_rulemetric_load_config
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# RuleMetric: Lazy session recovery
|
|
3
|
+
# If session-start failed (API was down), this creates the session on demand.
|
|
4
|
+
# Must be sourced AFTER _normalize.sh and _config.sh.
|
|
5
|
+
#
|
|
6
|
+
# Requires: HOOK_SESSION_ID, HOOK_TOOL, HOOK_CWD, RULEMETRIC_API_URL, AUTH_HEADER
|
|
7
|
+
# Sets: RW_SESSION_ID (non-empty on success)
|
|
8
|
+
|
|
9
|
+
_rulemetric_ensure_session() {
|
|
10
|
+
TEMP_DIR="${TMPDIR:-/tmp}/rulemetric"
|
|
11
|
+
RW_SESSION_ID=""
|
|
12
|
+
|
|
13
|
+
# Check for existing temp file first
|
|
14
|
+
if [ -f "$TEMP_DIR/$HOOK_SESSION_ID" ]; then
|
|
15
|
+
RW_SESSION_ID=$(cat "$TEMP_DIR/$HOOK_SESSION_ID")
|
|
16
|
+
if [ -n "$RW_SESSION_ID" ]; then
|
|
17
|
+
return 0
|
|
18
|
+
fi
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
# No session — try to create one (lazy recovery)
|
|
22
|
+
# Use mkdir-based locking to prevent parallel tool calls from creating duplicate sessions
|
|
23
|
+
mkdir -p "$TEMP_DIR" || return 1
|
|
24
|
+
local lock_dir="$TEMP_DIR/$HOOK_SESSION_ID.sesslock"
|
|
25
|
+
|
|
26
|
+
# Acquire lock (mkdir is atomic on all POSIX systems)
|
|
27
|
+
local tries=0
|
|
28
|
+
while ! mkdir "$lock_dir" 2>/dev/null; do
|
|
29
|
+
sleep 0.01
|
|
30
|
+
tries=$((tries + 1))
|
|
31
|
+
if [ "$tries" -ge 500 ]; then
|
|
32
|
+
# Stale lock — force remove and retry
|
|
33
|
+
rmdir "$lock_dir" 2>/dev/null || rm -rf "$lock_dir" 2>/dev/null
|
|
34
|
+
mkdir "$lock_dir" 2>/dev/null || return 1
|
|
35
|
+
break
|
|
36
|
+
fi
|
|
37
|
+
done
|
|
38
|
+
|
|
39
|
+
# Re-check after acquiring lock (another process may have created it)
|
|
40
|
+
if [ -f "$TEMP_DIR/$HOOK_SESSION_ID" ]; then
|
|
41
|
+
RW_SESSION_ID=$(cat "$TEMP_DIR/$HOOK_SESSION_ID")
|
|
42
|
+
if [ -n "$RW_SESSION_ID" ]; then
|
|
43
|
+
rmdir "$lock_dir" 2>/dev/null
|
|
44
|
+
return 0
|
|
45
|
+
fi
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
local cwd="${HOOK_CWD:-$(pwd)}"
|
|
49
|
+
# Normalize to git root so subdirectories map to the same project
|
|
50
|
+
local git_root
|
|
51
|
+
git_root=$(cd "$cwd" 2>/dev/null && git rev-parse --show-toplevel 2>/dev/null || echo "")
|
|
52
|
+
if [ -n "$git_root" ]; then
|
|
53
|
+
cwd="$git_root"
|
|
54
|
+
fi
|
|
55
|
+
local git_branch
|
|
56
|
+
git_branch=$(cd "$cwd" 2>/dev/null && git branch --show-current 2>/dev/null || echo "")
|
|
57
|
+
local git_remote
|
|
58
|
+
git_remote=$(cd "$cwd" 2>/dev/null && git remote get-url origin 2>/dev/null || echo "")
|
|
59
|
+
|
|
60
|
+
local metadata="{}"
|
|
61
|
+
if [ -n "$git_remote" ]; then
|
|
62
|
+
metadata=$(printf '{"gitRemote":%s}' "$(printf '%s' "$git_remote" | jq -Rs .)")
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
local payload
|
|
66
|
+
payload=$(jq -n \
|
|
67
|
+
--arg tool "$HOOK_TOOL" \
|
|
68
|
+
--arg externalSessionId "$HOOK_SESSION_ID" \
|
|
69
|
+
--arg projectPath "$cwd" \
|
|
70
|
+
--arg gitBranch "$git_branch" \
|
|
71
|
+
--argjson metadata "$metadata" \
|
|
72
|
+
'{tool: $tool, externalSessionId: $externalSessionId, projectPath: $projectPath, gitBranch: $gitBranch, metadata: $metadata}')
|
|
73
|
+
|
|
74
|
+
local response
|
|
75
|
+
response=$(curl -sf -X POST "${RULEMETRIC_API_URL}/api/sessions" \
|
|
76
|
+
-H "$AUTH_HEADER" \
|
|
77
|
+
-H "Content-Type: application/json" \
|
|
78
|
+
-d "$payload" \
|
|
79
|
+
2>/dev/null)
|
|
80
|
+
|
|
81
|
+
if [ "$?" -ne 0 ]; then
|
|
82
|
+
rmdir "$lock_dir" 2>/dev/null
|
|
83
|
+
return 1
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
RW_SESSION_ID=$(echo "$response" | jq -r '.id // empty')
|
|
87
|
+
if [ -z "$RW_SESSION_ID" ]; then
|
|
88
|
+
rmdir "$lock_dir" 2>/dev/null
|
|
89
|
+
return 1
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
echo "$RW_SESSION_ID" > "$TEMP_DIR/$HOOK_SESSION_ID"
|
|
93
|
+
echo "0" > "$TEMP_DIR/$HOOK_SESSION_ID.seq"
|
|
94
|
+
|
|
95
|
+
_rulemetric_log "${1:-ensure-session}" "recovered" "lazy_create"
|
|
96
|
+
rmdir "$lock_dir" 2>/dev/null
|
|
97
|
+
|
|
98
|
+
# Re-read the session ID written by the locked subshell
|
|
99
|
+
if [ -f "$TEMP_DIR/$HOOK_SESSION_ID" ]; then
|
|
100
|
+
RW_SESSION_ID=$(cat "$TEMP_DIR/$HOOK_SESSION_ID")
|
|
101
|
+
fi
|
|
102
|
+
[ -n "$RW_SESSION_ID" ]
|
|
103
|
+
}
|
package/scripts/_log.sh
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# RuleMetric: Structured hook logging
|
|
3
|
+
# Appends JSON lines to $TMPDIR/rulemetric/hook.log
|
|
4
|
+
# Never fails — all writes guarded with || true
|
|
5
|
+
|
|
6
|
+
_rulemetric_log() {
|
|
7
|
+
local script="${1:-unknown}"
|
|
8
|
+
local outcome="${2:-unknown}"
|
|
9
|
+
local reason="${3:-}"
|
|
10
|
+
local timestamp
|
|
11
|
+
timestamp="$(date -u +%Y-%m-%dT%H:%M:%S.000Z 2>/dev/null || echo "")"
|
|
12
|
+
|
|
13
|
+
local log_dir="${TMPDIR:-/tmp}/rulemetric"
|
|
14
|
+
mkdir -p "$log_dir" 2>/dev/null || true
|
|
15
|
+
|
|
16
|
+
if command -v jq >/dev/null 2>&1; then
|
|
17
|
+
jq -cn --arg ts "$timestamp" --arg script "$script" --arg outcome "$outcome" \
|
|
18
|
+
--arg reason "$reason" --arg session "${HOOK_SESSION_ID:-}" \
|
|
19
|
+
'{ts: $ts, script: $script, outcome: $outcome, reason: $reason, session: $session}' \
|
|
20
|
+
>> "$log_dir/hook.log" 2>/dev/null || true
|
|
21
|
+
else
|
|
22
|
+
# Fallback: escape quotes and backslashes to prevent JSON injection
|
|
23
|
+
local esc_script esc_outcome esc_reason esc_session
|
|
24
|
+
esc_script="${script//\\/\\\\}"; esc_script="${esc_script//\"/\\\"}"
|
|
25
|
+
esc_outcome="${outcome//\\/\\\\}"; esc_outcome="${esc_outcome//\"/\\\"}"
|
|
26
|
+
esc_reason="${reason//\\/\\\\}"; esc_reason="${esc_reason//\"/\\\"}"
|
|
27
|
+
esc_session="${HOOK_SESSION_ID:-}"; esc_session="${esc_session//\\/\\\\}"; esc_session="${esc_session//\"/\\\"}"
|
|
28
|
+
printf '{"ts":"%s","script":"%s","outcome":"%s","reason":"%s","session":"%s"}\n' \
|
|
29
|
+
"$timestamp" "$esc_script" "$esc_outcome" "$esc_reason" "$esc_session" \
|
|
30
|
+
>> "$log_dir/hook.log" 2>/dev/null || true
|
|
31
|
+
fi
|
|
32
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# RuleMetric: Normalize hook input across AI coding tools
|
|
3
|
+
# Detects Claude Code, VS Code Copilot, Copilot CLI, and Cursor input formats,
|
|
4
|
+
# then exports canonical HOOK_* variables for use by other scripts.
|
|
5
|
+
#
|
|
6
|
+
# Usage: source "$SCRIPT_DIR/_normalize.sh" (after reading INPUT from stdin)
|
|
7
|
+
#
|
|
8
|
+
# Exports:
|
|
9
|
+
# HOOK_TOOL - "claude_code", "vscode_copilot", "copilot_cli", or "cursor"
|
|
10
|
+
# HOOK_SESSION_ID - Session identifier
|
|
11
|
+
# HOOK_CWD - Working directory
|
|
12
|
+
# HOOK_PROMPT - User prompt text
|
|
13
|
+
# HOOK_TOOL_NAME - Tool/command name
|
|
14
|
+
# HOOK_TOOL_INPUT - Tool input as JSON
|
|
15
|
+
# HOOK_TOOL_OUTPUT - Tool output as JSON
|
|
16
|
+
# HOOK_TRANSCRIPT_PATH - Path to transcript file (empty if unavailable)
|
|
17
|
+
|
|
18
|
+
_rulemetric_normalize() {
|
|
19
|
+
local input="$1"
|
|
20
|
+
|
|
21
|
+
# Detect format by checking for distinguishing fields
|
|
22
|
+
# Copilot CLI: has "toolName" (camelCase, no sessionId)
|
|
23
|
+
# VS Code Copilot: has "sessionId" (camelCase)
|
|
24
|
+
# Cursor: has "conversation_id" AND "hook_event_name"
|
|
25
|
+
# Claude Code: has "session_id" (snake_case) — fallback default
|
|
26
|
+
|
|
27
|
+
local has_session_id has_sessionId has_toolName has_conversation_id has_hook_event_name
|
|
28
|
+
has_session_id=$(echo "$input" | jq -r 'has("session_id")' 2>/dev/null)
|
|
29
|
+
has_sessionId=$(echo "$input" | jq -r 'has("sessionId")' 2>/dev/null)
|
|
30
|
+
has_toolName=$(echo "$input" | jq -r 'has("toolName")' 2>/dev/null)
|
|
31
|
+
has_conversation_id=$(echo "$input" | jq -r 'has("conversation_id")' 2>/dev/null)
|
|
32
|
+
has_hook_event_name=$(echo "$input" | jq -r 'has("hook_event_name")' 2>/dev/null)
|
|
33
|
+
|
|
34
|
+
if [ "$has_toolName" = "true" ] && [ "$has_sessionId" != "true" ]; then
|
|
35
|
+
# Copilot CLI: no session ID, uses toolName/toolArgs/toolResult
|
|
36
|
+
HOOK_TOOL="copilot_cli"
|
|
37
|
+
|
|
38
|
+
# Generate deterministic session ID from cwd hash (stable per-project)
|
|
39
|
+
local cwd
|
|
40
|
+
cwd=$(echo "$input" | jq -r '.cwd // empty')
|
|
41
|
+
if [ -n "$cwd" ]; then
|
|
42
|
+
HOOK_SESSION_ID="copilot-cli-$(printf '%s' "$cwd" | shasum -a 256 | cut -c1-16)"
|
|
43
|
+
else
|
|
44
|
+
HOOK_SESSION_ID="copilot-cli-$(printf '%s' "unknown" | shasum -a 256 | cut -c1-16)"
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
HOOK_CWD="$cwd"
|
|
48
|
+
HOOK_PROMPT=$(echo "$input" | jq -r '.prompt // empty')
|
|
49
|
+
HOOK_TOOL_NAME=$(echo "$input" | jq -r '.toolName // empty')
|
|
50
|
+
|
|
51
|
+
# toolArgs is a JSON string — parse it to an object
|
|
52
|
+
local tool_args_raw
|
|
53
|
+
tool_args_raw=$(echo "$input" | jq -r '.toolArgs // empty')
|
|
54
|
+
if [ -n "$tool_args_raw" ]; then
|
|
55
|
+
# Try parsing as JSON; if it fails, wrap as string
|
|
56
|
+
HOOK_TOOL_INPUT=$(printf '%s' "$tool_args_raw" | jq -c '.' 2>/dev/null || printf '{"raw":%s}' "$(printf '%s' "$tool_args_raw" | jq -Rs .)")
|
|
57
|
+
else
|
|
58
|
+
HOOK_TOOL_INPUT="{}"
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
# toolResult.textResultForLlm → wrap as {"result": "..."}
|
|
62
|
+
local tool_result
|
|
63
|
+
tool_result=$(echo "$input" | jq -r '.toolResult.textResultForLlm // empty')
|
|
64
|
+
if [ -n "$tool_result" ]; then
|
|
65
|
+
HOOK_TOOL_OUTPUT=$(printf '{"result":%s}' "$(printf '%s' "$tool_result" | jq -Rs .)")
|
|
66
|
+
else
|
|
67
|
+
HOOK_TOOL_OUTPUT="{}"
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
HOOK_TRANSCRIPT_PATH=""
|
|
71
|
+
|
|
72
|
+
elif [ "$has_sessionId" = "true" ]; then
|
|
73
|
+
# VS Code Copilot (agent mode): sessionId (camelCase), tool_name, tool_input, tool_response
|
|
74
|
+
HOOK_TOOL="vscode_copilot"
|
|
75
|
+
HOOK_SESSION_ID=$(echo "$input" | jq -r '.sessionId // empty')
|
|
76
|
+
HOOK_CWD=$(echo "$input" | jq -r '.cwd // empty')
|
|
77
|
+
HOOK_PROMPT=$(echo "$input" | jq -r '.prompt // empty')
|
|
78
|
+
HOOK_TOOL_NAME=$(echo "$input" | jq -r '.tool_name // empty')
|
|
79
|
+
HOOK_TOOL_INPUT=$(echo "$input" | jq -c '.tool_input // {}')
|
|
80
|
+
HOOK_TOOL_OUTPUT=$(echo "$input" | jq -c '.tool_response // {}')
|
|
81
|
+
HOOK_TRANSCRIPT_PATH=$(echo "$input" | jq -r '.transcript_path // empty')
|
|
82
|
+
|
|
83
|
+
elif [ "$has_conversation_id" = "true" ] && [ "$has_hook_event_name" = "true" ]; then
|
|
84
|
+
# Cursor: conversation_id + hook_event_name
|
|
85
|
+
HOOK_TOOL="cursor"
|
|
86
|
+
|
|
87
|
+
# Generate deterministic session ID from conversation_id hash
|
|
88
|
+
local conversation_id
|
|
89
|
+
conversation_id=$(echo "$input" | jq -r '.conversation_id // empty')
|
|
90
|
+
if [ -n "$conversation_id" ]; then
|
|
91
|
+
HOOK_SESSION_ID="cursor-$(printf '%s' "$conversation_id" | shasum -a 256 | cut -c1-16)"
|
|
92
|
+
else
|
|
93
|
+
HOOK_SESSION_ID="cursor-$(printf '%s' "unknown" | shasum -a 256 | cut -c1-16)"
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
HOOK_CWD=$(echo "$input" | jq -r '.workspace_roots[0] // .cwd // empty')
|
|
97
|
+
HOOK_PROMPT=$(echo "$input" | jq -r '.prompt // empty')
|
|
98
|
+
HOOK_TRANSCRIPT_PATH=$(echo "$input" | jq -r '.transcript_path // empty')
|
|
99
|
+
|
|
100
|
+
# Map hook_event_name to tool fields
|
|
101
|
+
local event_name
|
|
102
|
+
event_name=$(echo "$input" | jq -r '.hook_event_name // empty')
|
|
103
|
+
|
|
104
|
+
case "$event_name" in
|
|
105
|
+
beforeShellExecution)
|
|
106
|
+
HOOK_TOOL_NAME="bash"
|
|
107
|
+
HOOK_TOOL_INPUT=$(echo "$input" | jq -c '{command: .command, cwd: .cwd} | with_entries(select(.value != null))' 2>/dev/null || echo '{}')
|
|
108
|
+
HOOK_TOOL_OUTPUT="{}"
|
|
109
|
+
;;
|
|
110
|
+
afterFileEdit)
|
|
111
|
+
HOOK_TOOL_NAME="edit"
|
|
112
|
+
HOOK_TOOL_INPUT=$(echo "$input" | jq -c '{file_path: .file_path, edits: .edits} | with_entries(select(.value != null))' 2>/dev/null || echo '{}')
|
|
113
|
+
HOOK_TOOL_OUTPUT="{}"
|
|
114
|
+
;;
|
|
115
|
+
beforeReadFile)
|
|
116
|
+
HOOK_TOOL_NAME="read"
|
|
117
|
+
HOOK_TOOL_INPUT=$(echo "$input" | jq -c '{file_path: .file_path} | with_entries(select(.value != null))' 2>/dev/null || echo '{}')
|
|
118
|
+
HOOK_TOOL_OUTPUT="{}"
|
|
119
|
+
;;
|
|
120
|
+
beforeMCPExecution)
|
|
121
|
+
HOOK_TOOL_NAME=$(echo "$input" | jq -r '.tool_name // empty')
|
|
122
|
+
HOOK_TOOL_INPUT=$(echo "$input" | jq -c '.parameters // {}')
|
|
123
|
+
HOOK_TOOL_OUTPUT="{}"
|
|
124
|
+
;;
|
|
125
|
+
*)
|
|
126
|
+
# Default: use generic fields if present (stop, beforeSubmitPrompt, etc.)
|
|
127
|
+
HOOK_TOOL_NAME=$(echo "$input" | jq -r '.tool_name // empty')
|
|
128
|
+
HOOK_TOOL_INPUT=$(echo "$input" | jq -c '.tool_input // {}')
|
|
129
|
+
HOOK_TOOL_OUTPUT=$(echo "$input" | jq -c '.tool_response // {}')
|
|
130
|
+
;;
|
|
131
|
+
esac
|
|
132
|
+
|
|
133
|
+
else
|
|
134
|
+
# Claude Code (default): session_id (snake_case)
|
|
135
|
+
HOOK_TOOL="claude_code"
|
|
136
|
+
HOOK_SESSION_ID=$(echo "$input" | jq -r '.session_id // empty')
|
|
137
|
+
HOOK_CWD=$(echo "$input" | jq -r '.cwd // empty')
|
|
138
|
+
HOOK_PROMPT=$(echo "$input" | jq -r '.prompt // empty')
|
|
139
|
+
HOOK_TOOL_NAME=$(echo "$input" | jq -r '.tool_name // empty')
|
|
140
|
+
HOOK_TOOL_INPUT=$(echo "$input" | jq -c '.tool_input // {}')
|
|
141
|
+
HOOK_TOOL_OUTPUT=$(echo "$input" | jq -c '.tool_response // {}')
|
|
142
|
+
HOOK_TRANSCRIPT_PATH=$(echo "$input" | jq -r '.transcript_path // empty')
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
# Truncate large payloads (Read tool output can be massive)
|
|
146
|
+
local max_len=4000
|
|
147
|
+
if [ "${#HOOK_TOOL_OUTPUT}" -gt "$max_len" ]; then
|
|
148
|
+
HOOK_TOOL_OUTPUT=$(printf '%s' "$HOOK_TOOL_OUTPUT" | head -c "$max_len")
|
|
149
|
+
# Re-wrap as valid JSON string if we truncated
|
|
150
|
+
HOOK_TOOL_OUTPUT="{\"truncated\":true,\"preview\":$(printf '%s' "$HOOK_TOOL_OUTPUT" | jq -Rs . 2>/dev/null || echo '""')}"
|
|
151
|
+
fi
|
|
152
|
+
if [ "${#HOOK_TOOL_INPUT}" -gt "$max_len" ]; then
|
|
153
|
+
HOOK_TOOL_INPUT=$(printf '%s' "$HOOK_TOOL_INPUT" | head -c "$max_len")
|
|
154
|
+
HOOK_TOOL_INPUT="{\"truncated\":true,\"preview\":$(printf '%s' "$HOOK_TOOL_INPUT" | jq -Rs . 2>/dev/null || echo '""')}"
|
|
155
|
+
fi
|
|
156
|
+
|
|
157
|
+
# Validate HOOK_SESSION_ID format (prevent path traversal)
|
|
158
|
+
# Accepts: standard UUIDs (case-insensitive), copilot-cli-<hex>, and cursor-<hex> synthetic IDs
|
|
159
|
+
if [ -n "$HOOK_SESSION_ID" ]; then
|
|
160
|
+
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
|
|
161
|
+
HOOK_SESSION_ID=""
|
|
162
|
+
fi
|
|
163
|
+
fi
|
|
164
|
+
|
|
165
|
+
export HOOK_TOOL HOOK_SESSION_ID HOOK_CWD HOOK_PROMPT
|
|
166
|
+
export HOOK_TOOL_NAME HOOK_TOOL_INPUT HOOK_TOOL_OUTPUT HOOK_TRANSCRIPT_PATH
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
# INPUT must be set before sourcing this file
|
|
170
|
+
_rulemetric_normalize "$INPUT"
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# RuleMetric: Capture tool use events in real-time
|
|
3
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
4
|
+
source "$SCRIPT_DIR/_log.sh" 2>/dev/null || _rulemetric_log() { :; }
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
trap 'echo "rulemetric post-tool-use failed at line $LINENO: $BASH_COMMAND" >&2; _rulemetric_log "post-tool-use" "error" "line $LINENO: $BASH_COMMAND"; exit 1' ERR
|
|
8
|
+
|
|
9
|
+
# Load config (env vars, ~/.config/rulemetric/env, or .env.local)
|
|
10
|
+
source "$SCRIPT_DIR/_config.sh" 2>/dev/null || true
|
|
11
|
+
|
|
12
|
+
# Read hook input from stdin and normalize across tools
|
|
13
|
+
INPUT=$(cat)
|
|
14
|
+
source "$SCRIPT_DIR/_normalize.sh"
|
|
15
|
+
|
|
16
|
+
if [ -z "$HOOK_SESSION_ID" ] || [ -z "${RULEMETRIC_API_URL:-}" ]; then
|
|
17
|
+
_rulemetric_log "post-tool-use" "skip" "no_config"
|
|
18
|
+
exit 0
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
# Auth
|
|
22
|
+
AUTH_HEADER=""
|
|
23
|
+
if [ -n "${RULEMETRIC_API_KEY:-}" ]; then
|
|
24
|
+
AUTH_HEADER="Authorization: Bearer $RULEMETRIC_API_KEY"
|
|
25
|
+
elif [ -n "${RULEMETRIC_ACCESS_TOKEN:-}" ]; then
|
|
26
|
+
AUTH_HEADER="Authorization: Bearer $RULEMETRIC_ACCESS_TOKEN"
|
|
27
|
+
else
|
|
28
|
+
_rulemetric_log "post-tool-use" "skip" "no_auth"
|
|
29
|
+
exit 0
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
# Get or lazily create RuleMetric session
|
|
33
|
+
source "$SCRIPT_DIR/_ensure-session.sh"
|
|
34
|
+
if ! _rulemetric_ensure_session "post-tool-use"; then
|
|
35
|
+
_rulemetric_log "post-tool-use" "skip" "no_rw_session"
|
|
36
|
+
exit 0
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
# Increment sequence counter (atomic mkdir lock, works on macOS + Linux)
|
|
40
|
+
SEQ_FILE="$TEMP_DIR/$HOOK_SESSION_ID.seq"
|
|
41
|
+
LOCK_DIR="$TEMP_DIR/$HOOK_SESSION_ID.seqlock"
|
|
42
|
+
while ! mkdir "$LOCK_DIR" 2>/dev/null; do sleep 0.01; done
|
|
43
|
+
SEQ=1
|
|
44
|
+
if [ -f "$SEQ_FILE" ]; then
|
|
45
|
+
SEQ=$(( $(cat "$SEQ_FILE") + 1 ))
|
|
46
|
+
fi
|
|
47
|
+
echo "$SEQ" > "$SEQ_FILE"
|
|
48
|
+
rmdir "$LOCK_DIR"
|
|
49
|
+
|
|
50
|
+
# Build payload — toolInput/toolOutput as JSON objects
|
|
51
|
+
PAYLOAD=$(jq -n \
|
|
52
|
+
--argjson seq "$SEQ" \
|
|
53
|
+
--arg toolName "$HOOK_TOOL_NAME" \
|
|
54
|
+
--argjson toolInput "$HOOK_TOOL_INPUT" \
|
|
55
|
+
--argjson toolOutput "$HOOK_TOOL_OUTPUT" \
|
|
56
|
+
--arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%S.000Z)" \
|
|
57
|
+
'{sequence: $seq, eventType: "tool_call", toolName: $toolName, toolInput: $toolInput, toolOutput: $toolOutput, timestamp: $timestamp}')
|
|
58
|
+
|
|
59
|
+
_rulemetric_log "post-tool-use" "success"
|
|
60
|
+
|
|
61
|
+
# Fire-and-forget (capture HTTP errors for debugging)
|
|
62
|
+
(CURL_OUT=$(curl -s -w "\n%{http_code}" -X POST "${RULEMETRIC_API_URL}/api/sessions/${RW_SESSION_ID}/events" \
|
|
63
|
+
-H "$AUTH_HEADER" \
|
|
64
|
+
-H "Content-Type: application/json" \
|
|
65
|
+
-d "$PAYLOAD" 2>&1)
|
|
66
|
+
HTTP_CODE=$(echo "$CURL_OUT" | tail -1)
|
|
67
|
+
if [ "$HTTP_CODE" != "200" ] && [ "$HTTP_CODE" != "201" ]; then
|
|
68
|
+
BODY=$(echo "$CURL_OUT" | sed '$d')
|
|
69
|
+
_rulemetric_log "post-tool-use" "error" "http_${HTTP_CODE}_seq_${SEQ}: ${BODY}"
|
|
70
|
+
fi) &
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# RuleMetric: Finalize session with full transcript import
|
|
3
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
4
|
+
source "$SCRIPT_DIR/_log.sh" 2>/dev/null || _rulemetric_log() { :; }
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
trap 'echo "rulemetric session-end failed at line $LINENO: $BASH_COMMAND" >&2; _rulemetric_log "session-end" "error" "line $LINENO: $BASH_COMMAND"; exit 1' ERR
|
|
8
|
+
|
|
9
|
+
# Load config (env vars, ~/.config/rulemetric/env, or .env.local)
|
|
10
|
+
source "$SCRIPT_DIR/_config.sh" 2>/dev/null || true
|
|
11
|
+
|
|
12
|
+
# Read hook input from stdin and normalize across tools
|
|
13
|
+
INPUT=$(cat)
|
|
14
|
+
source "$SCRIPT_DIR/_normalize.sh"
|
|
15
|
+
|
|
16
|
+
if [ -z "$HOOK_SESSION_ID" ] || [ -z "${RULEMETRIC_API_URL:-}" ]; then
|
|
17
|
+
_rulemetric_log "session-end" "skip" "no_config"
|
|
18
|
+
exit 0
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
# Auth
|
|
22
|
+
AUTH_HEADER=""
|
|
23
|
+
if [ -n "${RULEMETRIC_API_KEY:-}" ]; then
|
|
24
|
+
AUTH_HEADER="Authorization: Bearer $RULEMETRIC_API_KEY"
|
|
25
|
+
elif [ -n "${RULEMETRIC_ACCESS_TOKEN:-}" ]; then
|
|
26
|
+
AUTH_HEADER="Authorization: Bearer $RULEMETRIC_ACCESS_TOKEN"
|
|
27
|
+
else
|
|
28
|
+
_rulemetric_log "session-end" "skip" "no_auth"
|
|
29
|
+
exit 0
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
# Get or lazily create RuleMetric session
|
|
33
|
+
source "$SCRIPT_DIR/_ensure-session.sh"
|
|
34
|
+
if ! _rulemetric_ensure_session "session-end"; then
|
|
35
|
+
_rulemetric_log "session-end" "skip" "no_rw_session"
|
|
36
|
+
exit 0
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
# Try to reimport with full transcript, fall back to close
|
|
40
|
+
if [ -n "$HOOK_TRANSCRIPT_PATH" ] && [ -f "$HOOK_TRANSCRIPT_PATH" ]; then
|
|
41
|
+
CONTENT=$(cat "$HOOK_TRANSCRIPT_PATH")
|
|
42
|
+
curl -sf -X POST "${RULEMETRIC_API_URL}/api/sessions/${RW_SESSION_ID}/reimport" \
|
|
43
|
+
-H "$AUTH_HEADER" \
|
|
44
|
+
-H "Content-Type: application/json" \
|
|
45
|
+
-d "{\"content\":$(echo "$CONTENT" | jq -Rs .),\"tool\":\"$HOOK_TOOL\"}" \
|
|
46
|
+
> /dev/null 2>&1 || {
|
|
47
|
+
_rulemetric_log "session-end" "fallback" "reimport_failed"
|
|
48
|
+
curl -sf -X POST "${RULEMETRIC_API_URL}/api/sessions/${RW_SESSION_ID}/close" \
|
|
49
|
+
-H "$AUTH_HEADER" \
|
|
50
|
+
> /dev/null 2>&1 || \
|
|
51
|
+
_rulemetric_log "session-end" "error" "close_failed"
|
|
52
|
+
}
|
|
53
|
+
else
|
|
54
|
+
# No transcript available — just close the session
|
|
55
|
+
curl -sf -X POST "${RULEMETRIC_API_URL}/api/sessions/${RW_SESSION_ID}/close" \
|
|
56
|
+
-H "$AUTH_HEADER" \
|
|
57
|
+
> /dev/null 2>&1 || \
|
|
58
|
+
_rulemetric_log "session-end" "error" "close_failed"
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
_rulemetric_log "session-end" "success"
|
|
62
|
+
|
|
63
|
+
# ── Auto-link instructions from context snapshots ──
|
|
64
|
+
# Fire-and-forget: link any captured context to instruction records
|
|
65
|
+
(curl -sf -X POST "${RULEMETRIC_API_URL}/api/sessions/link-instructions" \
|
|
66
|
+
-H "$AUTH_HEADER" \
|
|
67
|
+
-H "Content-Type: application/json" \
|
|
68
|
+
-d '{}' > /dev/null 2>&1) &
|
|
69
|
+
|
|
70
|
+
# ── Auto-enrich from local session-meta ──
|
|
71
|
+
# For Claude Code: read local session-meta file
|
|
72
|
+
# For other tools: derive enrichment from stored session events via API
|
|
73
|
+
if [ "$HOOK_TOOL" = "claude_code" ]; then
|
|
74
|
+
SESSION_META_FILE="$HOME/.claude/usage-data/session-meta/$HOOK_SESSION_ID.json"
|
|
75
|
+
if [ -f "$SESSION_META_FILE" ]; then
|
|
76
|
+
# Build enrichment payload from local session-meta
|
|
77
|
+
ENRICH_PAYLOAD=$(SESSION_META_FILE="$SESSION_META_FILE" python3 -c "
|
|
78
|
+
import json, os, sys
|
|
79
|
+
try:
|
|
80
|
+
meta_file = os.environ['SESSION_META_FILE']
|
|
81
|
+
meta = json.load(open(meta_file, 'rb'), strict=False)
|
|
82
|
+
payload = {
|
|
83
|
+
'metadata': {
|
|
84
|
+
'local_session_meta': {
|
|
85
|
+
'duration_minutes': meta.get('duration_minutes', 0),
|
|
86
|
+
'user_message_count': meta.get('user_message_count', 0),
|
|
87
|
+
'assistant_message_count': meta.get('assistant_message_count', 0),
|
|
88
|
+
'tool_counts': meta.get('tool_counts', {}),
|
|
89
|
+
'languages': meta.get('languages', {}),
|
|
90
|
+
'git_commits': meta.get('git_commits', 0),
|
|
91
|
+
'git_pushes': meta.get('git_pushes', 0),
|
|
92
|
+
'user_interruptions': meta.get('user_interruptions', 0),
|
|
93
|
+
'tool_errors': meta.get('tool_errors', 0),
|
|
94
|
+
'lines_added': meta.get('lines_added', 0),
|
|
95
|
+
'lines_removed': meta.get('lines_removed', 0),
|
|
96
|
+
'files_modified': meta.get('files_modified', 0),
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if meta.get('input_tokens', 0) > 0 or meta.get('output_tokens', 0) > 0:
|
|
101
|
+
payload['tokenUsage'] = {
|
|
102
|
+
'input_tokens': meta.get('input_tokens', 0),
|
|
103
|
+
'output_tokens': meta.get('output_tokens', 0),
|
|
104
|
+
}
|
|
105
|
+
print(json.dumps(payload))
|
|
106
|
+
except:
|
|
107
|
+
pass
|
|
108
|
+
" 2>/dev/null)
|
|
109
|
+
|
|
110
|
+
if [ -n "$ENRICH_PAYLOAD" ]; then
|
|
111
|
+
(curl -sf -X PATCH "${RULEMETRIC_API_URL}/api/sessions/${RW_SESSION_ID}/enrich" \
|
|
112
|
+
-H "$AUTH_HEADER" \
|
|
113
|
+
-H "Content-Type: application/json" \
|
|
114
|
+
-d "$ENRICH_PAYLOAD" > /dev/null 2>&1) &
|
|
115
|
+
fi
|
|
116
|
+
fi
|
|
117
|
+
else
|
|
118
|
+
# Non-Claude tools: derive enrichment from stored session events
|
|
119
|
+
(curl -sf -X POST "${RULEMETRIC_API_URL}/api/sessions/${RW_SESSION_ID}/derive-enrichment" \
|
|
120
|
+
-H "$AUTH_HEADER" \
|
|
121
|
+
-H "Content-Type: application/json" \
|
|
122
|
+
-d '{}' > /dev/null 2>&1) &
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
# ── Auto-generate recommendations (daily debounce) ──
|
|
126
|
+
# Queue a recommendation job if one hasn't been generated today.
|
|
127
|
+
# The worker picks it up and runs 7 Opus prompts (~$5-15 per run).
|
|
128
|
+
(curl -sf -X POST "${RULEMETRIC_API_URL}/api/insights-jobs" \
|
|
129
|
+
-H "$AUTH_HEADER" \
|
|
130
|
+
-H "Content-Type: application/json" \
|
|
131
|
+
-d "{\"dedup\":\"daily\",\"projectPath\":\"${HOOK_CWD}\"}" > /dev/null 2>&1) &
|
|
132
|
+
|
|
133
|
+
# Clean up temp files
|
|
134
|
+
rm -f "$TEMP_DIR/$HOOK_SESSION_ID" 2>/dev/null || true
|
|
135
|
+
rm -f "$TEMP_DIR/$HOOK_SESSION_ID.seq" 2>/dev/null || true
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# RuleMetric: Session start hook
|
|
3
|
+
# Ensures gateway + proxy are running. Session creation is handled by the proxy.
|
|
4
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
|
+
source "$SCRIPT_DIR/_log.sh" 2>/dev/null || _rulemetric_log() { :; }
|
|
6
|
+
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
trap 'echo "rulemetric session-start failed at line $LINENO: $BASH_COMMAND" >&2; _rulemetric_log "session-start" "error" "line $LINENO: $BASH_COMMAND"; exit 1' ERR
|
|
9
|
+
|
|
10
|
+
# Load config (env vars, ~/.config/rulemetric/env, or .env.local)
|
|
11
|
+
source "$SCRIPT_DIR/_config.sh" 2>/dev/null || true
|
|
12
|
+
|
|
13
|
+
# Read hook input from stdin and normalize across tools
|
|
14
|
+
INPUT=$(cat)
|
|
15
|
+
source "$SCRIPT_DIR/_normalize.sh"
|
|
16
|
+
|
|
17
|
+
if [ -z "$HOOK_SESSION_ID" ]; then
|
|
18
|
+
_rulemetric_log "session-start" "skip" "no_session_id"
|
|
19
|
+
exit 0
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
_rulemetric_log "session-start" "success"
|
|
23
|
+
|
|
24
|
+
# ── Gateway auto-recovery ──
|
|
25
|
+
# Ensure the gateway proxy is running so Claude Code can always connect.
|
|
26
|
+
# The gateway routes direct when mitmproxy is off, so this is non-breaking.
|
|
27
|
+
GATEWAY_PID_FILE="$HOME/.config/rulemetric/gateway.pid"
|
|
28
|
+
|
|
29
|
+
if [ -f "$GATEWAY_PID_FILE" ]; then
|
|
30
|
+
GATEWAY_PID=$(cat "$GATEWAY_PID_FILE" 2>/dev/null)
|
|
31
|
+
if ! kill -0 "$GATEWAY_PID" 2>/dev/null; then
|
|
32
|
+
# Gateway crashed — respawn it
|
|
33
|
+
rulemetric gateway ensure 2>/dev/null || true
|
|
34
|
+
fi
|
|
35
|
+
else
|
|
36
|
+
# No PID file — try to start gateway
|
|
37
|
+
rulemetric gateway ensure 2>/dev/null || true
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
# ── Proxy auto-recovery ──
|
|
41
|
+
# Ensure the capture proxy is running so events + context are captured.
|
|
42
|
+
# Same pattern as gateway recovery above.
|
|
43
|
+
PROXY_PID_FILE="$HOME/.config/rulemetric/proxy.pid"
|
|
44
|
+
|
|
45
|
+
proxy_alive() {
|
|
46
|
+
if [ ! -f "$PROXY_PID_FILE" ]; then
|
|
47
|
+
return 1
|
|
48
|
+
fi
|
|
49
|
+
local pid
|
|
50
|
+
pid=$(cat "$PROXY_PID_FILE" 2>/dev/null) || return 1
|
|
51
|
+
kill -0 "$pid" 2>/dev/null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if ! proxy_alive; then
|
|
55
|
+
# Proxy is not running — try to restart it
|
|
56
|
+
rm -f "$PROXY_PID_FILE" 2>/dev/null
|
|
57
|
+
if command -v rulemetric >/dev/null 2>&1; then
|
|
58
|
+
rulemetric proxy start --port 8788 --skip-checks 2>/dev/null &
|
|
59
|
+
disown 2>/dev/null || true
|
|
60
|
+
fi
|
|
61
|
+
fi
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# RuleMetric: Capture user prompts in real-time
|
|
3
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
4
|
+
source "$SCRIPT_DIR/_log.sh" 2>/dev/null || _rulemetric_log() { :; }
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
trap 'echo "rulemetric user-prompt failed at line $LINENO: $BASH_COMMAND" >&2; _rulemetric_log "user-prompt" "error" "line $LINENO: $BASH_COMMAND"; exit 1' ERR
|
|
8
|
+
|
|
9
|
+
# Load config (env vars, ~/.config/rulemetric/env, or .env.local)
|
|
10
|
+
source "$SCRIPT_DIR/_config.sh" 2>/dev/null || true
|
|
11
|
+
|
|
12
|
+
# Read hook input from stdin and normalize across tools
|
|
13
|
+
INPUT=$(cat)
|
|
14
|
+
source "$SCRIPT_DIR/_normalize.sh"
|
|
15
|
+
|
|
16
|
+
if [ -z "$HOOK_SESSION_ID" ] || [ -z "${RULEMETRIC_API_URL:-}" ]; then
|
|
17
|
+
_rulemetric_log "user-prompt" "skip" "no_config"
|
|
18
|
+
exit 0
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
# Auth
|
|
22
|
+
AUTH_HEADER=""
|
|
23
|
+
if [ -n "${RULEMETRIC_API_KEY:-}" ]; then
|
|
24
|
+
AUTH_HEADER="Authorization: Bearer $RULEMETRIC_API_KEY"
|
|
25
|
+
elif [ -n "${RULEMETRIC_ACCESS_TOKEN:-}" ]; then
|
|
26
|
+
AUTH_HEADER="Authorization: Bearer $RULEMETRIC_ACCESS_TOKEN"
|
|
27
|
+
else
|
|
28
|
+
_rulemetric_log "user-prompt" "skip" "no_auth"
|
|
29
|
+
exit 0
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
# Get or lazily create RuleMetric session
|
|
33
|
+
source "$SCRIPT_DIR/_ensure-session.sh"
|
|
34
|
+
if ! _rulemetric_ensure_session "user-prompt"; then
|
|
35
|
+
_rulemetric_log "user-prompt" "skip" "no_rw_session"
|
|
36
|
+
exit 0
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
# Increment sequence counter (atomic mkdir lock, works on macOS + Linux)
|
|
40
|
+
SEQ_FILE="$TEMP_DIR/$HOOK_SESSION_ID.seq"
|
|
41
|
+
LOCK_DIR="$TEMP_DIR/$HOOK_SESSION_ID.seqlock"
|
|
42
|
+
while ! mkdir "$LOCK_DIR" 2>/dev/null; do sleep 0.01; done
|
|
43
|
+
SEQ=1
|
|
44
|
+
if [ -f "$SEQ_FILE" ]; then
|
|
45
|
+
SEQ=$(( $(cat "$SEQ_FILE") + 1 ))
|
|
46
|
+
fi
|
|
47
|
+
echo "$SEQ" > "$SEQ_FILE"
|
|
48
|
+
rmdir "$LOCK_DIR"
|
|
49
|
+
|
|
50
|
+
# 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" \
|
|
52
|
+
-H "$AUTH_HEADER" \
|
|
53
|
+
-H "Content-Type: application/json" \
|
|
54
|
+
-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)
|
|
55
|
+
HTTP_CODE=$(echo "$CURL_OUT" | tail -1)
|
|
56
|
+
if [ "$HTTP_CODE" != "200" ] && [ "$HTTP_CODE" != "201" ]; then
|
|
57
|
+
BODY=$(echo "$CURL_OUT" | sed '$d')
|
|
58
|
+
_rulemetric_log "user-prompt" "error" "http_${HTTP_CODE}_seq_${SEQ}: ${BODY}"
|
|
59
|
+
fi) &
|
|
60
|
+
|
|
61
|
+
_rulemetric_log "user-prompt" "success"
|
|
62
|
+
|
|
63
|
+
# Context capture is now handled by the proxy sniffer (`rulemetric proxy start`).
|
|
64
|
+
# File-based scanning via _context-scan.sh is available as a manual fallback.
|
package/scripts-dir.js
ADDED