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