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
@@ -4,7 +4,7 @@ import type { GlobalOptions } from '../program.js';
4
4
 
5
5
  // Mock all dependencies
6
6
  vi.mock('../../mcp/server.js', () => ({
7
- runMCPServer: vi.fn()
7
+ runMCPServer: vi.fn(),
8
8
  }));
9
9
 
10
10
  describe('MCP Command - Execution Tests', () => {
@@ -35,7 +35,7 @@ describe('MCP Command - Execution Tests', () => {
35
35
 
36
36
  expect(runMCPServer).toHaveBeenCalledWith({
37
37
  dataDir: '/tmp/test-data',
38
- config: undefined
38
+ config: undefined,
39
39
  });
40
40
  });
41
41
 
@@ -55,7 +55,7 @@ describe('MCP Command - Execution Tests', () => {
55
55
 
56
56
  expect(runMCPServer).toHaveBeenCalledWith({
57
57
  dataDir: '/tmp/test-data',
58
- config: '/custom/config.json'
58
+ config: '/custom/config.json',
59
59
  });
60
60
  });
61
61
 
@@ -75,7 +75,7 @@ describe('MCP Command - Execution Tests', () => {
75
75
 
76
76
  expect(runMCPServer).toHaveBeenCalledWith({
77
77
  dataDir: '/custom/data',
78
- config: undefined
78
+ config: undefined,
79
79
  });
80
80
  });
81
81
 
@@ -95,7 +95,7 @@ describe('MCP Command - Execution Tests', () => {
95
95
 
96
96
  expect(runMCPServer).toHaveBeenCalledWith({
97
97
  dataDir: '/custom/data',
98
- config: '/custom/config.json'
98
+ config: '/custom/config.json',
99
99
  });
100
100
  });
101
101
  });
