byterover-cli 1.0.4 → 1.1.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 (172) hide show
  1. package/README.md +24 -11
  2. package/dist/commands/curate.js +1 -1
  3. package/dist/commands/hook-prompt-submit.d.ts +27 -0
  4. package/dist/commands/hook-prompt-submit.js +39 -0
  5. package/dist/commands/main.d.ts +13 -0
  6. package/dist/commands/main.js +53 -2
  7. package/dist/commands/query.js +1 -1
  8. package/dist/commands/status.js +8 -3
  9. package/dist/constants.d.ts +2 -2
  10. package/dist/constants.js +2 -2
  11. package/dist/core/domain/cipher/llm/registry.js +53 -2
  12. package/dist/core/domain/cipher/llm/types.d.ts +2 -0
  13. package/dist/core/domain/cipher/process/types.d.ts +7 -0
  14. package/dist/core/domain/cipher/session/session-metadata.d.ts +178 -0
  15. package/dist/core/domain/cipher/session/session-metadata.js +147 -0
  16. package/dist/core/domain/cipher/tools/constants.d.ts +1 -0
  17. package/dist/core/domain/cipher/tools/constants.js +1 -0
  18. package/dist/core/domain/entities/agent.d.ts +16 -0
  19. package/dist/core/domain/entities/agent.js +24 -0
  20. package/dist/core/domain/entities/connector-type.d.ts +9 -0
  21. package/dist/core/domain/entities/connector-type.js +8 -0
  22. package/dist/core/domain/entities/event.d.ts +1 -1
  23. package/dist/core/domain/entities/event.js +2 -0
  24. package/dist/core/domain/errors/task-error.d.ts +4 -0
  25. package/dist/core/domain/errors/task-error.js +7 -0
  26. package/dist/core/domain/knowledge/markdown-writer.d.ts +15 -18
  27. package/dist/core/domain/knowledge/markdown-writer.js +232 -34
  28. package/dist/core/domain/knowledge/relation-parser.d.ts +25 -39
  29. package/dist/core/domain/knowledge/relation-parser.js +39 -61
  30. package/dist/core/domain/transport/schemas.d.ts +77 -2
  31. package/dist/core/domain/transport/schemas.js +51 -2
  32. package/dist/core/interfaces/cipher/i-session-persistence.d.ts +133 -0
  33. package/dist/core/interfaces/cipher/i-session-persistence.js +7 -0
  34. package/dist/core/interfaces/cipher/message-types.d.ts +6 -0
  35. package/dist/core/interfaces/connectors/connector-types.d.ts +57 -0
  36. package/dist/core/interfaces/connectors/i-connector-manager.d.ts +72 -0
  37. package/dist/core/interfaces/connectors/i-connector.d.ts +54 -0
  38. package/dist/core/interfaces/connectors/i-connector.js +1 -0
  39. package/dist/core/interfaces/executor/i-curate-executor.d.ts +2 -2
  40. package/dist/core/interfaces/i-context-file-reader.d.ts +3 -0
  41. package/dist/core/interfaces/i-file-service.d.ts +7 -0
  42. package/dist/core/interfaces/usecase/i-connectors-use-case.d.ts +3 -0
  43. package/dist/core/interfaces/usecase/i-connectors-use-case.js +1 -0
  44. package/dist/core/interfaces/usecase/{i-clear-use-case.d.ts → i-reset-use-case.d.ts} +1 -1
  45. package/dist/core/interfaces/usecase/i-reset-use-case.js +1 -0
  46. package/dist/hooks/init/update-notifier.d.ts +1 -0
  47. package/dist/hooks/init/update-notifier.js +10 -1
  48. package/dist/infra/cipher/agent/agent-schemas.d.ts +6 -6
  49. package/dist/infra/cipher/agent/service-initializer.js +4 -4
  50. package/dist/infra/cipher/file-system/binary-utils.d.ts +7 -12
  51. package/dist/infra/cipher/file-system/binary-utils.js +46 -31
  52. package/dist/infra/cipher/file-system/context-tree-file-system-factory.js +3 -2
  53. package/dist/infra/cipher/file-system/file-system-service.js +1 -0
  54. package/dist/infra/cipher/http/internal-llm-http-service.js +3 -5
  55. package/dist/infra/cipher/interactive-loop.js +3 -1
  56. package/dist/infra/cipher/llm/context/context-manager.d.ts +2 -2
  57. package/dist/infra/cipher/llm/context/context-manager.js +63 -18
  58. package/dist/infra/cipher/llm/formatters/gemini-formatter.d.ts +13 -0
  59. package/dist/infra/cipher/llm/formatters/gemini-formatter.js +146 -15
  60. package/dist/infra/cipher/llm/generators/byterover-content-generator.js +6 -2
  61. package/dist/infra/cipher/llm/internal-llm-service.js +2 -2
  62. package/dist/infra/cipher/llm/thought-parser.d.ts +21 -0
  63. package/dist/infra/cipher/llm/thought-parser.js +27 -0
  64. package/dist/infra/cipher/llm/tool-output-processor.d.ts +10 -0
  65. package/dist/infra/cipher/llm/tool-output-processor.js +80 -7
  66. package/dist/infra/cipher/process/process-service.js +11 -3
  67. package/dist/infra/cipher/session/chat-session.d.ts +7 -2
  68. package/dist/infra/cipher/session/chat-session.js +90 -52
  69. package/dist/infra/cipher/session/session-metadata-store.d.ts +52 -0
  70. package/dist/infra/cipher/session/session-metadata-store.js +406 -0
  71. package/dist/infra/cipher/system-prompt/contributors/context-tree-structure-contributor.d.ts +6 -7
  72. package/dist/infra/cipher/system-prompt/contributors/context-tree-structure-contributor.js +57 -18
  73. package/dist/infra/cipher/tools/implementations/curate-tool.js +132 -36
  74. package/dist/infra/cipher/tools/implementations/read-file-tool.js +38 -17
  75. package/dist/infra/cipher/tools/implementations/search-knowledge-tool.d.ts +7 -0
  76. package/dist/infra/cipher/tools/implementations/search-knowledge-tool.js +303 -0
  77. package/dist/infra/cipher/tools/implementations/task-tool.js +1 -0
  78. package/dist/infra/cipher/tools/index.d.ts +1 -0
  79. package/dist/infra/cipher/tools/index.js +1 -0
  80. package/dist/infra/cipher/tools/tool-manager.js +1 -0
  81. package/dist/infra/cipher/tools/tool-registry.js +7 -0
  82. package/dist/infra/connectors/connector-manager.d.ts +32 -0
  83. package/dist/infra/connectors/connector-manager.js +156 -0
  84. package/dist/infra/connectors/hook/hook-connector-config.d.ts +52 -0
  85. package/dist/infra/connectors/hook/hook-connector-config.js +41 -0
  86. package/dist/infra/connectors/hook/hook-connector.d.ts +46 -0
  87. package/dist/infra/connectors/hook/hook-connector.js +231 -0
  88. package/dist/infra/{rule → connectors/rules}/legacy-rule-detector.d.ts +2 -2
  89. package/dist/infra/{rule → connectors/rules}/legacy-rule-detector.js +1 -1
  90. package/dist/infra/connectors/rules/rules-connector-config.d.ts +95 -0
  91. package/dist/infra/{rule/agent-rule-config.js → connectors/rules/rules-connector-config.js} +10 -10
  92. package/dist/infra/connectors/rules/rules-connector.d.ts +41 -0
  93. package/dist/infra/connectors/rules/rules-connector.js +204 -0
  94. package/dist/infra/{rule/rule-template-service.d.ts → connectors/shared/template-service.d.ts} +3 -3
  95. package/dist/infra/{rule/rule-template-service.js → connectors/shared/template-service.js} +1 -1
  96. package/dist/infra/context-tree/file-context-file-reader.js +4 -0
  97. package/dist/infra/context-tree/file-context-tree-writer-service.d.ts +5 -2
  98. package/dist/infra/context-tree/file-context-tree-writer-service.js +20 -5
  99. package/dist/infra/core/executors/curate-executor.d.ts +2 -2
  100. package/dist/infra/core/executors/curate-executor.js +7 -7
  101. package/dist/infra/core/executors/query-executor.d.ts +12 -0
  102. package/dist/infra/core/executors/query-executor.js +62 -1
  103. package/dist/infra/core/task-processor.d.ts +2 -2
  104. package/dist/infra/file/fs-file-service.d.ts +7 -0
  105. package/dist/infra/file/fs-file-service.js +15 -1
  106. package/dist/infra/process/agent-worker.d.ts +2 -2
  107. package/dist/infra/process/agent-worker.js +626 -142
  108. package/dist/infra/process/constants.d.ts +1 -1
  109. package/dist/infra/process/constants.js +1 -1
  110. package/dist/infra/process/ipc-types.d.ts +17 -4
  111. package/dist/infra/process/ipc-types.js +3 -3
  112. package/dist/infra/process/parent-heartbeat.d.ts +47 -0
  113. package/dist/infra/process/parent-heartbeat.js +118 -0
  114. package/dist/infra/process/process-manager.d.ts +89 -1
  115. package/dist/infra/process/process-manager.js +293 -9
  116. package/dist/infra/process/task-queue-manager.d.ts +13 -0
  117. package/dist/infra/process/task-queue-manager.js +19 -0
  118. package/dist/infra/process/transport-handlers.d.ts +3 -0
  119. package/dist/infra/process/transport-handlers.js +82 -5
  120. package/dist/infra/process/transport-worker.js +9 -69
  121. package/dist/infra/repl/commands/connectors-command.d.ts +8 -0
  122. package/dist/infra/repl/commands/{gen-rules-command.js → connectors-command.js} +21 -10
  123. package/dist/infra/repl/commands/index.js +8 -4
  124. package/dist/infra/repl/commands/init-command.js +11 -7
  125. package/dist/infra/repl/commands/new-command.d.ts +14 -0
  126. package/dist/infra/repl/commands/new-command.js +61 -0
  127. package/dist/infra/repl/commands/query-command.js +22 -2
  128. package/dist/infra/repl/commands/{clear-command.d.ts → reset-command.d.ts} +2 -2
  129. package/dist/infra/repl/commands/{clear-command.js → reset-command.js} +11 -11
  130. package/dist/infra/transport/socket-io-transport-client.d.ts +68 -0
  131. package/dist/infra/transport/socket-io-transport-client.js +283 -7
  132. package/dist/infra/usecase/connectors-use-case.d.ts +59 -0
  133. package/dist/infra/usecase/connectors-use-case.js +203 -0
  134. package/dist/infra/usecase/init-use-case.d.ts +8 -43
  135. package/dist/infra/usecase/init-use-case.js +29 -253
  136. package/dist/infra/usecase/logout-use-case.js +2 -2
  137. package/dist/infra/usecase/pull-use-case.js +5 -5
  138. package/dist/infra/usecase/push-use-case.js +5 -5
  139. package/dist/infra/usecase/{clear-use-case.d.ts → reset-use-case.d.ts} +5 -5
  140. package/dist/infra/usecase/{clear-use-case.js → reset-use-case.js} +7 -8
  141. package/dist/infra/usecase/space-list-use-case.js +3 -3
  142. package/dist/infra/usecase/space-switch-use-case.js +3 -3
  143. package/dist/resources/prompts/curate.yml +75 -13
  144. package/dist/resources/prompts/explore.yml +34 -0
  145. package/dist/resources/prompts/query-orchestrator.yml +112 -0
  146. package/dist/resources/prompts/system-prompt.yml +12 -2
  147. package/dist/resources/tools/curate.txt +60 -15
  148. package/dist/resources/tools/search_knowledge.txt +32 -0
  149. package/dist/templates/sections/brv-instructions.md +98 -0
  150. package/dist/tui/components/inline-prompts/inline-confirm.js +2 -2
  151. package/dist/tui/components/onboarding/onboarding-flow.js +14 -10
  152. package/dist/tui/components/onboarding/welcome-box.js +1 -1
  153. package/dist/tui/contexts/onboarding-context.d.ts +4 -0
  154. package/dist/tui/contexts/onboarding-context.js +14 -2
  155. package/dist/tui/views/command-view.js +19 -0
  156. package/dist/utils/file-validator.d.ts +1 -1
  157. package/dist/utils/file-validator.js +34 -35
  158. package/dist/utils/type-guards.d.ts +5 -0
  159. package/dist/utils/type-guards.js +7 -0
  160. package/oclif.manifest.json +32 -6
  161. package/package.json +4 -1
  162. package/dist/config/context-tree-domains.d.ts +0 -29
  163. package/dist/config/context-tree-domains.js +0 -29
  164. package/dist/core/interfaces/usecase/i-generate-rules-use-case.d.ts +0 -3
  165. package/dist/infra/repl/commands/gen-rules-command.d.ts +0 -7
  166. package/dist/infra/rule/agent-rule-config.d.ts +0 -19
  167. package/dist/infra/usecase/generate-rules-use-case.d.ts +0 -61
  168. package/dist/infra/usecase/generate-rules-use-case.js +0 -285
  169. /package/dist/core/interfaces/{usecase/i-clear-use-case.js → connectors/connector-types.js} +0 -0
  170. /package/dist/core/interfaces/{usecase/i-generate-rules-use-case.js → connectors/i-connector-manager.js} +0 -0
  171. /package/dist/infra/{rule → connectors/shared}/constants.d.ts +0 -0
  172. /package/dist/infra/{rule → connectors/shared}/constants.js +0 -0
