byterover-cli 1.3.0 → 1.5.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 (171) hide show
  1. package/README.md +71 -6
  2. package/dist/core/domain/cipher/errors/file-system-error.d.ts +11 -0
  3. package/dist/core/domain/cipher/errors/file-system-error.js +17 -0
  4. package/dist/core/domain/cipher/file-system/types.d.ts +40 -6
  5. package/dist/core/domain/cipher/process/types.d.ts +1 -1
  6. package/dist/core/domain/entities/agent.d.ts +1 -1
  7. package/dist/core/domain/entities/agent.js +5 -0
  8. package/dist/core/domain/entities/provider-config.d.ts +92 -0
  9. package/dist/core/domain/entities/provider-config.js +181 -0
  10. package/dist/core/domain/entities/provider-registry.d.ts +55 -0
  11. package/dist/core/domain/entities/provider-registry.js +74 -0
  12. package/dist/core/interfaces/cipher/cipher-services.d.ts +0 -3
  13. package/dist/core/interfaces/cipher/i-content-generator.d.ts +30 -0
  14. package/dist/core/interfaces/cipher/i-content-generator.js +12 -1
  15. package/dist/core/interfaces/cipher/index.d.ts +0 -2
  16. package/dist/core/interfaces/cipher/message-factory.d.ts +4 -1
  17. package/dist/core/interfaces/cipher/message-factory.js +5 -0
  18. package/dist/core/interfaces/cipher/message-types.d.ts +19 -1
  19. package/dist/core/interfaces/i-provider-config-store.d.ts +88 -0
  20. package/dist/core/interfaces/i-provider-keychain-store.d.ts +33 -0
  21. package/dist/infra/cipher/file-system/binary-utils.d.ts +15 -2
  22. package/dist/infra/cipher/file-system/binary-utils.js +26 -3
  23. package/dist/infra/cipher/file-system/file-system-service.d.ts +9 -0
  24. package/dist/infra/cipher/file-system/file-system-service.js +96 -13
  25. package/dist/infra/cipher/file-system/pdf-extractor.d.ts +100 -0
  26. package/dist/infra/cipher/file-system/pdf-extractor.js +226 -0
  27. package/dist/infra/cipher/http/internal-llm-http-service.d.ts +40 -0
  28. package/dist/infra/cipher/http/internal-llm-http-service.js +152 -2
  29. package/dist/infra/cipher/llm/formatters/gemini-formatter.js +8 -1
  30. package/dist/infra/cipher/llm/generators/byterover-content-generator.d.ts +2 -3
  31. package/dist/infra/cipher/llm/generators/byterover-content-generator.js +20 -11
  32. package/dist/infra/cipher/llm/generators/openrouter-content-generator.d.ts +1 -0
  33. package/dist/infra/cipher/llm/generators/openrouter-content-generator.js +26 -0
  34. package/dist/infra/cipher/llm/internal-llm-service.d.ts +13 -0
  35. package/dist/infra/cipher/llm/internal-llm-service.js +75 -4
  36. package/dist/infra/cipher/llm/model-capabilities.d.ts +74 -0
  37. package/dist/infra/cipher/llm/model-capabilities.js +157 -0
  38. package/dist/infra/cipher/llm/openrouter-llm-service.d.ts +35 -1
  39. package/dist/infra/cipher/llm/openrouter-llm-service.js +216 -28
  40. package/dist/infra/cipher/llm/stream-processor.d.ts +22 -2
  41. package/dist/infra/cipher/llm/stream-processor.js +78 -4
  42. package/dist/infra/cipher/llm/thought-parser.d.ts +1 -1
  43. package/dist/infra/cipher/llm/thought-parser.js +5 -5
  44. package/dist/infra/cipher/llm/transformers/openrouter-stream-transformer.d.ts +49 -0
  45. package/dist/infra/cipher/llm/transformers/openrouter-stream-transformer.js +272 -0
  46. package/dist/infra/cipher/llm/transformers/reasoning-extractor.d.ts +71 -0
  47. package/dist/infra/cipher/llm/transformers/reasoning-extractor.js +253 -0
  48. package/dist/infra/cipher/process/process-service.js +1 -1
  49. package/dist/infra/cipher/session/chat-session.d.ts +2 -0
  50. package/dist/infra/cipher/session/chat-session.js +13 -2
  51. package/dist/infra/cipher/storage/message-storage-service.js +4 -0
  52. package/dist/infra/cipher/tools/implementations/bash-exec-tool.js +3 -3
  53. package/dist/infra/cipher/tools/implementations/read-file-tool.js +24 -4
  54. package/dist/infra/cipher/tools/implementations/task-tool.js +1 -1
  55. package/dist/infra/connectors/rules/rules-connector-config.d.ts +4 -0
  56. package/dist/infra/connectors/rules/rules-connector-config.js +4 -0
  57. package/dist/infra/http/openrouter-api-client.d.ts +148 -0
  58. package/dist/infra/http/openrouter-api-client.js +161 -0
  59. package/dist/infra/mcp/tools/brv-curate-tool.d.ts +10 -4
  60. package/dist/infra/mcp/tools/brv-curate-tool.js +9 -4
  61. package/dist/infra/mcp/tools/task-result-waiter.js +9 -1
  62. package/dist/infra/process/agent-worker.js +178 -70
  63. package/dist/infra/process/transport-handlers.d.ts +25 -4
  64. package/dist/infra/process/transport-handlers.js +57 -10
  65. package/dist/infra/repl/commands/connectors-command.js +2 -2
  66. package/dist/infra/repl/commands/index.js +5 -0
  67. package/dist/infra/repl/commands/model-command.d.ts +13 -0
  68. package/dist/infra/repl/commands/model-command.js +212 -0
  69. package/dist/infra/repl/commands/provider-command.d.ts +13 -0
  70. package/dist/infra/repl/commands/provider-command.js +181 -0
  71. package/dist/infra/repl/commands/space/switch-command.js +0 -2
  72. package/dist/infra/repl/transport-client-helper.js +6 -2
  73. package/dist/infra/storage/file-provider-config-store.d.ts +83 -0
  74. package/dist/infra/storage/file-provider-config-store.js +157 -0
  75. package/dist/infra/storage/provider-keychain-store.d.ts +37 -0
  76. package/dist/infra/storage/provider-keychain-store.js +75 -0
  77. package/dist/infra/transport/socket-io-transport-client.d.ts +20 -0
  78. package/dist/infra/transport/socket-io-transport-client.js +88 -1
  79. package/dist/infra/usecase/curate-use-case.js +10 -4
  80. package/dist/infra/usecase/space-switch-use-case.d.ts +0 -10
  81. package/dist/infra/usecase/space-switch-use-case.js +7 -37
  82. package/dist/oclif/hooks/init/welcome.js +4 -17
  83. package/dist/resources/prompts/curate.yml +1 -0
  84. package/dist/resources/tools/bash_exec.txt +1 -1
  85. package/dist/resources/tools/read_file.txt +5 -2
  86. package/dist/tui/components/api-key-dialog.d.ts +39 -0
  87. package/dist/tui/components/api-key-dialog.js +94 -0
  88. package/dist/tui/components/execution/execution-changes.d.ts +3 -1
  89. package/dist/tui/components/execution/execution-changes.js +4 -4
  90. package/dist/tui/components/execution/execution-content.d.ts +1 -1
  91. package/dist/tui/components/execution/execution-content.js +4 -12
  92. package/dist/tui/components/execution/execution-input.js +1 -1
  93. package/dist/tui/components/execution/execution-progress.d.ts +10 -13
  94. package/dist/tui/components/execution/execution-progress.js +70 -17
  95. package/dist/tui/components/execution/execution-reasoning.d.ts +16 -0
  96. package/dist/tui/components/execution/execution-reasoning.js +34 -0
  97. package/dist/tui/components/execution/execution-tool.d.ts +23 -0
  98. package/dist/tui/components/execution/execution-tool.js +125 -0
  99. package/dist/tui/components/execution/expanded-log-view.js +3 -3
  100. package/dist/tui/components/execution/log-item.d.ts +2 -0
  101. package/dist/tui/components/execution/log-item.js +6 -4
  102. package/dist/tui/components/index.d.ts +2 -0
  103. package/dist/tui/components/index.js +2 -0
  104. package/dist/tui/components/inline-prompts/inline-select.js +3 -2
  105. package/dist/tui/components/model-dialog.d.ts +63 -0
  106. package/dist/tui/components/model-dialog.js +89 -0
  107. package/dist/tui/components/onboarding/onboarding-flow.js +8 -2
  108. package/dist/tui/components/provider-dialog.d.ts +27 -0
  109. package/dist/tui/components/provider-dialog.js +31 -0
  110. package/dist/tui/components/reasoning-text.d.ts +26 -0
  111. package/dist/tui/components/reasoning-text.js +49 -0
  112. package/dist/tui/components/selectable-list.d.ts +54 -0
  113. package/dist/tui/components/selectable-list.js +180 -0
  114. package/dist/tui/components/streaming-text.d.ts +30 -0
  115. package/dist/tui/components/streaming-text.js +52 -0
  116. package/dist/tui/contexts/tasks-context.d.ts +15 -0
  117. package/dist/tui/contexts/tasks-context.js +224 -40
  118. package/dist/tui/contexts/theme-context.d.ts +1 -0
  119. package/dist/tui/contexts/theme-context.js +3 -2
  120. package/dist/tui/hooks/use-activity-logs.js +7 -1
  121. package/dist/tui/types/messages.d.ts +32 -5
  122. package/dist/tui/utils/index.d.ts +1 -1
  123. package/dist/tui/utils/index.js +1 -1
  124. package/dist/tui/utils/log.d.ts +0 -9
  125. package/dist/tui/utils/log.js +2 -53
  126. package/dist/tui/views/command-view.js +4 -1
  127. package/dist/utils/file-validator.js +8 -4
  128. package/oclif.manifest.json +1 -54
  129. package/package.json +4 -2
  130. package/dist/core/interfaces/cipher/i-coding-agent-log-parser.d.ts +0 -20
  131. package/dist/core/interfaces/cipher/i-coding-agent-log-watcher.d.ts +0 -31
  132. package/dist/core/interfaces/i-file-watcher-service.d.ts +0 -41
  133. package/dist/core/interfaces/i-file-watcher-service.js +0 -1
  134. package/dist/core/interfaces/parser/i-clean-parser-service.d.ts +0 -18
  135. package/dist/core/interfaces/parser/i-clean-parser-service.js +0 -1
  136. package/dist/core/interfaces/parser/i-raw-parser-service.d.ts +0 -17
  137. package/dist/core/interfaces/parser/i-raw-parser-service.js +0 -1
  138. package/dist/core/interfaces/parser/i-session-normalizer.d.ts +0 -56
  139. package/dist/core/interfaces/parser/i-session-normalizer.js +0 -1
  140. package/dist/infra/cipher/parsers/coding-agent-log-parser.d.ts +0 -24
  141. package/dist/infra/cipher/parsers/coding-agent-log-parser.js +0 -51
  142. package/dist/infra/cipher/watcher/coding-agent-log-watcher.d.ts +0 -14
  143. package/dist/infra/cipher/watcher/coding-agent-log-watcher.js +0 -55
  144. package/dist/infra/parsers/clean/clean-claude-service.d.ts +0 -111
  145. package/dist/infra/parsers/clean/clean-claude-service.js +0 -271
  146. package/dist/infra/parsers/clean/clean-codex-service.d.ts +0 -231
  147. package/dist/infra/parsers/clean/clean-codex-service.js +0 -534
  148. package/dist/infra/parsers/clean/clean-copilot-service.d.ts +0 -255
  149. package/dist/infra/parsers/clean/clean-copilot-service.js +0 -729
  150. package/dist/infra/parsers/clean/clean-cursor-service.d.ts +0 -161
  151. package/dist/infra/parsers/clean/clean-cursor-service.js +0 -432
  152. package/dist/infra/parsers/clean/clean-parser-service-factory.d.ts +0 -54
  153. package/dist/infra/parsers/clean/clean-parser-service-factory.js +0 -80
  154. package/dist/infra/parsers/clean/shared.d.ts +0 -84
  155. package/dist/infra/parsers/clean/shared.js +0 -273
  156. package/dist/infra/parsers/raw/raw-claude-service.d.ts +0 -195
  157. package/dist/infra/parsers/raw/raw-claude-service.js +0 -548
  158. package/dist/infra/parsers/raw/raw-codex-service.d.ts +0 -313
  159. package/dist/infra/parsers/raw/raw-codex-service.js +0 -782
  160. package/dist/infra/parsers/raw/raw-copilot-service.d.ts +0 -196
  161. package/dist/infra/parsers/raw/raw-copilot-service.js +0 -558
  162. package/dist/infra/parsers/raw/raw-cursor-service.d.ts +0 -316
  163. package/dist/infra/parsers/raw/raw-cursor-service.js +0 -818
  164. package/dist/infra/parsers/raw/raw-parser-service-factory.d.ts +0 -54
  165. package/dist/infra/parsers/raw/raw-parser-service-factory.js +0 -81
  166. package/dist/infra/watcher/file-watcher-service.d.ts +0 -10
  167. package/dist/infra/watcher/file-watcher-service.js +0 -81
  168. package/dist/oclif/commands/watch.d.ts +0 -25
  169. package/dist/oclif/commands/watch.js +0 -175
  170. /package/dist/core/interfaces/{cipher/i-coding-agent-log-parser.js → i-provider-config-store.js} +0 -0
  171. /package/dist/core/interfaces/{cipher/i-coding-agent-log-watcher.js → i-provider-keychain-store.js} +0 -0
