bluera-knowledge 0.13.0 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/rules/code-quality.md +12 -0
- package/.claude/rules/git.md +5 -0
- package/.claude/rules/versioning.md +7 -0
- package/CLAUDE.md +5 -13
- package/README.md +8 -0
- package/commands/crawl.md +2 -1
- package/commands/test-plugin.md +197 -72
- package/docs/claude-code-best-practices.md +458 -0
- package/eslint.config.js +1 -1
- package/hooks/check-dependencies.sh +18 -1
- package/hooks/hooks.json +2 -2
- package/hooks/posttooluse-bk-reminder.py +30 -2
- package/package.json +1 -1
- package/scripts/test-mcp-dev.js +260 -0
- package/tests/integration/cli-consistency.test.ts +3 -2
- package/docs/plans/2024-12-17-ai-search-quality-implementation.md +0 -752
- package/docs/plans/2024-12-17-ai-search-quality-testing-design.md +0 -201
- package/docs/plans/2025-12-16-bluera-knowledge-cli.md +0 -2951
- package/docs/plans/2025-12-16-phase2-features.md +0 -1518
- package/docs/plans/2025-12-17-hil-implementation.md +0 -926
- package/docs/plans/2025-12-17-hil-quality-testing.md +0 -224
- package/docs/plans/2025-12-17-search-quality-phase1-implementation.md +0 -1416
- package/docs/plans/2025-12-17-search-quality-testing-v2-design.md +0 -212
- package/docs/plans/2025-12-28-ai-agent-optimization.md +0 -1630
|
@@ -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();
|
|
@@ -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:');
|