byterover-cli 1.1.0 → 1.2.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 (103) hide show
  1. package/README.md +8 -4
  2. package/dist/commands/mcp.d.ts +13 -0
  3. package/dist/commands/mcp.js +61 -0
  4. package/dist/core/domain/cipher/agent-events/types.d.ts +44 -1
  5. package/dist/core/domain/entities/agent.js +72 -18
  6. package/dist/core/domain/entities/connector-type.d.ts +2 -1
  7. package/dist/core/domain/entities/connector-type.js +2 -1
  8. package/dist/core/interfaces/connectors/connector-types.d.ts +13 -0
  9. package/dist/core/interfaces/i-mcp-config-writer.d.ts +40 -0
  10. package/dist/core/interfaces/i-mcp-config-writer.js +1 -0
  11. package/dist/core/interfaces/i-rule-template-service.d.ts +4 -2
  12. package/dist/core/interfaces/transport/i-transport-client.d.ts +7 -0
  13. package/dist/infra/cipher/agent/cipher-agent.d.ts +8 -0
  14. package/dist/infra/cipher/agent/cipher-agent.js +16 -0
  15. package/dist/infra/cipher/llm/context/context-manager.d.ts +8 -0
  16. package/dist/infra/cipher/llm/context/context-manager.js +16 -0
  17. package/dist/infra/cipher/llm/internal-llm-service.d.ts +4 -0
  18. package/dist/infra/cipher/llm/internal-llm-service.js +38 -10
  19. package/dist/infra/cipher/session/chat-session.d.ts +3 -0
  20. package/dist/infra/cipher/session/chat-session.js +7 -1
  21. package/dist/infra/cipher/tools/implementations/curate-tool.d.ts +1 -8
  22. package/dist/infra/cipher/tools/implementations/curate-tool.js +360 -22
  23. package/dist/infra/connectors/connector-manager.js +2 -0
  24. package/dist/infra/connectors/mcp/index.d.ts +4 -0
  25. package/dist/infra/connectors/mcp/index.js +4 -0
  26. package/dist/infra/connectors/mcp/json-mcp-config-writer.d.ts +26 -0
  27. package/dist/infra/connectors/mcp/json-mcp-config-writer.js +71 -0
  28. package/dist/infra/connectors/mcp/mcp-connector-config.d.ts +229 -0
  29. package/dist/infra/connectors/mcp/mcp-connector-config.js +173 -0
  30. package/dist/infra/connectors/mcp/mcp-connector.d.ts +80 -0
  31. package/dist/infra/connectors/mcp/mcp-connector.js +324 -0
  32. package/dist/infra/connectors/mcp/toml-mcp-config-writer.d.ts +45 -0
  33. package/dist/infra/connectors/mcp/toml-mcp-config-writer.js +134 -0
  34. package/dist/infra/connectors/rules/rules-connector.d.ts +1 -8
  35. package/dist/infra/connectors/rules/rules-connector.js +20 -85
  36. package/dist/infra/connectors/shared/rule-file-manager.d.ts +72 -0
  37. package/dist/infra/connectors/shared/rule-file-manager.js +119 -0
  38. package/dist/infra/connectors/shared/template-service.d.ts +10 -1
  39. package/dist/infra/connectors/shared/template-service.js +53 -16
  40. package/dist/infra/mcp/index.d.ts +2 -0
  41. package/dist/infra/mcp/index.js +2 -0
  42. package/dist/infra/mcp/mcp-server.d.ts +58 -0
  43. package/dist/infra/mcp/mcp-server.js +178 -0
  44. package/dist/infra/mcp/tools/brv-curate-tool.d.ts +23 -0
  45. package/dist/infra/mcp/tools/brv-curate-tool.js +68 -0
  46. package/dist/infra/mcp/tools/brv-query-tool.d.ts +17 -0
  47. package/dist/infra/mcp/tools/brv-query-tool.js +68 -0
  48. package/dist/infra/mcp/tools/index.d.ts +3 -0
  49. package/dist/infra/mcp/tools/index.js +3 -0
  50. package/dist/infra/mcp/tools/task-result-waiter.d.ts +30 -0
  51. package/dist/infra/mcp/tools/task-result-waiter.js +56 -0
  52. package/dist/infra/process/agent-worker.js +37 -0
  53. package/dist/infra/repl/commands/curate-command.js +2 -2
  54. package/dist/infra/transport/socket-io-transport-client.d.ts +7 -0
  55. package/dist/infra/transport/socket-io-transport-client.js +25 -0
  56. package/dist/infra/transport/socket-io-transport-server.js +4 -0
  57. package/dist/infra/usecase/connectors-use-case.d.ts +4 -0
  58. package/dist/infra/usecase/connectors-use-case.js +29 -10
  59. package/dist/infra/usecase/init-use-case.js +2 -3
  60. package/dist/infra/usecase/status-use-case.d.ts +10 -0
  61. package/dist/infra/usecase/status-use-case.js +53 -0
  62. package/dist/resources/prompts/curate.yml +107 -4
  63. package/dist/templates/mcp-base.md +1 -0
  64. package/dist/templates/sections/mcp-workflow.md +13 -0
  65. package/dist/tui/app.js +4 -1
  66. package/dist/tui/components/command-details.js +1 -1
  67. package/dist/tui/components/execution/execution-changes.d.ts +2 -0
  68. package/dist/tui/components/execution/execution-changes.js +5 -1
  69. package/dist/tui/components/execution/execution-content.d.ts +2 -0
  70. package/dist/tui/components/execution/execution-content.js +8 -18
  71. package/dist/tui/components/execution/execution-input.d.ts +2 -0
  72. package/dist/tui/components/execution/execution-input.js +6 -4
  73. package/dist/tui/components/execution/execution-progress.d.ts +2 -0
  74. package/dist/tui/components/execution/execution-progress.js +6 -2
  75. package/dist/tui/components/execution/expanded-log-view.d.ts +20 -0
  76. package/dist/tui/components/execution/expanded-log-view.js +75 -0
  77. package/dist/tui/components/execution/expanded-message-view.d.ts +24 -0
  78. package/dist/tui/components/execution/expanded-message-view.js +68 -0
  79. package/dist/tui/components/execution/index.d.ts +2 -0
  80. package/dist/tui/components/execution/index.js +2 -0
  81. package/dist/tui/components/execution/log-item.d.ts +4 -0
  82. package/dist/tui/components/execution/log-item.js +2 -2
  83. package/dist/tui/components/footer.js +1 -1
  84. package/dist/tui/components/index.d.ts +2 -1
  85. package/dist/tui/components/index.js +2 -1
  86. package/dist/tui/components/init.js +2 -9
  87. package/dist/tui/components/logo.js +4 -3
  88. package/dist/tui/components/markdown.d.ts +13 -0
  89. package/dist/tui/components/markdown.js +88 -0
  90. package/dist/tui/components/message-item.js +1 -1
  91. package/dist/tui/components/onboarding/onboarding-flow.js +1 -1
  92. package/dist/tui/components/suggestions.js +3 -3
  93. package/dist/tui/contexts/mode-context.js +6 -2
  94. package/dist/tui/hooks/index.d.ts +1 -0
  95. package/dist/tui/hooks/index.js +1 -0
  96. package/dist/tui/hooks/use-is-latest-version.d.ts +6 -0
  97. package/dist/tui/hooks/use-is-latest-version.js +22 -0
  98. package/dist/tui/views/command-view.d.ts +1 -1
  99. package/dist/tui/views/command-view.js +83 -98
  100. package/dist/tui/views/logs-view.d.ts +8 -0
  101. package/dist/tui/views/logs-view.js +55 -27
  102. package/oclif.manifest.json +26 -1
  103. package/package.json +9 -1
