dexto 1.4.0 → 1.5.1

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 (231) hide show
  1. package/README.md +62 -7
  2. package/dist/agents/agent-template.yml +2 -2
  3. package/dist/agents/coding-agent/coding-agent.yml +22 -16
  4. package/dist/agents/database-agent/database-agent.yml +2 -2
  5. package/dist/agents/default-agent.yml +7 -5
  6. package/dist/agents/github-agent/github-agent.yml +2 -2
  7. package/dist/agents/product-name-researcher/product-name-researcher.yml +2 -2
  8. package/dist/agents/talk2pdf-agent/talk2pdf-agent.yml +2 -2
  9. package/dist/analytics/events.d.ts +13 -6
  10. package/dist/analytics/events.d.ts.map +1 -1
  11. package/dist/analytics/index.d.ts +1 -1
  12. package/dist/analytics/index.d.ts.map +1 -1
  13. package/dist/analytics/index.js +6 -2
  14. package/dist/api/server-hono.d.ts.map +1 -1
  15. package/dist/api/server-hono.js +27 -5
  16. package/dist/cli/cli-subscriber.d.ts +4 -0
  17. package/dist/cli/cli-subscriber.d.ts.map +1 -1
  18. package/dist/cli/cli-subscriber.js +40 -2
  19. package/dist/cli/commands/create-app.d.ts +16 -14
  20. package/dist/cli/commands/create-app.d.ts.map +1 -1
  21. package/dist/cli/commands/create-app.js +626 -102
  22. package/dist/cli/commands/create-image.d.ts +7 -0
  23. package/dist/cli/commands/create-image.d.ts.map +1 -0
  24. package/dist/cli/commands/create-image.js +201 -0
  25. package/dist/cli/commands/helpers/formatters.js +7 -7
  26. package/dist/cli/commands/index.d.ts +2 -1
  27. package/dist/cli/commands/index.d.ts.map +1 -1
  28. package/dist/cli/commands/index.js +2 -1
  29. package/dist/cli/commands/init-app.js +7 -7
  30. package/dist/cli/commands/install.d.ts +0 -3
  31. package/dist/cli/commands/install.d.ts.map +1 -1
  32. package/dist/cli/commands/install.js +10 -35
  33. package/dist/cli/commands/interactive-commands/command-parser.d.ts +1 -1
  34. package/dist/cli/commands/interactive-commands/command-parser.d.ts.map +1 -1
  35. package/dist/cli/commands/interactive-commands/command-parser.js +18 -8
  36. package/dist/cli/commands/interactive-commands/general-commands.d.ts.map +1 -1
  37. package/dist/cli/commands/interactive-commands/general-commands.js +64 -1
  38. package/dist/cli/commands/interactive-commands/prompt-commands.js +11 -11
  39. package/dist/cli/commands/interactive-commands/system/system-commands.d.ts.map +1 -1
  40. package/dist/cli/commands/interactive-commands/system/system-commands.js +6 -5
  41. package/dist/cli/commands/list-agents.js +2 -2
  42. package/dist/cli/commands/session-commands.js +16 -16
  43. package/dist/cli/commands/setup.d.ts +13 -5
  44. package/dist/cli/commands/setup.d.ts.map +1 -1
  45. package/dist/cli/commands/setup.js +995 -65
  46. package/dist/cli/commands/which.js +1 -1
  47. package/dist/cli/ink-cli/components/ApprovalPrompt.d.ts +2 -0
  48. package/dist/cli/ink-cli/components/ApprovalPrompt.d.ts.map +1 -1
  49. package/dist/cli/ink-cli/components/ApprovalPrompt.js +29 -7
  50. package/dist/cli/ink-cli/components/CustomInput.js +1 -1
  51. package/dist/cli/ink-cli/components/EditableMultiLineInput.js +4 -4
  52. package/dist/cli/ink-cli/components/ElicitationForm.d.ts.map +1 -1
  53. package/dist/cli/ink-cli/components/ElicitationForm.js +6 -6
  54. package/dist/cli/ink-cli/components/ErrorBoundary.d.ts.map +1 -1
  55. package/dist/cli/ink-cli/components/ErrorBoundary.js +1 -1
  56. package/dist/cli/ink-cli/components/Footer.d.ts.map +1 -1
  57. package/dist/cli/ink-cli/components/Footer.js +1 -1
  58. package/dist/cli/ink-cli/components/HistorySearchBar.d.ts.map +1 -1
  59. package/dist/cli/ink-cli/components/HistorySearchBar.js +1 -1
  60. package/dist/cli/ink-cli/components/MultiLineInput.d.ts.map +1 -1
  61. package/dist/cli/ink-cli/components/MultiLineInput.js +3 -3
  62. package/dist/cli/ink-cli/components/ResourceAutocomplete.d.ts.map +1 -1
  63. package/dist/cli/ink-cli/components/ResourceAutocomplete.js +4 -4
  64. package/dist/cli/ink-cli/components/SlashCommandAutocomplete.js +3 -3
  65. package/dist/cli/ink-cli/components/StatusBar.d.ts.map +1 -1
  66. package/dist/cli/ink-cli/components/StatusBar.js +7 -5
  67. package/dist/cli/ink-cli/components/TextBufferInput.d.ts.map +1 -1
  68. package/dist/cli/ink-cli/components/TextBufferInput.js +13 -9
  69. package/dist/cli/ink-cli/components/base/BaseAutocomplete.js +4 -4
  70. package/dist/cli/ink-cli/components/base/BaseSelector.js +2 -2
  71. package/dist/cli/ink-cli/components/chat/Footer.js +1 -1
  72. package/dist/cli/ink-cli/components/chat/Header.d.ts.map +1 -1
  73. package/dist/cli/ink-cli/components/chat/Header.js +2 -4
  74. package/dist/cli/ink-cli/components/chat/MessageItem.d.ts.map +1 -1
  75. package/dist/cli/ink-cli/components/chat/MessageItem.js +9 -6
  76. package/dist/cli/ink-cli/components/chat/MessageList.js +1 -1
  77. package/dist/cli/ink-cli/components/chat/QueuedMessagesDisplay.js +1 -1
  78. package/dist/cli/ink-cli/components/chat/ToolIcon.d.ts +1 -1
  79. package/dist/cli/ink-cli/components/chat/ToolIcon.js +4 -4
  80. package/dist/cli/ink-cli/components/chat/styled-boxes/ConfigBox.js +1 -1
  81. package/dist/cli/ink-cli/components/chat/styled-boxes/HelpBox.js +1 -1
  82. package/dist/cli/ink-cli/components/chat/styled-boxes/LogConfigBox.d.ts.map +1 -1
  83. package/dist/cli/ink-cli/components/chat/styled-boxes/LogConfigBox.js +2 -2
  84. package/dist/cli/ink-cli/components/chat/styled-boxes/SessionHistoryBox.js +5 -5
  85. package/dist/cli/ink-cli/components/chat/styled-boxes/SessionListBox.js +2 -2
  86. package/dist/cli/ink-cli/components/chat/styled-boxes/ShortcutsBox.js +1 -1
  87. package/dist/cli/ink-cli/components/chat/styled-boxes/StatsBox.js +1 -1
  88. package/dist/cli/ink-cli/components/chat/styled-boxes/StyledBox.js +2 -2
  89. package/dist/cli/ink-cli/components/modes/AlternateBufferCLI.d.ts.map +1 -1
  90. package/dist/cli/ink-cli/components/modes/AlternateBufferCLI.js +1 -1
  91. package/dist/cli/ink-cli/components/modes/StaticCLI.d.ts.map +1 -1
  92. package/dist/cli/ink-cli/components/modes/StaticCLI.js +1 -1
  93. package/dist/cli/ink-cli/components/overlays/ApiKeyInput.js +1 -1
  94. package/dist/cli/ink-cli/components/overlays/CustomModelWizard.d.ts +10 -2
  95. package/dist/cli/ink-cli/components/overlays/CustomModelWizard.d.ts.map +1 -1
  96. package/dist/cli/ink-cli/components/overlays/CustomModelWizard.js +209 -89
  97. package/dist/cli/ink-cli/components/overlays/LogLevelSelector.d.ts.map +1 -1
  98. package/dist/cli/ink-cli/components/overlays/LogLevelSelector.js +2 -2
  99. package/dist/cli/ink-cli/components/overlays/McpAddChoice.d.ts.map +1 -1
  100. package/dist/cli/ink-cli/components/overlays/McpAddChoice.js +1 -1
  101. package/dist/cli/ink-cli/components/overlays/McpAddSelector.js +1 -1
  102. package/dist/cli/ink-cli/components/overlays/McpCustomTypeSelector.d.ts.map +1 -1
  103. package/dist/cli/ink-cli/components/overlays/McpCustomTypeSelector.js +2 -2
  104. package/dist/cli/ink-cli/components/overlays/McpCustomWizard.js +1 -1
  105. package/dist/cli/ink-cli/components/overlays/McpRemoveSelector.d.ts.map +1 -1
  106. package/dist/cli/ink-cli/components/overlays/McpRemoveSelector.js +1 -1
  107. package/dist/cli/ink-cli/components/overlays/McpSelector.d.ts.map +1 -1
  108. package/dist/cli/ink-cli/components/overlays/McpSelector.js +1 -1
  109. package/dist/cli/ink-cli/components/overlays/McpServerActions.d.ts.map +1 -1
  110. package/dist/cli/ink-cli/components/overlays/McpServerActions.js +2 -2
  111. package/dist/cli/ink-cli/components/overlays/McpServerList.d.ts.map +1 -1
  112. package/dist/cli/ink-cli/components/overlays/McpServerList.js +1 -1
  113. package/dist/cli/ink-cli/components/overlays/ModelSelectorRefactored.d.ts +6 -5
  114. package/dist/cli/ink-cli/components/overlays/ModelSelectorRefactored.d.ts.map +1 -1
  115. package/dist/cli/ink-cli/components/overlays/ModelSelectorRefactored.js +284 -68
  116. package/dist/cli/ink-cli/components/overlays/PromptAddChoice.d.ts.map +1 -1
  117. package/dist/cli/ink-cli/components/overlays/PromptAddChoice.js +2 -2
  118. package/dist/cli/ink-cli/components/overlays/PromptAddWizard.js +1 -1
  119. package/dist/cli/ink-cli/components/overlays/PromptDeleteSelector.d.ts.map +1 -1
  120. package/dist/cli/ink-cli/components/overlays/PromptDeleteSelector.js +2 -2
  121. package/dist/cli/ink-cli/components/overlays/PromptList.d.ts.map +1 -1
  122. package/dist/cli/ink-cli/components/overlays/PromptList.js +2 -2
  123. package/dist/cli/ink-cli/components/overlays/SearchOverlay.d.ts.map +1 -1
  124. package/dist/cli/ink-cli/components/overlays/SearchOverlay.js +4 -4
  125. package/dist/cli/ink-cli/components/overlays/SessionSubcommandSelector.d.ts.map +1 -1
  126. package/dist/cli/ink-cli/components/overlays/SessionSubcommandSelector.js +1 -1
  127. package/dist/cli/ink-cli/components/overlays/StreamSelector.d.ts.map +1 -1
  128. package/dist/cli/ink-cli/components/overlays/StreamSelector.js +1 -1
  129. package/dist/cli/ink-cli/components/overlays/ToolBrowser.js +12 -12
  130. package/dist/cli/ink-cli/components/overlays/custom-model-wizard/LocalModelWizard.d.ts +25 -0
  131. package/dist/cli/ink-cli/components/overlays/custom-model-wizard/LocalModelWizard.d.ts.map +1 -0
  132. package/dist/cli/ink-cli/components/overlays/custom-model-wizard/LocalModelWizard.js +609 -0
  133. package/dist/cli/ink-cli/components/overlays/custom-model-wizard/index.d.ts +15 -0
  134. package/dist/cli/ink-cli/components/overlays/custom-model-wizard/index.d.ts.map +1 -0
  135. package/dist/cli/ink-cli/components/overlays/custom-model-wizard/index.js +14 -0
  136. package/dist/cli/ink-cli/components/overlays/custom-model-wizard/provider-config.d.ts +33 -0
  137. package/dist/cli/ink-cli/components/overlays/custom-model-wizard/provider-config.d.ts.map +1 -0
  138. package/dist/cli/ink-cli/components/overlays/custom-model-wizard/provider-config.js +462 -0
  139. package/dist/cli/ink-cli/components/overlays/custom-model-wizard/shared/ApiKeyStep.d.ts +25 -0
  140. package/dist/cli/ink-cli/components/overlays/custom-model-wizard/shared/ApiKeyStep.d.ts.map +1 -0
  141. package/dist/cli/ink-cli/components/overlays/custom-model-wizard/shared/ApiKeyStep.js +29 -0
  142. package/dist/cli/ink-cli/components/overlays/custom-model-wizard/shared/ProviderSelector.d.ts +17 -0
  143. package/dist/cli/ink-cli/components/overlays/custom-model-wizard/shared/ProviderSelector.d.ts.map +1 -0
  144. package/dist/cli/ink-cli/components/overlays/custom-model-wizard/shared/ProviderSelector.js +11 -0
  145. package/dist/cli/ink-cli/components/overlays/custom-model-wizard/shared/SetupInfoBanner.d.ts +20 -0
  146. package/dist/cli/ink-cli/components/overlays/custom-model-wizard/shared/SetupInfoBanner.d.ts.map +1 -0
  147. package/dist/cli/ink-cli/components/overlays/custom-model-wizard/shared/SetupInfoBanner.js +10 -0
  148. package/dist/cli/ink-cli/components/overlays/custom-model-wizard/shared/WizardStepInput.d.ts +30 -0
  149. package/dist/cli/ink-cli/components/overlays/custom-model-wizard/shared/WizardStepInput.d.ts.map +1 -0
  150. package/dist/cli/ink-cli/components/overlays/custom-model-wizard/shared/WizardStepInput.js +13 -0
  151. package/dist/cli/ink-cli/components/overlays/custom-model-wizard/shared/index.d.ts +8 -0
  152. package/dist/cli/ink-cli/components/overlays/custom-model-wizard/shared/index.d.ts.map +1 -0
  153. package/dist/cli/ink-cli/components/overlays/custom-model-wizard/shared/index.js +7 -0
  154. package/dist/cli/ink-cli/components/overlays/custom-model-wizard/types.d.ts +85 -0
  155. package/dist/cli/ink-cli/components/overlays/custom-model-wizard/types.d.ts.map +1 -0
  156. package/dist/cli/ink-cli/components/overlays/custom-model-wizard/types.js +38 -0
  157. package/dist/cli/ink-cli/components/renderers/DiffRenderer.js +2 -2
  158. package/dist/cli/ink-cli/components/renderers/FilePreviewRenderer.js +1 -1
  159. package/dist/cli/ink-cli/components/renderers/FileRenderer.js +4 -4
  160. package/dist/cli/ink-cli/components/renderers/GenericRenderer.js +2 -2
  161. package/dist/cli/ink-cli/components/renderers/SearchRenderer.js +1 -1
  162. package/dist/cli/ink-cli/components/renderers/ShellRenderer.js +3 -3
  163. package/dist/cli/ink-cli/components/renderers/diff-shared.js +1 -1
  164. package/dist/cli/ink-cli/components/shared/MarkdownText.d.ts.map +1 -1
  165. package/dist/cli/ink-cli/components/shared/MarkdownText.js +8 -6
  166. package/dist/cli/ink-cli/containers/InputContainer.d.ts.map +1 -1
  167. package/dist/cli/ink-cli/containers/InputContainer.js +23 -1
  168. package/dist/cli/ink-cli/containers/OverlayContainer.d.ts.map +1 -1
  169. package/dist/cli/ink-cli/containers/OverlayContainer.js +81 -25
  170. package/dist/cli/ink-cli/hooks/useAgentEvents.d.ts +9 -2
  171. package/dist/cli/ink-cli/hooks/useAgentEvents.d.ts.map +1 -1
  172. package/dist/cli/ink-cli/hooks/useAgentEvents.js +148 -6
  173. package/dist/cli/ink-cli/hooks/useCLIState.d.ts +1 -1
  174. package/dist/cli/ink-cli/hooks/useCLIState.d.ts.map +1 -1
  175. package/dist/cli/ink-cli/hooks/useCLIState.js +7 -2
  176. package/dist/cli/ink-cli/hooks/useTokenCounter.d.ts +11 -7
  177. package/dist/cli/ink-cli/hooks/useTokenCounter.d.ts.map +1 -1
  178. package/dist/cli/ink-cli/hooks/useTokenCounter.js +41 -18
  179. package/dist/cli/ink-cli/services/processStream.d.ts.map +1 -1
  180. package/dist/cli/ink-cli/services/processStream.js +97 -17
  181. package/dist/cli/ink-cli/state/types.d.ts +5 -4
  182. package/dist/cli/ink-cli/state/types.d.ts.map +1 -1
  183. package/dist/cli/ink-cli/utils/messageFormatting.d.ts +5 -0
  184. package/dist/cli/ink-cli/utils/messageFormatting.d.ts.map +1 -1
  185. package/dist/cli/ink-cli/utils/messageFormatting.js +59 -1
  186. package/dist/cli/ink-cli/utils/toolUtils.d.ts.map +1 -1
  187. package/dist/cli/ink-cli/utils/toolUtils.js +2 -0
  188. package/dist/cli/utils/api-key-setup.d.ts +54 -4
  189. package/dist/cli/utils/api-key-setup.d.ts.map +1 -1
  190. package/dist/cli/utils/api-key-setup.js +433 -107
  191. package/dist/cli/utils/api-key-verification.d.ts +17 -0
  192. package/dist/cli/utils/api-key-verification.d.ts.map +1 -0
  193. package/dist/cli/utils/api-key-verification.js +211 -0
  194. package/dist/cli/utils/config-validation.d.ts +22 -2
  195. package/dist/cli/utils/config-validation.d.ts.map +1 -1
  196. package/dist/cli/utils/config-validation.js +354 -25
  197. package/dist/cli/utils/local-model-setup.d.ts +46 -0
  198. package/dist/cli/utils/local-model-setup.d.ts.map +1 -0
  199. package/dist/cli/utils/local-model-setup.js +662 -0
  200. package/dist/cli/utils/options.js +1 -1
  201. package/dist/cli/utils/prompt-helpers.d.ts +47 -0
  202. package/dist/cli/utils/prompt-helpers.d.ts.map +1 -0
  203. package/dist/cli/utils/prompt-helpers.js +66 -0
  204. package/dist/cli/utils/provider-setup.d.ts +66 -8
  205. package/dist/cli/utils/provider-setup.d.ts.map +1 -1
  206. package/dist/cli/utils/provider-setup.js +324 -84
  207. package/dist/cli/utils/scaffolding-utils.d.ts +76 -0
  208. package/dist/cli/utils/scaffolding-utils.d.ts.map +1 -0
  209. package/dist/cli/utils/scaffolding-utils.js +246 -0
  210. package/dist/cli/utils/setup-utils.d.ts +16 -0
  211. package/dist/cli/utils/setup-utils.d.ts.map +1 -1
  212. package/dist/cli/utils/setup-utils.js +72 -21
  213. package/dist/cli/utils/template-engine.d.ts +65 -0
  214. package/dist/cli/utils/template-engine.d.ts.map +1 -0
  215. package/dist/cli/utils/template-engine.js +1089 -0
  216. package/dist/config/cli-overrides.d.ts +44 -1
  217. package/dist/config/cli-overrides.d.ts.map +1 -1
  218. package/dist/config/cli-overrides.js +102 -0
  219. package/dist/index.js +339 -56
  220. package/dist/webui/assets/index-8j-KMkX1.js +2054 -0
  221. package/dist/webui/assets/index-c_AX24V4.css +1 -0
  222. package/dist/webui/index.html +3 -9
  223. package/dist/webui/logos/aws-color.svg +1 -0
  224. package/dist/webui/logos/dexto/dexto_logo.svg +1 -1
  225. package/dist/webui/logos/dexto/dexto_logo_light.svg +6 -6
  226. package/dist/webui/logos/glama.svg +7 -0
  227. package/dist/webui/logos/litellm.svg +7 -0
  228. package/dist/webui/logos/openrouter.svg +1 -0
  229. package/package.json +8 -7
  230. package/dist/webui/assets/index-BkwPkZpd.css +0 -1
  231. package/dist/webui/assets/index-D9u1XfyH.js +0 -2025
