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
@@ -47,6 +47,8 @@ export class ChatSession {
47
47
  llmService;
48
48
  messageQueue;
49
49
  sharedServices;
50
+ /** When true, strip taskId from forwarded events (subagent mode via emitTaskId: false) */
51
+ suppressTaskIdForwarding = false;
50
52
  /**
51
53
  * Creates a new chat session
52
54
  *
@@ -184,6 +186,9 @@ export class ChatSession {
184
186
  }
185
187
  // Store taskId for event forwarding only if emitTaskId is true
186
188
  this.currentTaskId = emitTaskId ? taskId : undefined;
189
+ // Suppress taskId in forwarded events for subagent sessions (prevents subagent
190
+ // events from appearing under the parent task in the TUI)
191
+ this.suppressTaskIdForwarding = !emitTaskId;
187
192
  this.isExecuting = true;
188
193
  sessionStatusManager.setBusy(this.id, this.eventBus);
189
194
  try {
@@ -217,6 +222,8 @@ export class ChatSession {
217
222
  if (this.currentTaskId === taskId) {
218
223
  this.currentTaskId = undefined;
219
224
  }
225
+ // Reset suppression flag
226
+ this.suppressTaskIdForwarding = false;
220
227
  // Only mark idle if no active tasks
221
228
  if (this.activeControllers.size === 0) {
222
229
  this.isExecuting = false;
@@ -290,6 +297,7 @@ export class ChatSession {
290
297
  await this.llmService.completeTask(finalInput, {
291
298
  executionContext: options?.executionContext,
292
299
  signal: controller.signal,
300
+ stream: true,
293
301
  taskId,
294
302
  });
295
303
  }
@@ -341,9 +349,12 @@ export class ChatSession {
341
349
  const forwarder = (payload) => {
342
350
  // Add sessionId and taskId to payload
343
351
  const basePayload = payload && typeof payload === 'object' ? payload : {};
344
- // Preserve taskId from payload if present, fallback to currentTaskId for backward compat
352
+ // When suppressTaskIdForwarding is true (subagent mode), strip taskId from
353
+ // forwarded events so they don't appear under the parent task in the TUI
345
354
  const payloadTaskId = basePayload.taskId;
346
- const effectiveTaskId = payloadTaskId ?? this.currentTaskId;
355
+ const effectiveTaskId = this.suppressTaskIdForwarding
356
+ ? undefined
357
+ : (payloadTaskId ?? this.currentTaskId);
347
358
  const payloadWithSession = {
348
359
  ...basePayload,
349
360
  sessionId: this.id,
@@ -523,8 +523,12 @@ export class MessageStorageService {
523
523
  case 'reasoning': {
524
524
  return {
525
525
  messagePart: {
526
+ id: part.id ?? `reasoning-${Date.now()}`,
526
527
  summary: part.reasoningSummary,
527
528
  text: part.content,
529
+ time: {
530
+ start: part.createdAt ?? Date.now(),
531
+ },
528
532
  type: 'reasoning',
529
533
  },
530
534
  };
@@ -26,7 +26,7 @@ const BashExecInputSchema = z
26
26
  .default(false)
27
27
  .describe('Execute command in background'),
28
28
  /**
29
- * Timeout in milliseconds (max: 600000).
29
+ * Timeout in milliseconds (max: 600000, default: 300000 = 5 minutes).
30
30
  */
31
31
  timeout: z
32
32
  .number()
@@ -34,8 +34,8 @@ const BashExecInputSchema = z
34
34
  .positive()
35
35
  .max(600_000)
36
36
  .optional()
37
- .default(120_000)
38
- .describe('Timeout in milliseconds'),
37
+ .default(300_000)
38
+ .describe('Timeout in milliseconds (default: 5 minutes)'),
39
39
  })
40
40
  .strict();
