bluera-knowledge 0.9.32 → 0.9.34
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/hooks/post-edit-check.sh +5 -3
- package/.claude/skills/atomic-commits/SKILL.md +3 -1
- package/.husky/pre-commit +3 -2
- package/.prettierrc +9 -0
- package/.versionrc.json +1 -1
- package/CHANGELOG.md +33 -0
- package/CLAUDE.md +6 -0
- package/README.md +25 -13
- package/bun.lock +277 -33
- package/dist/{chunk-L2YVNC63.js → chunk-6FHWC36B.js} +9 -1
- package/dist/chunk-6FHWC36B.js.map +1 -0
- package/dist/{chunk-RST4XGRL.js → chunk-DC7CGSGT.js} +288 -241
- package/dist/chunk-DC7CGSGT.js.map +1 -0
- package/dist/{chunk-6PBP5DVD.js → chunk-WFNPNAAP.js} +3212 -3054
- package/dist/chunk-WFNPNAAP.js.map +1 -0
- package/dist/{chunk-WT2DAEO7.js → chunk-Z2KKVH45.js} +548 -482
- package/dist/chunk-Z2KKVH45.js.map +1 -0
- package/dist/index.js +871 -758
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +3 -3
- package/dist/watch.service-BJV3TI3F.js +7 -0
- package/dist/workers/background-worker-cli.js +46 -45
- package/dist/workers/background-worker-cli.js.map +1 -1
- package/eslint.config.js +43 -1
- package/package.json +18 -11
- package/plugin.json +8 -0
- package/python/requirements.txt +1 -1
- package/src/analysis/ast-parser.test.ts +12 -11
- package/src/analysis/ast-parser.ts +28 -22
- package/src/analysis/code-graph.test.ts +52 -62
- package/src/analysis/code-graph.ts +9 -13
- package/src/analysis/dependency-usage-analyzer.test.ts +91 -271
- package/src/analysis/dependency-usage-analyzer.ts +52 -24
- package/src/analysis/go-ast-parser.test.ts +22 -22
- package/src/analysis/go-ast-parser.ts +18 -25
- package/src/analysis/parser-factory.test.ts +9 -9
- package/src/analysis/parser-factory.ts +3 -3
- package/src/analysis/python-ast-parser.test.ts +27 -27
- package/src/analysis/python-ast-parser.ts +2 -2
- package/src/analysis/repo-url-resolver.test.ts +82 -82
- package/src/analysis/rust-ast-parser.test.ts +19 -19
- package/src/analysis/rust-ast-parser.ts +17 -27
- package/src/analysis/tree-sitter-parser.test.ts +3 -3
- package/src/analysis/tree-sitter-parser.ts +10 -16
- package/src/cli/commands/crawl.test.ts +40 -24
- package/src/cli/commands/crawl.ts +186 -166
- package/src/cli/commands/index-cmd.test.ts +90 -90
- package/src/cli/commands/index-cmd.ts +52 -36
- package/src/cli/commands/mcp.test.ts +6 -6
- package/src/cli/commands/mcp.ts +2 -2
- package/src/cli/commands/plugin-api.test.ts +16 -18
- package/src/cli/commands/plugin-api.ts +9 -6
- package/src/cli/commands/search.test.ts +16 -7
- package/src/cli/commands/search.ts +124 -87
- package/src/cli/commands/serve.test.ts +67 -25
- package/src/cli/commands/serve.ts +18 -3
- package/src/cli/commands/setup.test.ts +176 -101
- package/src/cli/commands/setup.ts +140 -117
- package/src/cli/commands/store.test.ts +82 -53
- package/src/cli/commands/store.ts +56 -37
- package/src/cli/program.ts +2 -2
- package/src/crawl/article-converter.test.ts +4 -1
- package/src/crawl/article-converter.ts +46 -31
- package/src/crawl/bridge.test.ts +240 -132
- package/src/crawl/bridge.ts +87 -30
- package/src/crawl/claude-client.test.ts +124 -56
- package/src/crawl/claude-client.ts +7 -15
- package/src/crawl/intelligent-crawler.test.ts +65 -22
- package/src/crawl/intelligent-crawler.ts +86 -53
- package/src/crawl/markdown-utils.ts +1 -4
- package/src/db/embeddings.ts +4 -6
- package/src/db/lance.test.ts +4 -4
- package/src/db/lance.ts +16 -12
- package/src/index.ts +26 -17
- package/src/logging/index.ts +1 -5
- package/src/logging/logger.ts +3 -5
- package/src/logging/payload.test.ts +1 -1
- package/src/logging/payload.ts +3 -5
- package/src/mcp/commands/index.ts +2 -2
- package/src/mcp/commands/job.commands.ts +12 -18
- package/src/mcp/commands/meta.commands.ts +13 -13
- package/src/mcp/commands/registry.ts +5 -8
- package/src/mcp/commands/store.commands.ts +19 -19
- package/src/mcp/handlers/execute.handler.test.ts +10 -10
- package/src/mcp/handlers/execute.handler.ts +4 -5
- package/src/mcp/handlers/index.ts +10 -14
- package/src/mcp/handlers/job.handler.test.ts +10 -10
- package/src/mcp/handlers/job.handler.ts +22 -25
- package/src/mcp/handlers/search.handler.test.ts +36 -65
- package/src/mcp/handlers/search.handler.ts +135 -104
- package/src/mcp/handlers/store.handler.test.ts +41 -52
- package/src/mcp/handlers/store.handler.ts +108 -88
- package/src/mcp/schemas/index.test.ts +73 -68
- package/src/mcp/schemas/index.ts +18 -12
- package/src/mcp/server.test.ts +1 -1
- package/src/mcp/server.ts +59 -46
- package/src/plugin/commands.test.ts +230 -95
- package/src/plugin/commands.ts +24 -25
- package/src/plugin/dependency-analyzer.test.ts +52 -52
- package/src/plugin/dependency-analyzer.ts +85 -22
- package/src/plugin/git-clone.test.ts +24 -13
- package/src/plugin/git-clone.ts +3 -7
- package/src/server/app.test.ts +109 -109
- package/src/server/app.ts +32 -23
- package/src/server/index.test.ts +64 -66
- package/src/services/chunking.service.test.ts +32 -32
- package/src/services/chunking.service.ts +16 -9
- package/src/services/code-graph.service.test.ts +30 -36
- package/src/services/code-graph.service.ts +24 -10
- package/src/services/code-unit.service.test.ts +55 -11
- package/src/services/code-unit.service.ts +85 -11
- package/src/services/config.service.test.ts +37 -18
- package/src/services/config.service.ts +30 -7
- package/src/services/index.service.test.ts +49 -18
- package/src/services/index.service.ts +98 -48
- package/src/services/index.ts +6 -9
- package/src/services/job.service.test.ts +22 -22
- package/src/services/job.service.ts +18 -18
- package/src/services/project-root.service.test.ts +1 -3
- package/src/services/search.service.test.ts +248 -120
- package/src/services/search.service.ts +286 -156
- package/src/services/services.test.ts +1 -1
- package/src/services/snippet.service.test.ts +14 -6
- package/src/services/snippet.service.ts +7 -5
- package/src/services/store.service.test.ts +68 -29
- package/src/services/store.service.ts +41 -12
- package/src/services/watch.service.test.ts +34 -14
- package/src/services/watch.service.ts +11 -1
- package/src/types/brands.test.ts +3 -1
- package/src/types/index.ts +2 -13
- package/src/types/search.ts +10 -8
- package/src/utils/type-guards.test.ts +20 -15
- package/src/utils/type-guards.ts +1 -1
- package/src/workers/background-worker-cli.ts +2 -2
- package/src/workers/background-worker.test.ts +54 -40
- package/src/workers/background-worker.ts +76 -60
- package/src/workers/spawn-worker.test.ts +22 -10
- package/src/workers/spawn-worker.ts +6 -6
- package/tests/analysis/ast-parser.test.ts +3 -3
- package/tests/analysis/code-graph.test.ts +5 -5
- package/tests/fixtures/code-snippets/api/error-handling.ts +4 -15
- package/tests/fixtures/code-snippets/api/rest-controller.ts +3 -9
- package/tests/fixtures/code-snippets/auth/jwt-auth.ts +5 -21
- package/tests/fixtures/code-snippets/auth/oauth-flow.ts +4 -4
- package/tests/fixtures/code-snippets/database/repository-pattern.ts +11 -3
- package/tests/fixtures/corpus/oss-repos/hono/src/adapter/aws-lambda/handler.ts +2 -2
- package/tests/fixtures/corpus/oss-repos/hono/src/adapter/cloudflare-pages/handler.ts +1 -1
- package/tests/fixtures/corpus/oss-repos/hono/src/adapter/cloudflare-workers/serve-static.ts +2 -2
- package/tests/fixtures/corpus/oss-repos/hono/src/client/client.ts +2 -2
- package/tests/fixtures/corpus/oss-repos/hono/src/client/types.ts +22 -20
- package/tests/fixtures/corpus/oss-repos/hono/src/context.ts +13 -10
- package/tests/fixtures/corpus/oss-repos/hono/src/helper/accepts/accepts.ts +10 -7
- package/tests/fixtures/corpus/oss-repos/hono/src/helper/adapter/index.ts +2 -2
- package/tests/fixtures/corpus/oss-repos/hono/src/helper/css/index.ts +1 -1
- package/tests/fixtures/corpus/oss-repos/hono/src/helper/factory/index.ts +16 -16
- package/tests/fixtures/corpus/oss-repos/hono/src/helper/ssg/ssg.ts +2 -2
- package/tests/fixtures/corpus/oss-repos/hono/src/hono-base.ts +3 -3
- package/tests/fixtures/corpus/oss-repos/hono/src/hono.ts +1 -1
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/dom/css.ts +2 -2
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/dom/intrinsic-element/components.ts +1 -1
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/dom/render.ts +7 -7
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/hooks/index.ts +3 -3
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/intrinsic-element/components.ts +1 -1
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/utils.ts +6 -6
- package/tests/fixtures/corpus/oss-repos/hono/src/middleware/jsx-renderer/index.ts +3 -3
- package/tests/fixtures/corpus/oss-repos/hono/src/middleware/serve-static/index.ts +1 -1
- package/tests/fixtures/corpus/oss-repos/hono/src/preset/quick.ts +1 -1
- package/tests/fixtures/corpus/oss-repos/hono/src/preset/tiny.ts +1 -1
- package/tests/fixtures/corpus/oss-repos/hono/src/router/pattern-router/router.ts +2 -2
- package/tests/fixtures/corpus/oss-repos/hono/src/router/reg-exp-router/node.ts +4 -4
- package/tests/fixtures/corpus/oss-repos/hono/src/router/reg-exp-router/router.ts +1 -1
- package/tests/fixtures/corpus/oss-repos/hono/src/router/trie-router/node.ts +1 -1
- package/tests/fixtures/corpus/oss-repos/hono/src/types.ts +166 -169
- package/tests/fixtures/corpus/oss-repos/hono/src/utils/body.ts +8 -8
- package/tests/fixtures/corpus/oss-repos/hono/src/utils/color.ts +3 -3
- package/tests/fixtures/corpus/oss-repos/hono/src/utils/cookie.ts +2 -2
- package/tests/fixtures/corpus/oss-repos/hono/src/utils/encode.ts +2 -2
- package/tests/fixtures/corpus/oss-repos/hono/src/utils/types.ts +30 -33
- package/tests/fixtures/corpus/oss-repos/hono/src/validator/validator.ts +2 -2
- package/tests/fixtures/test-server.ts +3 -2
- package/tests/helpers/performance-metrics.ts +8 -25
- package/tests/helpers/search-relevance.ts +14 -69
- package/tests/integration/cli-consistency.test.ts +5 -4
- package/tests/integration/python-bridge.test.ts +13 -3
- package/tests/mcp/server.test.ts +1 -1
- package/tests/services/code-unit.service.test.ts +48 -0
- package/tests/services/job.service.test.ts +124 -0
- package/tests/services/search.progressive-context.test.ts +2 -2
- package/.claude-plugin/plugin.json +0 -13
- package/dist/chunk-6PBP5DVD.js.map +0 -1
- package/dist/chunk-L2YVNC63.js.map +0 -1
- package/dist/chunk-RST4XGRL.js.map +0 -1
- package/dist/chunk-WT2DAEO7.js.map +0 -1
- package/dist/watch.service-YAIKKDCF.js +0 -7
- package/skills/atomic-commits/SKILL.md +0 -77
- /package/dist/{watch.service-YAIKKDCF.js.map → watch.service-BJV3TI3F.js.map} +0 -0
|
@@ -62,7 +62,7 @@ describe('destroyServices', () => {
|
|
|
62
62
|
it('waits for LanceStore async cleanup before returning', async () => {
|
|
63
63
|
let closeCompleted = false;
|
|
64
64
|
mockLance.closeAsync.mockImplementation(async () => {
|
|
65
|
-
await new Promise(resolve => setTimeout(resolve, 50));
|
|
65
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
66
66
|
closeCompleted = true;
|
|
67
67
|
});
|
|
68
68
|
|
|
@@ -24,7 +24,8 @@ describe('SnippetService', () => {
|
|
|
24
24
|
});
|
|
25
25
|
|
|
26
26
|
it('extracts snippet around single query term match', () => {
|
|
27
|
-
const content =
|
|
27
|
+
const content =
|
|
28
|
+
'The quick brown fox jumps over the lazy dog. ' +
|
|
28
29
|
'This is some filler text to make the content longer. ' +
|
|
29
30
|
'Here is the important keyword we want to find. ' +
|
|
30
31
|
'More filler text at the end to extend the content.';
|
|
@@ -34,7 +35,8 @@ describe('SnippetService', () => {
|
|
|
34
35
|
});
|
|
35
36
|
|
|
36
37
|
it('extracts snippet around multiple clustered query terms', () => {
|
|
37
|
-
const content =
|
|
38
|
+
const content =
|
|
39
|
+
'Unrelated text at the start. '.repeat(10) +
|
|
38
40
|
'Here is the target area with multiple keywords: search query terms matching. ' +
|
|
39
41
|
'Unrelated text at the end. '.repeat(10);
|
|
40
42
|
const snippet = extractSnippet(content, 'target keywords search');
|
|
@@ -115,7 +117,8 @@ describe('SnippetService', () => {
|
|
|
115
117
|
});
|
|
116
118
|
|
|
117
119
|
it('finds best position when multiple query terms appear', () => {
|
|
118
|
-
const content =
|
|
120
|
+
const content =
|
|
121
|
+
'First occurrence of term. ' +
|
|
119
122
|
'A'.repeat(200) +
|
|
120
123
|
' Second occurrence with more terms from query nearby. ' +
|
|
121
124
|
'A'.repeat(200);
|
|
@@ -139,7 +142,9 @@ describe('SnippetService', () => {
|
|
|
139
142
|
|
|
140
143
|
it('calculates proximity score correctly', () => {
|
|
141
144
|
// Create content where terms are closer together in one location
|
|
142
|
-
const content =
|
|
145
|
+
const content =
|
|
146
|
+
'distant word here. ' +
|
|
147
|
+
'A'.repeat(300) +
|
|
143
148
|
' clustered search query terms all together here ' +
|
|
144
149
|
'A'.repeat(300);
|
|
145
150
|
const snippet = extractSnippet(content, 'search query terms', { maxLength: 100 });
|
|
@@ -186,11 +191,14 @@ describe('SnippetService', () => {
|
|
|
186
191
|
});
|
|
187
192
|
|
|
188
193
|
it('scores positions based on unique nearby terms', () => {
|
|
189
|
-
const content =
|
|
194
|
+
const content =
|
|
195
|
+
'repeated repeated repeated. ' +
|
|
190
196
|
'A'.repeat(100) +
|
|
191
197
|
' diverse unique different varied terms here ' +
|
|
192
198
|
'A'.repeat(100);
|
|
193
|
-
const snippet = extractSnippet(content, 'diverse unique different varied', {
|
|
199
|
+
const snippet = extractSnippet(content, 'diverse unique different varied', {
|
|
200
|
+
maxLength: 100,
|
|
201
|
+
});
|
|
194
202
|
expect(snippet).toContain('diverse');
|
|
195
203
|
expect(snippet).toContain('unique');
|
|
196
204
|
});
|
|
@@ -30,7 +30,7 @@ export function extractSnippet(
|
|
|
30
30
|
const queryTerms = query
|
|
31
31
|
.toLowerCase()
|
|
32
32
|
.split(/\s+/)
|
|
33
|
-
.filter(t => t.length > 2); // Skip very short words
|
|
33
|
+
.filter((t) => t.length > 2); // Skip very short words
|
|
34
34
|
|
|
35
35
|
// Normalize content for extraction
|
|
36
36
|
const normalizedContent = content.replace(/\s+/g, ' ').trim();
|
|
@@ -112,7 +112,9 @@ function findBestMatchPosition(content: string, queryTerms: string[]): number {
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
// Bonus: Near markdown section header
|
|
115
|
-
const headerMatch = content
|
|
115
|
+
const headerMatch = content
|
|
116
|
+
.slice(Math.max(0, position - 100), position)
|
|
117
|
+
.match(/^#{1,3}\s+.+$/m);
|
|
116
118
|
if (headerMatch) {
|
|
117
119
|
score += 4;
|
|
118
120
|
}
|
|
@@ -166,10 +168,10 @@ function extractContextWindow(
|
|
|
166
168
|
|
|
167
169
|
// Add ellipsis indicators
|
|
168
170
|
if (start > 0) {
|
|
169
|
-
snippet =
|
|
171
|
+
snippet = `...${snippet}`;
|
|
170
172
|
}
|
|
171
173
|
if (end < content.length) {
|
|
172
|
-
snippet = snippet
|
|
174
|
+
snippet = `${snippet}...`;
|
|
173
175
|
}
|
|
174
176
|
|
|
175
177
|
return snippet;
|
|
@@ -187,5 +189,5 @@ function truncateWithEllipsis(content: string, maxLength: number): string {
|
|
|
187
189
|
const truncateAt = content.lastIndexOf(' ', maxLength - 3);
|
|
188
190
|
const cutPoint = truncateAt > maxLength * 0.5 ? truncateAt : maxLength - 3;
|
|
189
191
|
|
|
190
|
-
return content.slice(0, cutPoint).trim()
|
|
192
|
+
return `${content.slice(0, cutPoint).trim()}...`;
|
|
191
193
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
2
|
import { StoreService } from './store.service.js';
|
|
3
|
-
import { rm, mkdtemp } from 'node:fs/promises';
|
|
3
|
+
import { rm, mkdtemp, writeFile, access } from 'node:fs/promises';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
|
|
@@ -91,7 +91,7 @@ describe('StoreService', () => {
|
|
|
91
91
|
name: 'Described Files',
|
|
92
92
|
type: 'file',
|
|
93
93
|
path: tempDir,
|
|
94
|
-
description: 'My file collection'
|
|
94
|
+
description: 'My file collection',
|
|
95
95
|
});
|
|
96
96
|
|
|
97
97
|
expect(result.success).toBe(true);
|
|
@@ -105,7 +105,7 @@ describe('StoreService', () => {
|
|
|
105
105
|
name: 'Tagged Files',
|
|
106
106
|
type: 'file',
|
|
107
107
|
path: tempDir,
|
|
108
|
-
tags: ['important', 'work']
|
|
108
|
+
tags: ['important', 'work'],
|
|
109
109
|
});
|
|
110
110
|
|
|
111
111
|
expect(result.success).toBe(true);
|
|
@@ -117,7 +117,7 @@ describe('StoreService', () => {
|
|
|
117
117
|
it('returns error when path not provided for file store', async () => {
|
|
118
118
|
const result = await storeService.create({
|
|
119
119
|
name: 'No Path',
|
|
120
|
-
type: 'file'
|
|
120
|
+
type: 'file',
|
|
121
121
|
});
|
|
122
122
|
|
|
123
123
|
expect(result.success).toBe(false);
|
|
@@ -130,7 +130,7 @@ describe('StoreService', () => {
|
|
|
130
130
|
const result = await storeService.create({
|
|
131
131
|
name: 'Bad Path',
|
|
132
132
|
type: 'file',
|
|
133
|
-
path: '/nonexistent/path'
|
|
133
|
+
path: '/nonexistent/path',
|
|
134
134
|
});
|
|
135
135
|
|
|
136
136
|
expect(result.success).toBe(false);
|
|
@@ -145,7 +145,7 @@ describe('StoreService', () => {
|
|
|
145
145
|
const result = await storeService.create({
|
|
146
146
|
name: 'My Repo',
|
|
147
147
|
type: 'repo',
|
|
148
|
-
path: tempDir
|
|
148
|
+
path: tempDir,
|
|
149
149
|
});
|
|
150
150
|
|
|
151
151
|
expect(result.success).toBe(true);
|
|
@@ -160,7 +160,7 @@ describe('StoreService', () => {
|
|
|
160
160
|
name: 'Branched Repo',
|
|
161
161
|
type: 'repo',
|
|
162
162
|
path: tempDir,
|
|
163
|
-
branch: 'develop'
|
|
163
|
+
branch: 'develop',
|
|
164
164
|
});
|
|
165
165
|
|
|
166
166
|
expect(result.success).toBe(true);
|
|
@@ -172,7 +172,7 @@ describe('StoreService', () => {
|
|
|
172
172
|
it('returns error when neither path nor URL provided for repo', async () => {
|
|
173
173
|
const result = await storeService.create({
|
|
174
174
|
name: 'No Path No URL',
|
|
175
|
-
type: 'repo'
|
|
175
|
+
type: 'repo',
|
|
176
176
|
});
|
|
177
177
|
|
|
178
178
|
expect(result.success).toBe(false);
|
|
@@ -187,7 +187,7 @@ describe('StoreService', () => {
|
|
|
187
187
|
const result = await storeService.create({
|
|
188
188
|
name: 'My Website',
|
|
189
189
|
type: 'web',
|
|
190
|
-
url: 'https://example.com'
|
|
190
|
+
url: 'https://example.com',
|
|
191
191
|
});
|
|
192
192
|
|
|
193
193
|
expect(result.success).toBe(true);
|
|
@@ -202,7 +202,7 @@ describe('StoreService', () => {
|
|
|
202
202
|
name: 'Deep Site',
|
|
203
203
|
type: 'web',
|
|
204
204
|
url: 'https://example.com',
|
|
205
|
-
depth: 3
|
|
205
|
+
depth: 3,
|
|
206
206
|
});
|
|
207
207
|
|
|
208
208
|
expect(result.success).toBe(true);
|
|
@@ -215,7 +215,7 @@ describe('StoreService', () => {
|
|
|
215
215
|
const result = await storeService.create({
|
|
216
216
|
name: 'Default Depth',
|
|
217
217
|
type: 'web',
|
|
218
|
-
url: 'https://example.com'
|
|
218
|
+
url: 'https://example.com',
|
|
219
219
|
});
|
|
220
220
|
|
|
221
221
|
expect(result.success).toBe(true);
|
|
@@ -227,7 +227,7 @@ describe('StoreService', () => {
|
|
|
227
227
|
it('returns error when URL not provided for web store', async () => {
|
|
228
228
|
const result = await storeService.create({
|
|
229
229
|
name: 'No URL',
|
|
230
|
-
type: 'web'
|
|
230
|
+
type: 'web',
|
|
231
231
|
});
|
|
232
232
|
|
|
233
233
|
expect(result.success).toBe(false);
|
|
@@ -242,7 +242,7 @@ describe('StoreService', () => {
|
|
|
242
242
|
const result = await storeService.create({
|
|
243
243
|
name: '',
|
|
244
244
|
type: 'file',
|
|
245
|
-
path: tempDir
|
|
245
|
+
path: tempDir,
|
|
246
246
|
});
|
|
247
247
|
|
|
248
248
|
expect(result.success).toBe(false);
|
|
@@ -255,7 +255,7 @@ describe('StoreService', () => {
|
|
|
255
255
|
const result = await storeService.create({
|
|
256
256
|
name: ' ',
|
|
257
257
|
type: 'file',
|
|
258
|
-
path: tempDir
|
|
258
|
+
path: tempDir,
|
|
259
259
|
});
|
|
260
260
|
|
|
261
261
|
expect(result.success).toBe(false);
|
|
@@ -273,13 +273,13 @@ describe('StoreService', () => {
|
|
|
273
273
|
await storeService.create({
|
|
274
274
|
name: 'Duplicate',
|
|
275
275
|
type: 'file',
|
|
276
|
-
path: dir1
|
|
276
|
+
path: dir1,
|
|
277
277
|
});
|
|
278
278
|
|
|
279
279
|
const result = await storeService.create({
|
|
280
280
|
name: 'Duplicate',
|
|
281
281
|
type: 'file',
|
|
282
|
-
path: dir2
|
|
282
|
+
path: dir2,
|
|
283
283
|
});
|
|
284
284
|
|
|
285
285
|
expect(result.success).toBe(false);
|
|
@@ -344,7 +344,7 @@ describe('StoreService', () => {
|
|
|
344
344
|
const createResult = await storeService.create({
|
|
345
345
|
name: 'Test Store',
|
|
346
346
|
type: 'file',
|
|
347
|
-
path: tempDir
|
|
347
|
+
path: tempDir,
|
|
348
348
|
});
|
|
349
349
|
|
|
350
350
|
if (!createResult.success) throw new Error('Create failed');
|
|
@@ -357,7 +357,7 @@ describe('StoreService', () => {
|
|
|
357
357
|
await storeService.create({
|
|
358
358
|
name: 'Test Store',
|
|
359
359
|
type: 'file',
|
|
360
|
-
path: tempDir
|
|
360
|
+
path: tempDir,
|
|
361
361
|
});
|
|
362
362
|
|
|
363
363
|
const store = await storeService.getByIdOrName('Test Store');
|
|
@@ -375,13 +375,13 @@ describe('StoreService', () => {
|
|
|
375
375
|
const createResult = await storeService.create({
|
|
376
376
|
name: 'Original Name',
|
|
377
377
|
type: 'file',
|
|
378
|
-
path: tempDir
|
|
378
|
+
path: tempDir,
|
|
379
379
|
});
|
|
380
380
|
|
|
381
381
|
if (!createResult.success) throw new Error('Create failed');
|
|
382
382
|
|
|
383
383
|
const updateResult = await storeService.update(createResult.data.id, {
|
|
384
|
-
name: 'Updated Name'
|
|
384
|
+
name: 'Updated Name',
|
|
385
385
|
});
|
|
386
386
|
|
|
387
387
|
expect(updateResult.success).toBe(true);
|
|
@@ -394,13 +394,13 @@ describe('StoreService', () => {
|
|
|
394
394
|
const createResult = await storeService.create({
|
|
395
395
|
name: 'Test Store',
|
|
396
396
|
type: 'file',
|
|
397
|
-
path: tempDir
|
|
397
|
+
path: tempDir,
|
|
398
398
|
});
|
|
399
399
|
|
|
400
400
|
if (!createResult.success) throw new Error('Create failed');
|
|
401
401
|
|
|
402
402
|
const updateResult = await storeService.update(createResult.data.id, {
|
|
403
|
-
description: 'New description'
|
|
403
|
+
description: 'New description',
|
|
404
404
|
});
|
|
405
405
|
|
|
406
406
|
expect(updateResult.success).toBe(true);
|
|
@@ -413,13 +413,13 @@ describe('StoreService', () => {
|
|
|
413
413
|
const createResult = await storeService.create({
|
|
414
414
|
name: 'Test Store',
|
|
415
415
|
type: 'file',
|
|
416
|
-
path: tempDir
|
|
416
|
+
path: tempDir,
|
|
417
417
|
});
|
|
418
418
|
|
|
419
419
|
if (!createResult.success) throw new Error('Create failed');
|
|
420
420
|
|
|
421
421
|
const updateResult = await storeService.update(createResult.data.id, {
|
|
422
|
-
tags: ['new', 'tags']
|
|
422
|
+
tags: ['new', 'tags'],
|
|
423
423
|
});
|
|
424
424
|
|
|
425
425
|
expect(updateResult.success).toBe(true);
|
|
@@ -432,17 +432,17 @@ describe('StoreService', () => {
|
|
|
432
432
|
const createResult = await storeService.create({
|
|
433
433
|
name: 'Test Store',
|
|
434
434
|
type: 'file',
|
|
435
|
-
path: tempDir
|
|
435
|
+
path: tempDir,
|
|
436
436
|
});
|
|
437
437
|
|
|
438
438
|
if (!createResult.success) throw new Error('Create failed');
|
|
439
439
|
const originalUpdatedAt = createResult.data.updatedAt;
|
|
440
440
|
|
|
441
441
|
// Wait a bit to ensure timestamp is different
|
|
442
|
-
await new Promise(resolve => setTimeout(resolve, 10));
|
|
442
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
443
443
|
|
|
444
444
|
const updateResult = await storeService.update(createResult.data.id, {
|
|
445
|
-
name: 'Updated'
|
|
445
|
+
name: 'Updated',
|
|
446
446
|
});
|
|
447
447
|
|
|
448
448
|
expect(updateResult.success).toBe(true);
|
|
@@ -453,7 +453,7 @@ describe('StoreService', () => {
|
|
|
453
453
|
|
|
454
454
|
it('returns error when updating nonexistent store', async () => {
|
|
455
455
|
const result = await storeService.update('nonexistent-id' as any, {
|
|
456
|
-
name: 'Updated'
|
|
456
|
+
name: 'Updated',
|
|
457
457
|
});
|
|
458
458
|
|
|
459
459
|
expect(result.success).toBe(false);
|
|
@@ -480,7 +480,7 @@ describe('StoreService', () => {
|
|
|
480
480
|
await storeService.create({
|
|
481
481
|
name: 'Persistent Store',
|
|
482
482
|
type: 'file',
|
|
483
|
-
path: storeDir
|
|
483
|
+
path: storeDir,
|
|
484
484
|
});
|
|
485
485
|
|
|
486
486
|
// Create new service instance with same data dir
|
|
@@ -499,4 +499,43 @@ describe('StoreService', () => {
|
|
|
499
499
|
expect(stores).toHaveLength(0);
|
|
500
500
|
});
|
|
501
501
|
});
|
|
502
|
+
|
|
503
|
+
describe('first-run vs corruption handling (CLAUDE.md compliance)', () => {
|
|
504
|
+
it('creates stores.json file on first run', async () => {
|
|
505
|
+
const freshDir = await mkdtemp(join(tmpdir(), 'fresh-'));
|
|
506
|
+
const freshService = new StoreService(freshDir);
|
|
507
|
+
await freshService.initialize();
|
|
508
|
+
|
|
509
|
+
// File should now exist
|
|
510
|
+
const registryPath = join(freshDir, 'stores.json');
|
|
511
|
+
await expect(access(registryPath)).resolves.toBeUndefined();
|
|
512
|
+
|
|
513
|
+
await rm(freshDir, { recursive: true, force: true });
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
it('throws on corrupted stores.json', async () => {
|
|
517
|
+
const corruptDir = await mkdtemp(join(tmpdir(), 'corrupt-'));
|
|
518
|
+
const registryPath = join(corruptDir, 'stores.json');
|
|
519
|
+
await writeFile(registryPath, '{invalid json syntax');
|
|
520
|
+
|
|
521
|
+
const freshService = new StoreService(corruptDir);
|
|
522
|
+
|
|
523
|
+
// Should throw per CLAUDE.md "fail early and fast"
|
|
524
|
+
await expect(freshService.initialize()).rejects.toThrow();
|
|
525
|
+
|
|
526
|
+
await rm(corruptDir, { recursive: true, force: true });
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it('throws with descriptive message on JSON parse error', async () => {
|
|
530
|
+
const corruptDir = await mkdtemp(join(tmpdir(), 'corrupt-'));
|
|
531
|
+
const registryPath = join(corruptDir, 'stores.json');
|
|
532
|
+
await writeFile(registryPath, '{"stores": [');
|
|
533
|
+
|
|
534
|
+
const freshService = new StoreService(corruptDir);
|
|
535
|
+
|
|
536
|
+
await expect(freshService.initialize()).rejects.toThrow(/JSON|parse|registry/i);
|
|
537
|
+
|
|
538
|
+
await rm(corruptDir, { recursive: true, force: true });
|
|
539
|
+
});
|
|
540
|
+
});
|
|
502
541
|
});
|
|
@@ -1,12 +1,24 @@
|
|
|
1
|
-
import { readFile, writeFile, mkdir, stat } from 'node:fs/promises';
|
|
2
|
-
import { join, resolve } from 'node:path';
|
|
3
1
|
import { randomUUID } from 'node:crypto';
|
|
4
|
-
import
|
|
5
|
-
import
|
|
2
|
+
import { readFile, writeFile, mkdir, stat, access } from 'node:fs/promises';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { cloneRepository } from '../plugin/git-clone.js';
|
|
6
5
|
import { createStoreId } from '../types/brands.js';
|
|
7
|
-
import type { Result } from '../types/result.js';
|
|
8
6
|
import { ok, err } from '../types/result.js';
|
|
9
|
-
import {
|
|
7
|
+
import type { StoreId } from '../types/brands.js';
|
|
8
|
+
import type { Result } from '../types/result.js';
|
|
9
|
+
import type { Store, FileStore, RepoStore, WebStore, StoreType } from '../types/store.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Check if a file exists
|
|
13
|
+
*/
|
|
14
|
+
async function fileExists(path: string): Promise<boolean> {
|
|
15
|
+
try {
|
|
16
|
+
await access(path);
|
|
17
|
+
return true;
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
10
22
|
|
|
11
23
|
export interface CreateStoreInput {
|
|
12
24
|
name: string;
|
|
@@ -91,7 +103,7 @@ export class StoreService {
|
|
|
91
103
|
url: input.url,
|
|
92
104
|
targetDir: cloneDir,
|
|
93
105
|
...(input.branch !== undefined ? { branch: input.branch } : {}),
|
|
94
|
-
depth: input.depth ?? 1
|
|
106
|
+
depth: input.depth ?? 1,
|
|
95
107
|
});
|
|
96
108
|
|
|
97
109
|
if (!result.success) {
|
|
@@ -164,10 +176,15 @@ export class StoreService {
|
|
|
164
176
|
}
|
|
165
177
|
|
|
166
178
|
async getByIdOrName(idOrName: string): Promise<Store | undefined> {
|
|
167
|
-
return Promise.resolve(
|
|
179
|
+
return Promise.resolve(
|
|
180
|
+
this.registry.stores.find((s) => s.id === idOrName || s.name === idOrName)
|
|
181
|
+
);
|
|
168
182
|
}
|
|
169
183
|
|
|
170
|
-
async update(
|
|
184
|
+
async update(
|
|
185
|
+
id: StoreId,
|
|
186
|
+
updates: Partial<Pick<Store, 'name' | 'description' | 'tags'>>
|
|
187
|
+
): Promise<Result<Store>> {
|
|
171
188
|
const index = this.registry.stores.findIndex((s) => s.id === id);
|
|
172
189
|
if (index === -1) {
|
|
173
190
|
return err(new Error(`Store not found: ${id}`));
|
|
@@ -205,8 +222,18 @@ export class StoreService {
|
|
|
205
222
|
|
|
206
223
|
private async loadRegistry(): Promise<void> {
|
|
207
224
|
const registryPath = join(this.dataDir, 'stores.json');
|
|
225
|
+
const exists = await fileExists(registryPath);
|
|
226
|
+
|
|
227
|
+
if (!exists) {
|
|
228
|
+
// First run - create empty registry
|
|
229
|
+
this.registry = { stores: [] };
|
|
230
|
+
await this.saveRegistry();
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// File exists - load it (throws on corruption per CLAUDE.md "fail early")
|
|
235
|
+
const content = await readFile(registryPath, 'utf-8');
|
|
208
236
|
try {
|
|
209
|
-
const content = await readFile(registryPath, 'utf-8');
|
|
210
237
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
211
238
|
const data = JSON.parse(content) as { stores: Store[] };
|
|
212
239
|
this.registry = {
|
|
@@ -217,8 +244,10 @@ export class StoreService {
|
|
|
217
244
|
updatedAt: new Date(s.updatedAt),
|
|
218
245
|
})),
|
|
219
246
|
};
|
|
220
|
-
} catch {
|
|
221
|
-
|
|
247
|
+
} catch (error) {
|
|
248
|
+
throw new Error(
|
|
249
|
+
`Failed to parse store registry at ${registryPath}: ${error instanceof Error ? error.message : String(error)}`
|
|
250
|
+
);
|
|
222
251
|
}
|
|
223
252
|
}
|
|
224
253
|
|
|
@@ -80,11 +80,14 @@ describe('WatchService', () => {
|
|
|
80
80
|
|
|
81
81
|
await watchService.watch(mockFileStore);
|
|
82
82
|
|
|
83
|
-
expect(watch).toHaveBeenCalledWith(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
83
|
+
expect(watch).toHaveBeenCalledWith(
|
|
84
|
+
mockFileStore.path,
|
|
85
|
+
expect.objectContaining({
|
|
86
|
+
ignored: expect.any(RegExp),
|
|
87
|
+
persistent: true,
|
|
88
|
+
ignoreInitial: true,
|
|
89
|
+
})
|
|
90
|
+
);
|
|
88
91
|
});
|
|
89
92
|
|
|
90
93
|
it('starts watching a repo store', async () => {
|
|
@@ -343,10 +346,7 @@ describe('WatchService', () => {
|
|
|
343
346
|
vi.advanceTimersByTime(1100);
|
|
344
347
|
await vi.runAllTimersAsync();
|
|
345
348
|
|
|
346
|
-
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
347
|
-
'Error during reindexing:',
|
|
348
|
-
expect.any(Error)
|
|
349
|
-
);
|
|
349
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith('Error during reindexing:', expect.any(Error));
|
|
350
350
|
|
|
351
351
|
consoleErrorSpy.mockRestore();
|
|
352
352
|
});
|
|
@@ -367,10 +367,7 @@ describe('WatchService', () => {
|
|
|
367
367
|
vi.advanceTimersByTime(1100);
|
|
368
368
|
await vi.runAllTimersAsync();
|
|
369
369
|
|
|
370
|
-
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
371
|
-
'Error during reindexing:',
|
|
372
|
-
expect.any(Error)
|
|
373
|
-
);
|
|
370
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith('Error during reindexing:', expect.any(Error));
|
|
374
371
|
|
|
375
372
|
consoleErrorSpy.mockRestore();
|
|
376
373
|
});
|
|
@@ -397,7 +394,8 @@ describe('WatchService', () => {
|
|
|
397
394
|
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
398
395
|
|
|
399
396
|
// First call fails
|
|
400
|
-
mockIndexService.indexStore = vi
|
|
397
|
+
mockIndexService.indexStore = vi
|
|
398
|
+
.fn()
|
|
401
399
|
.mockRejectedValueOnce(new Error('First fail'))
|
|
402
400
|
.mockResolvedValueOnce({ ok: true });
|
|
403
401
|
|
|
@@ -464,6 +462,28 @@ describe('WatchService', () => {
|
|
|
464
462
|
expect(watcher1?.close).toHaveBeenCalled();
|
|
465
463
|
expect(watcher2?.close).not.toHaveBeenCalled();
|
|
466
464
|
});
|
|
465
|
+
|
|
466
|
+
it('clears pending timeout to prevent timer leak', async () => {
|
|
467
|
+
await watchService.watch(mockFileStore, 1000);
|
|
468
|
+
|
|
469
|
+
const watcher = mockWatchers[0];
|
|
470
|
+
const allHandler = (watcher?.on as ReturnType<typeof vi.fn>).mock.calls.find(
|
|
471
|
+
(call: unknown[]) => call[0] === 'all'
|
|
472
|
+
)?.[1] as (() => void) | undefined;
|
|
473
|
+
|
|
474
|
+
// Trigger a file change (sets timeout)
|
|
475
|
+
allHandler?.();
|
|
476
|
+
|
|
477
|
+
// Unwatch before timeout fires
|
|
478
|
+
await watchService.unwatch(mockFileStore.id);
|
|
479
|
+
|
|
480
|
+
// Advance past debounce time - timeout should NOT fire
|
|
481
|
+
vi.advanceTimersByTime(1500);
|
|
482
|
+
await vi.runAllTimersAsync();
|
|
483
|
+
|
|
484
|
+
// indexStore should NOT have been called since we unwatched
|
|
485
|
+
expect(mockIndexService.indexStore).not.toHaveBeenCalled();
|
|
486
|
+
});
|
|
467
487
|
});
|
|
468
488
|
|
|
469
489
|
describe('unwatchAll', () => {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { watch, type FSWatcher } from 'chokidar';
|
|
2
|
-
import type { FileStore, RepoStore } from '../types/store.js';
|
|
3
2
|
import type { IndexService } from './index.service.js';
|
|
4
3
|
import type { LanceStore } from '../db/lance.js';
|
|
4
|
+
import type { FileStore, RepoStore } from '../types/store.js';
|
|
5
5
|
|
|
6
6
|
export class WatchService {
|
|
7
7
|
private readonly watchers: Map<string, FSWatcher> = new Map();
|
|
8
|
+
private readonly pendingTimeouts: Map<string, NodeJS.Timeout> = new Map();
|
|
8
9
|
private readonly indexService: IndexService;
|
|
9
10
|
private readonly lanceStore: LanceStore;
|
|
10
11
|
|
|
@@ -33,6 +34,7 @@ export class WatchService {
|
|
|
33
34
|
const reindexHandler = (): void => {
|
|
34
35
|
if (timeout) clearTimeout(timeout);
|
|
35
36
|
timeout = setTimeout(() => {
|
|
37
|
+
this.pendingTimeouts.delete(store.id);
|
|
36
38
|
void (async (): Promise<void> => {
|
|
37
39
|
try {
|
|
38
40
|
await this.lanceStore.initialize(store.id);
|
|
@@ -43,6 +45,7 @@ export class WatchService {
|
|
|
43
45
|
}
|
|
44
46
|
})();
|
|
45
47
|
}, debounceMs);
|
|
48
|
+
this.pendingTimeouts.set(store.id, timeout);
|
|
46
49
|
};
|
|
47
50
|
|
|
48
51
|
watcher.on('all', reindexHandler);
|
|
@@ -56,6 +59,13 @@ export class WatchService {
|
|
|
56
59
|
}
|
|
57
60
|
|
|
58
61
|
async unwatch(storeId: string): Promise<void> {
|
|
62
|
+
// Clear any pending timeout to prevent timer leak
|
|
63
|
+
const pendingTimeout = this.pendingTimeouts.get(storeId);
|
|
64
|
+
if (pendingTimeout) {
|
|
65
|
+
clearTimeout(pendingTimeout);
|
|
66
|
+
this.pendingTimeouts.delete(storeId);
|
|
67
|
+
}
|
|
68
|
+
|
|
59
69
|
const watcher = this.watchers.get(storeId);
|
|
60
70
|
if (watcher) {
|
|
61
71
|
await watcher.close();
|
package/src/types/brands.test.ts
CHANGED
|
@@ -15,7 +15,9 @@ describe('branded types', () => {
|
|
|
15
15
|
});
|
|
16
16
|
|
|
17
17
|
it('throws error for invalid store ID', () => {
|
|
18
|
-
expect(() => createStoreId('invalid id with spaces')).toThrow(
|
|
18
|
+
expect(() => createStoreId('invalid id with spaces')).toThrow(
|
|
19
|
+
'Invalid store ID: invalid id with spaces'
|
|
20
|
+
);
|
|
19
21
|
});
|
|
20
22
|
|
|
21
23
|
it('throws error for empty store ID', () => {
|
package/src/types/index.ts
CHANGED
|
@@ -9,15 +9,7 @@ export {
|
|
|
9
9
|
} from './brands.js';
|
|
10
10
|
|
|
11
11
|
// Result type
|
|
12
|
-
export {
|
|
13
|
-
type Result,
|
|
14
|
-
ok,
|
|
15
|
-
err,
|
|
16
|
-
isOk,
|
|
17
|
-
isErr,
|
|
18
|
-
unwrap,
|
|
19
|
-
unwrapOr,
|
|
20
|
-
} from './result.js';
|
|
12
|
+
export { type Result, ok, err, isOk, isErr, unwrap, unwrapOr } from './result.js';
|
|
21
13
|
|
|
22
14
|
// Store types
|
|
23
15
|
export {
|
|
@@ -60,7 +52,4 @@ export {
|
|
|
60
52
|
} from './config.js';
|
|
61
53
|
|
|
62
54
|
// Progress types
|
|
63
|
-
export {
|
|
64
|
-
type ProgressEvent,
|
|
65
|
-
type ProgressCallback,
|
|
66
|
-
} from './progress.js';
|
|
55
|
+
export { type ProgressEvent, type ProgressCallback } from './progress.js';
|
package/src/types/search.ts
CHANGED
|
@@ -75,14 +75,16 @@ export interface SearchResult {
|
|
|
75
75
|
readonly full?: ResultFull | undefined;
|
|
76
76
|
|
|
77
77
|
// Ranking attribution metadata for transparency
|
|
78
|
-
readonly rankingMetadata?:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
78
|
+
readonly rankingMetadata?:
|
|
79
|
+
| {
|
|
80
|
+
readonly vectorRank?: number; // Position in vector results (1-based)
|
|
81
|
+
readonly ftsRank?: number; // Position in FTS results (1-based)
|
|
82
|
+
readonly vectorRRF: number; // Vector contribution to RRF score
|
|
83
|
+
readonly ftsRRF: number; // FTS contribution to RRF score
|
|
84
|
+
readonly fileTypeBoost: number; // File type multiplier applied
|
|
85
|
+
readonly frameworkBoost: number; // Framework context multiplier
|
|
86
|
+
}
|
|
87
|
+
| undefined;
|
|
86
88
|
}
|
|
87
89
|
|
|
88
90
|
export interface SearchResponse {
|