bluera-knowledge 0.13.0 → 0.13.2

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 (34) hide show
  1. package/.claude/rules/code-quality.md +12 -0
  2. package/.claude/rules/git.md +5 -0
  3. package/.claude/rules/versioning.md +7 -0
  4. package/.claude-plugin/plugin.json +2 -15
  5. package/.mcp.json +11 -0
  6. package/CHANGELOG.md +7 -0
  7. package/CLAUDE.md +5 -13
  8. package/CONTRIBUTING.md +307 -0
  9. package/README.md +58 -1167
  10. package/commands/crawl.md +2 -1
  11. package/commands/test-plugin.md +197 -72
  12. package/docs/claude-code-best-practices.md +458 -0
  13. package/docs/cli.md +170 -0
  14. package/docs/commands.md +392 -0
  15. package/docs/crawler-architecture.md +89 -0
  16. package/docs/mcp-integration.md +130 -0
  17. package/docs/token-efficiency.md +91 -0
  18. package/eslint.config.js +1 -1
  19. package/hooks/check-dependencies.sh +18 -1
  20. package/hooks/hooks.json +2 -2
  21. package/hooks/posttooluse-bk-reminder.py +30 -2
  22. package/package.json +1 -1
  23. package/scripts/test-mcp-dev.js +260 -0
  24. package/src/mcp/plugin-mcp-config.test.ts +26 -19
  25. package/tests/integration/cli-consistency.test.ts +3 -2
  26. package/docs/plans/2024-12-17-ai-search-quality-implementation.md +0 -752
  27. package/docs/plans/2024-12-17-ai-search-quality-testing-design.md +0 -201
  28. package/docs/plans/2025-12-16-bluera-knowledge-cli.md +0 -2951
  29. package/docs/plans/2025-12-16-phase2-features.md +0 -1518
  30. package/docs/plans/2025-12-17-hil-implementation.md +0 -926
  31. package/docs/plans/2025-12-17-hil-quality-testing.md +0 -224
  32. package/docs/plans/2025-12-17-search-quality-phase1-implementation.md +0 -1416
  33. package/docs/plans/2025-12-17-search-quality-testing-v2-design.md +0 -212
  34. package/docs/plans/2025-12-28-ai-agent-optimization.md +0 -1630
package/eslint.config.js CHANGED
@@ -83,7 +83,7 @@ export default tseslint.config(
83
83
  },
84
84
  },
85
85
  {
86
- ignores: ['dist/**', 'node_modules/**', '*.config.js', '*.config.ts', '**/*.test.ts', 'tests/**/*.ts'],
86
+ ignores: ['dist/**', 'node_modules/**', 'scripts/**', '*.config.js', '*.config.ts', '**/*.test.ts', 'tests/**/*.ts'],
87
87
  },
88
88
  // Test files: Apply custom skip-comment rule only
