mirror of
https://github.com/LerianStudio/ring
synced 2026-04-21 21:47:49 +00:00
Removes all features that saved session data to .ring/ folders in user projects. This includes artifact indexing (SQLite FTS5), context usage tracking, learning extraction, continuity ledgers, outcome inference, and automated session tracking. Deleted infrastructure: 7 hooks, 4 skills, 2 commands, artifact-index lib (SQLite), compound_learnings lib, outcome-inference lib, shared utilities (ledger-utils, context-check, get-context-usage), tests, and documentation. Updated: hooks.json (removed PostToolUse/PreCompact/Stop hooks), session-start.sh (removed ledger loading), hook-utils.sh (removed .ring directory functions), test files (removed context/ledger tests). X-Lerian-Ref: 0x1
170 lines
4.4 KiB
Bash
Executable file
170 lines
4.4 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# shellcheck disable=SC2034 # Unused variables OK for exported config
|
|
# Common hook utilities for Ring hooks
|
|
#
|
|
# Usage:
|
|
# source /path/to/hook-utils.sh
|
|
# input=$(read_hook_stdin)
|
|
# output_hook_result "continue" "Optional message"
|
|
#
|
|
# Provides:
|
|
# - read_hook_stdin: Read JSON from stdin (Claude Code hook input)
|
|
# - output_hook_result: Output valid hook JSON response
|
|
# - output_hook_context: Output hook response with additionalContext
|
|
# - get_json_field: Extract field from JSON using jq or grep fallback
|
|
# - get_project_root: Get project root directory
|
|
|
|
set -euo pipefail
|
|
|
|
# Determine script location and source dependencies
|
|
HOOK_UTILS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
# Source json-escape if not already sourced
|
|
if ! declare -f json_escape &>/dev/null; then
|
|
# shellcheck source=json-escape.sh
|
|
source "${HOOK_UTILS_DIR}/json-escape.sh"
|
|
fi
|
|
|
|
# Read hook input from stdin
|
|
# Returns: JSON string via stdout
|
|
read_hook_stdin() {
|
|
local input=""
|
|
local timeout_seconds="${1:-5}"
|
|
|
|
# Read all stdin with timeout
|
|
if command -v timeout &>/dev/null; then
|
|
input=$(timeout "${timeout_seconds}" cat 2>/dev/null || true)
|
|
else
|
|
# macOS fallback: use read with timeout
|
|
local line
|
|
input=""
|
|
while IFS= read -r -t "${timeout_seconds}" line; do
|
|
input="${input}${line}"$'\n'
|
|
done
|
|
# Remove trailing newline if present
|
|
input="${input%$'\n'}"
|
|
fi
|
|
|
|
printf '%s' "$input"
|
|
}
|
|
|
|
# Extract a field from JSON input
|
|
# Args: $1 - JSON string, $2 - field name (top-level only, alphanumeric/underscore only)
|
|
# Returns: field value via stdout
|
|
get_json_field() {
|
|
local json="${1:-}"
|
|
local field="${2:-}"
|
|
|
|
if [[ -z "$json" ]] || [[ -z "$field" ]]; then
|
|
return 1
|
|
fi
|
|
|
|
# Validate field contains only safe characters (security: prevent jq injection)
|
|
if [[ ! "$field" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]; then
|
|
return 1
|
|
fi
|
|
|
|
# Prefer jq for reliable parsing
|
|
if command -v jq &>/dev/null; then
|
|
printf '%s' "$json" | jq -r ".${field} // empty" 2>/dev/null
|
|
return 0
|
|
fi
|
|
|
|
# Fallback: grep-based extraction (handles strings, numbers, booleans, null)
|
|
# Try string value first: "field": "value"
|
|
local result
|
|
result=$(printf '%s' "$json" | grep -oE "\"${field}\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | \
|
|
sed 's/.*:[[:space:]]*"\([^"]*\)"/\1/' | head -1)
|
|
|
|
if [[ -n "$result" ]]; then
|
|
printf '%s' "$result"
|
|
return 0
|
|
fi
|
|
|
|
# Try non-string value: "field": value (number, boolean, null)
|
|
result=$(printf '%s' "$json" | grep -oE "\"${field}\"[[:space:]]*:[[:space:]]*[^,}\][:space:]]+" | \
|
|
sed 's/.*:[[:space:]]*//' | head -1)
|
|
|
|
if [[ -n "$result" ]]; then
|
|
printf '%s' "$result"
|
|
return 0
|
|
fi
|
|
|
|
return 1
|
|
}
|
|
|
|
# Get project root directory
|
|
# Uses CLAUDE_PROJECT_DIR if set, otherwise pwd
|
|
get_project_root() {
|
|
local project_dir="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
printf '%s' "$project_dir"
|
|
}
|
|
|
|
# Output a basic hook result
|
|
# Args: $1 - result ("continue" or "block"), $2 - message (optional)
|
|
output_hook_result() {
|
|
local result="${1:-continue}"
|
|
local message="${2:-}"
|
|
|
|
if [[ -n "$message" ]]; then
|
|
local escaped_message
|
|
escaped_message=$(json_escape "$message")
|
|
cat <<EOF
|
|
{
|
|
"result": "${result}",
|
|
"message": "${escaped_message}"
|
|
}
|
|
EOF
|
|
else
|
|
cat <<EOF
|
|
{
|
|
"result": "${result}"
|
|
}
|
|
EOF
|
|
fi
|
|
}
|
|
|
|
# Output a hook response with additionalContext
|
|
# Args: $1 - hook event name, $2 - additional context string
|
|
output_hook_context() {
|
|
local event_name="${1:-SessionStart}"
|
|
local context="${2:-}"
|
|
|
|
local escaped_context
|
|
escaped_context=$(json_escape "$context")
|
|
|
|
cat <<EOF
|
|
{
|
|
"hookSpecificOutput": {
|
|
"hookEventName": "${event_name}",
|
|
"additionalContext": "${escaped_context}"
|
|
}
|
|
}
|
|
EOF
|
|
}
|
|
|
|
# Output an empty hook response (no-op)
|
|
# Args: $1 - hook event name (optional)
|
|
output_hook_empty() {
|
|
local event_name="${1:-}"
|
|
|
|
if [[ -n "$event_name" ]]; then
|
|
cat <<EOF
|
|
{
|
|
"hookSpecificOutput": {
|
|
"hookEventName": "${event_name}"
|
|
}
|
|
}
|
|
EOF
|
|
else
|
|
echo '{}'
|
|
fi
|
|
}
|
|
|
|
# Export functions for subshells
|
|
export -f read_hook_stdin 2>/dev/null || true
|
|
export -f get_json_field 2>/dev/null || true
|
|
export -f get_project_root 2>/dev/null || true
|
|
export -f output_hook_result 2>/dev/null || true
|
|
export -f output_hook_context 2>/dev/null || true
|
|
export -f output_hook_empty 2>/dev/null || true
|