bluera-knowledge 0.31.0 → 0.33.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.
Files changed (70) hide show
  1. package/.claude-plugin/plugin.json +23 -0
  2. package/.mcp.json +13 -0
  3. package/CHANGELOG.md +42 -0
  4. package/NOTICE +47 -0
  5. package/README.md +2 -2
  6. package/bun.lock +1978 -0
  7. package/dist/{chunk-B335UOU7.js → chunk-3TB7TDVF.js} +24 -3
  8. package/dist/chunk-3TB7TDVF.js.map +1 -0
  9. package/dist/{chunk-KCI4U6FH.js → chunk-KDZDLJUY.js} +2 -2
  10. package/dist/{chunk-AEXFPA57.js → chunk-YDTTD53Y.js} +158 -26
  11. package/dist/chunk-YDTTD53Y.js.map +1 -0
  12. package/dist/index.js +3 -3
  13. package/dist/mcp/bootstrap.js +10 -0
  14. package/dist/mcp/bootstrap.js.map +1 -1
  15. package/dist/mcp/server.d.ts +5 -3
  16. package/dist/mcp/server.js +2 -2
  17. package/dist/workers/background-worker-cli.js +2 -2
  18. package/hooks/check-ready.sh +109 -0
  19. package/hooks/hooks.json +97 -0
  20. package/hooks/job-status-hook.sh +51 -0
  21. package/hooks/posttooluse-bk-reminder.py +126 -0
  22. package/hooks/posttooluse-web-research.py +209 -0
  23. package/hooks/posttooluse-websearch-bk.py +158 -0
  24. package/hooks/pretooluse-bk-suggest.py +296 -0
  25. package/hooks/skill-activation.py +221 -0
  26. package/hooks/skill-rules.json +131 -0
  27. package/package.json +9 -2
  28. package/scripts/CLAUDE.md +65 -0
  29. package/scripts/auto-setup.sh +65 -0
  30. package/scripts/bench-regression.sh +345 -0
  31. package/scripts/dev.sh +16 -0
  32. package/scripts/doctor.sh +103 -0
  33. package/scripts/download-models.ts +188 -0
  34. package/scripts/export-web-store.ts +142 -0
  35. package/scripts/lib/mock-server.sh +70 -0
  36. package/scripts/mcp-wrapper.sh +91 -0
  37. package/scripts/setup.sh +224 -0
  38. package/scripts/statusline-module.sh +29 -0
  39. package/scripts/test-mcp-dev.js +260 -0
  40. package/scripts/validate-local.sh +412 -0
  41. package/scripts/validate-npm-release.sh +406 -0
  42. package/skills/add-folder/SKILL.md +48 -0
  43. package/skills/add-repo/SKILL.md +50 -0
  44. package/skills/advanced-workflows/SKILL.md +273 -0
  45. package/skills/cancel/SKILL.md +63 -0
  46. package/skills/check-status/SKILL.md +130 -0
  47. package/skills/crawl/SKILL.md +61 -0
  48. package/skills/doctor/SKILL.md +27 -0
  49. package/skills/eval/SKILL.md +222 -0
  50. package/skills/health/SKILL.md +72 -0
  51. package/skills/index/SKILL.md +48 -0
  52. package/skills/knowledge-search/SKILL.md +110 -0
  53. package/skills/remove-store/SKILL.md +52 -0
  54. package/skills/search/SKILL.md +80 -0
  55. package/skills/search/search.sh +63 -0
  56. package/skills/search-optimization/SKILL.md +199 -0
  57. package/skills/search-optimization/references/mistakes.md +21 -0
  58. package/skills/search-optimization/references/strategies.md +80 -0
  59. package/skills/skill-activation/SKILL.md +131 -0
  60. package/skills/statusline/SKILL.md +19 -0
  61. package/skills/store-lifecycle/SKILL.md +470 -0
  62. package/skills/stores/SKILL.md +54 -0
  63. package/skills/suggest/SKILL.md +118 -0
  64. package/skills/sync/SKILL.md +96 -0
  65. package/skills/test-plugin/SKILL.md +547 -0
  66. package/skills/uninstall/SKILL.md +65 -0
  67. package/skills/when-to-query/SKILL.md +160 -0
  68. package/dist/chunk-AEXFPA57.js.map +0 -1
  69. package/dist/chunk-B335UOU7.js.map +0 -1
  70. /package/dist/{chunk-KCI4U6FH.js.map → chunk-KDZDLJUY.js.map} +0 -0