41
41
  /**
@@ -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,
@@ -152,7 +152,7 @@ export function createTaskTool(dependencies) {
152
152
  session = await sessionManager.createChildSessionWithOverrides(context?.sessionId ?? 'parent', agent.name, subagentSessionId, { fileSystemService: restrictedFs });
153
153
  }
154
154
  else {
155
- session = await sessionManager.createSession(subagentSessionId);
155
+ session = await sessionManager.createChildSession(context?.sessionId ?? 'parent', agent.name, subagentSessionId);
156
156
  }
157
157
  // Build the system prompt for the subagent
158
158
  // Load the agent's prompt from YAML file or use inline prompt
@@ -20,6 +20,10 @@ export declare const RULES_CONNECTOR_CONFIGS: {
20
20
  readonly filePath: "AGENTS.md";
21
21
  readonly writeMode: "append";
22
22
  };
23
+ readonly Antigravity: {
24
+ readonly filePath: ".agent/rules/agent-context.md";
25
+ readonly writeMode: "overwrite";
26
+ };
23
27
  readonly 'Augment Code': {
24
28
  readonly filePath: ".augment/rules/agent-context.md";
25
29
  readonly writeMode: "overwrite";
@@ -6,6 +6,10 @@ export const RULES_CONNECTOR_CONFIGS = {
6
6
  filePath: 'AGENTS.md',
7
7
  writeMode: 'append',
8
8
  },
9
+ Antigravity: {
10
+ filePath: '.agent/rules/agent-context.md',
11
+ writeMode: 'overwrite',
12
+ },
9
13
  'Augment Code': {
10
14
  filePath: '.augment/rules/agent-context.md',
11
15
  writeMode: 'overwrite',
@@ -0,0 +1,148 @@
1
+ /**
2
+ * OpenRouter API Client
3
+ *
4
+ * Handles API calls to OpenRouter for:
5
+ * - Fetching available models
6
+ * - Validating API keys
7
+ *
8
+ * Uses the OpenRouter REST API: https://openrouter.ai/api/v1
9
+ */
10
+ import type { ProviderDefinition } from '../../core/domain/entities/provider-registry.js';
11
+ /**
12
+ * OpenRouter model from the /models endpoint.
13
+ * Based on: https://openrouter.ai/docs#models
14
+ */
15
+ export interface OpenRouterModel {
16
+ /** Supported modalities */
17
+ architecture?: {
18
+ instruct_type?: string;
19
+ modality: string;
20
+ tokenizer: string;
21
+ };
22
+ /** Context length (max tokens) */
23
+ context_length: number;
24
+ /** Description */
25
+ description?: string;
26
+ /** Model ID (e.g., 'anthropic/claude-3.5-sonnet') */
27
+ id: string;
28
+ /** Display name */
29
+ name: string;
30
+ /** Per-request limits */
31
+ per_request_limits?: {
32
+ completion_tokens?: string;
33
+ prompt_tokens?: string;
34
+ };
35
+ /** Pricing per token (as string) */
36
+ pricing: {
37
+ completion: string;
38
+ prompt: string;
39
+ };
40
+ /** Top provider info */
41
+ top_provider?: {
42
+ context_length?: number;
43
+ is_moderated?: boolean;
44
+ max_completion_tokens?: number;
45
+ };
46
+ }
47
+ /**
48
+ * Normalized model for use in the application.
49
+ */
50
+ export interface NormalizedModel {
51
+ /** Context window size */
52
+ contextLength: number;
53
+ /** Optional description */
54
+ description?: string;
55
+ /** Model ID (e.g., 'anthropic/claude-3.5-sonnet') */
56
+ id: string;
57
+ /** Whether this model is free */
58
+ isFree: boolean;
59
+ /** Display name */
60
+ name: string;
61
+ /** Pricing per million tokens */
62
+ pricing: {
63
+ inputPerM: number;
64
+ outputPerM: number;
65
+ };
66
+ /** Provider name extracted from ID (e.g., 'anthropic') */
67
+ provider: string;
68
+ }
69
+ /**
70
+ * OpenRouter API client configuration.
71
+ */
72
+ export interface OpenRouterApiClientConfig {
73
+ /** Base URL for OpenRouter API */
74
+ baseUrl?: string;
75
+ /** Cache TTL in milliseconds (default: 1 hour) */
76
+ cacheTtlMs?: number;
77
+ /** HTTP Referer header */
78
+ httpReferer?: string;
79
+ /** X-Title header */
80
+ xTitle?: string;
81
+ }
82
+ /**
83
+ * OpenRouter API Client.
84
+ *
85
+ * Provides methods to interact with the OpenRouter API for fetching models
86
+ * and validating API keys.
87
+ *
88
+ * @example
89
+ * ```typescript
90
+ * const client = new OpenRouterApiClient()
91
+ *
92
+ * // Validate API key
93
+ * const isValid = await client.validateApiKey('sk-or-v1-...')
94
+ *
95
+ * // Fetch models
96
+ * const models = await client.fetchModels('sk-or-v1-...')
97
+ * ```
98
+ */
99
+ export declare class OpenRouterApiClient {
100
+ private readonly baseUrl;
101
+ private readonly cacheTtlMs;
102
+ private readonly httpReferer?;
103
+ private modelCache;
104
+ private readonly xTitle?;
105
+ constructor(config?: OpenRouterApiClientConfig);
106
+ /**
107
+ * Clears the model cache.
108
+ */
109
+ clearCache(): void;
110
+ /**
111
+ * Fetches available models from OpenRouter.
112
+ * Results are cached for the configured TTL.
113
+ *
114
+ * @param apiKey - The API key to use
115
+ * @param forceRefresh - If true, bypasses cache
116
+ * @returns Array of normalized models
117
+ */
118
+ fetchModels(apiKey: string, forceRefresh?: boolean): Promise<NormalizedModel[]>;
119
+ /**
120
+ * Validates an API key by attempting to fetch models.
121
+ *
122
+ * @param apiKey - The API key to validate
123
+ * @returns Object with isValid flag and optional error message
124
+ */
125
+ validateApiKey(apiKey: string): Promise<{
126
+ error?: string;
127
+ isValid: boolean;
128
+ }>;
129
+ /**
130
+ * Internal method to fetch models from OpenRouter API.
131
+ */
132
+ private fetchModelsInternal;
133
+ /**
134
+ * Normalizes an OpenRouter model to our standard format.
135
+ */
136
+ private normalizeModel;
137
+ }
138
+ /**
139
+ * Creates an OpenRouterApiClient configured from a provider definition.
140
+ *
141
+ * @param provider - Provider definition from the registry
142
+ * @returns Configured OpenRouterApiClient
143
+ */
144
+ export declare function createOpenRouterApiClient(provider: ProviderDefinition): OpenRouterApiClient;
145
+ /**
146
+ * Gets or creates the singleton OpenRouter API client.
147
+ */
148
+ export declare function getOpenRouterApiClient(): OpenRouterApiClient;
@@ -0,0 +1,161 @@
1
+ /**
2
+ * OpenRouter API Client
3
+ *
4
+ * Handles API calls to OpenRouter for:
5
+ * - Fetching available models
6
+ * - Validating API keys
7
+ *
8
+ * Uses the OpenRouter REST API: https://openrouter.ai/api/v1
9
+ */
10
+ import axios, { isAxiosError } from 'axios';
11
+ const DEFAULT_BASE_URL = 'https://openrouter.ai/api/v1';
12
+ const DEFAULT_CACHE_TTL = 60 * 60 * 1000; // 1 hour
13
+ /**
14
+ * OpenRouter API Client.
15
+ *
16
+ * Provides methods to interact with the OpenRouter API for fetching models
17
+ * and validating API keys.
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * const client = new OpenRouterApiClient()
22
+ *
23
+ * // Validate API key
24
+ * const isValid = await client.validateApiKey('sk-or-v1-...')
25
+ *
26
+ * // Fetch models
27
+ * const models = await client.fetchModels('sk-or-v1-...')
28
+ * ```
29
+ */
30
+ export class OpenRouterApiClient {
31
+ baseUrl;
32
+ cacheTtlMs;
33
+ httpReferer;
34
+ modelCache;
35
+ xTitle;
36
+ constructor(config = {}) {
37
+ this.baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
38
+ this.cacheTtlMs = config.cacheTtlMs ?? DEFAULT_CACHE_TTL;
39
+ this.httpReferer = config.httpReferer ?? 'https://byterover.dev';
40
+ this.xTitle = config.xTitle ?? 'byterover-cli';
41
+ }
42
+ /**
43
+ * Clears the model cache.
44
+ */
45
+ clearCache() {
46
+ this.modelCache = undefined;
47
+ }
48
+ /**
49
+ * Fetches available models from OpenRouter.
50
+ * Results are cached for the configured TTL.
51
+ *
52
+ * @param apiKey - The API key to use
53
+ * @param forceRefresh - If true, bypasses cache
54
+ * @returns Array of normalized models
55
+ */
56
+ async fetchModels(apiKey, forceRefresh = false) {
57
+ // Check cache
58
+ if (!forceRefresh && this.modelCache && Date.now() - this.modelCache.timestamp < this.cacheTtlMs) {
59
+ return this.modelCache.models;
60
+ }
61
+ const models = await this.fetchModelsInternal(apiKey);
62
+ // Update cache
63
+ this.modelCache = {
64
+ models,
65
+ timestamp: Date.now(),
66
+ };
67
+ return models;
68
+ }
69
+ /**
70
+ * Validates an API key by attempting to fetch models.
71
+ *
72
+ * @param apiKey - The API key to validate
73
+ * @returns Object with isValid flag and optional error message
74
+ */
75
+ async validateApiKey(apiKey) {
76
+ try {
77
+ await this.fetchModelsInternal(apiKey);
78
+ return { isValid: true };
79
+ }
80
+ catch (error) {
81
+ if (isAxiosError(error)) {
82
+ if (error.response?.status === 401) {
83
+ return { error: 'Invalid API key', isValid: false };
84
+ }
85
+ if (error.response?.status === 403) {
86
+ return { error: 'API key does not have required permissions', isValid: false };
87
+ }
88
+ return { error: `API error: ${error.response?.statusText ?? error.message}`, isValid: false };
89
+ }
90
+ return { error: error instanceof Error ? error.message : 'Unknown error', isValid: false };
91
+ }
92
+ }
93
+ /**
94
+ * Internal method to fetch models from OpenRouter API.
95
+ */
96
+ async fetchModelsInternal(apiKey) {
97
+ const response = await axios.get(`${this.baseUrl}/models`, {
98
+ headers: {
99
+ Authorization: `Bearer ${apiKey}`,
100
+ 'HTTP-Referer': this.httpReferer,
101
+ 'X-Title': this.xTitle,
102
+ },
103
+ timeout: 30_000,
104
+ });
105
+ return response.data.data.map((model) => this.normalizeModel(model));
106
+ }
107
+ /**
108
+ * Normalizes an OpenRouter model to our standard format.
109
+ */
110
+ normalizeModel(model) {
111
+ // Extract provider from model ID (e.g., 'anthropic' from 'anthropic/claude-3.5-sonnet')
112
+ const [provider, ...nameParts] = model.id.split('/');
113
+ const shortName = nameParts.join('/') || model.id;
114
+ // Parse pricing (convert from string to number)
115
+ // OpenRouter returns price per token, multiply by 1M to get price per million tokens
116
+ const inputPricePerToken = Number.parseFloat(model.pricing.prompt) || 0;
117
+ const outputPricePerToken = Number.parseFloat(model.pricing.completion) || 0;
118
+ const inputPerM = inputPricePerToken * 1_000_000;
119
+ const outputPerM = outputPricePerToken * 1_000_000;
120
+ // Check if free (both prices are 0)
121
+ const isFree = inputPricePerToken === 0 && outputPricePerToken === 0;
122
+ return {
123
+ contextLength: model.context_length,
124
+ description: model.description,
125
+ id: model.id,
126
+ isFree,
127
+ name: model.name || shortName,
128
+ pricing: {
129
+ inputPerM,
130
+ outputPerM,
131
+ },
132
+ provider: provider.charAt(0).toUpperCase() + provider.slice(1), // Capitalize
133
+ };
134
+ }
135
+ }
136
+ /**
137
+ * Creates an OpenRouterApiClient configured from a provider definition.
138
+ *
139
+ * @param provider - Provider definition from the registry
140
+ * @returns Configured OpenRouterApiClient
141
+ */
142
+ export function createOpenRouterApiClient(provider) {
143
+ return new OpenRouterApiClient({
144
+ baseUrl: provider.baseUrl || DEFAULT_BASE_URL,
145
+ httpReferer: provider.headers['HTTP-Referer'],
146
+ xTitle: provider.headers['X-Title'],
147
+ });
148
+ }
149
+ /**
150
+ * Singleton instance of the OpenRouter API client.
151
+ */
152
+ let _openRouterApiClient;
153
+ /**
154
+ * Gets or creates the singleton OpenRouter API client.
155
+ */
156
+ export function getOpenRouterApiClient() {
157
+ if (!_openRouterApiClient) {
158
+ _openRouterApiClient = new OpenRouterApiClient();
159
+ }
160
+ return _openRouterApiClient;
161
+ }
@@ -1,14 +1,20 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { z } from 'zod';
3
3
  import type { ITransportClient } from '../../../core/interfaces/transport/index.js';
