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.
- package/README.md +71 -6
- package/dist/core/domain/cipher/errors/file-system-error.d.ts +11 -0
- package/dist/core/domain/cipher/errors/file-system-error.js +17 -0
- package/dist/core/domain/cipher/file-system/types.d.ts +40 -6
- package/dist/core/domain/cipher/process/types.d.ts +1 -1
- package/dist/core/domain/entities/agent.d.ts +1 -1
- package/dist/core/domain/entities/agent.js +5 -0
- package/dist/core/domain/entities/provider-config.d.ts +92 -0
- package/dist/core/domain/entities/provider-config.js +181 -0
- package/dist/core/domain/entities/provider-registry.d.ts +55 -0
- package/dist/core/domain/entities/provider-registry.js +74 -0
- package/dist/core/interfaces/cipher/cipher-services.d.ts +0 -3
- package/dist/core/interfaces/cipher/i-content-generator.d.ts +30 -0
- package/dist/core/interfaces/cipher/i-content-generator.js +12 -1
- package/dist/core/interfaces/cipher/index.d.ts +0 -2
- package/dist/core/interfaces/cipher/message-factory.d.ts +4 -1
- package/dist/core/interfaces/cipher/message-factory.js +5 -0
- package/dist/core/interfaces/cipher/message-types.d.ts +19 -1
- package/dist/core/interfaces/i-provider-config-store.d.ts +88 -0
- package/dist/core/interfaces/i-provider-keychain-store.d.ts +33 -0
- package/dist/infra/cipher/file-system/binary-utils.d.ts +15 -2
- package/dist/infra/cipher/file-system/binary-utils.js +26 -3
- package/dist/infra/cipher/file-system/file-system-service.d.ts +9 -0
- package/dist/infra/cipher/file-system/file-system-service.js +96 -13
- package/dist/infra/cipher/file-system/pdf-extractor.d.ts +100 -0
- package/dist/infra/cipher/file-system/pdf-extractor.js +226 -0
- package/dist/infra/cipher/http/internal-llm-http-service.d.ts +40 -0
- package/dist/infra/cipher/http/internal-llm-http-service.js +152 -2
- package/dist/infra/cipher/llm/formatters/gemini-formatter.js +8 -1
- package/dist/infra/cipher/llm/generators/byterover-content-generator.d.ts +2 -3
- package/dist/infra/cipher/llm/generators/byterover-content-generator.js +20 -11
- package/dist/infra/cipher/llm/generators/openrouter-content-generator.d.ts +1 -0
- package/dist/infra/cipher/llm/generators/openrouter-content-generator.js +26 -0
- package/dist/infra/cipher/llm/internal-llm-service.d.ts +13 -0
- package/dist/infra/cipher/llm/internal-llm-service.js +75 -4
- package/dist/infra/cipher/llm/model-capabilities.d.ts +74 -0
- package/dist/infra/cipher/llm/model-capabilities.js +157 -0
- package/dist/infra/cipher/llm/openrouter-llm-service.d.ts +35 -1
- package/dist/infra/cipher/llm/openrouter-llm-service.js +216 -28
- package/dist/infra/cipher/llm/stream-processor.d.ts +22 -2
- package/dist/infra/cipher/llm/stream-processor.js +78 -4
- package/dist/infra/cipher/llm/thought-parser.d.ts +1 -1
- package/dist/infra/cipher/llm/thought-parser.js +5 -5
- package/dist/infra/cipher/llm/transformers/openrouter-stream-transformer.d.ts +49 -0
- package/dist/infra/cipher/llm/transformers/openrouter-stream-transformer.js +272 -0
- package/dist/infra/cipher/llm/transformers/reasoning-extractor.d.ts +71 -0
- package/dist/infra/cipher/llm/transformers/reasoning-extractor.js +253 -0
- package/dist/infra/cipher/process/process-service.js +1 -1
- package/dist/infra/cipher/session/chat-session.d.ts +2 -0
- package/dist/infra/cipher/session/chat-session.js +13 -2
- package/dist/infra/cipher/storage/message-storage-service.js +4 -0
- package/dist/infra/cipher/tools/implementations/bash-exec-tool.js +3 -3
- package/dist/infra/cipher/tools/implementations/read-file-tool.js +24 -4
- package/dist/infra/cipher/tools/implementations/task-tool.js +1 -1
- package/dist/infra/connectors/rules/rules-connector-config.d.ts +4 -0
- package/dist/infra/connectors/rules/rules-connector-config.js +4 -0
- package/dist/infra/http/openrouter-api-client.d.ts +148 -0
- package/dist/infra/http/openrouter-api-client.js +161 -0
- package/dist/infra/mcp/tools/brv-curate-tool.d.ts +10 -4
- package/dist/infra/mcp/tools/brv-curate-tool.js +9 -4
- package/dist/infra/mcp/tools/task-result-waiter.js +9 -1
- package/dist/infra/process/agent-worker.js +178 -70
- package/dist/infra/process/transport-handlers.d.ts +25 -4
- package/dist/infra/process/transport-handlers.js +57 -10
- package/dist/infra/repl/commands/connectors-command.js +2 -2
- package/dist/infra/repl/commands/index.js +5 -0
- package/dist/infra/repl/commands/model-command.d.ts +13 -0
- package/dist/infra/repl/commands/model-command.js +212 -0
- package/dist/infra/repl/commands/provider-command.d.ts +13 -0
- package/dist/infra/repl/commands/provider-command.js +181 -0
- package/dist/infra/repl/commands/space/switch-command.js +0 -2
- package/dist/infra/repl/transport-client-helper.js +6 -2
- package/dist/infra/storage/file-provider-config-store.d.ts +83 -0
- package/dist/infra/storage/file-provider-config-store.js +157 -0
- package/dist/infra/storage/provider-keychain-store.d.ts +37 -0
- package/dist/infra/storage/provider-keychain-store.js +75 -0
- package/dist/infra/transport/socket-io-transport-client.d.ts +20 -0
- package/dist/infra/transport/socket-io-transport-client.js +88 -1
- package/dist/infra/usecase/curate-use-case.js +10 -4
- package/dist/infra/usecase/space-switch-use-case.d.ts +0 -10
- package/dist/infra/usecase/space-switch-use-case.js +7 -37
- package/dist/oclif/hooks/init/welcome.js +4 -17
- package/dist/resources/prompts/curate.yml +1 -0
- package/dist/resources/tools/bash_exec.txt +1 -1
- package/dist/resources/tools/read_file.txt +5 -2
- package/dist/tui/components/api-key-dialog.d.ts +39 -0
- package/dist/tui/components/api-key-dialog.js +94 -0
- package/dist/tui/components/execution/execution-changes.d.ts +3 -1
- package/dist/tui/components/execution/execution-changes.js +4 -4
- package/dist/tui/components/execution/execution-content.d.ts +1 -1
- package/dist/tui/components/execution/execution-content.js +4 -12
- package/dist/tui/components/execution/execution-input.js +1 -1
- package/dist/tui/components/execution/execution-progress.d.ts +10 -13
- package/dist/tui/components/execution/execution-progress.js +70 -17
- package/dist/tui/components/execution/execution-reasoning.d.ts +16 -0
- package/dist/tui/components/execution/execution-reasoning.js +34 -0
- package/dist/tui/components/execution/execution-tool.d.ts +23 -0
- package/dist/tui/components/execution/execution-tool.js +125 -0
- package/dist/tui/components/execution/expanded-log-view.js +3 -3
- package/dist/tui/components/execution/log-item.d.ts +2 -0
- package/dist/tui/components/execution/log-item.js +6 -4
- package/dist/tui/components/index.d.ts +2 -0
- package/dist/tui/components/index.js +2 -0
- package/dist/tui/components/inline-prompts/inline-select.js +3 -2
- package/dist/tui/components/model-dialog.d.ts +63 -0
- package/dist/tui/components/model-dialog.js +89 -0
- package/dist/tui/components/onboarding/onboarding-flow.js +8 -2
- package/dist/tui/components/provider-dialog.d.ts +27 -0
- package/dist/tui/components/provider-dialog.js +31 -0
- package/dist/tui/components/reasoning-text.d.ts +26 -0
- package/dist/tui/components/reasoning-text.js +49 -0
- package/dist/tui/components/selectable-list.d.ts +54 -0
- package/dist/tui/components/selectable-list.js +180 -0
- package/dist/tui/components/streaming-text.d.ts +30 -0
- package/dist/tui/components/streaming-text.js +52 -0
- package/dist/tui/contexts/tasks-context.d.ts +15 -0
- package/dist/tui/contexts/tasks-context.js +224 -40
- package/dist/tui/contexts/theme-context.d.ts +1 -0
- package/dist/tui/contexts/theme-context.js +3 -2
- package/dist/tui/hooks/use-activity-logs.js +7 -1
- package/dist/tui/types/messages.d.ts +32 -5
- package/dist/tui/utils/index.d.ts +1 -1
- package/dist/tui/utils/index.js +1 -1
- package/dist/tui/utils/log.d.ts +0 -9
- package/dist/tui/utils/log.js +2 -53
- package/dist/tui/views/command-view.js +4 -1
- package/dist/utils/file-validator.js +8 -4
- package/oclif.manifest.json +1 -54
- package/package.json +4 -2
- package/dist/core/interfaces/cipher/i-coding-agent-log-parser.d.ts +0 -20
- package/dist/core/interfaces/cipher/i-coding-agent-log-watcher.d.ts +0 -31
- package/dist/core/interfaces/i-file-watcher-service.d.ts +0 -41
- package/dist/core/interfaces/i-file-watcher-service.js +0 -1
- package/dist/core/interfaces/parser/i-clean-parser-service.d.ts +0 -18
- package/dist/core/interfaces/parser/i-clean-parser-service.js +0 -1
- package/dist/core/interfaces/parser/i-raw-parser-service.d.ts +0 -17
- package/dist/core/interfaces/parser/i-raw-parser-service.js +0 -1
- package/dist/core/interfaces/parser/i-session-normalizer.d.ts +0 -56
- package/dist/core/interfaces/parser/i-session-normalizer.js +0 -1
- package/dist/infra/cipher/parsers/coding-agent-log-parser.d.ts +0 -24
- package/dist/infra/cipher/parsers/coding-agent-log-parser.js +0 -51
- package/dist/infra/cipher/watcher/coding-agent-log-watcher.d.ts +0 -14
- package/dist/infra/cipher/watcher/coding-agent-log-watcher.js +0 -55
- package/dist/infra/parsers/clean/clean-claude-service.d.ts +0 -111
- package/dist/infra/parsers/clean/clean-claude-service.js +0 -271
- package/dist/infra/parsers/clean/clean-codex-service.d.ts +0 -231
- package/dist/infra/parsers/clean/clean-codex-service.js +0 -534
- package/dist/infra/parsers/clean/clean-copilot-service.d.ts +0 -255
- package/dist/infra/parsers/clean/clean-copilot-service.js +0 -729
- package/dist/infra/parsers/clean/clean-cursor-service.d.ts +0 -161
- package/dist/infra/parsers/clean/clean-cursor-service.js +0 -432
- package/dist/infra/parsers/clean/clean-parser-service-factory.d.ts +0 -54
- package/dist/infra/parsers/clean/clean-parser-service-factory.js +0 -80
- package/dist/infra/parsers/clean/shared.d.ts +0 -84
- package/dist/infra/parsers/clean/shared.js +0 -273
- package/dist/infra/parsers/raw/raw-claude-service.d.ts +0 -195
- package/dist/infra/parsers/raw/raw-claude-service.js +0 -548
- package/dist/infra/parsers/raw/raw-codex-service.d.ts +0 -313
- package/dist/infra/parsers/raw/raw-codex-service.js +0 -782
- package/dist/infra/parsers/raw/raw-copilot-service.d.ts +0 -196
- package/dist/infra/parsers/raw/raw-copilot-service.js +0 -558
- package/dist/infra/parsers/raw/raw-cursor-service.d.ts +0 -316
- package/dist/infra/parsers/raw/raw-cursor-service.js +0 -818
- package/dist/infra/parsers/raw/raw-parser-service-factory.d.ts +0 -54
- package/dist/infra/parsers/raw/raw-parser-service-factory.js +0 -81
- package/dist/infra/watcher/file-watcher-service.d.ts +0 -10
- package/dist/infra/watcher/file-watcher-service.js +0 -81
- package/dist/oclif/commands/watch.d.ts +0 -25
- package/dist/oclif/commands/watch.js +0 -175
- /package/dist/core/interfaces/{cipher/i-coding-agent-log-parser.js → i-provider-config-store.js} +0 -0
- /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
|
-
//
|
|
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 =
|
|
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(
|
|
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
|
|
11
|
-
|
|
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
|
|
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.
|
|
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
|
|
8
|
+
context?: string | undefined;
|
|
9
9
|
files?: string[] | undefined;
|
|
10
10
|
}, {
|
|
11
|
-
context
|
|
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
|
|
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:
|
|
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 =
|
|
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
|
}
|