89
89
  {
@@ -1,6 +1,15 @@
1
1
  #!/bin/bash
2
2
  # Bluera Knowledge Plugin - Dependency Checker
3
3
  # Automatically checks and installs dependencies for the plugin
4
+ #
5
+ # Environment variables:
6
+ # BK_SKIP_AUTO_INSTALL=1 - Skip automatic pip installation of crawl4ai
7
+ # Set this if you prefer to manage Python packages manually
8
+ #
9
+ # What this script auto-installs (if missing):
10
+ # - Node.js dependencies (via bun or npm, from package.json)
11
+ # - crawl4ai Python package (via pip, for web crawling)
12
+ # - Playwright Chromium browser (via playwright CLI, for headless crawling)
4
13
 
5
14
  set -e
6
15
 
@@ -95,9 +104,17 @@ echo -e "${YELLOW}To enable web crawling, install crawl4ai:${NC}"
95
104
  echo -e " ${GREEN}pip install crawl4ai${NC}"
96
105
  echo ""
97
106
 
107
+ # Check if auto-install is disabled
108
+ if [ "${BK_SKIP_AUTO_INSTALL:-}" = "1" ]; then
109
+ echo -e "${YELLOW}[bluera-knowledge] Auto-install disabled (BK_SKIP_AUTO_INSTALL=1)${NC}"
110
+ echo -e "${YELLOW}Install manually: pip install crawl4ai && python3 -m playwright install chromium${NC}"
111
+ exit 0
112
+ fi
113
+
98
114
  # Check if we should auto-install
99
115
  if command -v pip3 &> /dev/null || command -v pip &> /dev/null; then
100
- echo -e "${YELLOW}Attempting automatic installation via pip...${NC}"
116
+ echo -e "${YELLOW}[bluera-knowledge] Auto-installing crawl4ai via pip...${NC}"
117
+ echo -e "${YELLOW}(Set BK_SKIP_AUTO_INSTALL=1 to disable auto-install)${NC}"
101
118
 
102
119
  # Try to install using pip3 or pip
103
120
  if command -v pip3 &> /dev/null; then
package/hooks/hooks.json CHANGED
@@ -40,12 +40,12 @@
40
40
  {
41
41
  "type": "command",
42
42
  "command": "${CLAUDE_PLUGIN_ROOT}/hooks/job-status-hook.sh",
43
- "timeout": 5
43
+ "timeout": 2
44
44
  },
45
45
  {
46
46
  "type": "command",
47
47
  "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/skill-activation.py",
48
- "timeout": 5
48
+ "timeout": 2
49
49
  }
50
50
  ]
51
51
  }
@@ -10,6 +10,20 @@ import re
10
10
  import sys
11
11
  from typing import Any
12
12
 
13
+ # Fast string checks - if none of these are in the path, skip regex entirely
14
+ # This avoids regex overhead for the vast majority of file accesses
15
+ LIBRARY_QUICK_CHECKS = frozenset([
16
+ "node_modules",
17
+ "vendor",
18
+ "site-packages",
19
+ "venv",
20
+ "bower_components",
21
+ "packages",
22
+ ".npm",
23
+ ".cargo",
24
+ "go/pkg",
25
+ ])
26
+
13
27
  # Patterns indicating library/dependency code
