bluera-knowledge 0.10.0 → 0.10.1

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 (34) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +12 -0
  3. package/README.md +73 -2
  4. package/commands/sync.md +96 -0
  5. package/dist/{chunk-ITH6FWQY.js → chunk-6U45VP5Z.js} +24 -3
  6. package/dist/{chunk-ITH6FWQY.js.map → chunk-6U45VP5Z.js.map} +1 -1
  7. package/dist/{chunk-CUHYSPRV.js → chunk-DP5XBPQV.js} +372 -2
  8. package/dist/chunk-DP5XBPQV.js.map +1 -0
  9. package/dist/{chunk-DWAIT2OD.js → chunk-UE4ZIJYA.js} +74 -5
  10. package/dist/{chunk-DWAIT2OD.js.map → chunk-UE4ZIJYA.js.map} +1 -1
  11. package/dist/index.js +213 -5
  12. package/dist/index.js.map +1 -1
  13. package/dist/mcp/server.js +2 -2
  14. package/dist/workers/background-worker-cli.js +2 -2
  15. package/package.json +1 -1
  16. package/src/cli/commands/sync.test.ts +54 -0
  17. package/src/cli/commands/sync.ts +264 -0
  18. package/src/cli/index.ts +1 -0
  19. package/src/crawl/claude-client.test.ts +56 -0
  20. package/src/crawl/claude-client.ts +27 -1
  21. package/src/index.ts +2 -0
  22. package/src/mcp/commands/index.ts +2 -0
  23. package/src/mcp/commands/sync.commands.test.ts +283 -0
  24. package/src/mcp/commands/sync.commands.ts +233 -0
  25. package/src/services/gitignore.service.test.ts +157 -0
  26. package/src/services/gitignore.service.ts +132 -0
  27. package/src/services/store-definition.service.test.ts +440 -0
  28. package/src/services/store-definition.service.ts +198 -0
  29. package/src/services/store.service.test.ts +279 -1
  30. package/src/services/store.service.ts +101 -4
  31. package/src/types/index.ts +18 -0
  32. package/src/types/store-definition.test.ts +492 -0
  33. package/src/types/store-definition.ts +129 -0
  34. package/dist/chunk-CUHYSPRV.js.map +0 -1
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  createMCPServer,
3
3
  runMCPServer
4
- } from "../chunk-CUHYSPRV.js";
5
- import "../chunk-DWAIT2OD.js";
4
+ } from "../chunk-DP5XBPQV.js";
5
+ import "../chunk-UE4ZIJYA.js";
6
6
  import "../chunk-6FHWC36B.js";
7
7
  export {
8
8
  createMCPServer,
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  IntelligentCrawler
4
- } from "../chunk-ITH6FWQY.js";
4
+ } from "../chunk-6U45VP5Z.js";
5
5
  import {
6
6
  JobService,
7
7
  createDocumentId,
8
8
  createServices,
9
9
  createStoreId
10
- } from "../chunk-DWAIT2OD.js";
10
+ } from "../chunk-UE4ZIJYA.js";
11
11
  import "../chunk-6FHWC36B.js";
12
12
 
13
13
  // src/workers/background-worker.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bluera-knowledge",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "CLI tool for managing knowledge stores with semantic search",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,54 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { createSyncCommand } from './sync.js';
