byterover-cli 1.7.1 → 1.8.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 (67) hide show
  1. package/README.md +21 -5
  2. package/dist/agent/core/domain/tools/constants.d.ts +0 -15
  3. package/dist/agent/core/domain/tools/constants.js +0 -15
  4. package/dist/agent/core/interfaces/i-cipher-agent.d.ts +6 -0
  5. package/dist/agent/core/interfaces/i-curate-service.d.ts +12 -0
  6. package/dist/agent/infra/llm/internal-llm-service.d.ts +13 -0
  7. package/dist/agent/infra/llm/internal-llm-service.js +61 -21
  8. package/dist/agent/infra/sandbox/local-sandbox.js +9 -2
  9. package/dist/agent/infra/tools/implementations/curate-tool.d.ts +133 -0
  10. package/dist/agent/infra/tools/implementations/curate-tool.js +14 -0
  11. package/dist/agent/infra/tools/implementations/search-knowledge-service.js +91 -14
  12. package/dist/agent/infra/tools/index.d.ts +0 -4
  13. package/dist/agent/infra/tools/index.js +0 -4
  14. package/dist/agent/infra/tools/tool-registry.js +0 -113
  15. package/dist/agent/resources/prompts/curate-detail-preservation.yml +73 -0
  16. package/dist/agent/resources/prompts/system-prompt.yml +69 -3
  17. package/dist/server/core/domain/knowledge/markdown-writer.d.ts +13 -0
  18. package/dist/server/core/domain/knowledge/markdown-writer.js +116 -8
  19. package/dist/server/infra/executor/curate-executor.js +1 -1
  20. package/dist/server/infra/executor/direct-search-responder.d.ts +45 -0
  21. package/dist/server/infra/executor/direct-search-responder.js +86 -0
  22. package/dist/server/infra/executor/folder-pack-executor.d.ts +13 -5
  23. package/dist/server/infra/executor/folder-pack-executor.js +739 -39
  24. package/dist/server/infra/executor/query-executor.d.ts +49 -3
  25. package/dist/server/infra/executor/query-executor.js +194 -9
  26. package/dist/server/infra/executor/query-result-cache.d.ts +87 -0
  27. package/dist/server/infra/executor/query-result-cache.js +127 -0
  28. package/dist/server/infra/executor/query-similarity.d.ts +28 -0
  29. package/dist/server/infra/executor/query-similarity.js +41 -0
  30. package/dist/server/infra/process/agent-worker.js +9 -2
  31. package/dist/server/infra/process/inline-agent-executor.js +16 -5
  32. package/dist/server/infra/usecase/curate-use-case.js +6 -1
  33. package/dist/server/infra/usecase/query-use-case.js +10 -0
  34. package/dist/server/utils/file-validator.js +78 -1
  35. package/dist/tui/hooks/use-slash-completion.js +25 -4
  36. package/oclif.manifest.json +1 -1
  37. package/package.json +2 -1
  38. package/dist/agent/infra/tools/implementations/bash-exec-tool.d.ts +0 -13
  39. package/dist/agent/infra/tools/implementations/bash-exec-tool.js +0 -110
  40. package/dist/agent/infra/tools/implementations/bash-output-tool.d.ts +0 -12
  41. package/dist/agent/infra/tools/implementations/bash-output-tool.js +0 -43
  42. package/dist/agent/infra/tools/implementations/batch-tool.d.ts +0 -12
  43. package/dist/agent/infra/tools/implementations/batch-tool.js +0 -142
  44. package/dist/agent/infra/tools/implementations/create-knowledge-topic-tool.d.ts +0 -11
  45. package/dist/agent/infra/tools/implementations/create-knowledge-topic-tool.js +0 -149
  46. package/dist/agent/infra/tools/implementations/delete-memory-tool.d.ts +0 -12
  47. package/dist/agent/infra/tools/implementations/delete-memory-tool.js +0 -37
  48. package/dist/agent/infra/tools/implementations/edit-file-tool.d.ts +0 -13
  49. package/dist/agent/infra/tools/implementations/edit-file-tool.js +0 -50
  50. package/dist/agent/infra/tools/implementations/edit-memory-tool.d.ts +0 -13
  51. package/dist/agent/infra/tools/implementations/edit-memory-tool.js +0 -53
  52. package/dist/agent/infra/tools/implementations/kill-process-tool.d.ts +0 -12
  53. package/dist/agent/infra/tools/implementations/kill-process-tool.js +0 -55
  54. package/dist/agent/infra/tools/implementations/list-memories-tool.d.ts +0 -12
  55. package/dist/agent/infra/tools/implementations/list-memories-tool.js +0 -63
  56. package/dist/agent/infra/tools/implementations/read-memory-tool.d.ts +0 -12
  57. package/dist/agent/infra/tools/implementations/read-memory-tool.js +0 -39
  58. package/dist/agent/infra/tools/implementations/read-todos-tool.d.ts +0 -11
  59. package/dist/agent/infra/tools/implementations/read-todos-tool.js +0 -39
  60. package/dist/agent/infra/tools/implementations/search-history-tool.d.ts +0 -10
  61. package/dist/agent/infra/tools/implementations/search-history-tool.js +0 -36
  62. package/dist/agent/infra/tools/implementations/spec-analyze-tool.d.ts +0 -7
  63. package/dist/agent/infra/tools/implementations/spec-analyze-tool.js +0 -78
  64. package/dist/agent/infra/tools/implementations/write-memory-tool.d.ts +0 -13
  65. package/dist/agent/infra/tools/implementations/write-memory-tool.js +0 -52
  66. package/dist/agent/infra/tools/implementations/write-todos-tool.d.ts +0 -13
  67. package/dist/agent/infra/tools/implementations/write-todos-tool.js +0 -121
