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.
@@ -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
- }
@@ -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())