bluera-knowledge 0.11.20 → 0.11.21
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-plugin/plugin.json +1 -1
- package/CHANGELOG.md +16 -0
- package/README.md +42 -5
- package/commands/crawl.md +7 -7
- package/commands/search.md +9 -2
- package/dist/{chunk-MQGRQ2EG.js → chunk-C4SYGLAI.js} +27 -7
- package/dist/chunk-C4SYGLAI.js.map +1 -0
- package/dist/{chunk-ZSKQIMD7.js → chunk-CC6EGZ4D.js} +48 -8
- package/dist/chunk-CC6EGZ4D.js.map +1 -0
- package/dist/{chunk-Q2ZGPJ66.js → chunk-QCSFBMYW.js} +2 -2
- package/dist/index.js +64 -12
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +2 -2
- package/dist/workers/background-worker-cli.js +2 -2
- package/package.json +1 -1
- package/src/analysis/code-graph.test.ts +30 -0
- package/src/analysis/code-graph.ts +10 -2
- package/src/cli/commands/store.test.ts +78 -0
- package/src/cli/commands/store.ts +19 -0
- package/src/cli/commands/sync.test.ts +1 -1
- package/src/cli/commands/sync.ts +50 -1
- package/src/mcp/commands/sync.commands.test.ts +94 -6
- package/src/mcp/commands/sync.commands.ts +36 -6
- package/src/mcp/handlers/search.handler.ts +3 -1
- package/src/mcp/handlers/store.handler.test.ts +3 -0
- package/src/mcp/handlers/store.handler.ts +5 -2
- package/src/mcp/schemas/index.test.ts +36 -0
- package/src/mcp/schemas/index.ts +6 -0
- package/src/mcp/server.ts +11 -0
- package/src/services/code-graph.service.ts +11 -1
- package/src/services/job.service.test.ts +23 -0
- package/src/services/job.service.ts +10 -6
- package/vitest.config.ts +1 -1
- package/dist/chunk-MQGRQ2EG.js.map +0 -1
- package/dist/chunk-ZSKQIMD7.js.map +0 -1
- /package/dist/{chunk-Q2ZGPJ66.js.map → chunk-QCSFBMYW.js.map} +0 -0
package/dist/mcp/server.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
IntelligentCrawler
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-QCSFBMYW.js";
|
|
5
5
|
import {
|
|
6
6
|
JobService,
|
|
7
7
|
createDocumentId,
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
createServices,
|
|
10
10
|
createStoreId,
|
|
11
11
|
shutdownLogger
|
|
12
|
-
} from "../chunk-
|
|
12
|
+
} from "../chunk-C4SYGLAI.js";
|
|
13
13
|
import "../chunk-HRQD3MPH.js";
|
|
14
14
|
|
|
15
15
|
// src/workers/background-worker.ts
|
package/package.json
CHANGED
|
@@ -328,6 +328,36 @@ describe('CodeGraph', () => {
|
|
|
328
328
|
expect(edgeTypes).toContain('calls');
|
|
329
329
|
});
|
|
330
330
|
|
|
331
|
+
it('includes confidence in serialized edges', () => {
|
|
332
|
+
const graph = new CodeGraph();
|
|
333
|
+
const nodes: CodeNode[] = [
|
|
334
|
+
{
|
|
335
|
+
type: 'function',
|
|
336
|
+
name: 'fn',
|
|
337
|
+
exported: false,
|
|
338
|
+
startLine: 1,
|
|
339
|
+
endLine: 2,
|
|
340
|
+
},
|
|
341
|
+
];
|
|
342
|
+
|
|
343
|
+
graph.addNodes(nodes, '/src/test.ts');
|
|
344
|
+
graph.addImport('/src/test.ts', 'module', ['util']); // confidence: 1.0
|
|
345
|
+
graph.analyzeCallRelationships('other();', '/src/test.ts', 'fn'); // confidence: 0.5
|
|
346
|
+
|
|
347
|
+
const json = graph.toJSON();
|
|
348
|
+
|
|
349
|
+
// All edges should have confidence property preserved
|
|
350
|
+
expect(json.edges.every((e) => typeof e.confidence === 'number')).toBe(true);
|
|
351
|
+
|
|
352
|
+
// Import edges have confidence 1.0
|
|
353
|
+
const importEdge = json.edges.find((e) => e.type === 'imports');
|
|
354
|
+
expect(importEdge?.confidence).toBe(1.0);
|
|
355
|
+
|
|
356
|
+
// Call edges from regex detection have confidence 0.5
|
|
357
|
+
const callEdge = json.edges.find((e) => e.type === 'calls');
|
|
358
|
+
expect(callEdge?.confidence).toBe(0.5);
|
|
359
|
+
});
|
|
360
|
+
|
|
331
361
|
it('handles empty graph', () => {
|
|
332
362
|
const graph = new CodeGraph();
|
|
333
363
|
const json = graph.toJSON();
|
|
@@ -227,7 +227,10 @@ export class CodeGraph {
|
|
|
227
227
|
return importPath;
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
-
toJSON(): {
|
|
230
|
+
toJSON(): {
|
|
231
|
+
nodes: GraphNode[];
|
|
232
|
+
edges: Array<{ from: string; to: string; type: string; confidence: number }>;
|
|
233
|
+
} {
|
|
231
234
|
const allEdges: GraphEdge[] = [];
|
|
232
235
|
for (const edges of this.edges.values()) {
|
|
233
236
|
allEdges.push(...edges);
|
|
@@ -235,7 +238,12 @@ export class CodeGraph {
|
|
|
235
238
|
|
|
236
239
|
return {
|
|
237
240
|
nodes: Array.from(this.nodes.values()),
|
|
238
|
-
edges: allEdges.map((e) => ({
|
|
241
|
+
edges: allEdges.map((e) => ({
|
|
242
|
+
from: e.from,
|
|
243
|
+
to: e.to,
|
|
244
|
+
type: e.type,
|
|
245
|
+
confidence: e.confidence,
|
|
246
|
+
})),
|
|
239
247
|
};
|
|
240
248
|
}
|
|
241
249
|
}
|
|
@@ -17,8 +17,23 @@ interface MockStoreService {
|
|
|
17
17
|
delete: MockInstance;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
interface MockLanceService {
|
|
21
|
+
deleteStore: MockInstance;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface MockCodeGraphService {
|
|
25
|
+
deleteGraph: MockInstance;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface MockConfigService {
|
|
29
|
+
resolveDataDir: MockInstance;
|
|
30
|
+
}
|
|
31
|
+
|
|
20
32
|
interface MockServices {
|
|
21
33
|
store: MockStoreService;
|
|
34
|
+
lance: MockLanceService;
|
|
35
|
+
codeGraph: MockCodeGraphService;
|
|
36
|
+
config: MockConfigService;
|
|
22
37
|
}
|
|
23
38
|
|
|
24
39
|
describe('store command execution', () => {
|
|
@@ -38,6 +53,15 @@ describe('store command execution', () => {
|
|
|
38
53
|
create: vi.fn(),
|
|
39
54
|
delete: vi.fn(),
|
|
40
55
|
},
|
|
56
|
+
lance: {
|
|
57
|
+
deleteStore: vi.fn().mockResolvedValue(undefined),
|
|
58
|
+
},
|
|
59
|
+
codeGraph: {
|
|
60
|
+
deleteGraph: vi.fn().mockResolvedValue(undefined),
|
|
61
|
+
},
|
|
62
|
+
config: {
|
|
63
|
+
resolveDataDir: vi.fn().mockReturnValue('/tmp/test-data'),
|
|
64
|
+
},
|
|
41
65
|
};
|
|
42
66
|
|
|
43
67
|
vi.mocked(createServices).mockResolvedValue(mockServices);
|
|
@@ -366,6 +390,51 @@ describe('store command execution', () => {
|
|
|
366
390
|
});
|
|
367
391
|
});
|
|
368
392
|
|
|
393
|
+
it('creates repo store with branch option', async () => {
|
|
394
|
+
const mockStore: RepoStore = {
|
|
395
|
+
id: createStoreId('new-store-6'),
|
|
396
|
+
name: 'branched-repo',
|
|
397
|
+
type: 'repo',
|
|
398
|
+
path: '/path/to/cloned/repo',
|
|
399
|
+
url: 'https://github.com/user/repo',
|
|
400
|
+
branch: 'develop',
|
|
401
|
+
createdAt: new Date(),
|
|
402
|
+
updatedAt: new Date(),
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
mockServices.store.create.mockResolvedValue({
|
|
406
|
+
success: true,
|
|
407
|
+
data: mockStore,
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
const command = createStoreCommand(getOptions);
|
|
411
|
+
const createCommand = command.commands.find((c) => c.name() === 'create');
|
|
412
|
+
const actionHandler = createCommand?._actionHandler;
|
|
413
|
+
|
|
414
|
+
createCommand.parseOptions([
|
|
415
|
+
'--type',
|
|
416
|
+
'repo',
|
|
417
|
+
'--source',
|
|
418
|
+
'https://github.com/user/repo',
|
|
419
|
+
'--branch',
|
|
420
|
+
'develop',
|
|
421
|
+
]);
|
|
422
|
+
await actionHandler!(['branched-repo']);
|
|
423
|
+
|
|
424
|
+
expect(mockServices.store.create).toHaveBeenCalledWith({
|
|
425
|
+
name: 'branched-repo',
|
|
426
|
+
type: 'repo',
|
|
427
|
+
path: undefined,
|
|
428
|
+
url: 'https://github.com/user/repo',
|
|
429
|
+
branch: 'develop',
|
|
430
|
+
description: undefined,
|
|
431
|
+
tags: undefined,
|
|
432
|
+
});
|
|
433
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
434
|
+
expect.stringContaining('Created store: branched-repo')
|
|
435
|
+
);
|
|
436
|
+
});
|
|
437
|
+
|
|
369
438
|
it('outputs JSON when format is json', async () => {
|
|
370
439
|
const mockStore: FileStore = {
|
|
371
440
|
id: createStoreId('new-store-5'),
|
|
@@ -860,6 +929,15 @@ describe('store command execution', () => {
|
|
|
860
929
|
expect(tagsOption?.mandatory).toBe(false);
|
|
861
930
|
});
|
|
862
931
|
|
|
932
|
+
it('create subcommand has --branch option for repo type', () => {
|
|
933
|
+
const command = createStoreCommand(getOptions);
|
|
934
|
+
const createCommand = command.commands.find((c) => c.name() === 'create');
|
|
935
|
+
const branchOption = createCommand?.options.find((o) => o.long === '--branch');
|
|
936
|
+
|
|
937
|
+
expect(branchOption).toBeDefined();
|
|
938
|
+
expect(branchOption?.mandatory).toBe(false);
|
|
939
|
+
});
|
|
940
|
+
|
|
863
941
|
it('delete subcommand has force and yes options', () => {
|
|
864
942
|
const command = createStoreCommand(getOptions);
|
|
865
943
|
const deleteCommand = command.commands.find((c) => c.name() === 'delete');
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { rm } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
1
3
|
import { Command } from 'commander';
|
|
2
4
|
import { createServices, destroyServices } from '../../services/index.js';
|
|
3
5
|
import type { StoreType } from '../../types/store.js';
|
|
@@ -49,6 +51,7 @@ export function createStoreCommand(getOptions: () => GlobalOptions): Command {
|
|
|
49
51
|
'Store type: file (local dir), repo (git), web (crawled site)'
|
|
50
52
|
)
|
|
51
53
|
.requiredOption('-s, --source <path>', 'Local path for file/repo stores, URL for web stores')
|
|
54
|
+
.option('-b, --branch <branch>', 'Git branch to clone (repo stores only)')
|
|
52
55
|
.option('-d, --description <desc>', 'Optional description for the store')
|
|
53
56
|
.option('--tags <tags>', 'Comma-separated tags for filtering')
|
|
54
57
|
.action(
|
|
@@ -57,6 +60,7 @@ export function createStoreCommand(getOptions: () => GlobalOptions): Command {
|
|
|
57
60
|
options: {
|
|
58
61
|
type: StoreType;
|
|
59
62
|
source: string;
|
|
63
|
+
branch?: string;
|
|
60
64
|
description?: string;
|
|
61
65
|
tags?: string;
|
|
62
66
|
}
|
|
@@ -79,6 +83,7 @@ export function createStoreCommand(getOptions: () => GlobalOptions): Command {
|
|
|
79
83
|
options.type === 'web' || (options.type === 'repo' && isUrl)
|
|
80
84
|
? options.source
|
|
81
85
|
: undefined,
|
|
86
|
+
branch: options.type === 'repo' ? options.branch : undefined,
|
|
82
87
|
description: options.description,
|
|
83
88
|
tags: options.tags?.split(',').map((t) => t.trim()),
|
|
84
89
|
});
|
|
@@ -188,6 +193,20 @@ export function createStoreCommand(getOptions: () => GlobalOptions): Command {
|
|
|
188
193
|
}
|
|
189
194
|
}
|
|
190
195
|
|
|
196
|
+
// Delete LanceDB table first (so searches don't return results for deleted store)
|
|
197
|
+
await services.lance.deleteStore(s.id);
|
|
198
|
+
|
|
199
|
+
// Delete code graph file
|
|
200
|
+
await services.codeGraph.deleteGraph(s.id);
|
|
201
|
+
|
|
202
|
+
// For repo stores cloned from URL, remove the cloned directory
|
|
203
|
+
if (s.type === 'repo' && 'url' in s && s.url !== undefined) {
|
|
204
|
+
const dataDir = services.config.resolveDataDir();
|
|
205
|
+
const repoPath = join(dataDir, 'repos', s.id);
|
|
206
|
+
await rm(repoPath, { recursive: true, force: true });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Delete from registry last
|
|
191
210
|
const result = await services.store.delete(s.id);
|
|
192
211
|
|
|
193
212
|
if (result.success) {
|
|
@@ -49,6 +49,6 @@ describe('createSyncCommand', () => {
|
|
|
49
49
|
expect(pruneOpt?.description).toContain('Remove');
|
|
50
50
|
|
|
51
51
|
const reindexOpt = options.find((o) => o.long === '--reindex');
|
|
52
|
-
expect(reindexOpt?.description).toContain('index');
|
|
52
|
+
expect(reindexOpt?.description).toContain('Re-index');
|
|
53
53
|
});
|
|
54
54
|
});
|
package/src/cli/commands/sync.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { createServices, destroyServices } from '../../services/index.js';
|
|
3
|
+
import { JobService } from '../../services/job.service.js';
|
|
3
4
|
import { StoreDefinitionService } from '../../services/store-definition.service.js';
|
|
4
5
|
import {
|
|
5
6
|
isFileStoreDefinition,
|
|
6
7
|
isRepoStoreDefinition,
|
|
7
8
|
isWebStoreDefinition,
|
|
8
9
|
} from '../../types/store-definition.js';
|
|
10
|
+
import { spawnBackgroundWorker } from '../../workers/spawn-worker.js';
|
|
9
11
|
import type { StoreService } from '../../services/store.service.js';
|
|
10
12
|
import type { StoreDefinition } from '../../types/store-definition.js';
|
|
11
13
|
import type { GlobalOptions } from '../program.js';
|
|
@@ -19,6 +21,8 @@ interface SyncResult {
|
|
|
19
21
|
dryRun: boolean;
|
|
20
22
|
wouldCreate: string[];
|
|
21
23
|
wouldPrune: string[];
|
|
24
|
+
reindexJobs: Array<{ store: string; jobId: string }>;
|
|
25
|
+
wouldReindex: string[];
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
/**
|
|
@@ -125,6 +129,8 @@ export function createSyncCommand(getOptions: () => GlobalOptions): Command {
|
|
|
125
129
|
dryRun: options.dryRun === true,
|
|
126
130
|
wouldCreate: [],
|
|
127
131
|
wouldPrune: [],
|
|
132
|
+
reindexJobs: [],
|
|
133
|
+
wouldReindex: [],
|
|
128
134
|
};
|
|
129
135
|
|
|
130
136
|
// Process each definition
|
|
@@ -173,6 +179,29 @@ export function createSyncCommand(getOptions: () => GlobalOptions): Command {
|
|
|
173
179
|
}
|
|
174
180
|
}
|
|
175
181
|
|
|
182
|
+
// Re-index existing stores if requested
|
|
183
|
+
if (options.reindex === true && result.skipped.length > 0) {
|
|
184
|
+
if (options.dryRun === true) {
|
|
185
|
+
result.wouldReindex = [...result.skipped];
|
|
186
|
+
} else {
|
|
187
|
+
const dataDir = globalOpts.dataDir ?? services.config.resolveDataDir();
|
|
188
|
+
const jobService = new JobService(dataDir);
|
|
189
|
+
|
|
190
|
+
for (const storeName of result.skipped) {
|
|
191
|
+
const store = await services.store.getByName(storeName);
|
|
192
|
+
if (store !== undefined) {
|
|
193
|
+
const job = jobService.createJob({
|
|
194
|
+
type: 'index',
|
|
195
|
+
details: { storeId: store.id, storeName: store.name },
|
|
196
|
+
message: `Re-indexing ${storeName}...`,
|
|
197
|
+
});
|
|
198
|
+
spawnBackgroundWorker(job.id, dataDir);
|
|
199
|
+
result.reindexJobs.push({ store: storeName, jobId: job.id });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
176
205
|
// Output result
|
|
177
206
|
if (globalOpts.format === 'json') {
|
|
178
207
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -189,19 +218,25 @@ export function createSyncCommand(getOptions: () => GlobalOptions): Command {
|
|
|
189
218
|
|
|
190
219
|
function printHumanReadable(result: SyncResult, quiet: boolean): void {
|
|
191
220
|
if (quiet) {
|
|
192
|
-
// Just print created/pruned store names
|
|
221
|
+
// Just print created/pruned/reindexed store names
|
|
193
222
|
for (const name of result.created) {
|
|
194
223
|
console.log(`created: ${name}`);
|
|
195
224
|
}
|
|
196
225
|
for (const name of result.pruned) {
|
|
197
226
|
console.log(`pruned: ${name}`);
|
|
198
227
|
}
|
|
228
|
+
for (const { store, jobId } of result.reindexJobs) {
|
|
229
|
+
console.log(`reindexing: ${store} (${jobId})`);
|
|
230
|
+
}
|
|
199
231
|
for (const name of result.wouldCreate) {
|
|
200
232
|
console.log(`would create: ${name}`);
|
|
201
233
|
}
|
|
202
234
|
for (const name of result.wouldPrune) {
|
|
203
235
|
console.log(`would prune: ${name}`);
|
|
204
236
|
}
|
|
237
|
+
for (const name of result.wouldReindex) {
|
|
238
|
+
console.log(`would reindex: ${name}`);
|
|
239
|
+
}
|
|
205
240
|
return;
|
|
206
241
|
}
|
|
207
242
|
|
|
@@ -260,5 +295,19 @@ function printHumanReadable(result: SyncResult, quiet: boolean): void {
|
|
|
260
295
|
}
|
|
261
296
|
}
|
|
262
297
|
|
|
298
|
+
if (result.reindexJobs.length > 0) {
|
|
299
|
+
console.log(`Reindexing started (${String(result.reindexJobs.length)}):`);
|
|
300
|
+
for (const { store, jobId } of result.reindexJobs) {
|
|
301
|
+
console.log(` ↻ ${store} (Job: ${jobId})`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (result.wouldReindex.length > 0) {
|
|
306
|
+
console.log(`Would reindex (${String(result.wouldReindex.length)}):`);
|
|
307
|
+
for (const name of result.wouldReindex) {
|
|
308
|
+
console.log(` ↻ ${name}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
263
312
|
console.log('');
|
|
264
313
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
import { rm, mkdtemp, mkdir, writeFile } from 'node:fs/promises';
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { rm, mkdtemp, mkdir } from 'node:fs/promises';
|
|
4
3
|
import { tmpdir } from 'node:os';
|
|
5
4
|
import { join } from 'node:path';
|
|
6
5
|
import { syncCommands, handleStoresSync } from './sync.commands.js';
|
|
@@ -8,7 +7,11 @@ import { StoreService } from '../../services/store.service.js';
|
|
|
8
7
|
import { StoreDefinitionService } from '../../services/store-definition.service.js';
|
|
9
8
|
import type { HandlerContext } from '../types.js';
|
|
10
9
|
import type { ServiceContainer } from '../../services/index.js';
|
|
11
|
-
|
|
10
|
+
|
|
11
|
+
// Mock spawnBackgroundWorker
|
|
12
|
+
vi.mock('../../workers/spawn-worker.js', () => ({
|
|
13
|
+
spawnBackgroundWorker: vi.fn(),
|
|
14
|
+
}));
|
|
12
15
|
|
|
13
16
|
/**
|
|
14
17
|
* Create a minimal mock service container for testing
|
|
@@ -45,11 +48,18 @@ describe('sync.commands', () => {
|
|
|
45
48
|
|
|
46
49
|
// Valid with options
|
|
47
50
|
const result2 = syncCmd?.argsSchema?.safeParse({
|
|
48
|
-
reindex: true,
|
|
49
51
|
prune: true,
|
|
50
52
|
dryRun: true,
|
|
51
53
|
});
|
|
52
54
|
expect(result2?.success).toBe(true);
|
|
55
|
+
|
|
56
|
+
// Valid with reindex option
|
|
57
|
+
const result3 = syncCmd?.argsSchema?.safeParse({
|
|
58
|
+
reindex: true,
|
|
59
|
+
prune: true,
|
|
60
|
+
dryRun: true,
|
|
61
|
+
});
|
|
62
|
+
expect(result3?.success).toBe(true);
|
|
53
63
|
});
|
|
54
64
|
});
|
|
55
65
|
|
|
@@ -69,7 +79,7 @@ describe('sync.commands', () => {
|
|
|
69
79
|
|
|
70
80
|
context = {
|
|
71
81
|
services: createMockServices(storeService),
|
|
72
|
-
options: { projectRoot },
|
|
82
|
+
options: { projectRoot, dataDir },
|
|
73
83
|
};
|
|
74
84
|
});
|
|
75
85
|
|
|
@@ -279,5 +289,83 @@ describe('sync.commands', () => {
|
|
|
279
289
|
expect(response.orphans).toHaveLength(0);
|
|
280
290
|
});
|
|
281
291
|
});
|
|
292
|
+
|
|
293
|
+
describe('reindex mode', () => {
|
|
294
|
+
it('reports wouldReindex in dry run mode', async () => {
|
|
295
|
+
const docsDir = join(projectRoot, 'docs');
|
|
296
|
+
await mkdir(docsDir, { recursive: true });
|
|
297
|
+
|
|
298
|
+
// Create store (auto-adds definition)
|
|
299
|
+
await storeService.create({
|
|
300
|
+
name: 'existing-store',
|
|
301
|
+
type: 'file',
|
|
302
|
+
path: docsDir,
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
const result = await handleStoresSync({ reindex: true, dryRun: true }, context);
|
|
306
|
+
const response = JSON.parse(result.content[0].text);
|
|
307
|
+
|
|
308
|
+
expect(response.dryRun).toBe(true);
|
|
309
|
+
expect(response.wouldReindex).toContain('existing-store');
|
|
310
|
+
expect(response.reindexJobs).toBeUndefined();
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('starts reindex jobs for existing stores', async () => {
|
|
314
|
+
const docsDir = join(projectRoot, 'docs');
|
|
315
|
+
await mkdir(docsDir, { recursive: true });
|
|
316
|
+
|
|
317
|
+
// Create store (auto-adds definition)
|
|
318
|
+
await storeService.create({
|
|
319
|
+
name: 'reindex-store',
|
|
320
|
+
type: 'file',
|
|
321
|
+
path: docsDir,
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
const result = await handleStoresSync({ reindex: true }, context);
|
|
325
|
+
const response = JSON.parse(result.content[0].text);
|
|
326
|
+
|
|
327
|
+
expect(response.reindexJobs).toHaveLength(1);
|
|
328
|
+
expect(response.reindexJobs[0].store).toBe('reindex-store');
|
|
329
|
+
expect(response.reindexJobs[0].jobId).toMatch(/^job_/);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('does not reindex if reindex flag is not set', async () => {
|
|
333
|
+
const docsDir = join(projectRoot, 'docs');
|
|
334
|
+
await mkdir(docsDir, { recursive: true });
|
|
335
|
+
|
|
336
|
+
await storeService.create({
|
|
337
|
+
name: 'no-reindex-store',
|
|
338
|
+
type: 'file',
|
|
339
|
+
path: docsDir,
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
const result = await handleStoresSync({}, context);
|
|
343
|
+
const response = JSON.parse(result.content[0].text);
|
|
344
|
+
|
|
345
|
+
expect(response.reindexJobs).toBeUndefined();
|
|
346
|
+
expect(response.wouldReindex).toBeUndefined();
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('throws error when dataDir is undefined during reindex', async () => {
|
|
350
|
+
const docsDir = join(projectRoot, 'docs');
|
|
351
|
+
await mkdir(docsDir, { recursive: true });
|
|
352
|
+
|
|
353
|
+
await storeService.create({
|
|
354
|
+
name: 'error-store',
|
|
355
|
+
type: 'file',
|
|
356
|
+
path: docsDir,
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// Create context without dataDir
|
|
360
|
+
const contextWithoutDataDir: HandlerContext = {
|
|
361
|
+
services: createMockServices(storeService),
|
|
362
|
+
options: { projectRoot },
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
await expect(handleStoresSync({ reindex: true }, contextWithoutDataDir)).rejects.toThrow(
|
|
366
|
+
'dataDir is required for reindexing'
|
|
367
|
+
);
|
|
368
|
+
});
|
|
369
|
+
});
|
|
282
370
|
});
|
|
283
371
|
});
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { JobService } from '../../services/job.service.js';
|
|
2
3
|
import { StoreDefinitionService } from '../../services/store-definition.service.js';
|
|
3
4
|
import {
|
|
4
5
|
isFileStoreDefinition,
|
|
5
6
|
isRepoStoreDefinition,
|
|
6
7
|
isWebStoreDefinition,
|
|
7
8
|
} from '../../types/store-definition.js';
|
|
9
|
+
import { spawnBackgroundWorker } from '../../workers/spawn-worker.js';
|
|
8
10
|
import type { CommandDefinition } from './registry.js';
|
|
9
11
|
import type { StoreDefinition } from '../../types/store-definition.js';
|
|
10
12
|
import type { HandlerContext, ToolResponse } from '../types.js';
|
|
@@ -13,9 +15,9 @@ import type { HandlerContext, ToolResponse } from '../types.js';
|
|
|
13
15
|
* Arguments for stores:sync command
|
|
14
16
|
*/
|
|
15
17
|
export interface SyncStoresArgs {
|
|
16
|
-
reindex?: boolean;
|
|
17
18
|
prune?: boolean;
|
|
18
19
|
dryRun?: boolean;
|
|
20
|
+
reindex?: boolean;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
/**
|
|
@@ -30,6 +32,8 @@ interface SyncResult {
|
|
|
30
32
|
dryRun?: boolean;
|
|
31
33
|
wouldCreate?: string[];
|
|
32
34
|
wouldPrune?: string[];
|
|
35
|
+
reindexJobs?: Array<{ store: string; jobId: string }>;
|
|
36
|
+
wouldReindex?: string[];
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
/**
|
|
@@ -39,7 +43,6 @@ interface SyncResult {
|
|
|
39
43
|
* - Creates missing stores from definitions
|
|
40
44
|
* - Reports stores not in definitions (orphans)
|
|
41
45
|
* - Optionally prunes orphan stores
|
|
42
|
-
* - Optionally re-indexes existing stores
|
|
43
46
|
*/
|
|
44
47
|
export async function handleStoresSync(
|
|
45
48
|
args: SyncStoresArgs,
|
|
@@ -119,6 +122,33 @@ export async function handleStoresSync(
|
|
|
119
122
|
}
|
|
120
123
|
}
|
|
121
124
|
|
|
125
|
+
// Re-index existing stores if requested
|
|
126
|
+
if (args.reindex === true && result.skipped.length > 0) {
|
|
127
|
+
if (args.dryRun === true) {
|
|
128
|
+
result.wouldReindex = [...result.skipped];
|
|
129
|
+
} else {
|
|
130
|
+
result.reindexJobs = [];
|
|
131
|
+
const dataDir = options.dataDir;
|
|
132
|
+
if (dataDir === undefined) {
|
|
133
|
+
throw new Error('dataDir is required for reindexing');
|
|
134
|
+
}
|
|
135
|
+
const jobService = new JobService(dataDir);
|
|
136
|
+
|
|
137
|
+
for (const storeName of result.skipped) {
|
|
138
|
+
const store = await services.store.getByName(storeName);
|
|
139
|
+
if (store !== undefined) {
|
|
140
|
+
const job = jobService.createJob({
|
|
141
|
+
type: 'index',
|
|
142
|
+
details: { storeId: store.id, storeName: store.name },
|
|
143
|
+
message: `Re-indexing ${storeName}...`,
|
|
144
|
+
});
|
|
145
|
+
spawnBackgroundWorker(job.id, dataDir);
|
|
146
|
+
result.reindexJobs.push({ store: storeName, jobId: job.id });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
122
152
|
return {
|
|
123
153
|
content: [
|
|
124
154
|
{
|
|
@@ -212,21 +242,21 @@ export const syncCommands: CommandDefinition[] = [
|
|
|
212
242
|
name: 'stores:sync',
|
|
213
243
|
description: 'Sync stores from definitions config (bootstrap on fresh clone)',
|
|
214
244
|
argsSchema: z.object({
|
|
215
|
-
reindex: z.boolean().optional().describe('Re-index existing stores after sync'),
|
|
216
245
|
prune: z.boolean().optional().describe('Remove stores not in definitions'),
|
|
217
246
|
dryRun: z.boolean().optional().describe('Show what would happen without making changes'),
|
|
247
|
+
reindex: z.boolean().optional().describe('Re-index existing stores after sync'),
|
|
218
248
|
}),
|
|
219
249
|
handler: (args: Record<string, unknown>, context: HandlerContext): Promise<ToolResponse> => {
|
|
220
250
|
const syncArgs: SyncStoresArgs = {};
|
|
221
|
-
if (typeof args['reindex'] === 'boolean') {
|
|
222
|
-
syncArgs.reindex = args['reindex'];
|
|
223
|
-
}
|
|
224
251
|
if (typeof args['prune'] === 'boolean') {
|
|
225
252
|
syncArgs.prune = args['prune'];
|
|
226
253
|
}
|
|
227
254
|
if (typeof args['dryRun'] === 'boolean') {
|
|
228
255
|
syncArgs.dryRun = args['dryRun'];
|
|
229
256
|
}
|
|
257
|
+
if (typeof args['reindex'] === 'boolean') {
|
|
258
|
+
syncArgs.reindex = args['reindex'];
|
|
259
|
+
}
|
|
230
260
|
return handleStoresSync(syncArgs, context);
|
|
231
261
|
},
|
|
232
262
|
},
|
|
@@ -30,6 +30,7 @@ export const handleSearch: ToolHandler<SearchArgs> = async (
|
|
|
30
30
|
{
|
|
31
31
|
query: validated.query,
|
|
32
32
|
stores: validated.stores,
|
|
33
|
+
mode: validated.mode,
|
|
33
34
|
detail: validated.detail,
|
|
34
35
|
limit: validated.limit,
|
|
35
36
|
intent: validated.intent,
|
|
@@ -69,9 +70,10 @@ export const handleSearch: ToolHandler<SearchArgs> = async (
|
|
|
69
70
|
const searchQuery: SearchQuery = {
|
|
70
71
|
query: validated.query,
|
|
71
72
|
stores: storeIds,
|
|
72
|
-
mode:
|
|
73
|
+
mode: validated.mode,
|
|
73
74
|
limit: validated.limit,
|
|
74
75
|
detail: validated.detail,
|
|
76
|
+
threshold: validated.threshold,
|
|
75
77
|
minRelevance: validated.minRelevance,
|
|
76
78
|
};
|
|
77
79
|
|
|
@@ -274,9 +274,12 @@ export const handleDeleteStore: ToolHandler<DeleteStoreArgs> = async (
|
|
|
274
274
|
throw new Error(`Store not found: ${validated.store}`);
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
-
// Delete LanceDB table
|
|
277
|
+
// Delete LanceDB table first (so searches don't return results for deleted store)
|
|
278
278
|
await services.lance.deleteStore(store.id);
|
|
279
279
|
|
|
280
|
+
// Delete code graph file
|
|
281
|
+
await services.codeGraph.deleteGraph(store.id);
|
|
282
|
+
|
|
280
283
|
// For repo stores cloned from URL, remove the cloned directory
|
|
281
284
|
if (store.type === 'repo' && 'url' in store && store.url !== undefined) {
|
|
282
285
|
if (options.dataDir === undefined) {
|
|
@@ -286,7 +289,7 @@ export const handleDeleteStore: ToolHandler<DeleteStoreArgs> = async (
|
|
|
286
289
|
await rm(repoPath, { recursive: true, force: true });
|
|
287
290
|
}
|
|
288
291
|
|
|
289
|
-
// Delete from registry
|
|
292
|
+
// Delete from registry last
|
|
290
293
|
const result = await services.store.delete(store.id);
|
|
291
294
|
if (!result.success) {
|
|
292
295
|
throw new Error(result.error.message);
|