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.
- package/README.md +8 -4
- package/dist/commands/mcp.d.ts +13 -0
- package/dist/commands/mcp.js +61 -0
- package/dist/core/domain/cipher/agent-events/types.d.ts +44 -1
- package/dist/core/domain/entities/agent.js +72 -18
- package/dist/core/domain/entities/connector-type.d.ts +2 -1
- package/dist/core/domain/entities/connector-type.js +2 -1
- package/dist/core/interfaces/connectors/connector-types.d.ts +13 -0
- package/dist/core/interfaces/i-mcp-config-writer.d.ts +40 -0
- package/dist/core/interfaces/i-mcp-config-writer.js +1 -0
- package/dist/core/interfaces/i-rule-template-service.d.ts +4 -2
- package/dist/core/interfaces/transport/i-transport-client.d.ts +7 -0
- package/dist/infra/cipher/agent/cipher-agent.d.ts +8 -0
- package/dist/infra/cipher/agent/cipher-agent.js +16 -0
- package/dist/infra/cipher/llm/context/context-manager.d.ts +8 -0
- package/dist/infra/cipher/llm/context/context-manager.js +16 -0
- package/dist/infra/cipher/llm/internal-llm-service.d.ts +4 -0
- package/dist/infra/cipher/llm/internal-llm-service.js +38 -10
- package/dist/infra/cipher/session/chat-session.d.ts +3 -0
- package/dist/infra/cipher/session/chat-session.js +7 -1
- package/dist/infra/cipher/tools/implementations/curate-tool.d.ts +1 -8
- package/dist/infra/cipher/tools/implementations/curate-tool.js +360 -22
- package/dist/infra/connectors/connector-manager.js +2 -0
- package/dist/infra/connectors/mcp/index.d.ts +4 -0
- package/dist/infra/connectors/mcp/index.js +4 -0
- package/dist/infra/connectors/mcp/json-mcp-config-writer.d.ts +26 -0
- package/dist/infra/connectors/mcp/json-mcp-config-writer.js +71 -0
- package/dist/infra/connectors/mcp/mcp-connector-config.d.ts +229 -0
- package/dist/infra/connectors/mcp/mcp-connector-config.js +173 -0
- package/dist/infra/connectors/mcp/mcp-connector.d.ts +80 -0
- package/dist/infra/connectors/mcp/mcp-connector.js +324 -0
- package/dist/infra/connectors/mcp/toml-mcp-config-writer.d.ts +45 -0
- package/dist/infra/connectors/mcp/toml-mcp-config-writer.js +134 -0
- package/dist/infra/connectors/rules/rules-connector.d.ts +1 -8
- package/dist/infra/connectors/rules/rules-connector.js +20 -85
- package/dist/infra/connectors/shared/rule-file-manager.d.ts +72 -0
- package/dist/infra/connectors/shared/rule-file-manager.js +119 -0
- package/dist/infra/connectors/shared/template-service.d.ts +10 -1
- package/dist/infra/connectors/shared/template-service.js +53 -16
- package/dist/infra/mcp/index.d.ts +2 -0
- package/dist/infra/mcp/index.js +2 -0
- package/dist/infra/mcp/mcp-server.d.ts +58 -0
- package/dist/infra/mcp/mcp-server.js +178 -0
- package/dist/infra/mcp/tools/brv-curate-tool.d.ts +23 -0
- package/dist/infra/mcp/tools/brv-curate-tool.js +68 -0
- package/dist/infra/mcp/tools/brv-query-tool.d.ts +17 -0
- package/dist/infra/mcp/tools/brv-query-tool.js +68 -0
- package/dist/infra/mcp/tools/index.d.ts +3 -0
- package/dist/infra/mcp/tools/index.js +3 -0
- package/dist/infra/mcp/tools/task-result-waiter.d.ts +30 -0
- package/dist/infra/mcp/tools/task-result-waiter.js +56 -0
- package/dist/infra/process/agent-worker.js +37 -0
- package/dist/infra/repl/commands/curate-command.js +2 -2
- package/dist/infra/transport/socket-io-transport-client.d.ts +7 -0
- package/dist/infra/transport/socket-io-transport-client.js +25 -0
- package/dist/infra/transport/socket-io-transport-server.js +4 -0
- package/dist/infra/usecase/connectors-use-case.d.ts +4 -0
- package/dist/infra/usecase/connectors-use-case.js +29 -10
- package/dist/infra/usecase/init-use-case.js +2 -3
- package/dist/infra/usecase/status-use-case.d.ts +10 -0
- package/dist/infra/usecase/status-use-case.js +53 -0
- package/dist/resources/prompts/curate.yml +107 -4
- package/dist/templates/mcp-base.md +1 -0
- package/dist/templates/sections/mcp-workflow.md +13 -0
- package/dist/tui/app.js +4 -1
- package/dist/tui/components/command-details.js +1 -1
- package/dist/tui/components/execution/execution-changes.d.ts +2 -0
- package/dist/tui/components/execution/execution-changes.js +5 -1
- package/dist/tui/components/execution/execution-content.d.ts +2 -0
- package/dist/tui/components/execution/execution-content.js +8 -18
- package/dist/tui/components/execution/execution-input.d.ts +2 -0
- package/dist/tui/components/execution/execution-input.js +6 -4
- package/dist/tui/components/execution/execution-progress.d.ts +2 -0
- package/dist/tui/components/execution/execution-progress.js +6 -2
- package/dist/tui/components/execution/expanded-log-view.d.ts +20 -0
- package/dist/tui/components/execution/expanded-log-view.js +75 -0
- package/dist/tui/components/execution/expanded-message-view.d.ts +24 -0
- package/dist/tui/components/execution/expanded-message-view.js +68 -0
- package/dist/tui/components/execution/index.d.ts +2 -0
- package/dist/tui/components/execution/index.js +2 -0
- package/dist/tui/components/execution/log-item.d.ts +4 -0
- package/dist/tui/components/execution/log-item.js +2 -2
- package/dist/tui/components/footer.js +1 -1
- package/dist/tui/components/index.d.ts +2 -1
- package/dist/tui/components/index.js +2 -1
- package/dist/tui/components/init.js +2 -9
- package/dist/tui/components/logo.js +4 -3
- package/dist/tui/components/markdown.d.ts +13 -0
- package/dist/tui/components/markdown.js +88 -0
- package/dist/tui/components/message-item.js +1 -1
- package/dist/tui/components/onboarding/onboarding-flow.js +1 -1
- package/dist/tui/components/suggestions.js +3 -3
- package/dist/tui/contexts/mode-context.js +6 -2
- package/dist/tui/hooks/index.d.ts +1 -0
- package/dist/tui/hooks/index.js +1 -0
- package/dist/tui/hooks/use-is-latest-version.d.ts +6 -0
- package/dist/tui/hooks/use-is-latest-version.js +22 -0
- package/dist/tui/views/command-view.d.ts +1 -1
- package/dist/tui/views/command-view.js +83 -98
- package/dist/tui/views/logs-view.d.ts +8 -0
- package/dist/tui/views/logs-view.js +55 -27
- package/oclif.manifest.json +26 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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,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
|
+
}
|