14
28
  LIBRARY_PATH_PATTERNS = [
15
29
  r"node_modules/",
@@ -28,6 +42,12 @@ LIBRARY_PATH_PATTERNS = [
28
42
  LIBRARY_PATTERNS_RE = re.compile("|".join(LIBRARY_PATH_PATTERNS), re.IGNORECASE)
29
43
 
30
44
 
45
+ def quick_path_check(path: str) -> bool:
46
+ """Fast check if path might contain library code. Avoids regex for most paths."""
47
+ path_lower = path.lower()
48
+ return any(keyword in path_lower for keyword in LIBRARY_QUICK_CHECKS)
49
+
50
+
31
51
  def extract_library_name(path: str) -> str | None:
32
52
  """Extract library name from dependency path."""
33
53
  # node_modules/package-name/...
@@ -62,7 +82,11 @@ def check_grep_tool(tool_input: dict[str, Any]) -> tuple[str | None, str | None]
62
82
  """Check if Grep targeted library code. Returns (action, library_name)."""
63
83
  path = tool_input.get("path", "")
64
84
 
65
- if path and LIBRARY_PATTERNS_RE.search(path):
85
+ # Fast path: skip regex if no library keywords present
86
+ if not path or not quick_path_check(path):
87
+ return None, None
88
+
89
+ if LIBRARY_PATTERNS_RE.search(path):
66
90
  lib_name = extract_library_name(path)
67
91
  return f"grepped in `{path}`", lib_name
68
92
 
@@ -73,7 +97,11 @@ def check_read_tool(tool_input: dict[str, Any]) -> tuple[str | None, str | None]
73
97
  """Check if Read targeted library code. Returns (action, library_name)."""
74
98
  file_path = tool_input.get("file_path", "")
75
99
 
76
- if file_path and LIBRARY_PATTERNS_RE.search(file_path):
100
+ # Fast path: skip regex if no library keywords present
101
+ if not file_path or not quick_path_check(file_path):
102
+ return None, None
103
+
104
+ if LIBRARY_PATTERNS_RE.search(file_path):
77
105
  lib_name = extract_library_name(file_path)
78
106
  return f"read `{file_path}`", lib_name
79
107
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bluera-knowledge",
3
- "version": "0.13.0",
3
+ "version": "0.13.2",
4
4
  "description": "CLI tool for managing knowledge stores with semantic search",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,260 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * MCP Development Test Helper
4
+ *
5
+ * Spawns the MCP server and communicates with it directly via JSON-RPC over stdio.
6
+ * Used by test-plugin --dev to test MCP functionality without needing the plugin installed.
7
+ *
8
+ * Usage:
9
+ * ./scripts/test-mcp-dev.mjs call <tool-name> '<json-args>'
10
+ * ./scripts/test-mcp-dev.mjs call execute '{"command":"help"}'
11
+ * ./scripts/test-mcp-dev.mjs call search '{"query":"test"}'
12
+ */
13
+
14
+ import { spawn } from 'node:child_process';
15
+ import { createInterface } from 'node:readline';
16
+ import { dirname, join } from 'node:path';
17
+ import { fileURLToPath } from 'node:url';
18
+
19
+ const __dirname = dirname(fileURLToPath(import.meta.url));
20
+ const PROJECT_ROOT = join(__dirname, '..');
21
+
22
+ class MCPTestClient {
23
+ constructor() {
24
+ this.server = null;
25
+ this.messageId = 0;
26
+ this.pendingRequests = new Map();
27
+ this.initialized = false;
28
+ }
29
+
30
+ async start() {
31
+ return new Promise((resolve, reject) => {
32
+ const serverPath = join(PROJECT_ROOT, 'dist', 'mcp', 'server.js');
33
+
34
+ this.server = spawn('node', [serverPath], {
35
+ cwd: PROJECT_ROOT,
36
+ env: {
37
+ ...process.env,
38
+ PROJECT_ROOT: PROJECT_ROOT,
39
+ DATA_DIR: '.bluera/bluera-knowledge/data',
40
+ CONFIG_PATH: '.bluera/bluera-knowledge/config.json',
41
+ },
42
+ stdio: ['pipe', 'pipe', 'pipe'],
43
+ });
44
+
45
+ // Handle stderr (logs)
46
+ this.server.stderr.on('data', (data) => {
47
+ // Suppress server logs in test output unless DEBUG
48
+ if (process.env.DEBUG) {
49
+ process.stderr.write(`[server] ${data}`);
50
+ }
51
+ });
52
+
53
+ // Handle stdout (JSON-RPC responses)
54
+ const rl = createInterface({ input: this.server.stdout });
55
+ rl.on('line', (line) => {
56
+ try {
57
+ const msg = JSON.parse(line);
58
+ if (msg.id !== undefined && this.pendingRequests.has(msg.id)) {
59
+ const { resolve, reject } = this.pendingRequests.get(msg.id);
60
+ this.pendingRequests.delete(msg.id);
61
+ if (msg.error) {
62
+ reject(new Error(msg.error.message || JSON.stringify(msg.error)));
63
+ } else {
64
+ resolve(msg.result);
65
+ }
66
+ }
67
+ } catch {
68
+ // Not JSON or parse error - ignore
69
+ }
70
+ });
71
+
72
+ this.server.on('error', reject);
73
+ this.server.on('spawn', () => {
74
+ // Give server a moment to initialize
75
+ setTimeout(() => resolve(), 100);
76
+ });
77
+ });
78
+ }
79
+
80
+ async initialize() {
81
+ if (this.initialized) return;
82
+
83
+ // Send initialize request
84
+ const initResult = await this.sendRequest('initialize', {
85
+ protocolVersion: '2024-11-05',
86
+ capabilities: {},
87
+ clientInfo: { name: 'test-mcp-dev', version: '1.0.0' },
88
+ });
89
+
90
+ // Send initialized notification
91
+ this.sendNotification('notifications/initialized', {});
92
+
93
+ this.initialized = true;
94
+ return initResult;
95
+ }
96
+
97
+ sendRequest(method, params) {
98
+ return new Promise((resolve, reject) => {
99
+ const id = ++this.messageId;
100
+ const request = { jsonrpc: '2.0', id, method, params };
101
+
102
+ this.pendingRequests.set(id, { resolve, reject });
103
+
104
+ // Set timeout for request
105
+ setTimeout(() => {
106
+ if (this.pendingRequests.has(id)) {
107
+ this.pendingRequests.delete(id);
108
+ reject(new Error(`Request timeout: ${method}`));
109
+ }
110
+ }, 30000);
111
+
112
+ this.server.stdin.write(JSON.stringify(request) + '\n');
113
+ });
114
+ }
115
+
116
+ sendNotification(method, params) {
117
+ const notification = { jsonrpc: '2.0', method, params };
118
+ this.server.stdin.write(JSON.stringify(notification) + '\n');
119
+ }
120
+
121
+ async callTool(name, args) {
122
+ if (!this.initialized) {
123
+ await this.initialize();
124
+ }
125
+
126
+ const result = await this.sendRequest('tools/call', {
127
+ name,
128
+ arguments: args,
129
+ });
130
+
131
+ return result;
132
+ }
133
+
134
+ async listTools() {
135
+ if (!this.initialized) {
136
+ await this.initialize();
137
+ }
138
+
139
+ return this.sendRequest('tools/list', {});
140
+ }
141
+
142
+ stop() {
143
+ if (this.server) {
144
+ this.server.kill('SIGTERM');
145
+ this.server = null;
146
+ }
147
+ }
148
+ }
149
+
150
+ // Session mode - reads multiple commands from stdin, maintains server across calls
151
+ async function sessionMode() {
152
+ const client = new MCPTestClient();
153
+ await client.start();
154
+
155
+ const rl = createInterface({ input: process.stdin });
156
+ const results = [];
157
+ let lastResultId = null;
158
+
159
+ for await (const line of rl) {
160
+ const trimmed = line.trim();
161
+ if (!trimmed || trimmed.startsWith('#')) continue;
162
+
163
+ const spaceIndex = trimmed.indexOf(' ');
164
+ const toolName = spaceIndex > 0 ? trimmed.slice(0, spaceIndex) : trimmed;
165
+ let argsStr = spaceIndex > 0 ? trimmed.slice(spaceIndex + 1) : '{}';
166
+
167
+ // Substitute $LAST_ID with actual result ID from previous search
168
+ if (lastResultId && argsStr.includes('$LAST_ID')) {
169
+ argsStr = argsStr.replace(/\$LAST_ID/g, lastResultId);
170
+ }
171
+
172
+ const args = JSON.parse(argsStr);
173
+ const result = await client.callTool(toolName, args);
174
+ results.push(result);
175
+
176
+ // Extract result ID for next call (search results have results[0].id)
177
+ // Search results have a header line before JSON, so find first '{'
178
+ if (result?.content?.[0]?.text) {
179
+ const text = result.content[0].text;
180
+ const jsonStart = text.indexOf('{');
181
+ if (jsonStart >= 0) {
182
+ try {
183
+ const parsed = JSON.parse(text.slice(jsonStart));
184
+ if (parsed.results?.[0]?.id) {
185
+ lastResultId = parsed.results[0].id;
186
+ }
187
+ } catch {
188
+ // Not valid JSON or no results - ignore
189
+ }
190
+ }
191
+ }
192
+ }
193
+
194
+ client.stop();
195
+ console.log(JSON.stringify(results, null, 2));
196
+ }
197
+
198
+ // CLI interface
199
+ async function main() {
200
+ const args = process.argv.slice(2);
201
+
202
+ if (args.length < 1) {
203
+ console.error('Usage: test-mcp-dev.js <command> [args...]');
204
+ console.error('Commands:');
205
+ console.error(' call <tool-name> <json-args> - Call an MCP tool (one-shot)');
206
+ console.error(' session - Read commands from stdin (persistent server)');
207
+ console.error(' list - List available tools');
208
+ console.error('');
209
+ console.error('Examples:');
210
+ console.error(' ./scripts/test-mcp-dev.js call execute \'{"command":"help"}\'');
211
+ console.error(' ./scripts/test-mcp-dev.js call search \'{"query":"test"}\'');
212
+ console.error(' ./scripts/test-mcp-dev.js list');
213
+ console.error('');
214
+ console.error('Session mode (maintains cache across calls):');
215
+ console.error(' echo -e \'search {"query":"test"}\\nget_full_context {"resultId":"$LAST_ID"}\' | ./scripts/test-mcp-dev.js session');
216
+ process.exit(1);
217
+ }
218
+
219
+ const command = args[0];
220
+
221
+ // Session mode handles its own client lifecycle
222
+ if (command === 'session') {
223
+ await sessionMode();
224
+ return;
225
+ }
226
+
227
+ const client = new MCPTestClient();
228
+
229
+ try {
230
+ await client.start();
231
+
232
+ if (command === 'call') {
233
+ if (args.length < 3) {
234
+ console.error('Usage: test-mcp-dev.js call <tool-name> <json-args>');
235
+ process.exit(1);
236
+ }
237
+
238
+ const toolName = args[1];
239
+ const toolArgs = JSON.parse(args[2]);
240
+
241
+ const result = await client.callTool(toolName, toolArgs);
242
+
243
+ // Output result as JSON for parsing by test-plugin
244
+ console.log(JSON.stringify(result, null, 2));
245
+ } else if (command === 'list') {
246
+ const result = await client.listTools();
247
+ console.log(JSON.stringify(result, null, 2));
248
+ } else {
249
+ console.error(`Unknown command: ${command}`);
250
+ process.exit(1);
251
+ }
252
+ } catch (error) {
253
+ console.error(`Error: ${error.message}`);
254
+ process.exit(1);
255
+ } finally {
256
+ client.stop();
257
+ }
258
+ }
259
+
260
+ main();
@@ -3,25 +3,32 @@ import { readFileSync, existsSync } from 'fs';
3
3
  import { join } from 'path';
4
4
 
5
5
  /**
6
- * Tests to verify plugin.json is correctly configured for MCP server.
6
+ * Tests to verify .mcp.json is correctly configured for MCP server.
7
7
  * The MCP server must work when the plugin is installed via marketplace.
8
8
  *
9
9
  * Key requirements:
10
10
  * - Must use ${CLAUDE_PLUGIN_ROOT} for server path (resolves to plugin cache)
11
11
  * - Must set PROJECT_ROOT env var (required by server fail-fast check)
12
12
  * - Must NOT use relative paths (would resolve to user's project, not plugin)
13
+ *
14
+ * Note: We use .mcp.json at plugin root (not inline in plugin.json) due to
15
+ * Claude Code Bug #16143 where inline mcpServers is ignored during parsing.
16
+ * See: https://github.com/anthropics/claude-code/issues/16143
13
17
  */
14
- describe('Plugin MCP Configuration (.claude-plugin/plugin.json)', () => {
15
- const configPath = join(process.cwd(), '.claude-plugin/plugin.json');
16
- const config = JSON.parse(readFileSync(configPath, 'utf-8'));
18
+ describe('Plugin MCP Configuration (.mcp.json)', () => {
19
+ const mcpJsonPath = join(process.cwd(), '.mcp.json');
20
+ const config = JSON.parse(readFileSync(mcpJsonPath, 'utf-8'));
21
+
22
+ it('has .mcp.json file at plugin root', () => {
23
+ expect(existsSync(mcpJsonPath)).toBe(true);
24
+ });
17
25
 
18
- it('has mcpServers configuration inline', () => {
19
- expect(config).toHaveProperty('mcpServers');
20
- expect(config.mcpServers).toHaveProperty('bluera-knowledge');
26
+ it('has bluera-knowledge server configuration', () => {
27
+ expect(config).toHaveProperty('bluera-knowledge');
21
28
  });
22
29
 
23
30
  it('uses ${CLAUDE_PLUGIN_ROOT} for server path (required for plugin mode)', () => {
24
- const serverConfig = config.mcpServers['bluera-knowledge'];
31
+ const serverConfig = config['bluera-knowledge'];
25
32
  const argsString = JSON.stringify(serverConfig.args);
26
33
 
27
34
  // CLAUDE_PLUGIN_ROOT is set by Claude Code when plugin is installed
@@ -31,7 +38,7 @@ describe('Plugin MCP Configuration (.claude-plugin/plugin.json)', () => {
31
38
  });
32
39
 
33
40
  it('does NOT use relative paths (would break in plugin mode)', () => {
34
- const serverConfig = config.mcpServers['bluera-knowledge'];
41
+ const serverConfig = config['bluera-knowledge'];
35
42
  const argsString = JSON.stringify(serverConfig.args);
36
43
 
37
44
  // Relative paths like ./dist would resolve to user's project directory
@@ -40,7 +47,7 @@ describe('Plugin MCP Configuration (.claude-plugin/plugin.json)', () => {
40
47
  });
41
48
 
42
49
  it('sets PROJECT_ROOT environment variable (required by fail-fast server)', () => {
43
- const serverConfig = config.mcpServers['bluera-knowledge'];
50
+ const serverConfig = config['bluera-knowledge'];
44
51
 
45
52
  // PROJECT_ROOT is required since b404cd6 (fail-fast change)
46
53
  expect(serverConfig.env).toHaveProperty('PROJECT_ROOT');
@@ -49,16 +56,16 @@ describe('Plugin MCP Configuration (.claude-plugin/plugin.json)', () => {
49
56
  });
50
57
 
51
58
  /**
52
- * Tests to ensure .mcp.json is NOT distributed with the plugin.
53
- * .mcp.json at project root causes confusion between plugin and project config.
59
+ * Tests to ensure plugin.json does NOT have inline mcpServers.
60
+ * Inline mcpServers is broken due to Claude Code Bug #16143.
54
61
  */
55
- describe('No conflicting .mcp.json in repo', () => {
56
- it('does NOT have .mcp.json in repo root (prevents config confusion)', () => {
57
- const mcpJsonPath = join(process.cwd(), '.mcp.json');
62
+ describe('plugin.json does NOT have inline mcpServers', () => {
63
+ it('plugin.json does NOT contain mcpServers (would be ignored by Claude Code)', () => {
64
+ const pluginJsonPath = join(process.cwd(), '.claude-plugin/plugin.json');
65
+ const pluginConfig = JSON.parse(readFileSync(pluginJsonPath, 'utf-8'));
58
66
 
59
- // .mcp.json should NOT exist in the repo
60
- // - For plugin mode: use mcpServers in plugin.json
61
- // - For development: use ~/.claude.json per README
62
- expect(existsSync(mcpJsonPath)).toBe(false);
67
+ // mcpServers in plugin.json is ignored due to Bug #16143
68
+ // All MCP config should be in .mcp.json at plugin root
69
+ expect(pluginConfig).not.toHaveProperty('mcpServers');
63
70
  });
64
71
  });
@@ -182,10 +182,11 @@ describe('CLI Consistency', () => {
182
182
  beforeAll(async () => {
183
183
  try {
184
184
  cli(`store create quiet-test-store --type file --source "${testFilesDir}"`);
185
+ cli('index quiet-test-store');
185
186
  } catch {
186
187
  // Store may already exist
187
188
  }
188
- });
189
+ }, 120000);
189
190
 
190
191
  it('--quiet suppresses all output for index on success', () => {
191
192
  const result = runCli('index quiet-test-store --quiet');
@@ -204,7 +205,7 @@ describe('CLI Consistency', () => {
204
205
  });
205
206
 
206
207
  it('--quiet outputs only paths for search', () => {
207
- const result = runCli('search "test" --quiet');
208
+ const result = runCli('search "test" --stores quiet-test-store --quiet');
208
209
  expect(result.exitCode).toBe(0);
209
210
  // Should not contain verbose headers
210
211
  expect(result.stdout).not.toContain('Search:');