3
+ import type { GlobalOptions } from '../program.js';
4
+
5
+ describe('createSyncCommand', () => {
6
+ function createTestOptions(): GlobalOptions {
7
+ return {
8
+ dataDir: '/tmp/test-data',
9
+ projectRoot: '/tmp/test-project',
10
+ format: 'table',
11
+ };
12
+ }
13
+
14
+ it('creates sync command with correct name and description', () => {
15
+ const cmd = createSyncCommand(createTestOptions);
16
+ expect(cmd.name()).toBe('sync');
17
+ expect(cmd.description()).toContain('Sync');
18
+ });
19
+
20
+ it('has --dry-run option', () => {
21
+ const cmd = createSyncCommand(createTestOptions);
22
+ const options = cmd.options;
23
+ const dryRunOpt = options.find((o) => o.long === '--dry-run');
24
+ expect(dryRunOpt).toBeDefined();
25
+ });
26
+
27
+ it('has --prune option', () => {
28
+ const cmd = createSyncCommand(createTestOptions);
29
+ const options = cmd.options;
30
+ const pruneOpt = options.find((o) => o.long === '--prune');
31
+ expect(pruneOpt).toBeDefined();
32
+ });
33
+
34
+ it('has --reindex option', () => {
35
+ const cmd = createSyncCommand(createTestOptions);
36
+ const options = cmd.options;
37
+ const reindexOpt = options.find((o) => o.long === '--reindex');
38
+ expect(reindexOpt).toBeDefined();
39
+ });
40
+
41
+ it('has correct option descriptions', () => {
42
+ const cmd = createSyncCommand(createTestOptions);
43
+ const options = cmd.options;
44
+
45
+ const dryRunOpt = options.find((o) => o.long === '--dry-run');
46
+ expect(dryRunOpt?.description).toContain('without making changes');
47
+
48
+ const pruneOpt = options.find((o) => o.long === '--prune');
49
+ expect(pruneOpt?.description).toContain('Remove');
50
+
51
+ const reindexOpt = options.find((o) => o.long === '--reindex');
52
+ expect(reindexOpt?.description).toContain('index');
53
+ });
54
+ });
@@ -0,0 +1,264 @@
1
+ import { Command } from 'commander';
2
+ import { createServices, destroyServices } from '../../services/index.js';
3
+ import { StoreDefinitionService } from '../../services/store-definition.service.js';
4
+ import {
5
+ isFileStoreDefinition,
6
+ isRepoStoreDefinition,
7
+ isWebStoreDefinition,
8
+ } from '../../types/store-definition.js';
9
+ import type { StoreService } from '../../services/store.service.js';
10
+ import type { StoreDefinition } from '../../types/store-definition.js';
11
+ import type { GlobalOptions } from '../program.js';
12
+
13
+ interface SyncResult {
14
+ created: string[];
15
+ skipped: string[];
16
+ failed: Array<{ name: string; error: string }>;
17
+ orphans: string[];
18
+ pruned: string[];
19
+ dryRun: boolean;
20
+ wouldCreate: string[];
21
+ wouldPrune: string[];
22
+ }
23
+
24
+ /**
25
+ * Create a store from a definition
26
+ */
27
+ async function createStoreFromDefinition(
28
+ def: StoreDefinition,
29
+ defService: StoreDefinitionService,
30
+ storeService: StoreService
31
+ ): Promise<{ success: true } | { success: false; error: string }> {
32
+ try {
33
+ if (isFileStoreDefinition(def)) {
34
+ const resolvedPath = defService.resolvePath(def.path);
35
+ const createResult = await storeService.create(
36
+ {
37
+ name: def.name,
38
+ type: 'file',
39
+ path: resolvedPath,
40
+ description: def.description,
41
+ tags: def.tags,
42
+ },
43
+ { skipDefinitionSync: true }
44
+ );
45
+ if (!createResult.success) {
46
+ return { success: false, error: createResult.error.message };
47
+ }
48
+ return { success: true };
49
+ }
50
+
51
+ if (isRepoStoreDefinition(def)) {
52
+ const createResult = await storeService.create(
53
+ {
54
+ name: def.name,
55
+ type: 'repo',
56
+ url: def.url,
57
+ branch: def.branch,
58
+ depth: def.depth,
59
+ description: def.description,
60
+ tags: def.tags,
61
+ },
62
+ { skipDefinitionSync: true }
63
+ );
64
+ if (!createResult.success) {
65
+ return { success: false, error: createResult.error.message };
66
+ }
67
+ return { success: true };
68
+ }
69
+
70
+ if (isWebStoreDefinition(def)) {
71
+ const createResult = await storeService.create(
72
+ {
73
+ name: def.name,
74
+ type: 'web',
75
+ url: def.url,
76
+ depth: def.depth,
77
+ description: def.description,
78
+ tags: def.tags,
79
+ },
80
+ { skipDefinitionSync: true }
81
+ );
82
+ if (!createResult.success) {
83
+ return { success: false, error: createResult.error.message };
84
+ }
85
+ return { success: true };
86
+ }
87
+
88
+ return { success: false, error: 'Unknown store definition type' };
89
+ } catch (error) {
90
+ return {
91
+ success: false,
92
+ error: error instanceof Error ? error.message : String(error),
93
+ };
94
+ }
95
+ }
96
+
97
+ export function createSyncCommand(getOptions: () => GlobalOptions): Command {
98
+ const sync = new Command('sync').description(
99
+ 'Sync stores from definitions config (bootstrap on fresh clone)'
100
+ );
101
+
102
+ sync
103
+ .option('--dry-run', 'Show what would happen without making changes')
104
+ .option('--prune', 'Remove stores not in definitions')
105
+ .option('--reindex', 'Re-index existing stores after sync')
106
+ .action(async (options: { dryRun?: boolean; prune?: boolean; reindex?: boolean }) => {
107
+ const globalOpts = getOptions();
108
+ const projectRoot = globalOpts.projectRoot ?? process.cwd();
109
+
110
+ const defService = new StoreDefinitionService(projectRoot);
111
+ const services = await createServices(globalOpts.config, globalOpts.dataDir, projectRoot);
112
+
113
+ try {
114
+ const config = await defService.load();
115
+ const existingStores = await services.store.list();
116
+ const existingNames = new Set(existingStores.map((s) => s.name));
117
+ const definedNames = new Set(config.stores.map((d) => d.name));
118
+
119
+ const result: SyncResult = {
120
+ created: [],
121
+ skipped: [],
122
+ failed: [],
123
+ orphans: [],
124
+ pruned: [],
125
+ dryRun: options.dryRun === true,
126
+ wouldCreate: [],
127
+ wouldPrune: [],
128
+ };
129
+
130
+ // Process each definition
131
+ for (const def of config.stores) {
132
+ if (existingNames.has(def.name)) {
133
+ result.skipped.push(def.name);
134
+ continue;
135
+ }
136
+
137
+ if (options.dryRun === true) {
138
+ result.wouldCreate.push(def.name);
139
+ continue;
140
+ }
141
+
142
+ const createResult = await createStoreFromDefinition(def, defService, services.store);
143
+ if (createResult.success) {
144
+ result.created.push(def.name);
145
+ } else {
146
+ result.failed.push({ name: def.name, error: createResult.error });
147
+ }
148
+ }
149
+
150
+ // Find orphans
151
+ for (const store of existingStores) {
152
+ if (!definedNames.has(store.name)) {
153
+ result.orphans.push(store.name);
154
+ }
155
+ }
156
+
157
+ // Prune orphans if requested
158
+ if (options.prune === true && result.orphans.length > 0) {
159
+ if (options.dryRun === true) {
160
+ result.wouldPrune = [...result.orphans];
161
+ } else {
162
+ for (const orphanName of result.orphans) {
163
+ const store = await services.store.getByName(orphanName);
164
+ if (store !== undefined) {
165
+ const deleteResult = await services.store.delete(store.id, {
166
+ skipDefinitionSync: true,
167
+ });
168
+ if (deleteResult.success) {
169
+ result.pruned.push(orphanName);
170
+ }
171
+ }
172
+ }
173
+ }
174
+ }
175
+
176
+ // Output result
177
+ if (globalOpts.format === 'json') {
178
+ console.log(JSON.stringify(result, null, 2));
179
+ } else {
180
+ printHumanReadable(result, globalOpts.quiet === true);
181
+ }
182
+ } finally {
183
+ await destroyServices(services);
184
+ }
185
+ });
186
+
187
+ return sync;
188
+ }
189
+
190
+ function printHumanReadable(result: SyncResult, quiet: boolean): void {
191
+ if (quiet) {
192
+ // Just print created/pruned store names
193
+ for (const name of result.created) {
194
+ console.log(`created: ${name}`);
195
+ }
196
+ for (const name of result.pruned) {
197
+ console.log(`pruned: ${name}`);
198
+ }
199
+ for (const name of result.wouldCreate) {
200
+ console.log(`would create: ${name}`);
201
+ }
202
+ for (const name of result.wouldPrune) {
203
+ console.log(`would prune: ${name}`);
204
+ }
205
+ return;
206
+ }
207
+
208
+ if (result.dryRun) {
209
+ console.log('\n[DRY RUN] No changes made.\n');
210
+ } else {
211
+ console.log('\nSync completed.\n');
212
+ }
213
+
214
+ if (result.created.length > 0) {
215
+ console.log(`Created (${String(result.created.length)}):`);
216
+ for (const name of result.created) {
217
+ console.log(` + ${name}`);
218
+ }
219
+ }
220
+
221
+ if (result.wouldCreate.length > 0) {
222
+ console.log(`Would create (${String(result.wouldCreate.length)}):`);
223
+ for (const name of result.wouldCreate) {
224
+ console.log(` + ${name}`);
225
+ }
226
+ }
227
+
228
+ if (result.skipped.length > 0) {
229
+ console.log(`Skipped (already exist) (${String(result.skipped.length)}):`);
230
+ for (const name of result.skipped) {
231
+ console.log(` - ${name}`);
232
+ }
233
+ }
234
+
235
+ if (result.failed.length > 0) {
236
+ console.log(`Failed (${String(result.failed.length)}):`);
237
+ for (const { name, error } of result.failed) {
238
+ console.log(` ! ${name}: ${error}`);
239
+ }
240
+ }
241
+
242
+ if (result.orphans.length > 0) {
243
+ console.log(`Orphans (not in definitions) (${String(result.orphans.length)}):`);
244
+ for (const name of result.orphans) {
245
+ console.log(` ? ${name}`);
246
+ }
247
+ }
248
+
249
+ if (result.pruned.length > 0) {
250
+ console.log(`Pruned (${String(result.pruned.length)}):`);
251
+ for (const name of result.pruned) {
252
+ console.log(` x ${name}`);
253
+ }
254
+ }
255
+
256
+ if (result.wouldPrune.length > 0) {
257
+ console.log(`Would prune (${String(result.wouldPrune.length)}):`);
258
+ for (const name of result.wouldPrune) {
259
+ console.log(` x ${name}`);
260
+ }
261
+ }
262
+
263
+ console.log('');
264
+ }
package/src/cli/index.ts CHANGED
@@ -5,3 +5,4 @@ export { createIndexCommand } from './commands/index-cmd.js';
5
5
  export { createServeCommand } from './commands/serve.js';
