Files
gh-withzombies-hyperpowers/hooks/utils/skill-matcher.sh
2025-11-30 09:06:38 +08:00

143 lines
3.4 KiB
Bash
Executable File

#!/usr/bin/env bash
set -e
check_dependencies() {
local missing=()
command -v jq >/dev/null 2>&1 || missing+=("jq")
command -v grep >/dev/null 2>&1 || missing+=("grep")
if [ ${#missing[@]} -gt 0 ]; then
echo "ERROR: Missing required dependencies: ${missing[*]}" >&2
echo "Please install missing tools and try again." >&2
return 1
fi
return 0
}
check_dependencies || exit 1
# Load and validate skill-rules.json
load_skill_rules() {
local rules_path="$1"
if [ -z "$rules_path" ]; then
echo "ERROR: No rules path provided" >&2
return 1
fi
if [ ! -f "$rules_path" ]; then
echo "ERROR: Rules file not found: $rules_path" >&2
return 1
fi
if ! jq . "$rules_path" 2>/dev/null; then
echo "ERROR: Invalid JSON in $rules_path" >&2
return 1
fi
return 0
}
# Match keywords (case-insensitive substring matching)
match_keywords() {
local text="$1"
local keywords="$2"
if [ -z "$text" ] || [ -z "$keywords" ]; then
return 1
fi
local lower_text=$(echo "$text" | tr '[:upper:]' '[:lower:]')
IFS=',' read -ra KEYWORD_ARRAY <<< "$keywords"
for keyword in "${KEYWORD_ARRAY[@]}"; do
local lower_keyword=$(echo "$keyword" | tr '[:upper:]' '[:lower:]' | xargs)
if [[ "$lower_text" == *"$lower_keyword"* ]]; then
return 0
fi
done
return 1
}
# Match regex patterns (case-insensitive)
match_patterns() {
local text="$1"
local patterns="$2"
if [ -z "$text" ] || [ -z "$patterns" ]; then
return 1
fi
# Use bash regex matching for performance (no external process spawning)
local lower_text=$(echo "$text" | tr '[:upper:]' '[:lower:]')
IFS=',' read -ra PATTERN_ARRAY <<< "$patterns"
for pattern in "${PATTERN_ARRAY[@]}"; do
pattern=$(echo "$pattern" | xargs | tr '[:upper:]' '[:lower:]')
# Use bash's built-in regex matching (much faster than spawning grep)
if [[ "$lower_text" =~ $pattern ]]; then
return 0
fi
done
return 1
}
# Find matching skills from prompt
# Returns JSON array of skill names, sorted by priority
find_matching_skills() {
local prompt="$1"
local rules_path="$2"
local max_skills="${3:-3}"
if [ -z "$prompt" ] || [ -z "$rules_path" ]; then
echo "[]"
return 0
fi
if ! load_skill_rules "$rules_path" >/dev/null; then
echo "[]"
return 1
fi
# Load all skill data in one jq call for performance
local skill_data=$(jq -r '
to_entries |
map(select(.key != "_comment" and .key != "_schema")) |
map({
name: .key,
priority: .value.priority,
keywords: (.value.promptTriggers.keywords | join(",")),
patterns: (.value.promptTriggers.intentPatterns | join(","))
}) |
.[] |
"\(.name)|\(.priority)|\(.keywords)|\(.patterns)"
' "$rules_path")
local matches=()
while IFS='|' read -r skill priority keywords patterns; do
# Check if keywords or patterns match
if match_keywords "$prompt" "$keywords" || match_patterns "$prompt" "$patterns"; then
matches+=("$priority:$skill")
fi
done <<< "$skill_data"
# Sort by priority (critical > high > medium > low) and limit to max_skills
if [ ${#matches[@]} -eq 0 ]; then
echo "[]"
return 0
fi
# Sort and format as JSON array
printf '%s\n' "${matches[@]}" | \
sed 's/^critical:/0:/; s/^high:/1:/; s/^medium:/2:/; s/^low:/3:/' | \
sort -t: -k1,1n | \
head -n "$max_skills" | \
cut -d: -f2- | \
jq -R . | \
jq -s .
}