@@ -1,4 +1,3 @@
1
- import * as fs from 'node:fs/promises';
2
1
  import { join } from 'node:path';
3
2
  import { z } from 'zod';
4
3
  import { ToolName } from '../../../../core/domain/cipher/tools/constants.js';
@@ -10,14 +9,43 @@ import { toSnakeCase } from '../../../../utils/file-helpers.js';
10
9
  * Inspired by ACE Curator patterns.
11
10
  */
12
11
  const OperationType = z.enum(['ADD', 'UPDATE', 'MERGE', 'DELETE']);
12
+ /**
13
+ * Raw Concept schema for structured metadata and technical footprint.
14
+ */
15
+ const RawConceptSchema = z.object({
16
+ changes: z.array(z.string()).optional().describe('What changes in the codebase are induced by this concept'),
17
+ files: z.array(z.string()).optional().describe('Which files are related to this concept'),
18
+ flow: z.string().optional().describe('What is the flow included in this concept'),
19
+ task: z.string().optional().describe('What is the task related to this concept'),
20
+ timestamp: z
21
+ .string()
22
+ .optional()
23
+ .describe('When the concept was created or modified (ISO 8601 format, e.g., 2025-03-18)'),
24
+ });
25
+ /**
26
+ * Narrative schema for descriptive and structural context.
27
+ */
28
+ const NarrativeSchema = z.object({
29
+ dependencies: z
30
+ .string()
31
+ .optional()
32
+ .describe('Dependency management information (e.g., "Singleton, init when service starts, hard dependency in smoke test")'),
33
+ features: z
34
+ .string()
35
+ .optional()
36
+ .describe('Feature documentation for this concept (e.g., "User permission can be stale for up to 300 seconds due to Redis cache")'),
37
+ structure: z.string().optional().describe('Code structure documentation (e.g., "clients/redis_client.go")'),
38
+ });
13
39
  /**
14
40
  * Content structure for ADD and UPDATE operations.
15
41
  */
