bluera-knowledge 0.11.21 → 0.12.1

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.
@@ -0,0 +1,7 @@
1
+ {
2
+ "provider": "openai",
3
+ "model": "codex-mini-latest",
4
+ "prompt": "What are the best trigger keywords and patterns for a skill auto-activation system in a Claude Code plugin? Context: We're building a UserPromptSubmit hook for the bluera-knowledge plugin that detects when users would benefit from learning about specific skills. The 5 skills are: (1) knowledge-search - how to query indexed library sources, (2) when-to-query - deciding between BK vs Grep/Read, (3) search-optimization - optimizing search parameters, (4) advanced-workflows - multi-tool orchestration, (5) store-lifecycle - managing knowledge stores. Key insight: if a user already uses BK terminology, they don't need the skill suggested. We want to catch users who SHOULD be using BK but don't know it exists.",
5
+ "response": "Below is a bite‑sized “pattern cookbook” you can drop into your `UserPromptSubmit` hook in the bluera‑knowledge Claude Code plugin. The idea is simple:\n\n1. **Look for “natural” signals** that a user is trying to do something Blue‑Knowledge (BK) was built for, but \n2. **Exclude** anyone already using BK lingo (e.g. “knowledge‑search,” “index,” “store,” “workflow”) so you don’t remind the experienced.\n\n---\n\n## 1. Skill → Trigger‑patterns matrix\n\n| Skill | What it is | Trigger patterns (if user text _contains_) | Exclude if user text _contains_ |\n|:---------------------|:---------------------------------------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------|:--------------------------------------------------------|\n| **knowledge‑search** | How to query an indexed library of docs/sources. | `\\b(find|lookup|search|get me|show me|where can i find)\\b.*\\b(docs|documentation|references?|articles?)\\b` | `\\bknowledge[- ]search\\b|\\bindex(ed)?\\b|\\bstore\\b` |\n| **when‑to‑query** | Deciding whether to use BK vs. grep/`read()` etc. | `\\b(grep|read\\(\\)|scan files|open files manually|search local files)\\b|\\b(is there a better way)\\b` | `\\bwhen[- ]to[- ]query\\b|\\bblue[- ]knowledge\\b` |\n| **search‑optimization** | Tuning filters, relevance, context windows, metadata to get better hits. | `\\b(narrow|filter|refine|limit|boost|prioritize|sort)\\b.*\\b(results|hits|matches)\\b` | `\\bsearch[- ]optimization\\b|\\brelevance\\b|\\bcontext window\\b` |\n| **advanced‑workflows** | Chaining multiple tools/APIs, branching, retrieval‑then‑generation, tool orchestration. | `\\b(chain|pipeline|orchestrate|automate|workflow|multi[- ]step|multi[- ]tool)\\b` | `\\badvanced[- ]workflows\\b|\\btool orchestration\\b` |\n| **store‑lifecycle** | Setting up/updating/archiving knowledge stores, versioning, pruning old info. | `\\b(create|update|delete|archive|prune|version)\\b.*\\b(store|index|database|repository)\\b` | `\\bstore[- ]lifecycle\\b|\\bknowledge[- ]store\\b` |\n\n---\n\n### 1.1. Example Regex snippets\n\n```js\n// snippet for knowledge-search\nconst KNOWLEDGE_SEARCH_RE = /\\b(?:find|lookup|search|get me|show me|where can i find)\\b.*\\b(?:docs|documentation|references?|articles?)\\b/i;\n\n// snippet for when-to-query\nconst WHEN_TO_QUERY_RE = /\\b(?:grep|read\\(\\)|scan files|open files manually|search local files)\\b|\\b(?:is there a better way)\\b/i;\n\n// snippet for store-lifecycle\nconst STORE_LIFECYCLE_RE = /\\b(?:create|update|delete|archive|prune|version)\\b.*\\b(?:store|index|database|repository)\\b/i;\n```\n\n---\n\n## 2. Putting it together in your hook\n\nBelow is a simplified example of how you might wire these patterns into a Claude Code `UserPromptSubmit` hook. You’ll want to adjust the messaging and localization to taste.\n\n```js\n/**\n * bluera-knowledge UserPromptSubmit hook\n * Detect users who could benefit from Blue‑Knowledge skills.\n */\nexport async function onUserPromptSubmit(event, ctx) {\n const text = event.prompt.text;\n\n // quick bailout: if they already speak BK\n const hasBKTerm = /\\bknowledge[- ]search\\b|\\bwhen[- ]to[- ]query\\b|\\bsearch[- ]optimization\\b|\\badvanced[- ]workflows\\b|\\bstore[- ]lifecycle\\b|\\bblue[- ]knowledge\\b/i.test(text);\n if (hasBKTerm) return;\n\n // mapping skill → [pattern, user‐facing hint]\n const SKILLS = [\n {\n name: \"knowledge-search\",\n re: /\\b(?:find|lookup|search|get me|show me|where can i find)\\b.*\\b(?:docs|documentation|references?|articles?)\\b/i,\n hint: \"🔍 You can try using our **knowledge‑search** feature to query indexed docs—no manual grepping needed!\"\n },\n {\n name: \"when-to-query\",\n re: /\\b(?:grep|read\\(\\)|scan files|open files manually|search local files)\\b|\\b(?:is there a better way)\\b/i,\n hint: \"💡 Noticed you're grepping or opening files one by one. Our **when‑to‑query** guide helps decide BK vs. direct file search.\"\n },\n {\n name: \"search-optimization\",\n re: /\\b(?:narrow|filter|refine|limit|boost|prioritize|sort)\\b.*\\b(?:results|hits|matches)\\b/i,\n hint: \"⚙️ Want more relevant hits? See our **search‑optimization** tips for tuning relevance and filters.\"\n },\n {\n name: \"advanced-workflows\",\n re: /\\b(?:chain|pipeline|orchestrate|automate|workflow|multi[- ]step|multi[- ]tool)\\b/i,\n hint: \"🔗 Looking to chain tools or build pipelines? Check out **advanced‑workflows** for multi‑tool orchestration.\"\n },\n {\n name: \"store-lifecycle\",\n re: /\\b(?:create|update|delete|archive|prune|version)\\b.*\\b(?:store|index|database|repository)\\b/i,\n hint: \"🗄️ Managing indexes or archiving old data? Our **store‑lifecycle** guide covers store setup, pruning, and versioning.\"\n }\n ];\n\n // find the first match\n for (const skill of SKILLS) {\n if (skill.re.test(text)) {\n await ctx.reply({\n type: \"skill-suggestion\",\n skill: skill.name,\n message: skill.hint\n });\n break;\n }\n }\n}\n```\n\n---\n\n## 3. Tuning tips\n\n1. **Balance precision/recall**: \n - Start _wide_: shorter patterns catch more but risk noise. \n - Refine as you gather real prompts: update regex to avoid false positives.\n\n2. **Avoid overlap**: \n If two patterns both match, pick the one that feels _most_ immediately helpful. (Above example picks the first.)\n\n3. **User‑facing copy**: \n Keep hints crisp, emoji‑tagged, and link directly to your docs or `@help` commands.\n\n4. **Metrics & feedback loop**: \n Log suggestions shown vs. accepted. Tune patterns around low‑acceptance hits.\n\n---\n\n### Summary\n\nBy combining simple regex‑based detectors with an exclusion of existing BK terminology, you’ll catch “hidden” use cases where users are laboring manually and would love a BK shortcut. Tweak patterns over time against your live traffic, and you’ll have a reliable auto‑activation layer that boosts discoverability _without_ nagging your power users.",
6
+ "timestamp": 1768432278
7
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bluera-knowledge",
3
- "version": "0.11.21",
3
+ "version": "0.12.1",
4
4
  "description": "Clone repos, crawl docs, search locally. Fast, authoritative answers for AI coding agents.",
5
5
  "mcpServers": {
6
6
  "bluera-knowledge": {
package/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
4
4
 
5
+ ## [0.12.1](https://github.com/blueraai/bluera-knowledge/compare/v0.11.21...v0.12.1) (2026-01-14)
6
+
7
+
8
+ ### Features
9
+
10
+ * **hooks:** add PreToolUse hooks for BK suggestions ([23d3fa4](https://github.com/blueraai/bluera-knowledge/commit/23d3fa493dd16427d6bda3ea80064622c6244bba))
11
+ * **hooks:** add skill auto-activation system ([2b4e96b](https://github.com/blueraai/bluera-knowledge/commit/2b4e96bd29f28df63377cdaacab922d4f4321a8f))
12
+
13
+ ## [0.12.0](https://github.com/blueraai/bluera-knowledge/compare/v0.11.21...v0.12.0) (2026-01-14)
14
+
15
+
16
+ ### Features
17
+
18
+ * **hooks:** add skill auto-activation system ([2b4e96b](https://github.com/blueraai/bluera-knowledge/commit/2b4e96bd29f28df63377cdaacab922d4f4321a8f))
19
+
5
20
  ## [0.11.21](https://github.com/blueraai/bluera-knowledge/compare/v0.11.20...v0.11.21) (2026-01-10)
6
21
 
7
22
 
package/README.md CHANGED
@@ -1486,6 +1486,28 @@ This hybrid approach reduces unnecessary tool calls and context usage while main
1486
1486
 
1487
1487
  Result: Fewer tool calls, more accurate results, less context consumed.
1488
1488
 
1489
+ ### 🎯 Skill Auto-Activation
1490
+
1491
+ Skills can automatically suggest themselves when your prompt matches certain patterns.
1492
+
1493
+ **Toggle via slash command:**
1494
+ - `/bluera-knowledge:skill-activation` - Show current status
1495
+ - `/bluera-knowledge:skill-activation on` - Enable (default)
1496
+ - `/bluera-knowledge:skill-activation off` - Disable
1497
+ - `/bluera-knowledge:skill-activation config` - Toggle individual skills
1498
+
1499
+ **How it works:**
1500
+ When enabled, a UserPromptSubmit hook analyzes your prompt for patterns like:
1501
+ - "How does [library] work?" → suggests `knowledge-search`
1502
+ - "Should I grep or search?" → suggests `when-to-query`
1503
+ - "Too many results" → suggests `search-optimization`
1504
+ - "Multi-step workflow" → suggests `advanced-workflows`
1505
+ - "Add/delete store" → suggests `store-lifecycle`
1506
+
1507
+ Claude evaluates each suggestion and invokes relevant skills before answering. Users who already use BK terminology are excluded (they already know the tool).
1508
+
1509
+ **Configuration stored in:** `~/.local/share/bluera-knowledge/skill-activation.json`
1510
+
1489
1511
  ---
1490
1512
 
1491
1513
  ## 💾 Data Storage
@@ -0,0 +1,130 @@
1
+ ---
2
+ description: Toggle skill auto-activation on/off or configure individual skills
3
+ argument-hint: "[on|off|status|config]"
4
+ allowed-tools: ["Read", "Write", "AskUserQuestion"]
5
+ ---
6
+
7
+ # Skill Activation Configuration
8
+
9
+ Manage the bluera-knowledge skill auto-activation system.
10
+
11
+ ## Configuration File
12
+
13
+ Location: `~/.local/share/bluera-knowledge/skill-activation.json`
14
+
15
+ Default configuration (created if missing):
16
+ ```json
17
+ {
18
+ "enabled": true,
19
+ "threshold": 1,
20
+ "skills": {
21
+ "knowledge-search": true,
22
+ "when-to-query": true,
23
+ "search-optimization": true,
24
+ "advanced-workflows": true,
25
+ "store-lifecycle": true
26
+ }
27
+ }
28
+ ```
29
+
30
+ ## Steps
31
+
32
+ ### 1. Parse Arguments
33
+
34
+ Extract the subcommand from $ARGUMENTS:
35
+ - Empty or "status": Show current status
36
+ - "on": Enable skill activation
37
+ - "off": Disable skill activation
38
+ - "config": Interactive skill configuration
39
+
40
+ ### 2. Read Current Configuration
41
+
42
+ Read `~/.local/share/bluera-knowledge/skill-activation.json`
43
+
44
+ If the file doesn't exist, use the default configuration shown above.
45
+
46
+ ### 3. Execute Subcommand
47
+
48
+ **For "status" or empty arguments:**
49
+
50
+ Display the current configuration:
51
+
52
+ ```
53
+ ## Skill Activation Status
54
+
55
+ **Status**: [Enabled/Disabled]
56
+ **Threshold**: [threshold value]
57
+
58
+ ### Individual Skills
59
+ | Skill | Status |
60
+ |-------|--------|
61
+ | knowledge-search | enabled/disabled |
62
+ | when-to-query | enabled/disabled |
63
+ | search-optimization | enabled/disabled |
64
+ | advanced-workflows | enabled/disabled |
65
+ | store-lifecycle | enabled/disabled |
66
+
67
+ Use `/bluera-knowledge:skill-activation config` to toggle individual skills.
68
+ ```
69
+
70
+ **For "on":**
71
+
72
+ 1. Read configuration (or use defaults)
73
+ 2. Set `enabled: true`
74
+ 3. Ensure directory exists: `~/.local/share/bluera-knowledge/`
75
+ 4. Write updated configuration
76
+ 5. Confirm: "Skill activation **enabled**. Skills will be suggested based on your prompts."
77
+
78
+ **For "off":**
79
+
80
+ 1. Read configuration (or use defaults)
81
+ 2. Set `enabled: false`
82
+ 3. Write updated configuration
83
+ 4. Confirm: "Skill activation **disabled**. No skill suggestions will appear."
84
+
85
+ **For "config":**
86
+
87
+ 1. Read current configuration
88
+ 2. Use AskUserQuestion to let user toggle skills:
89
+
90
+ ```json
91
+ {
92
+ "questions": [{
93
+ "question": "Which skills should auto-activate when relevant patterns are detected?",
94
+ "header": "Skills",
95
+ "multiSelect": true,
96
+ "options": [
97
+ {
98
+ "label": "knowledge-search",
99
+ "description": "Suggests when to query BK for library questions"
100
+ },
101
+ {
102
+ "label": "when-to-query",
103
+ "description": "Guides BK vs Grep/Read decisions"
104
+ },
105
+ {
106
+ "label": "search-optimization",
107
+ "description": "Tips for optimizing search parameters"
108
+ },
109
+ {
110
+ "label": "advanced-workflows",
111
+ "description": "Multi-tool orchestration patterns"
112
+ },
113
+ {
114
+ "label": "store-lifecycle",
115
+ "description": "Managing knowledge stores"
116
+ }
117
+ ]
118
+ }]
119
+ }
120
+ ```
121
+
122
+ 3. Update skills based on selection (selected = enabled, unselected = disabled)
123
+ 4. Write updated configuration
124
+ 5. Show updated status table
125
+
126
+ ## Notes
127
+
128
+ - The configuration directory is created automatically if it doesn't exist
129
+ - Changes take effect immediately on the next prompt
130
+ - When disabled globally, no skills are suggested regardless of individual settings
package/hooks/hooks.json CHANGED
@@ -1,6 +1,28 @@
1
1
  {
2
- "description": "bluera-knowledge plugin hooks - automatic dependency checking and job status monitoring",
2
+ "description": "bluera-knowledge plugin hooks - dependency checking, job monitoring, and BK suggestions",
3
3
  "hooks": {
4
+ "PreToolUse": [
5
+ {
6
+ "matcher": "Grep",
7
+ "hooks": [
8
+ {
9
+ "type": "command",
10
+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/pretooluse-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/pretooluse-bk-reminder.py",
21
+ "timeout": 3
22
+ }
23
+ ]
24
+ }
25
+ ],
4
26
  "SessionStart": [
5
27
  {
6
28
  "hooks": [
@@ -19,6 +41,11 @@
19
41
  "type": "command",
20
42
  "command": "${CLAUDE_PLUGIN_ROOT}/hooks/job-status-hook.sh",
21
43
  "timeout": 5
44
+ },
45
+ {
46
+ "type": "command",
47
+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/skill-activation.py",
48
+ "timeout": 5
22
49
  }
23
50
  ]
24
51
  }
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ PreToolUse hook for bluera-knowledge plugin.
4
+ Fires when Claude is about to Grep/Read in dependency directories,
5
+ reminding to consider using BK instead.
6
+ """
7
+
8
+ import json
9
+ import re
10
+ import sys
11
+ from typing import Any
12
+
13
+ # Patterns indicating library/dependency code
14
+ LIBRARY_PATH_PATTERNS = [
15
+ r"node_modules/",
16
+ r"vendor/",
17
+ r"site-packages/",
18
+ r"\.venv/",
19
+ r"venv/",
20
+ r"bower_components/",
21
+ r"packages/.*/node_modules/",
22
+ r"\.npm/",
23
+ r"\.cargo/registry/",
24
+ r"go/pkg/mod/",
25
+ ]
26
+
27
+ # Compile patterns for efficiency
28
+ LIBRARY_PATTERNS_RE = re.compile("|".join(LIBRARY_PATH_PATTERNS), re.IGNORECASE)
29
+
30
+
31
+ def check_grep_tool(tool_input: dict[str, Any]) -> str | None:
32
+ """Check if Grep is targeting library code."""
33
+ path = tool_input.get("path", "")
34
+ pattern = tool_input.get("pattern", "")
35
+
36
+ # Check if searching in library directories
37
+ if path and LIBRARY_PATTERNS_RE.search(path):
38
+ return f"grep in `{path}`"
39
+
40
+ # Check if pattern suggests library-specific search
41
+ # (searching for common library internals)
42
+ return None
43
+
44
+
45
+ def check_read_tool(tool_input: dict[str, Any]) -> str | None:
46
+ """Check if Read is targeting library code."""
47
+ file_path = tool_input.get("file_path", "")
48
+
49
+ if file_path and LIBRARY_PATTERNS_RE.search(file_path):
50
+ return f"read `{file_path}`"
51
+
52
+ return None
53
+
54
+
55
+ def main() -> int:
56
+ try:
57
+ stdin_data = sys.stdin.read()
58
+ if not stdin_data.strip():
59
+ return 0
60
+ hook_input = json.loads(stdin_data)
61
+ except json.JSONDecodeError:
62
+ return 0
63
+
64
+ tool_name = hook_input.get("tool_name", "")
65
+ tool_input = hook_input.get("tool_input", {})
66
+
67
+ trigger_reason = None
68
+
69
+ if tool_name == "Grep":
70
+ trigger_reason = check_grep_tool(tool_input)
71
+ elif tool_name == "Read":
72
+ trigger_reason = check_read_tool(tool_input)
73
+
74
+ if not trigger_reason:
75
+ return 0
76
+
77
+ # Output reminder
78
+ reminder = f"""<system-reminder>
79
+ BLUERA-KNOWLEDGE SUGGESTION
80
+
81
+ You're about to {trigger_reason} which appears to be dependency/library code.
82
+
83
+ Consider querying Bluera Knowledge instead:
84
+ - Use MCP tool `search` with a relevant query
85
+ - Or invoke `/bluera-knowledge:search <query>`
86
+
87
+ BK provides indexed, searchable access to library sources - faster and more context-efficient than grepping through node_modules.
88
+
89
+ If you don't have this library indexed, continue with your current approach.
90
+ </system-reminder>"""
91
+
92
+ print(reminder)
93
+ return 0
94
+
95
+
96
+ if __name__ == "__main__":
97
+ raise SystemExit(main())
@@ -0,0 +1,190 @@
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 the system-reminder for matched skills."""
99
+ lines = [
100
+ "<system-reminder>",
101
+ "BLUERA-KNOWLEDGE SKILL ACTIVATION",
102
+ "",
103
+ "The user's prompt suggests they may benefit from these skills.",
104
+ "For EACH skill below, decide YES/NO:",
105
+ "- YES: Invoke via Skill tool: Skill(skill='bluera-knowledge:<skill-name>')",
106
+ "- NO: Skip (user doesn't need this guidance)",
107
+ "",
108
+ "Candidate skills (ranked by relevance):",
109
+ ]
110
+
111
+ for name, score, reasons, description in matches:
112
+ lines.append(f"")
113
+ lines.append(f" [{name}] (score={score})")
114
+ lines.append(f" Purpose: {description}")
115
+ lines.append(f" Matched: {', '.join(reasons[:3])}")
116
+
117
+ lines.append("")
118
+ lines.append("Evaluate quickly, then answer the user's question.")
119
+ lines.append("</system-reminder>")
120
+
121
+ return "\n".join(lines)
122
+
123
+
124
+ def main() -> int:
125
+ # Load configuration
126
+ config = load_config()
127
+
128
+ # Check if skill activation is enabled
129
+ if not config.get("enabled", True):
130
+ return 0
131
+
132
+ # Get plugin root from environment
133
+ plugin_root_env = os.environ.get("CLAUDE_PLUGIN_ROOT", "")
134
+ if not plugin_root_env:
135
+ return 0
136
+ plugin_root = Path(plugin_root_env)
137
+
138
+ # Read hook input from stdin
139
+ try:
140
+ stdin_data = sys.stdin.read()
141
+ if not stdin_data.strip():
142
+ return 0
143
+ hook_input = json.loads(stdin_data)
144
+ except json.JSONDecodeError:
145
+ return 0
146
+
147
+ prompt = hook_input.get("prompt", "")
148
+ if not prompt.strip():
149
+ return 0
150
+
151
+ # Load rules
152
+ rules = load_rules(plugin_root)
153
+
154
+ # Check global exclusions first
155
+ if check_exclusions(prompt, rules.get("globalExclusions", [])):
156
+ return 0
157
+
158
+ threshold = config.get("threshold", rules.get("threshold", 1))
159
+ enabled_skills = config.get("skills", {})
160
+
161
+ # Score each skill
162
+ matches: list[tuple[str, int, list[str], str]] = []
163
+
164
+ for skill in rules.get("skills", []):
165
+ name = skill["name"]
166
+
167
+ # Skip disabled skills
168
+ if not enabled_skills.get(name, True):
169
+ continue
170
+
171
+ score, reasons = score_skill(prompt, skill)
172
+ if score >= threshold:
173
+ matches.append((name, score, reasons, skill.get("description", "")))
174
+
175
+ # No matches - silent exit
176
+ if not matches:
177
+ return 0
178
+
179
+ # Sort by score (highest first)
180
+ matches.sort(key=lambda t: t[1], reverse=True)
181
+
182
+ # Generate and output the reminder
183
+ reminder = generate_reminder(matches)
184
+ print(reminder)
185
+
186
+ return 0
187
+
188
+
189
+ if __name__ == "__main__":
190
+ raise SystemExit(main())
@@ -0,0 +1,122 @@
1
+ {
2
+ "description": "bluera-knowledge skill activation rules - technology-agnostic patterns for development scenarios",
3
+ "version": 2,
4
+ "globalExclusions": [
5
+ { "keyword": "bluera-knowledge" },
6
+ { "keyword": "bluera knowledge" },
7
+ { "keyword": "/bluera-knowledge:" },
8
+ { "regex": "mcp__.*bluera" }
9
+ ],
10
+ "threshold": 2,
11
+ "skills": [
12
+ {
13
+ "name": "knowledge-search",
14
+ "description": "How to query Bluera Knowledge for library/dependency questions",
15
+ "triggers": [
16
+ { "regex": "the\\s+\\w+(-\\w+)*\\s+(package|library|module|framework|dependency)", "weight": 3 },
17
+ { "regex": "\\w+(-\\w+)*\\s+(package|library|module)\\s+(is|does|keeps|isn't|won't|doesn't)", "weight": 3 },
18
+ { "regex": "\\w+(-\\w+)*\\s+(documentation|docs)\\b", "weight": 2 },
19
+ { "regex": "error\\s+(from|in|with)\\s+(the\\s+)?\\w+(-\\w+)*\\s+(package|library|module)", "weight": 3 },
20
+ { "regex": "(package|library|dependency|module)\\s+(is\\s+)?(throwing|throws|error|failing)", "weight": 3 },
21
+ { "regex": "how\\s+does\\s+(the\\s+)?\\w+(-\\w+)*\\s+(package|library|module|framework)\\s+(handle|work|process)", "weight": 3 },
22
+ { "regex": "what\\s+does\\s+(the\\s+)?\\w+(-\\w+)*\\s+(package|library)\\s+(do|return|accept)", "weight": 3 },
23
+ { "regex": "why\\s+does\\s+(the\\s+)?\\w+(-\\w+)*\\s+(package|library|module)", "weight": 3 },
24
+ { "regex": "(configure|config|settings)\\s+(for\\s+)?(the\\s+)?\\w+(-\\w+)*\\s+(package|library)", "weight": 3 },
25
+ { "regex": "(upgraded?|updated?)\\s+(the\\s+)?\\w+(-\\w+)*\\s+(package|library|dependency)", "weight": 2 },
26
+ { "regex": "integrate\\s+(the\\s+)?\\w+(-\\w+)*\\s+(package|library)", "weight": 2 },
27
+ { "regex": "dependency\\s+(is|keeps|isn't|won't|error|issue|problem)", "weight": 2 },
28
+ { "regex": "third[- ]party\\s+(library|package|code)", "weight": 2 }
29
+ ],
30
+ "exclusions": [
31
+ { "keyword": "my code" },
32
+ { "keyword": "our code" },
33
+ { "keyword": "this function" },
34
+ { "keyword": "this file" },
35
+ { "keyword": "this component" },
36
+ { "keyword": "this class" },
37
+ { "keyword": "this project" },
38
+ { "keyword": "I wrote" },
39
+ { "keyword": "we wrote" },
40
+ { "keyword": "my implementation" },
41
+ { "keyword": "store" },
42
+ { "keyword": "index" }
43
+ ]
44
+ },
45
+ {
46
+ "name": "when-to-query",
47
+ "description": "Decision guide for Bluera Knowledge vs Grep/Read",
48
+ "triggers": [
49
+ { "keyword": "should i grep", "weight": 2 },
50
+ { "keyword": "where should i look", "weight": 2 },
51
+ { "keyword": "grep or search", "weight": 2 },
52
+ { "keyword": "search or grep", "weight": 2 },
53
+ { "regex": "where\\s+(should|do)\\s+i\\s+(find|look)\\s+.*(library|package|dependency)", "weight": 3 },
54
+ { "regex": "is\\s+there\\s+a\\s+better\\s+way\\s+to\\s+(search|find)", "weight": 2 },
55
+ { "regex": "should\\s+i\\s+(use\\s+)?grep\\s+(for|to)", "weight": 2 },
56
+ { "regex": "how\\s+(do|should)\\s+i\\s+find.*(in|from)\\s+(a\\s+)?(library|package|dependency)", "weight": 3 }
57
+ ],
58
+ "exclusions": [
59
+ { "keyword": "store" },
60
+ { "keyword": "index" }
61
+ ]
62
+ },
63
+ {
64
+ "name": "search-optimization",
65
+ "description": "Optimizing search parameters and token usage",
66
+ "triggers": [
67
+ { "keyword": "too many results", "weight": 2 },
68
+ { "keyword": "too few results", "weight": 2 },
69
+ { "keyword": "limit results", "weight": 2 },
70
+ { "keyword": "reduce tokens", "weight": 2 },
71
+ { "keyword": "token usage", "weight": 2 },
72
+ { "keyword": "optimize search", "weight": 2 },
73
+ { "keyword": "detail level", "weight": 2 },
74
+ { "regex": "\\b(minimal|contextual|full)\\s+detail", "weight": 2 },
75
+ { "regex": "\\b(vector|fts|hybrid)\\s+(search|mode)", "weight": 2 },
76
+ { "regex": "narrow\\s+(down\\s+)?(the\\s+)?results", "weight": 2 },
77
+ { "regex": "search\\s+(is\\s+)?(returning|giving)\\s+(too\\s+)?(many|few)", "weight": 2 }
78
+ ],
79
+ "exclusions": [
80
+ { "regex": "--?(limit|detail|mode|threshold)\\s*=" }
81
+ ]
82
+ },
83
+ {
84
+ "name": "advanced-workflows",
85
+ "description": "Multi-tool orchestration patterns",
86
+ "triggers": [
87
+ { "keyword": "multi-step", "weight": 2 },
88
+ { "keyword": "orchestration", "weight": 2 },
89
+ { "keyword": "job monitoring", "weight": 2 },
90
+ { "keyword": "background job", "weight": 2 },
91
+ { "keyword": "combine tools", "weight": 2 },
92
+ { "keyword": "chain operations", "weight": 2 },
93
+ { "regex": "chain.*searches", "weight": 2 },
94
+ { "regex": "multiple.*searches", "weight": 2 },
95
+ { "regex": "search.*then\\s+(summarize|extract|filter)", "weight": 2 },
96
+ { "regex": "for\\s+each\\s+(search\\s+)?(result|match)", "weight": 2 }
97
+ ],
98
+ "exclusions": []
99
+ },
100
+ {
101
+ "name": "store-lifecycle",
102
+ "description": "Managing knowledge stores",
103
+ "triggers": [
104
+ { "keyword": "add store", "weight": 2 },
105
+ { "keyword": "create store", "weight": 2 },
106
+ { "keyword": "delete store", "weight": 2 },
107
+ { "keyword": "remove store", "weight": 2 },
108
+ { "keyword": "index store", "weight": 2 },
109
+ { "keyword": "re-index", "weight": 2 },
110
+ { "keyword": "reindex", "weight": 2 },
111
+ { "keyword": "knowledge store", "weight": 2 },
112
+ { "regex": "add\\s+(a\\s+)?(repo|repository|folder|directory)\\s+(to|for)\\s+(knowledge|indexing|search)", "weight": 3 },
113
+ { "regex": "index\\s+(a|the|my)\\s+(repo|repository|folder|directory|library|package)", "weight": 2 },
114
+ { "regex": "set\\s+up.*(knowledge|search)\\s*(store|index)", "weight": 2 },
115
+ { "regex": "(backup|snapshot|archive).*(knowledge|search)\\s*(store|index)", "weight": 2 }
116
+ ],
117
+ "exclusions": [
118
+ { "regex": "/bluera-knowledge:(add-repo|add-folder|remove-store|index)" }
119
+ ]
120
+ }
121
+ ]
122
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bluera-knowledge",
3
- "version": "0.11.21",
3
+ "version": "0.12.1",
4
4
  "description": "CLI tool for managing knowledge stores with semantic search",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,6 +21,9 @@ vi.mock('chokidar', () => ({
21
21
  }),
22
22
  }));
23
23
 
24
+ // Import chokidar once after mock is set up to avoid dynamic import flakiness
25
+ import * as chokidar from 'chokidar';
26
+
24
27
  describe('WatchService', () => {
25
28
  let watchService: WatchService;
26
29
  let mockIndexService: IndexService;
@@ -78,7 +81,7 @@ describe('WatchService', () => {
78
81
  const noopErrorHandler = (): void => {};
79
82
 
80
83
  it('starts watching a file store', async () => {
81
- const { watch } = await import('chokidar');
84
+ const { watch } = chokidar;
82
85
 
83
86
  await watchService.watch(mockFileStore, 1000, undefined, noopErrorHandler);
84
87
 
@@ -93,7 +96,7 @@ describe('WatchService', () => {
93
96
  });
94
97
 
95
98
  it('starts watching a repo store', async () => {
96
- const { watch } = await import('chokidar');
99
+ const { watch } = chokidar;
97
100
 
98
101
  await watchService.watch(mockRepoStore, 1000, undefined, noopErrorHandler);
99
102
 
@@ -109,7 +112,7 @@ describe('WatchService', () => {
109
112
  });
110
113
 
111
114
  it('does not start watching if already watching the same store', async () => {
112
- const { watch } = await import('chokidar');
115
+ const { watch } = chokidar;
113
116
 
114
117
  await watchService.watch(mockFileStore, 1000, undefined, noopErrorHandler);
115
118
  const callCount1 = (watch as ReturnType<typeof vi.fn>).mock.calls.length;
@@ -121,7 +124,7 @@ describe('WatchService', () => {
121
124
  });
122
125
 
123
126
  it('allows watching multiple different stores', async () => {
124
- const { watch } = await import('chokidar');
127
+ const { watch } = chokidar;
125
128
 
126
129
  await watchService.watch(mockFileStore, 1000, undefined, noopErrorHandler);
127
130
  await watchService.watch(mockRepoStore, 1000, undefined, noopErrorHandler);
@@ -454,7 +457,7 @@ describe('WatchService', () => {
454
457
  });
455
458
 
456
459
  it('removes watcher from internal map', async () => {
457
- const { watch } = await import('chokidar');
460
+ const { watch } = chokidar;
458
461
 
459
462
  await watchService.watch(mockFileStore, 1000, undefined, noopErrorHandler);
460
463
  await watchService.unwatch(mockFileStore.id);
@@ -523,7 +526,7 @@ describe('WatchService', () => {
523
526
  });
524
527
 
525
528
  it('clears all watchers from map', async () => {
526
- const { watch } = await import('chokidar');
529
+ const { watch } = chokidar;
527
530
 
528
531
  await watchService.watch(mockFileStore, 1000, undefined, noopErrorHandler);
529
532
  await watchService.watch(mockRepoStore, 1000, undefined, noopErrorHandler);
@@ -546,7 +549,7 @@ describe('WatchService', () => {
546
549
  const noopErrorHandler = (): void => {};
547
550
 
548
551
  it('ignores .git directories', async () => {
549
- const { watch } = await import('chokidar');
552
+ const { watch } = chokidar;
550
553
 
551
554
  await watchService.watch(mockFileStore, 1000, undefined, noopErrorHandler);
552
555
 
@@ -557,7 +560,7 @@ describe('WatchService', () => {
557
560
  });
558
561
 
559
562
  it('ignores node_modules directories', async () => {
560
- const { watch } = await import('chokidar');
563
+ const { watch } = chokidar;
561
564
 
562
565
  await watchService.watch(mockFileStore, 1000, undefined, noopErrorHandler);
563
566
 
@@ -567,7 +570,7 @@ describe('WatchService', () => {
567
570
  });
568
571
 
569
572
  it('ignores dist and build directories', async () => {
570
- const { watch } = await import('chokidar');
573
+ const { watch } = chokidar;
571
574
 
572
575
  await watchService.watch(mockFileStore, 1000, undefined, noopErrorHandler);
573
576
 
@@ -577,7 +580,7 @@ describe('WatchService', () => {
577
580
  });
578
581
 
579
582
  it('sets persistent to true', async () => {
580
- const { watch } = await import('chokidar');
583
+ const { watch } = chokidar;
581
584
 
582
585
  await watchService.watch(mockFileStore, 1000, undefined, noopErrorHandler);
583
586
 
@@ -586,7 +589,7 @@ describe('WatchService', () => {
586
589
  });
587
590
 
588
591
  it('sets ignoreInitial to true', async () => {
589
- const { watch } = await import('chokidar');
592
+ const { watch } = chokidar;
590
593
 
591
594
  await watchService.watch(mockFileStore, 1000, undefined, noopErrorHandler);
592
595