bluera-knowledge 0.34.1 → 0.35.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/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +21 -0
- package/dist/{chunk-V5MWZM5X.js → chunk-DNGE7FZ4.js} +53 -2
- package/dist/chunk-DNGE7FZ4.js.map +1 -0
- package/dist/{chunk-VELBEZVB.js → chunk-L2SC6J4K.js} +10 -2
- package/dist/{chunk-VELBEZVB.js.map → chunk-L2SC6J4K.js.map} +1 -1
- package/dist/{chunk-TD3VX74F.js → chunk-MQQ46BST.js} +2 -2
- package/dist/index.js +3 -3
- package/dist/mcp/server.js +2 -2
- package/dist/workers/background-worker-cli.js +2 -2
- package/hooks/posttooluse-bk-reminder.py +0 -0
- package/hooks/posttooluse-web-research.py +0 -0
- package/hooks/posttooluse-websearch-bk.py +0 -0
- package/package.json +1 -1
- package/python/ast_worker.py +10 -0
- package/scripts/auto-setup.sh +3 -0
- package/skills/advanced-workflows/SKILL.md +26 -246
- package/skills/advanced-workflows/references/examples.md +86 -0
- package/skills/eval/SKILL.md +16 -203
- package/skills/eval/references/output-format.md +73 -0
- package/skills/eval/references/procedures.md +61 -0
- package/skills/store-lifecycle/SKILL.md +16 -441
- package/skills/store-lifecycle/references/operations.md +75 -0
- package/skills/store-lifecycle/references/source-types.md +48 -0
- package/skills/test-plugin/SKILL.md +8 -515
- package/skills/test-plugin/references/output-format.md +43 -0
- package/skills/test-plugin/references/test-procedures.md +107 -0
- package/dist/chunk-V5MWZM5X.js.map +0 -1
- package/hooks/pretooluse-bk-suggest.py +0 -296
- package/hooks/skill-activation.py +0 -221
- package/hooks/skill-rules.json +0 -131
- /package/dist/{chunk-TD3VX74F.js.map → chunk-MQQ46BST.js.map} +0 -0
|
@@ -1,296 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
PreToolUse hook for bluera-knowledge plugin.
|
|
4
|
-
|
|
5
|
-
Fires before Claude reads/greps in dependency directories.
|
|
6
|
-
- If library IS indexed in BK: BLOCKS the read and suggests BK search
|
|
7
|
-
- If library NOT indexed: suggests BK but allows the read
|
|
8
|
-
|
|
9
|
-
Escape hatch: Set BK_ALLOW_DIRECT_READ=1 env var to bypass all blocking.
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
import json
|
|
13
|
-
import logging
|
|
14
|
-
import os
|
|
15
|
-
import re
|
|
16
|
-
import sys
|
|
17
|
-
from pathlib import Path
|
|
18
|
-
from typing import TypedDict
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class StoreDict(TypedDict, total=False):
|
|
22
|
-
"""Store structure from stores.json."""
|
|
23
|
-
|
|
24
|
-
name: str
|
|
25
|
-
tags: list[str] | None
|
|
26
|
-
url: str | None
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class ToolInputDict(TypedDict, total=False):
|
|
30
|
-
"""Tool input structure from hook."""
|
|
31
|
-
|
|
32
|
-
path: str # For Grep
|
|
33
|
-
file_path: str # For Read
|
|
34
|
-
|
|
35
|
-
# Configure logging to stderr (stdout is for hook output)
|
|
36
|
-
logging.basicConfig(
|
|
37
|
-
level=logging.DEBUG if os.environ.get("BK_DEBUG") else logging.WARNING,
|
|
38
|
-
format="%(name)s: %(message)s",
|
|
39
|
-
stream=sys.stderr,
|
|
40
|
-
)
|
|
41
|
-
logger = logging.getLogger("bk-suggest")
|
|
42
|
-
|
|
43
|
-
# Dependency path patterns with boundary markers to avoid false positives
|
|
44
|
-
# like "my-node_modules-backup/"
|
|
45
|
-
DEPENDENCY_PATTERNS = (
|
|
46
|
-
"/node_modules/",
|
|
47
|
-
"/vendor/",
|
|
48
|
-
"/site-packages/",
|
|
49
|
-
"/.venv/",
|
|
50
|
-
"/venv/",
|
|
51
|
-
"/bower_components/",
|
|
52
|
-
"/.npm/",
|
|
53
|
-
"/.cargo/registry/",
|
|
54
|
-
"/go/pkg/mod/",
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
# Blocking criteria - avoid false positives on ultra-common names
|
|
58
|
-
MIN_NAME_LENGTH = 3
|
|
59
|
-
COMMON_NAME_DENYLIST = frozenset([
|
|
60
|
-
"fs", "os", "core", "util", "utils", "lib", "src", "app", "api",
|
|
61
|
-
"test", "tests", "spec", "dist", "build", "bin", "pkg", "mod",
|
|
62
|
-
])
|
|
63
|
-
|
|
64
|
-
# Module-level cache for stores.json
|
|
65
|
-
_stores_cache: list[StoreDict] = []
|
|
66
|
-
_stores_mtime: float = 0.0
|
|
67
|
-
_stores_path: Path | None = None
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def is_dependency_path(path: str) -> bool:
|
|
71
|
-
"""Return True only if path is inside a dependency directory.
|
|
72
|
-
|
|
73
|
-
Uses boundary markers to avoid false positives like 'my-node_modules-backup/'.
|
|
74
|
-
"""
|
|
75
|
-
# Normalize path separators and ensure leading slash for boundary matching
|
|
76
|
-
normalized = "/" + path.replace("\\", "/").lower()
|
|
77
|
-
return any(pattern in normalized for pattern in DEPENDENCY_PATTERNS)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def extract_library_name(path: str) -> str | None:
|
|
81
|
-
"""Extract library name from dependency path."""
|
|
82
|
-
# node_modules/package-name/... or node_modules/@scope/package/...
|
|
83
|
-
match = re.search(r"node_modules/(@[^/]+/[^/]+|[^/]+)", path)
|
|
84
|
-
if match:
|
|
85
|
-
return match.group(1)
|
|
86
|
-
|
|
87
|
-
# site-packages/package_name/...
|
|
88
|
-
match = re.search(r"site-packages/([^/]+)", path)
|
|
89
|
-
if match:
|
|
90
|
-
return match.group(1)
|
|
91
|
-
|
|
92
|
-
# vendor/package/...
|
|
93
|
-
match = re.search(r"vendor/([^/]+)", path)
|
|
94
|
-
if match:
|
|
95
|
-
return match.group(1)
|
|
96
|
-
|
|
97
|
-
# .cargo/registry/.../package-name-version/...
|
|
98
|
-
match = re.search(r"\.cargo/registry/[^/]+/([^/]+)-\d", path)
|
|
99
|
-
if match:
|
|
100
|
-
return match.group(1)
|
|
101
|
-
|
|
102
|
-
# go/pkg/mod/package@version/...
|
|
103
|
-
match = re.search(r"go/pkg/mod/([^@]+)@", path)
|
|
104
|
-
if match:
|
|
105
|
-
return match.group(1)
|
|
106
|
-
|
|
107
|
-
return None
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def normalize_name(name: str) -> str:
|
|
111
|
-
"""Normalize library name for matching.
|
|
112
|
-
|
|
113
|
-
- Strips scope: @scope/pkg -> pkg
|
|
114
|
-
- Removes hyphens and underscores
|
|
115
|
-
- Lowercases
|
|
116
|
-
"""
|
|
117
|
-
# Strip scope: @scope/pkg -> pkg
|
|
118
|
-
if "/" in name:
|
|
119
|
-
name = name.split("/")[-1]
|
|
120
|
-
# Remove hyphens, underscores, lowercase
|
|
121
|
-
return name.replace("-", "").replace("_", "").lower()
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
def extract_repo_name(url: str | None) -> str | None:
|
|
125
|
-
"""Extract repo name from URL like github.com/owner/repo."""
|
|
126
|
-
if not url:
|
|
127
|
-
return None
|
|
128
|
-
# github.com/owner/repo -> repo
|
|
129
|
-
# github.com/owner/repo.git -> repo
|
|
130
|
-
match = re.search(r"/([^/]+?)(?:\.git)?$", url)
|
|
131
|
-
return match.group(1) if match else None
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
def get_stores_path() -> Path | None:
|
|
135
|
-
"""Get path to stores.json from project root."""
|
|
136
|
-
project_root = os.environ.get("PROJECT_ROOT") or os.environ.get("PWD")
|
|
137
|
-
if not project_root:
|
|
138
|
-
return None
|
|
139
|
-
return Path(project_root) / ".bluera" / "bluera-knowledge" / "data" / "stores.json"
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
def load_stores(stores_path: Path) -> list[StoreDict]:
|
|
143
|
-
"""Load stores from stores.json. Returns empty list on error."""
|
|
144
|
-
if not stores_path.exists():
|
|
145
|
-
logger.debug("stores.json not found: %s", stores_path)
|
|
146
|
-
return []
|
|
147
|
-
|
|
148
|
-
try:
|
|
149
|
-
with open(stores_path, encoding="utf-8") as f:
|
|
150
|
-
data = json.load(f)
|
|
151
|
-
# Registry format: {"version": 1, "stores": [...]}
|
|
152
|
-
if isinstance(data, dict):
|
|
153
|
-
stores = data.get("stores", [])
|
|
154
|
-
return stores if isinstance(stores, list) else []
|
|
155
|
-
# Direct list format: [...]
|
|
156
|
-
return data if isinstance(data, list) else []
|
|
157
|
-
except (json.JSONDecodeError, IOError, OSError) as e:
|
|
158
|
-
logger.debug("Failed to read stores.json: %s", e)
|
|
159
|
-
return []
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
def get_cached_stores() -> list[StoreDict]:
|
|
163
|
-
"""Get stores with mtime-based cache invalidation."""
|
|
164
|
-
global _stores_cache, _stores_mtime, _stores_path
|
|
165
|
-
|
|
166
|
-
if _stores_path is None:
|
|
167
|
-
_stores_path = get_stores_path()
|
|
168
|
-
if _stores_path is None:
|
|
169
|
-
logger.debug("No project root found, skipping store check")
|
|
170
|
-
return []
|
|
171
|
-
|
|
172
|
-
try:
|
|
173
|
-
current_mtime = _stores_path.stat().st_mtime
|
|
174
|
-
if current_mtime != _stores_mtime:
|
|
175
|
-
logger.debug(
|
|
176
|
-
"Cache refresh: stores.json mtime changed (%.3f -> %.3f)",
|
|
177
|
-
_stores_mtime,
|
|
178
|
-
current_mtime,
|
|
179
|
-
)
|
|
180
|
-
_stores_cache = load_stores(_stores_path)
|
|
181
|
-
_stores_mtime = current_mtime
|
|
182
|
-
except (OSError, FileNotFoundError):
|
|
183
|
-
_stores_cache = []
|
|
184
|
-
_stores_mtime = 0.0
|
|
185
|
-
|
|
186
|
-
return _stores_cache
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
def library_is_indexed(lib_name: str, stores: list[StoreDict]) -> str | None:
|
|
190
|
-
"""Return matching store name if library is indexed, None otherwise.
|
|
191
|
-
|
|
192
|
-
Denylist is soft - if an EXACT store name match exists,
|
|
193
|
-
the denylist is overridden (user explicitly created a store for that name).
|
|
194
|
-
Tag/URL fuzzy matching is skipped for denylisted names.
|
|
195
|
-
"""
|
|
196
|
-
if len(lib_name) < MIN_NAME_LENGTH:
|
|
197
|
-
return None # Too short, skip
|
|
198
|
-
|
|
199
|
-
normalized = normalize_name(lib_name)
|
|
200
|
-
is_denylisted = lib_name.lower() in COMMON_NAME_DENYLIST
|
|
201
|
-
|
|
202
|
-
for store in stores:
|
|
203
|
-
store_name = store.get("name", "")
|
|
204
|
-
|
|
205
|
-
# Check store name - exact match overrides denylist
|
|
206
|
-
if normalize_name(store_name) == normalized:
|
|
207
|
-
return store_name
|
|
208
|
-
|
|
209
|
-
# If denylisted, skip tag/URL fuzzy matching (only exact store name counts)
|
|
210
|
-
if is_denylisted:
|
|
211
|
-
continue
|
|
212
|
-
|
|
213
|
-
# Check tags (only if not denylisted)
|
|
214
|
-
for tag in store.get("tags") or []:
|
|
215
|
-
if normalize_name(tag) == normalized:
|
|
216
|
-
return store_name
|
|
217
|
-
|
|
218
|
-
# Check repo URL (only if not denylisted)
|
|
219
|
-
repo_name = extract_repo_name(store.get("url"))
|
|
220
|
-
if repo_name and normalize_name(repo_name) == normalized:
|
|
221
|
-
return store_name
|
|
222
|
-
|
|
223
|
-
return None
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
def get_path_from_input(tool_name: str, tool_input: ToolInputDict) -> str | None:
|
|
227
|
-
"""Extract the path being accessed from tool input."""
|
|
228
|
-
if tool_name == "Grep":
|
|
229
|
-
return tool_input.get("path", "")
|
|
230
|
-
elif tool_name == "Read":
|
|
231
|
-
return tool_input.get("file_path", "")
|
|
232
|
-
return None
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
def main() -> int:
|
|
236
|
-
try:
|
|
237
|
-
# Escape hatch: bypass all blocking if env var is set
|
|
238
|
-
if os.environ.get("BK_ALLOW_DIRECT_READ"):
|
|
239
|
-
logger.debug("Escape hatch: BK_ALLOW_DIRECT_READ env var set, bypassing")
|
|
240
|
-
return 0
|
|
241
|
-
|
|
242
|
-
stdin_data = sys.stdin.read()
|
|
243
|
-
if not stdin_data.strip():
|
|
244
|
-
return 0
|
|
245
|
-
hook_input = json.loads(stdin_data)
|
|
246
|
-
|
|
247
|
-
tool_name = hook_input.get("tool_name", "")
|
|
248
|
-
tool_input = hook_input.get("tool_input", {})
|
|
249
|
-
|
|
250
|
-
path = get_path_from_input(tool_name, tool_input)
|
|
251
|
-
if not path:
|
|
252
|
-
return 0
|
|
253
|
-
|
|
254
|
-
# Fast path: skip if not a dependency path (with boundary checking)
|
|
255
|
-
if not is_dependency_path(path):
|
|
256
|
-
return 0
|
|
257
|
-
|
|
258
|
-
lib_name = extract_library_name(path)
|
|
259
|
-
if not lib_name:
|
|
260
|
-
return 0
|
|
261
|
-
|
|
262
|
-
# Check if library is indexed in BK
|
|
263
|
-
stores = get_cached_stores()
|
|
264
|
-
matching_store = library_is_indexed(lib_name, stores)
|
|
265
|
-
|
|
266
|
-
if matching_store:
|
|
267
|
-
# BLOCK: Library is indexed, force BK usage
|
|
268
|
-
reason = f"'{lib_name}' is indexed in BK store '{matching_store}'. Use BK search tool instead."
|
|
269
|
-
output = {
|
|
270
|
-
"hookSpecificOutput": {
|
|
271
|
-
"hookEventName": "PreToolUse",
|
|
272
|
-
"permissionDecision": "deny",
|
|
273
|
-
"reasonForDecision": reason,
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
print(json.dumps(output))
|
|
277
|
-
else:
|
|
278
|
-
# SUGGEST: Library not indexed, allow but suggest
|
|
279
|
-
suggestion = f"TIP: '{lib_name}' isn't indexed in BK. Consider /bluera-knowledge:add-repo after this task."
|
|
280
|
-
output = {
|
|
281
|
-
"hookSpecificOutput": {
|
|
282
|
-
"hookEventName": "PreToolUse",
|
|
283
|
-
"additionalContext": suggestion,
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
print(json.dumps(output))
|
|
287
|
-
|
|
288
|
-
return 0
|
|
289
|
-
|
|
290
|
-
except Exception as e:
|
|
291
|
-
logger.debug("Hook error, allowing: %s", e)
|
|
292
|
-
return 0 # Never block on unexpected errors
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
if __name__ == "__main__":
|
|
296
|
-
raise SystemExit(main())
|
|
@@ -1,221 +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
|
-
DEFAULT_CONFIG: dict[str, Any] = {
|
|
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
|
-
def get_project_root() -> Path | None:
|
|
31
|
-
"""Get project root from environment variables (per-repo config)."""
|
|
32
|
-
# Check environment variables in priority order
|
|
33
|
-
for env_var in ["PROJECT_ROOT", "PWD"]:
|
|
34
|
-
value = os.environ.get(env_var, "")
|
|
35
|
-
if value:
|
|
36
|
-
return Path(value)
|
|
37
|
-
return None
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def get_config_path() -> Path | None:
|
|
41
|
-
"""Get per-repo config path for skill activation."""
|
|
42
|
-
project_root = get_project_root()
|
|
43
|
-
if project_root is None:
|
|
44
|
-
return None
|
|
45
|
-
return project_root / ".bluera" / "bluera-knowledge" / "skill-activation.json"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def load_config() -> dict[str, Any]:
|
|
49
|
-
"""Load skill activation configuration from per-repo path."""
|
|
50
|
-
config_path = get_config_path()
|
|
51
|
-
if config_path is None or not config_path.exists():
|
|
52
|
-
return DEFAULT_CONFIG.copy()
|
|
53
|
-
try:
|
|
54
|
-
with open(config_path, encoding="utf-8") as f:
|
|
55
|
-
return json.load(f)
|
|
56
|
-
except (json.JSONDecodeError, IOError):
|
|
57
|
-
return DEFAULT_CONFIG.copy()
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def load_rules(plugin_root: Path) -> dict[str, Any]:
|
|
61
|
-
"""Load skill rules from plugin hooks directory."""
|
|
62
|
-
rules_path = plugin_root / "hooks" / "skill-rules.json"
|
|
63
|
-
if not rules_path.exists():
|
|
64
|
-
return {"skills": [], "threshold": 1, "globalExclusions": []}
|
|
65
|
-
with open(rules_path, encoding="utf-8") as f:
|
|
66
|
-
return json.load(f)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def matches_condition(prompt: str, condition: dict[str, Any]) -> bool:
|
|
70
|
-
"""Check if prompt matches a single condition (keyword or regex)."""
|
|
71
|
-
prompt_lower = prompt.lower()
|
|
72
|
-
if "keyword" in condition:
|
|
73
|
-
return condition["keyword"].lower() in prompt_lower
|
|
74
|
-
if "regex" in condition:
|
|
75
|
-
return bool(re.search(condition["regex"], prompt, flags=re.IGNORECASE))
|
|
76
|
-
return False
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def check_exclusions(
|
|
80
|
-
prompt: str, exclusions: list[dict[str, Any]]
|
|
81
|
-
) -> bool:
|
|
82
|
-
"""Check if any exclusion pattern matches. Returns True if excluded."""
|
|
83
|
-
for exc in exclusions:
|
|
84
|
-
if matches_condition(prompt, exc):
|
|
85
|
-
return True
|
|
86
|
-
return False
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def score_skill(
|
|
90
|
-
prompt: str, skill: dict[str, Any]
|
|
91
|
-
) -> tuple[int, list[str]]:
|
|
92
|
-
"""Score a skill against the user prompt. Returns (score, reasons)."""
|
|
93
|
-
reasons: list[str] = []
|
|
94
|
-
score = 0
|
|
95
|
-
|
|
96
|
-
# Check skill-specific exclusions first
|
|
97
|
-
if check_exclusions(prompt, skill.get("exclusions", [])):
|
|
98
|
-
return 0, []
|
|
99
|
-
|
|
100
|
-
for trigger in skill.get("triggers", []):
|
|
101
|
-
if matches_condition(prompt, trigger):
|
|
102
|
-
weight = trigger.get("weight", 1)
|
|
103
|
-
score += weight
|
|
104
|
-
if "keyword" in trigger:
|
|
105
|
-
reasons.append(f'keyword "{trigger["keyword"]}"')
|
|
106
|
-
elif "regex" in trigger:
|
|
107
|
-
reasons.append(f'pattern match')
|
|
108
|
-
|
|
109
|
-
return score, reasons
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
def generate_reminder(
|
|
113
|
-
matches: list[tuple[str, int, list[str], str]]
|
|
114
|
-
) -> str:
|
|
115
|
-
"""Generate assertive skill activation reminder using forced evaluation pattern."""
|
|
116
|
-
lines = [
|
|
117
|
-
"CRITICAL DIRECTIVE: Library/Dependency Question Detected",
|
|
118
|
-
"",
|
|
119
|
-
"BEFORE answering, you MUST:",
|
|
120
|
-
"",
|
|
121
|
-
"1. CHECK if a bluera-knowledge store exists for this library:",
|
|
122
|
-
" → Use mcp__bluera-knowledge__execute(command='stores') to list indexed libraries",
|
|
123
|
-
"",
|
|
124
|
-
"2. IF INDEXED: Query the indexed source FIRST:",
|
|
125
|
-
" → mcp__bluera-knowledge__search(query='your question', intent='find-implementation')",
|
|
126
|
-
" → This is FASTER and more accurate than grepping node_modules/vendor",
|
|
127
|
-
"",
|
|
128
|
-
"3. IF NOT INDEXED: Suggest adding it:",
|
|
129
|
-
" → Tell the user: '/bluera-knowledge:add-repo to index this library for future queries'",
|
|
130
|
-
"",
|
|
131
|
-
"MATCHED SKILLS (invoke for guidance):",
|
|
132
|
-
]
|
|
133
|
-
|
|
134
|
-
for name, score, reasons, description in matches:
|
|
135
|
-
lines.append(f" • Skill(skill='bluera-knowledge:{name}') - {description}")
|
|
136
|
-
lines.append(f" Triggered by: {', '.join(reasons[:2])}")
|
|
137
|
-
|
|
138
|
-
lines.append("")
|
|
139
|
-
lines.append("WHY THIS MATTERS:")
|
|
140
|
-
lines.append(" - Grepping node_modules is SLOW (reads thousands of files)")
|
|
141
|
-
lines.append(" - Indexed search returns pre-chunked, semantic results")
|
|
142
|
-
lines.append(" - Token cost is 10-100x lower with indexed search")
|
|
143
|
-
lines.append("")
|
|
144
|
-
lines.append("FAILURE TO CONSULT INDEXED SOURCES = SUBOPTIMAL RESPONSE")
|
|
145
|
-
|
|
146
|
-
return "\n".join(lines)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
def main() -> int:
|
|
150
|
-
# Load configuration
|
|
151
|
-
config = load_config()
|
|
152
|
-
|
|
153
|
-
# Check if skill activation is enabled
|
|
154
|
-
if not config.get("enabled", True):
|
|
155
|
-
return 0
|
|
156
|
-
|
|
157
|
-
# Get plugin root from environment
|
|
158
|
-
plugin_root_env = os.environ.get("CLAUDE_PLUGIN_ROOT", "")
|
|
159
|
-
if not plugin_root_env:
|
|
160
|
-
return 0
|
|
161
|
-
plugin_root = Path(plugin_root_env)
|
|
162
|
-
|
|
163
|
-
# Read hook input from stdin
|
|
164
|
-
try:
|
|
165
|
-
stdin_data = sys.stdin.read()
|
|
166
|
-
if not stdin_data.strip():
|
|
167
|
-
return 0
|
|
168
|
-
hook_input = json.loads(stdin_data)
|
|
169
|
-
except json.JSONDecodeError:
|
|
170
|
-
return 0
|
|
171
|
-
|
|
172
|
-
prompt = hook_input.get("prompt", "")
|
|
173
|
-
if not prompt.strip():
|
|
174
|
-
return 0
|
|
175
|
-
|
|
176
|
-
# Load rules
|
|
177
|
-
rules = load_rules(plugin_root)
|
|
178
|
-
|
|
179
|
-
# Check global exclusions first
|
|
180
|
-
if check_exclusions(prompt, rules.get("globalExclusions", [])):
|
|
181
|
-
return 0
|
|
182
|
-
|
|
183
|
-
threshold = config.get("threshold", rules.get("threshold", 1))
|
|
184
|
-
enabled_skills = config.get("skills", {})
|
|
185
|
-
|
|
186
|
-
# Score each skill
|
|
187
|
-
matches: list[tuple[str, int, list[str], str]] = []
|
|
188
|
-
|
|
189
|
-
for skill in rules.get("skills", []):
|
|
190
|
-
name = skill["name"]
|
|
191
|
-
|
|
192
|
-
# Skip disabled skills
|
|
193
|
-
if not enabled_skills.get(name, True):
|
|
194
|
-
continue
|
|
195
|
-
|
|
196
|
-
score, reasons = score_skill(prompt, skill)
|
|
197
|
-
if score >= threshold:
|
|
198
|
-
matches.append((name, score, reasons, skill.get("description", "")))
|
|
199
|
-
|
|
200
|
-
# No matches - silent exit
|
|
201
|
-
if not matches:
|
|
202
|
-
return 0
|
|
203
|
-
|
|
204
|
-
# Sort by score (highest first)
|
|
205
|
-
matches.sort(key=lambda t: t[1], reverse=True)
|
|
206
|
-
|
|
207
|
-
# Generate and output the reminder using proper JSON format
|
|
208
|
-
reminder = generate_reminder(matches)
|
|
209
|
-
output = {
|
|
210
|
-
"hookSpecificOutput": {
|
|
211
|
-
"hookEventName": "UserPromptSubmit",
|
|
212
|
-
"additionalContext": reminder,
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
print(json.dumps(output))
|
|
216
|
-
|
|
217
|
-
return 0
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if __name__ == "__main__":
|
|
221
|
-
raise SystemExit(main())
|
package/hooks/skill-rules.json
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
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
|
-
{ "regex": "look\\s+(at|in|into)\\s+(the\\s+)?\\w+(-\\w+)*\\s+(source|code|internals)", "weight": 3 },
|
|
30
|
-
{ "regex": "\\w+(-\\w+)*\\s+source\\s+code", "weight": 2 },
|
|
31
|
-
{ "regex": "(find|show|get)\\s+(me\\s+)?(the\\s+)?\\w+(-\\w+)*\\s+(implementation|source|code)", "weight": 3 },
|
|
32
|
-
{ "regex": "TypeError|ImportError|ModuleNotFoundError|AttributeError", "weight": 2 },
|
|
33
|
-
{ "regex": "\\w+(-\\w+)*\\s+is\\s+(deprecated|removed|changed)", "weight": 2 },
|
|
34
|
-
{ "regex": "breaking\\s+change.*(\\w+(-\\w+)*\\s+)?(package|library|dependency)", "weight": 3 },
|
|
35
|
-
{ "regex": "migrate\\s+(from|to)\\s+\\w+(-\\w+)*", "weight": 2 }
|
|
36
|
-
],
|
|
37
|
-
"exclusions": [
|
|
38
|
-
{ "keyword": "my code" },
|
|
39
|
-
{ "keyword": "our code" },
|
|
40
|
-
{ "keyword": "this function" },
|
|
41
|
-
{ "keyword": "this file" },
|
|
42
|
-
{ "keyword": "this component" },
|
|
43
|
-
{ "keyword": "this class" },
|
|
44
|
-
{ "keyword": "this project" },
|
|
45
|
-
{ "keyword": "I wrote" },
|
|
46
|
-
{ "keyword": "we wrote" },
|
|
47
|
-
{ "keyword": "my implementation" },
|
|
48
|
-
{ "keyword": "store" },
|
|
49
|
-
{ "keyword": "index" },
|
|
50
|
-
{ "keyword": "in my" },
|
|
51
|
-
{ "keyword": "in our" }
|
|
52
|
-
]
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
"name": "when-to-query",
|
|
56
|
-
"description": "Decision guide for Bluera Knowledge vs Grep/Read",
|
|
57
|
-
"triggers": [
|
|
58
|
-
{ "keyword": "should i grep", "weight": 2 },
|
|
59
|
-
{ "keyword": "where should i look", "weight": 2 },
|
|
60
|
-
{ "keyword": "grep or search", "weight": 2 },
|
|
61
|
-
{ "keyword": "search or grep", "weight": 2 },
|
|
62
|
-
{ "regex": "where\\s+(should|do)\\s+i\\s+(find|look)\\s+.*(library|package|dependency)", "weight": 3 },
|
|
63
|
-
{ "regex": "is\\s+there\\s+a\\s+better\\s+way\\s+to\\s+(search|find)", "weight": 2 },
|
|
64
|
-
{ "regex": "should\\s+i\\s+(use\\s+)?grep\\s+(for|to)", "weight": 2 },
|
|
65
|
-
{ "regex": "how\\s+(do|should)\\s+i\\s+find.*(in|from)\\s+(a\\s+)?(library|package|dependency)", "weight": 3 }
|
|
66
|
-
],
|
|
67
|
-
"exclusions": [
|
|
68
|
-
{ "keyword": "store" },
|
|
69
|
-
{ "keyword": "index" }
|
|
70
|
-
]
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
"name": "search-optimization",
|
|
74
|
-
"description": "Optimizing search parameters and token usage",
|
|
75
|
-
"triggers": [
|
|
76
|
-
{ "keyword": "too many results", "weight": 2 },
|
|
77
|
-
{ "keyword": "too few results", "weight": 2 },
|
|
78
|
-
{ "keyword": "limit results", "weight": 2 },
|
|
79
|
-
{ "keyword": "reduce tokens", "weight": 2 },
|
|
80
|
-
{ "keyword": "token usage", "weight": 2 },
|
|
81
|
-
{ "keyword": "optimize search", "weight": 2 },
|
|
82
|
-
{ "keyword": "detail level", "weight": 2 },
|
|
83
|
-
{ "regex": "\\b(minimal|contextual|full)\\s+detail", "weight": 2 },
|
|
84
|
-
{ "regex": "\\b(vector|fts|hybrid)\\s+(search|mode)", "weight": 2 },
|
|
85
|
-
{ "regex": "narrow\\s+(down\\s+)?(the\\s+)?results", "weight": 2 },
|
|
86
|
-
{ "regex": "search\\s+(is\\s+)?(returning|giving)\\s+(too\\s+)?(many|few)", "weight": 2 }
|
|
87
|
-
],
|
|
88
|
-
"exclusions": [
|
|
89
|
-
{ "regex": "--?(limit|detail|mode|threshold)\\s*=" }
|
|
90
|
-
]
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
"name": "advanced-workflows",
|
|
94
|
-
"description": "Multi-tool orchestration patterns",
|
|
95
|
-
"triggers": [
|
|
96
|
-
{ "keyword": "multi-step", "weight": 2 },
|
|
97
|
-
{ "keyword": "orchestration", "weight": 2 },
|
|
98
|
-
{ "keyword": "job monitoring", "weight": 2 },
|
|
99
|
-
{ "keyword": "background job", "weight": 2 },
|
|
100
|
-
{ "keyword": "combine tools", "weight": 2 },
|
|
101
|
-
{ "keyword": "chain operations", "weight": 2 },
|
|
102
|
-
{ "regex": "chain.*searches", "weight": 2 },
|
|
103
|
-
{ "regex": "multiple.*searches", "weight": 2 },
|
|
104
|
-
{ "regex": "search.*then\\s+(summarize|extract|filter)", "weight": 2 },
|
|
105
|
-
{ "regex": "for\\s+each\\s+(search\\s+)?(result|match)", "weight": 2 }
|
|
106
|
-
],
|
|
107
|
-
"exclusions": []
|
|
108
|
-
},
|
|
109
|
-
{
|
|
110
|
-
"name": "store-lifecycle",
|
|
111
|
-
"description": "Managing knowledge stores",
|
|
112
|
-
"triggers": [
|
|
113
|
-
{ "keyword": "add store", "weight": 2 },
|
|
114
|
-
{ "keyword": "create store", "weight": 2 },
|
|
115
|
-
{ "keyword": "delete store", "weight": 2 },
|
|
116
|
-
{ "keyword": "remove store", "weight": 2 },
|
|
117
|
-
{ "keyword": "index store", "weight": 2 },
|
|
118
|
-
{ "keyword": "re-index", "weight": 2 },
|
|
119
|
-
{ "keyword": "reindex", "weight": 2 },
|
|
120
|
-
{ "keyword": "knowledge store", "weight": 2 },
|
|
121
|
-
{ "regex": "add\\s+(a\\s+)?(repo|repository|folder|directory)\\s+(to|for)\\s+(knowledge|indexing|search)", "weight": 3 },
|
|
122
|
-
{ "regex": "index\\s+(a|the|my)\\s+(repo|repository|folder|directory|library|package)", "weight": 2 },
|
|
123
|
-
{ "regex": "set\\s+up.*(knowledge|search)\\s*(store|index)", "weight": 2 },
|
|
124
|
-
{ "regex": "(backup|snapshot|archive).*(knowledge|search)\\s*(store|index)", "weight": 2 }
|
|
125
|
-
],
|
|
126
|
-
"exclusions": [
|
|
127
|
-
{ "regex": "/bluera-knowledge:(add-repo|add-folder|remove-store|index)" }
|
|
128
|
-
]
|
|
129
|
-
}
|
|
130
|
-
]
|
|
131
|
-
}
|
|
File without changes
|