byterover-cli 1.2.1 → 1.4.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 (147) hide show
  1. package/README.md +76 -8
  2. package/dist/constants.d.ts +0 -5
  3. package/dist/constants.js +0 -5
  4. package/dist/core/domain/cipher/agent/agent-info.d.ts +17 -17
  5. package/dist/core/domain/cipher/errors/file-system-error.d.ts +11 -0
  6. package/dist/core/domain/cipher/errors/file-system-error.js +17 -0
  7. package/dist/core/domain/cipher/file-system/types.d.ts +40 -6
  8. package/dist/core/domain/cipher/llm/schemas.d.ts +14 -14
  9. package/dist/core/domain/cipher/session/session-metadata.d.ts +2 -2
  10. package/dist/core/domain/entities/agent.d.ts +1 -1
  11. package/dist/core/domain/entities/agent.js +11 -6
  12. package/dist/core/domain/entities/connector-type.d.ts +2 -1
  13. package/dist/core/domain/entities/connector-type.js +2 -1
  14. package/dist/core/domain/transport/schemas.d.ts +66 -66
  15. package/dist/core/interfaces/cipher/cipher-services.d.ts +0 -3
  16. package/dist/core/interfaces/cipher/index.d.ts +0 -2
  17. package/dist/core/interfaces/connectors/i-connector.d.ts +2 -2
  18. package/dist/core/interfaces/i-file-service.d.ts +7 -0
  19. package/dist/infra/auth/oauth-service.d.ts +15 -0
  20. package/dist/infra/auth/oauth-service.js +38 -2
  21. package/dist/infra/cipher/agent/agent-schemas.d.ts +42 -42
  22. package/dist/infra/cipher/file-system/binary-utils.d.ts +15 -2
  23. package/dist/infra/cipher/file-system/binary-utils.js +26 -3
  24. package/dist/infra/cipher/file-system/file-system-service.d.ts +9 -0
  25. package/dist/infra/cipher/file-system/file-system-service.js +91 -8
  26. package/dist/infra/cipher/file-system/pdf-extractor.d.ts +100 -0
  27. package/dist/infra/cipher/file-system/pdf-extractor.js +226 -0
  28. package/dist/infra/cipher/llm/context/context-manager.js +7 -9
  29. package/dist/infra/cipher/llm/internal-llm-service.d.ts +5 -1
  30. package/dist/infra/cipher/llm/internal-llm-service.js +57 -46
  31. package/dist/infra/cipher/system-prompt/contributor-schemas.d.ts +8 -8
  32. package/dist/infra/cipher/system-prompt/schemas.d.ts +5 -5
  33. package/dist/infra/cipher/tools/implementations/read-file-tool.js +24 -4
  34. package/dist/infra/connectors/connector-manager.js +2 -0
  35. package/dist/infra/connectors/hook/hook-connector.d.ts +1 -1
  36. package/dist/infra/connectors/hook/hook-connector.js +3 -3
  37. package/dist/infra/connectors/mcp/mcp-connector.d.ts +1 -1
  38. package/dist/infra/connectors/mcp/mcp-connector.js +4 -4
  39. package/dist/infra/connectors/rules/rules-connector-config.d.ts +4 -0
  40. package/dist/infra/connectors/rules/rules-connector-config.js +4 -0
  41. package/dist/infra/connectors/rules/rules-connector.d.ts +1 -1
  42. package/dist/infra/connectors/rules/rules-connector.js +4 -4
  43. package/dist/infra/connectors/shared/template-service.js +4 -0
  44. package/dist/infra/connectors/skill/index.d.ts +1 -0
  45. package/dist/infra/connectors/skill/index.js +1 -0
  46. package/dist/infra/connectors/skill/skill-connector-config.d.ts +45 -0
  47. package/dist/infra/connectors/skill/skill-connector-config.js +26 -0
  48. package/dist/infra/connectors/skill/skill-connector.d.ts +39 -0
  49. package/dist/infra/connectors/skill/skill-connector.js +160 -0
  50. package/dist/infra/connectors/skill/skill-content-loader.d.ts +18 -0
  51. package/dist/infra/connectors/skill/skill-content-loader.js +33 -0
  52. package/dist/infra/file/fs-file-service.d.ts +7 -0
  53. package/dist/infra/file/fs-file-service.js +15 -1
  54. package/dist/infra/mcp/tools/brv-curate-tool.d.ts +10 -4
  55. package/dist/infra/mcp/tools/brv-curate-tool.js +9 -4
  56. package/dist/infra/mcp/tools/task-result-waiter.js +8 -0
  57. package/dist/infra/process/agent-worker.js +30 -14
  58. package/dist/infra/process/task-queue-manager.d.ts +23 -34
  59. package/dist/infra/process/task-queue-manager.js +57 -118
  60. package/dist/infra/process/transport-handlers.js +1 -7
  61. package/dist/infra/repl/commands/connectors-command.js +1 -1
  62. package/dist/infra/repl/commands/space/switch-command.js +0 -2
  63. package/dist/infra/usecase/connectors-use-case.js +8 -2
  64. package/dist/infra/usecase/curate-use-case.js +10 -4
  65. package/dist/infra/usecase/init-use-case.js +1 -1
  66. package/dist/infra/usecase/reset-use-case.d.ts +1 -0
  67. package/dist/infra/usecase/reset-use-case.js +4 -1
  68. package/dist/infra/usecase/space-switch-use-case.d.ts +0 -10
  69. package/dist/infra/usecase/space-switch-use-case.js +7 -37
  70. package/dist/{commands → oclif/commands}/curate.d.ts +1 -1
  71. package/dist/{commands → oclif/commands}/curate.js +6 -6
  72. package/dist/{commands → oclif/commands}/hook-prompt-submit.d.ts +1 -1
  73. package/dist/{commands → oclif/commands}/hook-prompt-submit.js +3 -3
  74. package/dist/{commands → oclif/commands}/main.js +10 -10
  75. package/dist/{commands → oclif/commands}/mcp.js +2 -2
  76. package/dist/{commands → oclif/commands}/query.d.ts +1 -1
  77. package/dist/{commands → oclif/commands}/query.js +6 -6
  78. package/dist/{commands → oclif/commands}/status.d.ts +1 -1
  79. package/dist/{commands → oclif/commands}/status.js +8 -8
  80. package/dist/oclif/constants.d.ts +11 -0
  81. package/dist/oclif/constants.js +11 -0
  82. package/dist/{hooks → oclif/hooks}/init/welcome.js +4 -17
  83. package/dist/{hooks → oclif/hooks}/prerun/validate-brv-config-version.d.ts +1 -1
  84. package/dist/{hooks → oclif/hooks}/prerun/validate-brv-config-version.js +2 -2
  85. package/dist/resources/prompts/curate.yml +1 -0
  86. package/dist/resources/tools/read_file.txt +5 -2
  87. package/dist/templates/skill/SKILL.md +91 -0
  88. package/dist/templates/skill/TROUBLESHOOTING.md +50 -0
  89. package/dist/templates/skill/WORKFLOWS.md +229 -0
  90. package/dist/utils/file-validator.js +8 -4
  91. package/dist/utils/type-guards.d.ts +11 -0
  92. package/dist/utils/type-guards.js +13 -0
  93. package/oclif.manifest.json +7 -53
  94. package/package.json +12 -10
  95. package/dist/commands/watch.d.ts +0 -25
  96. package/dist/commands/watch.js +0 -175
  97. package/dist/core/interfaces/cipher/i-coding-agent-log-parser.d.ts +0 -20
  98. package/dist/core/interfaces/cipher/i-coding-agent-log-parser.js +0 -1
  99. package/dist/core/interfaces/cipher/i-coding-agent-log-watcher.d.ts +0 -31
  100. package/dist/core/interfaces/cipher/i-coding-agent-log-watcher.js +0 -1
  101. package/dist/core/interfaces/i-file-watcher-service.d.ts +0 -41
  102. package/dist/core/interfaces/i-file-watcher-service.js +0 -1
  103. package/dist/core/interfaces/parser/i-clean-parser-service.d.ts +0 -18
  104. package/dist/core/interfaces/parser/i-clean-parser-service.js +0 -1
  105. package/dist/core/interfaces/parser/i-raw-parser-service.d.ts +0 -17
  106. package/dist/core/interfaces/parser/i-raw-parser-service.js +0 -1
  107. package/dist/core/interfaces/parser/i-session-normalizer.d.ts +0 -56
  108. package/dist/core/interfaces/parser/i-session-normalizer.js +0 -1
  109. package/dist/infra/cipher/parsers/coding-agent-log-parser.d.ts +0 -24
  110. package/dist/infra/cipher/parsers/coding-agent-log-parser.js +0 -51
  111. package/dist/infra/cipher/watcher/coding-agent-log-watcher.d.ts +0 -14
  112. package/dist/infra/cipher/watcher/coding-agent-log-watcher.js +0 -55
  113. package/dist/infra/parsers/clean/clean-claude-service.d.ts +0 -111
  114. package/dist/infra/parsers/clean/clean-claude-service.js +0 -271
  115. package/dist/infra/parsers/clean/clean-codex-service.d.ts +0 -231
  116. package/dist/infra/parsers/clean/clean-codex-service.js +0 -534
  117. package/dist/infra/parsers/clean/clean-copilot-service.d.ts +0 -255
  118. package/dist/infra/parsers/clean/clean-copilot-service.js +0 -729
  119. package/dist/infra/parsers/clean/clean-cursor-service.d.ts +0 -161
  120. package/dist/infra/parsers/clean/clean-cursor-service.js +0 -432
  121. package/dist/infra/parsers/clean/clean-parser-service-factory.d.ts +0 -54
  122. package/dist/infra/parsers/clean/clean-parser-service-factory.js +0 -80
  123. package/dist/infra/parsers/clean/shared.d.ts +0 -84
  124. package/dist/infra/parsers/clean/shared.js +0 -273
  125. package/dist/infra/parsers/raw/raw-claude-service.d.ts +0 -195
  126. package/dist/infra/parsers/raw/raw-claude-service.js +0 -548
  127. package/dist/infra/parsers/raw/raw-codex-service.d.ts +0 -313
  128. package/dist/infra/parsers/raw/raw-codex-service.js +0 -782
  129. package/dist/infra/parsers/raw/raw-copilot-service.d.ts +0 -196
  130. package/dist/infra/parsers/raw/raw-copilot-service.js +0 -558
  131. package/dist/infra/parsers/raw/raw-cursor-service.d.ts +0 -316
  132. package/dist/infra/parsers/raw/raw-cursor-service.js +0 -818
  133. package/dist/infra/parsers/raw/raw-parser-service-factory.d.ts +0 -54
  134. package/dist/infra/parsers/raw/raw-parser-service-factory.js +0 -81
  135. package/dist/infra/process/constants.d.ts +0 -1
  136. package/dist/infra/process/constants.js +0 -1
  137. package/dist/infra/watcher/file-watcher-service.d.ts +0 -10
  138. package/dist/infra/watcher/file-watcher-service.js +0 -81
  139. /package/dist/{commands → oclif/commands}/main.d.ts +0 -0
  140. /package/dist/{commands → oclif/commands}/mcp.d.ts +0 -0
  141. /package/dist/{hooks → oclif/hooks}/command_not_found/handle-invalid-commands.d.ts +0 -0
  142. /package/dist/{hooks → oclif/hooks}/command_not_found/handle-invalid-commands.js +0 -0
  143. /package/dist/{hooks → oclif/hooks}/error/clean-errors.d.ts +0 -0
  144. /package/dist/{hooks → oclif/hooks}/error/clean-errors.js +0 -0
  145. /package/dist/{hooks → oclif/hooks}/init/update-notifier.d.ts +0 -0
  146. /package/dist/{hooks → oclif/hooks}/init/update-notifier.js +0 -0
  147. /package/dist/{hooks → oclif/hooks}/init/welcome.d.ts +0 -0