@@ -0,0 +1,100 @@
1
+ import type { PdfMetadata, PdfPageContent } from '../../../core/domain/cipher/file-system/types.js';
2
+ /**
3
+ * Options for PDF text extraction.
4
+ */
5
+ export interface PdfExtractOptions {
6
+ /** Maximum number of pages to extract (default: 100, max: 200) */
7
+ limit?: number;
8
+ /** Starting page number (1-based, default: 1) */
9
+ offset?: number;
10
+ }
11
+ /**
12
+ * Result of PDF text extraction.
13
+ */
14
+ export interface PdfExtractResult {
15
+ /** Whether there are more pages available after this extraction */
16
+ hasMore: boolean;
17
+ /** PDF metadata (page count, title, author, etc.) */
18
+ metadata: PdfMetadata;
19
+ /** Extracted page contents */
20
+ pages: PdfPageContent[];
21
+ }
22
+ /**
23
+ * PDF text extraction and metadata extraction utility.
24
+ * Provides page-by-page extraction with pagination support.
25
+ *
26
+ * Features:
27
+ * - Magic byte validation
28
+ * - Fast metadata-only extraction
29
+ * - Page-by-page text extraction with offset/limit
30
+ * - Default: 100 pages, max: 200 pages per extraction
31
+ */
32
+ export declare class PdfExtractor {
33
+ /**
34
+ * Extracts metadata from a PDF buffer without extracting text.
35
+ * This is a fast path when you only need page count, title, author, etc.
36
+ *
37
+ * @param buffer - PDF file buffer
38
+ * @param filePath - Path to the PDF file (for error messages)
39
+ * @returns PDF metadata
40
+ */
41
+ static extractMetadata(buffer: Buffer, filePath: string): Promise<PdfMetadata>;
42
+ /**
43
+ * Extracts text from a PDF buffer with pagination support.
44
+ *
45
+ * @param buffer - PDF file buffer
46
+ * @param filePath - Path to the PDF file (for error messages)
47
+ * @param options - Extraction options (offset, limit)
48
+ * @returns Extraction result with pages, metadata, and continuation info
49
+ */
50
+ static extractText(buffer: Buffer, filePath: string, options?: PdfExtractOptions): Promise<PdfExtractResult>;
51
+ /**
52
+ * Checks if a buffer contains valid PDF magic bytes.
53
+ * @param buffer - Buffer to check
54
+ * @returns true if buffer starts with %PDF-
55
+ */
56
+ static isValidPdf(buffer: Buffer): boolean;
57
+ /**
58
+ * Builds PdfMetadata from unpdf meta info object.
59
+ * @param pageCount - Total number of pages
60
+ * @param info - Optional info object from unpdf getMeta
61
+ * @returns PdfMetadata object
62
+ */
63
+ private static buildMetadataFromInfo;
64
+ /**
65
+ * Extracts text from specific pages of a PDF document.
66
+ * Uses PDF.js page-level API for efficient extraction of page ranges.
67
+ *
68
+ * @param pdf - PDF document proxy from unpdf
69
+ * @param startPage - Starting page number (1-based)
70
+ * @param endPage - Ending page number (1-based, inclusive)
71
+ * @returns Array of PdfPageContent with extracted text
72
+ */
73
+ private static extractPagesFromDocument;
74
+ /**
75
+ * Extracts a meaningful error message from an unknown error.
76
+ */
77
+ private static getExtractionErrorMessage;
78
+ /**
79
+ * Parses PDF date string format (D:YYYYMMDDHHmmSS) to Date object.
80
+ * @param dateStr - PDF date string
81
+ * @returns Parsed Date or undefined if invalid
82
+ */
83
+ private static parsePdfDate;
84
+ /**
85
+ * Wraps extraction errors with appropriate PdfExtractionError.
86
+ * @param error - The caught error
87
+ * @param filePath - Path to the PDF file
88
+ * @returns PdfExtractionError with appropriate message
89
+ */
90
+ private static wrapExtractionError;
91
+ }
92
+ /**
93
+ * Formats extracted PDF pages into a readable string with page separators.
94
+ * @param pages - Array of extracted page contents
95
+ * @param metadata - PDF metadata
96
+ * @param hasMore - Whether there are more pages
97
+ * @param nextOffset - Next offset for continuation (if hasMore is true)
98
+ * @returns Formatted string with page separators
99
+ */
100
+ export declare function formatPdfContent(pages: PdfPageContent[], metadata: PdfMetadata, hasMore: boolean, nextOffset: number): string;
@@ -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
+ }
@@ -1,6 +1,7 @@
1
1
  import type { RequestOptions } from '@anthropic-ai/sdk/internal/request-options';
