dexto 1.5.7 → 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 +3 -3
- package/dist/agents/agent-template.yml +2 -2
- package/dist/agents/coding-agent/README.md +10 -10
- package/dist/agents/coding-agent/coding-agent.yml +81 -80
- package/dist/agents/default-agent.yml +32 -47
- package/dist/agents/explore-agent/explore-agent.yml +3 -6
- package/dist/agents/image-editor-agent/image-editor-agent.yml +1 -1
- package/dist/agents/nano-banana-agent/nano-banana-agent.yml +1 -1
- package/dist/agents/podcast-agent/podcast-agent.yml +1 -1
- package/dist/agents/product-name-researcher/product-name-researcher.yml +1 -1
- package/dist/agents/sora-video-agent/sora-video-agent.yml +4 -6
- package/dist/agents/triage-demo/triage-agent.yml +1 -1
- package/dist/analytics/events.d.ts +2 -2
- package/dist/analytics/events.d.ts.map +1 -1
- package/dist/api/mcp/tool-aggregation-handler.d.ts +2 -2
- package/dist/api/server-hono.d.ts +2 -2
- package/dist/api/server-hono.d.ts.map +1 -1
- package/dist/api/server-hono.js +37 -60
- package/dist/cli/approval/cli-approval-handler.d.ts +10 -3
- package/dist/cli/approval/cli-approval-handler.d.ts.map +1 -1
- package/dist/cli/approval/cli-approval-handler.js +1 -1
- package/dist/cli/auth/constants.d.ts +4 -0
- package/dist/cli/auth/constants.d.ts.map +1 -1
- package/dist/cli/auth/constants.js +4 -0
- package/dist/cli/commands/auth/logout.js +2 -2
- package/dist/cli/commands/billing/status.d.ts +3 -1
- package/dist/cli/commands/billing/status.d.ts.map +1 -1
- package/dist/cli/commands/billing/status.js +23 -1
- package/dist/cli/commands/create-app.d.ts +1 -11
- package/dist/cli/commands/create-app.d.ts.map +1 -1
- package/dist/cli/commands/create-app.js +21 -545
- package/dist/cli/commands/create-image.d.ts.map +1 -1
- package/dist/cli/commands/create-image.js +54 -53
- package/dist/cli/commands/image.d.ts +52 -0
- package/dist/cli/commands/image.d.ts.map +1 -0
- package/dist/cli/commands/image.js +118 -0
- package/dist/cli/commands/index.d.ts +2 -1
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +3 -1
- package/dist/cli/commands/init-app.d.ts +4 -8
- package/dist/cli/commands/init-app.d.ts.map +1 -1
- package/dist/cli/commands/init-app.js +37 -161
- package/dist/cli/commands/interactive-commands/command-parser.d.ts +2 -0
- package/dist/cli/commands/interactive-commands/command-parser.d.ts.map +1 -1
- package/dist/cli/commands/interactive-commands/commands.d.ts +1 -1
- package/dist/cli/commands/interactive-commands/commands.d.ts.map +1 -1
- package/dist/cli/commands/interactive-commands/commands.js +2 -2
- package/dist/cli/commands/interactive-commands/general-commands.js +2 -2
- package/dist/cli/commands/interactive-commands/prompt-commands.d.ts.map +1 -1
- package/dist/cli/commands/interactive-commands/prompt-commands.js +13 -2
- package/dist/cli/commands/interactive-commands/session/index.d.ts +2 -1
- package/dist/cli/commands/interactive-commands/session/index.d.ts.map +1 -1
- package/dist/cli/commands/interactive-commands/session/index.js +2 -1
- package/dist/cli/commands/interactive-commands/session/session-commands.d.ts +2 -2
- package/dist/cli/commands/interactive-commands/session/session-commands.js +2 -2
- package/dist/cli/commands/interactive-commands/system/system-commands.d.ts.map +1 -1
- package/dist/cli/commands/interactive-commands/system/system-commands.js +7 -29
- package/dist/cli/commands/list-agents.d.ts.map +1 -1
- package/dist/cli/commands/list-agents.js +3 -2
- package/dist/cli/commands/plugin.d.ts +4 -4
- package/dist/cli/commands/setup.d.ts +5 -5
- package/dist/cli/commands/setup.d.ts.map +1 -1
- package/dist/cli/commands/setup.js +766 -207
- package/dist/cli/commands/sync-agents.d.ts +2 -12
- package/dist/cli/commands/sync-agents.d.ts.map +1 -1
- package/dist/cli/commands/sync-agents.js +2 -50
- package/dist/cli/ink-cli/InkCLIRefactored.d.ts +7 -1
- package/dist/cli/ink-cli/InkCLIRefactored.d.ts.map +1 -1
- package/dist/cli/ink-cli/InkCLIRefactored.js +17 -7
- package/dist/cli/ink-cli/components/ApprovalPrompt.d.ts +2 -2
- package/dist/cli/ink-cli/components/ApprovalPrompt.d.ts.map +1 -1
- package/dist/cli/ink-cli/components/ApprovalPrompt.js +15 -14
- package/dist/cli/ink-cli/components/BackgroundTasksPanel.d.ts +18 -0
- package/dist/cli/ink-cli/components/BackgroundTasksPanel.d.ts.map +1 -0
- package/dist/cli/ink-cli/components/BackgroundTasksPanel.js +48 -0
- package/dist/cli/ink-cli/components/ErrorBoundary.js +1 -1
- package/dist/cli/ink-cli/components/Footer.d.ts.map +1 -1
- package/dist/cli/ink-cli/components/Footer.js +5 -6
- package/dist/cli/ink-cli/components/ResourceAutocomplete.d.ts.map +1 -1
- package/dist/cli/ink-cli/components/ResourceAutocomplete.js +150 -41
- package/dist/cli/ink-cli/components/StatusBar.d.ts +3 -1
- package/dist/cli/ink-cli/components/StatusBar.d.ts.map +1 -1
- package/dist/cli/ink-cli/components/StatusBar.js +27 -7
- package/dist/cli/ink-cli/components/TodoPanel.js +1 -1
- package/dist/cli/ink-cli/components/chat/MessageItem.d.ts.map +1 -1
- package/dist/cli/ink-cli/components/chat/MessageItem.js +9 -5
- package/dist/cli/ink-cli/components/chat/styled-boxes/ConfigBox.js +1 -1
- package/dist/cli/ink-cli/components/modes/AlternateBufferCLI.d.ts +3 -1
- package/dist/cli/ink-cli/components/modes/AlternateBufferCLI.d.ts.map +1 -1
- package/dist/cli/ink-cli/components/modes/AlternateBufferCLI.js +3 -2
- package/dist/cli/ink-cli/components/modes/StaticCLI.d.ts +3 -1
- package/dist/cli/ink-cli/components/modes/StaticCLI.d.ts.map +1 -1
- package/dist/cli/ink-cli/components/modes/StaticCLI.js +3 -2
- package/dist/cli/ink-cli/components/overlays/ContextStatsOverlay.js +1 -1
- package/dist/cli/ink-cli/components/overlays/CustomModelWizard.d.ts.map +1 -1
- package/dist/cli/ink-cli/components/overlays/CustomModelWizard.js +8 -4
- package/dist/cli/ink-cli/components/overlays/LogLevelSelector.js +1 -1
- package/dist/cli/ink-cli/components/overlays/McpRemoveSelector.js +1 -1
- package/dist/cli/ink-cli/components/overlays/ModelSelectorRefactored.d.ts +1 -0
- package/dist/cli/ink-cli/components/overlays/ModelSelectorRefactored.d.ts.map +1 -1
- package/dist/cli/ink-cli/components/overlays/ModelSelectorRefactored.js +144 -41
- package/dist/cli/ink-cli/components/overlays/ToolBrowser.d.ts +2 -1
- package/dist/cli/ink-cli/components/overlays/ToolBrowser.d.ts.map +1 -1
- package/dist/cli/ink-cli/components/overlays/ToolBrowser.js +286 -44
- package/dist/cli/ink-cli/components/overlays/custom-model-wizard/provider-config.d.ts +9 -1
- package/dist/cli/ink-cli/components/overlays/custom-model-wizard/provider-config.d.ts.map +1 -1
- package/dist/cli/ink-cli/components/overlays/custom-model-wizard/provider-config.js +35 -9
- package/dist/cli/ink-cli/constants/tips.js +1 -1
- package/dist/cli/ink-cli/containers/InputContainer.d.ts +4 -0
- package/dist/cli/ink-cli/containers/InputContainer.d.ts.map +1 -1
- package/dist/cli/ink-cli/containers/InputContainer.js +30 -8
- package/dist/cli/ink-cli/containers/OverlayContainer.d.ts +2 -0
- package/dist/cli/ink-cli/containers/OverlayContainer.d.ts.map +1 -1
- package/dist/cli/ink-cli/containers/OverlayContainer.js +215 -59
- package/dist/cli/ink-cli/hooks/useAgentEvents.d.ts.map +1 -1
- package/dist/cli/ink-cli/hooks/useAgentEvents.js +73 -13
- package/dist/cli/ink-cli/hooks/useCLIState.d.ts.map +1 -1
- package/dist/cli/ink-cli/hooks/useCLIState.js +3 -0
- package/dist/cli/ink-cli/hooks/useInputOrchestrator.d.ts.map +1 -1
- package/dist/cli/ink-cli/hooks/useInputOrchestrator.js +8 -0
- package/dist/cli/ink-cli/hooks/useTokenCounter.d.ts.map +1 -1
- package/dist/cli/ink-cli/hooks/useTokenCounter.js +7 -4
- package/dist/cli/ink-cli/services/CommandService.d.ts +1 -1
- package/dist/cli/ink-cli/services/CommandService.d.ts.map +1 -1
- package/dist/cli/ink-cli/services/CommandService.js +2 -2
- package/dist/cli/ink-cli/services/processStream.d.ts +2 -2
- package/dist/cli/ink-cli/services/processStream.d.ts.map +1 -1
- package/dist/cli/ink-cli/services/processStream.js +55 -8
- package/dist/cli/ink-cli/state/initialState.d.ts.map +1 -1
- package/dist/cli/ink-cli/state/initialState.js +3 -0
- package/dist/cli/ink-cli/state/types.d.ts +11 -2
- package/dist/cli/ink-cli/state/types.d.ts.map +1 -1
- package/dist/cli/ink-cli/utils/llm-provider-display.d.ts +3 -0
- package/dist/cli/ink-cli/utils/llm-provider-display.d.ts.map +1 -0
- package/dist/cli/ink-cli/utils/llm-provider-display.js +22 -0
- package/dist/cli/ink-cli/utils/messageFormatting.d.ts +13 -9
- package/dist/cli/ink-cli/utils/messageFormatting.d.ts.map +1 -1
- package/dist/cli/ink-cli/utils/messageFormatting.js +106 -151
- package/dist/cli/ink-cli/utils/toolUtils.d.ts.map +1 -1
- package/dist/cli/ink-cli/utils/toolUtils.js +2 -9
- package/dist/cli/utils/config-validation.d.ts +11 -11
- package/dist/cli/utils/config-validation.d.ts.map +1 -1
- package/dist/cli/utils/config-validation.js +56 -290
- package/dist/cli/utils/dexto-auth-check.d.ts +7 -7
- package/dist/cli/utils/dexto-auth-check.d.ts.map +1 -1
- package/dist/cli/utils/dexto-auth-check.js +16 -16
- package/dist/cli/utils/image-store.d.ts +16 -0
- package/dist/cli/utils/image-store.d.ts.map +1 -0
- package/dist/cli/utils/image-store.js +289 -0
- package/dist/cli/utils/options.js +1 -1
- package/dist/cli/utils/provider-setup.d.ts +2 -2
- package/dist/cli/utils/provider-setup.d.ts.map +1 -1
- package/dist/cli/utils/provider-setup.js +10 -2
- package/dist/cli/utils/scaffolding-utils.d.ts +5 -0
- package/dist/cli/utils/scaffolding-utils.d.ts.map +1 -1
- package/dist/cli/utils/scaffolding-utils.js +46 -4
- package/dist/cli/utils/template-engine.d.ts +28 -16
- package/dist/cli/utils/template-engine.d.ts.map +1 -1
- package/dist/cli/utils/template-engine.js +339 -479
- package/dist/config/cli-overrides.d.ts +4 -3
- package/dist/config/cli-overrides.d.ts.map +1 -1
- package/dist/config/cli-overrides.js +8 -10
- package/dist/config/effective-llm.d.ts +4 -4
- package/dist/config/effective-llm.d.ts.map +1 -1
- package/dist/config/effective-llm.js +4 -4
- package/dist/index-main.d.ts +2 -0
- package/dist/index-main.d.ts.map +1 -0
- package/dist/index-main.js +1554 -0
- package/dist/index.js +2 -1580
- package/dist/utils/session-logger-factory.d.ts +3 -0
- package/dist/utils/session-logger-factory.d.ts.map +1 -0
- package/dist/utils/session-logger-factory.js +19 -0
- package/dist/webui/assets/{index-Dl3mj53P.js → index-DwtueA8l.js} +231 -231
- package/dist/webui/index.html +1 -1
- package/package.json +10 -7
- package/dist/cli/cli-subscriber.d.ts +0 -45
- package/dist/cli/cli-subscriber.d.ts.map +0 -1
- package/dist/cli/cli-subscriber.js +0 -204
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
// packages/cli/src/cli/commands/setup.ts
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
-
import
|
|
4
|
+
import open from 'open';
|
|
5
|
+
import { getDefaultModelForProvider, LLM_PROVIDERS, LLM_REGISTRY, isValidProviderModel, getSupportedModels, acceptsAnyModel, supportsCustomModels, requiresApiKey, isReasoningCapableModel, getCuratedModelsForProvider, } from '@dexto/core';
|
|
5
6
|
import { resolveApiKeyForProvider } from '@dexto/core';
|
|
6
|
-
import { createInitialPreferences, saveGlobalPreferences, loadGlobalPreferences, getGlobalPreferencesPath, updateGlobalPreferences, setActiveModel, isDextoAuthEnabled, } from '@dexto/agent-management';
|
|
7
|
+
import { createInitialPreferences, saveGlobalPreferences, loadGlobalPreferences, getGlobalPreferencesPath, updateGlobalPreferences, setActiveModel, isDextoAuthEnabled, loadCustomModels, saveCustomModel, deleteCustomModel, } from '@dexto/agent-management';
|
|
7
8
|
import { interactiveApiKeySetup, hasApiKeyConfigured } from '../utils/api-key-setup.js';
|
|
8
9
|
import { selectProvider, getProviderDisplayName, getProviderEnvVar, providerRequiresBaseURL, getDefaultModel, } from '../utils/provider-setup.js';
|
|
9
10
|
import { setupLocalModels, setupOllamaModels, hasSelectedModel, getModelFromResult, } from '../utils/local-model-setup.js';
|
|
10
11
|
import { requiresSetup } from '../utils/setup-utils.js';
|
|
11
12
|
import { canUseDextoProvider } from '../utils/dexto-setup.js';
|
|
12
13
|
import { handleBrowserLogin } from './auth/login.js';
|
|
14
|
+
import { loadAuth, getDextoApiClient } from '../auth/index.js';
|
|
15
|
+
import { DEXTO_CREDITS_URL } from '../auth/constants.js';
|
|
13
16
|
import * as p from '@clack/prompts';
|
|
14
17
|
import { logger } from '@dexto/core';
|
|
15
18
|
import { capture } from '../../analytics/index.js';
|
|
@@ -145,7 +148,7 @@ export async function handleSetupCommand(options) {
|
|
|
145
148
|
}
|
|
146
149
|
// Handle quick start
|
|
147
150
|
if (validated.quickStart) {
|
|
148
|
-
await handleQuickStart();
|
|
151
|
+
await handleQuickStart({ onCancel: 'exit' });
|
|
149
152
|
return;
|
|
150
153
|
}
|
|
151
154
|
// Handle interactive full setup
|
|
@@ -156,157 +159,243 @@ export async function handleSetupCommand(options) {
|
|
|
156
159
|
// Handle non-interactive setup with provided options
|
|
157
160
|
await handleNonInteractiveSetup(validated);
|
|
158
161
|
}
|
|
159
|
-
|
|
160
|
-
* Quick start flow - pick a free provider with minimal prompts
|
|
161
|
-
*/
|
|
162
|
-
async function handleQuickStart() {
|
|
162
|
+
async function handleQuickStart(options = { onCancel: 'exit' }) {
|
|
163
163
|
console.log(chalk.cyan('\n🚀 Quick Start\n'));
|
|
164
164
|
p.intro(chalk.cyan('Quick Setup'));
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if (
|
|
194
|
-
|
|
195
|
-
|
|
165
|
+
while (true) {
|
|
166
|
+
// Let user pick from popular free providers
|
|
167
|
+
const quickProvider = await p.select({
|
|
168
|
+
message: 'Choose a provider',
|
|
169
|
+
options: [
|
|
170
|
+
{
|
|
171
|
+
value: 'google',
|
|
172
|
+
label: `${chalk.green('●')} Google Gemini`,
|
|
173
|
+
hint: 'Free, 1M+ context (recommended)',
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
value: 'groq',
|
|
177
|
+
label: `${chalk.green('●')} Groq`,
|
|
178
|
+
hint: 'Free, ultra-fast',
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
value: 'openrouter',
|
|
182
|
+
label: `${chalk.green('●')} OpenRouter (Free)`,
|
|
183
|
+
hint: 'Use free-tier models via OpenRouter',
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
value: 'local',
|
|
187
|
+
label: `${chalk.cyan('●')} Local Models`,
|
|
188
|
+
hint: 'Free, private, runs on your machine',
|
|
189
|
+
},
|
|
190
|
+
{ value: '_back', label: chalk.gray('← Back'), hint: 'Return' },
|
|
191
|
+
],
|
|
192
|
+
});
|
|
193
|
+
if (p.isCancel(quickProvider) || quickProvider === '_back') {
|
|
194
|
+
if (options.onCancel === 'exit') {
|
|
195
|
+
p.cancel('Setup cancelled');
|
|
196
|
+
}
|
|
197
|
+
return 'cancelled';
|
|
196
198
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
+
// Handle local models with dedicated setup flow
|
|
200
|
+
if (quickProvider === 'local') {
|
|
201
|
+
const localResult = await setupLocalModels();
|
|
202
|
+
if (!hasSelectedModel(localResult)) {
|
|
203
|
+
if (options.onCancel === 'exit') {
|
|
204
|
+
p.cancel('Setup cancelled');
|
|
205
|
+
return 'cancelled';
|
|
206
|
+
}
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
const model = getModelFromResult(localResult);
|
|
210
|
+
// CLI mode confirmation for local
|
|
211
|
+
const useCli = await p.confirm({
|
|
212
|
+
message: 'Start in Terminal mode? (You can change this later)',
|
|
213
|
+
initialValue: true,
|
|
214
|
+
});
|
|
215
|
+
if (p.isCancel(useCli)) {
|
|
216
|
+
if (options.onCancel === 'exit') {
|
|
217
|
+
p.cancel('Setup cancelled');
|
|
218
|
+
return 'cancelled';
|
|
219
|
+
}
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const defaultMode = useCli ? 'cli' : await selectDefaultMode();
|
|
223
|
+
if (defaultMode === null) {
|
|
224
|
+
if (options.onCancel === 'exit') {
|
|
225
|
+
p.cancel('Setup cancelled');
|
|
226
|
+
return 'cancelled';
|
|
227
|
+
}
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
// Sync the active model for local provider
|
|
231
|
+
await setActiveModel(model);
|
|
232
|
+
const preferences = createInitialPreferences({
|
|
233
|
+
provider: 'local',
|
|
234
|
+
model,
|
|
235
|
+
defaultMode,
|
|
236
|
+
setupCompleted: true,
|
|
237
|
+
apiKeyPending: false,
|
|
238
|
+
});
|
|
239
|
+
await saveGlobalPreferences(preferences);
|
|
240
|
+
capture('dexto_setup', {
|
|
241
|
+
provider: 'local',
|
|
242
|
+
model,
|
|
243
|
+
setupMode: 'interactive',
|
|
244
|
+
setupVariant: 'quick-start',
|
|
245
|
+
defaultMode,
|
|
246
|
+
apiKeySkipped: false,
|
|
247
|
+
});
|
|
248
|
+
await showSetupComplete('local', model, defaultMode, false);
|
|
249
|
+
return 'completed';
|
|
250
|
+
}
|
|
251
|
+
// Cloud provider flow (google, groq, openrouter)
|
|
252
|
+
const provider = quickProvider;
|
|
253
|
+
let model;
|
|
254
|
+
if (provider === 'openrouter') {
|
|
255
|
+
const selected = await p.select({
|
|
256
|
+
message: 'Select a model for OpenRouter',
|
|
257
|
+
options: [
|
|
258
|
+
{
|
|
259
|
+
value: 'openrouter/free',
|
|
260
|
+
label: 'OpenRouter Free Models',
|
|
261
|
+
hint: 'Free-tier access via OpenRouter',
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
value: 'custom',
|
|
265
|
+
label: 'Enter a model ID',
|
|
266
|
+
hint: 'e.g., anthropic/claude-3.5-sonnet',
|
|
267
|
+
},
|
|
268
|
+
{ value: '_back', label: chalk.gray('← Back'), hint: 'Return' },
|
|
269
|
+
],
|
|
270
|
+
});
|
|
271
|
+
if (p.isCancel(selected) || selected === '_back') {
|
|
272
|
+
if (options.onCancel === 'exit') {
|
|
273
|
+
p.cancel('Setup cancelled');
|
|
274
|
+
return 'cancelled';
|
|
275
|
+
}
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (selected === 'openrouter/free') {
|
|
279
|
+
model = 'openrouter/free';
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
const modelInput = await p.text({
|
|
283
|
+
message: 'Enter model name for OpenRouter',
|
|
284
|
+
placeholder: 'e.g., anthropic/claude-3.5-sonnet',
|
|
285
|
+
validate: (value) => {
|
|
286
|
+
const trimmed = typeof value === 'string' ? value.trim() : '';
|
|
287
|
+
if (!trimmed)
|
|
288
|
+
return 'Model name is required';
|
|
289
|
+
return undefined;
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
if (p.isCancel(modelInput)) {
|
|
293
|
+
if (options.onCancel === 'exit') {
|
|
294
|
+
p.cancel('Setup cancelled');
|
|
295
|
+
return 'cancelled';
|
|
296
|
+
}
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
model = modelInput.trim();
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
model =
|
|
304
|
+
getDefaultModelForProvider(provider) ||
|
|
305
|
+
(provider === 'google' ? 'gemini-2.5-pro' : 'llama-3.3-70b-versatile');
|
|
306
|
+
}
|
|
307
|
+
const apiKeyVar = getProviderEnvVar(provider);
|
|
308
|
+
let apiKeySkipped = false;
|
|
309
|
+
// Check if API key exists
|
|
310
|
+
const hasKey = hasApiKeyConfigured(provider);
|
|
311
|
+
if (!hasKey) {
|
|
312
|
+
const providerName = getProviderDisplayName(provider);
|
|
313
|
+
p.note(`${providerName} is ${chalk.green('free')} to use!\n\n` +
|
|
314
|
+
`We'll help you get an API key in just a few seconds.`, 'Free AI Access');
|
|
315
|
+
const result = await interactiveApiKeySetup(provider, {
|
|
316
|
+
exitOnCancel: false, // Don't exit - allow skipping
|
|
317
|
+
model,
|
|
318
|
+
});
|
|
319
|
+
if (result.cancelled) {
|
|
320
|
+
if (options.onCancel === 'exit') {
|
|
321
|
+
p.cancel('Setup cancelled');
|
|
322
|
+
return 'cancelled';
|
|
323
|
+
}
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (result.skipped || !result.success) {
|
|
327
|
+
apiKeySkipped = true;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
p.log.success(`API key for ${getProviderDisplayName(provider)} already configured`);
|
|
332
|
+
}
|
|
333
|
+
// CLI mode confirmation
|
|
199
334
|
const useCli = await p.confirm({
|
|
200
335
|
message: 'Start in Terminal mode? (You can change this later)',
|
|
201
336
|
initialValue: true,
|
|
202
337
|
});
|
|
203
338
|
if (p.isCancel(useCli)) {
|
|
204
|
-
|
|
205
|
-
|
|
339
|
+
if (options.onCancel === 'exit') {
|
|
340
|
+
p.cancel('Setup cancelled');
|
|
341
|
+
return 'cancelled';
|
|
342
|
+
}
|
|
343
|
+
continue;
|
|
206
344
|
}
|
|
207
345
|
const defaultMode = useCli ? 'cli' : await selectDefaultMode();
|
|
346
|
+
// Handle cancellation
|
|
208
347
|
if (defaultMode === null) {
|
|
209
|
-
|
|
210
|
-
|
|
348
|
+
if (options.onCancel === 'exit') {
|
|
349
|
+
p.cancel('Setup cancelled');
|
|
350
|
+
return 'cancelled';
|
|
351
|
+
}
|
|
352
|
+
continue;
|
|
211
353
|
}
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
provider: 'local',
|
|
354
|
+
// Save preferences
|
|
355
|
+
const preferencesOptions = {
|
|
356
|
+
provider,
|
|
216
357
|
model,
|
|
217
358
|
defaultMode,
|
|
218
359
|
setupCompleted: true,
|
|
219
|
-
apiKeyPending:
|
|
220
|
-
}
|
|
360
|
+
apiKeyPending: apiKeySkipped,
|
|
361
|
+
};
|
|
362
|
+
// Only include apiKeyVar if not skipped
|
|
363
|
+
if (!apiKeySkipped) {
|
|
364
|
+
preferencesOptions.apiKeyVar = apiKeyVar;
|
|
365
|
+
}
|
|
366
|
+
const preferences = createInitialPreferences(preferencesOptions);
|
|
221
367
|
await saveGlobalPreferences(preferences);
|
|
222
368
|
capture('dexto_setup', {
|
|
223
|
-
provider
|
|
369
|
+
provider,
|
|
224
370
|
model,
|
|
225
371
|
setupMode: 'interactive',
|
|
226
372
|
setupVariant: 'quick-start',
|
|
227
373
|
defaultMode,
|
|
228
|
-
apiKeySkipped
|
|
229
|
-
});
|
|
230
|
-
await showSetupComplete('local', model, defaultMode, false);
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
// Cloud provider flow (google or groq)
|
|
234
|
-
const provider = quickProvider;
|
|
235
|
-
const model = getDefaultModelForProvider(provider) ||
|
|
236
|
-
(provider === 'google' ? 'gemini-2.5-pro' : 'llama-3.3-70b-versatile');
|
|
237
|
-
const apiKeyVar = getProviderEnvVar(provider);
|
|
238
|
-
let apiKeySkipped = false;
|
|
239
|
-
// Check if API key exists
|
|
240
|
-
const hasKey = hasApiKeyConfigured(provider);
|
|
241
|
-
if (!hasKey) {
|
|
242
|
-
const providerName = getProviderDisplayName(provider);
|
|
243
|
-
p.note(`${providerName} is ${chalk.green('free')} to use!\n\n` +
|
|
244
|
-
`We'll help you get an API key in just a few seconds.`, 'Free AI Access');
|
|
245
|
-
const result = await interactiveApiKeySetup(provider, {
|
|
246
|
-
exitOnCancel: false, // Don't exit - allow skipping
|
|
247
|
-
model,
|
|
374
|
+
apiKeySkipped,
|
|
248
375
|
});
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
process.exit(0);
|
|
252
|
-
}
|
|
253
|
-
if (result.skipped || !result.success) {
|
|
254
|
-
apiKeySkipped = true;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
else {
|
|
258
|
-
p.log.success(`API key for ${getProviderDisplayName(provider)} already configured`);
|
|
259
|
-
}
|
|
260
|
-
// CLI mode confirmation
|
|
261
|
-
const useCli = await p.confirm({
|
|
262
|
-
message: 'Start in Terminal mode? (You can change this later)',
|
|
263
|
-
initialValue: true,
|
|
264
|
-
});
|
|
265
|
-
if (p.isCancel(useCli)) {
|
|
266
|
-
p.cancel('Setup cancelled');
|
|
267
|
-
process.exit(0);
|
|
268
|
-
}
|
|
269
|
-
const defaultMode = useCli ? 'cli' : await selectDefaultMode();
|
|
270
|
-
// Handle cancellation
|
|
271
|
-
if (defaultMode === null) {
|
|
272
|
-
p.cancel('Setup cancelled');
|
|
273
|
-
process.exit(0);
|
|
274
|
-
}
|
|
275
|
-
// Save preferences
|
|
276
|
-
const preferencesOptions = {
|
|
277
|
-
provider,
|
|
278
|
-
model,
|
|
279
|
-
defaultMode,
|
|
280
|
-
setupCompleted: true,
|
|
281
|
-
apiKeyPending: apiKeySkipped,
|
|
282
|
-
};
|
|
283
|
-
// Only include apiKeyVar if not skipped
|
|
284
|
-
if (!apiKeySkipped) {
|
|
285
|
-
preferencesOptions.apiKeyVar = apiKeyVar;
|
|
376
|
+
await showSetupComplete(provider, model, defaultMode, apiKeySkipped);
|
|
377
|
+
return 'completed';
|
|
286
378
|
}
|
|
287
|
-
const preferences = createInitialPreferences(preferencesOptions);
|
|
288
|
-
await saveGlobalPreferences(preferences);
|
|
289
|
-
capture('dexto_setup', {
|
|
290
|
-
provider,
|
|
291
|
-
model,
|
|
292
|
-
setupMode: 'interactive',
|
|
293
|
-
setupVariant: 'quick-start',
|
|
294
|
-
defaultMode,
|
|
295
|
-
apiKeySkipped,
|
|
296
|
-
});
|
|
297
|
-
await showSetupComplete(provider, model, defaultMode, apiKeySkipped);
|
|
298
379
|
}
|
|
299
380
|
/**
|
|
300
381
|
* Dexto setup flow - login if needed, select model, save preferences
|
|
301
382
|
*
|
|
302
383
|
* Config storage:
|
|
303
|
-
* - provider: 'dexto' (the gateway provider)
|
|
384
|
+
* - provider: 'dexto-nova' (the gateway provider)
|
|
304
385
|
* - model: OpenRouter-style ID (e.g., 'anthropic/claude-haiku-4.5')
|
|
305
386
|
*
|
|
306
387
|
* Runtime handles routing requests through the Dexto gateway to the underlying provider.
|
|
307
388
|
*/
|
|
308
|
-
async function handleDextoProviderSetup() {
|
|
309
|
-
|
|
389
|
+
async function handleDextoProviderSetup(options = {}) {
|
|
390
|
+
const exitOnCancel = options.exitOnCancel ?? true;
|
|
391
|
+
const abort = (message, exitCode = 0) => {
|
|
392
|
+
p.cancel(message);
|
|
393
|
+
if (exitOnCancel) {
|
|
394
|
+
process.exit(exitCode);
|
|
395
|
+
}
|
|
396
|
+
return false;
|
|
397
|
+
};
|
|
398
|
+
console.log(chalk.magenta('\n★ Dexto Nova Setup\n'));
|
|
310
399
|
// Check if user already has DEXTO_API_KEY
|
|
311
400
|
const hasKey = await canUseDextoProvider();
|
|
312
401
|
if (!hasKey) {
|
|
@@ -317,32 +406,53 @@ async function handleDextoProviderSetup() {
|
|
|
317
406
|
initialValue: true,
|
|
318
407
|
});
|
|
319
408
|
if (p.isCancel(shouldLogin) || !shouldLogin) {
|
|
320
|
-
|
|
321
|
-
process.exit(0);
|
|
409
|
+
return abort('Setup cancelled');
|
|
322
410
|
}
|
|
323
411
|
try {
|
|
324
412
|
await handleBrowserLogin();
|
|
325
413
|
// Verify key was actually provisioned (provisionKeys silently catches errors)
|
|
326
414
|
if (!(await canUseDextoProvider())) {
|
|
327
415
|
p.log.error('API key provisioning failed. Please try again or use `dexto setup` with a different provider.');
|
|
328
|
-
|
|
416
|
+
return abort('Setup cancelled', 1);
|
|
329
417
|
}
|
|
330
418
|
p.log.success('Login successful! Continuing with setup...');
|
|
331
419
|
}
|
|
332
420
|
catch (error) {
|
|
333
421
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
334
422
|
p.log.error(`Login failed: ${errorMessage}`);
|
|
335
|
-
|
|
336
|
-
process.exit(1);
|
|
423
|
+
return abort('Setup cancelled - login required for Dexto', 1);
|
|
337
424
|
}
|
|
338
425
|
}
|
|
339
426
|
else {
|
|
340
|
-
|
|
427
|
+
const auth = await loadAuth();
|
|
428
|
+
const userLabel = auth?.email || auth?.userId || 'unknown';
|
|
429
|
+
p.log.success(`Logged in to Dexto as: ${userLabel}`);
|
|
430
|
+
}
|
|
431
|
+
const balance = await getCreditsBalance();
|
|
432
|
+
if (balance !== null) {
|
|
433
|
+
p.note(`$${balance.toFixed(2)} remaining`, 'Dexto Nova balance');
|
|
434
|
+
}
|
|
435
|
+
const shouldOpenCredits = await p.confirm({
|
|
436
|
+
message: 'Want to buy or top up Dexto Nova credits now?',
|
|
437
|
+
initialValue: false,
|
|
438
|
+
});
|
|
439
|
+
if (p.isCancel(shouldOpenCredits)) {
|
|
440
|
+
return abort('Setup cancelled');
|
|
441
|
+
}
|
|
442
|
+
if (shouldOpenCredits) {
|
|
443
|
+
await openCreditsPage();
|
|
444
|
+
const continueSetup = await p.confirm({
|
|
445
|
+
message: 'Continue choosing a model?',
|
|
446
|
+
initialValue: true,
|
|
447
|
+
});
|
|
448
|
+
if (p.isCancel(continueSetup) || !continueSetup) {
|
|
449
|
+
return abort('Setup cancelled');
|
|
450
|
+
}
|
|
341
451
|
}
|
|
342
452
|
// Model selection - show popular models in OpenRouter format
|
|
343
453
|
// NOTE: This list is intentionally hardcoded (not from registry) to include
|
|
344
454
|
// curated hints for onboarding UX. Keep model IDs in sync with:
|
|
345
|
-
// packages/core/src/llm/registry.ts (LLM_REGISTRY
|
|
455
|
+
// packages/core/src/llm/registry/index.ts (LLM_REGISTRY['dexto-nova'].models)
|
|
346
456
|
const model = await p.select({
|
|
347
457
|
message: 'Select a model to start with',
|
|
348
458
|
options: [
|
|
@@ -406,24 +516,27 @@ async function handleDextoProviderSetup() {
|
|
|
406
516
|
label: 'Minimax M2.1',
|
|
407
517
|
hint: 'Fast model with 196k context',
|
|
408
518
|
},
|
|
519
|
+
{
|
|
520
|
+
value: 'moonshotai/kimi-k2.5',
|
|
521
|
+
label: 'Kimi K2.5',
|
|
522
|
+
hint: 'Multimodal coding model, 262k context',
|
|
523
|
+
},
|
|
409
524
|
],
|
|
410
525
|
});
|
|
411
526
|
if (p.isCancel(model)) {
|
|
412
|
-
|
|
413
|
-
process.exit(0);
|
|
527
|
+
return abort('Setup cancelled');
|
|
414
528
|
}
|
|
415
|
-
// Dexto setup always uses 'dexto' provider with OpenRouter model IDs
|
|
416
|
-
const provider = 'dexto';
|
|
529
|
+
// Dexto setup always uses 'dexto-nova' provider with OpenRouter model IDs
|
|
530
|
+
const provider = 'dexto-nova';
|
|
417
531
|
// Cast model to string (prompts library typing)
|
|
418
532
|
const selectedModel = model;
|
|
419
533
|
p.log.info(`${chalk.dim('Tip:')} You can switch models anytime with ${chalk.cyan('/model')}`);
|
|
420
534
|
// Ask about default mode
|
|
421
535
|
const defaultMode = await selectDefaultMode();
|
|
422
536
|
if (defaultMode === null) {
|
|
423
|
-
|
|
424
|
-
process.exit(0);
|
|
537
|
+
return abort('Setup cancelled');
|
|
425
538
|
}
|
|
426
|
-
// Save preferences with explicit dexto provider and OpenRouter model ID
|
|
539
|
+
// Save preferences with explicit dexto-nova provider and OpenRouter model ID
|
|
427
540
|
const preferences = createInitialPreferences({
|
|
428
541
|
provider,
|
|
429
542
|
model: selectedModel,
|
|
@@ -437,17 +550,41 @@ async function handleDextoProviderSetup() {
|
|
|
437
550
|
provider,
|
|
438
551
|
model: selectedModel,
|
|
439
552
|
setupMode: 'interactive',
|
|
440
|
-
setupVariant: 'dexto',
|
|
553
|
+
setupVariant: 'dexto-nova',
|
|
441
554
|
defaultMode,
|
|
442
555
|
});
|
|
443
556
|
await showSetupComplete(provider, selectedModel, defaultMode, false);
|
|
557
|
+
return true;
|
|
558
|
+
}
|
|
559
|
+
async function openCreditsPage() {
|
|
560
|
+
try {
|
|
561
|
+
await open(DEXTO_CREDITS_URL);
|
|
562
|
+
}
|
|
563
|
+
catch (error) {
|
|
564
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
565
|
+
p.log.warn(`Unable to open browser: ${errorMessage}`);
|
|
566
|
+
p.log.info(`Open this link to buy credits: ${DEXTO_CREDITS_URL}`);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
async function getCreditsBalance() {
|
|
570
|
+
try {
|
|
571
|
+
const auth = await loadAuth();
|
|
572
|
+
if (!auth?.dextoApiKey)
|
|
573
|
+
return null;
|
|
574
|
+
const apiClient = getDextoApiClient();
|
|
575
|
+
const usage = await apiClient.getUsageSummary(auth.dextoApiKey);
|
|
576
|
+
return usage.credits_usd;
|
|
577
|
+
}
|
|
578
|
+
catch {
|
|
579
|
+
return null;
|
|
580
|
+
}
|
|
444
581
|
}
|
|
445
582
|
/**
|
|
446
583
|
* Full interactive setup flow with wizard navigation.
|
|
447
584
|
* Users can go back to previous steps to change their selections.
|
|
448
585
|
*/
|
|
449
586
|
async function handleInteractiveSetup(_options) {
|
|
450
|
-
console.log(chalk.cyan('\n🗿 Dexto Setup\n'));
|
|
587
|
+
console.log(chalk.cyan('\n🗿 Dexto Nova Setup\n'));
|
|
451
588
|
p.intro(chalk.cyan("Let's configure your AI agent"));
|
|
452
589
|
// Initialize wizard state
|
|
453
590
|
let state = { step: 'setupType' };
|
|
@@ -484,12 +621,12 @@ async function handleInteractiveSetup(_options) {
|
|
|
484
621
|
* Wizard Step: Setup Type (Quick Start vs Custom)
|
|
485
622
|
*/
|
|
486
623
|
async function wizardStepSetupType(state) {
|
|
487
|
-
// Build options list - only show Dexto
|
|
624
|
+
// Build options list - only show Dexto Nova when feature is enabled
|
|
488
625
|
const options = [];
|
|
489
626
|
if (isDextoAuthEnabled()) {
|
|
490
627
|
options.push({
|
|
491
|
-
value: 'dexto',
|
|
492
|
-
label: `${chalk.magenta('★')} Dexto
|
|
628
|
+
value: 'dexto-nova',
|
|
629
|
+
label: `${chalk.magenta('★')} Dexto Nova`,
|
|
493
630
|
hint: 'All models, one account - login to get started (recommended)',
|
|
494
631
|
});
|
|
495
632
|
}
|
|
@@ -510,14 +647,17 @@ async function wizardStepSetupType(state) {
|
|
|
510
647
|
p.cancel('Setup cancelled');
|
|
511
648
|
process.exit(0);
|
|
512
649
|
}
|
|
513
|
-
if (setupType === 'dexto') {
|
|
514
|
-
// Handle Dexto
|
|
650
|
+
if (setupType === 'dexto-nova') {
|
|
651
|
+
// Handle Dexto Nova flow - login if needed, then proceed to model selection
|
|
515
652
|
await handleDextoProviderSetup();
|
|
516
653
|
return { ...state, step: 'complete', quickStartHandled: true };
|
|
517
654
|
}
|
|
518
655
|
if (setupType === 'quick') {
|
|
519
656
|
// Quick start bypasses the wizard - handle it directly
|
|
520
|
-
await handleQuickStart();
|
|
657
|
+
const result = await handleQuickStart({ onCancel: 'back' });
|
|
658
|
+
if (result === 'cancelled') {
|
|
659
|
+
return { ...state, step: 'setupType' };
|
|
660
|
+
}
|
|
521
661
|
return { ...state, step: 'complete', quickStartHandled: true };
|
|
522
662
|
}
|
|
523
663
|
return { ...state, step: 'provider', setupType: 'custom' };
|
|
@@ -529,8 +669,9 @@ async function wizardStepProvider(state) {
|
|
|
529
669
|
showStepProgress('provider', state.provider);
|
|
530
670
|
const provider = await selectProvider();
|
|
531
671
|
if (provider === null) {
|
|
532
|
-
|
|
533
|
-
|
|
672
|
+
// Treat prompt cancellation as "back" to avoid accidentally exiting the setup wizard.
|
|
673
|
+
// Users can still cancel setup from the initial setup type step.
|
|
674
|
+
return { ...state, step: 'setupType', provider: undefined };
|
|
534
675
|
}
|
|
535
676
|
if (provider === '_back') {
|
|
536
677
|
return { ...state, step: 'setupType', provider: undefined };
|
|
@@ -578,10 +719,11 @@ async function wizardStepModel(state) {
|
|
|
578
719
|
baseURL = result;
|
|
579
720
|
}
|
|
580
721
|
// Cloud provider model selection with back option
|
|
581
|
-
const
|
|
582
|
-
if (
|
|
722
|
+
const selection = await selectModelWithBack(provider);
|
|
723
|
+
if (selection === '_back') {
|
|
583
724
|
return { ...state, step: 'provider', model: undefined, baseURL: undefined };
|
|
584
725
|
}
|
|
726
|
+
const model = selection.model;
|
|
585
727
|
// Check if model supports reasoning effort
|
|
586
728
|
const nextStep = isReasoningCapableModel(model) ? 'reasoningEffort' : 'apiKey';
|
|
587
729
|
return { ...state, step: nextStep, model, baseURL };
|
|
@@ -619,8 +761,8 @@ async function wizardStepReasoningEffort(state) {
|
|
|
619
761
|
],
|
|
620
762
|
});
|
|
621
763
|
if (p.isCancel(result)) {
|
|
622
|
-
|
|
623
|
-
|
|
764
|
+
// Treat prompt cancellation as "back" to avoid accidentally exiting the setup wizard.
|
|
765
|
+
return { ...state, step: 'model', reasoningEffort: undefined };
|
|
624
766
|
}
|
|
625
767
|
if (result === '_back') {
|
|
626
768
|
return { ...state, step: 'model', reasoningEffort: undefined };
|
|
@@ -690,22 +832,103 @@ async function wizardStepMode(state) {
|
|
|
690
832
|
async function selectModelWithBack(provider) {
|
|
691
833
|
const providerInfo = LLM_REGISTRY[provider];
|
|
692
834
|
if (providerInfo?.models && providerInfo.models.length > 0) {
|
|
693
|
-
const
|
|
835
|
+
const curatedModels = getCuratedModelsForProvider(provider);
|
|
836
|
+
if (provider === 'openrouter') {
|
|
837
|
+
const curatedOptions = curatedModels
|
|
838
|
+
.slice(0, 8)
|
|
839
|
+
.filter((m) => m.name !== 'openrouter/free')
|
|
840
|
+
.map((m) => ({
|
|
841
|
+
value: m.name,
|
|
842
|
+
label: m.displayName || m.name,
|
|
843
|
+
}));
|
|
844
|
+
if (supportsCustomModels(provider)) {
|
|
845
|
+
p.log.info(chalk.gray('Tip: You can add or edit custom models via /model'));
|
|
846
|
+
const manageCustomModels = await p.confirm({
|
|
847
|
+
message: 'Manage custom models now?',
|
|
848
|
+
initialValue: false,
|
|
849
|
+
});
|
|
850
|
+
if (p.isCancel(manageCustomModels)) {
|
|
851
|
+
return '_back';
|
|
852
|
+
}
|
|
853
|
+
if (manageCustomModels) {
|
|
854
|
+
const customModel = await handleCustomModelManagement(provider);
|
|
855
|
+
if (customModel) {
|
|
856
|
+
return { model: customModel, isCustomSelection: true };
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
const result = await p.select({
|
|
861
|
+
message: `Select a model for ${getProviderDisplayName(provider)}`,
|
|
862
|
+
options: [
|
|
863
|
+
{
|
|
864
|
+
value: 'openrouter/free',
|
|
865
|
+
label: 'OpenRouter Free Models',
|
|
866
|
+
hint: '(recommended)',
|
|
867
|
+
},
|
|
868
|
+
...curatedOptions,
|
|
869
|
+
{
|
|
870
|
+
value: '_back',
|
|
871
|
+
label: chalk.gray('← Back'),
|
|
872
|
+
hint: 'Change provider',
|
|
873
|
+
},
|
|
874
|
+
],
|
|
875
|
+
});
|
|
876
|
+
if (p.isCancel(result)) {
|
|
877
|
+
return '_back';
|
|
878
|
+
}
|
|
879
|
+
return { model: result };
|
|
880
|
+
}
|
|
881
|
+
const defaultModel = curatedModels.find((m) => m.default) ??
|
|
882
|
+
providerInfo.models.find((m) => m.default) ??
|
|
883
|
+
curatedModels[0] ??
|
|
884
|
+
providerInfo.models[0];
|
|
885
|
+
if (!defaultModel) {
|
|
886
|
+
p.log.warn('No models available for this provider');
|
|
887
|
+
return '_back';
|
|
888
|
+
}
|
|
889
|
+
const curatedOptions = curatedModels
|
|
890
|
+
.slice(0, 8)
|
|
891
|
+
.filter((m) => m.name !== defaultModel.name)
|
|
892
|
+
.map((m) => ({
|
|
694
893
|
value: m.name,
|
|
695
894
|
label: m.displayName || m.name,
|
|
696
895
|
}));
|
|
896
|
+
if (supportsCustomModels(provider)) {
|
|
897
|
+
p.log.info(chalk.gray('Tip: You can add or edit custom models via /model'));
|
|
898
|
+
const manageCustomModels = await p.confirm({
|
|
899
|
+
message: 'Manage custom models now?',
|
|
900
|
+
initialValue: false,
|
|
901
|
+
});
|
|
902
|
+
if (p.isCancel(manageCustomModels)) {
|
|
903
|
+
return '_back';
|
|
904
|
+
}
|
|
905
|
+
if (manageCustomModels) {
|
|
906
|
+
const customModel = await handleCustomModelManagement(provider);
|
|
907
|
+
if (customModel) {
|
|
908
|
+
return { model: customModel, isCustomSelection: true };
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
697
912
|
const result = await p.select({
|
|
698
913
|
message: `Select a model for ${getProviderDisplayName(provider)}`,
|
|
699
914
|
options: [
|
|
700
|
-
|
|
701
|
-
|
|
915
|
+
{
|
|
916
|
+
value: defaultModel.name,
|
|
917
|
+
label: defaultModel.displayName || defaultModel.name,
|
|
918
|
+
hint: '(recommended)',
|
|
919
|
+
},
|
|
920
|
+
...curatedOptions,
|
|
921
|
+
{
|
|
922
|
+
value: '_back',
|
|
923
|
+
label: chalk.gray('← Back'),
|
|
924
|
+
hint: 'Change provider',
|
|
925
|
+
},
|
|
702
926
|
],
|
|
703
927
|
});
|
|
704
928
|
if (p.isCancel(result)) {
|
|
705
|
-
|
|
706
|
-
process.exit(0);
|
|
929
|
+
return '_back';
|
|
707
930
|
}
|
|
708
|
-
return result;
|
|
931
|
+
return { model: result };
|
|
709
932
|
}
|
|
710
933
|
// For providers that accept any model, show text input with back hint
|
|
711
934
|
p.log.info(chalk.gray('Press Ctrl+C to go back'));
|
|
@@ -714,7 +937,8 @@ async function selectModelWithBack(provider) {
|
|
|
714
937
|
message: `Enter model name for ${getProviderDisplayName(provider)}`,
|
|
715
938
|
placeholder: defaultModel || 'e.g., gpt-4-turbo',
|
|
716
939
|
validate: (value) => {
|
|
717
|
-
|
|
940
|
+
const trimmed = typeof value === 'string' ? value.trim() : '';
|
|
941
|
+
if (!trimmed)
|
|
718
942
|
return 'Model name is required';
|
|
719
943
|
return undefined;
|
|
720
944
|
},
|
|
@@ -722,11 +946,255 @@ async function selectModelWithBack(provider) {
|
|
|
722
946
|
if (p.isCancel(model)) {
|
|
723
947
|
return '_back';
|
|
724
948
|
}
|
|
725
|
-
return model;
|
|
949
|
+
return { model: typeof model === 'string' ? model.trim() : '' };
|
|
726
950
|
}
|
|
727
951
|
/**
|
|
728
952
|
* Select default mode with back option
|
|
729
953
|
*/
|
|
954
|
+
async function handleCustomModelManagement(providerOverride) {
|
|
955
|
+
const models = await loadCustomModels();
|
|
956
|
+
const choices = [
|
|
957
|
+
{ value: 'add', label: 'Add custom model' },
|
|
958
|
+
...(models.length > 0 ? [{ value: 'edit', label: 'Edit custom model' }] : []),
|
|
959
|
+
...(models.length > 0 ? [{ value: 'delete', label: 'Delete custom model' }] : []),
|
|
960
|
+
{ value: 'back', label: 'Back' },
|
|
961
|
+
];
|
|
962
|
+
const action = await p.select({
|
|
963
|
+
message: 'Custom models',
|
|
964
|
+
options: choices,
|
|
965
|
+
});
|
|
966
|
+
if (p.isCancel(action) || action === 'back') {
|
|
967
|
+
return null;
|
|
968
|
+
}
|
|
969
|
+
if (action === 'add') {
|
|
970
|
+
const created = await runCustomModelWizard(null, providerOverride);
|
|
971
|
+
return created?.name ?? null;
|
|
972
|
+
}
|
|
973
|
+
if (action === 'edit') {
|
|
974
|
+
const selected = await selectCustomModel(models);
|
|
975
|
+
if (!selected) {
|
|
976
|
+
return null;
|
|
977
|
+
}
|
|
978
|
+
const updated = await runCustomModelWizard(selected, providerOverride);
|
|
979
|
+
return updated?.name ?? null;
|
|
980
|
+
}
|
|
981
|
+
if (action === 'delete') {
|
|
982
|
+
const model = await selectCustomModel(models);
|
|
983
|
+
if (!model) {
|
|
984
|
+
return null;
|
|
985
|
+
}
|
|
986
|
+
const confirm = await p.confirm({
|
|
987
|
+
message: `Delete custom model "${model.displayName || model.name}"?`,
|
|
988
|
+
initialValue: false,
|
|
989
|
+
});
|
|
990
|
+
if (p.isCancel(confirm) || !confirm) {
|
|
991
|
+
return null;
|
|
992
|
+
}
|
|
993
|
+
await deleteCustomModel(model.name);
|
|
994
|
+
p.log.success(`Deleted ${model.displayName || model.name}`);
|
|
995
|
+
return null;
|
|
996
|
+
}
|
|
997
|
+
return null;
|
|
998
|
+
}
|
|
999
|
+
async function selectCustomModel(models) {
|
|
1000
|
+
if (models.length === 0) {
|
|
1001
|
+
p.log.info('No custom models available.');
|
|
1002
|
+
return null;
|
|
1003
|
+
}
|
|
1004
|
+
const selection = await p.select({
|
|
1005
|
+
message: 'Select a custom model',
|
|
1006
|
+
options: models.map((model) => ({
|
|
1007
|
+
value: model.name,
|
|
1008
|
+
label: model.displayName || model.name,
|
|
1009
|
+
})),
|
|
1010
|
+
});
|
|
1011
|
+
if (p.isCancel(selection)) {
|
|
1012
|
+
return null;
|
|
1013
|
+
}
|
|
1014
|
+
return models.find((model) => model.name === selection) ?? null;
|
|
1015
|
+
}
|
|
1016
|
+
async function runCustomModelWizard(initialModel, providerOverride) {
|
|
1017
|
+
const values = await promptCustomModelValues(initialModel ?? null, providerOverride);
|
|
1018
|
+
if (!values) {
|
|
1019
|
+
return null;
|
|
1020
|
+
}
|
|
1021
|
+
const model = {
|
|
1022
|
+
name: values.name,
|
|
1023
|
+
provider: values.provider,
|
|
1024
|
+
...(values.baseURL ? { baseURL: values.baseURL } : {}),
|
|
1025
|
+
...(values.displayName ? { displayName: values.displayName } : {}),
|
|
1026
|
+
...(values.maxInputTokens ? { maxInputTokens: values.maxInputTokens } : {}),
|
|
1027
|
+
...(values.apiKey ? { apiKey: values.apiKey } : {}),
|
|
1028
|
+
...(values.filePath ? { filePath: values.filePath } : {}),
|
|
1029
|
+
...(values.reasoningEffort ? { reasoningEffort: values.reasoningEffort } : {}),
|
|
1030
|
+
};
|
|
1031
|
+
await saveCustomModel(model);
|
|
1032
|
+
if (initialModel && initialModel.name !== model.name) {
|
|
1033
|
+
await deleteCustomModel(initialModel.name);
|
|
1034
|
+
}
|
|
1035
|
+
p.log.success(`${initialModel ? 'Updated' : 'Saved'} ${model.displayName || model.name}`);
|
|
1036
|
+
return model;
|
|
1037
|
+
}
|
|
1038
|
+
async function promptCustomModelValues(initialModel, providerOverride) {
|
|
1039
|
+
const providers = [
|
|
1040
|
+
'openai-compatible',
|
|
1041
|
+
'openrouter',
|
|
1042
|
+
'litellm',
|
|
1043
|
+
'glama',
|
|
1044
|
+
'bedrock',
|
|
1045
|
+
'ollama',
|
|
1046
|
+
'local',
|
|
1047
|
+
'vertex',
|
|
1048
|
+
...(isDextoAuthEnabled() ? ['dexto-nova'] : []),
|
|
1049
|
+
];
|
|
1050
|
+
const effectiveProvider = initialModel?.provider ?? providerOverride;
|
|
1051
|
+
let provider;
|
|
1052
|
+
if (effectiveProvider) {
|
|
1053
|
+
provider = effectiveProvider;
|
|
1054
|
+
}
|
|
1055
|
+
else {
|
|
1056
|
+
provider = (await p.select({
|
|
1057
|
+
message: 'Custom model provider',
|
|
1058
|
+
options: providers.map((value) => ({ value, label: value })),
|
|
1059
|
+
initialValue: 'openai-compatible',
|
|
1060
|
+
}));
|
|
1061
|
+
if (p.isCancel(provider)) {
|
|
1062
|
+
return null;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
const name = await p.text({
|
|
1066
|
+
message: 'Model name',
|
|
1067
|
+
initialValue: initialModel?.name ?? '',
|
|
1068
|
+
validate: (value) => {
|
|
1069
|
+
const trimmed = typeof value === 'string' ? value.trim() : '';
|
|
1070
|
+
return trimmed ? undefined : 'Model name is required';
|
|
1071
|
+
},
|
|
1072
|
+
});
|
|
1073
|
+
if (p.isCancel(name)) {
|
|
1074
|
+
return null;
|
|
1075
|
+
}
|
|
1076
|
+
const trimmedName = typeof name === 'string' ? name.trim() : '';
|
|
1077
|
+
if (provider === 'openrouter' || provider === 'glama' || provider === 'dexto-nova') {
|
|
1078
|
+
const isValidFormat = trimmedName.includes('/');
|
|
1079
|
+
if (!isValidFormat) {
|
|
1080
|
+
p.log.warn('Model name should include a provider prefix, e.g. anthropic/claude-3.5');
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
const displayName = await p.text({
|
|
1084
|
+
message: 'Display name (optional)',
|
|
1085
|
+
initialValue: initialModel?.displayName ?? '',
|
|
1086
|
+
});
|
|
1087
|
+
if (p.isCancel(displayName)) {
|
|
1088
|
+
return null;
|
|
1089
|
+
}
|
|
1090
|
+
let baseURL;
|
|
1091
|
+
if (provider === 'openai-compatible' || provider === 'litellm') {
|
|
1092
|
+
const baseURLInput = await p.text({
|
|
1093
|
+
message: 'Base URL',
|
|
1094
|
+
initialValue: initialModel?.baseURL?.trim() ?? '',
|
|
1095
|
+
validate: (value) => {
|
|
1096
|
+
const trimmed = typeof value === 'string' ? value.trim() : '';
|
|
1097
|
+
if (!trimmed)
|
|
1098
|
+
return 'Base URL is required';
|
|
1099
|
+
try {
|
|
1100
|
+
new URL(trimmed);
|
|
1101
|
+
return undefined;
|
|
1102
|
+
}
|
|
1103
|
+
catch {
|
|
1104
|
+
return 'Base URL must be a valid URL';
|
|
1105
|
+
}
|
|
1106
|
+
},
|
|
1107
|
+
});
|
|
1108
|
+
if (p.isCancel(baseURLInput)) {
|
|
1109
|
+
return null;
|
|
1110
|
+
}
|
|
1111
|
+
const baseURLValue = typeof baseURLInput === 'string' ? baseURLInput.trim() : '';
|
|
1112
|
+
baseURL = baseURLValue || undefined;
|
|
1113
|
+
}
|
|
1114
|
+
const maxInputTokensInput = await p.text({
|
|
1115
|
+
message: 'Max input tokens (optional)',
|
|
1116
|
+
initialValue: initialModel?.maxInputTokens?.toString() ?? '',
|
|
1117
|
+
validate: (value) => {
|
|
1118
|
+
const trimmed = typeof value === 'string' ? value.trim() : '';
|
|
1119
|
+
if (!trimmed)
|
|
1120
|
+
return undefined;
|
|
1121
|
+
const parsed = Number(value);
|
|
1122
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
1123
|
+
return 'Enter a positive integer';
|
|
1124
|
+
}
|
|
1125
|
+
return undefined;
|
|
1126
|
+
},
|
|
1127
|
+
});
|
|
1128
|
+
if (p.isCancel(maxInputTokensInput)) {
|
|
1129
|
+
return null;
|
|
1130
|
+
}
|
|
1131
|
+
let apiKey;
|
|
1132
|
+
if (provider !== 'bedrock' && provider !== 'vertex') {
|
|
1133
|
+
const apiKeyInput = await p.text({
|
|
1134
|
+
message: 'API key (optional)',
|
|
1135
|
+
initialValue: initialModel?.apiKey ?? '',
|
|
1136
|
+
});
|
|
1137
|
+
if (p.isCancel(apiKeyInput)) {
|
|
1138
|
+
return null;
|
|
1139
|
+
}
|
|
1140
|
+
const apiKeyValue = typeof apiKeyInput === 'string' ? apiKeyInput.trim() : '';
|
|
1141
|
+
apiKey = apiKeyValue || undefined;
|
|
1142
|
+
}
|
|
1143
|
+
let filePath;
|
|
1144
|
+
if (provider === 'local') {
|
|
1145
|
+
const filePathInput = await p.text({
|
|
1146
|
+
message: 'GGUF file path',
|
|
1147
|
+
initialValue: initialModel?.filePath ?? '',
|
|
1148
|
+
validate: (value) => {
|
|
1149
|
+
const trimmed = typeof value === 'string' ? value.trim() : '';
|
|
1150
|
+
if (!trimmed)
|
|
1151
|
+
return 'File path is required';
|
|
1152
|
+
if (!trimmed.toLowerCase().endsWith('.gguf')) {
|
|
1153
|
+
return 'File path must end with .gguf';
|
|
1154
|
+
}
|
|
1155
|
+
return undefined;
|
|
1156
|
+
},
|
|
1157
|
+
});
|
|
1158
|
+
if (p.isCancel(filePathInput)) {
|
|
1159
|
+
return null;
|
|
1160
|
+
}
|
|
1161
|
+
const filePathValue = typeof filePathInput === 'string' ? filePathInput.trim() : '';
|
|
1162
|
+
filePath = filePathValue || undefined;
|
|
1163
|
+
}
|
|
1164
|
+
const reasoningEffort = await p.text({
|
|
1165
|
+
message: 'Reasoning effort (optional)',
|
|
1166
|
+
initialValue: initialModel?.reasoningEffort?.toLowerCase() ?? '',
|
|
1167
|
+
validate: (value) => {
|
|
1168
|
+
const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
1169
|
+
if (!normalized)
|
|
1170
|
+
return undefined;
|
|
1171
|
+
const validValues = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh'];
|
|
1172
|
+
if (!validValues.includes(normalized)) {
|
|
1173
|
+
return `Use: ${validValues.join(', ')}`;
|
|
1174
|
+
}
|
|
1175
|
+
return undefined;
|
|
1176
|
+
},
|
|
1177
|
+
});
|
|
1178
|
+
if (p.isCancel(reasoningEffort)) {
|
|
1179
|
+
return null;
|
|
1180
|
+
}
|
|
1181
|
+
const trimmedDisplayName = typeof displayName === 'string' ? displayName.trim() : '';
|
|
1182
|
+
const trimmedApiKey = typeof apiKey === 'string' ? apiKey.trim() : '';
|
|
1183
|
+
const trimmedReasoningEffort = typeof reasoningEffort === 'string' ? reasoningEffort.trim().toLowerCase() : '';
|
|
1184
|
+
const trimmedMaxInputTokens = typeof maxInputTokensInput === 'string' ? maxInputTokensInput.trim() : '';
|
|
1185
|
+
return {
|
|
1186
|
+
name: trimmedName,
|
|
1187
|
+
provider,
|
|
1188
|
+
...(baseURL ? { baseURL } : {}),
|
|
1189
|
+
...(trimmedDisplayName ? { displayName: trimmedDisplayName } : {}),
|
|
1190
|
+
...(trimmedMaxInputTokens ? { maxInputTokens: Number(trimmedMaxInputTokens) } : {}),
|
|
1191
|
+
...(trimmedApiKey ? { apiKey: trimmedApiKey } : {}),
|
|
1192
|
+
...(filePath ? { filePath } : {}),
|
|
1193
|
+
...(trimmedReasoningEffort
|
|
1194
|
+
? { reasoningEffort: trimmedReasoningEffort }
|
|
1195
|
+
: {}),
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
730
1198
|
async function selectDefaultModeWithBack() {
|
|
731
1199
|
const result = await p.select({
|
|
732
1200
|
message: 'How do you want to use Dexto by default?',
|
|
@@ -863,40 +1331,50 @@ async function showSettingsMenu() {
|
|
|
863
1331
|
].join('\n');
|
|
864
1332
|
p.note(currentConfig, 'Current Configuration');
|
|
865
1333
|
}
|
|
1334
|
+
const currentProviderLabel = currentPrefs?.llm.provider ?? 'not set';
|
|
1335
|
+
const currentModelLabel = currentPrefs?.llm.model || 'not set';
|
|
1336
|
+
const options = [
|
|
1337
|
+
{
|
|
1338
|
+
value: 'model',
|
|
1339
|
+
label: 'Change model',
|
|
1340
|
+
hint: `Currently: ${currentProviderLabel} / ${currentModelLabel}`,
|
|
1341
|
+
},
|
|
1342
|
+
{
|
|
1343
|
+
value: 'mode',
|
|
1344
|
+
label: 'Change default mode',
|
|
1345
|
+
hint: `Currently: ${currentPrefs?.defaults.defaultMode || 'web'}`,
|
|
1346
|
+
},
|
|
1347
|
+
{
|
|
1348
|
+
value: 'apikey',
|
|
1349
|
+
label: 'Update API key',
|
|
1350
|
+
hint: 'Re-enter your API key',
|
|
1351
|
+
},
|
|
1352
|
+
{
|
|
1353
|
+
value: 'reset',
|
|
1354
|
+
label: 'Reset to defaults',
|
|
1355
|
+
hint: 'Start fresh with a new configuration',
|
|
1356
|
+
},
|
|
1357
|
+
{
|
|
1358
|
+
value: 'file',
|
|
1359
|
+
label: 'View preferences file',
|
|
1360
|
+
hint: 'See where your settings are stored',
|
|
1361
|
+
},
|
|
1362
|
+
{
|
|
1363
|
+
value: 'exit',
|
|
1364
|
+
label: 'Exit',
|
|
1365
|
+
hint: 'Done making changes',
|
|
1366
|
+
},
|
|
1367
|
+
];
|
|
1368
|
+
if (isDextoAuthEnabled()) {
|
|
1369
|
+
options.splice(2, 0, {
|
|
1370
|
+
value: 'credits',
|
|
1371
|
+
label: 'Buy Dexto Nova credits',
|
|
1372
|
+
hint: 'Open billing page in your browser',
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
866
1375
|
const action = await p.select({
|
|
867
1376
|
message: 'What would you like to do?',
|
|
868
|
-
options
|
|
869
|
-
{
|
|
870
|
-
value: 'model',
|
|
871
|
-
label: 'Change model',
|
|
872
|
-
hint: `Currently: ${currentPrefs?.llm.provider || 'not set'} / ${currentPrefs?.llm.model || 'not set'}`,
|
|
873
|
-
},
|
|
874
|
-
{
|
|
875
|
-
value: 'mode',
|
|
876
|
-
label: 'Change default mode',
|
|
877
|
-
hint: `Currently: ${currentPrefs?.defaults.defaultMode || 'web'}`,
|
|
878
|
-
},
|
|
879
|
-
{
|
|
880
|
-
value: 'apikey',
|
|
881
|
-
label: 'Update API key',
|
|
882
|
-
hint: 'Re-enter your API key',
|
|
883
|
-
},
|
|
884
|
-
{
|
|
885
|
-
value: 'reset',
|
|
886
|
-
label: 'Reset to defaults',
|
|
887
|
-
hint: 'Start fresh with a new configuration',
|
|
888
|
-
},
|
|
889
|
-
{
|
|
890
|
-
value: 'file',
|
|
891
|
-
label: 'View preferences file',
|
|
892
|
-
hint: 'See where your settings are stored',
|
|
893
|
-
},
|
|
894
|
-
{
|
|
895
|
-
value: 'exit',
|
|
896
|
-
label: 'Exit',
|
|
897
|
-
hint: 'Done making changes',
|
|
898
|
-
},
|
|
899
|
-
],
|
|
1377
|
+
options,
|
|
900
1378
|
});
|
|
901
1379
|
// Exit conditions
|
|
902
1380
|
if (p.isCancel(action) || action === 'exit') {
|
|
@@ -911,6 +1389,9 @@ async function showSettingsMenu() {
|
|
|
911
1389
|
case 'mode':
|
|
912
1390
|
await changeDefaultMode();
|
|
913
1391
|
break;
|
|
1392
|
+
case 'credits':
|
|
1393
|
+
await openCreditsPage();
|
|
1394
|
+
break;
|
|
914
1395
|
case 'apikey':
|
|
915
1396
|
await updateApiKey(currentPrefs?.llm.provider);
|
|
916
1397
|
break;
|
|
@@ -942,8 +1423,8 @@ async function changeModel(currentProvider) {
|
|
|
942
1423
|
message: 'Choose your model source',
|
|
943
1424
|
options: [
|
|
944
1425
|
{
|
|
945
|
-
value: 'dexto',
|
|
946
|
-
label: `${chalk.magenta('★')} Dexto
|
|
1426
|
+
value: 'dexto-nova',
|
|
1427
|
+
label: `${chalk.magenta('★')} Dexto Nova`,
|
|
947
1428
|
hint: 'All models, one account',
|
|
948
1429
|
},
|
|
949
1430
|
{
|
|
@@ -957,9 +1438,12 @@ async function changeModel(currentProvider) {
|
|
|
957
1438
|
p.log.warn('Model change cancelled');
|
|
958
1439
|
return;
|
|
959
1440
|
}
|
|
960
|
-
if (providerChoice === 'dexto') {
|
|
1441
|
+
if (providerChoice === 'dexto-nova') {
|
|
961
1442
|
// Use the same Dexto setup flow as first-time setup
|
|
962
|
-
await handleDextoProviderSetup();
|
|
1443
|
+
const completed = await handleDextoProviderSetup({ exitOnCancel: false });
|
|
1444
|
+
if (!completed) {
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
963
1447
|
return;
|
|
964
1448
|
}
|
|
965
1449
|
// 'other' - fall through to normal provider selection
|
|
@@ -1241,20 +1725,89 @@ async function selectModel(provider) {
|
|
|
1241
1725
|
const providerInfo = LLM_REGISTRY[provider];
|
|
1242
1726
|
// For providers with a fixed model list
|
|
1243
1727
|
if (providerInfo?.models && providerInfo.models.length > 0) {
|
|
1244
|
-
const
|
|
1245
|
-
|
|
1728
|
+
const curatedModels = getCuratedModelsForProvider(provider);
|
|
1729
|
+
if (provider === 'openrouter') {
|
|
1730
|
+
const curatedOptions = curatedModels
|
|
1731
|
+
.slice(0, 8)
|
|
1732
|
+
.filter((m) => m.name !== 'openrouter/free')
|
|
1733
|
+
.map((m) => ({
|
|
1246
1734
|
value: m.name,
|
|
1247
1735
|
label: m.displayName || m.name,
|
|
1248
|
-
};
|
|
1249
|
-
if (
|
|
1250
|
-
|
|
1736
|
+
}));
|
|
1737
|
+
if (supportsCustomModels(provider)) {
|
|
1738
|
+
p.log.info(chalk.gray('Tip: You can add or edit custom models via /model'));
|
|
1739
|
+
const manageCustomModels = await p.confirm({
|
|
1740
|
+
message: 'Manage custom models now?',
|
|
1741
|
+
initialValue: false,
|
|
1742
|
+
});
|
|
1743
|
+
if (p.isCancel(manageCustomModels)) {
|
|
1744
|
+
return null;
|
|
1745
|
+
}
|
|
1746
|
+
if (manageCustomModels) {
|
|
1747
|
+
const customModel = await handleCustomModelManagement(provider);
|
|
1748
|
+
if (customModel) {
|
|
1749
|
+
return customModel;
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1251
1752
|
}
|
|
1252
|
-
|
|
1253
|
-
|
|
1753
|
+
const selected = await p.select({
|
|
1754
|
+
message: `Select a model for ${getProviderDisplayName(provider)}`,
|
|
1755
|
+
options: [
|
|
1756
|
+
{
|
|
1757
|
+
value: 'openrouter/free',
|
|
1758
|
+
label: 'OpenRouter Free Models',
|
|
1759
|
+
hint: '(recommended)',
|
|
1760
|
+
},
|
|
1761
|
+
...curatedOptions,
|
|
1762
|
+
],
|
|
1763
|
+
initialValue: 'openrouter/free',
|
|
1764
|
+
});
|
|
1765
|
+
if (p.isCancel(selected)) {
|
|
1766
|
+
return null;
|
|
1767
|
+
}
|
|
1768
|
+
return selected;
|
|
1769
|
+
}
|
|
1770
|
+
const defaultModel = curatedModels.find((m) => m.default) ??
|
|
1771
|
+
providerInfo.models.find((m) => m.default) ??
|
|
1772
|
+
curatedModels[0] ??
|
|
1773
|
+
providerInfo.models[0];
|
|
1774
|
+
if (!defaultModel) {
|
|
1775
|
+
return null;
|
|
1776
|
+
}
|
|
1777
|
+
const curatedOptions = curatedModels
|
|
1778
|
+
.slice(0, 8)
|
|
1779
|
+
.filter((m) => m.name !== defaultModel.name)
|
|
1780
|
+
.map((m) => ({
|
|
1781
|
+
value: m.name,
|
|
1782
|
+
label: m.displayName || m.name,
|
|
1783
|
+
}));
|
|
1784
|
+
if (supportsCustomModels(provider)) {
|
|
1785
|
+
p.log.info(chalk.gray('Tip: You can add or edit custom models via /model'));
|
|
1786
|
+
const manageCustomModels = await p.confirm({
|
|
1787
|
+
message: 'Manage custom models now?',
|
|
1788
|
+
initialValue: false,
|
|
1789
|
+
});
|
|
1790
|
+
if (p.isCancel(manageCustomModels)) {
|
|
1791
|
+
return null;
|
|
1792
|
+
}
|
|
1793
|
+
if (manageCustomModels) {
|
|
1794
|
+
const customModel = await handleCustomModelManagement(provider);
|
|
1795
|
+
if (customModel) {
|
|
1796
|
+
return customModel;
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1254
1800
|
const selected = await p.select({
|
|
1255
1801
|
message: `Select a model for ${getProviderDisplayName(provider)}`,
|
|
1256
|
-
options
|
|
1257
|
-
|
|
1802
|
+
options: [
|
|
1803
|
+
{
|
|
1804
|
+
value: defaultModel.name,
|
|
1805
|
+
label: defaultModel.displayName || defaultModel.name,
|
|
1806
|
+
hint: '(recommended)',
|
|
1807
|
+
},
|
|
1808
|
+
...curatedOptions,
|
|
1809
|
+
],
|
|
1810
|
+
initialValue: defaultModel.name,
|
|
1258
1811
|
});
|
|
1259
1812
|
if (p.isCancel(selected)) {
|
|
1260
1813
|
return null;
|
|
@@ -1266,7 +1819,8 @@ async function selectModel(provider) {
|
|
|
1266
1819
|
message: `Enter model name for ${getProviderDisplayName(provider)}`,
|
|
1267
1820
|
placeholder: provider === 'openrouter' ? 'e.g., anthropic/claude-3.5-sonnet' : 'e.g., llama-3-70b',
|
|
1268
1821
|
validate: (value) => {
|
|
1269
|
-
|
|
1822
|
+
const trimmed = typeof value === 'string' ? value.trim() : '';
|
|
1823
|
+
if (!trimmed) {
|
|
1270
1824
|
return 'Model name is required';
|
|
1271
1825
|
}
|
|
1272
1826
|
return undefined;
|
|
@@ -1275,6 +1829,9 @@ async function selectModel(provider) {
|
|
|
1275
1829
|
if (p.isCancel(modelInput)) {
|
|
1276
1830
|
return null;
|
|
1277
1831
|
}
|
|
1832
|
+
if (typeof modelInput !== 'string') {
|
|
1833
|
+
return null;
|
|
1834
|
+
}
|
|
1278
1835
|
return modelInput.trim();
|
|
1279
1836
|
}
|
|
1280
1837
|
/**
|
|
@@ -1292,11 +1849,12 @@ async function promptForBaseURL(provider) {
|
|
|
1292
1849
|
message: `Enter base URL for ${getProviderDisplayName(provider)}`,
|
|
1293
1850
|
placeholder,
|
|
1294
1851
|
validate: (value) => {
|
|
1295
|
-
|
|
1852
|
+
const trimmed = typeof value === 'string' ? value.trim() : '';
|
|
1853
|
+
if (!trimmed) {
|
|
1296
1854
|
return 'Base URL is required for this provider';
|
|
1297
1855
|
}
|
|
1298
1856
|
try {
|
|
1299
|
-
new URL(
|
|
1857
|
+
new URL(trimmed);
|
|
1300
1858
|
}
|
|
1301
1859
|
catch {
|
|
1302
1860
|
return 'Please enter a valid URL';
|
|
@@ -1307,7 +1865,7 @@ async function promptForBaseURL(provider) {
|
|
|
1307
1865
|
if (p.isCancel(baseURL)) {
|
|
1308
1866
|
return null;
|
|
1309
1867
|
}
|
|
1310
|
-
return baseURL.trim();
|
|
1868
|
+
return typeof baseURL === 'string' ? baseURL.trim() : '';
|
|
1311
1869
|
}
|
|
1312
1870
|
/**
|
|
1313
1871
|
* Show setup complete message
|
|
@@ -1342,6 +1900,7 @@ async function showSetupComplete(provider, model, defaultMode, apiKeySkipped = f
|
|
|
1342
1900
|
...(isLocalProvider
|
|
1343
1901
|
? [` Run ${chalk.cyan('dexto setup')} again to manage local models`]
|
|
1344
1902
|
: []),
|
|
1903
|
+
` In the interactive CLI, run ${chalk.cyan('/model')} to switch models`,
|
|
1345
1904
|
` Run ${chalk.cyan('dexto --help')} for more options`,
|
|
1346
1905
|
].join('\n');
|
|
1347
1906
|
console.log(summary);
|