@@ -1,3 +1,4 @@
1
+ import { readdir } from 'node:fs/promises';
1
2
  import { join } from 'node:path';
2
3
  import { z } from 'zod';
3
4
  import { ToolName } from '../../../../core/domain/cipher/tools/constants.js';
@@ -48,19 +49,70 @@ const ContentSchema = z.object({
48
49
  .describe('Related topics using domain/topic/title.md or domain/topic/subtopic/title.md notation'),
49
50
  snippets: z.array(z.string()).optional().describe('Code/text snippets'),
50
51
  });
52
+ /**
53
+ * Domain context schema for domain-level context.md files.
54
+ * Provides metadata about a domain's purpose, scope, ownership, and usage.
55
+ */
56
+ const DomainContextSchema = z.object({
57
+ ownership: z
58
+ .string()
59
+ .optional()
60
+ .describe('Which system, team, or layer owns this domain (e.g., "Platform Security Team")'),
61
+ purpose: z
62
+ .string()
63
+ .describe('Describe what this domain represents and why it exists (e.g., "Contains all knowledge related to user and service authentication mechanisms")'),
64
+ scope: z.object({
65
+ excluded: z
66
+ .array(z.string())
67
+ .optional()
68
+ .describe('What does NOT belong in this domain (e.g., ["Authorization and permission models", "User profile management"])'),
69
+ included: z
70
+ .array(z.string())
71
+ .describe('What belongs in this domain (e.g., ["Login and signup flows", "Token-based authentication", "OAuth integrations"])'),
72
+ }).describe('Define what belongs and does not belong in this domain'),
73
+ usage: z
74
+ .string()
75
+ .optional()
76
+ .describe('How this domain should be used by agents and contributors'),
77
+ });
78
+ const TopicContextSchema = z.object({
79
+ keyConcepts: z
80
+ .array(z.string())
81
+ .optional()
82
+ .describe('Key concepts covered in this topic (e.g., ["JWT tokens", "Refresh token rotation", "Token blacklisting"])'),
83
+ overview: z
84
+ .string()
85
+ .describe('Describe what this topic covers and its main focus (e.g., "Covers all aspects of JWT-based authentication including token generation, validation, and refresh mechanisms")'),
86
+ relatedTopics: z
87
+ .array(z.string())
88
+ .optional()
89
+ .describe('Related topics and how they connect (e.g., ["authentication/session - for session-based alternatives", "security/encryption - for token signing"])'),
90
+ });
91
+ const SubtopicContextSchema = z.object({
92
+ focus: z
93
+ .string()
94
+ .describe('Describe the specific focus of this subtopic (e.g., "Focuses on refresh token rotation strategy and invalidation mechanisms")'),
95
+ parentRelation: z
96
+ .string()
97
+ .optional()
98
+ .describe('How this subtopic relates to its parent topic (e.g., "Handles the token refresh aspect of JWT authentication")'),
99
+ });
51
100
  /**
52
101
  * Single operation schema for curating knowledge.
53
102
  */