2
2
  import type { MessageCreateParamsNonStreaming } from '@anthropic-ai/sdk/resources/messages.js';
3
3
  import type { Content, GenerateContentConfig, GenerateContentResponse } from '@google/genai';
4
+ import type { GenerateContentChunk } from '../../../core/interfaces/cipher/i-content-generator.js';
4
5
  /**
5
6
  * ByteRover HTTP LLM provider configuration.
6
7
  */
@@ -61,6 +62,22 @@ export declare class ByteRoverLlmHttpService {
61
62
  * @returns Response in GenerateContentResponse format
62
63
  */
63
64
  generateContent(contents: Content[] | MessageCreateParamsNonStreaming, config: GenerateContentConfig | RequestOptions, model: string, executionMetadata?: Record<string, unknown>): Promise<GenerateContentResponse>;
65
+ /**
66
+ * Call ByteRover REST LLM service to generate content with streaming.
67
+ *
68
+ * Currently falls back to non-streaming endpoint since /api/llm/generate/stream
69
+ * doesn't exist on the backend yet. Extracts thinking/reasoning from the complete
70
+ * response and yields them as separate chunks.
71
+ *
72
+ * When backend streaming is available, this will use SSE for true streaming.
73
+ *
74
+ * @param contents - For Gemini: Content[]. For Claude: MessageCreateParamsNonStreaming (complete body)
75
+ * @param config - For Gemini: GenerateContentConfig. For Claude: RequestOptions (optional HTTP options)
76
+ * @param model - Model to use (detects provider from model name)
77
+ * @param executionMetadata - Optional execution metadata (mode, executionContext)
78
+ * @yields GenerateContentChunk objects as they are generated
79
+ */
80
+ generateContentStream(contents: Content[] | MessageCreateParamsNonStreaming, config: GenerateContentConfig | RequestOptions, model: string, executionMetadata?: Record<string, unknown>): AsyncGenerator<GenerateContentChunk>;
64
81
  /**
65
82
  * Call the ByteRover REST Generate endpoint.
66
83
  *
@@ -91,4 +108,27 @@ export declare class ByteRoverLlmHttpService {
91
108
  * @returns GCP region identifier ('us-east5' or 'global')
92
109
  */
93
110
  private detectRegionFromModel;
111
+ /**
112
+ * Extract content chunks from a complete response.
113
+ *
114
+ * Looks for text parts (excluding thinking) and function calls,
115
+ * yields them as final chunks.
116
+ *
117
+ * @param response - Complete GenerateContentResponse
118
+ * @yields GenerateContentChunk for content and tool calls
119
+ */
120
+ private extractContentFromResponse;
121
+ /**
122
+ * Extract thinking/reasoning chunks from a complete response.
123
+ *
124
+ * Looks for parts with `thought: true` and yields them as THINKING chunks.
125
+ *
126
+ * @param response - Complete GenerateContentResponse
127
+ * @yields GenerateContentChunk for each thinking part
128
+ */
129
+ private extractThinkingFromResponse;
130
+ /**
131
+ * Map provider finish reason to standard format.
132
+ */
133
+ private mapFinishReason;
94
134
  }
