byterover-cli 1.4.0 → 1.6.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 +193 -12
- package/dist/core/domain/cipher/process/types.d.ts +1 -1
- 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/domain/errors/headless-prompt-error.d.ts +11 -0
- package/dist/core/domain/errors/headless-prompt-error.js +18 -0
- 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/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-cogit-pull-service.d.ts +0 -1
- package/dist/core/interfaces/i-memory-retrieval-service.d.ts +0 -1
- package/dist/core/interfaces/i-memory-storage-service.d.ts +0 -2
- package/dist/core/interfaces/i-provider-config-store.d.ts +88 -0
- package/dist/core/interfaces/i-provider-config-store.js +1 -0
- package/dist/core/interfaces/i-provider-keychain-store.d.ts +33 -0
- package/dist/core/interfaces/i-provider-keychain-store.js +1 -0
- package/dist/core/interfaces/i-space-service.d.ts +1 -2
- package/dist/core/interfaces/i-team-service.d.ts +1 -2
- package/dist/core/interfaces/i-user-service.d.ts +1 -2
- package/dist/core/interfaces/usecase/i-curate-use-case.d.ts +2 -0
- package/dist/core/interfaces/usecase/i-init-use-case.d.ts +9 -3
- package/dist/core/interfaces/usecase/i-login-use-case.d.ts +4 -1
- package/dist/core/interfaces/usecase/i-pull-use-case.d.ts +5 -3
- package/dist/core/interfaces/usecase/i-push-use-case.d.ts +6 -4
- package/dist/core/interfaces/usecase/i-query-use-case.d.ts +2 -0
- package/dist/core/interfaces/usecase/i-status-use-case.d.ts +1 -0
- package/dist/infra/cipher/agent/service-initializer.d.ts +1 -1
- package/dist/infra/cipher/agent/service-initializer.js +0 -1
- package/dist/infra/cipher/file-system/file-system-service.js +5 -5
- package/dist/infra/cipher/http/internal-llm-http-service.d.ts +40 -1
- package/dist/infra/cipher/http/internal-llm-http-service.js +153 -4
- 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/task-tool.js +1 -1
- package/dist/infra/cogit/http-cogit-pull-service.js +1 -1
- package/dist/infra/cogit/http-cogit-push-service.js +0 -1
- package/dist/infra/http/authenticated-http-client.d.ts +1 -3
- package/dist/infra/http/authenticated-http-client.js +1 -5
- 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/task-result-waiter.js +9 -1
- package/dist/infra/memory/http-memory-retrieval-service.js +1 -1
- package/dist/infra/memory/http-memory-storage-service.js +2 -2
- package/dist/infra/process/agent-worker.js +178 -70
- package/dist/infra/process/inline-agent-executor.d.ts +32 -0
- package/dist/infra/process/inline-agent-executor.js +259 -0
- 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/transport-client-helper.js +6 -2
- package/dist/infra/space/http-space-service.d.ts +1 -1
- package/dist/infra/space/http-space-service.js +2 -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/storage/token-store.d.ts +4 -3
- package/dist/infra/storage/token-store.js +6 -5
- package/dist/infra/team/http-team-service.d.ts +1 -1
- package/dist/infra/team/http-team-service.js +2 -2
- package/dist/infra/terminal/headless-terminal.d.ts +91 -0
- package/dist/infra/terminal/headless-terminal.js +211 -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.d.ts +40 -1
- package/dist/infra/usecase/curate-use-case.js +176 -15
- package/dist/infra/usecase/init-use-case.d.ts +27 -5
- package/dist/infra/usecase/init-use-case.js +200 -34
- package/dist/infra/usecase/login-use-case.d.ts +10 -8
- package/dist/infra/usecase/login-use-case.js +35 -2
- package/dist/infra/usecase/pull-use-case.d.ts +19 -5
- package/dist/infra/usecase/pull-use-case.js +71 -13
- package/dist/infra/usecase/push-use-case.d.ts +18 -5
- package/dist/infra/usecase/push-use-case.js +81 -14
- package/dist/infra/usecase/query-use-case.d.ts +21 -0
- package/dist/infra/usecase/query-use-case.js +114 -29
- package/dist/infra/usecase/space-list-use-case.js +1 -1
- package/dist/infra/usecase/space-switch-use-case.js +2 -2
- package/dist/infra/usecase/status-use-case.d.ts +36 -0
- package/dist/infra/usecase/status-use-case.js +185 -48
- package/dist/infra/user/http-user-service.d.ts +1 -1
- package/dist/infra/user/http-user-service.js +2 -2
- package/dist/oclif/commands/curate.d.ts +6 -1
- package/dist/oclif/commands/curate.js +24 -3
- package/dist/oclif/commands/init.d.ts +18 -0
- package/dist/oclif/commands/init.js +129 -0
- package/dist/oclif/commands/login.d.ts +9 -0
- package/dist/oclif/commands/login.js +45 -0
- package/dist/oclif/commands/pull.d.ts +16 -0
- package/dist/oclif/commands/pull.js +78 -0
- package/dist/oclif/commands/push.d.ts +17 -0
- package/dist/oclif/commands/push.js +87 -0
- package/dist/oclif/commands/query.d.ts +6 -1
- package/dist/oclif/commands/query.js +29 -4
- package/dist/oclif/commands/status.d.ts +5 -1
- package/dist/oclif/commands/status.js +17 -5
- package/dist/resources/tools/bash_exec.txt +1 -1
- 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/hooks/use-auth-polling.js +1 -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/environment-detector.d.ts +15 -0
- package/dist/utils/environment-detector.js +62 -1
- package/oclif.manifest.json +287 -5
- package/package.json +1 -1
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model Command
|
|
3
|
+
*
|
|
4
|
+
* Interactive command for selecting LLM models from the active provider.
|
|
5
|
+
* Uses the streaming command pattern with inline prompts.
|
|
6
|
+
*
|
|
7
|
+
* Usage: /model
|
|
8
|
+
*/
|
|
9
|
+
import { getProviderById } from '../../../core/domain/entities/provider-registry.js';
|
|
10
|
+
import { CommandKind } from '../../../tui/types.js';
|
|
11
|
+
import { getOpenRouterApiClient } from '../../http/openrouter-api-client.js';
|
|
12
|
+
import { FileProviderConfigStore } from '../../storage/file-provider-config-store.js';
|
|
13
|
+
import { ProviderKeychainStore } from '../../storage/provider-keychain-store.js';
|
|
14
|
+
/**
|
|
15
|
+
* Format price for display.
|
|
16
|
+
* Shows prices in a compact format: <0.01 for tiny prices, otherwise 2 decimal places.
|
|
17
|
+
*/
|
|
18
|
+
function formatPrice(pricePerM) {
|
|
19
|
+
if (pricePerM === 0)
|
|
20
|
+
return '0';
|
|
21
|
+
if (pricePerM < 0.01)
|
|
22
|
+
return '<0.01';
|
|
23
|
+
return pricePerM.toFixed(2);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Build model choices for selection prompt.
|
|
27
|
+
*/
|
|
28
|
+
async function buildModelChoices(providerId, apiKey, config) {
|
|
29
|
+
const choices = [];
|
|
30
|
+
if (providerId === 'openrouter') {
|
|
31
|
+
const client = getOpenRouterApiClient();
|
|
32
|
+
const models = await client.fetchModels(apiKey);
|
|
33
|
+
// Sort models: favorites first, then recent, then by provider
|
|
34
|
+
const sortedModels = [...models].sort((a, b) => {
|
|
35
|
+
const aIsFavorite = config.favorites.includes(a.id);
|
|
36
|
+
const bIsFavorite = config.favorites.includes(b.id);
|
|
37
|
+
const aIsRecent = config.recent.includes(a.id);
|
|
38
|
+
const bIsRecent = config.recent.includes(b.id);
|
|
39
|
+
// Favorites first
|
|
40
|
+
if (aIsFavorite && !bIsFavorite)
|
|
41
|
+
return -1;
|
|
42
|
+
if (!aIsFavorite && bIsFavorite)
|
|
43
|
+
return 1;
|
|
44
|
+
// Then recent
|
|
45
|
+
if (aIsRecent && !bIsRecent)
|
|
46
|
+
return -1;
|
|
47
|
+
if (!aIsRecent && bIsRecent)
|
|
48
|
+
return 1;
|
|
49
|
+
// Then by provider
|
|
50
|
+
const providerCompare = a.provider.localeCompare(b.provider);
|
|
51
|
+
if (providerCompare !== 0)
|
|
52
|
+
return providerCompare;
|
|
53
|
+
// Then by name
|
|
54
|
+
return a.name.localeCompare(b.name);
|
|
55
|
+
});
|
|
56
|
+
for (const model of sortedModels) {
|
|
57
|
+
const isCurrent = model.id === config.activeModel;
|
|
58
|
+
const isFavorite = config.favorites.includes(model.id);
|
|
59
|
+
// Build indicators
|
|
60
|
+
const indicators = [];
|
|
61
|
+
if (isCurrent)
|
|
62
|
+
indicators.push('(Current)');
|
|
63
|
+
if (isFavorite && !isCurrent)
|
|
64
|
+
indicators.push('★');
|
|
65
|
+
if (model.isFree && !isCurrent)
|
|
66
|
+
indicators.push('[Free]');
|
|
67
|
+
const statusSuffix = indicators.length > 0 ? ` ${indicators.join(' ')}` : '';
|
|
68
|
+
// Build description with pricing and context
|
|
69
|
+
const descParts = [];
|
|
70
|
+
if (model.provider)
|
|
71
|
+
descParts.push(model.provider);
|
|
72
|
+
if (!model.isFree && model.pricing) {
|
|
73
|
+
// Show input/output pricing separately for more clarity
|
|
74
|
+
const inputPrice = formatPrice(model.pricing.inputPerM);
|
|
75
|
+
const outputPrice = formatPrice(model.pricing.outputPerM);
|
|
76
|
+
descParts.push(`$${inputPrice}/$${outputPrice}/M`);
|
|
77
|
+
}
|
|
78
|
+
if (model.contextLength) {
|
|
79
|
+
if (model.contextLength >= 1_000_000) {
|
|
80
|
+
descParts.push(`${(model.contextLength / 1_000_000).toFixed(1)}M ctx`);
|
|
81
|
+
}
|
|
82
|
+
else if (model.contextLength >= 1000) {
|
|
83
|
+
descParts.push(`${Math.round(model.contextLength / 1000)}K ctx`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
choices.push({
|
|
87
|
+
description: descParts.join(' • '),
|
|
88
|
+
name: `${model.name}${statusSuffix}`,
|
|
89
|
+
value: model.id,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else if (providerId === 'byterover') {
|
|
94
|
+
// ByteRover internal - show a single option
|
|
95
|
+
choices.push({
|
|
96
|
+
description: 'Internal ByteRover model',
|
|
97
|
+
name: 'ByteRover Default (Current)',
|
|
98
|
+
value: 'byterover-default',
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return choices;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Model command definition.
|
|
105
|
+
*/
|
|
106
|
+
export const modelCommand = {
|
|
107
|
+
action: () => ({
|
|
108
|
+
async execute(onMessage, onPrompt) {
|
|
109
|
+
const configStore = new FileProviderConfigStore();
|
|
110
|
+
const keychainStore = new ProviderKeychainStore();
|
|
111
|
+
// Get active provider
|
|
112
|
+
const config = await configStore.read();
|
|
113
|
+
const activeProviderId = config.activeProvider;
|
|
114
|
+
const provider = getProviderById(activeProviderId);
|
|
115
|
+
if (!provider) {
|
|
116
|
+
onMessage({
|
|
117
|
+
content: `Active provider "${activeProviderId}" not found. Run /provider to select a provider.`,
|
|
118
|
+
id: `error-${Date.now()}`,
|
|
119
|
+
type: 'error',
|
|
120
|
+
});
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
// ByteRover doesn't support model selection
|
|
124
|
+
if (activeProviderId === 'byterover') {
|
|
125
|
+
onMessage({
|
|
126
|
+
content: 'ByteRover uses an internal model. Run /provider to switch to an external provider for model selection.',
|
|
127
|
+
id: `info-${Date.now()}`,
|
|
128
|
+
type: 'output',
|
|
129
|
+
});
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
// Get API key for the provider
|
|
133
|
+
const apiKey = await keychainStore.getApiKey(activeProviderId);
|
|
134
|
+
if (!apiKey) {
|
|
135
|
+
onMessage({
|
|
136
|
+
content: `No API key found for ${provider.name}. Run /provider to connect.`,
|
|
137
|
+
id: `error-${Date.now()}`,
|
|
138
|
+
type: 'error',
|
|
139
|
+
});
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
// Fetch models
|
|
143
|
+
onMessage({
|
|
144
|
+
actionId: 'fetch-models',
|
|
145
|
+
content: `Fetching models from ${provider.name}...`,
|
|
146
|
+
id: `loading-${Date.now()}`,
|
|
147
|
+
type: 'action_start',
|
|
148
|
+
});
|
|
149
|
+
let choices;
|
|
150
|
+
try {
|
|
151
|
+
const activeModel = await configStore.getActiveModel(activeProviderId);
|
|
152
|
+
const favorites = await configStore.getFavoriteModels(activeProviderId);
|
|
153
|
+
const recent = await configStore.getRecentModels(activeProviderId);
|
|
154
|
+
choices = await buildModelChoices(activeProviderId, apiKey, {
|
|
155
|
+
activeModel,
|
|
156
|
+
favorites,
|
|
157
|
+
recent,
|
|
158
|
+
});
|
|
159
|
+
onMessage({
|
|
160
|
+
actionId: 'fetch-models',
|
|
161
|
+
content: `Found ${choices.length} models`,
|
|
162
|
+
id: `loaded-${Date.now()}`,
|
|
163
|
+
type: 'action_stop',
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
onMessage({
|
|
168
|
+
actionId: 'fetch-models',
|
|
169
|
+
content: 'Failed',
|
|
170
|
+
id: `error-${Date.now()}`,
|
|
171
|
+
type: 'action_stop',
|
|
172
|
+
});
|
|
173
|
+
onMessage({
|
|
174
|
+
content: error instanceof Error ? error.message : 'Failed to fetch models',
|
|
175
|
+
id: `error-details-${Date.now()}`,
|
|
176
|
+
type: 'error',
|
|
177
|
+
});
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (choices.length === 0) {
|
|
181
|
+
onMessage({
|
|
182
|
+
content: 'No models available from this provider.',
|
|
183
|
+
id: `empty-${Date.now()}`,
|
|
184
|
+
type: 'output',
|
|
185
|
+
});
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
// Show model selection
|
|
189
|
+
const selectedModelId = await new Promise((resolve) => {
|
|
190
|
+
onPrompt({
|
|
191
|
+
choices,
|
|
192
|
+
message: `Select a model (${provider.name})`,
|
|
193
|
+
onResponse: (value) => resolve(value),
|
|
194
|
+
type: 'select',
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
// Save selected model
|
|
198
|
+
await configStore.setActiveModel(activeProviderId, selectedModelId);
|
|
199
|
+
onMessage({
|
|
200
|
+
content: `Model set to: ${selectedModelId}`,
|
|
201
|
+
id: `selected-${Date.now()}`,
|
|
202
|
+
type: 'output',
|
|
203
|
+
});
|
|
204
|
+
},
|
|
205
|
+
type: 'streaming',
|
|
206
|
+
}),
|
|
207
|
+
aliases: ['models'],
|
|
208
|
+
autoExecute: true,
|
|
209
|
+
description: 'Select a model from the active provider',
|
|
210
|
+
kind: CommandKind.BUILT_IN,
|
|
211
|
+
name: 'model',
|
|
212
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider Command
|
|
3
|
+
*
|
|
4
|
+
* Interactive command for selecting and connecting LLM providers.
|
|
5
|
+
* Uses the streaming command pattern with inline prompts.
|
|
6
|
+
*
|
|
7
|
+
* Usage: /provider
|
|
8
|
+
*/
|
|
9
|
+
import type { SlashCommand } from '../../../tui/types.js';
|
|
10
|
+
/**
|
|
11
|
+
* Provider command definition.
|
|
12
|
+
*/
|
|
13
|
+
export declare const providerCommand: SlashCommand;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider Command
|
|
3
|
+
*
|
|
4
|
+
* Interactive command for selecting and connecting LLM providers.
|
|
5
|
+
* Uses the streaming command pattern with inline prompts.
|
|
6
|
+
*
|
|
7
|
+
* Usage: /provider
|
|
8
|
+
*/
|
|
9
|
+
import { getProviderById, getProvidersSortedByPriority, providerRequiresApiKey, } from '../../../core/domain/entities/provider-registry.js';
|
|
10
|
+
import { CommandKind } from '../../../tui/types.js';
|
|
11
|
+
import { getOpenRouterApiClient } from '../../http/openrouter-api-client.js';
|
|
12
|
+
import { FileProviderConfigStore } from '../../storage/file-provider-config-store.js';
|
|
13
|
+
import { ProviderKeychainStore } from '../../storage/provider-keychain-store.js';
|
|
14
|
+
/**
|
|
15
|
+
* Build provider choices for selection prompt.
|
|
16
|
+
*/
|
|
17
|
+
async function buildProviderChoices() {
|
|
18
|
+
const configStore = new FileProviderConfigStore();
|
|
19
|
+
const keychainStore = new ProviderKeychainStore();
|
|
20
|
+
const config = await configStore.read();
|
|
21
|
+
const providers = getProvidersSortedByPriority();
|
|
22
|
+
const choices = [];
|
|
23
|
+
// Check connection status for all providers
|
|
24
|
+
const connectionStatuses = await Promise.all(providers.map(async (provider) => ({
|
|
25
|
+
isConnected: provider.id === 'byterover' || (await keychainStore.hasApiKey(provider.id)),
|
|
26
|
+
provider,
|
|
27
|
+
})));
|
|
28
|
+
for (const { isConnected, provider } of connectionStatuses) {
|
|
29
|
+
const isCurrent = provider.id === config.activeProvider;
|
|
30
|
+
// Build status indicators
|
|
31
|
+
const indicators = [];
|
|
32
|
+
if (isCurrent)
|
|
33
|
+
indicators.push('(Current)');
|
|
34
|
+
else if (isConnected)
|
|
35
|
+
indicators.push('[Connected]');
|
|
36
|
+
const statusSuffix = indicators.length > 0 ? ` ${indicators.join(' ')}` : '';
|
|
37
|
+
choices.push({
|
|
38
|
+
description: provider.description,
|
|
39
|
+
name: `${provider.name}${statusSuffix}`,
|
|
40
|
+
value: provider.id,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return choices;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Validate API key for a provider.
|
|
47
|
+
*/
|
|
48
|
+
async function validateApiKey(apiKey, providerId) {
|
|
49
|
+
if (providerId === 'openrouter') {
|
|
50
|
+
const client = getOpenRouterApiClient();
|
|
51
|
+
return client.validateApiKey(apiKey);
|
|
52
|
+
}
|
|
53
|
+
// For other providers, assume valid
|
|
54
|
+
return { isValid: true };
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Provider command definition.
|
|
58
|
+
*/
|
|
59
|
+
export const providerCommand = {
|
|
60
|
+
action: () => ({
|
|
61
|
+
async execute(onMessage, onPrompt) {
|
|
62
|
+
const configStore = new FileProviderConfigStore();
|
|
63
|
+
const keychainStore = new ProviderKeychainStore();
|
|
64
|
+
// Step 1: Show provider selection
|
|
65
|
+
const choices = await buildProviderChoices();
|
|
66
|
+
const selectedProviderId = await new Promise((resolve) => {
|
|
67
|
+
onPrompt({
|
|
68
|
+
choices,
|
|
69
|
+
message: 'Select a provider',
|
|
70
|
+
onResponse: (value) => resolve(value),
|
|
71
|
+
type: 'select',
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
const provider = getProviderById(selectedProviderId);
|
|
75
|
+
if (!provider) {
|
|
76
|
+
onMessage({
|
|
77
|
+
content: `Provider "${selectedProviderId}" not found`,
|
|
78
|
+
id: `error-${Date.now()}`,
|
|
79
|
+
type: 'error',
|
|
80
|
+
});
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
// Step 2: Check if already connected
|
|
84
|
+
const isConnected = provider.id === 'byterover' || (await keychainStore.hasApiKey(provider.id));
|
|
85
|
+
if (isConnected) {
|
|
86
|
+
// Already connected - just set as active
|
|
87
|
+
await configStore.setActiveProvider(provider.id);
|
|
88
|
+
onMessage({
|
|
89
|
+
content: `Switched to ${provider.name}`,
|
|
90
|
+
id: `success-${Date.now()}`,
|
|
91
|
+
type: 'output',
|
|
92
|
+
});
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
// Step 3: If provider requires API key, prompt for it
|
|
96
|
+
if (providerRequiresApiKey(provider.id)) {
|
|
97
|
+
onMessage({
|
|
98
|
+
content: provider.apiKeyUrl
|
|
99
|
+
? `Get your API key at: ${provider.apiKeyUrl}`
|
|
100
|
+
: `Enter your ${provider.name} API key`,
|
|
101
|
+
id: `info-${Date.now()}`,
|
|
102
|
+
type: 'output',
|
|
103
|
+
});
|
|
104
|
+
// Prompt for API key with validation
|
|
105
|
+
let isValid = false;
|
|
106
|
+
let apiKey = '';
|
|
107
|
+
while (!isValid) {
|
|
108
|
+
// eslint-disable-next-line no-await-in-loop
|
|
109
|
+
apiKey = await new Promise((resolve) => {
|
|
110
|
+
onPrompt({
|
|
111
|
+
message: `Enter ${provider.name} API key`,
|
|
112
|
+
onResponse: resolve,
|
|
113
|
+
placeholder: 'sk-...',
|
|
114
|
+
type: 'input',
|
|
115
|
+
validate(value) {
|
|
116
|
+
if (!value.trim())
|
|
117
|
+
return 'API key is required';
|
|
118
|
+
return true;
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
// Validate the API key
|
|
123
|
+
onMessage({
|
|
124
|
+
actionId: 'validate-key',
|
|
125
|
+
content: 'Validating API key...',
|
|
126
|
+
id: `validating-${Date.now()}`,
|
|
127
|
+
type: 'action_start',
|
|
128
|
+
});
|
|
129
|
+
// eslint-disable-next-line no-await-in-loop
|
|
130
|
+
const result = await validateApiKey(apiKey, provider.id);
|
|
131
|
+
if (result.isValid) {
|
|
132
|
+
isValid = true;
|
|
133
|
+
onMessage({
|
|
134
|
+
actionId: 'validate-key',
|
|
135
|
+
content: 'Valid',
|
|
136
|
+
id: `validated-${Date.now()}`,
|
|
137
|
+
type: 'action_stop',
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
onMessage({
|
|
142
|
+
actionId: 'validate-key',
|
|
143
|
+
content: 'Invalid',
|
|
144
|
+
id: `invalid-${Date.now()}`,
|
|
145
|
+
type: 'action_stop',
|
|
146
|
+
});
|
|
147
|
+
onMessage({
|
|
148
|
+
content: result.error ?? 'Invalid API key. Please try again.',
|
|
149
|
+
id: `error-${Date.now()}`,
|
|
150
|
+
type: 'error',
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// Store API key in keychain
|
|
155
|
+
await keychainStore.setApiKey(provider.id, apiKey);
|
|
156
|
+
// Mark provider as connected and set as active
|
|
157
|
+
await configStore.connectProvider(provider.id);
|
|
158
|
+
onMessage({
|
|
159
|
+
content: `Connected to ${provider.name}`,
|
|
160
|
+
id: `connected-${Date.now()}`,
|
|
161
|
+
type: 'output',
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
// Provider doesn't require API key
|
|
166
|
+
await configStore.connectProvider(provider.id);
|
|
167
|
+
onMessage({
|
|
168
|
+
content: `Switched to ${provider.name}`,
|
|
169
|
+
id: `connected-${Date.now()}`,
|
|
170
|
+
type: 'output',
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
type: 'streaming',
|
|
175
|
+
}),
|
|
176
|
+
aliases: ['providers', 'connect'],
|
|
177
|
+
autoExecute: true,
|
|
178
|
+
description: 'Connect to an LLM provider (e.g., OpenRouter)',
|
|
179
|
+
kind: CommandKind.BUILT_IN,
|
|
180
|
+
name: 'provider',
|
|
181
|
+
};
|
|
@@ -68,6 +68,12 @@ export async function connectTransportClient() {
|
|
|
68
68
|
try {
|
|
69
69
|
const factory = createTransportClientFactory();
|
|
70
70
|
const { client } = await factory.connect();
|
|
71
|
+
// IMPORTANT: Join broadcast-room FIRST before subscribing to events.
|
|
72
|
+
// This prevents missing events that are broadcast during the subscription window.
|
|
73
|
+
// Pattern inspired by opencode's atomic room join approach.
|
|
74
|
+
await client.joinRoom('broadcast-room');
|
|
75
|
+
logEvent('_room', { room: 'broadcast-room', state: 'joined' });
|
|
76
|
+
// Now subscribe to events - we won't miss any since we're already in the room
|
|
71
77
|
client.onStateChange((state) => {
|
|
72
78
|
logEvent('_connection', { clientId: client.getClientId(), state });
|
|
73
79
|
});
|
|
@@ -75,8 +81,6 @@ export async function connectTransportClient() {
|
|
|
75
81
|
client.on(event, (data) => logEvent(event, data));
|
|
76
82
|
}
|
|
77
83
|
logEvent('_connection', { clientId: client.getClientId(), state: 'initialized' });
|
|
78
|
-
await client.joinRoom('broadcast-room');
|
|
79
|
-
logEvent('_room', { room: 'broadcast-room', state: 'joined' });
|
|
80
84
|
return client;
|
|
81
85
|
}
|
|
82
86
|
catch (error) {
|
|
@@ -7,7 +7,7 @@ export type SpaceServiceConfig = {
|
|
|
7
7
|
export declare class HttpSpaceService implements ISpaceService {
|
|
8
8
|
private readonly config;
|
|
9
9
|
constructor(config: SpaceServiceConfig);
|
|
10
|
-
getSpaces(
|
|
10
|
+
getSpaces(sessionKey: string, teamId: string, option?: {
|
|
11
11
|
fetchAll?: boolean;
|
|
12
12
|
limit?: number;
|
|
13
13
|
offset?: number;
|
|
@@ -9,9 +9,9 @@ export class HttpSpaceService {
|
|
|
9
9
|
timeout: 10_000, // Default 10 seconds timeout
|
|
10
10
|
};
|
|
11
11
|
}
|
|
12
|
-
async getSpaces(
|
|
12
|
+
async getSpaces(sessionKey, teamId, option) {
|
|
13
13
|
try {
|
|
14
|
-
const httpClient = new AuthenticatedHttpClient(
|
|
14
|
+
const httpClient = new AuthenticatedHttpClient(sessionKey);
|
|
15
15
|
// Scenario 1: Fetch all automatically via auto-pagination
|
|
16
16
|
if (option?.fetchAll === true) {
|
|
17
17
|
return await this.fetchAllSpaces(httpClient, teamId);
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-based Provider Config Store
|
|
3
|
+
*
|
|
4
|
+
* Stores provider configuration (non-sensitive data) in a JSON file.
|
|
5
|
+
* API keys are stored separately in the system keychain.
|
|
6
|
+
*/
|
|
7
|
+
import type { IProviderConfigStore } from '../../core/interfaces/i-provider-config-store.js';
|
|
8
|
+
import { ProviderConfig } from '../../core/domain/entities/provider-config.js';
|
|
9
|
+
/**
|
|
10
|
+
* Dependencies for FileProviderConfigStore.
|
|
11
|
+
* Allows injection for testing.
|
|
12
|
+
*/
|
|
13
|
+
export interface FileProviderConfigStoreDeps {
|
|
14
|
+
readonly getConfigDir: () => string;
|
|
15
|
+
readonly getConfigPath: () => string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* File-based implementation of IProviderConfigStore.
|
|
19
|
+
* Stores configuration in ~/.config/brv/providers.json (or platform equivalent).
|
|
20
|
+
*/
|
|
21
|
+
export declare class FileProviderConfigStore implements IProviderConfigStore {
|
|
22
|
+
private cachedConfig;
|
|
23
|
+
private readonly deps;
|
|
24
|
+
constructor(deps?: FileProviderConfigStoreDeps);
|
|
25
|
+
/**
|
|
26
|
+
* Clears the cached config. Useful for testing or forcing a re-read.
|
|
27
|
+
*/
|
|
28
|
+
clearCache(): void;
|
|
29
|
+
/**
|
|
30
|
+
* Marks a provider as connected.
|
|
31
|
+
*/
|
|
32
|
+
connectProvider(providerId: string, options?: {
|
|
33
|
+
activeModel?: string;
|
|
34
|
+
}): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Removes a provider connection.
|
|
37
|
+
*/
|
|
38
|
+
disconnectProvider(providerId: string): Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* Gets the active model for a provider.
|
|
41
|
+
*/
|
|
42
|
+
getActiveModel(providerId: string): Promise<string | undefined>;
|
|
43
|
+
/**
|
|
44
|
+
* Gets the active provider ID.
|
|
45
|
+
*/
|
|
46
|
+
getActiveProvider(): Promise<string>;
|
|
47
|
+
/**
|
|
48
|
+
* Gets favorite models for a provider.
|
|
49
|
+
*/
|
|
50
|
+
getFavoriteModels(providerId: string): Promise<readonly string[]>;
|
|
51
|
+
/**
|
|
52
|
+
* Gets recent models for a provider.
|
|
53
|
+
*/
|
|
54
|
+
getRecentModels(providerId: string): Promise<readonly string[]>;
|
|
55
|
+
/**
|
|
56
|
+
* Checks if a provider is connected.
|
|
57
|
+
*/
|
|
58
|
+
isProviderConnected(providerId: string): Promise<boolean>;
|
|
59
|
+
/**
|
|
60
|
+
* Reads the provider configuration from disk.
|
|
61
|
+
*/
|
|
62
|
+
read(): Promise<ProviderConfig>;
|
|
63
|
+
/**
|
|
64
|
+
* Sets the active model for a provider.
|
|
65
|
+
*/
|
|
66
|
+
setActiveModel(providerId: string, modelId: string): Promise<void>;
|
|
67
|
+
/**
|
|
68
|
+
* Sets the active provider.
|
|
69
|
+
*/
|
|
70
|
+
setActiveProvider(providerId: string): Promise<void>;
|
|
71
|
+
/**
|
|
72
|
+
* Toggles a model as favorite.
|
|
73
|
+
*/
|
|
74
|
+
toggleFavorite(providerId: string, modelId: string): Promise<void>;
|
|
75
|
+
/**
|
|
76
|
+
* Writes the provider configuration to disk.
|
|
77
|
+
*/
|
|
78
|
+
write(config: ProviderConfig): Promise<void>;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Creates a file-based provider config store instance.
|
|
82
|
+
*/
|
|
83
|
+
export declare function createProviderConfigStore(): IProviderConfigStore;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-based Provider Config Store
|
|
3
|
+
*
|
|
4
|
+
* Stores provider configuration (non-sensitive data) in a JSON file.
|
|
5
|
+
* API keys are stored separately in the system keychain.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync } from 'node:fs';
|
|
8
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { ProviderConfig } from '../../core/domain/entities/provider-config.js';
|
|
11
|
+
import { getGlobalConfigDir } from '../../utils/global-config-path.js';
|
|
12
|
+
const PROVIDER_CONFIG_FILE = 'providers.json';
|
|
13
|
+
/**
|
|
14
|
+
* Default dependencies using the real path functions.
|
|
15
|
+
*/
|
|
16
|
+
const defaultDeps = {
|
|
17
|
+
getConfigDir: getGlobalConfigDir,
|
|
18
|
+
getConfigPath: () => join(getGlobalConfigDir(), PROVIDER_CONFIG_FILE),
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* File-based implementation of IProviderConfigStore.
|
|
22
|
+
* Stores configuration in ~/.config/brv/providers.json (or platform equivalent).
|
|
23
|
+
*/
|
|
24
|
+
export class FileProviderConfigStore {
|
|
25
|
+
cachedConfig;
|
|
26
|
+
deps;
|
|
27
|
+
constructor(deps = defaultDeps) {
|
|
28
|
+
this.deps = deps;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Clears the cached config. Useful for testing or forcing a re-read.
|
|
32
|
+
*/
|
|
33
|
+
clearCache() {
|
|
34
|
+
this.cachedConfig = undefined;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Marks a provider as connected.
|
|
38
|
+
*/
|
|
39
|
+
async connectProvider(providerId, options) {
|
|
40
|
+
const config = await this.read();
|
|
41
|
+
const newConfig = config
|
|
42
|
+
.withProviderConnected(providerId, options)
|
|
43
|
+
.withActiveProvider(providerId);
|
|
44
|
+
await this.write(newConfig);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Removes a provider connection.
|
|
48
|
+
*/
|
|
49
|
+
async disconnectProvider(providerId) {
|
|
50
|
+
const config = await this.read();
|
|
51
|
+
const newConfig = config.withProviderDisconnected(providerId);
|
|
52
|
+
await this.write(newConfig);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Gets the active model for a provider.
|
|
56
|
+
*/
|
|
57
|
+
async getActiveModel(providerId) {
|
|
58
|
+
const config = await this.read();
|
|
59
|
+
return config.getActiveModel(providerId);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Gets the active provider ID.
|
|
63
|
+
*/
|
|
64
|
+
async getActiveProvider() {
|
|
65
|
+
const config = await this.read();
|
|
66
|
+
return config.activeProvider;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Gets favorite models for a provider.
|
|
70
|
+
*/
|
|
71
|
+
async getFavoriteModels(providerId) {
|
|
72
|
+
const config = await this.read();
|
|
73
|
+
return config.getFavoriteModels(providerId);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Gets recent models for a provider.
|
|
77
|
+
*/
|
|
78
|
+
async getRecentModels(providerId) {
|
|
79
|
+
const config = await this.read();
|
|
80
|
+
return config.getRecentModels(providerId);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Checks if a provider is connected.
|
|
84
|
+
*/
|
|
85
|
+
async isProviderConnected(providerId) {
|
|
86
|
+
const config = await this.read();
|
|
87
|
+
return config.isProviderConnected(providerId);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Reads the provider configuration from disk.
|
|
91
|
+
*/
|
|
92
|
+
async read() {
|
|
93
|
+
if (this.cachedConfig) {
|
|
94
|
+
return this.cachedConfig;
|
|
95
|
+
}
|
|
96
|
+
const configPath = this.deps.getConfigPath();
|
|
97
|
+
if (!existsSync(configPath)) {
|
|
98
|
+
this.cachedConfig = ProviderConfig.createDefault();
|
|
99
|
+
return this.cachedConfig;
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
const content = await readFile(configPath, 'utf8');
|
|
103
|
+
const json = JSON.parse(content);
|
|
104
|
+
this.cachedConfig = ProviderConfig.fromJson(json);
|
|
105
|
+
return this.cachedConfig;
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// Return default config for any error
|
|
109
|
+
this.cachedConfig = ProviderConfig.createDefault();
|
|
110
|
+
return this.cachedConfig;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Sets the active model for a provider.
|
|
115
|
+
*/
|
|
116
|
+
async setActiveModel(providerId, modelId) {
|
|
117
|
+
const config = await this.read();
|
|
118
|
+
const newConfig = config.withActiveModel(providerId, modelId);
|
|
119
|
+
await this.write(newConfig);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Sets the active provider.
|
|
123
|
+
*/
|
|
124
|
+
async setActiveProvider(providerId) {
|
|
125
|
+
const config = await this.read();
|
|
126
|
+
const newConfig = config.withActiveProvider(providerId);
|
|
127
|
+
await this.write(newConfig);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Toggles a model as favorite.
|
|
131
|
+
*/
|
|
132
|
+
async toggleFavorite(providerId, modelId) {
|
|
133
|
+
const config = await this.read();
|
|
134
|
+
const newConfig = config.withFavoriteToggled(providerId, modelId);
|
|
135
|
+
await this.write(newConfig);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Writes the provider configuration to disk.
|
|
139
|
+
*/
|
|
140
|
+
async write(config) {
|
|
141
|
+
const configDir = this.deps.getConfigDir();
|
|
142
|
+
const configPath = this.deps.getConfigPath();
|
|
143
|
+
// Create config directory if it doesn't exist
|
|
144
|
+
await mkdir(configDir, { recursive: true });
|
|
145
|
+
// Write config file
|
|
146
|
+
const content = JSON.stringify(config.toJson(), null, 2);
|
|
147
|
+
await writeFile(configPath, content, 'utf8');
|
|
148
|
+
// Update cache
|
|
149
|
+
this.cachedConfig = config;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Creates a file-based provider config store instance.
|
|
154
|
+
*/
|
|
155
|
+
export function createProviderConfigStore() {
|
|
156
|
+
return new FileProviderConfigStore();
|
|
157
|
+
}
|