54
103
  const OperationSchema = z.object({
55
104
  content: ContentSchema.optional().describe('Content for ADD/UPDATE operations'),
105
+ domainContext: DomainContextSchema.optional().describe('Domain-level context for new domains. When creating content in a NEW domain, provide this to auto-generate domain/context.md with purpose, scope, ownership, and usage. Only needed when the domain does not exist yet.'),
56
106
  mergeTarget: z.string().optional().describe('Target path for MERGE operation'),
57
107
  mergeTargetTitle: z.string().optional().describe('Title of the target file for MERGE operation'),
58
108
  path: z.string().describe('Path: domain/topic/title.md or domain/topic/subtopic/title.md'),
59
109
  reason: z.string().describe('Reasoning for this operation'),
110
+ subtopicContext: SubtopicContextSchema.optional().describe('Subtopic-level context for new subtopics. When creating content in a NEW subtopic, provide this to auto-generate subtopic/context.md with focus and parent relation. Only needed when the subtopic does not exist yet.'),
60
111
  title: z
61
112
  .string()
62
113
  .optional()
63
114
  .describe('Title for the context file (saved as {title}.md in snake_case). Required for ADD/UPDATE/MERGE, optional for DELETE'),
115
+ topicContext: TopicContextSchema.optional().describe('Topic-level context for new topics. When creating content in a NEW topic, provide this to auto-generate topic/context.md with overview, key concepts, and related topics. Only needed when the topic does not exist yet.'),
64
116
  type: OperationType.describe('Operation type: ADD, UPDATE, MERGE, or DELETE'),
65
117
  });