@@ -1,4 +1,6 @@
1
+ import { StreamChunkType } from '../../../core/interfaces/cipher/i-content-generator.js';
1
2
  import { AuthenticatedHttpClient } from '../../http/authenticated-http-client.js';
3
+ import { ThoughtParser } from '../llm/thought-parser.js';
2
4
  /**
3
5
  * ByteRover HTTP LLM API client.
4
6
  *
@@ -72,6 +74,30 @@ export class ByteRoverLlmHttpService {
72
74
  };
73
75
  return this.callHttpGenerate(request);
74
76
  }
77
+ /**
78
+ * Call ByteRover REST LLM service to generate content with streaming.
79
+ *
80
+ * Currently falls back to non-streaming endpoint since /api/llm/generate/stream
81
+ * doesn't exist on the backend yet. Extracts thinking/reasoning from the complete
82
+ * response and yields them as separate chunks.
83
+ *
84
+ * When backend streaming is available, this will use SSE for true streaming.
85
+ *
86
+ * @param contents - For Gemini: Content[]. For Claude: MessageCreateParamsNonStreaming (complete body)
87
+ * @param config - For Gemini: GenerateContentConfig. For Claude: RequestOptions (optional HTTP options)
88
+ * @param model - Model to use (detects provider from model name)
89
+ * @param executionMetadata - Optional execution metadata (mode, executionContext)
90
+ * @yields GenerateContentChunk objects as they are generated
91
+ */
92
+ async *generateContentStream(contents, config, model, executionMetadata) {
93
+ // Fall back to non-streaming endpoint and simulate streaming
94
+ // by extracting thinking from the complete response
95
+ const response = await this.generateContent(contents, config, model, executionMetadata);
96
+ // Extract and yield thinking/reasoning chunks first
97
+ yield* this.extractThinkingFromResponse(response);
98
+ // Then yield the final content
99
+ yield* this.extractContentFromResponse(response);
100
+ }
75
101
  /**
76
102
  * Call the ByteRover REST Generate endpoint.
77
103
  *
@@ -84,10 +110,10 @@ export class ByteRoverLlmHttpService {
84
110
  async callHttpGenerate(request) {
85
111
  const url = `${this.config.apiBaseUrl}/api/llm/generate`;
86
112
  const httpClient = new AuthenticatedHttpClient(this.config.accessToken, this.config.sessionKey);
87
- const response = await httpClient.post(url, request, {
113
+ const httpResponse = await httpClient.post(url, request, {
88
114
  timeout: this.config.timeout,
89
115
  });
90
- return response.data;
116
+ return httpResponse.data;
91
117
  }
92
118
  /**
93
119
  * Detect LLM provider from model identifier.
@@ -113,4 +139,128 @@ export class ByteRoverLlmHttpService {
113
139
  detectRegionFromModel(model) {
114
140
  return model.toLowerCase().startsWith('claude') ? 'us-east5' : 'global';
115
141
  }
142
+ /**
143
+ * Extract content chunks from a complete response.
144
+ *
145
+ * Looks for text parts (excluding thinking) and function calls,
146
+ * yields them as final chunks.
147
+ *
148
+ * @param response - Complete GenerateContentResponse
149
+ * @yields GenerateContentChunk for content and tool calls
150
+ */
151
+ *extractContentFromResponse(response) {
152
+ const { candidates } = response;
153
+ if (!candidates || candidates.length === 0) {
154
+ yield {
155
+ content: '',
156
+ finishReason: 'stop',
157
+ isComplete: true,
158
+ };
159
+ return;
160
+ }
161
+ const candidate = candidates[0];
162
+ const parts = candidate?.content?.parts;
163
+ const finishReason = this.mapFinishReason(candidate?.finishReason ?? 'STOP');
164
+ if (!parts || parts.length === 0) {
165
+ yield {
166
+ content: '',
167
+ finishReason,
168
+ isComplete: true,
169
+ };
170
+ return;
171
+ }
172
+ // Collect text content (excluding thinking parts)
173
+ const textParts = [];
174
+ const functionCalls = [];
175
+ for (const part of parts) {
176
+ const partRecord = part;
177
+ // Skip thinking parts
178
+ if (partRecord.thought === true)
179
+ continue;
180
+ // Collect text
181
+ if (partRecord.text && typeof partRecord.text === 'string') {
182
+ textParts.push(partRecord.text);
183
+ }
184
+ // Collect function calls
185
+ if (partRecord.functionCall) {
186
+ functionCalls.push(partRecord.functionCall);
187
+ }
188
+ }
189
+ // Yield final content chunk
190
+ yield {
191
+ content: textParts.join('').trimEnd(),
192
+ finishReason,
193
+ isComplete: true,
194
+ toolCalls: functionCalls.length > 0
195
+ ? functionCalls.map((fc, index) => ({
196
+ function: {
197
+ arguments: JSON.stringify(fc.args ?? {}),
198
+ name: fc.name ?? '',
199
+ },
200
+ id: `call_${Date.now()}_${index}`,
201
+ type: 'function',
202
+ }))
203
+ : undefined,
204
+ };
205
+ }
206
+ /**
207
+ * Extract thinking/reasoning chunks from a complete response.
208
+ *
209
+ * Looks for parts with `thought: true` and yields them as THINKING chunks.
210
+ *
211
+ * @param response - Complete GenerateContentResponse
212
+ * @yields GenerateContentChunk for each thinking part
213
+ */
214
+ *extractThinkingFromResponse(response) {
215
+ const { candidates } = response;
216
+ if (!candidates || candidates.length === 0)
217
+ return;
218
+ const parts = candidates[0]?.content?.parts;
219
+ if (!parts)
220
+ return;
221
+ let thinkingSubject;
222
+ for (const part of parts) {
223
+ const partRecord = part;
224
+ // Check for thinking part (thought: true)
225
+ if (partRecord.thought === true && partRecord.text && typeof partRecord.text === 'string') {
226
+ const delta = partRecord.text;
227
+ // Extract subject from **Subject** markdown if not already found
228
+ if (!thinkingSubject && delta) {
229
+ const parsed = ThoughtParser.parse(delta);
230
+ if (parsed.subject) {
231
+ thinkingSubject = parsed.subject;
232
+ }
233
+ }
234
+ yield {
235
+ isComplete: false,
236
+ providerMetadata: {
237
+ subject: thinkingSubject,
238
+ },
239
+ reasoning: delta.trimEnd(),
240
+ type: StreamChunkType.THINKING,
241
+ };
242
+ }
243
+ }
244
+ }
245
+ /**
246
+ * Map provider finish reason to standard format.
247
+ */
248
+ mapFinishReason(reason) {
249
+ switch (reason.toUpperCase()) {
250
+ case 'FUNCTION_CALL':
251
+ case 'TOOL_CALLS': {
252
+ return 'tool_calls';
253
+ }
254
+ case 'LENGTH':
255
+ case 'MAX_TOKENS': {
256
+ return 'max_tokens';
257
+ }
258
+ case 'STOP': {
259
+ return 'stop';
260
+ }
261
+ default: {
262
+ return 'stop';
263
+ }
264
+ }
265
+ }
116
266
  }