@@ -0,0 +1,226 @@
1
+ import { getDocumentProxy, getMeta } from 'unpdf';
2
+ import { PdfExtractionError } from '../../../core/domain/cipher/errors/file-system-error.js';
3
+ /**
4
+ * PDF magic bytes: %PDF-
5
+ */
6
+ const PDF_MAGIC_BYTES = [0x25, 0x50, 0x44, 0x46, 0x2d];
7
+ /**
8
+ * Default number of pages to extract when no limit specified.
9
+ */
10
+ const DEFAULT_PAGE_LIMIT = 100;
11
+ /**
12
+ * Maximum number of pages allowed per extraction.
13
+ */
14
+ const MAX_PAGE_LIMIT = 200;
15
+ /**
16
+ * PDF text extraction and metadata extraction utility.
17
+ * Provides page-by-page extraction with pagination support.
18
+ *
19
+ * Features:
20
+ * - Magic byte validation
21
+ * - Fast metadata-only extraction
22
+ * - Page-by-page text extraction with offset/limit
23
+ * - Default: 100 pages, max: 200 pages per extraction
24
+ */
25
+ export class PdfExtractor {
26
+ /**
27
+ * Extracts metadata from a PDF buffer without extracting text.
28
+ * This is a fast path when you only need page count, title, author, etc.
29
+ *
30
+ * @param buffer - PDF file buffer
31
+ * @param filePath - Path to the PDF file (for error messages)
32
+ * @returns PDF metadata
33
+ */
34
+ static async extractMetadata(buffer, filePath) {
35
+ try {
36
+ const pdf = await getDocumentProxy(new Uint8Array(buffer));
37
+ const meta = await getMeta(pdf);
38
+ return PdfExtractor.buildMetadataFromInfo(pdf.numPages, meta.info);
39
+ }
40
+ catch (error) {
41
+ throw PdfExtractor.wrapExtractionError(error, filePath);
42
+ }
43
+ }
44
+ /**
45
+ * Extracts text from a PDF buffer with pagination support.
46
+ *
47
+ * @param buffer - PDF file buffer
48
+ * @param filePath - Path to the PDF file (for error messages)
49
+ * @param options - Extraction options (offset, limit)
50
+ * @returns Extraction result with pages, metadata, and continuation info
51
+ */
52
+ static async extractText(buffer, filePath, options = {}) {
53
+ // Validate PDF magic bytes
54
+ if (!PdfExtractor.isValidPdf(buffer)) {
55
+ throw new PdfExtractionError(filePath, 'Invalid PDF file format (missing PDF header)');
56
+ }
57
+ try {
58
+ const pdf = await getDocumentProxy(new Uint8Array(buffer));
59
+ const meta = await getMeta(pdf);
60
+ const totalPages = pdf.numPages;
61
+ const metaInfo = meta.info;
62
+ // Calculate pagination
63
+ const offset = Math.max(1, options.offset ?? 1);
64
+ const limit = Math.min(options.limit ?? DEFAULT_PAGE_LIMIT, MAX_PAGE_LIMIT);
65
+ // Return empty result if offset is beyond total pages
66
+ if (offset > totalPages) {
67
+ return {
68
+ hasMore: false,
69
+ metadata: PdfExtractor.buildMetadataFromInfo(totalPages, metaInfo),
70
+ pages: [],
71
+ };
72
+ }
73
+ // Calculate page range
74
+ const endPage = Math.min(offset + limit - 1, totalPages);
75
+ // Extract text only from requested pages using PDF.js page-level API
76
+ const pages = await PdfExtractor.extractPagesFromDocument(pdf, offset, endPage);
77
+ return {
78
+ hasMore: endPage < totalPages,
79
+ metadata: PdfExtractor.buildMetadataFromInfo(totalPages, metaInfo),
80
+ pages,
81
+ };
82
+ }
83
+ catch (error) {
84
+ throw PdfExtractor.wrapExtractionError(error, filePath);
85
+ }
86
+ }
87
+ /**
88
+ * Checks if a buffer contains valid PDF magic bytes.
89
+ * @param buffer - Buffer to check
90
+ * @returns true if buffer starts with %PDF-
91
+ */
92
+ static isValidPdf(buffer) {
93
+ if (buffer.length < PDF_MAGIC_BYTES.length) {
94
+ return false;
95
+ }
96
+ for (const [index, byte] of PDF_MAGIC_BYTES.entries()) {
97
+ if (buffer[index] !== byte) {
98
+ return false;
99
+ }
100
+ }
101
+ return true;
102
+ }
103
+ /**
104
+ * Builds PdfMetadata from unpdf meta info object.
105
+ * @param pageCount - Total number of pages
106
+ * @param info - Optional info object from unpdf getMeta
107
+ * @returns PdfMetadata object
108
+ */
109
+ static buildMetadataFromInfo(pageCount, info) {
110
+ const metadata = { pageCount };
111
+ if (!info) {
112
+ return metadata;
113
+ }
114
+ if (typeof info.Title === 'string' && info.Title.trim()) {
115
+ metadata.title = info.Title.trim();
116
+ }
117
+ if (typeof info.Author === 'string' && info.Author.trim()) {
118
+ metadata.author = info.Author.trim();
119
+ }
120
+ if (info.CreationDate) {
121
+ const parsed = PdfExtractor.parsePdfDate(info.CreationDate);
122
+ if (parsed) {
123
+ metadata.creationDate = parsed;
124
+ }
125
+ }
126
+ return metadata;
127
+ }
128
+ /**
129
+ * Extracts text from specific pages of a PDF document.
130
+ * Uses PDF.js page-level API for efficient extraction of page ranges.
131
+ *
132
+ * @param pdf - PDF document proxy from unpdf
133
+ * @param startPage - Starting page number (1-based)
134
+ * @param endPage - Ending page number (1-based, inclusive)
135
+ * @returns Array of PdfPageContent with extracted text
136
+ */
137
+ static async extractPagesFromDocument(pdf, startPage, endPage) {
138
+ // Build array of page numbers to extract
139
+ const pageNumbers = [];
140
+ for (let pageNum = startPage; pageNum <= endPage; pageNum++) {
141
+ pageNumbers.push(pageNum);
142
+ }
143
+ // Extract all requested pages in parallel
144
+ const pages = await Promise.all(pageNumbers.map(async (pageNum) => {
145
+ const page = await pdf.getPage(pageNum);
146
+ const textContent = await page.getTextContent();
147
+ // Extract text from text items, handling EOL markers
148
+ // TextContent.items contains TextItem and TextMarkedContent - we only want TextItem (has 'str')
149
+ const text = textContent.items
150
+ .filter((item) => typeof item === 'object' && item !== null && 'str' in item)
151
+ .map((item) => item.str + (item.hasEOL ? '\n' : ''))
152
+ .join('');
153
+ return {
154
+ pageNumber: pageNum,
155
+ text: text.trim(),
156
+ };
157
+ }));
158
+ return pages;
159
+ }
160
+ /**
161
+ * Extracts a meaningful error message from an unknown error.
162
+ */
163
+ static getExtractionErrorMessage(error) {
164
+ if (error instanceof Error) {
165
+ return error.message;
166
+ }
167
+ if (typeof error === 'string') {
168
+ return error;
169
+ }
170
+ return 'Unknown PDF extraction error';
171
+ }
172
+ /**
173
+ * Parses PDF date string format (D:YYYYMMDDHHmmSS) to Date object.
174
+ * @param dateStr - PDF date string
175
+ * @returns Parsed Date or undefined if invalid
176
+ */
177
+ static parsePdfDate(dateStr) {
178
+ if (!dateStr) {
179
+ return undefined;
180
+ }
181
+ // PDF date format: D:YYYYMMDDHHmmSS+HH'mm' or variations
182
+ const match = dateStr.match(/D:(\d{4})(\d{2})?(\d{2})?(\d{2})?(\d{2})?(\d{2})?/);
183
+ if (!match) {
184
+ return undefined;
185
+ }
186
+ const year = Number.parseInt(match[1], 10);
187
+ const month = match[2] ? Number.parseInt(match[2], 10) - 1 : 0;
188
+ const day = match[3] ? Number.parseInt(match[3], 10) : 1;
189
+ const hour = match[4] ? Number.parseInt(match[4], 10) : 0;
190
+ const minute = match[5] ? Number.parseInt(match[5], 10) : 0;
191
+ const second = match[6] ? Number.parseInt(match[6], 10) : 0;
192
+ return new Date(year, month, day, hour, minute, second);
193
+ }
194
+ /**
195
+ * Wraps extraction errors with appropriate PdfExtractionError.
196
+ * @param error - The caught error
197
+ * @param filePath - Path to the PDF file
198
+ * @returns PdfExtractionError with appropriate message
199
+ */
200
+ static wrapExtractionError(error, filePath) {
201
+ const errorMessage = PdfExtractor.getExtractionErrorMessage(error);
202
+ const lowerMessage = errorMessage.toLowerCase();
203
+ if (lowerMessage.includes('password') || lowerMessage.includes('encrypted')) {
204
+ return new PdfExtractionError(filePath, 'PDF is password-protected or encrypted');
205
+ }
206
+ return new PdfExtractionError(filePath, errorMessage);
207
+ }
208
+ }
209
+ /**
210
+ * Formats extracted PDF pages into a readable string with page separators.
211
+ * @param pages - Array of extracted page contents
212
+ * @param metadata - PDF metadata
213
+ * @param hasMore - Whether there are more pages
214
+ * @param nextOffset - Next offset for continuation (if hasMore is true)
215
+ * @returns Formatted string with page separators
216
+ */
217
+ export function formatPdfContent(pages, metadata, hasMore, nextOffset) {
218
+ if (pages.length === 0) {
219
+ return '';
220
+ }
221
+ const formattedPages = pages.map((page) => `--- Page ${page.pageNumber} ---\n${page.text}`).join('\n\n');
222
+ const truncationNote = hasMore
223
+ ? `\n\n(PDF has more pages. Use offset=${nextOffset} to continue)`
224
+ : `\n\n(End of PDF - ${metadata.pageCount} pages)`;
225
+ return formattedPages + truncationNote;
226
+ }
@@ -200,15 +200,13 @@ export class ContextManager {
200
200
  content,
201
201
  role: 'user',
202
202
  };