4
- export declare const BrvCurateInputSchema: z.ZodObject<{
5
- context: z.ZodString;
4
+ export declare const BrvCurateInputSchema: z.ZodEffects<z.ZodObject<{
5
+ context: z.ZodOptional<z.ZodString>;
6
6
  files: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
7
7
  }, "strip", z.ZodTypeAny, {
8
- context: string;
8
+ context?: string | undefined;
9
9
  files?: string[] | undefined;
10
10
  }, {
11
- context: string;
11
+ context?: string | undefined;
12
+ files?: string[] | undefined;
13
+ }>, {
14
+ context?: string | undefined;
15
+ files?: string[] | undefined;
16
+ }, {
17
+ context?: string | undefined;
12
18
  files?: string[] | undefined;
13
19
  }>;
14
20
  /**
@@ -1,13 +1,16 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { z } from 'zod';
3
3
  export const BrvCurateInputSchema = z.object({
4
- context: z.string().describe('Knowledge to store: patterns, decisions, errors, or insights about the codebase'),
4
+ context: z
5
+ .string()
6
+ .optional()
7
+ .describe('Knowledge to store: patterns, decisions, errors, or insights about the codebase. Required unless files are provided.'),
5
8
  files: z
6
9
  .array(z.string())
7
10
  .max(5)
8
11
  .optional()
9
- .describe('Optional file paths with critical context to include (max 5 files)'),
10
- });
12
+ .describe('Optional file paths with critical context to include (max 5 files). Required if context not provided.'),
13
+ }).refine((data) => Boolean(data.context?.trim()) || Boolean(data.files?.length), { message: 'Either context or files must be provided' });
11
14
  /**
12
15
  * Registers the brv-curate tool with the MCP server.
13
16
  *
@@ -41,9 +44,11 @@ export function registerBrvCurateTool(server, getClient, getWorkingDirectory) {
41
44
  try {
42
45
  const taskId = randomUUID();
43
46
  // Create task via transport (same pattern as brv curate command)
47
+ // Use provided context, or empty string for file-only mode
48
+ const resolvedContent = context?.trim() ? context : '';
44
49
  await client.request('task:create', {
45
50
  clientCwd: getWorkingDirectory(),
46
- content: context,
51
+ content: resolvedContent,
47
52
  taskId,
48
53
  type: 'curate',
49
54
  ...(files?.length ? { files } : {}),
@@ -9,7 +9,7 @@
9
9
  * - task:completed: Task finished successfully
10
10
  * - task:error: Task failed with an error
11
11
  */
12
- export async function waitForTaskResult(client, taskId, timeoutMs = 120_000) {
12
+ export async function waitForTaskResult(client, taskId, timeoutMs = 300_000) {
13
13
  return new Promise((resolve, reject) => {
14
14
  let result = '';
15
15
  let completed = false;
@@ -59,6 +59,14 @@ export async function waitForTaskResult(client, taskId, timeoutMs = 120_000) {
59
59
  cleanup();
60
60
  reject(new Error(payload.error.message));
61
61
  }
62
+ }),
63
+ // Listen for task cancellation
64
+ client.on('task:cancelled', (payload) => {
65
+ if (payload.taskId === taskId && !completed) {
66
+ completed = true;
67
+ cleanup();
68
+ reject(new Error('Task was cancelled'));
69
+ }
62
70
  }));
63
71
  });
64
72
  }