@@ -1,23 +1,37 @@
1
1
  import { generateRelationsSection, parseRelations } from './relation-parser.js';
2
+ /**
3
+ * Normalizes newline characters in text.
4
+ * Converts literal \n strings to actual newlines.
5
+ */
6
+ function normalizeNewlines(text) {
7
+ return text.replaceAll(String.raw `\n`, '\n');
8
+ }
2
9
  function generateRawConceptSection(rawConcept) {
3
10
  if (!rawConcept) {
4
11
  return '';
5
12
  }
6
13
  const parts = [];
7
14
  if (rawConcept.task) {
8
- parts.push(`**Task:**\n${rawConcept.task}`);
15
+ parts.push(`**Task:**\n${normalizeNewlines(rawConcept.task)}`);
9
16
  }
10
17
  if (rawConcept.changes && rawConcept.changes.length > 0) {
11
- parts.push(`**Changes:**\n${rawConcept.changes.map(c => `- ${c}`).join('\n')}`);
18
+ parts.push(`**Changes:**\n${rawConcept.changes.map(c => `- ${normalizeNewlines(c)}`).join('\n')}`);
12
19
  }
13
20
  if (rawConcept.files && rawConcept.files.length > 0) {
14
- parts.push(`**Files:**\n${rawConcept.files.map(f => `- ${f}`).join('\n')}`);
21
+ parts.push(`**Files:**\n${rawConcept.files.map(f => `- ${normalizeNewlines(f)}`).join('\n')}`);
15
22
  }
16
23
  if (rawConcept.flow) {
17
- parts.push(`**Flow:**\n${rawConcept.flow}`);
24
+ parts.push(`**Flow:**\n${normalizeNewlines(rawConcept.flow)}`);
18
25
  }
19
26
  if (rawConcept.timestamp) {
20
- parts.push(`**Timestamp:** ${rawConcept.timestamp}`);
27
+ parts.push(`**Timestamp:** ${normalizeNewlines(rawConcept.timestamp)}`);
28
+ }
29
+ if (rawConcept.author) {
30
+ parts.push(`**Author:** ${rawConcept.author}`);
31
+ }
32
+ if (rawConcept.patterns && rawConcept.patterns.length > 0) {
33
+ const patternsText = rawConcept.patterns.map(p => `- \`${p.pattern}\`${p.flags ? ` (flags: ${p.flags})` : ''} - ${p.description}`).join('\n');
34
+ parts.push(`**Patterns:**\n${patternsText}`);
21
35
  }
22
36
  if (parts.length === 0) {
23
37
  return '';
@@ -30,13 +44,27 @@ function generateNarrativeSection(narrative) {
30
44
  }
31
45
  const parts = [];
32
46
  if (narrative.structure) {
33
- parts.push(`### Structure\n${narrative.structure}`);
47
+ parts.push(`### Structure\n${normalizeNewlines(narrative.structure)}`);
34
48
  }
35
49
  if (narrative.dependencies) {
36
- parts.push(`### Dependencies\n${narrative.dependencies}`);
50
+ parts.push(`### Dependencies\n${normalizeNewlines(narrative.dependencies)}`);
37
51
  }
38
52
  if (narrative.features) {
39
- parts.push(`### Features\n${narrative.features}`);
53
+ parts.push(`### Features\n${normalizeNewlines(narrative.features)}`);
54
+ }
55
+ if (narrative.rules) {
56
+ parts.push(`### Rules\n${narrative.rules}`);
57
+ }
58
+ if (narrative.examples) {
59
+ parts.push(`### Examples\n${narrative.examples}`);
60
+ }
61
+ if (narrative.diagrams && narrative.diagrams.length > 0) {
62
+ const diagramParts = narrative.diagrams.map(d => {
63
+ const lang = d.type === 'ascii' ? '' : d.type;
64
+ const titleLine = d.title ? `**${d.title}**\n` : '';
65
+ return `${titleLine}\`\`\`${lang}\n${d.content}\n\`\`\``;
66
+ });
67
+ parts.push(`### Diagrams\n${diagramParts.join('\n\n')}`);
40
68
  }
41
69
  if (parts.length === 0) {
42
70
  return '';
@@ -79,6 +107,31 @@ function parseRawConceptSection(content) {
79
107
  if (timestampMatch) {
80
108
  rawConcept.timestamp = timestampMatch[1].trim();
81
109
  }
110
+ // Author can be inline
111
+ const authorMatch = sectionContent.match(/\*\*\s*Author\s*:\s*\*\*\s*(.+)/i);
112
+ if (authorMatch) {
113
+ rawConcept.author = authorMatch[1].trim();
114
+ }
115
+ // Patterns is multi-line with list items
116
+ const patternsMatch = sectionContent.match(/\*\*\s*Patterns\s*:\s*\*\*\s*\n([\s\S]*?)(?=\n\*\*|\n##|$)/i);
117
+ if (patternsMatch) {
118
+ const patterns = [];
119
+ for (const line of patternsMatch[1]
120
+ .split('\n')
121
+ .filter(line => line.trim().startsWith('- `'))) {
122
+ const match = line.match(/- `(.+?)`(?:\s*\(flags:\s*(.+?)\))?\s*-\s*(.+)/);
123
+ if (match) {
124
+ patterns.push({
125
+ description: match[3].trim(),
126
+ pattern: match[1],
127
+ ...(match[2] ? { flags: match[2] } : {})
128
+ });
129
+ }
130
+ }
131
+ if (patterns.length > 0) {
132
+ rawConcept.patterns = patterns;
133
+ }
134
+ }
82
135
  if (Object.keys(rawConcept).length === 0) {
83
136
  return undefined;
84
137
  }
@@ -105,6 +158,30 @@ function parseNarrativeSection(content) {
105
158
  if (featuresMatch) {
106
159
  narrative.features = featuresMatch[1].trim();
107
160
  }
161
+ const rulesMatch = sectionContent.match(/###\s*Rules\s*\n([\s\S]*?)(?=\n###\s|\n##\s|$)/i);
162
+ if (rulesMatch) {
163
+ narrative.rules = rulesMatch[1].trim();
164
+ }
165
+ const examplesMatch = sectionContent.match(/###\s*Examples\s*\n([\s\S]*?)(?=\n###\s|\n##\s|$)/i);
166
+ if (examplesMatch) {
167
+ narrative.examples = examplesMatch[1].trim();
168
+ }
169
+ const diagramsMatch = sectionContent.match(/###\s*Diagrams\s*\n([\s\S]*?)(?=\n###\s|\n##\s|$)/i);
170
+ if (diagramsMatch) {
171
+ const diagrams = [];
172
+ const blockRegex = /(?:\*\*(.+?)\*\*\n)?```(\w*)\n([\s\S]*?)```/g;
173
+ let match;
174
+ while ((match = blockRegex.exec(diagramsMatch[1])) !== null) {
175
+ diagrams.push({
176
+ content: match[3].trimEnd(),
177
+ ...(match[1] ? { title: match[1] } : {}),
178
+ type: match[2] || 'ascii',
179
+ });
180
+ }
181
+ if (diagrams.length > 0) {
182
+ narrative.diagrams = diagrams;
183
+ }
184
+ }
108
185
  if (Object.keys(narrative).length === 0) {
109
186
  return undefined;
110
187
  }
@@ -159,6 +236,7 @@ function mergeRawConcepts(source, target) {
159
236
  merged.task = source.task || target.task;
160
237
  merged.flow = source.flow || target.flow;
161
238
  merged.timestamp = source.timestamp || target.timestamp;
239
+ merged.author = source.author || target.author;
162
240
  // Arrays: concatenate and deduplicate (target first, then source)
163
241
  const allChanges = [...(target.changes || []), ...(source.changes || [])];
164
242
  if (allChanges.length > 0) {
@@ -168,6 +246,18 @@ function mergeRawConcepts(source, target) {
168
246
  if (allFiles.length > 0) {
169
247
  merged.files = [...new Set(allFiles)];
170
248
  }
249
+ // Patterns: concatenate and deduplicate by pattern+flags
250
+ const allPatterns = [...(target.patterns || []), ...(source.patterns || [])];
251
+ if (allPatterns.length > 0) {
252
+ const seen = new Set();
253
+ merged.patterns = allPatterns.filter(p => {
254
+ const key = p.pattern + (p.flags || '');
255
+ if (seen.has(key))
256
+ return false;
257
+ seen.add(key);
258
+ return true;
259
+ });
260
+ }
171
261
  if (Object.keys(merged).length === 0) {
172
262
  return undefined;
173
263
  }
@@ -194,6 +284,24 @@ function mergeNarratives(source, target) {
194
284
  const parts = [target.features, source.features].filter(Boolean);
195
285
  merged.features = parts.join('\n\n');
196
286
  }
287
+ if (source.rules || target.rules) {
288
+ const parts = [target.rules, source.rules].filter(Boolean);
289
+ merged.rules = parts.join('\n\n');
290
+ }
291
+ if (source.examples || target.examples) {
292
+ const parts = [target.examples, source.examples].filter(Boolean);
293
+ merged.examples = parts.join('\n\n');
294
+ }
295
+ if (source.diagrams || target.diagrams) {
296
+ const allDiagrams = [...(target.diagrams || []), ...(source.diagrams || [])];
297
+ const seen = new Set();
298
+ merged.diagrams = allDiagrams.filter(d => {
299
+ if (seen.has(d.content))
300
+ return false;
301
+ seen.add(d.content);
302
+ return true;
303
+ });
304
+ }
197
305
  if (Object.keys(merged).length === 0) {
198
306
  return undefined;
199
307
  }
@@ -61,7 +61,7 @@ export class CurateExecutor {
61
61
  if (failedReads.length > 0 || skippedFiles.length > 0) {
62
62
  instructions.push('### Files that could not be read:', ...failedReads.map((r) => `- ${path.relative(projectRoot, r.filePath)}: ${r.error}`), ...skippedFiles.map((f) => `- ${f.path}: ${f.reason}`), '', '**Note:** You may use `read_file` or `grep_content` tools to find additional context if needed.', '');
63
63
  }
64
- instructions.push('**INSTRUCTIONS:**', '- The file contents above have been pre-loaded for you', '- Use this content to understand the context and create comprehensive knowledge topics', '- DO NOT use read_file tool for the files above - the content is already provided', '- Proceed with the normal workflow: detect domains, find existing knowledge, create/update topics', '');
64
+ instructions.push('**INSTRUCTIONS:**', '- The file contents above have been pre-loaded for you', '- Use this content to understand the context and create comprehensive knowledge topics', '- DO NOT use read_file tool for the files above - the content is already provided', '- Proceed with the normal workflow: detect domains, find existing knowledge, create/update topics', '- PRESERVE all diagrams (Mermaid, PlantUML, ASCII art) verbatim using narrative.diagrams array', '- PRESERVE all tables with every row - do not summarize table data', '- PRESERVE exact code examples, API signatures, and interface definitions', '- PRESERVE step-by-step procedures and numbered instructions in narrative.rules', '');
65
65
  return instructions.join('\n');
66
66
  }
67
67
  /**
@@ -0,0 +1,45 @@
1
+ /** Score at which the result is so strong that dominance check is skipped */
2
+ export declare const DIRECT_RESPONSE_HIGH_CONFIDENCE_THRESHOLD = 15;
3
+ /** Minimum score for the top result to qualify for a direct (no-LLM) response */
4
+ export declare const DIRECT_RESPONSE_SCORE_THRESHOLD = 8;
5
+ /** Top result must be N times the second result's score to be considered dominant */
6
+ export declare const DIRECT_RESPONSE_DOMINANCE_RATIO = 2;
7
+ /**
8
+ * A search result with full document content for direct response formatting.
9
+ */
10
+ export interface DirectSearchResult {
11
+ content: string;
12
+ path: string;
13
+ score: number;
14
+ title: string;
15
+ }
16
+ /**
17
+ * Determines if search results are confident enough for a direct response
18
+ * without involving the LLM.
19
+ *
20
+ * Requires:
21
+ * 1. Top result score >= DIRECT_RESPONSE_SCORE_THRESHOLD (minimum confidence)
22
+ * 2. Either: top score >= HIGH_CONFIDENCE_THRESHOLD (strong enough to skip dominance check)
23
+ * Or: top result dominates other results (score >= 2x the second result)
24
+ *
25
+ * @param results - Sorted search results (highest score first)
26
+ * @returns true if a direct response can be served
27
+ */
28
+ export declare function canRespondDirectly(results: DirectSearchResult[]): boolean;
29
+ /**
30
+ * Format a direct response from search results (no LLM involved).
31
+ * Uses a structured template matching the existing query response format.
32
+ *
33
+ * @param query - Original user query
34
+ * @param results - Search results with full content
35
+ * @returns Formatted response string
36
+ */
37
+ export declare function formatDirectResponse(query: string, results: DirectSearchResult[]): string;
38
+ /**
39
+ * Format a "not found" response when OOD detection determines
40
+ * the query topic is not covered in the knowledge base.
41
+ *
42
+ * @param query - Original user query
43
+ * @returns Formatted not-found response string
44
+ */
45
+ export declare function formatNotFoundResponse(query: string): string;
@@ -0,0 +1,86 @@
1
+ /** Score at which the result is so strong that dominance check is skipped */
2
+ export const DIRECT_RESPONSE_HIGH_CONFIDENCE_THRESHOLD = 15;
3
+ /** Minimum score for the top result to qualify for a direct (no-LLM) response */
4
+ export const DIRECT_RESPONSE_SCORE_THRESHOLD = 8;
5
+ /** Top result must be N times the second result's score to be considered dominant */
6
+ export const DIRECT_RESPONSE_DOMINANCE_RATIO = 2;
7
+ /** Maximum content length per document in the direct response */
8
+ const MAX_CONTENT_LENGTH = 1500;
9
+ /** Maximum number of documents to include in the direct response */
10
+ const MAX_DOCS = 3;
11
+ /**
12
+ * Determines if search results are confident enough for a direct response
13
+ * without involving the LLM.
14
+ *
15
+ * Requires:
16
+ * 1. Top result score >= DIRECT_RESPONSE_SCORE_THRESHOLD (minimum confidence)
17
+ * 2. Either: top score >= HIGH_CONFIDENCE_THRESHOLD (strong enough to skip dominance check)
18
+ * Or: top result dominates other results (score >= 2x the second result)
19
+ *
20
+ * @param results - Sorted search results (highest score first)
21
+ * @returns true if a direct response can be served
22
+ */
23
+ export function canRespondDirectly(results) {
24
+ if (results.length === 0)
25
+ return false;
26
+ const topResult = results[0];
27
+ if (topResult.score < DIRECT_RESPONSE_SCORE_THRESHOLD)
28
+ return false;
29
+ // Single result that passes threshold
30
+ if (results.length === 1)
31
+ return true;
32
+ // High-confidence path: score so strong that dominance is irrelevant
33
+ if (topResult.score >= DIRECT_RESPONSE_HIGH_CONFIDENCE_THRESHOLD)
34
+ return true;
35
+ const secondScore = results[1].score;
36
+ if (secondScore === 0)
37
+ return true;
38
+ return topResult.score / secondScore >= DIRECT_RESPONSE_DOMINANCE_RATIO;
39
+ }
40
+ /**
41
+ * Format a direct response from search results (no LLM involved).
42
+ * Uses a structured template matching the existing query response format.
43
+ *
44
+ * @param query - Original user query
45
+ * @param results - Search results with full content
46
+ * @returns Formatted response string
47
+ */
48
+ export function formatDirectResponse(query, results) {
49
+ const topResults = results.slice(0, MAX_DOCS);
50
+ const summary = topResults.length === 1
51
+ ? `Based on the curated knowledge, here is information about "${query}":`
52
+ : `Found ${topResults.length} relevant topics for "${query}":`;
53
+ const details = topResults
54
+ .map((r) => {
55
+ const truncatedContent = r.content.length > MAX_CONTENT_LENGTH ? `${r.content.slice(0, MAX_CONTENT_LENGTH).trim()}...` : r.content;
56
+ return `### ${r.title}\n\n${truncatedContent}`;
57
+ })
58
+ .join('\n\n---\n\n');
59
+ const sources = topResults.map((r) => `- \`.brv/context-tree/${r.path}\``).join('\n');
60
+ return `**Summary**: ${summary}
61
+
62
+ **Details**:
63
+
64
+ ${details}
65
+
66
+ **Sources**:
67
+ ${sources}
68
+
69
+ **Gaps**: This is a direct match from the context tree. For deeper analysis or cross-topic synthesis, try a more specific question.`;
70
+ }
71
+ /**
72
+ * Format a "not found" response when OOD detection determines
73
+ * the query topic is not covered in the knowledge base.
74
+ *
75
+ * @param query - Original user query
76
+ * @returns Formatted not-found response string
77
+ */
78
+ export function formatNotFoundResponse(query) {
79
+ return `**Summary**: No matching knowledge found for "${query}".
80
+
81
+ **Details**: The topic does not appear to be covered in the context tree. This could mean the topic hasn't been curated yet.
82
+
83
+ **Sources**: None
84
+
85
+ **Gaps**: Try rephrasing your query with different terms, or use /curate to add knowledge about this topic.`;
86
+ }
@@ -6,22 +6,30 @@ import type { FolderPackExecuteOptions, IFolderPackExecutor } from '../../core/i
6
6
  *
7
7
  * This executor:
8
8
  * 1. Packs the folder using FolderPackService
9
- * 2. Generates XML from the pack result
10
- * 3. Builds a prompt for the agent to analyze and curate the folder
11
- * 4. Executes with the agent
9
+ * 2. Stores packed data in sandbox environment as context variable
10
+ * 3. Guides agent to iteratively query and extract knowledge
11
+ * 4. Agent curates extracted pieces using tools.curate()
12
12
  *
13
13
  * Architecture:
14
14
  * - TaskProcessor injects the long-lived CipherAgent
15
15
  * - Event streaming is handled by agent-worker (subscribes to agentEventBus)
16
16
  * - Transport handles task lifecycle (task:started, task:completed, task:error)
17
17
  * - Executor focuses solely on folder pack + curate execution
18
+ * - Uses iterative extraction strategy (inspired by rlm) to avoid token limits
18
19
  */
19
20
  export declare class FolderPackExecutor implements IFolderPackExecutor {
20
21
  private readonly folderPackService;
21
22
  constructor(folderPackService: IFolderPackService);
22
23
  executeWithAgent(agent: ICipherAgent, options: FolderPackExecuteOptions): Promise<string>;
23
24
  /**
24
- * Build the analysis prompt for the agent.
25
+ * Build iterative extraction prompt with file-based access.
26
+ * Folder data is stored in a temporary file to avoid token limits.
25
27
  */
26
- private buildAnalysisPrompt;
28
+ private buildIterativePromptWithFileAccess;
29
+ /**
30
+ * Execute folder curation using iterative extraction strategy.
31
+ * Pre-loads folder data into REPL environment, then guides agent to iterate and curate.
32
+ * This avoids token limits entirely - data is stored in REPL, not in prompt.
33
+ */
34
+ private executeIterative;
27
35
  }