203
- await this.mutex.withLock(async () => {
204
- this.messages.push(message);
205
- try {
206
- await this.persistHistory();
207
- }
208
- catch (error) {
209
- this.logger.error('Failed to persist history after user message', { error, sessionId: this.sessionId });
210
- }
211
- });
203
+ this.messages.push(message);
204
+ try {
205
+ await this.persistHistory();
206
+ }
207
+ catch (error) {
208
+ this.logger.error('Failed to persist history after user message', { error, sessionId: this.sessionId });
209
+ }
212
210
  }
213
211
  /**
214
212
  * Clear all messages from the conversation history.
@@ -72,6 +72,7 @@ export declare class ByteRoverLLMService implements ILLMService {
72
72
  private readonly loopDetector;
73
73
  private readonly memoryManager?;
74
74
  private readonly metadataHandler;
75
+ private readonly mutex;
75
76
  private readonly outputProcessor;
76
77
  private readonly providerType;
77
78
  private readonly sessionEventBus;
@@ -257,10 +258,13 @@ export declare class ByteRoverLLMService implements ILLMService {
257
258
  * Execute a single iteration of the agentic loop.
258
259
  *
259
260
  * @param options - Iteration options
261
+ * @param options.executionContext - Optional execution context
262
+ * @param options.fileData - Optional file data (only used on first iteration)
263
+ * @param options.imageData - Optional image data (only used on first iteration)
260
264
  * @param options.iterationCount - Current iteration number
261
265
  * @param options.taskId - Task ID from usecase for billing tracking
266
+ * @param options.textInput - User input text (only used on first iteration)
262
267
  * @param options.tools - Available tools for this iteration
263
- * @param options.executionContext - Optional execution context
264
268
  * @returns Final response string if complete, null if more iterations needed
265
269
  */
