bluera-knowledge 0.9.32 → 0.9.36

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 (198) hide show
  1. package/.claude/hooks/post-edit-check.sh +5 -3
  2. package/.claude/skills/atomic-commits/SKILL.md +3 -1
  3. package/.husky/pre-commit +3 -2
  4. package/.prettierrc +9 -0
  5. package/.versionrc.json +1 -1
  6. package/CHANGELOG.md +70 -0
  7. package/CLAUDE.md +6 -0
  8. package/README.md +25 -13
  9. package/bun.lock +277 -33
  10. package/dist/{chunk-L2YVNC63.js → chunk-6FHWC36B.js} +9 -1
  11. package/dist/chunk-6FHWC36B.js.map +1 -0
  12. package/dist/{chunk-RST4XGRL.js → chunk-DC7CGSGT.js} +288 -241
  13. package/dist/chunk-DC7CGSGT.js.map +1 -0
  14. package/dist/{chunk-6PBP5DVD.js → chunk-WFNPNAAP.js} +3212 -3054
  15. package/dist/chunk-WFNPNAAP.js.map +1 -0
  16. package/dist/{chunk-WT2DAEO7.js → chunk-Z2KKVH45.js} +548 -482
  17. package/dist/chunk-Z2KKVH45.js.map +1 -0
  18. package/dist/index.js +871 -758
  19. package/dist/index.js.map +1 -1
  20. package/dist/mcp/server.js +3 -3
  21. package/dist/watch.service-BJV3TI3F.js +7 -0
  22. package/dist/workers/background-worker-cli.js +97 -71
  23. package/dist/workers/background-worker-cli.js.map +1 -1
  24. package/eslint.config.js +43 -1
  25. package/package.json +18 -11
  26. package/plugin.json +8 -0
  27. package/python/requirements.txt +1 -1
  28. package/src/analysis/ast-parser.test.ts +12 -11
  29. package/src/analysis/ast-parser.ts +28 -22
  30. package/src/analysis/code-graph.test.ts +52 -62
  31. package/src/analysis/code-graph.ts +9 -13
  32. package/src/analysis/dependency-usage-analyzer.test.ts +91 -271
  33. package/src/analysis/dependency-usage-analyzer.ts +52 -24
  34. package/src/analysis/go-ast-parser.test.ts +22 -22
  35. package/src/analysis/go-ast-parser.ts +18 -25
  36. package/src/analysis/parser-factory.test.ts +9 -9
  37. package/src/analysis/parser-factory.ts +3 -3
  38. package/src/analysis/python-ast-parser.test.ts +27 -27
  39. package/src/analysis/python-ast-parser.ts +2 -2
  40. package/src/analysis/repo-url-resolver.test.ts +82 -82
  41. package/src/analysis/rust-ast-parser.test.ts +19 -19
  42. package/src/analysis/rust-ast-parser.ts +17 -27
  43. package/src/analysis/tree-sitter-parser.test.ts +3 -3
  44. package/src/analysis/tree-sitter-parser.ts +10 -16
  45. package/src/cli/commands/crawl.test.ts +40 -24
  46. package/src/cli/commands/crawl.ts +186 -166
  47. package/src/cli/commands/index-cmd.test.ts +90 -90
  48. package/src/cli/commands/index-cmd.ts +52 -36
  49. package/src/cli/commands/mcp.test.ts +6 -6
  50. package/src/cli/commands/mcp.ts +2 -2
  51. package/src/cli/commands/plugin-api.test.ts +16 -18
  52. package/src/cli/commands/plugin-api.ts +9 -6
  53. package/src/cli/commands/search.test.ts +16 -7
  54. package/src/cli/commands/search.ts +124 -87
  55. package/src/cli/commands/serve.test.ts +67 -25
  56. package/src/cli/commands/serve.ts +18 -3
  57. package/src/cli/commands/setup.test.ts +176 -101
  58. package/src/cli/commands/setup.ts +140 -117
  59. package/src/cli/commands/store.test.ts +82 -53
  60. package/src/cli/commands/store.ts +56 -37
  61. package/src/cli/program.ts +2 -2
  62. package/src/crawl/article-converter.test.ts +4 -1
  63. package/src/crawl/article-converter.ts +46 -31
  64. package/src/crawl/bridge.test.ts +240 -132
  65. package/src/crawl/bridge.ts +87 -30
  66. package/src/crawl/claude-client.test.ts +124 -56
  67. package/src/crawl/claude-client.ts +7 -15
  68. package/src/crawl/intelligent-crawler.test.ts +65 -22
  69. package/src/crawl/intelligent-crawler.ts +86 -53
  70. package/src/crawl/markdown-utils.ts +1 -4
  71. package/src/db/embeddings.ts +4 -6
  72. package/src/db/lance.test.ts +4 -4
  73. package/src/db/lance.ts +16 -12
  74. package/src/index.ts +26 -17
  75. package/src/logging/index.ts +1 -5
  76. package/src/logging/logger.ts +3 -5
  77. package/src/logging/payload.test.ts +1 -1
  78. package/src/logging/payload.ts +3 -5
  79. package/src/mcp/commands/index.ts +2 -2
  80. package/src/mcp/commands/job.commands.ts +12 -18
  81. package/src/mcp/commands/meta.commands.ts +13 -13
  82. package/src/mcp/commands/registry.ts +5 -8
  83. package/src/mcp/commands/store.commands.ts +19 -19
  84. package/src/mcp/handlers/execute.handler.test.ts +10 -10
  85. package/src/mcp/handlers/execute.handler.ts +4 -5
  86. package/src/mcp/handlers/index.ts +10 -14
  87. package/src/mcp/handlers/job.handler.test.ts +10 -10
  88. package/src/mcp/handlers/job.handler.ts +22 -25
  89. package/src/mcp/handlers/search.handler.test.ts +36 -65
  90. package/src/mcp/handlers/search.handler.ts +135 -104
  91. package/src/mcp/handlers/store.handler.test.ts +41 -52
  92. package/src/mcp/handlers/store.handler.ts +108 -88
  93. package/src/mcp/schemas/index.test.ts +73 -68
  94. package/src/mcp/schemas/index.ts +18 -12
  95. package/src/mcp/server.test.ts +1 -1
  96. package/src/mcp/server.ts +59 -46
  97. package/src/plugin/commands.test.ts +230 -95
  98. package/src/plugin/commands.ts +24 -25
  99. package/src/plugin/dependency-analyzer.test.ts +52 -52
  100. package/src/plugin/dependency-analyzer.ts +85 -22
  101. package/src/plugin/git-clone.test.ts +24 -13
  102. package/src/plugin/git-clone.ts +3 -7
  103. package/src/server/app.test.ts +109 -109
  104. package/src/server/app.ts +32 -23
  105. package/src/server/index.test.ts +64 -66
  106. package/src/services/chunking.service.test.ts +32 -32
  107. package/src/services/chunking.service.ts +16 -9
  108. package/src/services/code-graph.service.test.ts +30 -36
  109. package/src/services/code-graph.service.ts +24 -10
  110. package/src/services/code-unit.service.test.ts +55 -11
  111. package/src/services/code-unit.service.ts +85 -11
  112. package/src/services/config.service.test.ts +37 -18
  113. package/src/services/config.service.ts +30 -7
  114. package/src/services/index.service.test.ts +49 -18
  115. package/src/services/index.service.ts +98 -48
  116. package/src/services/index.ts +6 -9
  117. package/src/services/job.service.test.ts +22 -22
  118. package/src/services/job.service.ts +18 -18
  119. package/src/services/project-root.service.test.ts +1 -3
  120. package/src/services/search.service.test.ts +248 -120
  121. package/src/services/search.service.ts +286 -156
  122. package/src/services/services.test.ts +1 -1
  123. package/src/services/snippet.service.test.ts +14 -6
  124. package/src/services/snippet.service.ts +7 -5
  125. package/src/services/store.service.test.ts +68 -29
  126. package/src/services/store.service.ts +41 -12
  127. package/src/services/watch.service.test.ts +34 -14
  128. package/src/services/watch.service.ts +11 -1
  129. package/src/types/brands.test.ts +3 -1
  130. package/src/types/index.ts +2 -13
  131. package/src/types/search.ts +10 -8
  132. package/src/utils/type-guards.test.ts +20 -15
  133. package/src/utils/type-guards.ts +1 -1
  134. package/src/workers/background-worker-cli.ts +28 -30
  135. package/src/workers/background-worker.test.ts +54 -40
  136. package/src/workers/background-worker.ts +76 -60
  137. package/src/workers/pid-file.test.ts +167 -0
  138. package/src/workers/pid-file.ts +82 -0
  139. package/src/workers/spawn-worker.test.ts +22 -10
  140. package/src/workers/spawn-worker.ts +6 -6
  141. package/tests/analysis/ast-parser.test.ts +3 -3
  142. package/tests/analysis/code-graph.test.ts +5 -5
  143. package/tests/fixtures/code-snippets/api/error-handling.ts +4 -15
  144. package/tests/fixtures/code-snippets/api/rest-controller.ts +3 -9
  145. package/tests/fixtures/code-snippets/auth/jwt-auth.ts +5 -21
  146. package/tests/fixtures/code-snippets/auth/oauth-flow.ts +4 -4
  147. package/tests/fixtures/code-snippets/database/repository-pattern.ts +11 -3
  148. package/tests/fixtures/corpus/oss-repos/hono/src/adapter/aws-lambda/handler.ts +2 -2
  149. package/tests/fixtures/corpus/oss-repos/hono/src/adapter/cloudflare-pages/handler.ts +1 -1
  150. package/tests/fixtures/corpus/oss-repos/hono/src/adapter/cloudflare-workers/serve-static.ts +2 -2
  151. package/tests/fixtures/corpus/oss-repos/hono/src/client/client.ts +2 -2
  152. package/tests/fixtures/corpus/oss-repos/hono/src/client/types.ts +22 -20
  153. package/tests/fixtures/corpus/oss-repos/hono/src/context.ts +13 -10
  154. package/tests/fixtures/corpus/oss-repos/hono/src/helper/accepts/accepts.ts +10 -7
  155. package/tests/fixtures/corpus/oss-repos/hono/src/helper/adapter/index.ts +2 -2
  156. package/tests/fixtures/corpus/oss-repos/hono/src/helper/css/index.ts +1 -1
  157. package/tests/fixtures/corpus/oss-repos/hono/src/helper/factory/index.ts +16 -16
  158. package/tests/fixtures/corpus/oss-repos/hono/src/helper/ssg/ssg.ts +2 -2
  159. package/tests/fixtures/corpus/oss-repos/hono/src/hono-base.ts +3 -3
  160. package/tests/fixtures/corpus/oss-repos/hono/src/hono.ts +1 -1
  161. package/tests/fixtures/corpus/oss-repos/hono/src/jsx/dom/css.ts +2 -2
  162. package/tests/fixtures/corpus/oss-repos/hono/src/jsx/dom/intrinsic-element/components.ts +1 -1
  163. package/tests/fixtures/corpus/oss-repos/hono/src/jsx/dom/render.ts +7 -7
  164. package/tests/fixtures/corpus/oss-repos/hono/src/jsx/hooks/index.ts +3 -3
  165. package/tests/fixtures/corpus/oss-repos/hono/src/jsx/intrinsic-element/components.ts +1 -1
  166. package/tests/fixtures/corpus/oss-repos/hono/src/jsx/utils.ts +6 -6
  167. package/tests/fixtures/corpus/oss-repos/hono/src/middleware/jsx-renderer/index.ts +3 -3
  168. package/tests/fixtures/corpus/oss-repos/hono/src/middleware/serve-static/index.ts +1 -1
  169. package/tests/fixtures/corpus/oss-repos/hono/src/preset/quick.ts +1 -1
  170. package/tests/fixtures/corpus/oss-repos/hono/src/preset/tiny.ts +1 -1
  171. package/tests/fixtures/corpus/oss-repos/hono/src/router/pattern-router/router.ts +2 -2
  172. package/tests/fixtures/corpus/oss-repos/hono/src/router/reg-exp-router/node.ts +4 -4
  173. package/tests/fixtures/corpus/oss-repos/hono/src/router/reg-exp-router/router.ts +1 -1
  174. package/tests/fixtures/corpus/oss-repos/hono/src/router/trie-router/node.ts +1 -1
  175. package/tests/fixtures/corpus/oss-repos/hono/src/types.ts +166 -169
  176. package/tests/fixtures/corpus/oss-repos/hono/src/utils/body.ts +8 -8
  177. package/tests/fixtures/corpus/oss-repos/hono/src/utils/color.ts +3 -3
  178. package/tests/fixtures/corpus/oss-repos/hono/src/utils/cookie.ts +2 -2
  179. package/tests/fixtures/corpus/oss-repos/hono/src/utils/encode.ts +2 -2
  180. package/tests/fixtures/corpus/oss-repos/hono/src/utils/types.ts +30 -33
  181. package/tests/fixtures/corpus/oss-repos/hono/src/validator/validator.ts +2 -2
  182. package/tests/fixtures/test-server.ts +3 -2
  183. package/tests/helpers/performance-metrics.ts +8 -25
  184. package/tests/helpers/search-relevance.ts +14 -69
  185. package/tests/integration/cli-consistency.test.ts +6 -5
  186. package/tests/integration/python-bridge.test.ts +13 -3
  187. package/tests/mcp/server.test.ts +1 -1
  188. package/tests/services/code-unit.service.test.ts +48 -0
  189. package/tests/services/job.service.test.ts +124 -0
  190. package/tests/services/search.progressive-context.test.ts +2 -2
  191. package/.claude-plugin/plugin.json +0 -13
  192. package/dist/chunk-6PBP5DVD.js.map +0 -1
  193. package/dist/chunk-L2YVNC63.js.map +0 -1
  194. package/dist/chunk-RST4XGRL.js.map +0 -1
  195. package/dist/chunk-WT2DAEO7.js.map +0 -1
  196. package/dist/watch.service-YAIKKDCF.js +0 -7
  197. package/skills/atomic-commits/SKILL.md +0 -77
  198. /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 = 'The quick brown fox jumps over the lazy dog. ' +
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 = 'Unrelated text at the start. '.repeat(10) +
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 = 'First occurrence of term. ' +
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 = 'distant word here. ' + 'A'.repeat(300) +
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 = 'repeated repeated repeated. ' +
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', { maxLength: 100 });
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.slice(Math.max(0, position - 100), position).match(/^#{1,3}\s+.+$/m);
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 = '...' + 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 type { Store, FileStore, RepoStore, WebStore, StoreType } from '../types/store.js';
5
- import type { StoreId } from '../types/brands.js';
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 { cloneRepository } from '../plugin/git-clone.js';
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(this.registry.stores.find((s) => s.id === idOrName || s.name === idOrName));
179
+ return Promise.resolve(
180
+ this.registry.stores.find((s) => s.id === idOrName || s.name === idOrName)
181
+ );
168
182
  }
169
183
 
170
- async update(id: StoreId, updates: Partial<Pick<Store, 'name' | 'description' | 'tags'>>): Promise<Result<Store>> {
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
- this.registry = { stores: [] };
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(mockFileStore.path, expect.objectContaining({
84
- ignored: expect.any(RegExp),
85
- persistent: true,
86
- ignoreInitial: true,
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.fn()
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();
@@ -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('Invalid store ID: invalid id with spaces');
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', () => {
@@ -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';
@@ -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
- readonly vectorRank?: number; // Position in vector results (1-based)
80
- readonly ftsRank?: number; // Position in FTS results (1-based)
81
- readonly vectorRRF: number; // Vector contribution to RRF score
82
- readonly ftsRRF: number; // FTS contribution to RRF score
83
- readonly fileTypeBoost: number; // File type multiplier applied
84
- readonly frameworkBoost: number; // Framework context multiplier
85
- } | undefined;
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 {