@@ -1,11 +1,12 @@
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, isValidProviderModel, getSupportedModels, } from '@dexto/core';
5
- import { getPrimaryApiKeyEnvVar, resolveApiKeyForProvider } from '@dexto/core';
6
- import { createInitialPreferences, saveGlobalPreferences } from '@dexto/agent-management';
7
- import { interactiveApiKeySetup } from '../utils/api-key-setup.js';
8
- import { selectProvider } from '../utils/provider-setup.js';
4
+ import { getDefaultModelForProvider, LLM_PROVIDERS, LLM_REGISTRY, isValidProviderModel, getSupportedModels, acceptsAnyModel, supportsCustomModels, requiresApiKey, isReasoningCapableModel, } from '@dexto/core';
5
+ import { resolveApiKeyForProvider } from '@dexto/core';
6
+ import { createInitialPreferences, saveGlobalPreferences, loadGlobalPreferences, getGlobalPreferencesPath, updateGlobalPreferences, setActiveModel, } from '@dexto/agent-management';
7
+ import { interactiveApiKeySetup, hasApiKeyConfigured } from '../utils/api-key-setup.js';
8
+ import { selectProvider, getProviderDisplayName, getProviderEnvVar, providerRequiresBaseURL, getDefaultModel, } from '../utils/provider-setup.js';
9
+ import { setupLocalModels, setupOllamaModels, hasSelectedModel, getModelFromResult, } from '../utils/local-model-setup.js';
9
10
  import { requiresSetup } from '../utils/setup-utils.js';
