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.
Files changed (178) hide show
  1. package/README.md +3 -3
  2. package/dist/agents/agent-template.yml +2 -2
  3. package/dist/agents/coding-agent/README.md +10 -10
  4. package/dist/agents/coding-agent/coding-agent.yml +81 -80
  5. package/dist/agents/default-agent.yml +32 -47
  6. package/dist/agents/explore-agent/explore-agent.yml +3 -6
  7. package/dist/agents/image-editor-agent/image-editor-agent.yml +1 -1
  8. package/dist/agents/nano-banana-agent/nano-banana-agent.yml +1 -1
  9. package/dist/agents/podcast-agent/podcast-agent.yml +1 -1
  10. package/dist/agents/product-name-researcher/product-name-researcher.yml +1 -1
  11. package/dist/agents/sora-video-agent/sora-video-agent.yml +4 -6
  12. package/dist/agents/triage-demo/triage-agent.yml +1 -1
  13. package/dist/analytics/events.d.ts +2 -2
  14. package/dist/analytics/events.d.ts.map +1 -1
  15. package/dist/api/mcp/tool-aggregation-handler.d.ts +2 -2
  16. package/dist/api/server-hono.d.ts +2 -2
  17. package/dist/api/server-hono.d.ts.map +1 -1
  18. package/dist/api/server-hono.js +37 -60
  19. package/dist/cli/approval/cli-approval-handler.d.ts +10 -3
  20. package/dist/cli/approval/cli-approval-handler.d.ts.map +1 -1
  21. package/dist/cli/approval/cli-approval-handler.js +1 -1
  22. package/dist/cli/auth/constants.d.ts +4 -0
  23. package/dist/cli/auth/constants.d.ts.map +1 -1
  24. package/dist/cli/auth/constants.js +4 -0
  25. package/dist/cli/commands/auth/logout.js +2 -2
  26. package/dist/cli/commands/billing/status.d.ts +3 -1
  27. package/dist/cli/commands/billing/status.d.ts.map +1 -1
  28. package/dist/cli/commands/billing/status.js +23 -1
  29. package/dist/cli/commands/create-app.d.ts +1 -11
  30. package/dist/cli/commands/create-app.d.ts.map +1 -1
  31. package/dist/cli/commands/create-app.js +21 -545
  32. package/dist/cli/commands/create-image.d.ts.map +1 -1
  33. package/dist/cli/commands/create-image.js +54 -53
  34. package/dist/cli/commands/image.d.ts +52 -0
  35. package/dist/cli/commands/image.d.ts.map +1 -0
  36. package/dist/cli/commands/image.js +118 -0
  37. package/dist/cli/commands/index.d.ts +2 -1
  38. package/dist/cli/commands/index.d.ts.map +1 -1
  39. package/dist/cli/commands/index.js +3 -1
  40. package/dist/cli/commands/init-app.d.ts +4 -8
  41. package/dist/cli/commands/init-app.d.ts.map +1 -1
  42. package/dist/cli/commands/init-app.js +37 -161
  43. package/dist/cli/commands/interactive-commands/command-parser.d.ts +2 -0
  44. package/dist/cli/commands/interactive-commands/command-parser.d.ts.map +1 -1
  45. package/dist/cli/commands/interactive-commands/commands.d.ts +1 -1
  46. package/dist/cli/commands/interactive-commands/commands.d.ts.map +1 -1
  47. package/dist/cli/commands/interactive-commands/commands.js +2 -2
  48. package/dist/cli/commands/interactive-commands/general-commands.js +2 -2
  49. package/dist/cli/commands/interactive-commands/prompt-commands.d.ts.map +1 -1
  50. package/dist/cli/commands/interactive-commands/prompt-commands.js +13 -2
  51. package/dist/cli/commands/interactive-commands/session/index.d.ts +2 -1
  52. package/dist/cli/commands/interactive-commands/session/index.d.ts.map +1 -1
  53. package/dist/cli/commands/interactive-commands/session/index.js +2 -1
  54. package/dist/cli/commands/interactive-commands/session/session-commands.d.ts +2 -2
  55. package/dist/cli/commands/interactive-commands/session/session-commands.js +2 -2
  56. package/dist/cli/commands/interactive-commands/system/system-commands.d.ts.map +1 -1
  57. package/dist/cli/commands/interactive-commands/system/system-commands.js +7 -29
  58. package/dist/cli/commands/list-agents.d.ts.map +1 -1
  59. package/dist/cli/commands/list-agents.js +3 -2
  60. package/dist/cli/commands/plugin.d.ts +4 -4
  61. package/dist/cli/commands/setup.d.ts +5 -5
  62. package/dist/cli/commands/setup.d.ts.map +1 -1
  63. package/dist/cli/commands/setup.js +766 -207
  64. package/dist/cli/commands/sync-agents.d.ts +2 -12
  65. package/dist/cli/commands/sync-agents.d.ts.map +1 -1
  66. package/dist/cli/commands/sync-agents.js +2 -50
  67. package/dist/cli/ink-cli/InkCLIRefactored.d.ts +7 -1
  68. package/dist/cli/ink-cli/InkCLIRefactored.d.ts.map +1 -1
  69. package/dist/cli/ink-cli/InkCLIRefactored.js +17 -7
  70. package/dist/cli/ink-cli/components/ApprovalPrompt.d.ts +2 -2
  71. package/dist/cli/ink-cli/components/ApprovalPrompt.d.ts.map +1 -1
  72. package/dist/cli/ink-cli/components/ApprovalPrompt.js +15 -14
  73. package/dist/cli/ink-cli/components/BackgroundTasksPanel.d.ts +18 -0
  74. package/dist/cli/ink-cli/components/BackgroundTasksPanel.d.ts.map +1 -0
  75. package/dist/cli/ink-cli/components/BackgroundTasksPanel.js +48 -0
  76. package/dist/cli/ink-cli/components/ErrorBoundary.js +1 -1
  77. package/dist/cli/ink-cli/components/Footer.d.ts.map +1 -1
  78. package/dist/cli/ink-cli/components/Footer.js +5 -6
  79. package/dist/cli/ink-cli/components/ResourceAutocomplete.d.ts.map +1 -1
  80. package/dist/cli/ink-cli/components/ResourceAutocomplete.js +150 -41
  81. package/dist/cli/ink-cli/components/StatusBar.d.ts +3 -1
  82. package/dist/cli/ink-cli/components/StatusBar.d.ts.map +1 -1
  83. package/dist/cli/ink-cli/components/StatusBar.js +27 -7
  84. package/dist/cli/ink-cli/components/TodoPanel.js +1 -1
  85. package/dist/cli/ink-cli/components/chat/MessageItem.d.ts.map +1 -1
  86. package/dist/cli/ink-cli/components/chat/MessageItem.js +9 -5
  87. package/dist/cli/ink-cli/components/chat/styled-boxes/ConfigBox.js +1 -1
  88. package/dist/cli/ink-cli/components/modes/AlternateBufferCLI.d.ts +3 -1
  89. package/dist/cli/ink-cli/components/modes/AlternateBufferCLI.d.ts.map +1 -1
  90. package/dist/cli/ink-cli/components/modes/AlternateBufferCLI.js +3 -2
  91. package/dist/cli/ink-cli/components/modes/StaticCLI.d.ts +3 -1
  92. package/dist/cli/ink-cli/components/modes/StaticCLI.d.ts.map +1 -1
  93. package/dist/cli/ink-cli/components/modes/StaticCLI.js +3 -2
  94. package/dist/cli/ink-cli/components/overlays/ContextStatsOverlay.js +1 -1
  95. package/dist/cli/ink-cli/components/overlays/CustomModelWizard.d.ts.map +1 -1
  96. package/dist/cli/ink-cli/components/overlays/CustomModelWizard.js +8 -4
  97. package/dist/cli/ink-cli/components/overlays/LogLevelSelector.js +1 -1
  98. package/dist/cli/ink-cli/components/overlays/McpRemoveSelector.js +1 -1
  99. package/dist/cli/ink-cli/components/overlays/ModelSelectorRefactored.d.ts +1 -0
  100. package/dist/cli/ink-cli/components/overlays/ModelSelectorRefactored.d.ts.map +1 -1
  101. package/dist/cli/ink-cli/components/overlays/ModelSelectorRefactored.js +144 -41
  102. package/dist/cli/ink-cli/components/overlays/ToolBrowser.d.ts +2 -1
  103. package/dist/cli/ink-cli/components/overlays/ToolBrowser.d.ts.map +1 -1
  104. package/dist/cli/ink-cli/components/overlays/ToolBrowser.js +286 -44
  105. package/dist/cli/ink-cli/components/overlays/custom-model-wizard/provider-config.d.ts +9 -1
  106. package/dist/cli/ink-cli/components/overlays/custom-model-wizard/provider-config.d.ts.map +1 -1
  107. package/dist/cli/ink-cli/components/overlays/custom-model-wizard/provider-config.js +35 -9
  108. package/dist/cli/ink-cli/constants/tips.js +1 -1
  109. package/dist/cli/ink-cli/containers/InputContainer.d.ts +4 -0
  110. package/dist/cli/ink-cli/containers/InputContainer.d.ts.map +1 -1
  111. package/dist/cli/ink-cli/containers/InputContainer.js +30 -8
  112. package/dist/cli/ink-cli/containers/OverlayContainer.d.ts +2 -0
  113. package/dist/cli/ink-cli/containers/OverlayContainer.d.ts.map +1 -1
  114. package/dist/cli/ink-cli/containers/OverlayContainer.js +215 -59
  115. package/dist/cli/ink-cli/hooks/useAgentEvents.d.ts.map +1 -1
  116. package/dist/cli/ink-cli/hooks/useAgentEvents.js +73 -13
  117. package/dist/cli/ink-cli/hooks/useCLIState.d.ts.map +1 -1
  118. package/dist/cli/ink-cli/hooks/useCLIState.js +3 -0
  119. package/dist/cli/ink-cli/hooks/useInputOrchestrator.d.ts.map +1 -1
  120. package/dist/cli/ink-cli/hooks/useInputOrchestrator.js +8 -0
  121. package/dist/cli/ink-cli/hooks/useTokenCounter.d.ts.map +1 -1
  122. package/dist/cli/ink-cli/hooks/useTokenCounter.js +7 -4
  123. package/dist/cli/ink-cli/services/CommandService.d.ts +1 -1
  124. package/dist/cli/ink-cli/services/CommandService.d.ts.map +1 -1
  125. package/dist/cli/ink-cli/services/CommandService.js +2 -2
  126. package/dist/cli/ink-cli/services/processStream.d.ts +2 -2
  127. package/dist/cli/ink-cli/services/processStream.d.ts.map +1 -1
  128. package/dist/cli/ink-cli/services/processStream.js +55 -8
  129. package/dist/cli/ink-cli/state/initialState.d.ts.map +1 -1
  130. package/dist/cli/ink-cli/state/initialState.js +3 -0
  131. package/dist/cli/ink-cli/state/types.d.ts +11 -2
  132. package/dist/cli/ink-cli/state/types.d.ts.map +1 -1
  133. package/dist/cli/ink-cli/utils/llm-provider-display.d.ts +3 -0
  134. package/dist/cli/ink-cli/utils/llm-provider-display.d.ts.map +1 -0
  135. package/dist/cli/ink-cli/utils/llm-provider-display.js +22 -0
  136. package/dist/cli/ink-cli/utils/messageFormatting.d.ts +13 -9
  137. package/dist/cli/ink-cli/utils/messageFormatting.d.ts.map +1 -1
  138. package/dist/cli/ink-cli/utils/messageFormatting.js +106 -151
  139. package/dist/cli/ink-cli/utils/toolUtils.d.ts.map +1 -1
  140. package/dist/cli/ink-cli/utils/toolUtils.js +2 -9
  141. package/dist/cli/utils/config-validation.d.ts +11 -11
  142. package/dist/cli/utils/config-validation.d.ts.map +1 -1
  143. package/dist/cli/utils/config-validation.js +56 -290
  144. package/dist/cli/utils/dexto-auth-check.d.ts +7 -7
  145. package/dist/cli/utils/dexto-auth-check.d.ts.map +1 -1
  146. package/dist/cli/utils/dexto-auth-check.js +16 -16
  147. package/dist/cli/utils/image-store.d.ts +16 -0
  148. package/dist/cli/utils/image-store.d.ts.map +1 -0
  149. package/dist/cli/utils/image-store.js +289 -0
  150. package/dist/cli/utils/options.js +1 -1
  151. package/dist/cli/utils/provider-setup.d.ts +2 -2
  152. package/dist/cli/utils/provider-setup.d.ts.map +1 -1
  153. package/dist/cli/utils/provider-setup.js +10 -2
  154. package/dist/cli/utils/scaffolding-utils.d.ts +5 -0
  155. package/dist/cli/utils/scaffolding-utils.d.ts.map +1 -1
  156. package/dist/cli/utils/scaffolding-utils.js +46 -4
  157. package/dist/cli/utils/template-engine.d.ts +28 -16
  158. package/dist/cli/utils/template-engine.d.ts.map +1 -1
  159. package/dist/cli/utils/template-engine.js +339 -479
  160. package/dist/config/cli-overrides.d.ts +4 -3
  161. package/dist/config/cli-overrides.d.ts.map +1 -1
  162. package/dist/config/cli-overrides.js +8 -10
  163. package/dist/config/effective-llm.d.ts +4 -4
  164. package/dist/config/effective-llm.d.ts.map +1 -1
  165. package/dist/config/effective-llm.js +4 -4
  166. package/dist/index-main.d.ts +2 -0
  167. package/dist/index-main.d.ts.map +1 -0
  168. package/dist/index-main.js +1554 -0
  169. package/dist/index.js +2 -1580
  170. package/dist/utils/session-logger-factory.d.ts +3 -0
  171. package/dist/utils/session-logger-factory.d.ts.map +1 -0
  172. package/dist/utils/session-logger-factory.js +19 -0
  173. package/dist/webui/assets/{index-Dl3mj53P.js → index-DwtueA8l.js} +231 -231
  174. package/dist/webui/index.html +1 -1
  175. package/package.json +10 -7
  176. package/dist/cli/cli-subscriber.d.ts +0 -45
  177. package/dist/cli/cli-subscriber.d.ts.map +0 -1
  178. 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 { getDefaultModelForProvider, LLM_PROVIDERS, LLM_REGISTRY, isValidProviderModel, getSupportedModels, acceptsAnyModel, supportsCustomModels, requiresApiKey, isReasoningCapableModel, } from '@dexto/core';
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
- // Let user pick from popular free providers
166
- const quickProvider = await p.select({
167
- message: 'Choose a provider',
168
- options: [
169
- {
170
- value: 'google',
171
- label: `${chalk.green('')} Google Gemini`,
172
- hint: 'Free, 1M+ context (recommended)',
173
- },
174
- {
175
- value: 'groq',
176
- label: `${chalk.green('')} Groq`,
177
- hint: 'Free, ultra-fast',
178
- },
179
- {
180
- value: 'local',
181
- label: `${chalk.cyan('')} Local Models`,
182
- hint: 'Free, private, runs on your machine',
183
- },
184
- ],
185
- });
186
- if (p.isCancel(quickProvider)) {
187
- p.cancel('Setup cancelled');
188
- process.exit(0);
189
- }
190
- // Handle local models with dedicated setup flow
191
- if (quickProvider === 'local') {
192
- const localResult = await setupLocalModels();
193
- if (!hasSelectedModel(localResult)) {
194
- p.cancel('Setup cancelled');
195
- process.exit(0);
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
- const model = getModelFromResult(localResult);
198
- // CLI mode confirmation for local
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
- p.cancel('Setup cancelled');
205
- process.exit(0);
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
- p.cancel('Setup cancelled');
210
- process.exit(0);
348
+ if (options.onCancel === 'exit') {
349
+ p.cancel('Setup cancelled');
350
+ return 'cancelled';
351
+ }
352
+ continue;
211
353
  }
212
- // Sync the active model for local provider
213
- await setActiveModel(model);
214
- const preferences = createInitialPreferences({
215
- provider: 'local',
354
+ // Save preferences
355
+ const preferencesOptions = {
356
+ provider,
216
357
  model,
217
358
  defaultMode,
218
359
  setupCompleted: true,
219
- apiKeyPending: false,
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: 'local',
369
+ provider,
224
370
  model,
225
371
  setupMode: 'interactive',
226
372
  setupVariant: 'quick-start',
227
373
  defaultMode,
228
- apiKeySkipped: false,
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
- if (result.cancelled) {
250
- p.cancel('Setup cancelled');
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
- console.log(chalk.magenta('\n★ Dexto Setup\n'));
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
- p.cancel('Setup cancelled');
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
- process.exit(1);
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
- p.cancel('Setup cancelled - login required for Dexto');
336
- process.exit(1);
423
+ return abort('Setup cancelled - login required for Dexto', 1);
337
424
  }
338
425
  }
339
426
  else {
340
- p.log.success('Already logged in to Dexto');
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.dexto.models)
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
- p.cancel('Setup cancelled');
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
- p.cancel('Setup cancelled');
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 Credits when feature is enabled
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 Credits`,
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 Credits flow - login if needed, then proceed to model selection
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
- p.cancel('Setup cancelled');
533
- process.exit(0);
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 model = await selectModelWithBack(provider);
582
- if (model === '_back') {
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
- p.cancel('Setup cancelled');
623
- process.exit(0);
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 modelOptions = providerInfo.models.map((m) => ({
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
- ...modelOptions,
701
- { value: '_back', label: chalk.gray('← Back'), hint: 'Change provider' },
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
- p.cancel('Setup cancelled');
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
- if (!value.trim())
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 Credits`,
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 options = providerInfo.models.map((m) => {
1245
- const option = {
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 (m.default) {
1250
- option.hint = '(default)';
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
- return option;
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
- initialValue: providerInfo.models.find((m) => m.default)?.name,
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
- if (!value || value.trim().length === 0) {
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
- if (!value || value.trim().length === 0) {
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(value.trim());
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);