orquesta-cli 0.1.2 → 0.1.4

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.
@@ -0,0 +1,221 @@
1
+ import React, { useState, useEffect, useMemo } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ import TextInput from 'ink-text-input';
4
+ import Spinner from 'ink-spinner';
5
+ export const OpenRouterModelBrowser = ({ onSelect, onCancel, }) => {
6
+ const [mode, setMode] = useState('list');
7
+ const [models, setModels] = useState([]);
8
+ const [isLoading, setIsLoading] = useState(true);
9
+ const [error, setError] = useState(null);
10
+ const [searchQuery, setSearchQuery] = useState('');
11
+ const [manualModelId, setManualModelId] = useState('');
12
+ const [selectedIndex, setSelectedIndex] = useState(0);
13
+ const [scrollOffset, setScrollOffset] = useState(0);
14
+ const MAX_VISIBLE = 10;
15
+ useEffect(() => {
16
+ const fetchModels = async () => {
17
+ try {
18
+ const response = await fetch('https://openrouter.ai/api/v1/models');
19
+ if (!response.ok) {
20
+ throw new Error(`Failed to fetch models: ${response.status}`);
21
+ }
22
+ const data = await response.json();
23
+ const sortedModels = (data.data || []).sort((a, b) => {
24
+ const aFree = parseFloat(a.pricing.prompt) === 0 && parseFloat(a.pricing.completion) === 0;
25
+ const bFree = parseFloat(b.pricing.prompt) === 0 && parseFloat(b.pricing.completion) === 0;
26
+ if (aFree && !bFree)
27
+ return -1;
28
+ if (!aFree && bFree)
29
+ return 1;
30
+ return a.name.localeCompare(b.name);
31
+ });
32
+ setModels(sortedModels);
33
+ setIsLoading(false);
34
+ }
35
+ catch (err) {
36
+ setError(err instanceof Error ? err.message : 'Failed to fetch models');
37
+ setIsLoading(false);
38
+ }
39
+ };
40
+ fetchModels();
41
+ }, []);
42
+ const filteredModels = useMemo(() => {
43
+ if (!searchQuery.trim())
44
+ return models;
45
+ const query = searchQuery.toLowerCase();
46
+ return models.filter(model => model.id.toLowerCase().includes(query) ||
47
+ model.name.toLowerCase().includes(query) ||
48
+ model.description?.toLowerCase().includes(query));
49
+ }, [models, searchQuery]);
50
+ const isFreeModel = (model) => {
51
+ return parseFloat(model.pricing.prompt) === 0 && parseFloat(model.pricing.completion) === 0;
52
+ };
53
+ const formatPricing = (model) => {
54
+ const promptPrice = parseFloat(model.pricing.prompt);
55
+ const completionPrice = parseFloat(model.pricing.completion);
56
+ if (promptPrice === 0 && completionPrice === 0) {
57
+ return 'Free';
58
+ }
59
+ const promptPer1M = (promptPrice * 1000000).toFixed(2);
60
+ return `$${promptPer1M}/1M`;
61
+ };
62
+ const handleSelect = () => {
63
+ if (mode === 'manual') {
64
+ if (manualModelId.trim()) {
65
+ onSelect({
66
+ id: manualModelId.trim(),
67
+ name: manualModelId.trim(),
68
+ contextLength: 128000,
69
+ isFree: false,
70
+ });
71
+ }
72
+ }
73
+ else {
74
+ const model = filteredModels[selectedIndex];
75
+ if (model) {
76
+ onSelect({
77
+ id: model.id,
78
+ name: model.name,
79
+ contextLength: model.context_length,
80
+ isFree: isFreeModel(model),
81
+ });
82
+ }
83
+ }
84
+ };
85
+ useInput((_input, key) => {
86
+ if (key.escape) {
87
+ if (mode === 'search' || mode === 'manual') {
88
+ setMode('list');
89
+ setSearchQuery('');
90
+ setManualModelId('');
91
+ }
92
+ else {
93
+ onCancel();
94
+ }
95
+ return;
96
+ }
97
+ if (mode === 'list') {
98
+ if (key.upArrow) {
99
+ setSelectedIndex((prev) => {
100
+ const newIndex = prev > 0 ? prev - 1 : filteredModels.length - 1;
101
+ if (newIndex < scrollOffset) {
102
+ setScrollOffset(newIndex);
103
+ }
104
+ return newIndex;
105
+ });
106
+ }
107
+ else if (key.downArrow) {
108
+ setSelectedIndex((prev) => {
109
+ const newIndex = prev < filteredModels.length - 1 ? prev + 1 : 0;
110
+ if (newIndex >= scrollOffset + MAX_VISIBLE) {
111
+ setScrollOffset(newIndex - MAX_VISIBLE + 1);
112
+ }
113
+ else if (newIndex < scrollOffset) {
114
+ setScrollOffset(0);
115
+ }
116
+ return newIndex;
117
+ });
118
+ }
119
+ else if (key.return) {
120
+ handleSelect();
121
+ }
122
+ else if (_input === '/' || _input === 's') {
123
+ setMode('search');
124
+ setSelectedIndex(0);
125
+ setScrollOffset(0);
126
+ }
127
+ else if (_input === 'm') {
128
+ setMode('manual');
129
+ }
130
+ }
131
+ else if (mode === 'search') {
132
+ if (key.return) {
133
+ setMode('list');
134
+ setSelectedIndex(0);
135
+ setScrollOffset(0);
136
+ }
137
+ }
138
+ else if (mode === 'manual') {
139
+ if (key.return) {
140
+ handleSelect();
141
+ }
142
+ }
143
+ });
144
+ useEffect(() => {
145
+ if (selectedIndex >= filteredModels.length) {
146
+ setSelectedIndex(Math.max(0, filteredModels.length - 1));
147
+ }
148
+ }, [filteredModels.length, selectedIndex]);
149
+ if (isLoading) {
150
+ return (React.createElement(Box, { flexDirection: "column", paddingY: 1 },
151
+ React.createElement(Text, { color: "cyan" },
152
+ React.createElement(Spinner, { type: "dots" }),
153
+ " Loading OpenRouter models...")));
154
+ }
155
+ if (error) {
156
+ return (React.createElement(Box, { flexDirection: "column", paddingY: 1 },
157
+ React.createElement(Text, { color: "red" },
158
+ "Error: ",
159
+ error),
160
+ React.createElement(Text, { color: "gray" }, "Press 'm' to enter model ID manually, or Esc to go back")));
161
+ }
162
+ const visibleModels = filteredModels.slice(scrollOffset, scrollOffset + MAX_VISIBLE);
163
+ return (React.createElement(Box, { flexDirection: "column" },
164
+ React.createElement(Box, { borderStyle: "round", borderColor: "magenta", paddingX: 2, marginBottom: 1 },
165
+ React.createElement(Text, { color: "magenta", bold: true },
166
+ "OpenRouter Models (",
167
+ filteredModels.length,
168
+ " available)")),
169
+ mode === 'search' && (React.createElement(Box, { paddingX: 1, marginBottom: 1 },
170
+ React.createElement(Text, { color: "yellow" }, "Search: "),
171
+ React.createElement(TextInput, { value: searchQuery, onChange: setSearchQuery, placeholder: "Type to filter models..." }))),
172
+ mode === 'manual' && (React.createElement(Box, { flexDirection: "column", paddingX: 1 },
173
+ React.createElement(Text, { color: "yellow", bold: true }, "Enter Model ID:"),
174
+ React.createElement(Box, { marginY: 1 },
175
+ React.createElement(Text, { color: "gray" }, "ID: "),
176
+ React.createElement(TextInput, { value: manualModelId, onChange: setManualModelId, placeholder: "provider/model-name" })),
177
+ React.createElement(Text, { color: "gray", dimColor: true }, "Example: anthropic/claude-3.5-sonnet, openai/gpt-4-turbo"),
178
+ React.createElement(Box, { marginTop: 1 },
179
+ React.createElement(Text, { dimColor: true }, "Enter: confirm | Esc: back to list")))),
180
+ mode !== 'manual' && (React.createElement(React.Fragment, null,
181
+ searchQuery && mode === 'list' && (React.createElement(Box, { paddingX: 1, marginBottom: 1 },
182
+ React.createElement(Text, { color: "gray" },
183
+ "Filtered by: \"",
184
+ searchQuery,
185
+ "\" "),
186
+ React.createElement(Text, { color: "cyan" }, "(press / to search again)"))),
187
+ scrollOffset > 0 && (React.createElement(Box, { paddingX: 1 },
188
+ React.createElement(Text, { color: "gray" },
189
+ "\u2191 ",
190
+ scrollOffset,
191
+ " more above"))),
192
+ React.createElement(Box, { flexDirection: "column", paddingX: 1 }, visibleModels.map((model, index) => {
193
+ const actualIndex = scrollOffset + index;
194
+ const isSelected = actualIndex === selectedIndex;
195
+ const isFree = isFreeModel(model);
196
+ return (React.createElement(Box, { key: model.id, flexDirection: "row" },
197
+ React.createElement(Text, { color: isSelected ? 'cyan' : undefined }, isSelected ? '> ' : ' '),
198
+ React.createElement(Text, { color: isFree ? 'green' : 'yellow', bold: true }, isFree ? '[FREE] ' : '[PAID] '),
199
+ React.createElement(Text, { color: isSelected ? 'cyan' : 'white', bold: isSelected }, model.id),
200
+ React.createElement(Text, { color: "gray" },
201
+ ' ',
202
+ "(",
203
+ Math.round(model.context_length / 1000),
204
+ "k ctx)"),
205
+ !isFree && (React.createElement(Text, { color: "gray", dimColor: true },
206
+ ' ',
207
+ formatPricing(model)))));
208
+ })),
209
+ scrollOffset + MAX_VISIBLE < filteredModels.length && (React.createElement(Box, { paddingX: 1 },
210
+ React.createElement(Text, { color: "gray" },
211
+ "\u2193 ",
212
+ filteredModels.length - scrollOffset - MAX_VISIBLE,
213
+ " more below"))),
214
+ filteredModels.length === 0 && (React.createElement(Box, { paddingX: 1 },
215
+ React.createElement(Text, { color: "yellow" }, "No models match your search."))))),
216
+ mode === 'list' && (React.createElement(Box, { marginTop: 1, flexDirection: "column" },
217
+ React.createElement(Text, { dimColor: true }, "\u2191\u2193: navigate | Enter: select | /: search | m: manual entry | Esc: cancel"))),
218
+ mode === 'search' && (React.createElement(Box, { marginTop: 1 },
219
+ React.createElement(Text, { dimColor: true }, "Type to filter | Enter: apply | Esc: clear search")))));
220
+ };
221
+ //# sourceMappingURL=OpenRouterModelBrowser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"OpenRouterModelBrowser.js","sourceRoot":"","sources":["../../../src/ui/components/OpenRouterModelBrowser.tsx"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orquesta-cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Orquesta CLI - AI-powered coding assistant with team collaboration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -9,6 +9,8 @@ import { Message, TodoItem } from '../types/index.js';
9
9
  import { sessionManager } from './session/session-manager.js';