66
118
  /**
@@ -70,6 +122,170 @@ const CurateInputSchema = z.object({
70
122
  basePath: z.string().default('.brv/context-tree').describe('Base path for knowledge storage'),
71
123
  operations: z.array(OperationSchema).describe('Array of curate operations to apply'),
72
124
  });
125
+ function generateDomainContextMarkdown(domainName, context) {
126
+ const sections = [
127
+ `# Domain: ${domainName}`,
128
+ '',
129
+ '## Purpose',
130
+ context.purpose,
131
+ '',
132
+ '## Scope',
133
+ ];
134
+ if (context.scope.included.length > 0) {
135
+ sections.push('Included in this domain:', ...context.scope.included.map((item) => `- ${item}`), '');
136
+ }
137
+ if (context.scope.excluded && context.scope.excluded.length > 0) {
138
+ sections.push('Excluded from this domain:', ...context.scope.excluded.map((item) => `- ${item}`), '');
139
+ }
140
+ if (context.ownership) {
141
+ sections.push('## Ownership', context.ownership, '');
142
+ }
143
+ if (context.usage) {
144
+ sections.push('## Usage', context.usage, '');
145
+ }
146
+ return sections.join('\n');
147
+ }
148
+ function generateMinimalDomainContextMarkdown(domainName) {
149
+ return `# Domain: ${domainName}
150
+
151
+ ## Purpose
152
+ Describe what this domain represents and why it exists.
153
+
154
+ ## Scope
155
+ Define what belongs in this domain and what does not.
156
+
157
+ ## Ownership
158
+ Which system, team, or layer owns this domain.
159
+
160
+ ## Usage
161
+ How this domain should be used by agents and contributors.
162
+ `;
163
+ }
164
+ function generateMinimalTopicContextMarkdown(topicName) {
165
+ return `# Topic: ${topicName}
166
+
167
+ ## Overview
168
+ Describe what this topic covers and its key concepts.
169
+
170
+ ## Related Topics
171
+ List related topics and how they connect to this one.
172
+ `;
173
+ }
174
+ function generateTopicContextMarkdown(topicName, context) {
175
+ const sections = [
176
+ `# Topic: ${topicName}`,
177
+ '',
178
+ '## Overview',
179
+ context.overview,
180
+ '',
181
+ ];
182
+ if (context.keyConcepts && context.keyConcepts.length > 0) {
183
+ sections.push('## Key Concepts', ...context.keyConcepts.map((concept) => `- ${concept}`), '');
184
+ }
185
+ if (context.relatedTopics && context.relatedTopics.length > 0) {
186
+ sections.push('## Related Topics', ...context.relatedTopics.map((topic) => `- ${topic}`), '');
187
+ }
188
+ return sections.join('\n');
189
+ }
190
+ function generateMinimalSubtopicContextMarkdown(subtopicName) {
191
+ return `# Subtopic: ${subtopicName}
192
+
193
+ ## Overview
194
+ Describe what this subtopic covers and its specific focus.
195
+ `;
196
+ }
197
+ function generateSubtopicContextMarkdown(subtopicName, context) {
198
+ const sections = [
199
+ `# Subtopic: ${subtopicName}`,
200
+ '',
201
+ '## Focus',
202
+ context.focus,
203
+ '',
204
+ ];
205
+ if (context.parentRelation) {
206
+ sections.push('## Parent Relation', context.parentRelation, '');
207
+ }
208
+ return sections.join('\n');
209
+ }
210
+ async function createDomainContextIfMissing(basePath, domain, domainContext) {
211
+ const normalizedDomain = toSnakeCase(domain);
212
+ const contextPath = join(basePath, normalizedDomain, 'context.md');
213
+ const exists = await DirectoryManager.fileExists(contextPath);
214
+ if (exists) {
215
+ return { created: false };
216
+ }
217
+ const content = domainContext
218
+ ? generateDomainContextMarkdown(normalizedDomain, domainContext)
219
+ : generateMinimalDomainContextMarkdown(normalizedDomain);
220
+ await DirectoryManager.writeFileAtomic(contextPath, content);
221
+ return { created: true, path: contextPath };
222
+ }
223
+ async function ensureTopicContextMd(basePath, domain, topic, topicContext) {
224
+ const normalizedDomain = toSnakeCase(domain);
225
+ const normalizedTopic = toSnakeCase(topic);
226
+ const topicPath = join(basePath, normalizedDomain, normalizedTopic);
227
+ const contextPath = join(topicPath, 'context.md');
228
+ // Check if topic folder exists first
229
+ const folderExists = await DirectoryManager.folderExists(topicPath);
230
+ if (!folderExists) {
231
+ return { created: false };
232
+ }
233
+ // Check if context.md already exists
234
+ const exists = await DirectoryManager.fileExists(contextPath);
235
+ if (exists) {
236
+ return { created: false };
237
+ }
238
+ const content = topicContext
239
+ ? generateTopicContextMarkdown(normalizedTopic, topicContext)
240
+ : generateMinimalTopicContextMarkdown(normalizedTopic);
241
+ await DirectoryManager.writeFileAtomic(contextPath, content);
242
+ return { created: true, path: contextPath };
243
+ }
244
+ /**
245
+ * Ensure context.md exists at subtopic level.
246
+ * Creates a context.md with LLM-provided content if available, otherwise creates a minimal template.
247
+ */
248
+ async function ensureSubtopicContextMd(options) {
249
+ const { basePath, domain, subtopic, subtopicContext, topic } = options;
250
+ const normalizedDomain = toSnakeCase(domain);
251
+ const normalizedTopic = toSnakeCase(topic);
252
+ const normalizedSubtopic = toSnakeCase(subtopic);
253
+ const subtopicPath = join(basePath, normalizedDomain, normalizedTopic, normalizedSubtopic);
254
+ const contextPath = join(subtopicPath, 'context.md');
255
+ // Check if subtopic folder exists first
256
+ const folderExists = await DirectoryManager.folderExists(subtopicPath);
257
+ if (!folderExists) {
258
+ return { created: false };
259
+ }
260
+ // Check if context.md already exists
261
+ const exists = await DirectoryManager.fileExists(contextPath);
262
+ if (exists) {
263
+ return { created: false };
264
+ }
265
+ const content = subtopicContext
266
+ ? generateSubtopicContextMarkdown(normalizedSubtopic, subtopicContext)
267
+ : generateMinimalSubtopicContextMarkdown(normalizedSubtopic);
268
+ await DirectoryManager.writeFileAtomic(contextPath, content);
269
+ return { created: true, path: contextPath };
270
+ }
271
+ /**
272
+ * Ensure context.md exists at all levels for a given path (topic and subtopic).
273
+ * This is called during ADD operations to create context.md files with LLM-provided content.
274
+ */
275
+ async function ensureContextMd(basePath, parsed, topicContext, subtopicContext) {
276
+ // Ensure topic-level context.md exists
277
+ await ensureTopicContextMd(basePath, parsed.domain, parsed.topic, topicContext);
278
+ // If subtopic exists, ensure subtopic-level context.md exists
279
+ if (parsed.subtopic) {
280
+ await ensureSubtopicContextMd({
281
+ basePath,
282
+ domain: parsed.domain,
283
+ subtopic: parsed.subtopic,
284
+ subtopicContext,
285
+ topic: parsed.topic,
286
+ });
287
+ }
288
+ }
73
289
  /**
74
290
  * Parse a path into domain, topic, and optional subtopic.
75
291
  */
