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
@@ -0,0 +1,233 @@
1
+ import { z } from 'zod';
2
+ import { StoreDefinitionService } from '../../services/store-definition.service.js';
3
+ import {
4
+ isFileStoreDefinition,
5
+ isRepoStoreDefinition,
6
+ isWebStoreDefinition,
7
+ } from '../../types/store-definition.js';
8
+ import type { CommandDefinition } from './registry.js';
9
+ import type { StoreDefinition } from '../../types/store-definition.js';
10
+ import type { HandlerContext, ToolResponse } from '../types.js';
11
+
12
+ /**
13
+ * Arguments for stores:sync command
14
+ */
15
+ export interface SyncStoresArgs {
16
+ reindex?: boolean;
17
+ prune?: boolean;
18
+ dryRun?: boolean;
19
+ }
20
+
21
+ /**
22
+ * Result of a sync operation
23
+ */
24
+ interface SyncResult {
25
+ created: string[];
26
+ skipped: string[];
27
+ failed: Array<{ name: string; error: string }>;
28
+ orphans: string[];
29
+ pruned?: string[];
30
+ dryRun?: boolean;
31
+ wouldCreate?: string[];
32
+ wouldPrune?: string[];
33
+ }
34
+
35
+ /**
36
+ * Handle stores:sync command
37
+ *
38
+ * Syncs stores from definitions config:
39
+ * - Creates missing stores from definitions
40
+ * - Reports stores not in definitions (orphans)
41
+ * - Optionally prunes orphan stores
42
+ * - Optionally re-indexes existing stores
43
+ */
44
+ export async function handleStoresSync(
45
+ args: SyncStoresArgs,
46
+ context: HandlerContext
47
+ ): Promise<ToolResponse> {
48
+ const { services, options } = context;
49
+ const projectRoot = options.projectRoot;
50
+
51
+ if (projectRoot === undefined) {
52
+ throw new Error('Project root is required for stores:sync');
53
+ }
54
+
55
+ const defService = new StoreDefinitionService(projectRoot);
56
+ const config = await defService.load();
57
+
58
+ const result: SyncResult = {
59
+ created: [],
60
+ skipped: [],
61
+ failed: [],
62
+ orphans: [],
63
+ };
64
+
65
+ if (args.dryRun === true) {
66
+ result.dryRun = true;
67
+ result.wouldCreate = [];
68
+ result.wouldPrune = [];
69
+ }
70
+
71
+ // Get existing stores
72
+ const existingStores = await services.store.list();
73
+ const existingNames = new Set(existingStores.map((s) => s.name));
74
+
75
+ // Process each definition
76
+ for (const def of config.stores) {
77
+ if (existingNames.has(def.name)) {
78
+ result.skipped.push(def.name);
79
+ continue;
80
+ }
81
+
82
+ if (args.dryRun === true) {
83
+ result.wouldCreate?.push(def.name);
84
+ continue;
85
+ }
86
+
87
+ // Try to create the store
88
+ const createResult = await createStoreFromDefinition(def, defService, services, context);
89
+ if (createResult.success) {
90
+ result.created.push(def.name);
91
+ } else {
92
+ result.failed.push({ name: def.name, error: createResult.error });
93
+ }
94
+ }
95
+
96
+ // Find orphans (stores not in definitions)
97
+ const definedNames = new Set(config.stores.map((d) => d.name));
98
+ for (const store of existingStores) {
99
+ if (!definedNames.has(store.name)) {
100
+ result.orphans.push(store.name);
101
+ }
102
+ }
103
+
104
+ // Prune orphans if requested
105
+ if (args.prune === true && result.orphans.length > 0) {
106
+ if (args.dryRun === true) {
107
+ result.wouldPrune = [...result.orphans];
108
+ } else {
109
+ result.pruned = [];
110
+ for (const orphanName of result.orphans) {
111
+ const store = await services.store.getByName(orphanName);
112
+ if (store !== undefined) {
113
+ const deleteResult = await services.store.delete(store.id, { skipDefinitionSync: true });
114
+ if (deleteResult.success) {
115
+ result.pruned.push(orphanName);
116
+ }
117
+ }
118
+ }
119
+ }
120
+ }
121
+
122
+ return {
123
+ content: [
124
+ {
125
+ type: 'text',
126
+ text: JSON.stringify(result, null, 2),
127
+ },
128
+ ],
129
+ };
130
+ }
131
+
132
+ /**
133
+ * Create a store from a definition
134
+ */
135
+ async function createStoreFromDefinition(
136
+ def: StoreDefinition,
137
+ defService: StoreDefinitionService,
138
+ services: HandlerContext['services'],
139
+ _context: HandlerContext
140
+ ): Promise<{ success: true } | { success: false; error: string }> {
141
+ try {
142
+ if (isFileStoreDefinition(def)) {
143
+ // Resolve path relative to project root
144
+ const resolvedPath = defService.resolvePath(def.path);
145
+ const createResult = await services.store.create(
146
+ {
147
+ name: def.name,
148
+ type: 'file',
149
+ path: resolvedPath,
150
+ description: def.description,
151
+ tags: def.tags,
152
+ },
153
+ { skipDefinitionSync: true } // Don't re-add to definitions
154
+ );
155
+ if (!createResult.success) {
156
+ return { success: false, error: createResult.error.message };
157
+ }
158
+ return { success: true };
159
+ }
160
+
161
+ if (isRepoStoreDefinition(def)) {
162
+ const createResult = await services.store.create(
163
+ {
164
+ name: def.name,
165
+ type: 'repo',
166
+ url: def.url,
167
+ branch: def.branch,
168
+ depth: def.depth,
169
+ description: def.description,
170
+ tags: def.tags,
171
+ },
172
+ { skipDefinitionSync: true }
173
+ );
174
+ if (!createResult.success) {
175
+ return { success: false, error: createResult.error.message };
176
+ }
177
+ return { success: true };
178
+ }
179
+
180
+ if (isWebStoreDefinition(def)) {
181
+ const createResult = await services.store.create(
182
+ {
183
+ name: def.name,
184
+ type: 'web',
185
+ url: def.url,
186
+ depth: def.depth,
187
+ description: def.description,
188
+ tags: def.tags,
189
+ },
190
+ { skipDefinitionSync: true }
191
+ );
192
+ if (!createResult.success) {
193
+ return { success: false, error: createResult.error.message };
194
+ }
195
+ return { success: true };
196
+ }
197
+
198
+ return { success: false, error: 'Unknown store definition type' };
199
+ } catch (error) {
200
+ return {
201
+ success: false,
202
+ error: error instanceof Error ? error.message : String(error),
203
+ };
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Sync commands for the execute meta-tool
209
+ */
210
+ export const syncCommands: CommandDefinition[] = [
211
+ {
212
+ name: 'stores:sync',
213
+ description: 'Sync stores from definitions config (bootstrap on fresh clone)',
214
+ argsSchema: z.object({
215
+ reindex: z.boolean().optional().describe('Re-index existing stores after sync'),
216
+ prune: z.boolean().optional().describe('Remove stores not in definitions'),
217
+ dryRun: z.boolean().optional().describe('Show what would happen without making changes'),
218
+ }),
219
+ handler: (args: Record<string, unknown>, context: HandlerContext): Promise<ToolResponse> => {
220
+ const syncArgs: SyncStoresArgs = {};
221
+ if (typeof args['reindex'] === 'boolean') {
222
+ syncArgs.reindex = args['reindex'];
223
+ }
224
+ if (typeof args['prune'] === 'boolean') {
225
+ syncArgs.prune = args['prune'];
226
+ }
227
+ if (typeof args['dryRun'] === 'boolean') {
228
+ syncArgs.dryRun = args['dryRun'];
229
+ }
230
+ return handleStoresSync(syncArgs, context);
231
+ },
232
+ },
233
+ ];
package/src/mcp/server.ts CHANGED
@@ -1,15 +1,23 @@
1
1
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
3
  import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
4
+ import { AdapterRegistry } from '../analysis/adapter-registry.js';
5
+ import { ZilAdapter } from '../analysis/zil/index.js';
6
+ import { createLogger } from '../logging/index.js';
4
7
  import { createServices } from '../services/index.js';
5
8
  import { handleExecute } from './handlers/execute.handler.js';
6
9
  import { tools } from './handlers/index.js';
7
10
  import { ExecuteArgsSchema } from './schemas/index.js';
8
- import { createLogger } from '../logging/index.js';
9
11
  import type { MCPServerOptions } from './types.js';
10
12
 
11
13
  const logger = createLogger('mcp-server');
12
14
 
15
+ // Register built-in language adapters
16
+ const registry = AdapterRegistry.getInstance();
17
+ if (!registry.hasExtension('.zil')) {
18
+ registry.register(new ZilAdapter());
19
+ }
20
+
13
21
  // eslint-disable-next-line @typescript-eslint/no-deprecated
14
22
  export function createMCPServer(options: MCPServerOptions): Server {
15
23
  // eslint-disable-next-line @typescript-eslint/no-deprecated
@@ -0,0 +1,157 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { GitignoreService } from './gitignore.service.js';
3
+ import { rm, mkdtemp, writeFile, readFile, access } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+
7
+ describe('GitignoreService', () => {
8
+ let projectRoot: string;
9
+ let service: GitignoreService;
10
+
11
+ beforeEach(async () => {
12
+ projectRoot = await mkdtemp(join(tmpdir(), 'gitignore-test-'));
13
+ service = new GitignoreService(projectRoot);
14
+ });
15
+
16
+ afterEach(async () => {
17
+ await rm(projectRoot, { recursive: true, force: true });
18
+ });
19
+
20
+ describe('hasRequiredPatterns', () => {
21
+ it('returns false when .gitignore does not exist', async () => {
22
+ const has = await service.hasRequiredPatterns();
23
+ expect(has).toBe(false);
24
+ });
25
+
26
+ it('returns false when .gitignore exists but is empty', async () => {
27
+ await writeFile(join(projectRoot, '.gitignore'), '');
28
+ const has = await service.hasRequiredPatterns();
29
+ expect(has).toBe(false);
30
+ });
31
+
32
+ it('returns false when .gitignore is missing bluera patterns', async () => {
33
+ await writeFile(join(projectRoot, '.gitignore'), 'node_modules/\n*.log\n');
34
+ const has = await service.hasRequiredPatterns();
35
+ expect(has).toBe(false);
36
+ });
37
+
38
+ it('returns true when all required patterns are present', async () => {
39
+ const content = `
40
+ node_modules/
41
+ .bluera/
42
+ !.bluera/bluera-knowledge/
43
+ !.bluera/bluera-knowledge/stores.config.json
44
+ `;
45
+ await writeFile(join(projectRoot, '.gitignore'), content);
46
+ const has = await service.hasRequiredPatterns();
47
+ expect(has).toBe(true);
48
+ });
49
+
50
+ it('returns false when only some patterns are present', async () => {
51
+ const content = `
52
+ .bluera/
53
+ `;
54
+ await writeFile(join(projectRoot, '.gitignore'), content);
55
+ const has = await service.hasRequiredPatterns();
56
+ expect(has).toBe(false);
57
+ });
58
+ });
59
+
60
+ describe('ensureGitignorePatterns', () => {
61
+ it('creates .gitignore if it does not exist', async () => {
62
+ const result = await service.ensureGitignorePatterns();
63
+
64
+ expect(result.updated).toBe(true);
65
+ expect(result.message).toContain('Created');
66
+
67
+ // Verify file exists
68
+ const gitignorePath = join(projectRoot, '.gitignore');
69
+ await expect(access(gitignorePath)).resolves.toBeUndefined();
70
+ });
71
+
72
+ it('adds patterns to empty .gitignore', async () => {
73
+ await writeFile(join(projectRoot, '.gitignore'), '');
74
+
75
+ const result = await service.ensureGitignorePatterns();
76
+
77
+ expect(result.updated).toBe(true);
78
+ expect(result.message).toContain('Updated');
79
+
80
+ const content = await readFile(join(projectRoot, '.gitignore'), 'utf-8');
81
+ expect(content).toContain('.bluera/');
82
+ expect(content).toContain('!.bluera/bluera-knowledge/');
83
+ expect(content).toContain('!.bluera/bluera-knowledge/stores.config.json');
84
+ });
85
+
86
+ it('appends patterns to existing .gitignore', async () => {
87
+ const existingContent = 'node_modules/\n*.log\n';
88
+ await writeFile(join(projectRoot, '.gitignore'), existingContent);
89
+
90
+ const result = await service.ensureGitignorePatterns();
91
+
92
+ expect(result.updated).toBe(true);
93
+
94
+ const content = await readFile(join(projectRoot, '.gitignore'), 'utf-8');
95
+ // Should preserve existing content
96
+ expect(content).toContain('node_modules/');
97
+ expect(content).toContain('*.log');
98
+ // Should add new patterns
99
+ expect(content).toContain('.bluera/');
100
+ expect(content).toContain('!.bluera/bluera-knowledge/');
101
+ });
102
+
103
+ it('skips if all patterns already present', async () => {
104
+ const existingContent = `
105
+ node_modules/
106
+ .bluera/
107
+ !.bluera/bluera-knowledge/
108
+ !.bluera/bluera-knowledge/stores.config.json
109
+ `;
110
+ await writeFile(join(projectRoot, '.gitignore'), existingContent);
111
+
112
+ const result = await service.ensureGitignorePatterns();
113
+
114
+ expect(result.updated).toBe(false);
115
+ expect(result.message).toContain('already');
116
+ });
117
+
118
+ it('adds missing patterns when some are present', async () => {
119
+ const existingContent = '.bluera/\n';
120
+ await writeFile(join(projectRoot, '.gitignore'), existingContent);
121
+
122
+ const result = await service.ensureGitignorePatterns();
123
+
124
+ expect(result.updated).toBe(true);
125
+
126
+ const content = await readFile(join(projectRoot, '.gitignore'), 'utf-8');
127
+ expect(content).toContain('!.bluera/bluera-knowledge/');
128
+ expect(content).toContain('!.bluera/bluera-knowledge/stores.config.json');
129
+ });
130
+
131
+ it('includes header comment in new additions', async () => {
132
+ const result = await service.ensureGitignorePatterns();
133
+
134
+ expect(result.updated).toBe(true);
135
+
136
+ const content = await readFile(join(projectRoot, '.gitignore'), 'utf-8');
137
+ expect(content).toContain('# Bluera Knowledge');
138
+ });
139
+
140
+ it('preserves trailing newline', async () => {
141
+ const existingContent = 'node_modules/\n';
142
+ await writeFile(join(projectRoot, '.gitignore'), existingContent);
143
+
144
+ await service.ensureGitignorePatterns();
145
+
146
+ const content = await readFile(join(projectRoot, '.gitignore'), 'utf-8');
147
+ expect(content.endsWith('\n')).toBe(true);
148
+ });
149
+ });
150
+
151
+ describe('getGitignorePath', () => {
152
+ it('returns correct path', () => {
153
+ const path = service.getGitignorePath();
154
+ expect(path).toBe(join(projectRoot, '.gitignore'));
155
+ });
156
+ });
157
+ });
@@ -0,0 +1,132 @@
1
+ import { readFile, writeFile, access } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+
4
+ /**
5
+ * Required .gitignore patterns for Bluera Knowledge
6
+ *
7
+ * These patterns ensure:
8
+ * - The .bluera/ data directory (vector DB, cloned repos) is ignored
9
+ * - The stores.config.json file is NOT ignored (committed for team sharing)
10
+ */
11
+ const REQUIRED_PATTERNS = [
12
+ '.bluera/',
13
+ '!.bluera/bluera-knowledge/',
14
+ '!.bluera/bluera-knowledge/stores.config.json',
15
+ ];
16
+
17
+ /**
18
+ * Header comment for the gitignore section
19
+ */
20
+ const SECTION_HEADER = `
21
+ # Bluera Knowledge - data directory (not committed)
22
+ # Store definitions at .bluera/bluera-knowledge/stores.config.json ARE committed for team sharing
23
+ `;
24
+
25
+ /**
26
+ * Check if a file exists
27
+ */
28
+ async function fileExists(path: string): Promise<boolean> {
29
+ try {
30
+ await access(path);
31
+ return true;
32
+ } catch {
33
+ return false;
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Service for managing .gitignore patterns for Bluera Knowledge.
39
+ *
40
+ * When stores are created, this service ensures the project's .gitignore
41
+ * is updated to:
42
+ * - Ignore the .bluera/ data directory (not committed)
43
+ * - Allow committing .bluera/bluera-knowledge/stores.config.json (for team sharing)
44
+ */
45
+ export class GitignoreService {
46
+ private readonly gitignorePath: string;
47
+
48
+ constructor(projectRoot: string) {
49
+ this.gitignorePath = join(projectRoot, '.gitignore');
50
+ }
51
+
52
+ /**
53
+ * Check if all required patterns are present in .gitignore
54
+ */
55
+ async hasRequiredPatterns(): Promise<boolean> {
56
+ const exists = await fileExists(this.gitignorePath);
57
+ if (!exists) {
58
+ return false;
59
+ }
60
+
61
+ const content = await readFile(this.gitignorePath, 'utf-8');
62
+ const lines = content.split('\n').map((l) => l.trim());
63
+
64
+ for (const pattern of REQUIRED_PATTERNS) {
65
+ if (!lines.includes(pattern)) {
66
+ return false;
67
+ }
68
+ }
69
+
70
+ return true;
71
+ }
72
+
73
+ /**
74
+ * Ensure required .gitignore patterns are present.
75
+ *
76
+ * - Creates .gitignore if it doesn't exist
77
+ * - Appends missing patterns if .gitignore exists
78
+ * - Does nothing if all patterns are already present
79
+ *
80
+ * @returns Object with updated flag and descriptive message
81
+ */
82
+ async ensureGitignorePatterns(): Promise<{ updated: boolean; message: string }> {
83
+ const exists = await fileExists(this.gitignorePath);
84
+
85
+ if (!exists) {
86
+ // Create new .gitignore with our patterns
87
+ const content = `${SECTION_HEADER.trim()}\n${REQUIRED_PATTERNS.join('\n')}\n`;
88
+ await writeFile(this.gitignorePath, content);
89
+ return {
90
+ updated: true,
91
+ message: 'Created .gitignore with Bluera Knowledge patterns',
92
+ };
93
+ }
94
+
95
+ // Read existing content
96
+ const existingContent = await readFile(this.gitignorePath, 'utf-8');
97
+ const lines = existingContent.split('\n').map((l) => l.trim());
98
+
99
+ // Find missing patterns
100
+ const missingPatterns = REQUIRED_PATTERNS.filter((pattern) => !lines.includes(pattern));
101
+
102
+ if (missingPatterns.length === 0) {
103
+ return {
104
+ updated: false,
105
+ message: 'All Bluera Knowledge patterns already present in .gitignore',
106
+ };
107
+ }
108
+
109
+ // Append missing patterns
110
+ let newContent = existingContent;
111
+ if (!newContent.endsWith('\n')) {
112
+ newContent += '\n';
113
+ }
114
+
115
+ newContent += SECTION_HEADER;
116
+ newContent += `${missingPatterns.join('\n')}\n`;
117
+
118
+ await writeFile(this.gitignorePath, newContent);
119
+
120
+ return {
121
+ updated: true,
122
+ message: `Updated .gitignore with ${String(missingPatterns.length)} Bluera Knowledge pattern(s)`,
123
+ };
124
+ }
125
+
126
+ /**
127
+ * Get the path to the .gitignore file
128
+ */
129
+ getGitignorePath(): string {
130
+ return this.gitignorePath;
131
+ }
132
+ }