132 lines
4.6 KiB
Python
Executable File
132 lines
4.6 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
import sys
|
|
import json
|
|
import re
|
|
from pathlib import Path
|
|
from typing import Dict, List, Literal, Optional, TypedDict
|
|
|
|
|
|
class PromptTriggers(TypedDict, total=False):
|
|
keywords: List[str]
|
|
intentPatterns: List[str]
|
|
|
|
|
|
class SkillRule(TypedDict):
|
|
type: Literal["guardrail", "domain"]
|
|
enforcement: Literal["block", "suggest", "warn"]
|
|
priority: Literal["critical", "high", "medium", "low"]
|
|
promptTriggers: Optional[PromptTriggers]
|
|
|
|
|
|
class SkillRules(TypedDict):
|
|
version: str
|
|
skills: Dict[str, SkillRule]
|
|
|
|
|
|
class HookInput(TypedDict):
|
|
session_id: str
|
|
transcript_path: str
|
|
cwd: str
|
|
permission_mode: str
|
|
prompt: str
|
|
|
|
|
|
class MatchedSkill(TypedDict):
|
|
name: str
|
|
matchType: Literal["keyword", "intent"]
|
|
config: SkillRule
|
|
|
|
|
|
def main() -> None:
|
|
try:
|
|
input_data = sys.stdin.read()
|
|
data: HookInput = json.loads(input_data)
|
|
prompt = data["prompt"].lower()
|
|
project_dir = (
|
|
data.get("cwd") or os.environ.get("CLAUDE_PROJECT_DIR") or os.getcwd()
|
|
)
|
|
rules_path = Path(project_dir) / ".claude" / "skills" / "skill-rules.json"
|
|
with open(rules_path, "r", encoding="utf-8") as f:
|
|
rules: SkillRules = json.load(f)
|
|
matched_skills: List[MatchedSkill] = []
|
|
for skill_name, config in rules["skills"].items():
|
|
triggers = config.get("promptTriggers")
|
|
if not triggers:
|
|
continue
|
|
keywords = triggers.get("keywords", [])
|
|
if keywords:
|
|
keyword_match = any(kw.lower() in prompt for kw in keywords)
|
|
if keyword_match:
|
|
matched_skills.append(
|
|
{"name": skill_name, "matchType": "keyword", "config": config}
|
|
)
|
|
continue
|
|
intent_patterns = triggers.get("intentPatterns", [])
|
|
if intent_patterns:
|
|
intent_match = any(
|
|
re.search(pattern, prompt, re.IGNORECASE)
|
|
for pattern in intent_patterns
|
|
)
|
|
if intent_match:
|
|
matched_skills.append(
|
|
{"name": skill_name, "matchType": "intent", "config": config}
|
|
)
|
|
additional_context = ""
|
|
if matched_skills:
|
|
output = "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
|
|
output += "🎯 SKILL ACTIVATION CHECK\n"
|
|
output += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
|
|
critical = [
|
|
s for s in matched_skills if s["config"]["priority"] == "critical"
|
|
]
|
|
high = [s for s in matched_skills if s["config"]["priority"] == "high"]
|
|
medium = [s for s in matched_skills if s["config"]["priority"] == "medium"]
|
|
low = [s for s in matched_skills if s["config"]["priority"] == "low"]
|
|
if critical:
|
|
output += "⚠️ CRITICAL SKILLS (REQUIRED):\n"
|
|
for s in critical:
|
|
output += f" → {s['name']}\n"
|
|
output += "\n"
|
|
if high:
|
|
output += "📚 RECOMMENDED SKILLS:\n"
|
|
for s in high:
|
|
output += f" → {s['name']}\n"
|
|
output += "\n"
|
|
if medium:
|
|
output += "💡 SUGGESTED SKILLS:\n"
|
|
for s in medium:
|
|
output += f" → {s['name']}\n"
|
|
output += "\n"
|
|
if low:
|
|
output += "📌 OPTIONAL SKILLS:\n"
|
|
for s in low:
|
|
output += f" → {s['name']}\n"
|
|
output += "\n"
|
|
output += "ACTION: Use Skill tool BEFORE responding\n"
|
|
output += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
|
|
additional_context = output
|
|
response = {
|
|
"hookSpecificOutput": {
|
|
"hookEventName": "UserPromptSubmit",
|
|
"additionalContext": additional_context,
|
|
}
|
|
}
|
|
print(json.dumps(response))
|
|
sys.exit(0)
|
|
except Exception as err:
|
|
print(f"Error in skill-activation-prompt hook: {err}", file=sys.stderr)
|
|
response = {
|
|
"hookSpecificOutput": {
|
|
"hookEventName": "UserPromptSubmit",
|
|
"additionalContext": "",
|
|
}
|
|
}
|
|
print(json.dumps(response))
|
|
sys.exit(0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import os
|
|
|
|
main()
|