@@ -128,7 +344,7 @@ function buildFullPath(basePath, knowledgePath) {
128
344
  * Execute ADD operation - create new domain/topic/subtopic with {title}.md
129
345
  */
130
346
  async function executeAdd(basePath, operation) {
131
- const { content, path, reason, title } = operation;
347
+ const { content, domainContext, path, reason, subtopicContext, title, topicContext } = operation;
132
348
  if (!title) {
133
349
  return {
134
350
  message: 'ADD operation requires a title',
@@ -155,7 +371,6 @@ async function executeAdd(basePath, operation) {
155
371
  type: 'ADD',
156
372
  };
157
373
  }
158
- // Validate domain before creating
159
374
  const domainValidation = validateDomain(parsed.domain);
160
375
  if (!domainValidation.allowed) {
161
376
  return {
@@ -165,12 +380,10 @@ async function executeAdd(basePath, operation) {
165
380
  type: 'ADD',
166
381
  };
167
382
  }
168
- // Build the final folder path (topic or subtopic)
383
+ await createDomainContextIfMissing(basePath, parsed.domain, domainContext);
169
384
  const domainPath = join(basePath, toSnakeCase(parsed.domain));
170
385
  const topicPath = join(domainPath, toSnakeCase(parsed.topic));
171
386
  const finalPath = parsed.subtopic ? join(topicPath, toSnakeCase(parsed.subtopic)) : topicPath;
172
- // Generate and write {title}.md (snake_case filename)
173
- // Note: writeFileAtomic creates parent directories as needed, avoiding empty folder creation
174
387
  const contextContent = MarkdownWriter.generateContext({
175
388
  name: title,
176
389
  narrative: content.narrative,
@@ -181,6 +394,7 @@ async function executeAdd(basePath, operation) {
181
394
  const filename = `${toSnakeCase(title)}.md`;
182
395
  const contextPath = join(finalPath, filename);
183
396
  await DirectoryManager.writeFileAtomic(contextPath, contextContent);
397
+ await ensureContextMd(basePath, parsed, topicContext, subtopicContext);
184
398
  return {
185
399
  filePath: contextPath,
186
400
  message: `Created ${path}/${filename} with ${content.snippets?.length || 0} snippets. Reason: ${reason}`,
@@ -202,7 +416,7 @@ async function executeAdd(basePath, operation) {
202
416
  * Execute UPDATE operation - modify existing {title}.md
203
417
  */
204
418
  async function executeUpdate(basePath, operation) {
205
- const { content, path, reason, title } = operation;
419
+ const { content, domainContext, path, reason, subtopicContext, title, topicContext } = operation;
206
420
  if (!title) {
207
421
  return {
208
422
  message: 'UPDATE operation requires a title',
@@ -220,10 +434,18 @@ async function executeUpdate(basePath, operation) {
220
434
  };
221
435
  }
222
436
  try {
437
+ const parsed = parsePath(path);
438
+ if (!parsed) {
439
+ return {
440
+ message: `Invalid path format: ${path}. Expected domain/topic or domain/topic/subtopic`,
441
+ path,
442
+ status: 'failed',
443
+ type: 'UPDATE',
444
+ };
445
+ }
223
446
  const fullPath = buildFullPath(basePath, path);
224
447
  const filename = `${toSnakeCase(title)}.md`;
225
448
  const contextPath = join(fullPath, filename);
226
- // Check if the specific titled file exists
227
449
  const exists = await DirectoryManager.fileExists(contextPath);
228
450
  if (!exists) {
229
451
  return {
@@ -233,7 +455,7 @@ async function executeUpdate(basePath, operation) {
233
455
  type: 'UPDATE',
234
456
  };
235
457
  }
236
- // Generate and write updated content (full replacement)
458
+ await createDomainContextIfMissing(basePath, parsed.domain, domainContext);
237
459
  const contextContent = MarkdownWriter.generateContext({
238
460
  name: title,
239
461
  narrative: content.narrative,
@@ -242,6 +464,7 @@ async function executeUpdate(basePath, operation) {
242
464
  snippets: content.snippets ?? [],
243
465
  });
244
466
  await DirectoryManager.writeFileAtomic(contextPath, contextContent);
467
+ await ensureContextMd(basePath, parsed, topicContext, subtopicContext);
245
468
  return {
246
469
  filePath: contextPath,
247
470
  message: `Updated ${path}/${filename}. Reason: ${reason}`,
@@ -263,7 +486,7 @@ async function executeUpdate(basePath, operation) {
263
486
  * Execute MERGE operation - combine source file into target file, delete source file
264
487
  */
265
488
  async function executeMerge(basePath, operation) {
266
- const { mergeTarget, mergeTargetTitle, path, reason, title } = operation;
489
+ const { domainContext, mergeTarget, mergeTargetTitle, path, reason, subtopicContext, title, topicContext } = operation;
267
490
  if (!title) {
268
491
  return {
269
492
  message: 'MERGE operation requires a title (source file)',
@@ -289,13 +512,22 @@ async function executeMerge(basePath, operation) {
289
512
  };
290
513
  }
291
514
  try {
515
+ const sourceParsed = parsePath(path);
516
+ const targetParsed = parsePath(mergeTarget);
517
+ if (!sourceParsed || !targetParsed) {
518
+ return {
519
+ message: `Invalid path format. Expected domain/topic or domain/topic/subtopic`,
520
+ path,
521
+ status: 'failed',
522
+ type: 'MERGE',
523
+ };
524
+ }
292
525
  const sourceFolderPath = buildFullPath(basePath, path);
293
526
  const targetFolderPath = buildFullPath(basePath, mergeTarget);
294
527
  const sourceFilename = `${toSnakeCase(title)}.md`;
295
528
  const targetFilename = `${toSnakeCase(mergeTargetTitle)}.md`;
296
529
  const sourceContextPath = join(sourceFolderPath, sourceFilename);
297
530
  const targetContextPath = join(targetFolderPath, targetFilename);
298
- // Check if both files exist
299
531
  const sourceExists = await DirectoryManager.fileExists(sourceContextPath);
300
532
  const targetExists = await DirectoryManager.fileExists(targetContextPath);
301
533
  if (!sourceExists) {
@@ -314,14 +546,15 @@ async function executeMerge(basePath, operation) {
314
546
  type: 'MERGE',
315
547
  };
316
548
  }
317
- // Read both files
549
+ await createDomainContextIfMissing(basePath, sourceParsed.domain, domainContext);
550
+ await createDomainContextIfMissing(basePath, targetParsed.domain, domainContext);
318
551
  const sourceContent = await DirectoryManager.readFile(sourceContextPath);
319
552
  const targetContent = await DirectoryManager.readFile(targetContextPath);
320
- // Merge the contents using MarkdownWriter
321
553
  const mergedContent = MarkdownWriter.mergeContexts(sourceContent, targetContent);
322
554
  await DirectoryManager.writeFileAtomic(targetContextPath, mergedContent);
323
- // Delete source file (not the entire folder, just the file)
324
555
  await DirectoryManager.deleteFile(sourceContextPath);
556
+ await ensureContextMd(basePath, sourceParsed, topicContext, subtopicContext);
557
+ await ensureContextMd(basePath, targetParsed, topicContext, subtopicContext);
325
558
  return {
326
559
  filePath: targetContextPath,
327
560
  message: `Merged ${path}/${sourceFilename} into ${mergeTarget}/${targetFilename}. Reason: ${reason}`,
@@ -420,6 +653,20 @@ async function executeCurate(input, _context) {
420
653
  };
421
654
  }
422
655
  const { basePath, operations } = parseResult.data;
656
+ const touchedDomains = new Set();
657
+ for (const op of operations) {
658
+ const parsed = parsePath(op.path);
659
+ if (parsed) {
660
+ touchedDomains.add(toSnakeCase(parsed.domain));
661
+ }
662
+ if (op.type === 'MERGE' && op.mergeTarget) {
663
+ const targetParsed = parsePath(op.mergeTarget);
664
+ if (targetParsed) {
665
+ touchedDomains.add(toSnakeCase(targetParsed.domain));
666
+ }
667
+ }
668
+ }
669
+ await backfillDomainContextFiles(basePath, touchedDomains);
423
670
  const applied = [];
424
671
  const summary = {
425
672
  added: 0,
@@ -428,7 +675,6 @@ async function executeCurate(input, _context) {
428
675
  merged: 0,
429
676
  updated: 0,
430
677
  };
431
- // Process operations sequentially to maintain consistency
432
678
  /* eslint-disable no-await-in-loop -- Sequential processing required for dependent operations */
433
679
  for (const operation of operations) {
434
680
  let result;
@@ -476,14 +722,38 @@ async function executeCurate(input, _context) {
476
722
  /* eslint-enable no-await-in-loop */
477
723
  return { applied, summary };
478
724
  }
479
- /**
480
- * Creates the curate tool.
481
- *
482
- * This tool manages knowledge topics with atomic operations (ADD, UPDATE, MERGE, DELETE).
483
- * It applies patterns from the ACE Curator for intelligent knowledge curation.
484
- *
485
- * @returns Configured curate tool
486
- */
725
+ export async function backfillDomainContextFiles(basePath, excludeDomains = new Set()) {
726
+ const createdPaths = [];
727
+ const baseExists = await DirectoryManager.folderExists(basePath);
728
+ if (!baseExists) {
729
+ return createdPaths;
730
+ }
731
+ let entries;
732
+ try {
733
+ entries = await readdir(basePath, { withFileTypes: true });
734
+ }
735
+ catch {
736
+ return createdPaths;
737
+ }
738
+ const domains = entries.filter((entry) => entry.isDirectory());
739
+ /* eslint-disable no-await-in-loop */
740
+ for (const domain of domains) {
741
+ if (excludeDomains.has(domain.name)) {
742
+ continue;
743
+ }
744
+ const contextPath = join(basePath, domain.name, 'context.md');
745
+ const exists = await DirectoryManager.fileExists(contextPath);
746
+ if (!exists) {
747
+ const mdFiles = await DirectoryManager.listMarkdownFiles(join(basePath, domain.name));
748
+ if (mdFiles.length > 0) {
749
+ const content = generateMinimalDomainContextMarkdown(domain.name);
750
+ await DirectoryManager.writeFileAtomic(contextPath, content);
751
+ createdPaths.push(contextPath);
752
+ }
753
+ }
754
+ }
755
+ return createdPaths;
756
+ }
487
757
  export function createCurateTool() {
488
758
  return {
489
759
  description: `Curate knowledge topics with atomic operations. This tool manages the knowledge structure using four operation types and supports a two-part context model: Raw Concept + Narrative.
@@ -579,6 +849,74 @@ export function createCurateTool() {
579
849
  - Avoid overly specific names that only fit one topic
580
850
  - Keep domain count reasonable by consolidating related concepts
581
851
 
852
+ **Automatic Domain Context (context.md):**
853
+ - When any operation (ADD/UPDATE/MERGE) touches a domain for the first time, a context.md file is automatically created at the domain root
854
+ - This context.md describes the domain's purpose, scope, ownership, and usage guidelines
855
+ - **IMPORTANT**: When creating content in a NEW domain, provide the \`domainContext\` field with:
856
+ - \`purpose\` (required): What this domain represents and why it exists
857
+ - \`scope.included\` (required): Array of what belongs in this domain
858
+ - \`scope.excluded\` (optional): Array of what does NOT belong in this domain
859
+ - \`ownership\` (optional): Which team/system owns this domain
860
+ - \`usage\` (optional): How this domain should be used
861
+ - Example with domainContext:
862
+ {
863
+ type: "ADD",
864
+ path: "authentication/jwt",
865
+ title: "Token Handling",
866
+ content: { ... },
867
+ domainContext: {
868
+ purpose: "Contains all knowledge related to user and service authentication mechanisms used across the platform.",
869
+ scope: {
870
+ included: ["Login and signup flows", "Token-based authentication (JWT, refresh tokens)", "OAuth integrations", "Session handling"],
871
+ excluded: ["Authorization and permission models", "User profile management"]
872
+ },
873
+ ownership: "Platform Security Team",
874
+ usage: "Use this domain for documenting authentication flows, token handling, and identity verification patterns."
875
+ },
876
+ reason: "Documenting JWT token handling"
877
+ }
878
+ - If domainContext is not provided for a new domain, a minimal template is created that can be updated later
879
+
880
+ **Topic Context (context.md at topic level):**
881
+ - When creating content in a NEW topic, provide the \`topicContext\` field to auto-generate topic/context.md
882
+ - **IMPORTANT**: When creating content in a NEW topic, provide the \`topicContext\` field with:
883
+ - \`overview\` (required): What this topic covers and its main focus
884
+ - \`keyConcepts\` (optional): Array of key concepts covered in this topic
885
+ - \`relatedTopics\` (optional): Array of related topics and how they connect
886
+ - Example with topicContext:
887
+ {
888
+ type: "ADD",
889
+ path: "authentication/jwt",
890
+ title: "Token Handling",
891
+ content: { ... },
892
+ topicContext: {
893
+ overview: "Covers all aspects of JWT-based authentication including token generation, validation, and refresh mechanisms.",
894
+ keyConcepts: ["JWT tokens", "Refresh token rotation", "Token blacklisting", "Token validation middleware"],
895
+ relatedTopics: ["authentication/session - for session-based alternatives", "security/encryption - for token signing"]
896
+ },
897
+ reason: "Documenting JWT token handling"
898
+ }
899
+ - If topicContext is not provided for a new topic, a minimal template is created that can be updated later
900
+
901
+ **Subtopic Context (context.md at subtopic level):**
902
+ - When creating content in a NEW subtopic, provide the \`subtopicContext\` field to auto-generate subtopic/context.md
903
+ - **IMPORTANT**: When creating content in a NEW subtopic, provide the \`subtopicContext\` field with:
904
+ - \`focus\` (required): The specific focus of this subtopic
905
+ - \`parentRelation\` (optional): How this subtopic relates to its parent topic
906
+ - Example with subtopicContext:
907
+ {
908
+ type: "ADD",
909
+ path: "authentication/jwt/refresh_tokens",
910
+ title: "Rotation Strategy",
911
+ content: { ... },
912
+ subtopicContext: {
913
+ focus: "Focuses on refresh token rotation strategy and invalidation mechanisms to prevent token reuse attacks.",
914
+ parentRelation: "Handles the token refresh aspect of JWT authentication, specifically how old tokens are invalidated when new ones are issued."
915
+ },
916
+ reason: "Documenting refresh token rotation"
917
+ }
918
+ - If subtopicContext is not provided for a new subtopic, a minimal template is created that can be updated later
919
+
582
920
  **Backward Compatibility:** Existing context entries using only snippets and relations continue to work.
583
921
 
584
922
  **Output:** Returns applied operations with status (success/failed), filePath (for created/modified files), and a summary of counts.`,
@@ -1,6 +1,7 @@
1
1
  import { AGENT_CONNECTOR_CONFIG, AGENT_VALUES } from '../../core/domain/entities/agent.js';
2
2
  import { CONNECTOR_TYPES } from '../../core/domain/entities/connector-type.js';
3
3
  import { HookConnector } from './hook/hook-connector.js';
4
+ import { McpConnector } from './mcp/mcp-connector.js';
4
5
  import { RulesConnector } from './rules/rules-connector.js';
5
6
  /**
6
7
  * Factory and orchestration layer for connectors.
@@ -13,6 +14,7 @@ export class ConnectorManager {
13
14
  // Create connector instances
14
15
  this.connectors = new Map([
15
16
  ['hook', new HookConnector({ fileService, projectRoot })],
17
+ ['mcp', new McpConnector({ fileService, projectRoot, templateService })],
16
18
  ['rules', new RulesConnector({ fileService, projectRoot, templateService })],
17
19
  ]);
18
20
  }
@@ -0,0 +1,4 @@
1
+ export * from './json-mcp-config-writer.js';
2
+ export * from './mcp-connector-config.js';
3
+ export * from './mcp-connector.js';
4
+ export * from './toml-mcp-config-writer.js';
@@ -0,0 +1,4 @@
1
+ export * from './json-mcp-config-writer.js';
2
+ export * from './mcp-connector-config.js';
3
+ export * from './mcp-connector.js';
4
+ export * from './toml-mcp-config-writer.js';
@@ -0,0 +1,26 @@
1
+ import type { IFileService } from '../../../core/interfaces/i-file-service.js';
2
+ import type { IMcpConfigWriter, McpConfigExistsResult } from '../../../core/interfaces/i-mcp-config-writer.js';
3
+ import type { McpServerConfig } from './mcp-connector-config.js';
4
+ /**
5
+ * Options for constructing JsonMcpConfigWriter.
6
+ */
7
+ export type JsonMcpConfigWriterOptions = {
8
+ fileService: IFileService;
9
+ /**
10
+ * JSON key path to the MCP server entry, including server name.
11
+ * e.g., ['mcpServers', 'brv'] navigates to { mcpServers: { brv: ... } }
12
+ */
13
+ serverKeyPath: readonly string[];
14
+ };
15
+ /**
16
+ * MCP config writer for JSON format files.
17
+ * Handles nested key path navigation for reading/writing MCP server config.
18
+ */
19
+ export declare class JsonMcpConfigWriter implements IMcpConfigWriter {
20
+ private readonly fileService;
21
+ private readonly serverKeyPath;
22
+ constructor(options: JsonMcpConfigWriterOptions);
23
+ exists(filePath: string): Promise<McpConfigExistsResult>;
24
+ remove(filePath: string): Promise<boolean>;
25
+ write(filePath: string, serverConfig: McpServerConfig): Promise<void>;
26
+ }
@@ -0,0 +1,71 @@
1
+ import { has, set, unset } from 'lodash-es';
2
+ import { isRecord } from '../../../utils/type-guards.js';
3
+ /**
4
+ * Parse JSON and validate it's a Record object.
5
+ * @throws Error if JSON is invalid or not an object
6
+ */
7
+ function parseJsonAsRecord(content) {
8
+ const parsed = JSON.parse(content);
9
+ if (!isRecord(parsed)) {
10
+ throw new Error('Expected JSON object');
11
+ }
12
+ return parsed;
13
+ }
14
+ /**
15
+ * MCP config writer for JSON format files.
16
+ * Handles nested key path navigation for reading/writing MCP server config.
17
+ */
18
+ export class JsonMcpConfigWriter {
19
+ fileService;
20
+ serverKeyPath;
21
+ constructor(options) {
22
+ this.fileService = options.fileService;
23
+ this.serverKeyPath = options.serverKeyPath;
24
+ }
25
+ async exists(filePath) {
26
+ const fileExists = await this.fileService.exists(filePath);
27
+ if (!fileExists) {
28
+ return { fileExists: false, serverExists: false };
29
+ }
30
+ try {
31
+ const content = await this.fileService.read(filePath);
32
+ const json = parseJsonAsRecord(content);
33
+ const serverExists = has(json, this.serverKeyPath);
34
+ return {
35
+ fileExists: true,
36
+ serverExists,
37
+ };
38
+ }
39
+ catch {
40
+ return { fileExists: true, serverExists: false };
41
+ }
42
+ }
43
+ async remove(filePath) {
44
+ const fileExists = await this.fileService.exists(filePath);
45
+ if (!fileExists) {
46
+ return false;
47
+ }
48
+ const content = await this.fileService.read(filePath);
49
+ const json = parseJsonAsRecord(content);
50
+ // Check if property exists before attempting to unset
51
+ if (!has(json, this.serverKeyPath)) {
52
+ return false;
53
+ }
54
+ unset(json, this.serverKeyPath);
55
+ await this.fileService.write(JSON.stringify(json, null, 2), filePath, 'overwrite');
56
+ return true;
57
+ }
58
+ async write(filePath, serverConfig) {
59
+ const fileExists = await this.fileService.exists(filePath);
60
+ let json;
61
+ if (fileExists) {
62
+ const content = await this.fileService.read(filePath);
63
+ json = parseJsonAsRecord(content);
64
+ }
65
+ else {
66
+ json = {};
67
+ }
68
+ set(json, this.serverKeyPath, { ...serverConfig });
69
+ await this.fileService.write(JSON.stringify(json, null, 2), filePath, 'overwrite');
70
+ }
71
+ }