@@ -0,0 +1,103 @@
1
+ #!/bin/bash
2
+ # Bluera Knowledge Doctor - Comprehensive Diagnostics
3
+ # Run with: /bluera-knowledge:doctor
4
+
5
+ PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(dirname "$(dirname "$0")")}"
6
+
7
+ echo "═══════════════════════════════════════════════════════════"
8
+ echo " Bluera Knowledge Doctor"
9
+ echo "═══════════════════════════════════════════════════════════"
10
+ echo ""
11
+
12
+ ISSUES=0
13
+
14
+ # Check 1: Build tools
15
+ echo "Checking build tools..."
16
+ if command -v make &>/dev/null; then
17
+ echo " [OK] make found"
18
+ else
19
+ echo " [FAIL] make NOT found - REQUIRED for native modules"
20
+ echo ""
21
+ echo " FIX: Install build tools:"
22
+ echo " Debian/Ubuntu: sudo apt install build-essential"
23
+ echo " Fedora/RHEL: sudo dnf groupinstall 'Development Tools'"
24
+ echo " macOS: xcode-select --install"
25
+ echo ""
26
+ ISSUES=$((ISSUES + 1))
27
+ fi
28
+
29
+ # Check 2: Node.js
30
+ echo "Checking Node.js..."
31
+ if command -v node &>/dev/null; then
32
+ NODE_VERSION=$(node --version)
33
+ NODE_MAJOR=$(echo "$NODE_VERSION" | cut -d. -f1 | tr -d 'v')
34
+ if [ "$NODE_MAJOR" -ge 24 ]; then
35
+ echo " [WARN] Node.js $NODE_VERSION - v24+ may have native module issues"
36
+ echo ""
37
+ echo " Native modules (tree-sitter, lancedb) may not compile on Node.js v24+"
38
+ echo " due to V8 API changes. Recommended: Use Node.js v20.x or v22.x (LTS)"
39
+ echo ""
40
+ else
41
+ echo " [OK] Node.js $NODE_VERSION"
42
+ fi
43
+ else
44
+ echo " [FAIL] Node.js NOT found"
45
+ ISSUES=$((ISSUES + 1))
46
+ fi
47
+
48
+ # Check 3: node_modules
49
+ echo "Checking plugin dependencies..."
50
+ if [ -d "$PLUGIN_ROOT/node_modules" ]; then
51
+ echo " [OK] node_modules installed"
52
+ else
53
+ echo " [FAIL] node_modules missing"
54
+ echo ""
55
+ echo " FIX: Run setup manually:"
56
+ echo " $PLUGIN_ROOT/scripts/setup.sh"
57
+ echo ""
58
+ ISSUES=$((ISSUES + 1))
59
+ fi
60
+
61
+ # Check 4: MCP wrapper
62
+ WRAPPER_PATH="$HOME/.local/bin/bluera-knowledge-mcp"
63
+ echo "Checking MCP wrapper..."
64
+ if [ -f "$WRAPPER_PATH" ]; then
65
+ echo " [OK] MCP wrapper installed at $WRAPPER_PATH"
66
+ else
67
+ echo " [FAIL] MCP wrapper NOT installed"
68
+ echo ""
69
+ echo " FIX: Run setup manually:"
70
+ echo " $PLUGIN_ROOT/scripts/setup.sh"
71
+ echo ""
72
+ ISSUES=$((ISSUES + 1))
73
+ fi
74
+
75
+ # Check 5: Python 3
76
+ echo "Checking Python 3..."
77
+ if command -v python3 &>/dev/null; then
78
+ PY_VERSION=$(python3 --version 2>&1)
79
+ echo " [OK] $PY_VERSION"
80
+ else
81
+ echo " [WARN] Python 3 not found (optional, needed for embeddings)"
82
+ fi
83
+
84
+ # Check 6: Playwright
85
+ PLAYWRIGHT_PATH="${PLAYWRIGHT_BROWSERS_PATH:-$HOME/.cache/ms-playwright}"
86
+ echo "Checking Playwright browser..."
87
+ if ls "$PLAYWRIGHT_PATH"/chromium-* 1>/dev/null 2>&1; then
88
+ echo " [OK] Playwright Chromium installed"
89
+ else
90
+ echo " [WARN] Playwright Chromium not found (optional, needed for web crawling)"
91
+ echo " FIX: npx playwright install chromium"
92
+ fi
93
+
94
+ echo ""
95
+ echo "═══════════════════════════════════════════════════════════"
96
+ if [ $ISSUES -eq 0 ]; then
97
+ echo " All required checks passed!"
98
+ echo ""
99
+ echo " If MCP is still failing, restart Claude Code."
100
+ else
101
+ echo " Found $ISSUES issue(s) - see FIX instructions above"
102
+ fi
103
+ echo "═══════════════════════════════════════════════════════════"
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Download Embedding Models
4
+ *
5
+ * Pre-downloads all registered embedding models to ~/.cache/huggingface-transformers/
6
+ * This ensures models are available offline and avoids download time during benchmarks.
7
+ *
8
+ * Usage:
9
+ * bun run models:download # Download all models
10
+ * bun run models:download --small # Download only small models
11
+ * bun run models:download --list # List available models
12
+ */
13
+
14
+ import { pipeline, env } from '@huggingface/transformers';
15
+ import { homedir } from 'node:os';
16
+ import { join } from 'node:path';
17
+ import { MODEL_REGISTRY, RERANKER_REGISTRY, listModels } from '../src/models/registry.js';
18
+ import type { ModelConfig } from '../src/models/registry.js';
19
+
20
+ // Set cache directory
21
+ env.cacheDir = join(homedir(), '.cache', 'huggingface-transformers');
22
+
23
+ interface DownloadResult {
24
+ model: string;
25
+ success: boolean;
26
+ error?: string;
27
+ timeMs: number;
28
+ }
29
+
30
+ async function downloadModel(config: ModelConfig): Promise<DownloadResult> {
31
+ const startTime = Date.now();
32
+ try {
33
+ console.log(` ↓ Downloading ${config.name} (${config.id})...`);
34
+
35
+ // Load the model to trigger download
36
+ const pipe = await pipeline('feature-extraction', config.id, {
37
+ dtype: 'fp32',
38
+ });
39
+
40
+ // Dispose to free memory
41
+ await pipe.dispose();
42
+
43
+ const timeMs = Date.now() - startTime;
44
+ console.log(` ✓ ${config.name} downloaded (${(timeMs / 1000).toFixed(1)}s)`);
45
+
46
+ return { model: config.id, success: true, timeMs };
47
+ } catch (error) {
48
+ const timeMs = Date.now() - startTime;
49
+ const message = error instanceof Error ? error.message : String(error);
50
+ console.log(` ✗ ${config.name} failed: ${message}`);
51
+
52
+ return { model: config.id, success: false, error: message, timeMs };
53
+ }
54
+ }
55
+
56
+ async function downloadReranker(id: string, name: string): Promise<DownloadResult> {
57
+ const startTime = Date.now();
58
+ try {
59
+ console.log(` ↓ Downloading reranker ${name} (${id})...`);
60
+
61
+ // Rerankers use text-classification pipeline
62
+ const { AutoModelForSequenceClassification, AutoTokenizer } = await import(
63
+ '@huggingface/transformers'
64
+ );
65
+
66
+ const [model, tokenizer] = await Promise.all([
67
+ AutoModelForSequenceClassification.from_pretrained(id, { dtype: 'fp32' }),
68
+ AutoTokenizer.from_pretrained(id),
69
+ ]);
70
+
71
+ // Dispose to free memory
72
+ await model.dispose();
73
+ // Tokenizer doesn't have dispose
74
+
75
+ const timeMs = Date.now() - startTime;
76
+ console.log(` ✓ ${name} downloaded (${(timeMs / 1000).toFixed(1)}s)`);
77
+
78
+ return { model: id, success: true, timeMs };
79
+ } catch (error) {
80
+ const timeMs = Date.now() - startTime;
81
+ const message = error instanceof Error ? error.message : String(error);
82
+ console.log(` ✗ ${name} failed: ${message}`);
83
+
84
+ return { model: id, success: false, error: message, timeMs };
85
+ }
86
+ }
87
+
88
+ function printModelList(): void {
89
+ console.log('\nAvailable Embedding Models:\n');
90
+
91
+ const categories = ['bge', 'e5', 'minilm', 'gte', 'nomic', 'other'] as const;
92
+
93
+ for (const category of categories) {
94
+ const models = listModels({ category });
95
+ if (models.length === 0) continue;
96
+
97
+ console.log(` ${category.toUpperCase()} Models:`);
98
+ for (const model of models) {
99
+ const size = model.sizeCategory.padEnd(5);
100
+ const dims = String(model.dimensions).padStart(4);
101
+ console.log(` [${size}] ${dims}d ${model.name}`);
102
+ if (model.notes) {
103
+ console.log(` ${model.notes}`);
104
+ }
105
+ }
106
+ console.log('');
107
+ }
108
+
109
+ console.log(' Reranker Models:');
110
+ for (const [key, config] of Object.entries(RERANKER_REGISTRY)) {
111
+ console.log(` ${config.name} (${key})`);
112
+ if (config.notes) {
113
+ console.log(` ${config.notes}`);
114
+ }
115
+ }
116
+ console.log('');
117
+ }
118
+
119
+ async function main(): Promise<void> {
120
+ const args = process.argv.slice(2);
121
+
122
+ if (args.includes('--list') || args.includes('-l')) {
123
+ printModelList();
124
+ return;
125
+ }
126
+
127
+ const smallOnly = args.includes('--small') || args.includes('-s');
128
+ const skipRerankers = args.includes('--no-rerankers');
129
+ const specificModel = args.find((a) => !a.startsWith('-'));
130
+
131
+ console.log('\n📦 Model Download Script\n');
132
+ console.log(`Cache directory: ${env.cacheDir}\n`);
133
+
134
+ const results: DownloadResult[] = [];
135
+
136
+ // Download embedding models
137
+ let models: ModelConfig[];
138
+ if (specificModel !== undefined) {
139
+ const config = MODEL_REGISTRY[specificModel];
140
+ if (config === undefined) {
141
+ console.error(`Unknown model: ${specificModel}`);
142
+ console.error(`Run with --list to see available models`);
143
+ process.exit(1);
144
+ }
145
+ models = [config];
146
+ } else if (smallOnly) {
147
+ models = listModels({ sizeCategory: 'small' });
148
+ console.log(`Downloading ${models.length} small embedding models...\n`);
149
+ } else {
150
+ models = Object.values(MODEL_REGISTRY);
151
+ console.log(`Downloading ${models.length} embedding models...\n`);
152
+ }
153
+
154
+ for (const config of models) {
155
+ const result = await downloadModel(config);
156
+ results.push(result);
157
+ }
158
+
159
+ // Download rerankers
160
+ if (!skipRerankers && specificModel === undefined) {
161
+ console.log('\nDownloading reranker models...\n');
162
+ for (const [, config] of Object.entries(RERANKER_REGISTRY)) {
163
+ const result = await downloadReranker(config.id, config.name);
164
+ results.push(result);
165
+ }
166
+ }
167
+
168
+ // Summary
169
+ console.log('\n' + '─'.repeat(50));
170
+ const succeeded = results.filter((r) => r.success).length;
171
+ const failed = results.filter((r) => !r.success).length;
172
+ const totalTime = results.reduce((sum, r) => sum + r.timeMs, 0);
173
+
174
+ console.log(`\n✓ ${succeeded} models downloaded`);
175
+ if (failed > 0) {
176
+ console.log(`✗ ${failed} models failed`);
177
+ }
178
+ console.log(`Total time: ${(totalTime / 1000).toFixed(1)}s\n`);
179
+
180
+ if (failed > 0) {
181
+ process.exit(1);
182
+ }
183
+ }
184
+
185
+ main().catch((error) => {
186
+ console.error('Fatal error:', error);
187
+ process.exit(1);
188
+ });
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Export web store documents to markdown fixtures
4
+ *
5
+ * Usage: bun run scripts/export-web-store.ts <store-name> <output-dir>
6
+ */
7
+
8
+ import * as fs from 'node:fs';
9
+ import * as path from 'node:path';
10
+
11
+ interface StoreEntry {
12
+ type: string;
13
+ id: string;
14
+ name: string;
15
+ url?: string;
16
+ status: string;
17
+ }
18
+
19
+ interface StoresData {
20
+ stores: StoreEntry[];
21
+ }
22
+
23
+ interface DocumentRow {
24
+ id: string;
25
+ content: string;
26
+ metadata: string;
27
+ }
28
+
29
+ function isRecord(value: unknown): value is Record<string, unknown> {
30
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
31
+ }
32
+
33
+ function isStoresData(value: unknown): value is StoresData {
34
+ if (!isRecord(value)) return false;
35
+ if (!Array.isArray(value['stores'])) return false;
36
+ return true;
37
+ }
38
+
39
+ function isDocumentRow(value: unknown): value is DocumentRow {
40
+ if (!isRecord(value)) return false;
41
+ return (
42
+ typeof value['id'] === 'string' &&
43
+ typeof value['content'] === 'string' &&
44
+ typeof value['metadata'] === 'string'
45
+ );
46
+ }
47
+
48
+ async function main(): Promise<void> {
49
+ const [storeName, outputDir] = process.argv.slice(2);
50
+
51
+ if (!storeName || !outputDir) {
52
+ console.error('Usage: bun run scripts/export-web-store.ts <store-name> <output-dir>');
53
+ process.exit(1);
54
+ }
55
+
56
+ // Find store in stores.json
57
+ const storesPath = path.join(process.cwd(), '.bluera/bluera-knowledge/data/stores.json');
58
+ if (!fs.existsSync(storesPath)) {
59
+ console.error('stores.json not found');
60
+ process.exit(1);
61
+ }
62
+
63
+ const storesJson: unknown = JSON.parse(fs.readFileSync(storesPath, 'utf-8'));
64
+ if (!isStoresData(storesJson)) {
65
+ console.error('Invalid stores.json format');
66
+ process.exit(1);
67
+ }
68
+
69
+ const store = storesJson.stores.find((s) => s.name === storeName);
70
+ if (!store) {
71
+ console.error(`Store not found: ${storeName}`);
72
+ console.error('Available stores:', storesJson.stores.map((s) => s.name).join(', '));
73
+ process.exit(1);
74
+ }
75
+
76
+ if (store.type !== 'web') {
77
+ console.error(`Store ${storeName} is not a web store (type: ${store.type})`);
78
+ process.exit(1);
79
+ }
80
+
81
+ // Load documents from LanceDB
82
+ const lanceDbPath = path.join(process.cwd(), `.bluera/bluera-knowledge/data/documents_${store.id}.lance`);
83
+ if (!fs.existsSync(lanceDbPath)) {
84
+ console.error(`LanceDB not found: ${lanceDbPath}`);
85
+ process.exit(1);
86
+ }
87
+
88
+ // Dynamic import for lancedb
89
+ const lancedb = await import('@lancedb/lancedb');
90
+ const db = await lancedb.connect(path.join(process.cwd(), '.bluera/bluera-knowledge/data'));
91
+ const table = await db.openTable(`documents_${store.id}`);
92
+
93
+ // Query all documents
94
+ const results = await table.query().select(['id', 'content', 'metadata']).toArray();
95
+
96
+ // Group chunks by source URL
97
+ const pageChunks = new Map<string, { content: string; title: string; chunks: string[] }>();
98
+
99
+ for (const row of results) {
100
+ if (!isDocumentRow(row)) continue;
101
+ const metadata: unknown = JSON.parse(row.metadata);
102
+ if (!isRecord(metadata)) continue;
103
+
104
+ const url = typeof metadata['url'] === 'string' ? metadata['url'] : 'unknown';
105
+ const title = typeof metadata['title'] === 'string' ? metadata['title'] : url;
106
+
107
+ if (!pageChunks.has(url)) {
108
+ pageChunks.set(url, { content: '', title, chunks: [] });
109
+ }
110
+ const page = pageChunks.get(url);
111
+ if (page) {
112
+ page.chunks.push(row.content);
113
+ }
114
+ }
115
+
116
+ // Create output directory
117
+ const fullOutputDir = path.resolve(outputDir);
118
+ if (!fs.existsSync(fullOutputDir)) {
119
+ fs.mkdirSync(fullOutputDir, { recursive: true });
120
+ }
121
+
122
+ // Write each page as a markdown file
123
+ let fileCount = 0;
124
+ for (const [url, page] of pageChunks) {
125
+ // Generate filename from URL
126
+ const urlPath = new URL(url).pathname.replace(/^\//, '').replace(/\/$/, '') || 'index';
127
+ const filename = urlPath.replace(/\//g, '-').replace(/[^a-zA-Z0-9-]/g, '') + '.md';
128
+
129
+ // Combine chunks (they may overlap, just concatenate for now)
130
+ const content = `# ${page.title}\n\nSource: ${url}\n\n---\n\n${page.chunks.join('\n\n---\n\n')}`;
131
+
132
+ fs.writeFileSync(path.join(fullOutputDir, filename), content);
133
+ fileCount++;
134
+ }
135
+
136
+ console.log(`Exported ${fileCount} pages to ${fullOutputDir}`);
137
+ }
138
+
139
+ main().catch((err: unknown) => {
140
+ console.error(err instanceof Error ? err.message : String(err));
141
+ process.exit(1);
142
+ });
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # mock-server.sh
4
+ #
5
+ # Start a simple HTTP server for testing crawl functionality.
6
+ # Serves static files from tests/fixtures/mock-server/
7
+ #
8
+ # Usage:
9
+ # # Start server in background, get PID:
10
+ # MOCK_PID=$(bash scripts/lib/mock-server.sh start)
11
+ #
12
+ # # Stop server:
13
+ # bash scripts/lib/mock-server.sh stop $MOCK_PID
14
+ #
15
+ # # Wait for server to be ready:
16
+ # bash scripts/lib/mock-server.sh wait
17
+ #
18
+
19
+ set -euo pipefail
20
+
21
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
22
+ REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
23
+ MOCK_SERVER_DIR="$REPO_ROOT/tests/fixtures/mock-server"
24
+ PORT="${MOCK_SERVER_PORT:-8765}"
25
+
26
+ start_server() {
27
+ # Start Python HTTP server in background
28
+ cd "$MOCK_SERVER_DIR"
29
+ python3 -m http.server "$PORT" --bind 127.0.0.1 >/dev/null 2>&1 &
30
+ local pid=$!
31
+ echo "$pid"
32
+ }
33
+
34
+ stop_server() {
35
+ local pid="$1"
36
+ if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
37
+ kill "$pid" 2>/dev/null || true
38
+ wait "$pid" 2>/dev/null || true
39
+ fi
40
+ }
41
+
42
+ wait_for_server() {
43
+ local max_attempts=30
44
+ local attempt=0
45
+ while [ $attempt -lt $max_attempts ]; do
46
+ if curl -s "http://127.0.0.1:$PORT/" >/dev/null 2>&1; then
47
+ return 0
48
+ fi
49
+ attempt=$((attempt + 1))
50
+ sleep 0.1
51
+ done
52
+ echo "Error: Mock server failed to start on port $PORT" >&2
53
+ return 1
54
+ }
55
+
56
+ case "${1:-start}" in
57
+ start)
58
+ start_server
59
+ ;;
60
+ stop)
61
+ stop_server "${2:-}"
62
+ ;;
63
+ wait)
64
+ wait_for_server
65
+ ;;
66
+ *)
67
+ echo "Usage: $0 {start|stop|wait}" >&2
68
+ exit 1
69
+ ;;
70
+ esac
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env bash
2
+ # =====================================================================
3
+ # bluera-knowledge MCP Wrapper Script
4
+ # =====================================================================
5
+ #
6
+ # WORKAROUND: ${CLAUDE_PLUGIN_ROOT} not expanding in plugin .mcp.json
7
+ #
8
+ # Claude Code's ${CLAUDE_PLUGIN_ROOT} environment variable is documented
9
+ # to work in plugin .mcp.json files, but doesn't expand correctly in
10
+ # some environments. This wrapper script locates the plugin installation
11
+ # directory dynamically and runs bootstrap.js.
12
+ #
13
+ # References:
14
+ # - Bug: https://github.com/anthropics/claude-code/issues/9427
15
+ # "env variable expansion not working in plugin .mcp.json"
16
+ # (Closed Jan 2026, but still occurs on some systems)
17
+ #
18
+ # - Docs: https://code.claude.com/docs/en/mcp
19
+ # "Plugin MCP features: Use ${CLAUDE_PLUGIN_ROOT} for plugin-relative paths"
20
+ #
21
+ # This script reads ~/.claude/plugins/installed_plugins.json to find the
22
+ # correct plugin path, with fallbacks to cache directory scanning.
23
+ # =====================================================================
24
+
25
+ set -e
26
+
27
+ # Find plugin root from installed_plugins.json
28
+ # Claude Code stores plugin metadata here when plugins are installed
29
+ INSTALLED_PLUGINS="$HOME/.claude/plugins/installed_plugins.json"
30
+
31
+ if [ -f "$INSTALLED_PLUGINS" ]; then
32
+ # Use jq if available (more reliable JSON parsing)
33
+ if command -v jq &> /dev/null; then
34
+ PLUGIN_ROOT=$(jq -r '.["bluera-knowledge"].path // empty' "$INSTALLED_PLUGINS" 2>/dev/null)
35
+ else
36
+ # Fallback: extract path with grep/sed (less reliable but works without jq)
37
+ # This handles the JSON structure: {"bluera-knowledge": {"path": "/path/to/plugin"}}
38
+ PLUGIN_ROOT=$(grep -o '"bluera-knowledge"[^}]*"path"[[:space:]]*:[[:space:]]*"[^"]*"' "$INSTALLED_PLUGINS" 2>/dev/null | sed 's/.*"path"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')
39
+ fi
40
+ fi
41
+
42
+ # Fallback: scan cache directories for valid plugin installations
43
+ # Plugins are cached at ~/.claude/plugins/cache/<org>/<name>/<version>/
44
+ #
45
+ # CRITICAL: Sort by version number (descending) to use the LATEST version!
46
+ # Bug fixed in v0.22.7: Alphabetical sort put 0.20.0 before 0.22.x, causing
47
+ # old bootstrap.ts (without --legacy-peer-deps) to run and fail.
48
+ # The -V flag does proper version sorting: 0.22.7 > 0.22.6 > 0.21.0 > 0.20.0
49
+ #
50
+ if [ -z "$PLUGIN_ROOT" ] || [ ! -d "$PLUGIN_ROOT" ]; then
51
+ CACHE_BASE="$HOME/.claude/plugins/cache/bluera/bluera-knowledge"
52
+ if [ -d "$CACHE_BASE" ]; then
53
+ # Sort versions numerically (descending) with -V flag and use the latest valid one
54
+ for cache_dir in $(ls -d "$CACHE_BASE"/*/ 2>/dev/null | sort -V -r); do
55
+ if [ -f "$cache_dir/dist/mcp/bootstrap.js" ]; then
56
+ PLUGIN_ROOT="$cache_dir"
57
+ break
58
+ fi
59
+ done
60
+ fi
61
+ fi
62
+
63
+ # Verify we found a valid plugin installation
64
+ if [ -z "$PLUGIN_ROOT" ] || [ ! -f "$PLUGIN_ROOT/dist/mcp/bootstrap.js" ]; then
65
+ echo "ERROR: Could not locate bluera-knowledge plugin" >&2
66
+ echo "Expected bootstrap.js at: \$PLUGIN_ROOT/dist/mcp/bootstrap.js" >&2
67
+ echo "Checked:" >&2
68
+ echo " - installed_plugins.json: $INSTALLED_PLUGINS" >&2
69
+ echo " - Cache dir: ~/.claude/plugins/cache/bluera/bluera-knowledge/" >&2
70
+ # Exit 2 = blocking error, stderr shown to user (exit 1 is non-blocking, hidden)
71
+ # See: https://code.claude.com/docs/en/hooks
72
+ exit 2
73
+ fi
74
+
75
+ # Check if build tools are available for native module compilation
76
+ # Required for native modules (LanceDB) - check BEFORE attempting to run bootstrap
77
+ if ! command -v make &>/dev/null; then
78
+ echo "ERROR: Build tools (make) not found - required for native modules." >&2
79
+ echo "" >&2
80
+ echo "Install build tools, then restart Claude Code:" >&2
81
+ echo " Debian/Ubuntu: sudo apt install build-essential" >&2
82
+ echo " Fedora/RHEL: sudo dnf groupinstall 'Development Tools'" >&2
83
+ echo " macOS: xcode-select --install" >&2
84
+ # Exit 2 = blocking error, stderr shown to user (exit 1 is non-blocking, hidden)
85
+ # See: https://code.claude.com/docs/en/hooks
86
+ exit 2
87
+ fi
88
+
89
+ # Run bootstrap.js with all environment variables passed through
90
+ # The bootstrap script handles dependency installation and MCP server startup
91
+ exec node "$PLUGIN_ROOT/dist/mcp/bootstrap.js" "$@"