10
11
  import * as p from '@clack/prompts';
11
12
  import { logger } from '@dexto/core';
@@ -25,107 +26,1036 @@ const SetupCommandSchema = z
25
26
  defaultAgent: z
26
27
  .string()
27
28
  .min(1, 'Default agent name cannot be empty')
28
- .default('default-agent')
29
+ .default('coding-agent')
29
30
  .describe('Registry agent id to use when none is specified'),
30
31
  interactive: z.boolean().default(true).describe('Enable interactive prompts'),
31
32
  force: z
32
33
  .boolean()
33
34
  .default(false)
34
35
  .describe('Overwrite existing setup when already configured'),
36
+ quickStart: z
37
+ .boolean()
38
+ .default(false)
39
+ .describe('Use quick start with Google Gemini (recommended for new users)'),
35
40
  })
36
41
  .strict()
37
42
  .superRefine((data, ctx) => {
38
43
  // Validate model against provider when both are provided
39
44
  if (data.provider && data.model) {
40
- if (!isValidProviderModel(data.provider, data.model)) {
41
- const supportedModels = getSupportedModels(data.provider);
42
- ctx.addIssue({
43
- code: z.ZodIssueCode.custom,
44
- path: ['model'],
45
- message: `Model '${data.model}' is not supported by provider '${data.provider}'. Supported models: ${supportedModels.join(', ')}`,
46
- });
45
+ // Skip validation for providers that accept any model
46
+ if (!acceptsAnyModel(data.provider) && !supportsCustomModels(data.provider)) {
47
+ if (!isValidProviderModel(data.provider, data.model)) {
48
+ const supportedModels = getSupportedModels(data.provider);
49
+ ctx.addIssue({
50
+ code: z.ZodIssueCode.custom,
51
+ path: ['model'],
52
+ message: `Model '${data.model}' is not supported by provider '${data.provider}'. Supported models: ${supportedModels.join(', ')}`,
53
+ });
54
+ }
47
55
  }
48
56
  }
49
57
  });
58
+ /**
59
+ * Get the steps to display for the current provider and model.
60
+ * - Local/Ollama providers skip the API Key step.
61
+ * - Reasoning-capable models (o1, o3, codex, gpt-5.x) show the Reasoning step.
62
+ */
63
+ function getWizardSteps(provider, model) {
64
+ const isLocalProvider = provider === 'local' || provider === 'ollama';
65
+ const showReasoningStep = model && isReasoningCapableModel(model);
66
+ if (isLocalProvider) {
67
+ const steps = [
68
+ { key: 'provider', label: 'Provider' },
69
+ { key: 'model', label: 'Model' },
70
+ ];
71
+ if (showReasoningStep) {
72
+ steps.push({ key: 'reasoningEffort', label: 'Reasoning' });
73
+ }
74
+ steps.push({ key: 'mode', label: 'Mode' });
75
+ return steps;
76
+ }
77
+ const steps = [
78
+ { key: 'provider', label: 'Provider' },
79
+ { key: 'model', label: 'Model' },
80
+ ];
81
+ if (showReasoningStep) {
82
+ steps.push({ key: 'reasoningEffort', label: 'Reasoning' });
83
+ }
84
+ steps.push({ key: 'apiKey', label: 'API Key' });
85
+ steps.push({ key: 'mode', label: 'Mode' });
86
+ return steps;
87
+ }
88
+ /**
89
+ * Display step progress indicator for the setup wizard
90
+ */
91
+ function showStepProgress(currentStep, provider, model) {
92
+ // Don't show progress for setupType (it's the entry point)
93
+ if (currentStep === 'setupType' || currentStep === 'complete') {
94
+ return;
95
+ }
96
+ const steps = getWizardSteps(provider, model);
97
+ const currentIndex = steps.findIndex((s) => s.key === currentStep);
98
+ if (currentIndex === -1) {
99
+ return;
100
+ }
101
+ const progress = steps
102
+ .map((step, i) => {
103
+ if (i < currentIndex)
104
+ return chalk.green(`✓ ${step.label}`);
105
+ if (i === currentIndex)
106
+ return chalk.cyan(`● ${step.label}`);
107
+ return chalk.gray(`○ ${step.label}`);
108
+ })
109
+ .join(' ');
110
+ console.log(`\n ${progress}\n`);
111
+ }
112
+ // ============================================================================
113
+ // Setup Command Validation
114
+ // ============================================================================
50
115
  /**
51
116
  * Validate setup command options with comprehensive validation
52
117
  */
53
118
  function validateSetupCommand(options) {
54
- // Basic structure validation
55
119
  const validated = SetupCommandSchema.parse(options);
56
120
  // Business logic validation
57
- if (!validated.interactive && !validated.provider) {
58
- throw new Error('Provider required in non-interactive mode. Use --provider option.');
121
+ if (!validated.interactive && !validated.provider && !validated.quickStart) {
122
+ throw new Error('Provider required in non-interactive mode. Use --provider or --quick-start option.');
59
123
  }
60
124
  return validated;
61
125
  }
126
+ /**
127
+ * Main setup command handler
128
+ */
62
129
  export async function handleSetupCommand(options) {
63
- // Validate command with Zod
64
130
  const validated = validateSetupCommand(options);
65
131
  logger.debug(`Validated setup command options: ${JSON.stringify(validated, null, 2)}`);
66
- // Check if setup is already complete and handle accordingly
132
+ // Check if setup is already complete
67
133
  const needsSetup = await requiresSetup();
68
- if (!needsSetup) {
69
- // Setup is already complete - handle based on mode
134
+ if (!needsSetup && !validated.force) {
70
135
  if (!validated.interactive) {
71
- // Non-interactive mode: require --force flag
72
- if (!validated.force) {
73
- console.error(chalk.red('❌ Setup is already complete.'));
74
- console.error(chalk.dim(' Use --force to overwrite existing setup, or run in interactive mode for confirmation.'));
75
- process.exit(1);
76
- }
77
- logger.warn('Overwriting existing setup due to --force flag');
78
- }
79
- else {
80
- // Interactive mode: ask for confirmation
81
- p.intro(chalk.yellow('⚠️ Setup Already Complete'));
82
- p.note('Dexto is already set up and configured.\n' +
83
- 'Re-running setup will overwrite your current preferences.', 'Current Setup Detected');
84
- const shouldContinue = await p.confirm({
85
- message: 'Do you want to continue and overwrite your current setup?',
86
- initialValue: false,
87
- });
88
- if (p.isCancel(shouldContinue) || !shouldContinue) {
89
- p.cancel('Setup cancelled. Your existing configuration remains unchanged.');
90
- process.exit(0);
91
- }
92
- p.log.warn('Proceeding with setup override...');
136
+ console.error(chalk.red('❌ Setup is already complete.'));
137
+ console.error(chalk.gray(' Use --force to overwrite, or run interactively for options.'));
138
+ process.exit(1);
139
+ }
140
+ // Interactive mode: show settings menu
141
+ await showSettingsMenu();
142
+ return;
143
+ }
144
+ // Handle quick start
145
+ if (validated.quickStart) {
146
+ await handleQuickStart();
147
+ return;
148
+ }
149
+ // Handle interactive full setup
150
+ if (validated.interactive && !validated.provider) {
151
+ await handleInteractiveSetup(validated);
152
+ return;
153
+ }
154
+ // Handle non-interactive setup with provided options
155
+ await handleNonInteractiveSetup(validated);
156
+ }
157
+ /**
158
+ * Quick start flow - uses Google Gemini with minimal prompts
159
+ */
160
+ async function handleQuickStart() {
161
+ console.log(chalk.cyan('\n🚀 Quick Start with Google Gemini\n'));
162
+ p.intro(chalk.cyan('Quick Setup'));
163
+ const provider = 'google';
164
+ const model = getDefaultModelForProvider(provider) || 'gemini-2.5-pro';
165
+ const apiKeyVar = getProviderEnvVar(provider);
166
+ let apiKeySkipped = false;
167
+ // Check if API key exists
168
+ const hasKey = hasApiKeyConfigured(provider);
169
+ if (!hasKey) {
170
+ p.note(`Google Gemini is ${chalk.green('free')} to use!\n\n` +
171
+ `We'll help you get an API key in just a few seconds.`, 'Free AI Access');
172
+ const result = await interactiveApiKeySetup(provider, {
173
+ exitOnCancel: false, // Don't exit - allow skipping
174
+ model,
175
+ });
176
+ if (result.cancelled) {
177
+ p.cancel('Setup cancelled');
178
+ process.exit(0);
93
179
  }
180
+ if (result.skipped || !result.success) {
181
+ apiKeySkipped = true;
182
+ }
183
+ }
184
+ else {
185
+ p.log.success(`API key for ${getProviderDisplayName(provider)} already configured`);
94
186
  }
95
- console.log(chalk.cyan('\n🗿 Setting up Dexto...\n'));
96
- // Determine provider (interactive or from options)
97
- let provider = validated.provider;
98
- if (!provider) {
99
- provider = await selectProvider();
187
+ // Ask about default mode
188
+ const defaultMode = await selectDefaultMode();
189
+ // Handle cancellation
190
+ if (defaultMode === null) {
191
+ p.cancel('Setup cancelled');
192
+ process.exit(0);
193
+ }
194
+ // Save preferences
195
+ const preferencesOptions = {
196
+ provider,
197
+ model,
198
+ defaultMode,
199
+ setupCompleted: true,
200
+ apiKeyPending: apiKeySkipped,
201
+ };
202
+ // Only include apiKeyVar if not skipped
203
+ if (!apiKeySkipped) {
204
+ preferencesOptions.apiKeyVar = apiKeyVar;
205
+ }
206
+ const preferences = createInitialPreferences(preferencesOptions);
207
+ await saveGlobalPreferences(preferences);
208
+ capture('dexto_setup', {
209
+ provider,
210
+ model,
211
+ setupMode: 'interactive',
212
+ setupVariant: 'quick-start',
213
+ defaultMode,
214
+ apiKeySkipped,
215
+ });
216
+ showSetupComplete(provider, model, defaultMode, apiKeySkipped);
217
+ }
218
+ /**
219
+ * Full interactive setup flow with wizard navigation.
220
+ * Users can go back to previous steps to change their selections.
221
+ */
222
+ async function handleInteractiveSetup(_options) {
223
+ console.log(chalk.cyan('\n🗿 Dexto Setup\n'));
224
+ p.intro(chalk.cyan("Let's configure your AI agent"));
225
+ // Initialize wizard state
226
+ let state = { step: 'setupType' };
227
+ // Wizard loop - process steps until complete
228
+ while (state.step !== 'complete') {
229
+ switch (state.step) {
230
+ case 'setupType':
231
+ state = await wizardStepSetupType(state);
232
+ break;
233
+ case 'provider':
234
+ state = await wizardStepProvider(state);
235
+ break;
236
+ case 'model':
237
+ state = await wizardStepModel(state);
238
+ break;
239
+ case 'reasoningEffort':
240
+ state = await wizardStepReasoningEffort(state);
241
+ break;
242
+ case 'apiKey':
243
+ state = await wizardStepApiKey(state);
244
+ break;
245
+ case 'mode':
246
+ state = await wizardStepMode(state);
247
+ break;
248
+ }
249
+ }
250
+ // Save preferences and show completion
251
+ // Quick start handles its own saving, so skip for that path
252
+ if (!state.quickStartHandled) {
253
+ await saveWizardPreferences(state);
254
+ }
255
+ }
256
+ /**
257
+ * Wizard Step: Setup Type (Quick Start vs Custom)
258
+ */
259
+ async function wizardStepSetupType(state) {
260
+ const setupType = await p.select({
261
+ message: 'How would you like to set up Dexto?',
262
+ options: [
263
+ {
264
+ value: 'quick',
265
+ label: `${chalk.green('●')} Quick Start`,
266
+ hint: 'Google Gemini (free) - recommended for new users',
267
+ },
268
+ {
269
+ value: 'custom',
270
+ label: `${chalk.blue('●')} Custom Setup`,
271
+ hint: 'Choose your provider (OpenAI, Anthropic, Ollama, etc.)',
272
+ },
273
+ ],
274
+ });
275
+ if (p.isCancel(setupType)) {
276
+ p.cancel('Setup cancelled');
277
+ process.exit(0);
278
+ }
279
+ if (setupType === 'quick') {
280
+ // Quick start bypasses the wizard - handle it directly
281
+ await handleQuickStart();
282
+ return { ...state, step: 'complete', quickStartHandled: true };
283
+ }
284
+ return { ...state, step: 'provider', setupType: 'custom' };
285
+ }
286
+ /**
287
+ * Wizard Step: Provider Selection
288
+ */
289
+ async function wizardStepProvider(state) {
290
+ showStepProgress('provider', state.provider);
291
+ const provider = await selectProvider();
292
+ if (provider === null) {
293
+ p.cancel('Setup cancelled');
294
+ process.exit(0);
295
+ }
296
+ if (provider === '_back') {
297
+ return { ...state, step: 'setupType', provider: undefined };
298
+ }
299
+ return { ...state, step: 'model', provider };
300
+ }
301
+ /**
302
+ * Wizard Step: Model Selection
303
+ */
304
+ async function wizardStepModel(state) {
305
+ const provider = state.provider;
306
+ showStepProgress('model', provider);
307
+ // Handle local providers with special setup flow
308
+ if (provider === 'local') {
309
+ const localResult = await setupLocalModels();
310
+ // Only proceed if user actually selected a model
311
+ // (handles cancelled, back, skipped, and any other incomplete states)
312
+ if (!hasSelectedModel(localResult)) {
313
+ return { ...state, step: 'provider', model: undefined };
314
+ }
315
+ const model = getModelFromResult(localResult);
316
+ // Check if model supports reasoning effort
317
+ const nextStep = isReasoningCapableModel(model) ? 'reasoningEffort' : 'mode';
318
+ return { ...state, step: nextStep, model };
319
+ }
320
+ if (provider === 'ollama') {
321
+ const ollamaResult = await setupOllamaModels();
322
+ // Only proceed if user actually selected a model
323
+ if (!hasSelectedModel(ollamaResult)) {
324
+ return { ...state, step: 'provider', model: undefined };
325
+ }
326
+ const model = getModelFromResult(ollamaResult);
327
+ // Check if model supports reasoning effort
328
+ const nextStep = isReasoningCapableModel(model) ? 'reasoningEffort' : 'mode';
329
+ return { ...state, step: nextStep, model };
330
+ }
331
+ // Handle baseURL for providers that need it
332
+ let baseURL;
333
+ if (providerRequiresBaseURL(provider)) {
334
+ const result = await promptForBaseURL(provider);
335
+ if (result === null) {
336
+ // User cancelled - go back to provider selection
337
+ return { ...state, step: 'provider', model: undefined, baseURL: undefined };
338
+ }
339
+ baseURL = result;
340
+ }
341
+ // Cloud provider model selection with back option
342
+ const model = await selectModelWithBack(provider);
343
+ if (model === '_back') {
344
+ return { ...state, step: 'provider', model: undefined, baseURL: undefined };
345
+ }
346
+ // Check if model supports reasoning effort
347
+ const nextStep = isReasoningCapableModel(model) ? 'reasoningEffort' : 'apiKey';
348
+ return { ...state, step: nextStep, model, baseURL };
349
+ }
350
+ /**
351
+ * Wizard Step: Reasoning Effort Selection (for OpenAI reasoning models)
352
+ */
353
+ async function wizardStepReasoningEffort(state) {
354
+ const provider = state.provider;
355
+ const model = state.model;
356
+ const isLocalProvider = provider === 'local' || provider === 'ollama';
357
+ showStepProgress('reasoningEffort', provider, model);
358
+ const result = await p.select({
359
+ message: 'Select reasoning effort level',
360
+ options: [
361
+ {
362
+ value: 'medium',
363
+ label: 'Medium (Recommended)',
364
+ hint: 'Balanced reasoning for most tasks',
365
+ },
366
+ { value: 'low', label: 'Low', hint: 'Light reasoning, fast responses' },
367
+ {
368
+ value: 'minimal',
369
+ label: 'Minimal',
370
+ hint: 'Barely any reasoning, very fast',
371
+ },
372
+ { value: 'high', label: 'High', hint: 'More thorough reasoning' },
373
+ {
374
+ value: 'xhigh',
375
+ label: 'Extra High',
376
+ hint: 'Maximum reasoning, slower responses',
377
+ },
378
+ { value: 'none', label: 'None', hint: 'Disable reasoning' },
379
+ { value: '_back', label: chalk.gray('← Back'), hint: 'Change model' },
380
+ ],
381
+ });
382
+ if (p.isCancel(result)) {
383
+ p.cancel('Setup cancelled');
384
+ process.exit(0);
385
+ }
386
+ if (result === '_back') {
387
+ return { ...state, step: 'model', reasoningEffort: undefined };
388
+ }
389
+ // Determine next step based on provider type
390
+ const nextStep = isLocalProvider ? 'mode' : 'apiKey';
391
+ return { ...state, step: nextStep, reasoningEffort: result };
392
+ }
393
+ /**
394
+ * Wizard Step: API Key Configuration
395
+ */
396
+ async function wizardStepApiKey(state) {
397
+ const provider = state.provider;
398
+ const model = state.model;
399
+ showStepProgress('apiKey', provider, model);
400
+ const hasKey = hasApiKeyConfigured(provider);
401
+ const needsApiKey = requiresApiKey(provider);
402
+ if (needsApiKey && !hasKey) {
403
+ const result = await interactiveApiKeySetup(provider, {
404
+ exitOnCancel: false,
405
+ model,
406
+ });
407
+ if (result.cancelled) {
408
+ // Go back to reasoning effort if model supports it, otherwise model selection
409
+ const prevStep = isReasoningCapableModel(model) ? 'reasoningEffort' : 'model';
410
+ return { ...state, step: prevStep, apiKeySkipped: undefined };
411
+ }
412
+ const apiKeySkipped = result.skipped || !result.success;
413
+ return { ...state, step: 'mode', apiKeySkipped };
414
+ }
415
+ else if (needsApiKey && hasKey) {
416
+ p.log.success(`API key for ${getProviderDisplayName(provider)} already configured`);
417
+ }
418
+ else if (!needsApiKey) {
419
+ p.log.info(`${getProviderDisplayName(provider)} does not require an API key`);
420
+ }
421
+ return { ...state, step: 'mode', apiKeySkipped: false };
422
+ }
423
+ /**
424
+ * Wizard Step: Default Mode Selection
425
+ */
426
+ async function wizardStepMode(state) {
427
+ const provider = state.provider;
428
+ const model = state.model;
429
+ const isLocalProvider = provider === 'local' || provider === 'ollama';
430
+ const hasReasoningStep = isReasoningCapableModel(model);
431
+ showStepProgress('mode', provider, model);
432
+ const mode = await selectDefaultModeWithBack();
433
+ if (mode === '_back') {
434
+ // Go back to previous step based on provider type and model capabilities
435
+ if (isLocalProvider) {
436
+ // Local: reasoning effort -> model
437
+ return {
438
+ ...state,
439
+ step: hasReasoningStep ? 'reasoningEffort' : 'model',
440
+ defaultMode: undefined,
441
+ };
442
+ }
443
+ // Cloud: always go back to apiKey (reasoning effort comes before apiKey)
444
+ return { ...state, step: 'apiKey', defaultMode: undefined };
445
+ }
446
+ return { ...state, step: 'complete', defaultMode: mode };
447
+ }
448
+ /**
449
+ * Select model with back option
450
+ */
451
+ async function selectModelWithBack(provider) {
452
+ const providerInfo = LLM_REGISTRY[provider];
453
+ if (providerInfo?.models && providerInfo.models.length > 0) {
454
+ const modelOptions = providerInfo.models.map((m) => ({
455
+ value: m.name,
456
+ label: m.displayName || m.name,
457
+ }));
458
+ const result = await p.select({
459
+ message: `Select a model for ${getProviderDisplayName(provider)}`,
460
+ options: [
461
+ ...modelOptions,
462
+ { value: '_back', label: chalk.gray('← Back'), hint: 'Change provider' },
463
+ ],
464
+ });
465
+ if (p.isCancel(result)) {
466
+ p.cancel('Setup cancelled');
467
+ process.exit(0);
468
+ }
469
+ return result;
470
+ }
471
+ // For providers that accept any model, show text input with back hint
472
+ p.log.info(chalk.gray('Press Ctrl+C to go back'));
473
+ const defaultModel = providerInfo?.models?.find((m) => m.default)?.name;
474
+ const model = await p.text({
475
+ message: `Enter model name for ${getProviderDisplayName(provider)}`,
476
+ placeholder: defaultModel || 'e.g., gpt-4-turbo',
477
+ validate: (value) => {
478
+ if (!value.trim())
479
+ return 'Model name is required';
480
+ return undefined;
481
+ },
482
+ });
483
+ if (p.isCancel(model)) {
484
+ return '_back';
485
+ }
486
+ return model;
487
+ }
488
+ /**
489
+ * Select default mode with back option
490
+ */
491
+ async function selectDefaultModeWithBack() {
492
+ const result = await p.select({
493
+ message: 'How do you want to use Dexto by default?',
494
+ options: [
495
+ {
496
+ value: 'web',
497
+ label: `${chalk.blue('●')} Web UI`,
498
+ hint: 'Opens in browser at localhost:3000 (recommended)',
499
+ },
500
+ {
501
+ value: 'cli',
502
+ label: `${chalk.green('●')} Terminal CLI`,
503
+ hint: 'Interactive command-line interface',
504
+ },
505
+ {
506
+ value: 'server',
507
+ label: `${chalk.rgb(255, 165, 0)('●')} API Server`,
508
+ hint: 'REST API for programmatic access',
509
+ },
510
+ { value: '_back', label: chalk.gray('← Back'), hint: 'Go to previous step' },
511
+ ],
512
+ });
513
+ if (p.isCancel(result)) {
514
+ return '_back';
515
+ }
516
+ return result;
517
+ }
518
+ /**
519
+ * Save wizard state to preferences
520
+ */
521
+ async function saveWizardPreferences(state) {
522
+ const provider = state.provider;
523
+ const model = state.model;
524
+ const defaultMode = state.defaultMode;
525
+ const apiKeySkipped = state.apiKeySkipped || false;
526
+ const needsApiKey = requiresApiKey(provider);
527
+ const apiKeyVar = getProviderEnvVar(provider);
528
+ // For local provider, sync the active model in state.json
529
+ if (provider === 'local') {
530
+ await setActiveModel(model);
531
+ }
532
+ // Build preferences options
533
+ const preferencesOptions = {
534
+ provider,
535
+ model,
536
+ defaultMode,
537
+ setupCompleted: true,
538
+ apiKeyPending: apiKeySkipped,
539
+ };
540
+ if (needsApiKey && !apiKeySkipped) {
541
+ preferencesOptions.apiKeyVar = apiKeyVar;
100
542
  }
101
- // Get model and API key details
102
- const model = validated.model || getDefaultModelForProvider(provider);
543
+ if (state.baseURL) {
544
+ preferencesOptions.baseURL = state.baseURL;
545
+ }
546
+ if (state.reasoningEffort) {
547
+ preferencesOptions.reasoningEffort = state.reasoningEffort;
548
+ }
549
+ const preferences = createInitialPreferences(preferencesOptions);
550
+ await saveGlobalPreferences(preferences);
551
+ // Analytics
552
+ capture('dexto_setup', {
553
+ provider,
554
+ model,
555
+ setupMode: 'interactive',
556
+ setupVariant: 'custom',
557
+ defaultMode,
558
+ hasBaseURL: Boolean(state.baseURL),
559
+ apiKeySkipped,
560
+ });
561
+ showSetupComplete(provider, model, defaultMode, apiKeySkipped);
562
+ }
563
+ /**
564
+ * Non-interactive setup with CLI options
565
+ */
566
+ async function handleNonInteractiveSetup(options) {
567
+ const provider = options.provider;
568
+ const model = options.model || getDefaultModel(provider);
103
569
  if (!model) {
104
- throw new Error(`Provider '${provider}' requires a specific model. Use --model option.`);
570
+ console.error(chalk.red(`❌ Model is required for provider '${provider}'.`));
571
+ console.error(chalk.gray(` Use --model option to specify a model.`));
572
+ process.exit(1);
105
573
  }
106
- const apiKeyVar = getPrimaryApiKeyEnvVar(provider);
574
+ const apiKeyVar = getProviderEnvVar(provider);
107
575
  const hadApiKeyBefore = Boolean(resolveApiKeyForProvider(provider));
108
- const defaultAgent = validated.defaultAgent;
109
- // Create and save preferences
110
- const preferences = createInitialPreferences(provider, model, apiKeyVar, defaultAgent);
576
+ const preferences = createInitialPreferences({
577
+ provider,
578
+ model,
579
+ apiKeyVar,
580
+ defaultAgent: options.defaultAgent,
581
+ setupCompleted: true,
582
+ });
111
583
  await saveGlobalPreferences(preferences);
112
- // Track provider/model selected during setup
584
+ // For local provider, sync the active model in state.json
585
+ if (provider === 'local') {
586
+ await setActiveModel(model);
587
+ }
113
588
  capture('dexto_setup', {
114
589
  provider,
115
590
  model,
116
591
  hadApiKeyBefore,
117
- setupMode: validated.interactive ? 'interactive' : 'non-interactive',
592
+ setupMode: 'non-interactive',
118
593
  });
119
- // Setup API key interactively (only if interactive mode enabled and key doesn't exist)
120
- if (validated.interactive) {
121
- const existingApiKey = resolveApiKeyForProvider(provider);
122
- if (existingApiKey) {
123
- p.outro(chalk.green(`✅ API key for ${provider} already configured`));
594
+ console.log(chalk.green('\n✨ Setup complete! Dexto is ready to use.\n'));
595
+ }
596
+ /**
597
+ * Settings menu for users who already have setup complete.
598
+ * Loops back to menu after each action until user exits.
599
+ */
600
+ async function showSettingsMenu() {
601
+ p.intro(chalk.cyan('⚙️ Dexto Settings'));
602
+ // Settings menu loop - returns to menu after each action
603
+ while (true) {
604
+ // Load current preferences (refresh each iteration)
605
+ let currentPrefs;
606
+ try {
607
+ currentPrefs = await loadGlobalPreferences();
608
+ }
609
+ catch {
610
+ currentPrefs = null;
124
611
  }
125
- else {
126
- p.outro(chalk.cyan(`API key not found for ${provider}, starting api key setup...`));
127
- await interactiveApiKeySetup(provider);
612
+ // Show current configuration
613
+ if (currentPrefs) {
614
+ const currentConfig = [
615
+ `Provider: ${chalk.cyan(getProviderDisplayName(currentPrefs.llm.provider))}`,
616
+ `Model: ${chalk.cyan(currentPrefs.llm.model)}`,
617
+ `Default Mode: ${chalk.cyan(currentPrefs.defaults.defaultMode)}`,
618
+ ...(currentPrefs.llm.baseURL
619
+ ? [`Base URL: ${chalk.cyan(currentPrefs.llm.baseURL)}`]
620
+ : []),
621
+ ...(currentPrefs.llm.reasoningEffort
622
+ ? [`Reasoning Effort: ${chalk.cyan(currentPrefs.llm.reasoningEffort)}`]
623
+ : []),
624
+ ].join('\n');
625
+ p.note(currentConfig, 'Current Configuration');
128
626
  }
627
+ const action = await p.select({
628
+ message: 'What would you like to do?',
629
+ options: [
630
+ {
631
+ value: 'model',
632
+ label: 'Change model',
633
+ hint: `Currently: ${currentPrefs?.llm.provider || 'not set'} / ${currentPrefs?.llm.model || 'not set'}`,
634
+ },
635
+ {
636
+ value: 'mode',
637
+ label: 'Change default mode',
638
+ hint: `Currently: ${currentPrefs?.defaults.defaultMode || 'web'}`,
639
+ },
640
+ {
641
+ value: 'apikey',
642
+ label: 'Update API key',
643
+ hint: 'Re-enter your API key',
644
+ },
645
+ {
646
+ value: 'reset',
647
+ label: 'Reset to defaults',
648
+ hint: 'Start fresh with a new configuration',
649
+ },
650
+ {
651
+ value: 'file',
652
+ label: 'View preferences file',
653
+ hint: 'See where your settings are stored',
654
+ },
655
+ {
656
+ value: 'exit',
657
+ label: 'Exit',
658
+ hint: 'Done making changes',
659
+ },
660
+ ],
661
+ });
662
+ // Exit conditions
663
+ if (p.isCancel(action) || action === 'exit') {
664
+ p.outro(chalk.gray('Settings closed'));
665
+ return;
666
+ }
667
+ // Execute action and loop back (except for reset which exits)
668
+ switch (action) {
669
+ case 'model':
670
+ await changeModel(); // Always prompt for provider selection
671
+ break;
672
+ case 'mode':
673
+ await changeDefaultMode();
674
+ break;
675
+ case 'apikey':
676
+ await updateApiKey(currentPrefs?.llm.provider);
677
+ break;
678
+ case 'reset': {
679
+ // Reset exits the menu after completion, but returns to menu if cancelled
680
+ const resetCompleted = await resetSetup();
681
+ if (resetCompleted) {
682
+ return;
683
+ }
684
+ break;
685
+ }
686
+ case 'file':
687
+ showPreferencesFilePath();
688
+ break;
689
+ }
690
+ // Add separator before next iteration
691
+ console.log('');
129
692
  }
130
- console.log(chalk.green('\n✨ Setup complete! Dexto is ready to use.\n'));
693
+ }
694
+ /**
695
+ * Change model setting (includes provider selection)
696
+ */
697
+ async function changeModel(currentProvider) {
698
+ const provider = currentProvider || (await selectProvider());
699
+ // Handle cancellation or back from selectProvider
700
+ if (provider === null || provider === '_back') {
701
+ p.log.warn('Model change cancelled');
702
+ return;
703
+ }
704
+ // Special handling for local providers - use dedicated setup flows
705
+ if (provider === 'local') {
706
+ const localResult = await setupLocalModels();
707
+ // Only proceed if user actually selected a model
708
+ // (handles cancelled, back, skipped - returns to menu without saving)
709
+ if (!hasSelectedModel(localResult)) {
710
+ p.log.warn('Model change cancelled');
711
+ return;
712
+ }
713
+ const model = getModelFromResult(localResult);
714
+ const llmUpdate = {
715
+ provider,
716
+ model,
717
+ };
718
+ // Ask for reasoning effort if applicable
719
+ if (isReasoningCapableModel(model)) {
720
+ const reasoningEffort = await selectReasoningEffort();
721
+ if (reasoningEffort !== null) {
722
+ llmUpdate.reasoningEffort = reasoningEffort;
723
+ }
724
+ }
725
+ await updateGlobalPreferences({ llm: llmUpdate });
726
+ p.log.success(`Model changed to ${model}`);
727
+ return;
728
+ }
729
+ if (provider === 'ollama') {
730
+ const ollamaResult = await setupOllamaModels();
731
+ // Only proceed if user actually selected a model
732
+ if (!hasSelectedModel(ollamaResult)) {
733
+ p.log.warn('Model change cancelled');
734
+ return;
735
+ }
736
+ const model = getModelFromResult(ollamaResult);
737
+ const llmUpdate = {
738
+ provider,
739
+ model,
740
+ };
741
+ // Ask for reasoning effort if applicable
742
+ if (isReasoningCapableModel(model)) {
743
+ const reasoningEffort = await selectReasoningEffort();
744
+ if (reasoningEffort !== null) {
745
+ llmUpdate.reasoningEffort = reasoningEffort;
746
+ }
747
+ }
748
+ await updateGlobalPreferences({ llm: llmUpdate });
749
+ p.log.success(`Model changed to ${model}`);
750
+ return;
751
+ }
752
+ // Standard flow for cloud providers
753
+ const model = await selectModel(provider);
754
+ // Handle cancellation from selectModel
755
+ if (model === null) {
756
+ p.log.warn('Model change cancelled');
757
+ return;
758
+ }
759
+ const apiKeyVar = getProviderEnvVar(provider);
760
+ const needsApiKey = requiresApiKey(provider);
761
+ const llmUpdate = {
762
+ provider,
763
+ model,
764
+ };
765
+ // Only include apiKey for providers that need it
766
+ if (needsApiKey) {
767
+ llmUpdate.apiKey = `$${apiKeyVar}`;
768
+ }
769
+ // Ask for reasoning effort if applicable
770
+ if (isReasoningCapableModel(model)) {
771
+ const reasoningEffort = await selectReasoningEffort();
772
+ if (reasoningEffort !== null) {
773
+ llmUpdate.reasoningEffort = reasoningEffort;
774
+ }
775
+ }
776
+ await updateGlobalPreferences({ llm: llmUpdate });
777
+ p.log.success(`Model changed to ${model}`);
778
+ }
779
+ /**
780
+ * Change default mode setting
781
+ */
782
+ async function changeDefaultMode() {
783
+ const mode = await selectDefaultMode();
784
+ // Handle cancellation
785
+ if (mode === null) {
786
+ p.log.warn('Mode change cancelled');
787
+ return;
788
+ }
789
+ await updateGlobalPreferences({
790
+ defaults: { defaultMode: mode },
791
+ });
792
+ p.log.success(`Default mode changed to ${mode}`);
793
+ }
794
+ /**
795
+ * Update API key for current provider
796
+ */
797
+ async function updateApiKey(currentProvider) {
798
+ const provider = currentProvider || (await selectProvider());
799
+ // Handle cancellation or back from selectProvider
800
+ if (provider === null || provider === '_back') {
801
+ p.log.warn('API key update cancelled');
802
+ return;
803
+ }
804
+ // Handle providers that use non-API-key authentication
805
+ if (provider === 'vertex') {
806
+ p.note(`Google Vertex AI uses Application Default Credentials (ADC).\n\n` +
807
+ `To authenticate:\n` +
808
+ ` 1. Install gcloud CLI: https://cloud.google.com/sdk/docs/install\n` +
809
+ ` 2. Run: gcloud auth application-default login\n` +
810
+ ` 3. Set GOOGLE_VERTEX_PROJECT environment variable`, 'Google Cloud Authentication');
811
+ return;
812
+ }
813
+ if (provider === 'bedrock') {
814
+ p.note(`Amazon Bedrock uses AWS credentials.\n\n` +
815
+ `To authenticate, set these environment variables:\n` +
816
+ ` • AWS_REGION (required)\n` +
817
+ ` • AWS_ACCESS_KEY_ID (required)\n` +
818
+ ` • AWS_SECRET_ACCESS_KEY (required)\n` +
819
+ ` • AWS_SESSION_TOKEN (optional, for temporary credentials)`, 'AWS Authentication');
820
+ return;
821
+ }
822
+ // For openai-compatible and litellm, API keys are optional but allowed
823
+ // Show a note but still allow setting one
824
+ if (provider === 'openai-compatible' || provider === 'litellm') {
825
+ const wantsKey = await p.confirm({
826
+ message: `API key is optional for ${getProviderDisplayName(provider)}. Set one anyway?`,
827
+ initialValue: true,
828
+ });
829
+ if (p.isCancel(wantsKey) || !wantsKey) {
830
+ p.log.info('Skipped API key setup');
831
+ return;
832
+ }
833
+ }
834
+ const result = await interactiveApiKeySetup(provider, {
835
+ exitOnCancel: false,
836
+ skipVerification: false,
837
+ });
838
+ if (result.success) {
839
+ p.log.success(`API key updated for ${getProviderDisplayName(provider)}`);
840
+ }
841
+ else {
842
+ p.log.warn('API key update cancelled');
843
+ }
844
+ }
845
+ /**
846
+ * Reset setup to start fresh
847
+ */
848
+ async function resetSetup() {
849
+ const confirm = await p.confirm({
850
+ message: 'This will erase your current configuration. Continue?',
851
+ initialValue: false,
852
+ });
853
+ if (p.isCancel(confirm) || !confirm) {
854
+ p.log.info('Reset cancelled');
855
+ return false; // Indicate reset was cancelled
856
+ }
857
+ // Run full setup - this will complete a fresh setup
858
+ await handleInteractiveSetup({
859
+ interactive: true,
860
+ force: true,
861
+ defaultAgent: 'coding-agent',
862
+ quickStart: false,
863
+ });
864
+ return true; // Indicate reset completed
865
+ }
866
+ /**
867
+ * Show preferences file path
868
+ */
869
+ function showPreferencesFilePath() {
870
+ const prefsPath = getGlobalPreferencesPath();
871
+ p.note([
872
+ `Your preferences are stored at:`,
873
+ ``,
874
+ ` ${chalk.cyan(prefsPath)}`,
875
+ ``,
876
+ `You can edit this file directly with any text editor.`,
877
+ `Changes take effect on the next run of dexto.`,
878
+ ``,
879
+ chalk.gray('Example commands:'),
880
+ chalk.gray(` code ${prefsPath} # Open in VS Code`),
881
+ chalk.gray(` nano ${prefsPath} # Edit in terminal`),
882
+ chalk.gray(` cat ${prefsPath} # View contents`),
883
+ ].join('\n'), 'Preferences File Location');
884
+ }
885
+ /**
886
+ * Select default mode interactively
887
+ * Returns null if user cancels
888
+ */
889
+ async function selectDefaultMode() {
890
+ const mode = await p.select({
891
+ message: 'How do you want to use Dexto by default?',
892
+ options: [
893
+ {
894
+ value: 'web',
895
+ label: `${chalk.blue('●')} Web UI`,
896
+ hint: 'Opens in browser at localhost:3000 (recommended)',
897
+ },
898
+ {
899
+ value: 'cli',
900
+ label: `${chalk.green('●')} Terminal CLI`,
901
+ hint: 'Interactive command-line interface',
902
+ },
903
+ {
904
+ value: 'server',
905
+ label: `${chalk.rgb(255, 165, 0)('●')} API Server`,
906
+ hint: 'REST API for programmatic access',
907
+ },
908
+ ],
909
+ });
910
+ if (p.isCancel(mode)) {
911
+ return null;
912
+ }
913
+ return mode;
914
+ }
915
+ /**
916
+ * Select reasoning effort level for reasoning-capable models
917
+ * Used in settings menu when changing to a reasoning-capable model
918
+ */
919
+ async function selectReasoningEffort() {
920
+ const effort = await p.select({
921
+ message: 'Select reasoning effort level',
922
+ options: [
923
+ {
924
+ value: 'medium',
925
+ label: 'Medium (Recommended)',
926
+ hint: 'Balanced reasoning for most tasks',
927
+ },
928
+ { value: 'low', label: 'Low', hint: 'Light reasoning, fast responses' },
929
+ {
930
+ value: 'minimal',
931
+ label: 'Minimal',
932
+ hint: 'Barely any reasoning, very fast',
933
+ },
934
+ { value: 'high', label: 'High', hint: 'More thorough reasoning' },
935
+ {
936
+ value: 'xhigh',
937
+ label: 'Extra High',
938
+ hint: 'Maximum reasoning, slower responses',
939
+ },
940
+ { value: 'none', label: 'None', hint: 'Disable reasoning' },
941
+ ],
942
+ });
943
+ if (p.isCancel(effort)) {
944
+ return null;
945
+ }
946
+ return effort;
947
+ }
948
+ /**
949
+ * Select model interactively
950
+ * Returns null if user cancels
951
+ */
952
+ async function selectModel(provider) {
953
+ const providerInfo = LLM_REGISTRY[provider];
954
+ // For providers with a fixed model list
955
+ if (providerInfo?.models && providerInfo.models.length > 0) {
956
+ const options = providerInfo.models.map((m) => {
957
+ const option = {
958
+ value: m.name,
959
+ label: m.displayName || m.name,
960
+ };
961
+ if (m.default) {
962
+ option.hint = '(default)';
963
+ }
964
+ return option;
965
+ });
966
+ const selected = await p.select({
967
+ message: `Select a model for ${getProviderDisplayName(provider)}`,
968
+ options,
969
+ initialValue: providerInfo.models.find((m) => m.default)?.name,
970
+ });
971
+ if (p.isCancel(selected)) {
972
+ return null;
973
+ }
974
+ return selected;
975
+ }
976
+ // For providers that accept any model (openai-compatible, openrouter, etc.)
977
+ const modelInput = await p.text({
978
+ message: `Enter model name for ${getProviderDisplayName(provider)}`,
979
+ placeholder: provider === 'openrouter' ? 'e.g., anthropic/claude-3.5-sonnet' : 'e.g., llama-3-70b',
980
+ validate: (value) => {
981
+ if (!value || value.trim().length === 0) {
982
+ return 'Model name is required';
983
+ }
984
+ return undefined;
985
+ },
986
+ });
987
+ if (p.isCancel(modelInput)) {
988
+ return null;
989
+ }
990
+ return modelInput.trim();
991
+ }
992
+ /**
993
+ * Prompt for base URL for custom endpoints
994
+ * Returns null if user cancels (to go back)
995
+ */
996
+ async function promptForBaseURL(provider) {
997
+ p.log.info(chalk.gray('Press Ctrl+C to go back'));
998
+ const placeholder = provider === 'openai-compatible'
999
+ ? 'http://localhost:11434/v1'
1000
+ : provider === 'litellm'
1001
+ ? 'http://localhost:4000'
1002
+ : 'https://your-api-endpoint.com/v1';
1003
+ const baseURL = await p.text({
1004
+ message: `Enter base URL for ${getProviderDisplayName(provider)}`,
1005
+ placeholder,
1006
+ validate: (value) => {
1007
+ if (!value || value.trim().length === 0) {
1008
+ return 'Base URL is required for this provider';
1009
+ }
1010
+ try {
1011
+ new URL(value.trim());
1012
+ }
1013
+ catch {
1014
+ return 'Please enter a valid URL';
1015
+ }
1016
+ return undefined;
1017
+ },
1018
+ });
1019
+ if (p.isCancel(baseURL)) {
1020
+ return null;
1021
+ }
1022
+ return baseURL.trim();
1023
+ }
1024
+ /**
1025
+ * Show setup complete message
1026
+ */
1027
+ function showSetupComplete(provider, model, defaultMode, apiKeySkipped = false) {
1028
+ const modeCommand = defaultMode === 'web' ? 'dexto' : `dexto --mode ${defaultMode}`;
1029
+ const isLocalProvider = provider === 'local' || provider === 'ollama';
1030
+ if (apiKeySkipped) {
1031
+ console.log(chalk.rgb(255, 165, 0)('\n⚠️ Setup complete (API key pending)\n'));
1032
+ }
1033
+ else {
1034
+ console.log(chalk.green('\n✨ Setup complete! Dexto is ready to use.\n'));
1035
+ }
1036
+ const summary = [
1037
+ `${chalk.bold('Configuration:')}`,
1038
+ ` Provider: ${chalk.cyan(getProviderDisplayName(provider))}`,
1039
+ ` Model: ${chalk.cyan(model)}`,
1040
+ ` Mode: ${chalk.cyan(defaultMode)}`,
1041
+ ...(apiKeySkipped
1042
+ ? [
1043
+ ` API Key: ${chalk.rgb(255, 165, 0)('Not configured')}`,
1044
+ ``,
1045
+ `${chalk.bold('To complete setup:')}`,
1046
+ ` Run ${chalk.cyan('dexto setup')} to add your API key`,
1047
+ ` Or set ${chalk.cyan(getProviderEnvVar(provider))} in your environment`,
1048
+ ]
1049
+ : []),
1050
+ ``,
1051
+ `${chalk.bold('Next steps:')}`,
1052
+ ` Run ${chalk.cyan(modeCommand)} to start`,
1053
+ ` Run ${chalk.cyan('dexto setup')} to change settings`,
1054
+ ...(isLocalProvider
1055
+ ? [` Run ${chalk.cyan('dexto setup')} again to manage local models`]
1056
+ : []),
1057
+ ` Run ${chalk.cyan('dexto --help')} for more options`,
1058
+ ].join('\n');
1059
+ console.log(summary);
1060
+ console.log('');
131
1061
  }