6
6
  export { createCrawlCommand } from './commands/crawl.js';
7
7
  export { createSetupCommand } from './commands/setup.js';
8
+ export { createSyncCommand } from './commands/sync.js';
@@ -106,6 +106,62 @@ describe('ClaudeClient', () => {
106
106
  expect(result.reasoning).toBe('Found documentation pages');
107
107
  });
108
108
 
109
+ it('should extract structured_output from Claude CLI wrapper format', async () => {
110
+ const promise = client.determineCrawlUrls(
111
+ 'https://example.com',
112
+ '<html>test</html>',
113
+ 'Find all docs'
114
+ );
115
+
116
+ // Claude CLI with --json-schema returns this wrapper format
117
+ setTimeout(() => {
118
+ mockProcess.stdout.emit(
119
+ 'data',
120
+ Buffer.from(
121
+ JSON.stringify({
122
+ type: 'result',
123
+ subtype: 'success',
124
+ result: '',
125
+ structured_output: {
126
+ urls: ['https://example.com/page1', 'https://example.com/page2'],
127
+ reasoning: 'Found documentation pages',
128
+ },
129
+ })
130
+ )
131
+ );
132
+ mockProcess.emit('close', 0);
133
+ }, 10);
134
+
135
+ const result = await promise;
136
+ expect(result.urls).toEqual(['https://example.com/page1', 'https://example.com/page2']);
137
+ expect(result.reasoning).toBe('Found documentation pages');
138
+ });
139
+
140
+ it('should fall back to raw response when structured_output is not an object', async () => {
141
+ const promise = client.determineCrawlUrls(
142
+ 'https://example.com',
143
+ '<html>test</html>',
144
+ 'Find all docs'
145
+ );
146
+
147
+ // When structured_output is not an object, use the raw response
148
+ // (which will fail validation if it doesn't have urls/reasoning)
149
+ setTimeout(() => {
150
+ mockProcess.stdout.emit(
151
+ 'data',
152
+ Buffer.from(
153
+ JSON.stringify({
154
+ type: 'result',
155
+ structured_output: 'not an object',
156
+ })
157
+ )
158
+ );
159
+ mockProcess.emit('close', 0);
160
+ }, 10);
161
+
162
+ await expect(promise).rejects.toThrow('invalid crawl strategy');
163
+ });
164
+
109
165
  it('should call spawn with correct arguments for determineCrawlUrls', async () => {
110
166
  const promise = client.determineCrawlUrls(
111
167
  'https://example.com',
@@ -96,7 +96,11 @@ Return only URLs that are relevant to the instruction. If the instruction mentio
96
96
 
97
97
  try {
98
98
  const result = await this.callClaude(prompt, CRAWL_STRATEGY_SCHEMA);
99
- const parsed: unknown = JSON.parse(result);
99
+ const rawParsed: unknown = JSON.parse(result);
100
+
101
+ // Claude CLI with --json-schema returns wrapper: {type, result, structured_output: {...}}
102
+ // Extract structured_output if present, otherwise use raw response
103
+ const parsed = this.extractStructuredOutput(rawParsed);
100
104
 
101
105
  // Validate and narrow type
102
106
  if (
@@ -232,4 +236,26 @@ ${this.truncateMarkdown(markdown, 100000)}`;
232
236
 
233
237
  return `${markdown.substring(0, maxLength)}\n\n[... content truncated ...]`;
234
238
  }
239
+
240
+ /**
241
+ * Type guard to check if value is a record (plain object)
242
+ */
243
+ private isRecord(value: unknown): value is Record<string, unknown> {
244
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
245
+ }
246
+
247
+ /**
248
+ * Extract structured_output from Claude CLI wrapper format if present.
249
+ * Claude CLI with --json-schema returns: {type, result, structured_output: {...}}
250
+ * This method extracts the inner structured_output, or returns the raw value if not wrapped.
251
+ */
252
+ private extractStructuredOutput(rawParsed: unknown): unknown {
253
+ if (this.isRecord(rawParsed) && 'structured_output' in rawParsed) {
254
+ const structuredOutput = rawParsed['structured_output'];
255
+ if (typeof structuredOutput === 'object') {
256
+ return structuredOutput;
257
+ }
258
+ }
259
+ return rawParsed;
260
+ }
235
261
  }
package/src/index.ts CHANGED
@@ -16,6 +16,7 @@ import { createSearchCommand } from './cli/commands/search.js';
16
16
  import { createServeCommand } from './cli/commands/serve.js';
17
17
  import { createSetupCommand } from './cli/commands/setup.js';
18
18
  import { createStoreCommand } from './cli/commands/store.js';
19
+ import { createSyncCommand } from './cli/commands/sync.js';
19
20
  import { createProgram, getGlobalOptions } from './cli/program.js';
20
21
 
21
22
  // Default paths
@@ -105,6 +106,7 @@ program.addCommand(createIndexCommand(() => getGlobalOptions(program)));
105
106
  program.addCommand(createServeCommand(() => getGlobalOptions(program)));
106
107
  program.addCommand(createCrawlCommand(() => getGlobalOptions(program)));
107
108
  program.addCommand(createSetupCommand(() => getGlobalOptions(program)));
109
+ program.addCommand(createSyncCommand(() => getGlobalOptions(program)));
108
110
  program.addCommand(createMCPCommand(() => getGlobalOptions(program)));
109
111
 
110
112
  // Show comprehensive help when no arguments provided
@@ -9,11 +9,13 @@ import { jobCommands } from './job.commands.js';
9
9
  import { metaCommands } from './meta.commands.js';
10
10
  import { commandRegistry } from './registry.js';
11
11
  import { storeCommands } from './store.commands.js';
12
+ import { syncCommands } from './sync.commands.js';
12
13
 
13
14
  // Register all commands
14
15
  commandRegistry.registerAll(storeCommands);
15
16
  commandRegistry.registerAll(jobCommands);
16
17
  commandRegistry.registerAll(metaCommands);
18
+ commandRegistry.registerAll(syncCommands);
17
19
 
18
20
  // Re-export for convenience
19
21
  export { commandRegistry, executeCommand, generateHelp } from './registry.js';