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
@@ -0,0 +1,662 @@
1
+ // packages/cli/src/cli/utils/local-model-setup.ts
2
+ /**
3
+ * Interactive setup flow for local AI models.
4
+ *
5
+ * This module provides the setup experience when a user selects
6
+ * 'local' or 'ollama' as their provider during `dexto setup`.
7
+ */
8
+ import chalk from 'chalk';
9
+ import * as p from '@clack/prompts';
10
+ import * as fs from 'fs';
11
+ import * as path from 'path';
12
+ import { getRecommendedLocalModels, getAllLocalModels, getLocalModelById, detectGPU, formatGPUInfo, downloadModel, checkOllamaStatus, listOllamaModels, isOllamaModelAvailable, pullOllamaModel, isNodeLlamaCppInstalled, } from '@dexto/core';
13
+ import { spawn } from 'child_process';
14
+ import { getAllInstalledModels, setActiveModel, addInstalledModel, getModelsDirectory, modelFileExists, getModelFileSize, formatSize, saveCustomModel, getDextoGlobalPath, } from '@dexto/agent-management';
15
+ /**
16
+ * Type guard: Check if local model setup result has a selected model.
17
+ * Use this before proceeding with model configuration.
18
+ *
19
+ * Returns false for: cancelled, back, skipped, or missing modelId
20
+ * Returns true only when: success=true AND modelId is present
21
+ */
22
+ export function hasSelectedModel(result) {
23
+ return (result.success && !result.cancelled && !result.back && !result.skipped && !!result.modelId);
24
+ }
25
+ /**
26
+ * Install node-llama-cpp to the global deps directory (~/.dexto/deps).
27
+ * This compiles native bindings for the user's system.
28
+ * Installing globally ensures it's available for CLI, WebUI, and all projects.
29
+ */
30
+ async function installNodeLlamaCpp() {
31
+ const depsDir = getDextoGlobalPath('deps');
32
+ // Ensure deps directory exists
33
+ if (!fs.existsSync(depsDir)) {
34
+ fs.mkdirSync(depsDir, { recursive: true });
35
+ }
36
+ // Initialize package.json if it doesn't exist (required for npm install)
37
+ const packageJsonPath = path.join(depsDir, 'package.json');
38
+ if (!fs.existsSync(packageJsonPath)) {
39
+ fs.writeFileSync(packageJsonPath, JSON.stringify({
40
+ name: 'dexto-deps',
41
+ version: '1.0.0',
42
+ private: true,
43
+ description: 'Native dependencies for Dexto',
44
+ }, null, 2));
45
+ }
46
+ return new Promise((resolve) => {
47
+ // Install to global deps directory using npm
48
+ const child = spawn('npm', ['install', 'node-llama-cpp'], {
49
+ stdio: ['ignore', 'pipe', 'pipe'],
50
+ cwd: depsDir,
51
+ shell: true,
52
+ });
53
+ let stderr = '';
54
+ child.stderr?.on('data', (data) => {
55
+ stderr += data.toString();
56
+ });
57
+ child.on('close', (code) => {
58
+ if (code === 0) {
59
+ resolve(true);
60
+ }
61
+ else {
62
+ console.error(chalk.gray(stderr));
63
+ resolve(false);
64
+ }
65
+ });
66
+ child.on('error', () => {
67
+ resolve(false);
68
+ });
69
+ });
70
+ }
71
+ /**
72
+ * Check and install node-llama-cpp if needed.
73
+ * Returns true if ready to use, false if installation failed/cancelled.
74
+ */
75
+ async function ensureNodeLlamaCpp() {
76
+ const isInstalled = await isNodeLlamaCppInstalled();
77
+ if (isInstalled) {
78
+ return true;
79
+ }
80
+ p.note('Local model execution requires node-llama-cpp.\n' +
81
+ 'This will compile native bindings for your system.\n\n' +
82
+ chalk.gray('Installation may take 1-2 minutes.'), 'Dependency Required');
83
+ const shouldInstall = await p.confirm({
84
+ message: 'Install node-llama-cpp now?',
85
+ initialValue: true,
86
+ });
87
+ if (p.isCancel(shouldInstall) || !shouldInstall) {
88
+ return false;
89
+ }
90
+ const spinner = p.spinner();
91
+ spinner.start('Installing node-llama-cpp (compiling native bindings)...');
92
+ const success = await installNodeLlamaCpp();
93
+ if (success) {
94
+ spinner.stop(chalk.green('✓ node-llama-cpp installed successfully'));
95
+ return true;
96
+ }
97
+ else {
98
+ spinner.stop(chalk.red('✗ Installation failed'));
99
+ p.log.error('Failed to install node-llama-cpp. You can try manually:\n' +
100
+ chalk.gray(' npm install node-llama-cpp'));
101
+ return false;
102
+ }
103
+ }
104
+ /**
105
+ * Interactive local model setup for 'local' provider.
106
+ *
107
+ * Shows available models, offers to download, and sets up the active model.
108
+ * Uses node-llama-cpp for native GGUF model execution.
109
+ */
110
+ export async function setupLocalModels() {
111
+ console.log(chalk.cyan('\n🤖 Local Model Setup\n'));
112
+ // Ensure node-llama-cpp is installed first
113
+ const dependencyReady = await ensureNodeLlamaCpp();
114
+ if (!dependencyReady) {
115
+ p.log.warn('Setup cancelled - node-llama-cpp is required for local models.');
116
+ return { success: false, cancelled: true };
117
+ }
118
+ // Get installed models first - if already installed, we can skip other checks
119
+ const installed = await getAllInstalledModels();
120
+ const installedIds = new Set(installed.map((m) => m.id));
121
+ // Check if any models are already installed - offer quick path
122
+ if (installed.length > 0) {
123
+ const useExisting = await p.confirm({
124
+ message: `You have ${installed.length} model(s) installed. Use an existing model?`,
125
+ initialValue: true,
126
+ });
127
+ if (p.isCancel(useExisting)) {
128
+ return { success: false, cancelled: true };
129
+ }
130
+ if (useExisting) {
131
+ // Let user select from installed models - no additional setup needed
132
+ const selected = await selectInstalledModel(installed);
133
+ if (selected.cancelled) {
134
+ return { success: false, cancelled: true };
135
+ }
136
+ if (selected.customGGUF) {
137
+ // User wants to use a custom GGUF file
138
+ return setupCustomGGUF();
139
+ }
140
+ if (selected.modelId) {
141
+ await setActiveModel(selected.modelId);
142
+ p.log.success(`Using ${selected.modelId} as active model`);
143
+ return { success: true, modelId: selected.modelId };
144
+ }
145
+ }
146
+ }
147
+ // Only detect GPU if we're going to show model recommendations
148
+ const gpuInfo = await detectGPU();
149
+ console.log(chalk.gray(`GPU detected: ${formatGPUInfo(gpuInfo)}\n`));
150
+ // Get recommended models
151
+ const recommendedModels = getRecommendedLocalModels();
152
+ // Build options with install status
153
+ const modelOptions = recommendedModels.map((model) => {
154
+ const isInstalled = installedIds.has(model.id);
155
+ const statusIcon = isInstalled ? chalk.green('✓') : chalk.gray('○');
156
+ const vramHint = model.minVRAM ? `${model.minVRAM}GB+ VRAM` : 'CPU OK';
157
+ return {
158
+ value: model.id,
159
+ label: `${statusIcon} ${model.name}`,
160
+ hint: `${formatSize(model.sizeBytes)} | ${vramHint}${isInstalled ? ' (installed)' : ''}`,
161
+ };
162
+ });
163
+ // Add option to see all models
164
+ modelOptions.push({
165
+ value: '_all_models',
166
+ label: `${chalk.blue('...')} Show all available models`,
167
+ hint: `${getAllLocalModels().length} models available`,
168
+ });
169
+ // Add option to use custom GGUF file
170
+ modelOptions.push({
171
+ value: '_custom_gguf',
172
+ label: `${chalk.blue('...')} Use custom GGUF file`,
173
+ hint: 'For GGUF files not in registry',
174
+ });
175
+ // Add option to skip
176
+ modelOptions.push({
177
+ value: '_skip',
178
+ label: `${chalk.rgb(255, 165, 0)('→')} Skip for now`,
179
+ hint: 'Configure later with: dexto setup',
180
+ });
181
+ // Add back option
182
+ modelOptions.push({
183
+ value: '_back',
184
+ label: chalk.gray('← Back'),
185
+ hint: 'Choose a different provider',
186
+ });
187
+ p.note('Local models run completely on your machine - free, private, and offline.\n' +
188
+ 'Select a model to download (or use an existing one).', 'Local AI');
189
+ const selected = await p.select({
190
+ message: 'Choose a model to use',
191
+ options: modelOptions,
192
+ });
193
+ if (p.isCancel(selected)) {
194
+ return { success: false, cancelled: true };
195
+ }
196
+ if (selected === '_skip') {
197
+ p.log.info(chalk.gray('Skipped model selection. Use `dexto setup` to configure later.'));
198
+ return { success: true, skipped: true };
199
+ }
200
+ if (selected === '_back') {
201
+ return { success: false, back: true };
202
+ }
203
+ if (selected === '_all_models') {
204
+ // Show all models
205
+ return await showAllModelsSelection(installedIds);
206
+ }
207
+ if (selected === '_custom_gguf') {
208
+ // Use custom GGUF file
209
+ return setupCustomGGUF();
210
+ }
211
+ const modelId = selected;
212
+ // Check if already installed
213
+ if (installedIds.has(modelId)) {
214
+ await setActiveModel(modelId);
215
+ p.log.success(`Using ${modelId} as active model`);
216
+ return { success: true, modelId };
217
+ }
218
+ // Download the model
219
+ const downloadResult = await downloadModelInteractive(modelId);
220
+ if (!downloadResult.success) {
221
+ if (downloadResult.cancelled) {
222
+ return { success: false, cancelled: true };
223
+ }
224
+ return { success: false };
225
+ }
226
+ // Set as active
227
+ await setActiveModel(modelId);
228
+ return { success: true, modelId };
229
+ }
230
+ /**
231
+ * Check if Ollama model is available, offer to pull if not.
232
+ * Returns true if model is ready to use, false if user declined pull or pull failed.
233
+ */
234
+ async function ensureOllamaModelAvailable(modelName) {
235
+ // Check if model is already available
236
+ const isAvailable = await isOllamaModelAvailable(modelName);
237
+ if (isAvailable) {
238
+ return true;
239
+ }
240
+ // Model not found - offer to pull it
241
+ console.log(chalk.rgb(255, 165, 0)(`\n⚠️ Model '${modelName}' is not available locally.\n`));
242
+ const shouldPull = await p.confirm({
243
+ message: `Pull '${modelName}' from Ollama now?`,
244
+ initialValue: true,
245
+ });
246
+ if (p.isCancel(shouldPull) || !shouldPull) {
247
+ p.log.warn('Skipping model pull. You can pull it later with: ollama pull ' + modelName);
248
+ return false;
249
+ }
250
+ // Pull the model with progress display
251
+ const spinner = p.spinner();
252
+ spinner.start(`Pulling ${modelName} from Ollama...`);
253
+ try {
254
+ await pullOllamaModel(modelName, undefined, (progress) => {
255
+ // Update spinner with progress (show percentage if available)
256
+ if (progress.completed && progress.total) {
257
+ const percent = Math.round((progress.completed / progress.total) * 100);
258
+ const sizeDownloaded = formatSize(progress.completed);
259
+ const sizeTotal = formatSize(progress.total);
260
+ spinner.message(`Pulling ${modelName}... ${percent}% (${sizeDownloaded}/${sizeTotal}) - ${progress.status}`);
261
+ }
262
+ else {
263
+ spinner.message(`Pulling ${modelName}... ${progress.status}`);
264
+ }
265
+ });
266
+ spinner.stop(chalk.green(`✓ Successfully pulled ${modelName}`));
267
+ return true;
268
+ }
269
+ catch (error) {
270
+ spinner.stop(chalk.red('✗ Failed to pull model'));
271
+ console.error(chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
272
+ p.log.warn('You can try pulling manually: ollama pull ' + modelName);
273
+ return false;
274
+ }
275
+ }
276
+ /**
277
+ * Interactive Ollama model setup for 'ollama' provider.
278
+ */
279
+ export async function setupOllamaModels() {
280
+ console.log(chalk.cyan('\n🦙 Ollama Setup\n'));
281
+ // Check if Ollama is running
282
+ const status = await checkOllamaStatus();
283
+ if (!status.running) {
284
+ p.note(chalk.rgb(255, 165, 0)('Ollama server is not running.\n\n') +
285
+ 'To use Ollama:\n' +
286
+ ' 1. Install Ollama: https://ollama.com/download\n' +
287
+ ' 2. Start the server: ollama serve\n' +
288
+ ' 3. Pull a model: ollama pull llama3.2', 'Ollama Required');
289
+ const proceed = await p.confirm({
290
+ message: 'Continue setup anyway? (You can configure Ollama later)',
291
+ initialValue: true,
292
+ });
293
+ if (p.isCancel(proceed)) {
294
+ return { success: false, cancelled: true };
295
+ }
296
+ if (!proceed) {
297
+ return { success: false };
298
+ }
299
+ // Let them specify a model name even without Ollama running
300
+ const modelName = await p.text({
301
+ message: 'Enter the Ollama model name to use',
302
+ placeholder: 'llama3.2',
303
+ initialValue: 'llama3.2',
304
+ });
305
+ if (p.isCancel(modelName)) {
306
+ return { success: false, cancelled: true };
307
+ }
308
+ return { success: true, modelId: modelName.trim() };
309
+ }
310
+ // Ollama is running - show available models
311
+ console.log(chalk.green(`✓ Ollama ${status.version || ''} running at ${status.url}\n`));
312
+ const ollamaModels = await listOllamaModels();
313
+ if (ollamaModels.length === 0) {
314
+ p.note('No models found in Ollama.\n\n' +
315
+ 'To pull a model:\n' +
316
+ ' ollama pull llama3.2\n\n' +
317
+ 'Popular models:\n' +
318
+ ' • llama3.2 (3B/8B general)\n' +
319
+ ' • qwen2.5-coder (coding)\n' +
320
+ ' • mistral (7B general)', 'No Models');
321
+ const modelName = await p.text({
322
+ message: 'Enter the model name to pull',
323
+ placeholder: 'llama3.2',
324
+ initialValue: 'llama3.2',
325
+ });
326
+ if (p.isCancel(modelName)) {
327
+ return { success: false, cancelled: true };
328
+ }
329
+ const trimmedName = modelName.trim();
330
+ const isReady = await ensureOllamaModelAvailable(trimmedName);
331
+ if (!isReady) {
332
+ // User declined pull or pull failed
333
+ return { success: false };
334
+ }
335
+ return { success: true, modelId: trimmedName };
336
+ }
337
+ // Show available Ollama models
338
+ const modelOptions = ollamaModels.map((model) => ({
339
+ value: model.name,
340
+ label: model.name,
341
+ hint: formatSize(model.size),
342
+ }));
343
+ // Add option to enter custom name
344
+ modelOptions.push({
345
+ value: '_custom',
346
+ label: `${chalk.blue('...')} Enter custom model name`,
347
+ hint: 'For models not yet pulled',
348
+ });
349
+ // Add back option
350
+ modelOptions.push({
351
+ value: '_back',
352
+ label: chalk.gray('← Back'),
353
+ hint: 'Choose a different provider',
354
+ });
355
+ const selected = await p.select({
356
+ message: 'Select an Ollama model',
357
+ options: modelOptions,
358
+ });
359
+ if (p.isCancel(selected)) {
360
+ return { success: false, cancelled: true };
361
+ }
362
+ if (selected === '_back') {
363
+ return { success: false, back: true };
364
+ }
365
+ if (selected === '_custom') {
366
+ const modelName = await p.text({
367
+ message: 'Enter the Ollama model name',
368
+ placeholder: 'llama3.2:70b',
369
+ });
370
+ if (p.isCancel(modelName)) {
371
+ return { success: false, cancelled: true };
372
+ }
373
+ const trimmedName = modelName.trim();
374
+ const isReady = await ensureOllamaModelAvailable(trimmedName);
375
+ if (!isReady) {
376
+ // User declined pull or pull failed
377
+ return { success: false };
378
+ }
379
+ return { success: true, modelId: trimmedName };
380
+ }
381
+ return { success: true, modelId: selected };
382
+ }
383
+ /**
384
+ * Select from installed models
385
+ */
386
+ async function selectInstalledModel(installed) {
387
+ const options = installed.map((model) => ({
388
+ value: model.id,
389
+ label: model.id,
390
+ hint: formatSize(model.sizeBytes),
391
+ }));
392
+ options.push({
393
+ value: '_download_new',
394
+ label: `${chalk.blue('+')} Download a new model`,
395
+ hint: 'Browse available models',
396
+ });
397
+ options.push({
398
+ value: '_custom_gguf',
399
+ label: `${chalk.blue('...')} Use custom GGUF file`,
400
+ hint: 'For GGUF files not in registry',
401
+ });
402
+ const selected = await p.select({
403
+ message: 'Select a model',
404
+ options,
405
+ });
406
+ if (p.isCancel(selected)) {
407
+ return { cancelled: true };
408
+ }
409
+ if (selected === '_download_new') {
410
+ return {}; // Continue to download flow
411
+ }
412
+ if (selected === '_custom_gguf') {
413
+ return { customGGUF: true };
414
+ }
415
+ return { modelId: selected };
416
+ }
417
+ /**
418
+ * Show all available models for selection
419
+ */
420
+ async function showAllModelsSelection(installedIds) {
421
+ const allModels = getAllLocalModels();
422
+ const modelOptions = allModels.map((model) => {
423
+ const isInstalled = installedIds.has(model.id);
424
+ const statusIcon = isInstalled ? chalk.green('✓') : chalk.gray('○');
425
+ const category = model.categories?.[0] || 'general';
426
+ const vramHint = model.minVRAM ? `${model.minVRAM}GB+` : 'CPU';
427
+ return {
428
+ value: model.id,
429
+ label: `${statusIcon} ${model.name}`,
430
+ hint: `${category} | ${formatSize(model.sizeBytes)} | ${vramHint}${isInstalled ? ' (installed)' : ''}`,
431
+ };
432
+ });
433
+ modelOptions.push({
434
+ value: '_back',
435
+ label: `${chalk.rgb(255, 165, 0)('←')} Back`,
436
+ hint: 'Return to recommended models',
437
+ });
438
+ const selected = await p.select({
439
+ message: 'Select a model',
440
+ options: modelOptions,
441
+ });
442
+ if (p.isCancel(selected)) {
443
+ return { success: false, cancelled: true };
444
+ }
445
+ if (selected === '_back') {
446
+ // Recurse back to main setup
447
+ return setupLocalModels();
448
+ }
449
+ const modelId = selected;
450
+ // Check if already installed
451
+ if (installedIds.has(modelId)) {
452
+ await setActiveModel(modelId);
453
+ p.log.success(`Using ${modelId} as active model`);
454
+ return { success: true, modelId };
455
+ }
456
+ // Download the model
457
+ const downloadResult = await downloadModelInteractive(modelId);
458
+ if (!downloadResult.success) {
459
+ if (downloadResult.cancelled) {
460
+ return { success: false, cancelled: true };
461
+ }
462
+ return { success: false };
463
+ }
464
+ await setActiveModel(modelId);
465
+ return { success: true, modelId };
466
+ }
467
+ /**
468
+ * Download a model with interactive progress
469
+ */
470
+ async function downloadModelInteractive(modelId) {
471
+ const modelInfo = getLocalModelById(modelId);
472
+ if (!modelInfo) {
473
+ p.log.error(`Model '${modelId}' not found in registry`);
474
+ return { success: false };
475
+ }
476
+ // Check if model file already exists on disk (but not registered)
477
+ // First check the expected subdirectory, then fallback to root models dir
478
+ const fileExistsInSubdir = await modelFileExists(modelId, modelInfo.filename);
479
+ const rootFilePath = `${getModelsDirectory()}/${modelInfo.filename}`;
480
+ let actualFilePath = null;
481
+ let fileSize = null;
482
+ if (fileExistsInSubdir) {
483
+ actualFilePath = `${getModelsDirectory()}/${modelId}/${modelInfo.filename}`;
484
+ fileSize = await getModelFileSize(modelId, modelInfo.filename);
485
+ }
486
+ else {
487
+ // Check root models directory (legacy or manual placement)
488
+ try {
489
+ const fs = await import('fs/promises');
490
+ const stats = await fs.stat(rootFilePath);
491
+ if (stats.isFile()) {
492
+ actualFilePath = rootFilePath;
493
+ fileSize = stats.size;
494
+ }
495
+ }
496
+ catch {
497
+ // File doesn't exist in root either
498
+ }
499
+ }
500
+ if (actualFilePath) {
501
+ p.log.info(chalk.green(`✓ Model file already exists on disk`));
502
+ // Register the existing model
503
+ const installedModel = {
504
+ id: modelId,
505
+ filePath: actualFilePath,
506
+ sizeBytes: fileSize ?? modelInfo.sizeBytes,
507
+ downloadedAt: new Date().toISOString(),
508
+ source: 'huggingface',
509
+ filename: modelInfo.filename,
510
+ };
511
+ await addInstalledModel(installedModel);
512
+ p.log.success(`Model '${modelId}' registered successfully`);
513
+ return { success: true };
514
+ }
515
+ // Show model info and confirm
516
+ p.note(`${modelInfo.name}\n` +
517
+ `${modelInfo.description}\n\n` +
518
+ `Size: ${formatSize(modelInfo.sizeBytes)}\n` +
519
+ `Context: ${modelInfo.contextLength.toLocaleString()} tokens\n` +
520
+ `Quantization: ${modelInfo.quantization}`, 'Model Details');
521
+ const confirmed = await p.confirm({
522
+ message: `Download ${modelInfo.name} (${formatSize(modelInfo.sizeBytes)})?`,
523
+ });
524
+ if (p.isCancel(confirmed) || !confirmed) {
525
+ return { success: false, cancelled: true };
526
+ }
527
+ // Start download with spinner
528
+ const spinner = p.spinner();
529
+ spinner.start('Starting download...');
530
+ try {
531
+ const result = await downloadModel(modelId, {
532
+ targetDir: getModelsDirectory(),
533
+ events: {
534
+ onProgress: (progress) => {
535
+ const pct = progress.percentage.toFixed(1);
536
+ const downloaded = formatSize(progress.bytesDownloaded);
537
+ const total = formatSize(progress.totalBytes);
538
+ const speedStr = progress.speed ? `${formatSize(progress.speed)}/s` : '';
539
+ const etaStr = progress.eta ? `ETA: ${Math.round(progress.eta)}s` : '';
540
+ spinner.message(`${pct}% (${downloaded}/${total}) ${speedStr} ${etaStr}`);
541
+ },
542
+ onComplete: () => {
543
+ spinner.stop(chalk.green(`✓ Downloaded ${modelInfo.name}`));
544
+ },
545
+ onError: (_modelId, error) => {
546
+ spinner.stop(chalk.red(`✗ Download failed: ${error.message}`));
547
+ },
548
+ },
549
+ });
550
+ // Register the installed model
551
+ const installedModel = {
552
+ id: modelId,
553
+ filePath: result.filePath,
554
+ sizeBytes: result.sizeBytes,
555
+ downloadedAt: new Date().toISOString(),
556
+ source: 'huggingface',
557
+ filename: modelInfo.filename,
558
+ };
559
+ if (result.sha256) {
560
+ installedModel.sha256 = result.sha256;
561
+ }
562
+ await addInstalledModel(installedModel);
563
+ p.log.success(`Model '${modelId}' installed successfully`);
564
+ return { success: true };
565
+ }
566
+ catch (error) {
567
+ spinner.stop(chalk.red('Download failed'));
568
+ p.log.error(`Failed to download: ${error instanceof Error ? error.message : String(error)}`);
569
+ return { success: false };
570
+ }
571
+ }
572
+ /**
573
+ * Setup a custom GGUF file.
574
+ * Prompts user for file path, validates it, and saves as a custom model.
575
+ * Mirrors the Ollama "Enter custom model name" pattern.
576
+ */
577
+ async function setupCustomGGUF() {
578
+ // Prompt for file path
579
+ const filePath = await p.text({
580
+ message: 'Enter path to GGUF file',
581
+ placeholder: '/path/to/model.gguf',
582
+ validate: (value) => {
583
+ if (!value.trim()) {
584
+ return 'File path is required';
585
+ }
586
+ if (!value.endsWith('.gguf')) {
587
+ return 'File must have .gguf extension';
588
+ }
589
+ if (!path.isAbsolute(value)) {
590
+ return 'Please enter an absolute path';
591
+ }
592
+ return undefined;
593
+ },
594
+ });
595
+ if (p.isCancel(filePath)) {
596
+ return { success: false, cancelled: true };
597
+ }
598
+ const trimmedPath = filePath.trim();
599
+ // Validate file exists
600
+ try {
601
+ const stats = fs.statSync(trimmedPath);
602
+ if (!stats.isFile()) {
603
+ p.log.error('Path is not a file');
604
+ return { success: false };
605
+ }
606
+ const sizeBytes = stats.size;
607
+ const filename = path.basename(trimmedPath, '.gguf');
608
+ console.log(chalk.green(`\n✓ Found: ${path.basename(trimmedPath)} (${formatSize(sizeBytes)})\n`));
609
+ // Prompt for display name (optional)
610
+ const displayName = await p.text({
611
+ message: 'Display name (optional)',
612
+ placeholder: filename,
613
+ initialValue: filename,
614
+ });
615
+ if (p.isCancel(displayName)) {
616
+ return { success: false, cancelled: true };
617
+ }
618
+ // Note: Context length is auto-detected by node-llama-cpp from the GGUF file
619
+ // Generate a model ID from the filename
620
+ // Convert to lowercase, replace spaces with dashes, remove special chars
621
+ let modelId = filename
622
+ .toLowerCase()
623
+ .replace(/\s+/g, '-')
624
+ .replace(/[^a-z0-9-]/g, '')
625
+ .substring(0, 50); // Limit length
626
+ // Fallback if modelId is empty after sanitization
627
+ if (!modelId) {
628
+ modelId = `custom-model-${Date.now()}`;
629
+ }
630
+ // Save as custom model
631
+ await saveCustomModel({
632
+ name: modelId,
633
+ provider: 'local',
634
+ filePath: trimmedPath,
635
+ displayName: displayName?.trim() || filename,
636
+ });
637
+ p.log.success(`Registered as '${modelId}'`);
638
+ return { success: true, modelId };
639
+ }
640
+ catch (error) {
641
+ const nodeError = error;
642
+ if (nodeError.code === 'ENOENT') {
643
+ p.log.error('File not found');
644
+ }
645
+ else if (nodeError.code === 'EACCES') {
646
+ p.log.error('Permission denied - file is not readable');
647
+ }
648
+ else {
649
+ p.log.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
650
+ }
651
+ return { success: false };
652
+ }
653
+ }
654
+ /**
655
+ * Get the model name for preferences from a validated setup result.
656
+ *
657
+ * IMPORTANT: Only call this after validating with hasSelectedModel().
658
+ * Throws if modelId is missing (indicates a bug in the calling code).
659
+ */
660
+ export function getModelFromResult(result) {
661
+ return result.modelId;
662
+ }
@@ -85,7 +85,7 @@ export function handleCliOptionsError(error) {
85
85
  const fieldName = err.path.join('.') || 'Unknown Option';
86
86
  console.error(chalk.red(` • Option '${fieldName}': ${err.message}`));
87
87
  });
88
- console.error(chalk.dim('\nPlease check your command-line arguments or run with --help for usage details.'));
88
+ console.error(chalk.gray('\nPlease check your command-line arguments or run with --help for usage details.'));
89
89
  }
90
90
  else {
91
91
  console.error(chalk.red(`❌ Validation error: ${error instanceof Error ? error.message : JSON.stringify(error)}`));