10
10
  import { usageTracker } from './usage-tracker.js';
11
11
  import { logger } from '../utils/logger.js';
12
+ import { fullSync } from '../orquesta/config-sync.js';
13
+ import { configManager } from './config/config-manager.js';
12
14
  // DISABLED: docs feature removed
13
15
  // import {
14
16
  // getDocsInfo,
@@ -225,6 +227,91 @@ export async function executeSlashCommand(
225
227
  };
226
228
  }
227
229
 
230
+ // Sync command - sync LLM configs from Orquesta dashboard
231
+ if (trimmedCommand === '/sync') {
232
+ logger.flow('Sync command received');
233
+
234
+ // Check if connected to Orquesta
235
+ const orquestaConfig = configManager.getOrquestaConfig();
236
+ if (!orquestaConfig?.token) {
237
+ const notConnectedMessage = `❌ Not connected to Orquesta.\n\nTo sync configurations, first connect using an Orquesta CLI token:\n1. Go to https://orquesta.live/dashboard/orquesta-cli\n2. Generate a CLI token\n3. Run orquesta-cli again and enter your token`;
238
+ const updatedMessages = [
239
+ ...context.messages,
240
+ { role: 'assistant' as const, content: notConnectedMessage },
241
+ ];
242
+ context.setMessages(updatedMessages);
243
+ return {
244
+ handled: true,
245
+ shouldContinue: false,
246
+ updatedContext: {
247
+ messages: updatedMessages,
248
+ },
249
+ };
250
+ }
251
+
252
+ // Perform full sync
253
+ const syncingMessage = `🔄 Syncing with Orquesta (${orquestaConfig.projectName || 'project'})...`;
254
+ const inProgressMessages = [
255
+ ...context.messages,
256
+ { role: 'assistant' as const, content: syncingMessage },
257
+ ];
258
+ context.setMessages(inProgressMessages);
259
+
260
+ try {
261
+ const { pullResult, pushResult } = await fullSync();
262
+
263
+ let resultMessage = '';
264
+
265
+ if (pullResult.success) {
266
+ if (pullResult.added > 0 || pullResult.updated > 0) {
267
+ resultMessage += `✅ Pulled from dashboard: ${pullResult.added} added, ${pullResult.updated} updated\n`;
268
+ } else {
269
+ resultMessage += `✅ Pulled from dashboard: No changes\n`;
270
+ }
271
+ } else {
272
+ resultMessage += `⚠️ Pull failed: ${pullResult.error}\n`;
273
+ }
274
+
275
+ if (pushResult.success) {
276
+ const pushed = pushResult.results?.length || 0;
277
+ if (pushed > 0) {
278
+ resultMessage += `✅ Pushed to dashboard: ${pushed} endpoint(s)`;
279
+ } else {
280
+ resultMessage += `✅ Pushed to dashboard: No local changes`;
281
+ }
282
+ } else {
283
+ resultMessage += `⚠️ Push failed: ${pushResult.error}`;
284
+ }
285
+
286
+ const updatedMessages = [
287
+ ...context.messages,
288
+ { role: 'assistant' as const, content: resultMessage },
289
+ ];
290
+ context.setMessages(updatedMessages);
291
+ return {
292
+ handled: true,
293
+ shouldContinue: false,
294
+ updatedContext: {
295
+ messages: updatedMessages,
296
+ },
297
+ };
298
+ } catch (error) {
299
+ const errorMessage = `❌ Sync failed: ${error instanceof Error ? error.message : 'Unknown error'}`;
300
+ const updatedMessages = [
301
+ ...context.messages,
302
+ { role: 'assistant' as const, content: errorMessage },
303
+ ];
304
+ context.setMessages(updatedMessages);
305
+ return {
306
+ handled: true,
307
+ shouldContinue: false,
308
+ updatedContext: {
309
+ messages: updatedMessages,
310
+ },
311
+ };
312
+ }
313
+ }
314
+
228
315
  // DISABLED: /docs command removed
