bluera-knowledge 0.9.38 → 0.9.40

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.
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  createMCPServer,
3
3
  runMCPServer
4
- } from "../chunk-36IFANFI.js";
5
- import "../chunk-XJFV7AJW.js";
4
+ } from "../chunk-TIGPI3BE.js";
5
+ import "../chunk-HUEWT6U5.js";
6
6
  import "../chunk-6FHWC36B.js";
7
7
  export {
8
8
  createMCPServer,
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  IntelligentCrawler
4
- } from "../chunk-ZAWIPEYX.js";
4
+ } from "../chunk-IZWOEBFM.js";
5
5
  import {
6
6
  JobService,
7
7
  createDocumentId,
8
8
  createServices,
9
9
  createStoreId
10
- } from "../chunk-XJFV7AJW.js";
10
+ } from "../chunk-HUEWT6U5.js";
11
11
  import "../chunk-6FHWC36B.js";
12
12
 
13
13
  // src/workers/background-worker.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bluera-knowledge",
3
- "version": "0.9.38",
3
+ "version": "0.9.40",
4
4
  "description": "CLI tool for managing knowledge stores with semantic search",
5
5
  "type": "module",
6
6
  "bin": {
@@ -28,13 +28,14 @@
28
28
  "lint:quiet": "eslint src/ --quiet && echo '✓ Lint passed' || { echo '❌ ESLint errors'; exit 1; }",
29
29
  "lint:deadcode:quiet": "knip --no-exit-code --quiet 2>&1 | grep -E '^(Unused|Configuration)' || echo '✓ No unused code'",
30
30
  "typecheck:quiet": "tsc --noEmit --pretty false && echo '✓ Type check passed'",
31
- "test:changed:quiet": "vitest run --changed --reporter=dot --silent 2>&1 | tail -3",
32
- "test:coverage:quiet": "vitest run --coverage --reporter=dot 2>&1 | grep -E '(FAIL|Test Files|passed|Coverage|threshold)' | tail -10",
31
+ "test:changed:quiet": "bash -c 'set -o pipefail && vitest run --changed --reporter=dot --silent 2>&1 | tail -3'",
32
+ "test:coverage:quiet": "bash -c 'set -o pipefail && vitest run --coverage --reporter=dot 2>&1 | grep -E \"(FAIL|Test Files|passed|Coverage|threshold)\" | tail -10'",
33
33
  "build:quiet": "tsup --silent && echo '✓ Build complete'",
34
+ "prerelease": "bun run format:check:quiet && bun run lint:quiet && bun run lint:deadcode:quiet && bun run typecheck:quiet && bun run test:coverage:quiet && bun run build:quiet",
34
35
  "prepare": "husky",
35
- "version:major": "commit-and-tag-version --release-as major --skip.commit --skip.tag",
36
- "version:minor": "commit-and-tag-version --release-as minor --skip.commit --skip.tag",
37
- "version:patch": "commit-and-tag-version --release-as patch --skip.commit --skip.tag",
36
+ "version:major": "bun run prerelease && commit-and-tag-version --release-as major --skip.commit --skip.tag",
37
+ "version:minor": "bun run prerelease && commit-and-tag-version --release-as minor --skip.commit --skip.tag",
38
+ "version:patch": "bun run prerelease && commit-and-tag-version --release-as patch --skip.commit --skip.tag",
38
39
  "release:patch": "commit-and-tag-version --release-as patch && git push --follow-tags",
39
40
  "release:minor": "commit-and-tag-version --release-as minor && git push --follow-tags",
40
41
  "release:major": "commit-and-tag-version --release-as major && git push --follow-tags"
package/plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bluera-knowledge",
3
- "version": "0.9.38",
3
+ "version": "0.9.40",
4
4
  "description": "Clone repos, crawl docs, search locally. Fast, authoritative answers for AI coding agents.",
5
5
  "commands": "./commands",
6
6
  "hooks": "./hooks/hooks.json",
@@ -18,6 +18,10 @@ export function createSearchCommand(getOptions: () => GlobalOptions): Command {
18
18
  )
19
19
  .option('-n, --limit <count>', 'Maximum results to return (default: 10)', '10')
20
20
  .option('-t, --threshold <score>', 'Minimum score 0-1; omit low-relevance results')
21
+ .option(
22
+ '--min-relevance <score>',
23
+ 'Minimum raw cosine similarity 0-1; returns empty if no results meet threshold'
24
+ )
21
25
  .option('--include-content', 'Show full document content, not just preview snippet')
22
26
  .option(
23
27
  '--detail <level>',
@@ -32,6 +36,7 @@ export function createSearchCommand(getOptions: () => GlobalOptions): Command {
32
36
  mode?: SearchMode;
33
37
  limit?: string;
34
38
  threshold?: string;
39
+ minRelevance?: string;
35
40
  includeContent?: boolean;
36
41
  detail?: DetailLevel;
37
42
  }
@@ -82,6 +87,8 @@ export function createSearchCommand(getOptions: () => GlobalOptions): Command {
82
87
  limit: parseInt(options.limit ?? '10', 10),
83
88
  threshold:
84
89
  options.threshold !== undefined ? parseFloat(options.threshold) : undefined,
90
+ minRelevance:
91
+ options.minRelevance !== undefined ? parseFloat(options.minRelevance) : undefined,
85
92
  includeContent: options.includeContent,
86
93
  detail: options.detail ?? 'minimal',
87
94
  });
@@ -96,12 +103,23 @@ export function createSearchCommand(getOptions: () => GlobalOptions): Command {
96
103
  }
97
104
  } else {
98
105
  console.log(`\nSearch: "${query}"`);
99
- console.log(
100
- `Mode: ${results.mode} | Detail: ${String(options.detail)} | Stores: ${String(results.stores.length)} | Results: ${String(results.totalResults)} | Time: ${String(results.timeMs)}ms\n`
101
- );
106
+
107
+ // Build status line with optional confidence info
108
+ let statusLine = `Mode: ${results.mode} | Detail: ${String(options.detail)} | Stores: ${String(results.stores.length)} | Results: ${String(results.totalResults)} | Time: ${String(results.timeMs)}ms`;
109
+ if (results.confidence !== undefined) {
110
+ statusLine += ` | Confidence: ${results.confidence}`;
111
+ }
112
+ if (results.maxRawScore !== undefined) {
113
+ statusLine += ` | MaxRaw: ${results.maxRawScore.toFixed(3)}`;
114
+ }
115
+ console.log(`${statusLine}\n`);
102
116
 
103
117
  if (results.results.length === 0) {
104
- console.log('No results found.\n');
118
+ if (results.confidence === 'low') {
119
+ console.log('No sufficiently relevant results found.\n');
120
+ } else {
121
+ console.log('No results found.\n');
122
+ }
105
123
  } else {
106
124
  for (let i = 0; i < results.results.length; i++) {
107
125
  const r = results.results[i];
@@ -72,6 +72,7 @@ export const handleSearch: ToolHandler<SearchArgs> = async (
72
72
  mode: 'hybrid',
73
73
  limit: validated.limit,
74
74
  detail: validated.detail,
75
+ minRelevance: validated.minRelevance,
75
76
  };
76
77
 
77
78
  const results = await services.search.search(searchQuery);
@@ -107,6 +108,8 @@ export const handleSearch: ToolHandler<SearchArgs> = async (
107
108
  totalResults: results.totalResults,
108
109
  mode: results.mode,
109
110
  timeMs: results.timeMs,
111
+ confidence: results.confidence,
112
+ maxRawScore: results.maxRawScore,
110
113
  },
111
114
  null,
112
115
  2
@@ -115,8 +118,10 @@ export const handleSearch: ToolHandler<SearchArgs> = async (
115
118
  // Calculate actual token estimate based on response content
116
119
  const responseTokens = estimateTokens(responseJson);
117
120
 
118
- // Create visible header with token usage
119
- const header = `Search: "${validated.query}" | Results: ${String(results.totalResults)} | ${formatTokenCount(responseTokens)} tokens | ${String(results.timeMs)}ms\n\n`;
121
+ // Create visible header with token usage and confidence
122
+ const confidenceInfo =
123
+ results.confidence !== undefined ? ` | Confidence: ${results.confidence}` : '';
124
+ const header = `Search: "${validated.query}" | Results: ${String(results.totalResults)} | ${formatTokenCount(responseTokens)} tokens | ${String(results.timeMs)}ms${confidenceInfo}\n\n`;
120
125
 
121
126
  // Log the complete MCP response that will be sent to Claude Code
122
127
  logger.info(
@@ -28,6 +28,11 @@ export const SearchArgsSchema = z.object({
28
28
  detail: z.enum(['minimal', 'contextual', 'full']).default('minimal'),
29
29
  limit: z.number().int().positive().default(10),
30
30
  stores: z.array(z.string()).optional(),
31
+ minRelevance: z
32
+ .number()
33
+ .min(0, 'minRelevance must be between 0 and 1')
34
+ .max(1, 'minRelevance must be between 0 and 1')
35
+ .optional(),
31
36
  });
32
37
 
33
38
  export type SearchArgs = z.infer<typeof SearchArgsSchema>;
package/src/mcp/server.ts CHANGED
@@ -70,6 +70,11 @@ export function createMCPServer(options: MCPServerOptions): Server {
70
70
  items: { type: 'string' },
71
71
  description: 'Specific store IDs to search (optional)',
72
72
  },
73
+ minRelevance: {
74
+ type: 'number',
75
+ description:
76
+ 'Minimum raw cosine similarity (0-1). Returns empty if no results meet threshold. Use to filter irrelevant results.',
77
+ },
73
78
  },
74
79
  required: ['query'],
75
80
  },
@@ -1161,6 +1161,7 @@ describe('SearchService - Edge Cases', () => {
1161
1161
  });
1162
1162
 
1163
1163
  it('handles threshold parameter for vector search', async () => {
1164
+ // Setup: multiple results with varying scores
1164
1165
  vi.mocked(mockLanceStore.search).mockResolvedValue([
1165
1166
  {
1166
1167
  id: createDocumentId('doc1'),
@@ -1168,22 +1169,41 @@ describe('SearchService - Edge Cases', () => {
1168
1169
  content: 'high score',
1169
1170
  metadata: { type: 'file' as const, storeId, indexedAt: new Date() },
1170
1171
  },
1172
+ {
1173
+ id: createDocumentId('doc2'),
1174
+ score: 0.5,
1175
+ content: 'medium score',
1176
+ metadata: { type: 'file' as const, storeId, indexedAt: new Date() },
1177
+ },
1178
+ {
1179
+ id: createDocumentId('doc3'),
1180
+ score: 0.3,
1181
+ content: 'low score',
1182
+ metadata: { type: 'file' as const, storeId, indexedAt: new Date() },
1183
+ },
1171
1184
  ]);
1172
1185
 
1186
+ // Search with high threshold
1173
1187
  const results = await searchService.search({
1174
1188
  query: 'test',
1175
1189
  stores: [storeId],
1176
1190
  mode: 'vector',
1177
1191
  limit: 10,
1178
- threshold: 0.9,
1192
+ threshold: 0.8,
1179
1193
  });
1180
1194
 
1195
+ // Verify lanceStore.search was called (without checking threshold param - it's unused in LanceStore)
1181
1196
  expect(vi.mocked(mockLanceStore.search)).toHaveBeenCalledWith(
1182
1197
  storeId,
1183
1198
  expect.anything(),
1184
- expect.anything(),
1185
- 0.9
1199
+ expect.anything()
1186
1200
  );
1201
+
1202
+ // Verify threshold filtering works: only high-score result should pass
1203
+ // After normalization, the top result has score 1.0, and threshold 0.8 filters out lower ones
1204
+ // The normalized scores are: doc1=1.0, doc2=0.31, doc3=0.0 (relative to max 0.95)
1205
+ expect(results.results.length).toBe(1);
1206
+ expect(results.results[0]?.id).toBe('doc1');
1187
1207
  });
1188
1208
  });
1189
1209
 
@@ -1992,3 +2012,171 @@ describe('SearchService - Threshold Filtering', () => {
1992
2012
  expect(results.query).toBe('test query');
1993
2013
  });
1994
2014
  });
2015
+
2016
+ describe('SearchService - Raw Score and Confidence', () => {
2017
+ let mockLanceStore: LanceStore;
2018
+ let mockEmbeddingEngine: EmbeddingEngine;
2019
+ let searchService: SearchService;
2020
+ const storeId = createStoreId('test-store');
2021
+
2022
+ beforeEach(() => {
2023
+ mockLanceStore = {
2024
+ search: vi.fn(),
2025
+ fullTextSearch: vi.fn(),
2026
+ } as unknown as LanceStore;
2027
+
2028
+ mockEmbeddingEngine = {
2029
+ embed: vi.fn().mockResolvedValue([0.1, 0.2, 0.3]),
2030
+ } as unknown as EmbeddingEngine;
2031
+
2032
+ searchService = new SearchService(mockLanceStore, mockEmbeddingEngine);
2033
+ });
2034
+
2035
+ it('exposes rawVectorScore in rankingMetadata for hybrid search', async () => {
2036
+ // Mock results with known raw vector scores
2037
+ vi.mocked(mockLanceStore.search).mockResolvedValue([
2038
+ {
2039
+ id: createDocumentId('doc1'),
2040
+ score: 0.85, // Raw cosine similarity
2041
+ content: 'vector result',
2042
+ metadata: { type: 'file' as const, storeId, indexedAt: new Date() },
2043
+ },
2044
+ {
2045
+ id: createDocumentId('doc2'),
2046
+ score: 0.65,
2047
+ content: 'another vector result',
2048
+ metadata: { type: 'file' as const, storeId, indexedAt: new Date() },
2049
+ },
2050
+ ]);
2051
+ vi.mocked(mockLanceStore.fullTextSearch).mockResolvedValue([]);
2052
+
2053
+ const results = await searchService.search({
2054
+ query: 'test query',
2055
+ stores: [storeId],
2056
+ mode: 'hybrid',
2057
+ limit: 10,
2058
+ });
2059
+
2060
+ // Verify results have rawVectorScore in rankingMetadata
2061
+ expect(results.results.length).toBeGreaterThan(0);
2062
+ const firstResult = results.results[0];
2063
+ expect(firstResult?.rankingMetadata).toBeDefined();
2064
+ expect(firstResult?.rankingMetadata?.rawVectorScore).toBeDefined();
2065
+ expect(firstResult?.rankingMetadata?.rawVectorScore).toBe(0.85);
2066
+ });
2067
+
2068
+ it('returns confidence level based on maxRawScore', async () => {
2069
+ // Mock results with high raw vector score (>= 0.5)
2070
+ vi.mocked(mockLanceStore.search).mockResolvedValue([
2071
+ {
2072
+ id: createDocumentId('doc1'),
2073
+ score: 0.6, // High confidence threshold
2074
+ content: 'high score result',
2075
+ metadata: { type: 'file' as const, storeId, indexedAt: new Date() },
2076
+ },
2077
+ ]);
2078
+ vi.mocked(mockLanceStore.fullTextSearch).mockResolvedValue([]);
2079
+
2080
+ const results = await searchService.search({
2081
+ query: 'test query',
2082
+ stores: [storeId],
2083
+ mode: 'hybrid',
2084
+ limit: 10,
2085
+ });
2086
+
2087
+ expect(results.confidence).toBe('high');
2088
+ expect(results.maxRawScore).toBe(0.6);
2089
+ });
2090
+
2091
+ it('returns medium confidence for scores between 0.3 and 0.5', async () => {
2092
+ vi.mocked(mockLanceStore.search).mockResolvedValue([
2093
+ {
2094
+ id: createDocumentId('doc1'),
2095
+ score: 0.4, // Medium confidence
2096
+ content: 'medium score result',
2097
+ metadata: { type: 'file' as const, storeId, indexedAt: new Date() },
2098
+ },
2099
+ ]);
2100
+ vi.mocked(mockLanceStore.fullTextSearch).mockResolvedValue([]);
2101
+
2102
+ const results = await searchService.search({
2103
+ query: 'test query',
2104
+ stores: [storeId],
2105
+ mode: 'hybrid',
2106
+ limit: 10,
2107
+ });
2108
+
2109
+ expect(results.confidence).toBe('medium');
2110
+ expect(results.maxRawScore).toBe(0.4);
2111
+ });
2112
+
2113
+ it('returns low confidence for scores below 0.3', async () => {
2114
+ vi.mocked(mockLanceStore.search).mockResolvedValue([
2115
+ {
2116
+ id: createDocumentId('doc1'),
2117
+ score: 0.2, // Low confidence
2118
+ content: 'low score result',
2119
+ metadata: { type: 'file' as const, storeId, indexedAt: new Date() },
2120
+ },
2121
+ ]);
2122
+ vi.mocked(mockLanceStore.fullTextSearch).mockResolvedValue([]);
2123
+
2124
+ const results = await searchService.search({
2125
+ query: 'test query',
2126
+ stores: [storeId],
2127
+ mode: 'hybrid',
2128
+ limit: 10,
2129
+ });
2130
+
2131
+ expect(results.confidence).toBe('low');
2132
+ expect(results.maxRawScore).toBe(0.2);
2133
+ });
2134
+
2135
+ it('filters results with minRelevance based on raw score', async () => {
2136
+ vi.mocked(mockLanceStore.search).mockResolvedValue([
2137
+ {
2138
+ id: createDocumentId('doc1'),
2139
+ score: 0.25, // Below minRelevance threshold
2140
+ content: 'low relevance result',
2141
+ metadata: { type: 'file' as const, storeId, indexedAt: new Date() },
2142
+ },
2143
+ ]);
2144
+ vi.mocked(mockLanceStore.fullTextSearch).mockResolvedValue([]);
2145
+
2146
+ const results = await searchService.search({
2147
+ query: 'irrelevant query',
2148
+ stores: [storeId],
2149
+ mode: 'hybrid',
2150
+ limit: 10,
2151
+ minRelevance: 0.4, // Filter out results below this raw score
2152
+ });
2153
+
2154
+ // Should return empty since max raw score (0.25) < minRelevance (0.4)
2155
+ expect(results.results.length).toBe(0);
2156
+ expect(results.confidence).toBe('low');
2157
+ });
2158
+
2159
+ it('returns results when maxRawScore meets minRelevance', async () => {
2160
+ vi.mocked(mockLanceStore.search).mockResolvedValue([
2161
+ {
2162
+ id: createDocumentId('doc1'),
2163
+ score: 0.5, // Above minRelevance threshold
2164
+ content: 'relevant result',
2165
+ metadata: { type: 'file' as const, storeId, indexedAt: new Date() },
2166
+ },
2167
+ ]);
2168
+ vi.mocked(mockLanceStore.fullTextSearch).mockResolvedValue([]);
2169
+
2170
+ const results = await searchService.search({
2171
+ query: 'relevant query',
2172
+ stores: [storeId],
2173
+ mode: 'hybrid',
2174
+ limit: 10,
2175
+ minRelevance: 0.4,
2176
+ });
2177
+
2178
+ // Should return results since max raw score (0.5) >= minRelevance (0.4)
2179
+ expect(results.results.length).toBe(1);
2180
+ expect(results.confidence).toBe('high');
2181
+ });
2182
+ });