@@ -72,8 +72,14 @@ export class GeminiMessageFormatter {
72
72
  }
73
73
  const textParts = [];
74
74
  const functionCallsWithSignatures = [];
75
- // Extract text and function calls from response parts
75
+ let thoughtText;
76
+ // Extract text, thoughts, and function calls from response parts
76
77
  for (const part of candidate.content.parts) {
78
+ // Check for thought parts first (Gemini 2.5+ with includeThoughts: true)
79
+ if ('thought' in part && part.thought === true && 'text' in part && part.text) {
80
+ thoughtText = part.text;
81
+ continue; // Don't add thought to textParts
82
+ }
77
83
  if ('text' in part && part.text) {
78
84
  textParts.push(part.text);
79
85
  }
@@ -102,6 +108,7 @@ export class GeminiMessageFormatter {
102
108
  {
103
109
  content: textParts.join('') || null,
104
110
  role: 'assistant',
111
+ thought: thoughtText,
105
112
  toolCalls,
106
113
  },
107
114
  ];
@@ -60,9 +60,8 @@ export declare class ByteRoverContentGenerator implements IContentGenerator {
60
60
  /**
61
61
  * Generate content with streaming.
62
62
  *
63
- * Note: The current gRPC service collects all chunks before returning.
64
- * This implementation yields the complete response as a single chunk.
65
- * True streaming can be implemented when the gRPC service exposes the stream.
63
+ * Uses the HTTP service's streaming endpoint to yield chunks as they arrive.
64
+ * Handles both regular content and thinking/reasoning parts from Gemini models.
66
65
  *
67
66
  * @param request - Generation request
68
67
  * @yields Content chunks as they are generated
@@ -117,24 +117,33 @@ export class ByteRoverContentGenerator {
117
117
  /**
118
118
  * Generate content with streaming.
119
119
  *
120
- * Note: The current gRPC service collects all chunks before returning.
121
- * This implementation yields the complete response as a single chunk.
122
- * True streaming can be implemented when the gRPC service exposes the stream.
120
+ * Uses the HTTP service's streaming endpoint to yield chunks as they arrive.
121
+ * Handles both regular content and thinking/reasoning parts from Gemini models.
123
122
  *
124
123
  * @param request - Generation request
125
124
  * @yields Content chunks as they are generated
126
125
  * @returns Async generator yielding content chunks
127
126
  */
128
127
  async *generateContentStream(request) {
129
- // For now, use non-streaming and yield complete response
130
- // True streaming can be added when gRPC service exposes the stream
131
- const response = await this.generateContent(request);
132
- yield {
133
- content: response.content,
134
- finishReason: response.finishReason,
135
- isComplete: true,
136
- toolCalls: response.toolCalls,
128
+ // Format messages for provider
129
+ let formattedMessages = this.formatter.format(request.contents);
130
+ // For Gemini 3+ models, ensure function calls in the active loop have thought signatures
131
+ if (this.providerType === 'gemini') {
132
+ formattedMessages = ensureActiveLoopHasThoughtSignatures(formattedMessages, this.config.model);
133
+ }
134
+ // Build generation config
135
+ const genConfig = this.buildGenerationConfig(request.tools ?? {}, request.systemPrompt ?? '', formattedMessages);
136
+ // Build execution metadata from request
137
+ const executionMetadata = {
138
+ sessionId: request.taskId,
139
+ taskId: request.taskId,
140
+ ...(request.executionContext && { executionContext: request.executionContext }),
137
141
  };
142
+ // Determine contents and config based on provider
143
+ const contents = this.providerType === 'claude' ? genConfig : formattedMessages;
144
+ const config = this.providerType === 'claude' ? {} : genConfig;
145
+ // Stream from HTTP service
146
+ yield* this.httpService.generateContentStream(contents, config, this.config.model, executionMetadata);
138
147
  }
139
148
  /**
140
149
  * Build Claude-specific generation configuration.
@@ -64,6 +64,7 @@ export declare class OpenRouterContentGenerator implements IContentGenerator {
64
64
  * Generate content with streaming.
65
65
  *
66
66
  * Uses OpenAI SDK's native streaming support for real-time content generation.
67
+ * Includes rawChunk for native reasoning extraction by the stream transformer.
67
68
  *
68
69
  * @param request - Generation request
69
70
  * @yields Content chunks as they are generated