266
270
  private executeAgenticIteration;
@@ -6,6 +6,7 @@ import { NoOpLogger } from '../../../core/interfaces/cipher/i-logger.js';
6
6
  import { getErrorMessage } from '../../../utils/error-helpers.js';
7
7
  import { EnvironmentContextBuilder } from '../system-prompt/environment-context-builder.js';
8
8
  import { ToolMetadataHandler } from '../tools/streaming/metadata-handler.js';
9
+ import { AsyncMutex } from './context/async-mutex.js';
9
10
  import { ContextManager } from './context/context-manager.js';
10
11
  import { LoopDetector } from './context/loop-detector.js';
11
12
  import { ClaudeMessageFormatter } from './formatters/claude-formatter.js';
@@ -44,6 +45,7 @@ export class ByteRoverLLMService {
44
45
  loopDetector;
45
46
  memoryManager;
46
47
  metadataHandler;
48
+ mutex = new AsyncMutex();
47
49
  outputProcessor;
48
50
  providerType;
49
51
  sessionEventBus;
@@ -147,8 +149,6 @@ export class ByteRoverLLMService {
147
149
  async completeTask(textInput, options) {
148
150
  // Extract options with defaults
149
151
  const { executionContext, fileData, imageData, signal, taskId } = options ?? {};
150
- // Add user message to context
151
- await this.contextManager.addUserMessage(textInput, imageData, fileData);
152
152
  // Get filtered tools based on command type (e.g., only read-only tools for 'query')
153
153
  const toolSet = this.toolManager.getToolsForCommand(options?.executionContext?.commandType);
154
154
  // Create state machine with configured limits
@@ -171,8 +171,11 @@ export class ByteRoverLLMService {
171
171
  // eslint-disable-next-line no-await-in-loop -- Sequential iterations required for agentic loop
172
172
  const result = await this.executeAgenticIteration({
173
173
  executionContext,
174
+ fileData,
175
+ imageData,
174
176
  iterationCount: stateMachine.getContext().turnCount,
175
177
  taskId,
178
+ textInput,
176
179
  tools: toolSet,
177
180
  });
178
181
  if (result !== null) {
@@ -477,14 +480,17 @@ export class ByteRoverLLMService {
477
480
  * Execute a single iteration of the agentic loop.
478
481
  *
479
482
  * @param options - Iteration options
483
+ * @param options.executionContext - Optional execution context
484
+ * @param options.fileData - Optional file data (only used on first iteration)
485
+ * @param options.imageData - Optional image data (only used on first iteration)
480
486
  * @param options.iterationCount - Current iteration number
481
487
  * @param options.taskId - Task ID from usecase for billing tracking
488
+ * @param options.textInput - User input text (only used on first iteration)
482
489
  * @param options.tools - Available tools for this iteration
483
- * @param options.executionContext - Optional execution context
484
490
  * @returns Final response string if complete, null if more iterations needed
485
491
  */
486
492
  async executeAgenticIteration(options) {
487
- const { executionContext, iterationCount, taskId, tools } = options;
493
+ const { executionContext, fileData, imageData, iterationCount, taskId, textInput, tools } = options;
488
494
  // Build system prompt using SystemPromptManager (before compression for correct token accounting)
489
495
  // Use filtered tool names based on command type (e.g., only read-only tools for 'query')
490
496
  const availableTools = this.toolManager.getToolNamesForCommand(executionContext?.commandType);
@@ -533,54 +539,59 @@ export class ByteRoverLLMService {
533
539
  reflectionType,
534
540
  });
535
541
  }
536
- // Get token count for logging (using system prompt for token accounting)
537
- const systemPromptTokens = this.generator.estimateTokensSync(systemPrompt);
538
- const messages = this.contextManager.getMessages();
539
- const messageTokenCounts = messages.map((msg) => this.generator.estimateTokensSync(typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content)));
540
- const maxMessageTokens = this.config.maxInputTokens - systemPromptTokens;
541
- // Target utilization to leave headroom for response
542
- const targetMessageTokens = Math.floor(maxMessageTokens * TARGET_MESSAGE_TOKEN_UTILIZATION);
543
- this.contextManager.compressMessage(targetMessageTokens, messageTokenCounts);
544
- // Calculate tokens after compression
545
- const compressedMessagesTokens = this.contextManager
546
- .getMessages()
547
- .reduce((total, msg) => total +
548
- this.generator.estimateTokensSync(typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content)), 0);
549
- const tokensUsed = systemPromptTokens + compressedMessagesTokens;
550
- // Verbose: Log messages that will be sent to LLM
551
- if (this.config.verbose) {
552
- console.log('\n========== MESSAGES (Sent to LLM) ==========');
553
- console.log(JSON.stringify(this.contextManager.getMessages(), null, 2));
554
- console.log('========== END MESSAGES ==========\n');
555
- // Log token usage for monitoring compression behavior
556
- console.log(`[ByteRoverLLMService] [Iter ${iterationCount + 1}/${this.config.maxIterations}] Sending to LLM: ${tokensUsed} tokens (max: ${this.config.maxInputTokens})`);
557
- }
558
542
  // Final iteration optimization for query: strip tools (reflection already added above)
559
543
  let toolsForThisIteration = tools;
560
544
  if (executionContext?.commandType === 'query' && iterationCount === this.config.maxIterations - 1) {
561
545
  toolsForThisIteration = {}; // Empty toolset forces text response
562
546
  }
563
- // Build generation request
564
- const request = this.buildGenerateContentRequest({
565
- executionContext,
566
- systemPrompt,
567
- taskId,
568
- tools: toolsForThisIteration,
569
- });
570
- // Call LLM via generator (retry + logging handled by decorators)
571
- const lastMessage = await this.callLLMAndParseResponse(request);
572
- // Check if there are tool calls
573
- if (!lastMessage.toolCalls || lastMessage.toolCalls.length === 0) {
574
- const response = await this.handleFinalResponse(lastMessage, taskId);
575
- // Auto-compaction check after assistant response
547
+ // Get token count for logging (using system prompt for token accounting)
548
+ const systemPromptTokens = this.generator.estimateTokensSync(systemPrompt);
549
+ // Add user message and compress context within mutex lock
550
+ return this.mutex.withLock(async () => {
551
+ // Add user message to context only on the first iteration
552
+ await this.contextManager.addUserMessage(textInput, imageData, fileData);
553
+ const messages = this.contextManager.getMessages();
554
+ const messageTokenCounts = messages.map((msg) => this.generator.estimateTokensSync(typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content)));
555
+ const maxMessageTokens = this.config.maxInputTokens - systemPromptTokens;
556
+ // Target utilization to leave headroom for response
557
+ const targetMessageTokens = Math.floor(maxMessageTokens * TARGET_MESSAGE_TOKEN_UTILIZATION);
558
+ this.contextManager.compressMessage(targetMessageTokens, messageTokenCounts);
559
+ // Calculate tokens after compression
560
+ const compressedMessagesTokens = this.contextManager
561
+ .getMessages()
562
+ .reduce((total, msg) => total +
563
+ this.generator.estimateTokensSync(typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content)), 0);
564
+ const tokensUsed = systemPromptTokens + compressedMessagesTokens;
565
+ // Verbose: Log messages that will be sent to LLM
566
+ if (this.config.verbose) {
567
+ console.log('\n========== MESSAGES (Sent to LLM) ==========');
568
+ console.log(JSON.stringify(this.contextManager.getMessages(), null, 2));
569
+ console.log('========== END MESSAGES ==========\n');
570
+ // Log token usage for monitoring compression behavior
571
+ console.log(`[ByteRoverLLMService] [Iter ${iterationCount + 1}/${this.config.maxIterations}] Sending to LLM: ${tokensUsed} tokens (max: ${this.config.maxInputTokens})`);
572
+ }
573
+ // Build generation request
574
+ const request = this.buildGenerateContentRequest({
575
+ executionContext,
576
+ systemPrompt,
577
+ taskId,
578
+ tools: toolsForThisIteration,
579
+ });
580
+ // Call LLM via generator (retry + logging handled by decorators)
581
+ const lastMessage = await this.callLLMAndParseResponse(request);
582
+ // Check if there are tool calls
583
+ if (!lastMessage.toolCalls || lastMessage.toolCalls.length === 0) {
584
+ const response = await this.handleFinalResponse(lastMessage, taskId);
585
+ // Auto-compaction check after assistant response
586
+ await this.checkAndTriggerCompaction(taskId ?? '');
587
+ return response;
588
+ }
589
+ // Has tool calls - handle them (pass taskId for subagent billing)
590
+ await this.handleToolCalls(lastMessage, taskId);
591
+ // Auto-compaction check after tool execution batch
576
592
  await this.checkAndTriggerCompaction(taskId ?? '');
577
- return response;
578
- }
579
- // Has tool calls - handle them (pass taskId for subagent billing)
580
- await this.handleToolCalls(lastMessage, taskId);
581
- // Auto-compaction check after tool execution batch
582
- await this.checkAndTriggerCompaction(taskId ?? '');
583
- return null;
593
+ return null;
594
+ });
584
595
  }
