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.
- package/.claude-plugin/plugin.json +23 -0
- package/.mcp.json +13 -0
- package/CHANGELOG.md +42 -0
- package/NOTICE +47 -0
- package/README.md +2 -2
- package/bun.lock +1978 -0
- package/dist/{chunk-B335UOU7.js → chunk-3TB7TDVF.js} +24 -3
- package/dist/chunk-3TB7TDVF.js.map +1 -0
- package/dist/{chunk-KCI4U6FH.js → chunk-KDZDLJUY.js} +2 -2
- package/dist/{chunk-AEXFPA57.js → chunk-YDTTD53Y.js} +158 -26
- package/dist/chunk-YDTTD53Y.js.map +1 -0
- package/dist/index.js +3 -3
- package/dist/mcp/bootstrap.js +10 -0
- package/dist/mcp/bootstrap.js.map +1 -1
- package/dist/mcp/server.d.ts +5 -3
- package/dist/mcp/server.js +2 -2
- package/dist/workers/background-worker-cli.js +2 -2
- package/hooks/check-ready.sh +109 -0
- package/hooks/hooks.json +97 -0
- package/hooks/job-status-hook.sh +51 -0
- package/hooks/posttooluse-bk-reminder.py +126 -0
- package/hooks/posttooluse-web-research.py +209 -0
- package/hooks/posttooluse-websearch-bk.py +158 -0
- package/hooks/pretooluse-bk-suggest.py +296 -0
- package/hooks/skill-activation.py +221 -0
- package/hooks/skill-rules.json +131 -0
- package/package.json +9 -2
- package/scripts/CLAUDE.md +65 -0
- package/scripts/auto-setup.sh +65 -0
- package/scripts/bench-regression.sh +345 -0
- package/scripts/dev.sh +16 -0
- package/scripts/doctor.sh +103 -0
- package/scripts/download-models.ts +188 -0
- package/scripts/export-web-store.ts +142 -0
- package/scripts/lib/mock-server.sh +70 -0
- package/scripts/mcp-wrapper.sh +91 -0
- package/scripts/setup.sh +224 -0
- package/scripts/statusline-module.sh +29 -0
- package/scripts/test-mcp-dev.js +260 -0
- package/scripts/validate-local.sh +412 -0
- package/scripts/validate-npm-release.sh +406 -0
- package/skills/add-folder/SKILL.md +48 -0
- package/skills/add-repo/SKILL.md +50 -0
- package/skills/advanced-workflows/SKILL.md +273 -0
- package/skills/cancel/SKILL.md +63 -0
- package/skills/check-status/SKILL.md +130 -0
- package/skills/crawl/SKILL.md +61 -0
- package/skills/doctor/SKILL.md +27 -0
- package/skills/eval/SKILL.md +222 -0
- package/skills/health/SKILL.md +72 -0
- package/skills/index/SKILL.md +48 -0
- package/skills/knowledge-search/SKILL.md +110 -0
- package/skills/remove-store/SKILL.md +52 -0
- package/skills/search/SKILL.md +80 -0
- package/skills/search/search.sh +63 -0
- package/skills/search-optimization/SKILL.md +199 -0
- package/skills/search-optimization/references/mistakes.md +21 -0
- package/skills/search-optimization/references/strategies.md +80 -0
- package/skills/skill-activation/SKILL.md +131 -0
- package/skills/statusline/SKILL.md +19 -0
- package/skills/store-lifecycle/SKILL.md +470 -0
- package/skills/stores/SKILL.md +54 -0
- package/skills/suggest/SKILL.md +118 -0
- package/skills/sync/SKILL.md +96 -0
- package/skills/test-plugin/SKILL.md +547 -0
- package/skills/uninstall/SKILL.md +65 -0
- package/skills/when-to-query/SKILL.md +160 -0
- package/dist/chunk-AEXFPA57.js.map +0 -1
- package/dist/chunk-B335UOU7.js.map +0 -1
- /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" "$@"
|