bluera-knowledge 0.10.0 → 0.11.0

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 (49) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +25 -0
  3. package/README.md +98 -2
  4. package/commands/sync.md +96 -0
  5. package/dist/{chunk-ITH6FWQY.js → chunk-2WBITQWZ.js} +24 -3
  6. package/dist/{chunk-ITH6FWQY.js.map → chunk-2WBITQWZ.js.map} +1 -1
  7. package/dist/{chunk-CUHYSPRV.js → chunk-565OVW3C.js} +999 -2
  8. package/dist/chunk-565OVW3C.js.map +1 -0
  9. package/dist/{chunk-DWAIT2OD.js → chunk-TRDMYKGC.js} +190 -5
  10. package/dist/chunk-TRDMYKGC.js.map +1 -0
  11. package/dist/index.js +217 -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/analysis/adapter-registry.test.ts +211 -0
  17. package/src/analysis/adapter-registry.ts +155 -0
  18. package/src/analysis/language-adapter.ts +127 -0
  19. package/src/analysis/parser-factory.test.ts +79 -1
  20. package/src/analysis/parser-factory.ts +8 -0
  21. package/src/analysis/zil/index.ts +34 -0
  22. package/src/analysis/zil/zil-adapter.test.ts +187 -0
  23. package/src/analysis/zil/zil-adapter.ts +121 -0
  24. package/src/analysis/zil/zil-lexer.test.ts +222 -0
  25. package/src/analysis/zil/zil-lexer.ts +239 -0
  26. package/src/analysis/zil/zil-parser.test.ts +210 -0
  27. package/src/analysis/zil/zil-parser.ts +360 -0
  28. package/src/analysis/zil/zil-special-forms.ts +193 -0
  29. package/src/cli/commands/sync.test.ts +54 -0
  30. package/src/cli/commands/sync.ts +264 -0
  31. package/src/cli/index.ts +1 -0
  32. package/src/crawl/claude-client.test.ts +56 -0
  33. package/src/crawl/claude-client.ts +27 -1
  34. package/src/index.ts +8 -0
  35. package/src/mcp/commands/index.ts +2 -0
  36. package/src/mcp/commands/sync.commands.test.ts +283 -0
  37. package/src/mcp/commands/sync.commands.ts +233 -0
  38. package/src/mcp/server.ts +9 -1
  39. package/src/services/gitignore.service.test.ts +157 -0
  40. package/src/services/gitignore.service.ts +132 -0
  41. package/src/services/store-definition.service.test.ts +440 -0
  42. package/src/services/store-definition.service.ts +198 -0
  43. package/src/services/store.service.test.ts +279 -1
  44. package/src/services/store.service.ts +101 -4
  45. package/src/types/index.ts +18 -0
  46. package/src/types/store-definition.test.ts +492 -0
  47. package/src/types/store-definition.ts +129 -0
  48. package/dist/chunk-CUHYSPRV.js.map +0 -1
  49. package/dist/chunk-DWAIT2OD.js.map +0 -1
@@ -1,6 +1,7 @@
1
1
  import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
2
  import { StoreService } from './store.service.js';
3
- import { rm, mkdtemp, writeFile, access } from 'node:fs/promises';
3
+ import { StoreDefinitionService } from './store-definition.service.js';
4
+ import { rm, mkdtemp, writeFile, readFile, access } from 'node:fs/promises';
4
5
  import { tmpdir } from 'node:os';
5
6
  import { join } from 'node:path';
6
7
 
@@ -538,4 +539,281 @@ describe('StoreService', () => {
538
539
  await rm(corruptDir, { recursive: true, force: true });
539
540
  });
540
541
  });