585
596
  /**
586
597
  * Execute a single tool call in parallel (without adding to context).
@@ -12,14 +12,14 @@ export declare const StaticContributorConfigSchema: z.ZodObject<{
12
12
  type: z.ZodLiteral<"static">;
13
13
  }, "strict", z.ZodTypeAny, {
14
14
  type: "static";
15
- content: string;
16
15
  id: string;
16
+ content: string;
17
17
  enabled: boolean;
18
18
  priority: number;
19
19
  }, {
20
20
  type: "static";
21
- content: string;
22
21
  id: string;
22
+ content: string;
23
23
  priority: number;
24
24
  enabled?: boolean | undefined;
25
25
  }>;
@@ -179,14 +179,14 @@ export declare const ContributorConfigSchema: z.ZodDiscriminatedUnion<"type", [z
179
179
  type: z.ZodLiteral<"static">;
180
180
  }, "strict", z.ZodTypeAny, {
181
181
  type: "static";
182
- content: string;
183
182
  id: string;
183
+ content: string;
184
184
  enabled: boolean;
185
185
  priority: number;
186
186
  }, {
187
187
  type: "static";
188
- content: string;
189
188
  id: string;
189
+ content: string;
190
190
  priority: number;
191
191
  enabled?: boolean | undefined;
192
192
  }>, z.ZodObject<{
@@ -311,14 +311,14 @@ export declare const SystemPromptManagerConfigSchema: z.ZodObject<{
311
311
  type: z.ZodLiteral<"static">;
312
312
  }, "strict", z.ZodTypeAny, {
313
313
  type: "static";
314
- content: string;
315
314
  id: string;
315
+ content: string;
316
316
  enabled: boolean;
317
317
  priority: number;
318
318
  }, {
319
319
  type: "static";
320
- content: string;
321
320
  id: string;
321
+ content: string;
322
322
  priority: number;
323
323
  enabled?: boolean | undefined;
324
324
  }>, z.ZodObject<{
@@ -433,8 +433,8 @@ export declare const SystemPromptManagerConfigSchema: z.ZodObject<{
433
433
  }, "strict", z.ZodTypeAny, {
434
434
  contributors: ({
435
435
  type: "static";
436
- content: string;
437
436
  id: string;
437
+ content: string;
438
438
  enabled: boolean;
439
439
  priority: number;
440
440
  } | {
@@ -471,8 +471,8 @@ export declare const SystemPromptManagerConfigSchema: z.ZodObject<{
471
471
  }, {
472
472
  contributors: ({
473
473
  type: "static";
474
- content: string;
475
474
  id: string;
475
+ content: string;
476
476
  priority: number;
477
477
  enabled?: boolean | undefined;
478
478
  } | {
@@ -12,28 +12,28 @@ export declare const PromptConfigSchema: z.ZodEffects<z.ZodEffects<z.ZodObject<{
12
12
  prompt: z.ZodOptional<z.ZodString>;
13
13
  prompts: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
14
14
  }, "strict", z.ZodTypeAny, {
15
- prompt?: string | undefined;
16
15
  description?: string | undefined;
16
+ prompt?: string | undefined;
17
17
  excludedTools?: string[] | undefined;
18
18
  prompts?: Record<string, string> | undefined;
19
19
  }, {
20
- prompt?: string | undefined;
21
20
  description?: string | undefined;
21
+ prompt?: string | undefined;
22
22
  excludedTools?: string[] | undefined;
23
23
  prompts?: Record<string, string> | undefined;
24
24
  }>, {
25
- prompt?: string | undefined;
26
25
  description?: string | undefined;
26
+ prompt?: string | undefined;
27
27
  excludedTools?: string[] | undefined;
28
28
  prompts?: Record<string, string> | undefined;
29
29
  }, {
30
- prompt?: string | undefined;
31
30
  description?: string | undefined;
31
+ prompt?: string | undefined;
32
32
  excludedTools?: string[] | undefined;
33
33
  prompts?: Record<string, string> | undefined;
34
34
  }>, {
35
- prompt?: string | undefined;
36
35
  description?: string | undefined;
36
+ prompt?: string | undefined;
37
37
  excludedTools?: string[] | undefined;
38
38
  prompts?: Record<string, string> | undefined;
39
39
  }, unknown>;
@@ -7,8 +7,23 @@ import { isImageFile } from '../../file-system/binary-utils.js';
7
7
  const ReadFileInputSchema = z
8
8
  .object({
9
9
  filePath: z.string().describe('Path to the file to read (absolute or relative to working directory)'),
10
- limit: z.number().int().positive().optional().describe('Maximum number of lines to read (optional, default: 2000)'),
11
- offset: z.number().int().min(1).optional().describe('Starting line number (1-based, optional)'),
10
+ limit: z
11
+ .number()
12
+ .int()
13
+ .positive()
14
+ .optional()
15
+ .describe('Maximum number of lines to read for text files (default: 2000), or pages for PDFs in text mode (default: 100, max: 200)'),
16
+ offset: z
17
+ .number()
18
+ .int()
19
+ .min(1)
20
+ .optional()
21
+ .describe('Starting line number (1-based) for text files, or starting page number for PDFs. ' +
22
+ 'If the file is truncated, you MUST set offset to the next line/page number to continue reading.'),
23
+ pdfMode: z
24
+ .enum(['text', 'base64'])
25
+ .optional()
26
+ .describe("PDF read mode: 'text' (default) extracts text page by page with pagination support, 'base64' returns raw PDF as attachment for multimodal analysis"),
12
27
  })
13
28
  .strict();
14
29
  /**
@@ -29,14 +44,18 @@ const ReadFileInputSchema = z
29
44
  */