16
42
  const ContentSchema = z.object({
43
+ narrative: NarrativeSchema.optional().describe('Narrative section with descriptive and structural context'),
44
+ rawConcept: RawConceptSchema.optional().describe('Raw concept section with metadata and technical footprint'),
17
45
  relations: z
18
46
  .array(z.string())
19
47
  .optional()
20
- .describe('Related topics using domain/topic or domain/topic/subtopic notation'),
48
+ .describe('Related topics using domain/topic/title.md or domain/topic/subtopic/title.md notation'),
21
49
  snippets: z.array(z.string()).optional().describe('Code/text snippets'),
22
50
  });
23
51
  /**
@@ -27,9 +55,12 @@ const OperationSchema = z.object({
27
55
  content: ContentSchema.optional().describe('Content for ADD/UPDATE operations'),
28
56
  mergeTarget: z.string().optional().describe('Target path for MERGE operation'),
29
57
  mergeTargetTitle: z.string().optional().describe('Title of the target file for MERGE operation'),
30
- path: z.string().describe('Path: domain/topic or domain/topic/subtopic'),
58
+ path: z.string().describe('Path: domain/topic/title.md or domain/topic/subtopic/title.md'),
31
59
  reason: z.string().describe('Reasoning for this operation'),
32
- title: z.string().optional().describe('Title for the context file (saved as {title}.md in snake_case). Required for ADD/UPDATE/MERGE, optional for DELETE'),
60
+ title: z
61
+ .string()
62
+ .optional()
63
+ .describe('Title for the context file (saved as {title}.md in snake_case). Required for ADD/UPDATE/MERGE, optional for DELETE'),
33
64
  type: OperationType.describe('Operation type: ADD, UPDATE, MERGE, or DELETE'),
34
65
  });
35
66
  /**
@@ -53,33 +84,17 @@ function parsePath(path) {
53
84
  topic: parts[1],
54
85
  };
55
86
  }
56
- /**
57
- * Get existing domain names from the context tree.
58
- * Returns domain folder names that exist in the context tree.
59
- */
60
- async function getExistingDomains(basePath) {
61
- try {
62
- const entries = await fs.readdir(basePath, { withFileTypes: true });
63
- return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
64
- }
65
- catch {
66
- // Directory doesn't exist yet
67
- return [];
68
- }
69
- }
70
87
  /**
71
88
  * Validate domain name format.
72
89
  * Dynamic domains are allowed - no predefined list or limits.
73
90
  * The agent is responsible for creating semantically meaningful domains.
74
91
  */