@@ -204,7 +204,7 @@ describe('MCP Command - Execution Tests', () => {
204
204
  let serverStarted = false;
205
205
 
206
206
  vi.mocked(runMCPServer).mockImplementation(async () => {
207
- await new Promise(resolve => setTimeout(resolve, 10));
207
+ await new Promise((resolve) => setTimeout(resolve, 10));
208
208
  serverStarted = true;
209
209
  });
210
210
 
@@ -1,6 +1,6 @@
1
1
  import { Command } from 'commander';
2
- import type { GlobalOptions } from '../program.js';
3
2
  import { runMCPServer } from '../../mcp/server.js';
3
+ import type { GlobalOptions } from '../program.js';
4
4
 
5
5
  export function createMCPCommand(getOptions: () => GlobalOptions): Command {
6
6
  const mcp = new Command('mcp')
@@ -10,7 +10,7 @@ export function createMCPCommand(getOptions: () => GlobalOptions): Command {
10
10
 
11
11
  await runMCPServer({
12
12
  dataDir: opts.dataDir,
13
- config: opts.config
13
+ config: opts.config,
14
14
  });
15
15
  });
16
16
 
@@ -12,7 +12,7 @@ vi.mock('../../plugin/commands.js', () => ({
12
12
  handleAddRepo: vi.fn(),
13
13
  handleAddFolder: vi.fn(),
14
14
  handleStores: vi.fn(),
15
- handleSuggest: vi.fn()
15
+ handleSuggest: vi.fn(),
16
16
  }));
17
17
 
18
18
  describe('Plugin API Commands - Execution Tests', () => {
@@ -24,12 +24,8 @@ describe('Plugin API Commands - Execution Tests', () => {
24
24
  });
25
25
 
26
26
  beforeEach(async () => {
27
- const {
28
- handleAddRepo,
29
- handleAddFolder,
30
- handleStores,
31
- handleSuggest
32
- } = await import('../../plugin/commands.js');
27
+ const { handleAddRepo, handleAddFolder, handleStores, handleSuggest } =
28
+ await import('../../plugin/commands.js');
33
29
 
34
30
  vi.mocked(handleAddRepo).mockClear().mockResolvedValue(undefined);
35
31
  vi.mocked(handleAddFolder).mockClear().mockResolvedValue(undefined);
@@ -50,7 +46,7 @@ describe('Plugin API Commands - Execution Tests', () => {
50
46
  await actionHandler(['https://github.com/user/repo.git']);
51
47
 
52
48
  expect(handleAddRepo).toHaveBeenCalledWith({
53
- url: 'https://github.com/user/repo.git'
49
+ url: 'https://github.com/user/repo.git',
54
50
  });
55
51
  });
56
52
 
@@ -64,7 +60,7 @@ describe('Plugin API Commands - Execution Tests', () => {
64
60
 
65
61
  expect(handleAddRepo).toHaveBeenCalledWith({
66
62
  url: 'https://github.com/user/repo.git',
67
- name: 'my-repo'
63
+ name: 'my-repo',
68
64
  });
69
65
  });
70
66
 
@@ -78,7 +74,7 @@ describe('Plugin API Commands - Execution Tests', () => {
78
74
 
79
75
  expect(handleAddRepo).toHaveBeenCalledWith({
80
76
  url: 'https://github.com/user/repo.git',
81
- branch: 'develop'
77
+ branch: 'develop',
82
78
  });
83
79
  });
84
80
 
@@ -93,7 +89,7 @@ describe('Plugin API Commands - Execution Tests', () => {
93
89
  expect(handleAddRepo).toHaveBeenCalledWith({
94
90
  url: 'https://github.com/user/repo.git',
95
91
  name: 'custom-name',
96
- branch: 'main'
92
+ branch: 'main',
97
93
  });
98
94
  });
99
95
 
@@ -105,7 +101,9 @@ describe('Plugin API Commands - Execution Tests', () => {
105
101
  const command = createAddRepoCommand(getOptions);
106
102
  const actionHandler = (command as any)._actionHandler;
107
103
 
108
- await expect(actionHandler(['https://github.com/user/repo.git'])).rejects.toThrow('Clone failed');
104
+ await expect(actionHandler(['https://github.com/user/repo.git'])).rejects.toThrow(
105
+ 'Clone failed'
106
+ );
109
107
  });
110
108
  });
111
109
 
@@ -118,7 +116,7 @@ describe('Plugin API Commands - Execution Tests', () => {
118
116
  await actionHandler(['/path/to/folder']);
119
117
 
120
118
  expect(handleAddFolder).toHaveBeenCalledWith({
121
- path: '/path/to/folder'
119
+ path: '/path/to/folder',
122
120
  });
123
121
  });
124
122
 
@@ -132,7 +130,7 @@ describe('Plugin API Commands - Execution Tests', () => {
132
130
 
133
131
  expect(handleAddFolder).toHaveBeenCalledWith({
134
132
  path: '/path/to/folder',
135
- name: 'my-folder'
133
+ name: 'my-folder',
136
134
  });
137
135
  });
138
136
 
@@ -144,7 +142,7 @@ describe('Plugin API Commands - Execution Tests', () => {
144
142
  await actionHandler(['./relative/path']);
145
143
 
146
144
  expect(handleAddFolder).toHaveBeenCalledWith({
147
- path: './relative/path'
145
+ path: './relative/path',
148
146
  });
149
147
  });
150
148
 
@@ -156,7 +154,7 @@ describe('Plugin API Commands - Execution Tests', () => {
156
154
  await actionHandler(['/path/with spaces/folder']);
157
155
 
158
156
  expect(handleAddFolder).toHaveBeenCalledWith({
159
- path: '/path/with spaces/folder'
157
+ path: '/path/with spaces/folder',
160
158
  });
161
159
  });
162
160
 
@@ -294,7 +292,7 @@ describe('Plugin API Commands - Execution Tests', () => {
294
292
  await actionHandler(['https://github.com/user/repo.git']);
295
293
 
296
294
  expect(handleAddRepo).toHaveBeenCalledWith({
297
- url: 'https://github.com/user/repo.git'
295
+ url: 'https://github.com/user/repo.git',
298
296
  });
299
297
  });
300
298
 
@@ -306,7 +304,7 @@ describe('Plugin API Commands - Execution Tests', () => {
306
304
  await actionHandler(['/path']);
307
305
 
308
306
  expect(handleAddFolder).toHaveBeenCalledWith({
309
- path: '/path'
307
+ path: '/path',
310
308
  });
311
309
  });
312
310
  });
@@ -1,5 +1,10 @@
1
1
  import { Command } from 'commander';
2
- import { handleAddRepo, handleAddFolder, handleStores, handleSuggest } from '../../plugin/commands.js';
2
+ import {
3
+ handleAddRepo,
4
+ handleAddFolder,
5
+ handleStores,
6
+ handleSuggest,
7
+ } from '../../plugin/commands.js';
3
8
  import type { GlobalOptions } from '../program.js';
4
9
 
5
10
  /**
@@ -29,11 +34,9 @@ export function createAddFolderCommand(_getOptions: () => GlobalOptions): Comman
29
34
  }
30
35
 
31
36
  export function createStoresCommand(_getOptions: () => GlobalOptions): Command {
32
- return new Command('stores')
33
- .description('List all indexed library stores')
34
- .action(async () => {
35
- await handleStores();
36
- });
37
+ return new Command('stores').description('List all indexed library stores').action(async () => {
38
+ await handleStores();
39
+ });
37
40
  }
38
41
 
39
42
  export function createSuggestCommand(_getOptions: () => GlobalOptions): Command {
@@ -374,6 +374,8 @@ describe('search command execution', () => {
374
374
 
375
375
  describe('error handling', () => {
376
376
  it('exits with code 3 when specified store not found', async () => {
377
+ const { destroyServices } = await import('../../services/index.js');
378
+
377
379
  mockServices.store.list.mockResolvedValue([]);
378
380
  mockServices.store.getByIdOrName.mockResolvedValue(undefined);
379
381
 
@@ -386,9 +388,13 @@ describe('search command execution', () => {
386
388
  expect(consoleErrorSpy).toHaveBeenCalledWith('Error: Store not found: nonexistent');
387
389
  expect(processExitSpy).toHaveBeenCalledWith(3);
388
390
  expect(mockServices.search.search).not.toHaveBeenCalled();
391
+ // Must call destroyServices before process.exit per CLAUDE.md
392
+ expect(destroyServices).toHaveBeenCalled();
389
393
  });
390
394
 
391
395
  it('exits with code 1 when no stores exist', async () => {
396
+ const { destroyServices } = await import('../../services/index.js');
397
+
392
398
  mockServices.store.list.mockResolvedValue([]);
393
399
 
394
400
  const command = createSearchCommand(getOptions);
@@ -399,9 +405,13 @@ describe('search command execution', () => {
399
405
  expect(consoleErrorSpy).toHaveBeenCalledWith('No stores to search. Create a store first.');
400
406
  expect(processExitSpy).toHaveBeenCalledWith(1);
401
407
  expect(mockServices.search.search).not.toHaveBeenCalled();
408
+ // Must call destroyServices before process.exit per CLAUDE.md
409
+ expect(destroyServices).toHaveBeenCalled();
402
410
  });
403
411
 
404
412
  it('exits with code 3 when one of multiple stores not found', async () => {
413
+ const { destroyServices } = await import('../../services/index.js');
414
+
405
415
  const mockStores = [{ id: createStoreId('store-1'), name: 'store1', type: 'file' as const }];
406
416
 
407
417
  mockServices.store.list.mockResolvedValue(mockStores);
@@ -418,6 +428,8 @@ describe('search command execution', () => {
418
428
 
419
429
  expect(consoleErrorSpy).toHaveBeenCalledWith('Error: Store not found: nonexistent');
420
430
  expect(processExitSpy).toHaveBeenCalledWith(3);
431
+ // Must call destroyServices before process.exit per CLAUDE.md
432
+ expect(destroyServices).toHaveBeenCalled();
421
433
  });
422
434
  });
423
435
 
@@ -461,9 +473,7 @@ describe('search command execution', () => {
461
473
 
462
474
  await actionHandler(['test query']);
463
475
 
464
- expect(consoleLogSpy).toHaveBeenCalledWith(
465
- expect.stringContaining('"query": "test query"')
466
- );
476
+ expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('"query": "test query"'));
467
477
  expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('"mode": "hybrid"'));
468
478
  });
469
479
 
@@ -561,9 +571,7 @@ describe('search command execution', () => {
561
571
  await actionHandler(['test query']);
562
572
 
563
573
  expect(consoleLogSpy).toHaveBeenCalledWith('\nSearch: "test query"');
564
- expect(consoleLogSpy).toHaveBeenCalledWith(
565
- expect.stringContaining('Mode: hybrid')
566
- );
574
+ expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Mode: hybrid'));
567
575
  expect(consoleLogSpy).toHaveBeenCalledWith(
568
576
  expect.stringContaining('1. [0.95] function: testFunction')
569
577
  );
@@ -580,7 +588,8 @@ describe('search command execution', () => {
580
588
  {
581
589
  id: createDocumentId('doc-1'),
582
590
  score: 0.95,
583
- content: 'This is a long piece of content that should be truncated in the preview because it exceeds the maximum length allowed for display in the search results view.',
591
+ content:
592
+ 'This is a long piece of content that should be truncated in the preview because it exceeds the maximum length allowed for display in the search results view.',
584
593
  highlight: 'This is a long piece of content',
585
594
  metadata: {
586
595
  type: 'file',
@@ -1,113 +1,150 @@
1
1
  import { Command } from 'commander';
2
2
  import { createServices, destroyServices } from '../../services/index.js';
3
- import type { GlobalOptions } from '../program.js';
4
3
  import type { SearchMode, DetailLevel } from '../../types/search.js';
4
+ import type { GlobalOptions } from '../program.js';
5
5
 
6
6
  export function createSearchCommand(getOptions: () => GlobalOptions): Command {
7
7
  const search = new Command('search')
8
8
  .description('Search indexed documents using vector similarity + full-text matching')
9
9
  .argument('<query>', 'Search query')
10
- .option('-s, --stores <stores>', 'Limit search to specific stores (comma-separated IDs or names)')
11
- .option('-m, --mode <mode>', 'vector (embeddings only), fts (text only), hybrid (both, default)', 'hybrid')
10
+ .option(
11
+ '-s, --stores <stores>',
12
+ 'Limit search to specific stores (comma-separated IDs or names)'
13
+ )
14
+ .option(
15
+ '-m, --mode <mode>',
16
+ 'vector (embeddings only), fts (text only), hybrid (both, default)',
17
+ 'hybrid'
18
+ )
12
19
  .option('-n, --limit <count>', 'Maximum results to return (default: 10)', '10')
13
20
  .option('-t, --threshold <score>', 'Minimum score 0-1; omit low-relevance results')
14
21
  .option('--include-content', 'Show full document content, not just preview snippet')
15
- .option('--detail <level>', 'Context detail: minimal, contextual, full (default: minimal)', 'minimal')
16
- .action(async (query: string, options: {
17
- stores?: string;
18
- mode?: SearchMode;
19
- limit?: string;
20
- threshold?: string;
21
- includeContent?: boolean;
22
- detail?: DetailLevel;
23
- }) => {
24
- const globalOpts = getOptions();
25
- const services = await createServices(globalOpts.config, globalOpts.dataDir);
26
- try {
27
- // Get store IDs
28
- let storeIds = (await services.store.list()).map((s) => s.id);
29
-
30
- if (options.stores !== undefined) {
31
- const requestedStores = options.stores.split(',').map((s) => s.trim());
32
- const resolvedStores = [];
33
-
34
- for (const requested of requestedStores) {
35
- const store = await services.store.getByIdOrName(requested);
36
- if (store !== undefined) {
37
- resolvedStores.push(store.id);
38
- } else {
39
- console.error(`Error: Store not found: ${requested}`);
40
- process.exit(3);
41
- }
42
- }
43
-
44
- storeIds = resolvedStores;
22
+ .option(
23
+ '--detail <level>',
24
+ 'Context detail: minimal, contextual, full (default: minimal)',
25
+ 'minimal'
26
+ )
27
+ .action(
28
+ async (
29
+ query: string,
30
+ options: {
31
+ stores?: string;
32
+ mode?: SearchMode;
33
+ limit?: string;
34
+ threshold?: string;
35
+ includeContent?: boolean;
36
+ detail?: DetailLevel;
45
37
  }
38
+ ) => {
39
+ const globalOpts = getOptions();
40
+ const services = await createServices(globalOpts.config, globalOpts.dataDir);
41
+ let exitCode = 0;
42
+ try {
43
+ // Get store IDs
44
+ let storeIds = (await services.store.list()).map((s) => s.id);
46
45
 
47
- if (storeIds.length === 0) {
48
- console.error('No stores to search. Create a store first.');
49
- process.exit(1);
50
- }
46
+ searchLogic: {
47
+ if (options.stores !== undefined) {
48
+ const requestedStores = options.stores.split(',').map((s) => s.trim());
49
+ const resolvedStores = [];
51
50
 
52
- // Initialize LanceDB for each store
53
- for (const storeId of storeIds) {
54
- await services.lance.initialize(storeId);
55
- }
51
+ for (const requested of requestedStores) {
52
+ const store = await services.store.getByIdOrName(requested);
53
+ if (store !== undefined) {
54
+ resolvedStores.push(store.id);
55
+ } else {
56
+ console.error(`Error: Store not found: ${requested}`);
57
+ exitCode = 3;
56
58
 
57
- const results = await services.search.search({
58
- query,
59
- stores: storeIds,
60
- mode: options.mode ?? 'hybrid',
61
- limit: parseInt(options.limit ?? '10', 10),
62
- threshold: options.threshold !== undefined ? parseFloat(options.threshold) : undefined,
63
- includeContent: options.includeContent,
64
- detail: options.detail ?? 'minimal',
65
- });
66
-
67
- if (globalOpts.format === 'json') {
68
- console.log(JSON.stringify(results, null, 2));
69
- } else if (globalOpts.quiet === true) {
70
- // Quiet mode: just list matching paths/URLs, one per line
71
- for (const r of results.results) {
72
- const path = r.metadata.path ?? r.metadata.url ?? 'unknown';
73
- console.log(path);
74
- }
75
- } else {
76
- console.log(`\nSearch: "${query}"`);
77
- console.log(`Mode: ${results.mode} | Detail: ${String(options.detail)} | Stores: ${String(results.stores.length)} | Results: ${String(results.totalResults)} | Time: ${String(results.timeMs)}ms\n`);
78
-
79
- if (results.results.length === 0) {
80
- console.log('No results found.\n');
81
- } else {
82
- for (let i = 0; i < results.results.length; i++) {
83
- const r = results.results[i];
84
- if (r === undefined) continue;
85
-
86
- if (r.summary) {
87
- console.log(`${String(i + 1)}. [${r.score.toFixed(2)}] ${r.summary.type}: ${r.summary.name}`);
88
- console.log(` ${r.summary.location}`);
89
- console.log(` ${r.summary.purpose}`);
90
-
91
- if (r.context && options.detail !== 'minimal') {
92
- console.log(` Imports: ${r.context.keyImports.slice(0, 3).join(', ')}`);
93
- console.log(` Related: ${r.context.relatedConcepts.slice(0, 3).join(', ')}`);
59
+ break searchLogic;
94
60
  }
61
+ }
95
62
 
96
- console.log();
97
- } else {
98
- // Fallback to old format
63
+ storeIds = resolvedStores;
64
+ }
65
+
66
+ if (storeIds.length === 0) {
67
+ console.error('No stores to search. Create a store first.');
68
+ exitCode = 1;
69
+
70
+ break searchLogic;
71
+ }
72
+
73
+ // Initialize LanceDB for each store
74
+ for (const storeId of storeIds) {
75
+ await services.lance.initialize(storeId);
76
+ }
77
+
78
+ const results = await services.search.search({
79
+ query,
80
+ stores: storeIds,
81
+ mode: options.mode ?? 'hybrid',
82
+ limit: parseInt(options.limit ?? '10', 10),
83
+ threshold:
84
+ options.threshold !== undefined ? parseFloat(options.threshold) : undefined,
85
+ includeContent: options.includeContent,
86
+ detail: options.detail ?? 'minimal',
87
+ });
88
+
89
+ if (globalOpts.format === 'json') {
90
+ console.log(JSON.stringify(results, null, 2));
91
+ } else if (globalOpts.quiet === true) {
92
+ // Quiet mode: just list matching paths/URLs, one per line
93
+ for (const r of results.results) {
99
94
  const path = r.metadata.path ?? r.metadata.url ?? 'unknown';
100
- console.log(`${String(i + 1)}. [${r.score.toFixed(2)}] ${path}`);
101
- const preview = r.highlight ?? r.content.slice(0, 150).replace(/\n/g, ' ') + (r.content.length > 150 ? '...' : '');
102
- console.log(` ${preview}\n`);
95
+ console.log(path);
96
+ }
97
+ } else {
98
+ 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
+ );
102
+
103
+ if (results.results.length === 0) {
104
+ console.log('No results found.\n');
105
+ } else {
106
+ for (let i = 0; i < results.results.length; i++) {
107
+ const r = results.results[i];
108
+ if (r === undefined) continue;
109
+
110
+ if (r.summary) {
111
+ console.log(
112
+ `${String(i + 1)}. [${r.score.toFixed(2)}] ${r.summary.type}: ${r.summary.name}`
113
+ );
114
+ console.log(` ${r.summary.location}`);
115
+ console.log(` ${r.summary.purpose}`);
116
+
117
+ if (r.context && options.detail !== 'minimal') {
118
+ console.log(` Imports: ${r.context.keyImports.slice(0, 3).join(', ')}`);
119
+ console.log(
120
+ ` Related: ${r.context.relatedConcepts.slice(0, 3).join(', ')}`
121
+ );
122
+ }
123
+
124
+ console.log();
125
+ } else {
126
+ // Display without summary
127
+ const path = r.metadata.path ?? r.metadata.url ?? 'unknown';
128
+ console.log(`${String(i + 1)}. [${r.score.toFixed(2)}] ${path}`);
129
+ const preview =
130
+ r.highlight ??
131
+ r.content.slice(0, 150).replace(/\n/g, ' ') +
132
+ (r.content.length > 150 ? '...' : '');
133
+ console.log(` ${preview}\n`);
134
+ }
135
+ }
103
136
  }
104
137
  }
105
138
  }
139
+ } finally {
140
+ await destroyServices(services);
141
+ }
142
+
143
+ if (exitCode !== 0) {
144
+ process.exit(exitCode);
106
145
  }
107
- } finally {
108
- await destroyServices(services);
109
146
  }
110
- });
147
+ );
111
148
 
112
149
  return search;
113
150
  }