542
+
543
+ describe('store definition auto-update', () => {
544
+ let projectRoot: string;
545
+ let dataDir: string;
546
+ let serviceWithDefs: StoreService;
547
+ let defService: StoreDefinitionService;
548
+
549
+ beforeEach(async () => {
550
+ projectRoot = await mkdtemp(join(tmpdir(), 'store-def-auto-'));
551
+ dataDir = join(projectRoot, '.bluera/bluera-knowledge/data');
552
+ defService = new StoreDefinitionService(projectRoot);
553
+ serviceWithDefs = new StoreService(dataDir, { definitionService: defService });
554
+ await serviceWithDefs.initialize();
555
+ });
556
+
557
+ afterEach(async () => {
558
+ await rm(projectRoot, { recursive: true, force: true });
559
+ });
560
+
561
+ describe('create adds definition', () => {
562
+ it('adds file store definition when creating file store', async () => {
563
+ const storeDir = await mkdtemp(join(tmpdir(), 'file-store-'));
564
+ const result = await serviceWithDefs.create({
565
+ name: 'my-docs',
566
+ type: 'file',
567
+ path: storeDir,
568
+ description: 'My documentation',
569
+ tags: ['docs'],
570
+ });
571
+
572
+ expect(result.success).toBe(true);
573
+
574
+ const def = await defService.getByName('my-docs');
575
+ expect(def).toBeDefined();
576
+ expect(def?.type).toBe('file');
577
+ expect(def?.name).toBe('my-docs');
578
+ if (def?.type === 'file') {
579
+ expect(def.path).toBe(storeDir);
580
+ }
581
+ expect(def?.description).toBe('My documentation');
582
+ expect(def?.tags).toEqual(['docs']);
583
+
584
+ await rm(storeDir, { recursive: true, force: true });
585
+ });
586
+
587
+ it('adds repo store definition when creating repo store with path', async () => {
588
+ const repoDir = await mkdtemp(join(tmpdir(), 'repo-store-'));
589
+ const result = await serviceWithDefs.create({
590
+ name: 'my-repo',
591
+ type: 'repo',
592
+ path: repoDir,
593
+ branch: 'main',
594
+ description: 'Example repo',
595
+ });
596
+
597
+ expect(result.success).toBe(true);
598
+
599
+ const def = await defService.getByName('my-repo');
600
+ expect(def).toBeDefined();
601
+ expect(def?.type).toBe('repo');
602
+ if (def?.type === 'repo') {
603
+ expect(def.branch).toBe('main');
604
+ }
605
+
606
+ await rm(repoDir, { recursive: true, force: true });
607
+ });
608
+
609
+ it('adds web store definition when creating web store', async () => {
610
+ const result = await serviceWithDefs.create({
611
+ name: 'my-site',
612
+ type: 'web',
613
+ url: 'https://example.com/docs',
614
+ depth: 2,
615
+ description: 'Example site',
616
+ });
617
+
618
+ expect(result.success).toBe(true);
619
+
620
+ const def = await defService.getByName('my-site');
621
+ expect(def).toBeDefined();
622
+ expect(def?.type).toBe('web');
623
+ if (def?.type === 'web') {
624
+ expect(def.url).toBe('https://example.com/docs');
625
+ expect(def.depth).toBe(2);
626
+ }
627
+ });
628
+
629
+ it('does not add definition when store creation fails', async () => {
630
+ const result = await serviceWithDefs.create({
631
+ name: 'bad-store',
632
+ type: 'file',
633
+ path: '/nonexistent/path',
634
+ });
635
+
636
+ expect(result.success).toBe(false);
637
+
638
+ const def = await defService.getByName('bad-store');
639
+ expect(def).toBeUndefined();
640
+ });
641
+
642
+ it('does not add definition when skipDefinitionSync is true', async () => {
643
+ const storeDir = await mkdtemp(join(tmpdir(), 'skip-def-'));
644
+ const result = await serviceWithDefs.create(
645
+ {
646
+ name: 'skip-store',
647
+ type: 'file',
648
+ path: storeDir,
649
+ },
650
+ { skipDefinitionSync: true }
651
+ );
652
+
653
+ expect(result.success).toBe(true);
654
+
655
+ const def = await defService.getByName('skip-store');
656
+ expect(def).toBeUndefined();
657
+
658
+ await rm(storeDir, { recursive: true, force: true });
659
+ });
660
+ });
661
+
662
+ describe('delete removes definition', () => {
663
+ it('removes definition when store is deleted', async () => {
664
+ const storeDir = await mkdtemp(join(tmpdir(), 'del-store-'));
665
+ const createResult = await serviceWithDefs.create({
666
+ name: 'to-delete',
667
+ type: 'file',
668
+ path: storeDir,
669
+ });
670
+
671
+ if (!createResult.success) throw new Error('Create failed');
672
+
673
+ // Verify definition exists
674
+ let def = await defService.getByName('to-delete');
675
+ expect(def).toBeDefined();
676
+
677
+ // Delete the store
678
+ const deleteResult = await serviceWithDefs.delete(createResult.data.id);
679
+ expect(deleteResult.success).toBe(true);
680
+
681
+ // Definition should be removed
682
+ def = await defService.getByName('to-delete');
683
+ expect(def).toBeUndefined();
684
+
685
+ await rm(storeDir, { recursive: true, force: true });
686
+ });
687
+
688
+ it('does not remove definition when skipDefinitionSync is true', async () => {
689
+ const storeDir = await mkdtemp(join(tmpdir(), 'del-skip-'));
690
+ const createResult = await serviceWithDefs.create({
691
+ name: 'keep-def',
692
+ type: 'file',
693
+ path: storeDir,
694
+ });
695
+
696
+ if (!createResult.success) throw new Error('Create failed');
697
+
698
+ // Delete with skipDefinitionSync
699
+ const deleteResult = await serviceWithDefs.delete(createResult.data.id, {
700
+ skipDefinitionSync: true,
701
+ });
702
+ expect(deleteResult.success).toBe(true);
703
+
704
+ // Definition should still exist
705
+ const def = await defService.getByName('keep-def');
706
+ expect(def).toBeDefined();
707
+
708
+ await rm(storeDir, { recursive: true, force: true });
709
+ });
710
+ });
711
+
712
+ describe('update syncs definition', () => {
713
+ it('updates definition when store description is updated', async () => {
714
+ const storeDir = await mkdtemp(join(tmpdir(), 'upd-store-'));
715
+ const createResult = await serviceWithDefs.create({
716
+ name: 'to-update',
717
+ type: 'file',
718
+ path: storeDir,
719
+ description: 'Original description',
720
+ });
721
+
722
+ if (!createResult.success) throw new Error('Create failed');
723
+
724
+ const updateResult = await serviceWithDefs.update(createResult.data.id, {
725
+ description: 'Updated description',
726
+ });
727
+ expect(updateResult.success).toBe(true);
728
+
729
+ const def = await defService.getByName('to-update');
730
+ expect(def?.description).toBe('Updated description');
731
+
732
+ await rm(storeDir, { recursive: true, force: true });
733
+ });
734
+
735
+ it('updates definition when store tags are updated', async () => {
736
+ const storeDir = await mkdtemp(join(tmpdir(), 'upd-tags-'));
737
+ const createResult = await serviceWithDefs.create({
738
+ name: 'tag-store',
739
+ type: 'file',
740
+ path: storeDir,
741
+ tags: ['old'],
742
+ });
743
+
744
+ if (!createResult.success) throw new Error('Create failed');
745
+
746
+ const updateResult = await serviceWithDefs.update(createResult.data.id, {
747
+ tags: ['new', 'tags'],
748
+ });
749
+ expect(updateResult.success).toBe(true);
750
+
751
+ const def = await defService.getByName('tag-store');
752
+ expect(def?.tags).toEqual(['new', 'tags']);
753
+
754
+ await rm(storeDir, { recursive: true, force: true });
755
+ });
756
+
757
+ it('does not update definition when skipDefinitionSync is true', async () => {
758
+ const storeDir = await mkdtemp(join(tmpdir(), 'upd-skip-'));
759
+ const createResult = await serviceWithDefs.create({
760
+ name: 'skip-update',
761
+ type: 'file',
762
+ path: storeDir,
763
+ description: 'Original',
764
+ });
765
+
766
+ if (!createResult.success) throw new Error('Create failed');
767
+
768
+ const updateResult = await serviceWithDefs.update(
769
+ createResult.data.id,
770
+ { description: 'Updated' },
771
+ { skipDefinitionSync: true }
772
+ );
773
+ expect(updateResult.success).toBe(true);
774
+
775
+ const def = await defService.getByName('skip-update');
776
+ expect(def?.description).toBe('Original');
777
+
778
+ await rm(storeDir, { recursive: true, force: true });
779
+ });
780
+ });
781
+
782
+ describe('persistence', () => {
783
+ it('persists definition to config file', async () => {
784
+ const storeDir = await mkdtemp(join(tmpdir(), 'persist-def-'));
785
+ await serviceWithDefs.create({
786
+ name: 'persistent-store',
787
+ type: 'file',
788
+ path: storeDir,
789
+ });
790
+
791
+ // Read config file directly
792
+ const configPath = join(projectRoot, '.bluera/bluera-knowledge/stores.config.json');
793
+ const content = await readFile(configPath, 'utf-8');
794
+ const config = JSON.parse(content);
795
+
796
+ expect(config.stores).toHaveLength(1);
797
+ expect(config.stores[0].name).toBe('persistent-store');
798
+
799
+ await rm(storeDir, { recursive: true, force: true });
800
+ });
801
+ });
802
+
803
+ describe('without definition service', () => {
804
+ it('works normally without definition service injected', async () => {
805
+ // Use the storeService from outer scope (no definition service)
806
+ const storeDir = await mkdtemp(join(tmpdir(), 'no-def-'));
807
+ const result = await storeService.create({
808
+ name: 'no-def-store',
809
+ type: 'file',
810
+ path: storeDir,
811
+ });
812
+
813
+ expect(result.success).toBe(true);
814
+
815
+ await rm(storeDir, { recursive: true, force: true });
816
+ });
817
+ });
818
+ });
541
819
  });