75
- async function validateDomain(basePath, domainName) {
92
+ function validateDomain(domainName) {
76
93
  const normalizedDomain = toSnakeCase(domainName);
77
- const existingDomains = await getExistingDomains(basePath);
78
94
  // Validate domain name format (must be non-empty and valid for filesystem)
79
95
  if (!normalizedDomain || normalizedDomain.length === 0) {
80
96
  return {
81
97
  allowed: false,
82
- existingDomains,
83
98
  reason: 'Domain name cannot be empty.',
84
99
  };
85
100
  }
@@ -87,12 +102,11 @@ async function validateDomain(basePath, domainName) {
87
102
  if (!/^[\w-]+$/.test(normalizedDomain)) {
88
103
  return {
89
104
  allowed: false,
90
- existingDomains,
91
105
  reason: `Domain name "${normalizedDomain}" contains invalid characters. Use only letters, numbers, underscores, and hyphens.`,
92
106
  };
93
107
  }
94
108
  // All valid domain names are allowed - dynamic domain creation enabled
95
- return { allowed: true, existingDomains };
109
+ return { allowed: true };
96
110
  }
97
111
  /**
98
112
  * Build the full filesystem path from base path and knowledge path.
@@ -142,7 +156,7 @@ async function executeAdd(basePath, operation) {
142
156
  };
143
157
  }
144
158
  // Validate domain before creating
145
- const domainValidation = await validateDomain(basePath, parsed.domain);
159
+ const domainValidation = validateDomain(parsed.domain);
146
160
  if (!domainValidation.allowed) {
147
161
  return {
148
162
  message: domainValidation.reason,
@@ -159,8 +173,10 @@ async function executeAdd(basePath, operation) {
159
173
  // Note: writeFileAtomic creates parent directories as needed, avoiding empty folder creation
160
174
  const contextContent = MarkdownWriter.generateContext({
161
175
  name: title,
176
+ narrative: content.narrative,
177
+ rawConcept: content.rawConcept,
162
178
  relations: content.relations,
163
- snippets: content.snippets || [],
179
+ snippets: content.snippets ?? [],
164
180
  });
165
181
  const filename = `${toSnakeCase(title)}.md`;
166
182
  const contextPath = join(finalPath, filename);
@@ -220,8 +236,10 @@ async function executeUpdate(basePath, operation) {
220
236
  // Generate and write updated content (full replacement)
221
237
  const contextContent = MarkdownWriter.generateContext({
222
238
  name: title,
239
+ narrative: content.narrative,
240
+ rawConcept: content.rawConcept,
223
241
  relations: content.relations,
224
- snippets: content.snippets || [],
242
+ snippets: content.snippets ?? [],
225
243
  });
226
244
  await DirectoryManager.writeFileAtomic(contextPath, contextContent);
227
245
  return {
@@ -381,7 +399,27 @@ async function executeDelete(basePath, operation) {
381
399
  * Execute curate operations on knowledge topics.
382
400
  */
383
401
  async function executeCurate(input, _context) {
384
- const { basePath, operations } = input;
402
+ const parseResult = CurateInputSchema.safeParse(input);
403
+ if (!parseResult.success) {
404
+ return {
405
+ applied: [
406
+ {
407
+ message: `Invalid input: ${parseResult.error.message}`,
408
+ path: '',
409
+ status: 'failed',
410
+ type: 'ADD',
411
+ },
412
+ ],
413
+ summary: {
414
+ added: 0,
415
+ deleted: 0,
416
+ failed: 1,
417
+ merged: 0,
418
+ updated: 0,
419
+ },
420
+ };
421
+ }
422
+ const { basePath, operations } = parseResult.data;
385
423
  const applied = [];
386
424
  const summary = {
387
425
  added: 0,
@@ -420,8 +458,10 @@ async function executeCurate(input, _context) {
420
458
  break;
421
459
  }
422
460
  default: {
461
+ // Exhaustive type check - TypeScript will error if any case is missed
462
+ const exhaustiveCheck = operation.type;
423
463
  result = {
424
- message: `Unknown operation type: ${operation.type}`,
464
+ message: `Unknown operation type: ${exhaustiveCheck}`,
425
465
  path: operation.path,
426
466
  status: 'failed',
427
467
  type: operation.type,
@@ -446,30 +486,84 @@ async function executeCurate(input, _context) {
446
486
  */
447
487
  export function createCurateTool() {
448
488
  return {
449
- description: `Curate knowledge topics with atomic operations. This tool manages the knowledge structure using four operation types:
489
+ 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.
490
+
491
+ **Content Structure (Two-Part Model):**
492
+ - **rawConcept**: Captures essential metadata and technical footprint
493
+ - task: What is the task related to this concept
494
+ - changes: Array of changes induced in the codebase
495
+ - files: Array of related files
496
+ - flow: The execution flow of this concept
497
+ - timestamp: When created/modified (ISO 8601 format)
498
+ - **narrative**: Captures descriptive and structural context
499
+ - structure: Code structure documentation
500
+ - dependencies: Dependency management information
501
+ - features: Feature documentation
502
+ - **snippets**: Code/text snippets (legacy support)
503
+ - **relations**: Related topics using @domain/topic notation
450
504
 
451
505
  **Operations:**
452
506
  1. **ADD** - Create new titled context file in domain/topic/subtopic
453
507
  - Requires: path, title, content (snippets and/or relations), reason
454
- - Example: { type: "ADD", path: "code_style/error_handling", title: "Best Practices", content: { snippets: ["..."], relations: ["logging/basics"] }, reason: "New pattern" }
455
- - Creates: code_style/error_handling/best_practices.md
508
+ - Relations must be in the format of "domain/topic/title.md" or "domain/topic/subtopic/title.md"
509
+ - Example with Raw Concept + Narrative:
510
+ {
511
+ type: "ADD",
512
+ path: "structure/caching",
513
+ title: "Redis User Permissions",
514
+ content: {
515
+ rawConcept: {
516
+ task: "Introduce Redis cache for getUserPermissions(userId)",
517
+ changes: ["Cached result using remote Redis", "Redis client: singleton"],
518
+ files: ["services/permission_service.go", "clients/redis_client.go"],
519
+ flow: "getUserPermissions -> check Redis -> on miss query DB -> store result -> return",
520
+ timestamp: "2025-03-18"
521
+ },
522
+ narrative: {
523
+ structure: "# Redis client\\n- clients/redis_client.go",
524
+ dependencies: "# Redis client\\n- Singleton, init when service starts",
525
+ features: "# Authorization\\n- User permission can be stale for up to 300 seconds"
526
+ },
527
+ relations: ["structure/api-endpoints/validation.md", "structure/api-endpoints/error-handling/retry-logic.md"]
528
+ },
529
+ reason: "New caching pattern"
530
+ }
531
+ - Creates: structure/caching/redis_user_permissions.md
456
532
 
457
533
  2. **UPDATE** - Modify existing titled context file (full replacement)
458
534
  - Requires: path, title, content, reason
459
- - Example: { type: "UPDATE", path: "code_style/error_handling", title: "Best Practices", content: { snippets: ["Updated"] }, reason: "Improved" }
535
+ - Relations must be in the format of "domain/topic/title.md" or "domain/topic/subtopic/title.md"
536
+ - Supports same content structure as ADD
460
537
 
461
538
  3. **MERGE** - Combine source file into target file, delete source
462
539
  - Requires: path (source), title (source file), mergeTarget (destination path), mergeTargetTitle (destination file), reason
463
540
  - Example: { type: "MERGE", path: "code_style/old_topic", title: "Old Guide", mergeTarget: "code_style/new_topic", mergeTargetTitle: "New Guide", reason: "Consolidating" }
541
+ - Raw concepts and narratives are intelligently merged
464
542
 
465
543
  4. **DELETE** - Remove specific file or entire folder
466
544
  - Requires: path, title (optional), reason
467
545
  - With title: deletes specific file; without title: deletes entire folder
468
- - Example (file): { type: "DELETE", path: "code_style/deprecated", title: "Old Guide", reason: "No longer relevant" }
469
- - Example (folder): { type: "DELETE", path: "code_style/deprecated", title: "", reason: "Removing topic" }
470
546
 
471
- **Path format:** domain/topic or domain/topic/subtopic (uses snake_case automatically)
472
- **File naming:** Titles are converted to snake_case (e.g., "Best Practices" -> "best_practices.md")
547
+ **CRITICAL - Path vs Title separation:**
548
+ - "path" = folder location only (domain/topic or domain/topic/subtopic) - NEVER include file extension suffixes
549
+ - "title" = the context name (becomes {title}.md file automatically)
550
+ - The system auto-generates the .md file from title - DO NOT put .md or _md anywhere in path
551
+
552
+ **Path format:** domain/topic OR domain/topic/subtopic (2-3 segments, NO filename, NO extension)
553
+ **File naming:** Title is auto-converted to snake_case and .md is auto-appended (e.g., title "Best Practices" -> best_practices.md)
554
+
555
+ **Good path examples:**
556
+ - path: "authentication/jwt", title: "Token Refresh" -> creates authentication/jwt/token_refresh.md
557
+ - path: "api_design/error_handling", title: "Retry Logic" -> creates api_design/error_handling/retry_logic.md
558
+ - path: "database/migrations/versioning", title: "Schema Changes" -> creates database/migrations/versioning/schema_changes.md
559
+
560
+ **Bad path examples (NEVER DO THIS):**
561
+ - "code_style/error_handling_md" - WRONG: _md suffix in path
562
+ - "code_style/error_handling.md" - WRONG: .md extension in path
563
+ - "authentication/jwt/token_refresh.md" - WRONG: filename in path (use title parameter instead)
564
+ - "authentication/jwt/token_refresh_md" - WRONG: _md suffix (this is NOT how to specify filename)
565
+ - "api/auth/jwt/tokens" - WRONG: 4 levels deep (max 3 allowed)
566
+ - "a/b" - WRONG: too vague, use descriptive names
473
567
 
474
568
  **Dynamic Domain Creation:**
475
569
  - Domains are created dynamically based on the context being curated
@@ -485,6 +579,8 @@ export function createCurateTool() {
485
579
  - Avoid overly specific names that only fit one topic
486
580
  - Keep domain count reasonable by consolidating related concepts
487
581
 
582
+ **Backward Compatibility:** Existing context entries using only snippets and relations continue to work.
583
+
488
584
  **Output:** Returns applied operations with status (success/failed), filePath (for created/modified files), and a summary of counts.`,
489
585
  execute: executeCurate,
490
586
  id: ToolName.CURATE,
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { ToolName } from '../../../../core/domain/cipher/tools/constants.js';
3
+ import { isImageFile } from '../../file-system/binary-utils.js';
3
4
  /**
4
5
  * Input schema for read file tool.
5
6
  */
@@ -31,23 +32,43 @@ export function createReadFileTool(fileSystemService) {
31
32
  description: 'Read the contents of a file. Supports relative/absolute paths, pagination, and returns images/PDFs as base64 attachments.',
32
33
  async execute(input, _context) {
33
34
  const { filePath, limit, offset } = input;
34
- // Call file system service
35
- const result = await fileSystemService.readFile(filePath, {
36
- limit,
37
- offset,
38
- });
39
- // Return formatted result with all metadata
40
- return {
41
- attachment: result.attachment,
42
- content: result.formattedContent,
43
- lines: result.lines,
44
- message: result.message,
45
- preview: result.preview,
46
- size: result.size,
47
- totalLines: result.totalLines,
48
- truncated: result.truncated,
49
- truncatedLineCount: result.truncatedLineCount,
50
- };
35
+ try {
36
+ // Call file system service
37
+ const result = await fileSystemService.readFile(filePath, {
38
+ limit,
39
+ offset,
40
+ });
41
+ // Transform attachment format (singular → plural array)
42
+ let attachments;
43
+ if (result.attachment) {
44
+ const type = isImageFile(filePath) ? 'image' : 'file';
45
+ attachments = [{
46
+ data: result.attachment.base64,
47
+ filename: result.attachment.fileName,
48
+ mimeType: result.attachment.mimeType,
49
+ type,
50
+ }];
51
+ }
52
+ // Return formatted result with all metadata
53
+ return {
54
+ attachments,
55
+ content: result.formattedContent,
56
+ lines: result.lines,
57
+ message: result.message,
58
+ preview: result.preview,
59
+ size: result.size,
60
+ success: true,
61
+ totalLines: result.totalLines,
62
+ truncated: result.truncated,
63
+ truncatedLineCount: result.truncatedLineCount,
64
+ };
65
+ }
66
+ catch (error) {
67
+ return {
68
+ error: error instanceof Error ? error.message : String(error),
69
+ success: false,
70
+ };
71
+ }
51
72
  },
52
73
  id: ToolName.READ_FILE,
53
74
  inputSchema: ReadFileInputSchema,
@@ -0,0 +1,7 @@
1
+ import type { Tool } from '../../../../core/domain/cipher/tools/types.js';
2
+ import type { IFileSystem } from '../../../../core/interfaces/cipher/i-file-system.js';
3
+ export interface SearchKnowledgeToolConfig {
4
+ baseDirectory?: string;
5
+ cacheTtlMs?: number;
6
+ }
7
+ export declare function createSearchKnowledgeTool(fileSystem: IFileSystem, config?: SearchKnowledgeToolConfig): Tool;
@@ -0,0 +1,303 @@
1
+ import MiniSearch from 'minisearch';
2
+ import { join } from 'node:path';
3
+ import { removeStopwords } from 'stopword';
4
+ import { z } from 'zod';
5
+ import { BRV_DIR, CONTEXT_FILE_EXTENSION, CONTEXT_TREE_DIR } from '../../../../constants.js';
6
+ import { ToolName } from '../../../../core/domain/cipher/tools/constants.js';
7
+ const MAX_CONTEXT_TREE_FILES = 10_000;
8
+ const CACHE_TTL_MS = 5000;
9
+ const MINISEARCH_OPTIONS = {
10
+ fields: ['title', 'content'],
11
+ idField: 'id',
12
+ searchOptions: {
13
+ boost: { title: 2 },
14
+ fuzzy: 0.2,
15
+ prefix: true,
16
+ },
17
+ storeFields: ['title', 'path'],
18
+ };
19
+ const SearchKnowledgeInputSchema = z
20
+ .object({
21
+ limit: z
22
+ .number()
23
+ .int()
24
+ .positive()
25
+ .optional()
26
+ .default(10)
27
+ .describe('Maximum number of results to return (default: 10)'),
28
+ query: z.string().min(1).describe('Natural language query string to search for in the knowledge base'),
29
+ })
30
+ .strict();
31
+ function filterStopWords(query) {
32
+ const words = query.toLowerCase().split(/\s+/);
33
+ const filtered = removeStopwords(words);
34
+ return filtered.length > 0 ? filtered.join(' ') : query;
35
+ }
36
+ function extractTitle(content, fallbackTitle) {
37
+ const match = /^# (.+)$/m.exec(content);
38
+ return match ? match[1].trim() : fallbackTitle;
39
+ }
40
+ function extractExcerpt(content, query, maxLength = 300) {
41
+ const relationsMatch = /^## Relations\n([\S\s]*?)(?=\n## |\n# |$)/m.exec(content);
42
+ let cleanContent = content;
43
+ if (relationsMatch) {
44
+ cleanContent = content.replace(relationsMatch[0], '').trim();
45
+ }
46
+ cleanContent = cleanContent.replace(/^# .+$/m, '').trim();
47
+ const queryTerms = query
48
+ .toLowerCase()
49
+ .split(/\s+/)
50
+ .filter((t) => t.length >= 2);
51
+ const lines = cleanContent.split('\n');
52
+ let bestStartIndex = 0;
53
+ let bestScore = 0;
54
+ for (const [i, line] of lines.entries()) {
55
+ const lineLower = line.toLowerCase();
56
+ let score = 0;
57
+ for (const term of queryTerms) {
58
+ if (lineLower.includes(term)) {
59
+ score++;
60
+ }
61
+ }
62
+ if (score > bestScore) {
63
+ bestScore = score;
64
+ bestStartIndex = i;
65
+ }
66
+ }
67
+ let excerpt = '';
68
+ for (const line of lines.slice(bestStartIndex)) {
69
+ if (excerpt.length >= maxLength)
70
+ break;
71
+ excerpt += line + '\n';
72
+ }
73
+ excerpt = excerpt.trim();
74
+ if (excerpt.length > maxLength) {
75
+ excerpt = excerpt.slice(0, maxLength).trim() + '...';
76
+ }
77
+ else if (bestStartIndex > 0 || excerpt.length < cleanContent.length) {
78
+ excerpt += '...';
79
+ }
80
+ return excerpt || cleanContent.slice(0, maxLength) + (cleanContent.length > maxLength ? '...' : '');
81
+ }
82
+ async function findMarkdownFilesWithMtime(fileSystem, contextTreePath) {
83
+ try {
84
+ const globResult = await fileSystem.globFiles(`**/*${CONTEXT_FILE_EXTENSION}`, {
85
+ cwd: contextTreePath,
86
+ includeMetadata: true,
87
+ maxResults: MAX_CONTEXT_TREE_FILES,
88
+ respectGitignore: false,
89
+ });
90
+ return globResult.files.map((f) => {
91
+ let relativePath = f.path;
92
+ if (f.path.startsWith(contextTreePath)) {
93
+ relativePath = f.path.slice(contextTreePath.length + 1);
94
+ }
95
+ return {
96
+ mtime: f.modified?.getTime() ?? 0,
97
+ path: relativePath,
98
+ };
99
+ });
100
+ }
101
+ catch {
102
+ return [];
103
+ }
104
+ }
105
+ function isCacheValid(cache, currentFiles) {
106
+ if (cache.fileMtimes.size !== currentFiles.length) {
107
+ return false;
108
+ }
109
+ for (const file of currentFiles) {
110
+ const cachedMtime = cache.fileMtimes.get(file.path);
111
+ if (cachedMtime === undefined || cachedMtime !== file.mtime) {
112
+ return false;
113
+ }
114
+ }
115
+ return true;
116
+ }
117
+ async function buildFreshIndex(fileSystem, contextTreePath, filesWithMtime) {
118
+ const now = Date.now();
119
+ if (filesWithMtime.length === 0) {
120
+ const index = new MiniSearch(MINISEARCH_OPTIONS);
121
+ return {
122
+ contextTreePath,
123
+ documentMap: new Map(),
124
+ fileMtimes: new Map(),
125
+ index,
126
+ lastValidatedAt: now,
127
+ };
128
+ }
129
+ const documentPromises = filesWithMtime.map(async ({ mtime, path: filePath }) => {
130
+ try {
131
+ const fullPath = join(contextTreePath, filePath);
132
+ const { content } = await fileSystem.readFile(fullPath);
133
+ const title = extractTitle(content, filePath.replace(/\.md$/, '').split('/').pop() || filePath);
134
+ return {
135
+ content,
136
+ id: filePath,
137
+ mtime,
138
+ path: filePath,
139
+ title,
140
+ };
141
+ }
142
+ catch {
143
+ return null;
144
+ }
145
+ });
146
+ const results = await Promise.all(documentPromises);
147
+ const documents = results.filter((doc) => doc !== null);
148
+ const documentMap = new Map();
149
+ const fileMtimes = new Map();
150
+ for (const doc of documents) {
151
+ documentMap.set(doc.id, doc);
152
+ fileMtimes.set(doc.path, doc.mtime);
153
+ }
154
+ const index = new MiniSearch(MINISEARCH_OPTIONS);
155
+ index.addAll(documents);
156
+ return {
157
+ contextTreePath,
158
+ documentMap,
159
+ fileMtimes,
160
+ index,
161
+ lastValidatedAt: now,
162
+ };
163
+ }
164
+ /**
165
+ * Acquires the search index, using cached data when valid or building a fresh index.
166
+ * Uses promise-based locking to prevent duplicate builds during parallel execution.
167
+ *
168
+ * @param state - Mutable state object for caching and locking
169
+ * @param fileSystem - File system service
170
+ * @param contextTreePath - Path to the context tree directory
171
+ * @param ttlMs - Cache TTL in milliseconds
172
+ * @returns The cached index or an error result
173
+ */
174
+ async function acquireIndex(state, fileSystem, contextTreePath, ttlMs) {
175
+ const now = Date.now();
176
+ // Fast path: TTL-based cache hit (no I/O needed)
177
+ if (state.cachedIndex &&
178
+ state.cachedIndex.contextTreePath === contextTreePath &&
179
+ ttlMs > 0 &&
180
+ now - state.cachedIndex.lastValidatedAt < ttlMs) {
181
+ return state.cachedIndex;
182
+ }
183
+ // If another call is already building the index, wait for it
184
+ if (state.buildingPromise) {
185
+ return state.buildingPromise;
186
+ }
187
+ // Create and store the build promise SYNCHRONOUSLY before any await
188
+ // This prevents race conditions where multiple parallel calls all start building
189
+ const buildPromise = (async () => {
190
+ // Check if context tree exists (only if no cache or different path)
191
+ if (!state.cachedIndex || state.cachedIndex.contextTreePath !== contextTreePath) {
192
+ try {
193
+ await fileSystem.listDirectory(contextTreePath);
194
+ }
195
+ catch {
196
+ // Return empty index to signal error - caller will handle
197
+ const emptyIndex = new MiniSearch(MINISEARCH_OPTIONS);
198
+ return {
199
+ contextTreePath: '',
200
+ documentMap: new Map(),
201
+ fileMtimes: new Map(),
202
+ index: emptyIndex,
203
+ lastValidatedAt: 0,
204
+ };
205
+ }
206
+ }
207
+ const currentFiles = await findMarkdownFilesWithMtime(fileSystem, contextTreePath);
208
+ // Re-check cache validity after getting file list (another call may have finished)
209
+ if (state.cachedIndex &&
210
+ state.cachedIndex.contextTreePath === contextTreePath &&
211
+ isCacheValid(state.cachedIndex, currentFiles)) {
212
+ // Update timestamp atomically by creating a new object
213
+ const updatedCache = {
214
+ ...state.cachedIndex,
215
+ lastValidatedAt: Date.now(),
216
+ };
217
+ state.cachedIndex = updatedCache;
218
+ return updatedCache;
219
+ }
220
+ // Build fresh index
221
+ const freshIndex = await buildFreshIndex(fileSystem, contextTreePath, currentFiles);
222
+ state.cachedIndex = freshIndex;
223
+ return freshIndex;
224
+ })();
225
+ // Store promise IMMEDIATELY (synchronously) so parallel calls can wait on it
226
+ state.buildingPromise = buildPromise;
227
+ try {
228
+ const result = await buildPromise;
229
+ // Check for error signal (empty contextTreePath means listDirectory failed)
230
+ if (result.contextTreePath === '') {
231
+ return {
232
+ error: true,
233
+ result: {
234
+ message: 'Context tree not initialized. Run /init to create it.',
235
+ results: [],
236
+ totalFound: 0,
237
+ },
238
+ };
239
+ }
240
+ return result;
241
+ }
242
+ finally {
243
+ // Clear the lock after completion (success or failure)
244
+ state.buildingPromise = undefined;
245
+ }
246
+ }
247
+ export function createSearchKnowledgeTool(fileSystem, config = {}) {
248
+ // Shared state for caching and locking across parallel executions
249
+ const state = {
250
+ buildingPromise: undefined,
251
+ cachedIndex: undefined,
252
+ };
253
+ return {
254
+ description: 'Search the curated knowledge base in .brv/context-tree/ for relevant topics. ' +
255
+ 'Use natural language queries to find knowledge about specific topics (e.g., "auth design", "API patterns"). ' +
256
+ 'Returns matching file paths, titles, and relevant excerpts.',
257
+ async execute(input, _context) {
258
+ const { limit, query } = SearchKnowledgeInputSchema.parse(input);
259
+ const baseDir = config.baseDirectory ?? process.cwd();
260
+ const contextTreePath = join(baseDir, BRV_DIR, CONTEXT_TREE_DIR);
261
+ const ttlMs = config.cacheTtlMs ?? CACHE_TTL_MS;
262
+ // Acquire index with parallel-safe locking
263
+ const indexResult = await acquireIndex(state, fileSystem, contextTreePath, ttlMs);
264
+ // Handle error case (context tree not initialized)
265
+ if ('error' in indexResult) {
266
+ return indexResult.result;
267
+ }
268
+ const { documentMap, index } = indexResult;
269
+ if (documentMap.size === 0) {
270
+ return {
271
+ message: 'Context tree is empty. Use /curate to add knowledge.',
272
+ results: [],
273
+ totalFound: 0,
274
+ };
275
+ }
276
+ const filteredQuery = filterStopWords(query);
277
+ const searchResults = index.search(filteredQuery, { combineWith: 'OR' });
278
+ const results = [];
279
+ const resultLimit = Math.min(limit, searchResults.length);
280
+ for (let i = 0; i < resultLimit; i++) {
281
+ const result = searchResults[i];
282
+ const document = documentMap.get(result.id);
283
+ if (document) {
284
+ results.push({
285
+ excerpt: extractExcerpt(document.content, query),
286
+ path: document.path,
287
+ score: Math.round(result.score * 100) / 100,
288
+ title: document.title,
289
+ });
290
+ }
291
+ }
292
+ return {
293
+ message: results.length > 0
294
+ ? `Found ${searchResults.length} result(s). Use read_file to view full content.`
295
+ : 'No matching knowledge found. Try different search terms or check available topics with /query.',
296
+ results,
297
+ totalFound: searchResults.length,
298
+ };
299
+ },
300
+ id: ToolName.SEARCH_KNOWLEDGE,
301
+ inputSchema: SearchKnowledgeInputSchema,
302
+ };
303
+ }
@@ -95,6 +95,7 @@ export function createTaskTool(dependencies) {
95
95
  const registry = getAgentRegistry();
96
96
  return {
97
97
  description: buildTaskToolDescription(registry),
98
+ // eslint-disable-next-line complexity -- Inherent complexity: validates agent, manages sessions, handles errors
98
99
  async execute(input, context) {
99
100
  const params = input;
100
101
  const { contextTreeOnly, description, prompt, sessionId, subagentType } = params;