bluera-knowledge 0.16.5 → 0.17.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/CHANGELOG.md +12 -0
- package/dist/mcp/bootstrap.js +19 -2
- package/dist/mcp/bootstrap.js.map +1 -1
- package/package.json +1 -5
- package/.claude-plugin/plugin.json +0 -9
- package/commands/add-folder.md +0 -48
- package/commands/add-repo.md +0 -50
- package/commands/cancel.md +0 -63
- package/commands/check-status.md +0 -78
- package/commands/crawl.md +0 -54
- package/commands/index.md +0 -48
- package/commands/remove-store.md +0 -52
- package/commands/search.md +0 -86
- package/commands/search.sh +0 -63
- package/commands/skill-activation.md +0 -130
- package/commands/stores.md +0 -54
- package/commands/suggest.md +0 -82
- package/commands/sync.md +0 -96
- package/commands/test-plugin.md +0 -408
- package/commands/uninstall.md +0 -65
- package/hooks/check-dependencies.sh +0 -145
- package/hooks/format-search-results.py +0 -132
- package/hooks/hooks.json +0 -54
- package/hooks/job-status-hook.sh +0 -51
- package/hooks/posttooluse-bk-reminder.py +0 -166
- package/hooks/skill-activation.py +0 -194
- package/hooks/skill-rules.json +0 -122
- package/skills/advanced-workflows/SKILL.md +0 -273
- package/skills/knowledge-search/SKILL.md +0 -110
- package/skills/search-optimization/SKILL.md +0 -396
- package/skills/store-lifecycle/SKILL.md +0 -470
- package/skills/when-to-query/SKILL.md +0 -160
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
PostToolUse hook to format mcp__bluera-knowledge__search results
|
|
4
|
-
into a fixed-width table with deterministic output.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import json
|
|
8
|
-
import sys
|
|
9
|
-
import os
|
|
10
|
-
|
|
11
|
-
# Column widths (content only, not including | separators)
|
|
12
|
-
SCORE_WIDTH = 6 # Right-aligned: " 1.00 "
|
|
13
|
-
STORE_WIDTH = 12 # Left-aligned: "store "
|
|
14
|
-
FILE_WIDTH = 45 # Left-aligned: "path/to/file..."
|
|
15
|
-
PURPOSE_WIDTH = 48 # Left-aligned: "Purpose text..."
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def truncate(text: str, max_len: int) -> str:
|
|
19
|
-
"""Truncate text with ellipsis if needed."""
|
|
20
|
-
if len(text) <= max_len:
|
|
21
|
-
return text
|
|
22
|
-
return text[:max_len - 3] + "..."
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def format_score(score: float) -> str:
|
|
26
|
-
"""Format score as right-aligned fixed-width string."""
|
|
27
|
-
return f"{score:5.2f}".rjust(SCORE_WIDTH)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def format_store(store_name: str) -> str:
|
|
31
|
-
"""Format store name as left-aligned fixed-width string."""
|
|
32
|
-
truncated = truncate(store_name, STORE_WIDTH)
|
|
33
|
-
return truncated.ljust(STORE_WIDTH)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def format_file(location: str, repo_root: str) -> str:
|
|
37
|
-
"""Format file path as left-aligned fixed-width string."""
|
|
38
|
-
# Strip repo root prefix
|
|
39
|
-
if repo_root and location.startswith(repo_root):
|
|
40
|
-
path = location[len(repo_root):].lstrip("/")
|
|
41
|
-
else:
|
|
42
|
-
path = location
|
|
43
|
-
truncated = truncate(path, FILE_WIDTH)
|
|
44
|
-
return truncated.ljust(FILE_WIDTH)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def format_purpose(purpose: str) -> str:
|
|
48
|
-
"""Format purpose as left-aligned fixed-width string."""
|
|
49
|
-
# Clean up purpose text
|
|
50
|
-
clean = purpose.replace("\n", " ").strip()
|
|
51
|
-
truncated = truncate(clean, PURPOSE_WIDTH)
|
|
52
|
-
return truncated.ljust(PURPOSE_WIDTH)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def format_table(results: list, query: str) -> str:
|
|
56
|
-
"""Format search results into a fixed-width markdown table."""
|
|
57
|
-
lines = []
|
|
58
|
-
|
|
59
|
-
# Header
|
|
60
|
-
lines.append(f"## Search Results for \"{query}\"")
|
|
61
|
-
lines.append("")
|
|
62
|
-
|
|
63
|
-
if not results:
|
|
64
|
-
lines.append(f"No results found for \"{query}\"")
|
|
65
|
-
lines.append("")
|
|
66
|
-
lines.append("Try:")
|
|
67
|
-
lines.append("- Broadening your search terms")
|
|
68
|
-
lines.append("- Checking if the relevant stores are indexed")
|
|
69
|
-
lines.append("- Using /bluera-knowledge:stores to see available stores")
|
|
70
|
-
return "\n".join(lines)
|
|
71
|
-
|
|
72
|
-
# Table header
|
|
73
|
-
header = f"| {'Score'.rjust(SCORE_WIDTH)} | {'Store'.ljust(STORE_WIDTH)} | {'File'.ljust(FILE_WIDTH)} | {'Purpose'.ljust(PURPOSE_WIDTH)} |"
|
|
74
|
-
# Separator: Score is right-aligned (colon at end), others are left-aligned
|
|
75
|
-
# The +2 accounts for the spaces around content, -1 for the colon on Score column
|
|
76
|
-
separator = f"|{'-' * (SCORE_WIDTH + 1)}:|{'-' * (STORE_WIDTH + 2)}|{'-' * (FILE_WIDTH + 2)}|{'-' * (PURPOSE_WIDTH + 2)}|"
|
|
77
|
-
|
|
78
|
-
lines.append(header)
|
|
79
|
-
lines.append(separator)
|
|
80
|
-
|
|
81
|
-
# Data rows
|
|
82
|
-
for result in results:
|
|
83
|
-
score = result.get("score", 0)
|
|
84
|
-
summary = result.get("summary", {})
|
|
85
|
-
store_name = summary.get("storeName", "unknown")
|
|
86
|
-
location = summary.get("location", "")
|
|
87
|
-
repo_root = summary.get("repoRoot", "")
|
|
88
|
-
purpose = summary.get("purpose", "")
|
|
89
|
-
|
|
90
|
-
row = f"| {format_score(score)} | {format_store(store_name)} | {format_file(location, repo_root)} | {format_purpose(purpose)} |"
|
|
91
|
-
lines.append(row)
|
|
92
|
-
|
|
93
|
-
lines.append("")
|
|
94
|
-
lines.append(f"**Found**: {len(results)} results")
|
|
95
|
-
|
|
96
|
-
return "\n".join(lines)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
def main():
|
|
100
|
-
try:
|
|
101
|
-
# Read hook input from stdin
|
|
102
|
-
input_data = json.load(sys.stdin)
|
|
103
|
-
|
|
104
|
-
tool_name = input_data.get("tool_name", "")
|
|
105
|
-
tool_input = input_data.get("tool_input", {})
|
|
106
|
-
tool_result = input_data.get("tool_result", {})
|
|
107
|
-
|
|
108
|
-
# Only process search results
|
|
109
|
-
if tool_name != "mcp__bluera-knowledge__search":
|
|
110
|
-
sys.exit(0)
|
|
111
|
-
|
|
112
|
-
# Extract results and query
|
|
113
|
-
results = tool_result.get("results", [])
|
|
114
|
-
query = tool_input.get("query", "")
|
|
115
|
-
|
|
116
|
-
# Format the table
|
|
117
|
-
formatted = format_table(results, query)
|
|
118
|
-
|
|
119
|
-
# Output the formatted table
|
|
120
|
-
# For PostToolUse, stdout is shown in the transcript
|
|
121
|
-
print(formatted)
|
|
122
|
-
|
|
123
|
-
except json.JSONDecodeError as e:
|
|
124
|
-
print(f"Error parsing hook input: {e}", file=sys.stderr)
|
|
125
|
-
sys.exit(1)
|
|
126
|
-
except Exception as e:
|
|
127
|
-
print(f"Error formatting results: {e}", file=sys.stderr)
|
|
128
|
-
sys.exit(1)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if __name__ == "__main__":
|
|
132
|
-
main()
|
package/hooks/hooks.json
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"description": "bluera-knowledge plugin hooks - dependency checking, job monitoring, and BK suggestions",
|
|
3
|
-
"hooks": {
|
|
4
|
-
"PostToolUse": [
|
|
5
|
-
{
|
|
6
|
-
"matcher": "Grep",
|
|
7
|
-
"hooks": [
|
|
8
|
-
{
|
|
9
|
-
"type": "command",
|
|
10
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/posttooluse-bk-reminder.py",
|
|
11
|
-
"timeout": 3
|
|
12
|
-
}
|
|
13
|
-
]
|
|
14
|
-
},
|
|
15
|
-
{
|
|
16
|
-
"matcher": "Read",
|
|
17
|
-
"hooks": [
|
|
18
|
-
{
|
|
19
|
-
"type": "command",
|
|
20
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/posttooluse-bk-reminder.py",
|
|
21
|
-
"timeout": 3
|
|
22
|
-
}
|
|
23
|
-
]
|
|
24
|
-
}
|
|
25
|
-
],
|
|
26
|
-
"SessionStart": [
|
|
27
|
-
{
|
|
28
|
-
"hooks": [
|
|
29
|
-
{
|
|
30
|
-
"type": "command",
|
|
31
|
-
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/check-dependencies.sh",
|
|
32
|
-
"timeout": 30
|
|
33
|
-
}
|
|
34
|
-
]
|
|
35
|
-
}
|
|
36
|
-
],
|
|
37
|
-
"UserPromptSubmit": [
|
|
38
|
-
{
|
|
39
|
-
"hooks": [
|
|
40
|
-
{
|
|
41
|
-
"type": "command",
|
|
42
|
-
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/job-status-hook.sh",
|
|
43
|
-
"timeout": 2
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
"type": "command",
|
|
47
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/skill-activation.py",
|
|
48
|
-
"timeout": 2
|
|
49
|
-
}
|
|
50
|
-
]
|
|
51
|
-
}
|
|
52
|
-
]
|
|
53
|
-
}
|
|
54
|
-
}
|
package/hooks/job-status-hook.sh
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# Show active jobs in context when user submits a prompt
|
|
3
|
-
#
|
|
4
|
-
# This hook runs on UserPromptSubmit events and injects
|
|
5
|
-
# information about active background jobs into the context.
|
|
6
|
-
|
|
7
|
-
JOBS_DIR="$HOME/.local/share/bluera-knowledge/jobs"
|
|
8
|
-
|
|
9
|
-
# Exit silently if jobs directory doesn't exist
|
|
10
|
-
if [ ! -d "$JOBS_DIR" ]; then
|
|
11
|
-
exit 0
|
|
12
|
-
fi
|
|
13
|
-
|
|
14
|
-
# Find active jobs (modified in last 60 minutes)
|
|
15
|
-
active_jobs=$(find "$JOBS_DIR" -name "*.json" -type f -not -name "*.pid" -mmin -60 2>/dev/null | while read -r file; do
|
|
16
|
-
# Skip if file doesn't exist or isn't readable
|
|
17
|
-
if [ ! -r "$file" ]; then
|
|
18
|
-
continue
|
|
19
|
-
fi
|
|
20
|
-
|
|
21
|
-
# Extract job details using jq (if available) or grep fallback
|
|
22
|
-
if command -v jq >/dev/null 2>&1; then
|
|
23
|
-
status=$(jq -r '.status' "$file" 2>/dev/null || echo "unknown")
|
|
24
|
-
if [ "$status" = "running" ] || [ "$status" = "pending" ]; then
|
|
25
|
-
job_id=$(basename "$file" .json)
|
|
26
|
-
type=$(jq -r '.type' "$file" 2>/dev/null || echo "unknown")
|
|
27
|
-
progress=$(jq -r '.progress' "$file" 2>/dev/null || echo "0")
|
|
28
|
-
message=$(jq -r '.message' "$file" 2>/dev/null || echo "No message")
|
|
29
|
-
echo "- $type job ($job_id): ${progress}% - $message"
|
|
30
|
-
fi
|
|
31
|
-
else
|
|
32
|
-
# Fallback using grep if jq not available
|
|
33
|
-
status=$(grep -o '"status"[[:space:]]*:[[:space:]]*"[^"]*"' "$file" | cut -d'"' -f4)
|
|
34
|
-
if [ "$status" = "running" ] || [ "$status" = "pending" ]; then
|
|
35
|
-
job_id=$(basename "$file" .json)
|
|
36
|
-
type=$(grep -o '"type"[[:space:]]*:[[:space:]]*"[^"]*"' "$file" | cut -d'"' -f4)
|
|
37
|
-
progress=$(grep -o '"progress"[[:space:]]*:[[:space:]]*[0-9.]*' "$file" | awk '{print $NF}')
|
|
38
|
-
message=$(grep -o '"message"[[:space:]]*:[[:space:]]*"[^"]*"' "$file" | cut -d'"' -f4)
|
|
39
|
-
echo "- $type job ($job_id): ${progress}% - $message"
|
|
40
|
-
fi
|
|
41
|
-
fi
|
|
42
|
-
done)
|
|
43
|
-
|
|
44
|
-
# Output active jobs if any found
|
|
45
|
-
if [ -n "$active_jobs" ]; then
|
|
46
|
-
echo ""
|
|
47
|
-
echo "Active background jobs:"
|
|
48
|
-
echo "$active_jobs"
|
|
49
|
-
echo ""
|
|
50
|
-
echo "Check status with: /bluera-knowledge:check-status"
|
|
51
|
-
fi
|
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
PostToolUse hook for bluera-knowledge plugin.
|
|
4
|
-
Fires after Claude reads/greps in dependency directories,
|
|
5
|
-
reminding to consider using BK for similar future queries.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import json
|
|
9
|
-
import re
|
|
10
|
-
import sys
|
|
11
|
-
from typing import Any
|
|
12
|
-
|
|
13
|
-
# Fast string checks - if none of these are in the path, skip regex entirely
|
|
14
|
-
# This avoids regex overhead for the vast majority of file accesses
|
|
15
|
-
LIBRARY_QUICK_CHECKS = frozenset([
|
|
16
|
-
"node_modules",
|
|
17
|
-
"vendor",
|
|
18
|
-
"site-packages",
|
|
19
|
-
"venv",
|
|
20
|
-
"bower_components",
|
|
21
|
-
"packages",
|
|
22
|
-
".npm",
|
|
23
|
-
".cargo",
|
|
24
|
-
"go/pkg",
|
|
25
|
-
])
|
|
26
|
-
|
|
27
|
-
# Patterns indicating library/dependency code
|
|
28
|
-
LIBRARY_PATH_PATTERNS = [
|
|
29
|
-
r"node_modules/",
|
|
30
|
-
r"vendor/",
|
|
31
|
-
r"site-packages/",
|
|
32
|
-
r"\.venv/",
|
|
33
|
-
r"venv/",
|
|
34
|
-
r"bower_components/",
|
|
35
|
-
r"packages/.*/node_modules/",
|
|
36
|
-
r"\.npm/",
|
|
37
|
-
r"\.cargo/registry/",
|
|
38
|
-
r"go/pkg/mod/",
|
|
39
|
-
]
|
|
40
|
-
|
|
41
|
-
# Compile patterns for efficiency
|
|
42
|
-
LIBRARY_PATTERNS_RE = re.compile("|".join(LIBRARY_PATH_PATTERNS), re.IGNORECASE)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def quick_path_check(path: str) -> bool:
|
|
46
|
-
"""Fast check if path might contain library code. Avoids regex for most paths."""
|
|
47
|
-
path_lower = path.lower()
|
|
48
|
-
return any(keyword in path_lower for keyword in LIBRARY_QUICK_CHECKS)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def extract_library_name(path: str) -> str | None:
|
|
52
|
-
"""Extract library name from dependency path."""
|
|
53
|
-
# node_modules/package-name/...
|
|
54
|
-
match = re.search(r"node_modules/(@[^/]+/[^/]+|[^/]+)", path)
|
|
55
|
-
if match:
|
|
56
|
-
return match.group(1)
|
|
57
|
-
|
|
58
|
-
# site-packages/package_name/...
|
|
59
|
-
match = re.search(r"site-packages/([^/]+)", path)
|
|
60
|
-
if match:
|
|
61
|
-
return match.group(1)
|
|
62
|
-
|
|
63
|
-
# vendor/package/...
|
|
64
|
-
match = re.search(r"vendor/([^/]+)", path)
|
|
65
|
-
if match:
|
|
66
|
-
return match.group(1)
|
|
67
|
-
|
|
68
|
-
# .cargo/registry/.../package-name-version/...
|
|
69
|
-
match = re.search(r"\.cargo/registry/[^/]+/([^/]+)-\d", path)
|
|
70
|
-
if match:
|
|
71
|
-
return match.group(1)
|
|
72
|
-
|
|
73
|
-
# go/pkg/mod/package@version/...
|
|
74
|
-
match = re.search(r"go/pkg/mod/([^@]+)@", path)
|
|
75
|
-
if match:
|
|
76
|
-
return match.group(1)
|
|
77
|
-
|
|
78
|
-
return None
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def check_grep_tool(tool_input: dict[str, Any]) -> tuple[str | None, str | None]:
|
|
82
|
-
"""Check if Grep targeted library code. Returns (action, library_name)."""
|
|
83
|
-
path = tool_input.get("path", "")
|
|
84
|
-
|
|
85
|
-
# Fast path: skip regex if no library keywords present
|
|
86
|
-
if not path or not quick_path_check(path):
|
|
87
|
-
return None, None
|
|
88
|
-
|
|
89
|
-
if LIBRARY_PATTERNS_RE.search(path):
|
|
90
|
-
lib_name = extract_library_name(path)
|
|
91
|
-
return f"grepped in `{path}`", lib_name
|
|
92
|
-
|
|
93
|
-
return None, None
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def check_read_tool(tool_input: dict[str, Any]) -> tuple[str | None, str | None]:
|
|
97
|
-
"""Check if Read targeted library code. Returns (action, library_name)."""
|
|
98
|
-
file_path = tool_input.get("file_path", "")
|
|
99
|
-
|
|
100
|
-
# Fast path: skip regex if no library keywords present
|
|
101
|
-
if not file_path or not quick_path_check(file_path):
|
|
102
|
-
return None, None
|
|
103
|
-
|
|
104
|
-
if LIBRARY_PATTERNS_RE.search(file_path):
|
|
105
|
-
lib_name = extract_library_name(file_path)
|
|
106
|
-
return f"read `{file_path}`", lib_name
|
|
107
|
-
|
|
108
|
-
return None, None
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
def main() -> int:
|
|
112
|
-
try:
|
|
113
|
-
stdin_data = sys.stdin.read()
|
|
114
|
-
if not stdin_data.strip():
|
|
115
|
-
return 0
|
|
116
|
-
hook_input = json.loads(stdin_data)
|
|
117
|
-
except json.JSONDecodeError:
|
|
118
|
-
return 0
|
|
119
|
-
|
|
120
|
-
tool_name = hook_input.get("tool_name", "")
|
|
121
|
-
tool_input = hook_input.get("tool_input", {})
|
|
122
|
-
|
|
123
|
-
action = None
|
|
124
|
-
lib_name = None
|
|
125
|
-
|
|
126
|
-
if tool_name == "Grep":
|
|
127
|
-
action, lib_name = check_grep_tool(tool_input)
|
|
128
|
-
elif tool_name == "Read":
|
|
129
|
-
action, lib_name = check_read_tool(tool_input)
|
|
130
|
-
|
|
131
|
-
if not action:
|
|
132
|
-
return 0
|
|
133
|
-
|
|
134
|
-
# Build context-aware reminder
|
|
135
|
-
lib_hint = f" ({lib_name})" if lib_name else ""
|
|
136
|
-
add_suggestion = (
|
|
137
|
-
f"If {lib_name} is not indexed, consider: /bluera-knowledge:add-repo"
|
|
138
|
-
if lib_name
|
|
139
|
-
else "Consider indexing frequently-used libraries with /bluera-knowledge:add-repo"
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
reminder_text = f"""BLUERA-KNOWLEDGE REMINDER
|
|
143
|
-
|
|
144
|
-
You just {action} - this is dependency/library code{lib_hint}.
|
|
145
|
-
|
|
146
|
-
For FUTURE queries about this library, use Bluera Knowledge instead:
|
|
147
|
-
- MCP tool: search(query="your question about {lib_name or 'the library'}")
|
|
148
|
-
- Slash command: /bluera-knowledge:search <query>
|
|
149
|
-
|
|
150
|
-
BK provides indexed, semantic search across library sources - significantly faster
|
|
151
|
-
and more context-efficient than reading through dependency directories.
|
|
152
|
-
|
|
153
|
-
{add_suggestion}"""
|
|
154
|
-
|
|
155
|
-
output = {
|
|
156
|
-
"hookSpecificOutput": {
|
|
157
|
-
"hookEventName": "PostToolUse",
|
|
158
|
-
"additionalContext": reminder_text,
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
print(json.dumps(output))
|
|
162
|
-
return 0
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if __name__ == "__main__":
|
|
166
|
-
raise SystemExit(main())
|
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Skill activation hook for bluera-knowledge plugin.
|
|
4
|
-
Matches user prompts against skill rules and injects activation reminders.
|
|
5
|
-
|
|
6
|
-
Runs on UserPromptSubmit to detect users who would benefit from learning
|
|
7
|
-
about BK skills, while excluding users who already know BK terminology.
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
import json
|
|
11
|
-
import os
|
|
12
|
-
import re
|
|
13
|
-
import sys
|
|
14
|
-
from pathlib import Path
|
|
15
|
-
from typing import Any
|
|
16
|
-
|
|
17
|
-
CONFIG_DIR = Path.home() / ".local" / "share" / "bluera-knowledge"
|
|
18
|
-
CONFIG_FILE = CONFIG_DIR / "skill-activation.json"
|
|
19
|
-
DEFAULT_CONFIG: dict[str, Any] = {
|
|
20
|
-
"enabled": True,
|
|
21
|
-
"threshold": 1,
|
|
22
|
-
"skills": {
|
|
23
|
-
"knowledge-search": True,
|
|
24
|
-
"when-to-query": True,
|
|
25
|
-
"search-optimization": True,
|
|
26
|
-
"advanced-workflows": True,
|
|
27
|
-
"store-lifecycle": True,
|
|
28
|
-
},
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def load_config() -> dict[str, Any]:
|
|
33
|
-
"""Load skill activation configuration."""
|
|
34
|
-
if not CONFIG_FILE.exists():
|
|
35
|
-
return DEFAULT_CONFIG.copy()
|
|
36
|
-
try:
|
|
37
|
-
with open(CONFIG_FILE, encoding="utf-8") as f:
|
|
38
|
-
return json.load(f)
|
|
39
|
-
except (json.JSONDecodeError, IOError):
|
|
40
|
-
return DEFAULT_CONFIG.copy()
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def load_rules(plugin_root: Path) -> dict[str, Any]:
|
|
44
|
-
"""Load skill rules from plugin hooks directory."""
|
|
45
|
-
rules_path = plugin_root / "hooks" / "skill-rules.json"
|
|
46
|
-
if not rules_path.exists():
|
|
47
|
-
return {"skills": [], "threshold": 1, "globalExclusions": []}
|
|
48
|
-
with open(rules_path, encoding="utf-8") as f:
|
|
49
|
-
return json.load(f)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def matches_condition(prompt: str, condition: dict[str, Any]) -> bool:
|
|
53
|
-
"""Check if prompt matches a single condition (keyword or regex)."""
|
|
54
|
-
prompt_lower = prompt.lower()
|
|
55
|
-
if "keyword" in condition:
|
|
56
|
-
return condition["keyword"].lower() in prompt_lower
|
|
57
|
-
if "regex" in condition:
|
|
58
|
-
return bool(re.search(condition["regex"], prompt, flags=re.IGNORECASE))
|
|
59
|
-
return False
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def check_exclusions(
|
|
63
|
-
prompt: str, exclusions: list[dict[str, Any]]
|
|
64
|
-
) -> bool:
|
|
65
|
-
"""Check if any exclusion pattern matches. Returns True if excluded."""
|
|
66
|
-
for exc in exclusions:
|
|
67
|
-
if matches_condition(prompt, exc):
|
|
68
|
-
return True
|
|
69
|
-
return False
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def score_skill(
|
|
73
|
-
prompt: str, skill: dict[str, Any]
|
|
74
|
-
) -> tuple[int, list[str]]:
|
|
75
|
-
"""Score a skill against the user prompt. Returns (score, reasons)."""
|
|
76
|
-
reasons: list[str] = []
|
|
77
|
-
score = 0
|
|
78
|
-
|
|
79
|
-
# Check skill-specific exclusions first
|
|
80
|
-
if check_exclusions(prompt, skill.get("exclusions", [])):
|
|
81
|
-
return 0, []
|
|
82
|
-
|
|
83
|
-
for trigger in skill.get("triggers", []):
|
|
84
|
-
if matches_condition(prompt, trigger):
|
|
85
|
-
weight = trigger.get("weight", 1)
|
|
86
|
-
score += weight
|
|
87
|
-
if "keyword" in trigger:
|
|
88
|
-
reasons.append(f'keyword "{trigger["keyword"]}"')
|
|
89
|
-
elif "regex" in trigger:
|
|
90
|
-
reasons.append(f'pattern match')
|
|
91
|
-
|
|
92
|
-
return score, reasons
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def generate_reminder(
|
|
96
|
-
matches: list[tuple[str, int, list[str], str]]
|
|
97
|
-
) -> str:
|
|
98
|
-
"""Generate assertive skill activation reminder using forced evaluation pattern."""
|
|
99
|
-
lines = [
|
|
100
|
-
"BLUERA-KNOWLEDGE SKILL ACTIVATION - MANDATORY EVALUATION",
|
|
101
|
-
"",
|
|
102
|
-
"CRITICAL: Complete these steps IN ORDER before answering:",
|
|
103
|
-
"",
|
|
104
|
-
"STEP 1 - EVALUATE each skill (YES/NO with one-line reason):",
|
|
105
|
-
]
|
|
106
|
-
|
|
107
|
-
for name, score, reasons, description in matches:
|
|
108
|
-
lines.append(f" • [{name}] - {description}")
|
|
109
|
-
lines.append(f" Triggered by: {', '.join(reasons[:2])}")
|
|
110
|
-
|
|
111
|
-
lines.append("")
|
|
112
|
-
lines.append("STEP 2 - ACTIVATE: For each YES, invoke Skill(skill='bluera-knowledge:<name>') NOW")
|
|
113
|
-
lines.append("")
|
|
114
|
-
lines.append("STEP 3 - RESPOND: Only after activation, answer the user's question")
|
|
115
|
-
lines.append("")
|
|
116
|
-
lines.append("WARNING: This evaluation is WORTHLESS if you skip activation.")
|
|
117
|
-
lines.append("Skills provide indexed library knowledge - faster than grepping node_modules.")
|
|
118
|
-
|
|
119
|
-
return "\n".join(lines)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
def main() -> int:
|
|
123
|
-
# Load configuration
|
|
124
|
-
config = load_config()
|
|
125
|
-
|
|
126
|
-
# Check if skill activation is enabled
|
|
127
|
-
if not config.get("enabled", True):
|
|
128
|
-
return 0
|
|
129
|
-
|
|
130
|
-
# Get plugin root from environment
|
|
131
|
-
plugin_root_env = os.environ.get("CLAUDE_PLUGIN_ROOT", "")
|
|
132
|
-
if not plugin_root_env:
|
|
133
|
-
return 0
|
|
134
|
-
plugin_root = Path(plugin_root_env)
|
|
135
|
-
|
|
136
|
-
# Read hook input from stdin
|
|
137
|
-
try:
|
|
138
|
-
stdin_data = sys.stdin.read()
|
|
139
|
-
if not stdin_data.strip():
|
|
140
|
-
return 0
|
|
141
|
-
hook_input = json.loads(stdin_data)
|
|
142
|
-
except json.JSONDecodeError:
|
|
143
|
-
return 0
|
|
144
|
-
|
|
145
|
-
prompt = hook_input.get("prompt", "")
|
|
146
|
-
if not prompt.strip():
|
|
147
|
-
return 0
|
|
148
|
-
|
|
149
|
-
# Load rules
|
|
150
|
-
rules = load_rules(plugin_root)
|
|
151
|
-
|
|
152
|
-
# Check global exclusions first
|
|
153
|
-
if check_exclusions(prompt, rules.get("globalExclusions", [])):
|
|
154
|
-
return 0
|
|
155
|
-
|
|
156
|
-
threshold = config.get("threshold", rules.get("threshold", 1))
|
|
157
|
-
enabled_skills = config.get("skills", {})
|
|
158
|
-
|
|
159
|
-
# Score each skill
|
|
160
|
-
matches: list[tuple[str, int, list[str], str]] = []
|
|
161
|
-
|
|
162
|
-
for skill in rules.get("skills", []):
|
|
163
|
-
name = skill["name"]
|
|
164
|
-
|
|
165
|
-
# Skip disabled skills
|
|
166
|
-
if not enabled_skills.get(name, True):
|
|
167
|
-
continue
|
|
168
|
-
|
|
169
|
-
score, reasons = score_skill(prompt, skill)
|
|
170
|
-
if score >= threshold:
|
|
171
|
-
matches.append((name, score, reasons, skill.get("description", "")))
|
|
172
|
-
|
|
173
|
-
# No matches - silent exit
|
|
174
|
-
if not matches:
|
|
175
|
-
return 0
|
|
176
|
-
|
|
177
|
-
# Sort by score (highest first)
|
|
178
|
-
matches.sort(key=lambda t: t[1], reverse=True)
|
|
179
|
-
|
|
180
|
-
# Generate and output the reminder using proper JSON format
|
|
181
|
-
reminder = generate_reminder(matches)
|
|
182
|
-
output = {
|
|
183
|
-
"hookSpecificOutput": {
|
|
184
|
-
"hookEventName": "UserPromptSubmit",
|
|
185
|
-
"additionalContext": reminder,
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
print(json.dumps(output))
|
|
189
|
-
|
|
190
|
-
return 0
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if __name__ == "__main__":
|
|
194
|
-
raise SystemExit(main())
|