@@ -4,8 +4,15 @@ import { join, resolve } from 'node:path';
4
4
  import { cloneRepository } from '../plugin/git-clone.js';
5
5
  import { createStoreId } from '../types/brands.js';
6
6
  import { ok, err } from '../types/result.js';
7
+ import type { StoreDefinitionService } from './store-definition.service.js';
7
8
  import type { StoreId } from '../types/brands.js';
8
9
  import type { Result } from '../types/result.js';
10
+ import type {
11
+ StoreDefinition,
12
+ FileStoreDefinition,
13
+ RepoStoreDefinition,
14
+ WebStoreDefinition,
15
+ } from '../types/store-definition.js';
9
16
  import type { Store, FileStore, RepoStore, WebStore, StoreType } from '../types/store.js';
10
17
 
11
18
  /**
@@ -31,16 +38,28 @@ export interface CreateStoreInput {
31
38
  depth?: number | undefined;
32
39
  }
33
40
 
41
+ export interface StoreServiceOptions {
42
+ /** Optional definition service for auto-updating git-committable config */
43
+ definitionService?: StoreDefinitionService;
44
+ }
45
+
46
+ export interface OperationOptions {
47
+ /** Skip syncing to store definitions (used by stores:sync command) */
48
+ skipDefinitionSync?: boolean;
49
+ }
50
+
34
51
  interface StoreRegistry {
35
52
  stores: Store[];
36
53
  }