30
45
  export function createReadFileTool(fileSystemService) {
31
46
  return {
32
- description: 'Read the contents of a file. Supports relative/absolute paths, pagination, and returns images/PDFs as base64 attachments.',
47
+ description: 'Read the contents of a file. Supports relative/absolute paths and pagination. ' +
48
+ 'For PDFs, defaults to text extraction with page-by-page pagination (use pdfMode="base64" for raw attachment). ' +
49
+ 'Images are returned as base64 attachments. ' +
50
+ 'Must continue running read_file tool with the correct offset to finish reading the file.',
33
51
  async execute(input, _context) {
34
- const { filePath, limit, offset } = input;
52
+ const { filePath, limit, offset, pdfMode } = input;
35
53
  try {
36
54
  // Call file system service
37
55
  const result = await fileSystemService.readFile(filePath, {
38
56
  limit,
39
57
  offset,
58
+ pdfMode,
40
59
  });
41
60
  // Transform attachment format (singular → plural array)
42
61
  let attachments;
@@ -55,6 +74,7 @@ export function createReadFileTool(fileSystemService) {
55
74
  content: result.formattedContent,
56
75
  lines: result.lines,
57
76
  message: result.message,
77
+ pdfMetadata: result.pdfMetadata,
58
78
  preview: result.preview,
59
79
  size: result.size,
60
80
  success: true,
@@ -3,6 +3,7 @@ import { CONNECTOR_TYPES } from '../../core/domain/entities/connector-type.js';
3
3
  import { HookConnector } from './hook/hook-connector.js';
4
4
  import { McpConnector } from './mcp/mcp-connector.js';
5
5
  import { RulesConnector } from './rules/rules-connector.js';
6
+ import { SkillConnector } from './skill/skill-connector.js';
6
7
  /**
7
8
  * Factory and orchestration layer for connectors.
8
9
  * Creates connector instances and manages connector operations.
@@ -16,6 +17,7 @@ export class ConnectorManager {
16
17
  ['hook', new HookConnector({ fileService, projectRoot })],
17
18
  ['mcp', new McpConnector({ fileService, projectRoot, templateService })],
18
19
  ['rules', new RulesConnector({ fileService, projectRoot, templateService })],
20
+ ['skill', new SkillConnector({ fileService, projectRoot })],
19
21
  ]);
20
22
  }
21
23
  async getAllInstalledConnectors() {
@@ -21,7 +21,7 @@ type HookConnectorOptions = {
21
21
  * - Safe uninstall: Only removes ByteRover hooks by command match
22
22
  */
23
23
  export declare class HookConnector implements IConnector {
24
- readonly type: ConnectorType;
24
+ readonly connectorType: ConnectorType;
25
25
  private readonly fileService;
26
26
  private readonly projectRoot;
27
27
  private readonly supportedAgents;
@@ -23,7 +23,7 @@ function parseJsonAsRecord(content) {
23
23
  * - Safe uninstall: Only removes ByteRover hooks by command match
24
24
  */
25
25
  export class HookConnector {
26
- type = 'hook';
26
+ connectorType = 'hook';
27
27
  fileService;
28
28
  projectRoot;
29
29
  supportedAgents;
@@ -31,7 +31,7 @@ export class HookConnector {
31
31
  this.fileService = options.fileService;
32
32
  this.projectRoot = options.projectRoot;
33
33
  this.supportedAgents = Object.entries(AGENT_CONNECTOR_CONFIG)
34
- .filter(([_, config]) => config.supported.includes(this.type))
34
+ .filter(([_, config]) => config.supported.includes(this.connectorType))
35
35
  .map(([agent]) => agent);
36
36
  }
37
37
  getConfigPath(agent) {
@@ -104,7 +104,7 @@ export class HookConnector {
104
104
  }
105
105
  }
106
106
  isSupported(agent) {
107
- return AGENT_CONNECTOR_CONFIG[agent].supported.includes(this.type);
107
+ return AGENT_CONNECTOR_CONFIG[agent].supported.includes(this.connectorType);
108
108
  }
109
109
  async status(agent) {
110
110
  if (!this.isSupported(agent)) {
@@ -24,7 +24,7 @@ type McpConnectorOptions = {
24
24
  * - Safe uninstall: Only removes ByteRover's MCP server entry and rule content
25
25
  */
26
26
  export declare class McpConnector implements IConnector {
27
- readonly type: ConnectorType;
27
+ readonly connectorType: ConnectorType;
28
28
  private readonly fileService;
29
29
  private readonly projectRoot;
30
30
  private readonly ruleFileManager;