229
316
  // The docs feature has been disabled
230
317
 
@@ -240,6 +327,7 @@ Available commands:
240
327
  /tool - Enable/disable optional tools (Browser, Background)
241
328
  /load - Load a saved session
242
329
  /usage - Show token usage statistics
330
+ /sync - Sync LLM configs with Orquesta dashboard
243
331
 
244
332
  Keyboard shortcuts:
245
333
  Ctrl+C - Exit
@@ -13,6 +13,8 @@ import { configManager } from '../../core/config/config-manager.js';
13
13
  import { LLMClient } from '../../core/llm/llm-client.js';
14
14
  import { EndpointConfig } from '../../types/index.js';
15
15
  import { fetchOrquestaProjects, syncOrquestaConfigs } from '../../orquesta/config-sync.js';
16
+ import { Logo } from './Logo.js';
17
+ import { OpenRouterModelBrowser } from './OpenRouterModelBrowser.js';
16
18
 
17
19
  interface LLMSetupWizardProps {
18
20
  onComplete: () => void;
@@ -28,9 +30,69 @@ interface FormData {
28
30
  maxContextLength: string;
29
31
  }
30
32
 
31
- type WizardStep = 'welcome' | 'token-input' | 'project-select' | 'manual-setup';
33
+ type WizardStep = 'welcome' | 'token-input' | 'project-select' | 'provider-select' | 'openrouter-browse' | 'manual-setup';
32
34
  type FormField = 'name' | 'baseUrl' | 'apiKey' | 'modelId' | 'modelName' | 'maxContextLength' | 'buttons';
33
35
 
36
+ interface ProviderPreset {
37
+ id: string;
38
+ name: string;
39
+ icon: string;
40
+ baseUrl: string;
41
+ requiresApiKey: boolean;
42
+ description: string;
43
+ }
44
+
45
+ const PROVIDER_PRESETS: ProviderPreset[] = [
46
+ {
47
+ id: 'openrouter',
48
+ name: 'OpenRouter',
49
+ icon: '🌐',
50
+ baseUrl: 'https://openrouter.ai/api/v1',
51
+ requiresApiKey: true,
52
+ description: 'Access 200+ models (GPT-4, Claude, Llama, etc.)',
53
+ },
54
+ {
55
+ id: 'ollama',
56
+ name: 'Ollama (Local)',
57
+ icon: '🦙',
58
+ baseUrl: 'http://localhost:11434/v1',
59
+ requiresApiKey: false,
60
+ description: 'Run models locally with Ollama',
61
+ },
62
+ {
63
+ id: 'openai',
64
+ name: 'OpenAI',
65
+ icon: '🤖',
66
+ baseUrl: 'https://api.openai.com/v1',
67
+ requiresApiKey: true,
68
+ description: 'GPT-4, GPT-4 Turbo, GPT-3.5',
69
+ },
70
+ {
71
+ id: 'anthropic',
72
+ name: 'Anthropic',
73
+ icon: '🧠',
74
+ baseUrl: 'https://api.anthropic.com/v1',
75
+ requiresApiKey: true,
76
+ description: 'Claude 3.5 Sonnet, Claude 3 Opus',
77
+ },
78
+ {
79
+ id: 'deepseek',
80
+ name: 'DeepSeek',
81
+ icon: '🔍',
82
+ baseUrl: 'https://api.deepseek.com/v1',
83
+ requiresApiKey: true,
84
+ description: 'DeepSeek Coder, DeepSeek Chat',
85
+ },
86
+ {
87
+ id: 'custom',
88
+ name: 'Custom Endpoint',
89
+ icon: '⚙️',
90
+ baseUrl: '',
91
+ requiresApiKey: false,
92
+ description: 'Configure any OpenAI-compatible API',
93
+ },
94
+ ];
95
+
34
96
  interface OrquestaProject {
35
97
  id: string;
36
98
  name: string;
@@ -55,6 +117,10 @@ export const LLMSetupWizard: React.FC<LLMSetupWizardProps> = ({ onComplete, onSk
55
117
  const [projectIndex, setProjectIndex] = useState(0);
56
118
  const [isSyncing, setIsSyncing] = useState(false);
57
119
 
120
+ // Provider selection state
121
+ const [providerIndex, setProviderIndex] = useState(0);
122
+ const [selectedProvider, setSelectedProvider] = useState<ProviderPreset | null>(null);
123
+
58
124
  // Manual setup form state
59
125
  const [formData, setFormData] = useState<FormData>({
60
126
  name: '',
@@ -86,14 +152,61 @@ export const LLMSetupWizard: React.FC<LLMSetupWizardProps> = ({ onComplete, onSk
86
152
  // "I have an Orquesta token"
87
153
  setStep('token-input');
88
154
  } else if (welcomeOptionIndex === 1) {
89
- // "Set up manually"
90
- setStep('manual-setup');
155
+ // "Set up manually" - go to provider selection
156
+ setStep('provider-select');
91
157
  } else {
92
158
  // "Skip for now"
93
159
  onSkip();
94
160
  }
95
161
  }, [welcomeOptionIndex, onSkip]);
96
162
 
163
+ // Handle provider selection
164
+ const handleProviderSelect = useCallback((provider: ProviderPreset) => {
165
+ setSelectedProvider(provider);
166
+
167
+ if (provider.id === 'openrouter') {
168
+ // Go to OpenRouter model browser
169
+ setStep('openrouter-browse');
170
+ } else if (provider.id === 'custom') {
171
+ // Go to full manual setup
172
+ setFormData({
173
+ name: '',
174
+ baseUrl: '',
175
+ apiKey: '',
176
+ modelId: '',
177
+ modelName: '',
178
+ maxContextLength: '128000',
179
+ });
180
+ setStep('manual-setup');
181
+ } else {
182
+ // Pre-fill form with provider info
183
+ setFormData({
184
+ name: provider.name,
185
+ baseUrl: provider.baseUrl,
186
+ apiKey: '',
187
+ modelId: '',
188
+ modelName: '',
189
+ maxContextLength: '128000',
190
+ });
191
+ setFormField(provider.requiresApiKey ? 'apiKey' : 'modelId');
192
+ setStep('manual-setup');
193
+ }
194
+ }, []);
195
+
196
+ // Handle OpenRouter model selection
197
+ const handleOpenRouterModelSelect = useCallback((model: { id: string; name: string; contextLength: number; isFree: boolean }) => {
198
+ setFormData({
199
+ name: 'OpenRouter',
200
+ baseUrl: 'https://openrouter.ai/api/v1',
201
+ apiKey: '',
202
+ modelId: model.id,
203
+ modelName: model.name,
204
+ maxContextLength: model.contextLength.toString(),
205
+ });
206
+ setFormField('apiKey');
207
+ setStep('manual-setup');
208
+ }, []);
209
+
97
210
  // Handle token validation
98
211
  const handleTokenSubmit = useCallback(async () => {
99
212
  if (!orquestaToken.trim()) {
@@ -338,6 +451,26 @@ export const LLMSetupWizard: React.FC<LLMSetupWizardProps> = ({ onComplete, onSk
338
451
  return;
339
452
  }
340
453
 
454
+ // Provider selection screen
455
+ if (step === 'provider-select') {
456
+ if (key.upArrow) {
457
+ setProviderIndex((prev) => (prev > 0 ? prev - 1 : PROVIDER_PRESETS.length - 1));
458
+ } else if (key.downArrow) {
459
+ setProviderIndex((prev) => (prev < PROVIDER_PRESETS.length - 1 ? prev + 1 : 0));
460
+ } else if (key.return) {
461
+ handleProviderSelect(PROVIDER_PRESETS[providerIndex]!);
462
+ } else if (key.escape) {
463
+ setStep('welcome');
464
+ }
465
+ return;
466
+ }
467
+
468
+ // OpenRouter model browser - handled by component itself
469
+ if (step === 'openrouter-browse') {
470
+ // Component handles its own input
471
+ return;
472
+ }
473
+
341
474
  // Manual setup form
342
475
  if (step === 'manual-setup') {
343
476
  if (key.tab) {
@@ -347,7 +480,12 @@ export const LLMSetupWizard: React.FC<LLMSetupWizardProps> = ({ onComplete, onSk
347
480
  } else if (key.return && formField === 'buttons') {
348
481
  handleFormSubmit();
349
482
  } else if (key.escape) {
350
- setStep('welcome');
483
+ // Go back to provider select or welcome
484
+ if (selectedProvider) {
485
+ setStep('provider-select');
486
+ } else {
487
+ setStep('welcome');
488
+ }
351
489
  }
352
490
  }
353
491
  });
@@ -356,28 +494,22 @@ export const LLMSetupWizard: React.FC<LLMSetupWizardProps> = ({ onComplete, onSk
356
494
  if (step === 'welcome') {
357
495
  return (
358
496
  <Box flexDirection="column">
497
+ {/* Logo with animation */}
498
+ <Logo
499
+ showVersion={true}
500
+ showTagline={true}
501
+ animate={true}
502
+ />
503
+
504
+ {/* Spacer */}
359
505
  <Text>{' '}</Text>
360
- <Text>{' '}</Text>
361
-
362
- {/* Header */}
363
- <Box borderStyle="double" borderColor="cyan" paddingX={2} marginBottom={1}>
364
- <Text color="cyan" bold>
365
- Welcome to Orquesta CLI!
366
- </Text>
367
- </Box>
368
-
369
- {/* Description */}
370
- <Box paddingX={1} marginBottom={1} flexDirection="column">
371
- <Text color="white">AI-powered coding assistant with local LLM support.</Text>
372
- <Text color="gray">Configure your LLM endpoint to get started.</Text>
373
- </Box>
374
506
 
375
507
  {/* Features */}
376
508
  <Box paddingX={1} marginBottom={1} flexDirection="column">
377
- <Text color="cyan">Features:</Text>
378
- <Text color="gray"> • Multi-LLM support (OpenAI, Anthropic, Ollama, etc.)</Text>
379
- <Text color="gray"> • Sync with Orquesta dashboard (optional)</Text>
380
- <Text color="gray"> • Works offline with local models</Text>
509
+ <Text color="white" bold>Get started with any LLM:</Text>
510
+ <Text color="gray"> • OpenAI, Anthropic, DeepSeek, OpenRouter</Text>
511
+ <Text color="gray"> • Local models via Ollama, vLLM, LM Studio</Text>
512
+ <Text color="gray"> • Sync configs with Orquesta dashboard</Text>
381
513
  </Box>
382
514
 
383
515
  {/* Options */}
@@ -403,7 +535,7 @@ export const LLMSetupWizard: React.FC<LLMSetupWizardProps> = ({ onComplete, onSk
403
535
  bold={welcomeOptionIndex === 2}
404
536
  >
405
537
  {welcomeOptionIndex === 2 ? '> ' : ' '}
406
- ⏭️ Skip for now (configure later via /settings)
538
+ ⏭️ Skip for now (configure later via /config)
407
539
  </Text>
408
540
  </Box>
409
541
 
@@ -526,6 +658,64 @@ export const LLMSetupWizard: React.FC<LLMSetupWizardProps> = ({ onComplete, onSk
526
658
  );
527
659
  }
528
660
 
661
+ // Render Provider Selection Screen
662
+ if (step === 'provider-select') {
663
+ return (
664
+ <Box flexDirection="column">
665
+ <Text>{' '}</Text>
666
+ <Text>{' '}</Text>
667
+
668
+ {/* Header */}
669
+ <Box borderStyle="double" borderColor="cyan" paddingX={2} marginBottom={1}>
670
+ <Text color="cyan" bold>
671
+ Select LLM Provider
672
+ </Text>
673
+ </Box>
674
+
675
+ {/* Instructions */}
676
+ <Box paddingX={1} marginBottom={1}>
677
+ <Text color="gray">Choose a provider to get started quickly:</Text>
678
+ </Box>
679
+
680
+ {/* Provider List */}
681
+ <Box paddingX={1} flexDirection="column">
682
+ {PROVIDER_PRESETS.map((provider, index) => (
683
+ <Box key={provider.id} flexDirection="column" marginBottom={index < PROVIDER_PRESETS.length - 1 ? 1 : 0}>
684
+ <Text
685
+ color={providerIndex === index ? 'cyan' : undefined}
686
+ bold={providerIndex === index}
687
+ >
688
+ {providerIndex === index ? '> ' : ' '}
689
+ {provider.icon} {provider.name}
690
+ </Text>
691
+ <Text color="gray" dimColor>
692
+ {' '}{provider.description}
693
+ </Text>
694
+ </Box>
695
+ ))}
696
+ </Box>
697
+
698
+ {/* Footer */}
699
+ <Box marginTop={2}>
700
+ <Text dimColor>↑↓: navigate | Enter: select | Esc: back</Text>
701
+ </Box>
702
+ </Box>
703
+ );
704
+ }
705
+
706
+ // Render OpenRouter Model Browser
707
+ if (step === 'openrouter-browse') {
708
+ return (
709
+ <Box flexDirection="column">
710
+ <Text>{' '}</Text>
711
+ <OpenRouterModelBrowser
712
+ onSelect={handleOpenRouterModelSelect}
713
+ onCancel={() => setStep('provider-select')}
714
+ />
715
+ </Box>
716
+ );
717
+ }
718
+
529
719
  // Render Manual Setup Form
530
720
  return (
531
721
  <Box flexDirection="column">