37
54
 
38
55
  export class StoreService {
39
56
  private readonly dataDir: string;
57
+ private readonly definitionService: StoreDefinitionService | undefined;
40
58
  private registry: StoreRegistry = { stores: [] };
41
59
 
42
- constructor(dataDir: string) {
60
+ constructor(dataDir: string, options?: StoreServiceOptions) {
43
61
  this.dataDir = dataDir;
62
+ this.definitionService = options?.definitionService ?? undefined;
44
63
  }
45
64
 
46
65
  async initialize(): Promise<void> {
@@ -48,7 +67,54 @@ export class StoreService {
48
67
  await this.loadRegistry();
49
68
  }
50
69
 
51
- async create(input: CreateStoreInput): Promise<Result<Store>> {
70
+ /**
71
+ * Convert a Store and CreateStoreInput to a StoreDefinition for persistence.
72
+ */
73
+ private createDefinitionFromStore(store: Store, input: CreateStoreInput): StoreDefinition {
74
+ // Copy tags array to convert from readonly to mutable
75
+ const tags = store.tags !== undefined ? [...store.tags] : undefined;
76
+ const base = {
77
+ name: store.name,
78
+ description: store.description,
79
+ tags,
80
+ };
81
+
82
+ switch (store.type) {
83
+ case 'file': {
84
+ const fileStore = store;
85
+ const fileDef: FileStoreDefinition = {
86
+ ...base,
87
+ type: 'file',
88
+ // Use original input path if provided (may be relative), otherwise use normalized
89
+ path: input.path ?? fileStore.path,
90
+ };
91
+ return fileDef;
92
+ }
93
+ case 'repo': {
94
+ const repoStore = store;
95
+ const repoDef: RepoStoreDefinition = {
96
+ ...base,
97
+ type: 'repo',
98
+ url: repoStore.url ?? '',
99
+ branch: repoStore.branch,
100
+ depth: input.depth,
101
+ };
102
+ return repoDef;
103
+ }
104
+ case 'web': {
105
+ const webStore = store;
106
+ const webDef: WebStoreDefinition = {
107
+ ...base,
108
+ type: 'web',
109
+ url: webStore.url,
110
+ depth: webStore.depth,
111
+ };
112
+ return webDef;
113
+ }
114
+ }
115
+ }
116
+
117
+ async create(input: CreateStoreInput, options?: OperationOptions): Promise<Result<Store>> {
52
118
  if (!input.name || input.name.trim() === '') {
53
119
  return err(new Error('Store name cannot be empty'));
54
120
  }
@@ -157,6 +223,12 @@ export class StoreService {
157
223
  this.registry.stores.push(store);
158
224
  await this.saveRegistry();
159
225
 
226
+ // Sync to store definitions if service is available and not skipped
227
+ if (this.definitionService !== undefined && options?.skipDefinitionSync !== true) {
228
+ const definition = this.createDefinitionFromStore(store, input);
229
+ await this.definitionService.addDefinition(definition);
230
+ }
231
+
160
232
  return ok(store);
161
233
  }
162
234
 
@@ -183,7 +255,8 @@ export class StoreService {
183
255
 
184
256
  async update(
185
257
  id: StoreId,
186
- updates: Partial<Pick<Store, 'name' | 'description' | 'tags'>>
258
+ updates: Partial<Pick<Store, 'name' | 'description' | 'tags'>>,
259
+ options?: OperationOptions
187
260
  ): Promise<Result<Store>> {
188
261
  const index = this.registry.stores.findIndex((s) => s.id === id);
189
262
  if (index === -1) {
@@ -205,18 +278,42 @@ export class StoreService {
205
278
  this.registry.stores[index] = updated;
206
279
  await this.saveRegistry();
207
280
 
281
+ // Sync to store definitions if service is available and not skipped
282
+ if (this.definitionService !== undefined && options?.skipDefinitionSync !== true) {
283
+ const defUpdates: { description?: string; tags?: string[] } = {};
284
+ if (updates.description !== undefined) {
285
+ defUpdates.description = updates.description;
286
+ }
287
+ if (updates.tags !== undefined) {
288
+ // Copy tags array to convert from readonly to mutable
289
+ defUpdates.tags = [...updates.tags];
290
+ }
291
+ await this.definitionService.updateDefinition(store.name, defUpdates);
292
+ }
293
+
208
294
  return ok(updated);
209
295
  }
210
296
 
211
- async delete(id: StoreId): Promise<Result<void>> {
297
+ async delete(id: StoreId, options?: OperationOptions): Promise<Result<void>> {
212
298
  const index = this.registry.stores.findIndex((s) => s.id === id);
213
299
  if (index === -1) {
214
300
  return err(new Error(`Store not found: ${id}`));
215
301
  }
216
302
 
303
+ const store = this.registry.stores[index];
304
+ if (store === undefined) {
305
+ return err(new Error(`Store not found: ${id}`));
306
+ }
307
+
308
+ const storeName = store.name;
217
309
  this.registry.stores.splice(index, 1);
218
310
  await this.saveRegistry();
219
311
 
312
+ // Sync to store definitions if service is available and not skipped
313
+ if (this.definitionService !== undefined && options?.skipDefinitionSync !== true) {
314
+ await this.definitionService.removeDefinition(storeName);
315
+ }
316
+
220
317
  return ok(undefined);
221
318
  }
222
319
 
@@ -53,3 +53,21 @@ export {
53
53
 
54
54
  // Progress types
55
55
  export { type ProgressEvent, type ProgressCallback } from './progress.js';
56
+
57
+ // Store definition types (for git-committable config)
58
+ export {
59
+ type StoreDefinition,
60
+ type FileStoreDefinition,
61
+ type RepoStoreDefinition,
62
+ type WebStoreDefinition,
63
+ type StoreDefinitionsConfig,
64
+ StoreDefinitionSchema,
65
+ FileStoreDefinitionSchema,
66
+ RepoStoreDefinitionSchema,
67
+ WebStoreDefinitionSchema,
68
+ StoreDefinitionsConfigSchema,
69
+ isFileStoreDefinition,
70
+ isRepoStoreDefinition,
71
+ isWebStoreDefinition,
72
+ DEFAULT_STORE_DEFINITIONS_CONFIG,
73
+ } from './store-definition.js';