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.
- package/README.md +62 -7
- package/dist/agents/agent-template.yml +2 -2
- package/dist/agents/coding-agent/coding-agent.yml +22 -16
- package/dist/agents/database-agent/database-agent.yml +2 -2
- package/dist/agents/default-agent.yml +7 -5
- package/dist/agents/github-agent/github-agent.yml +2 -2
- package/dist/agents/product-name-researcher/product-name-researcher.yml +2 -2
- package/dist/agents/talk2pdf-agent/talk2pdf-agent.yml +2 -2
- package/dist/analytics/events.d.ts +13 -6
- package/dist/analytics/events.d.ts.map +1 -1
- package/dist/analytics/index.d.ts +1 -1
- package/dist/analytics/index.d.ts.map +1 -1
- package/dist/analytics/index.js +6 -2
- package/dist/api/server-hono.d.ts.map +1 -1
- package/dist/api/server-hono.js +27 -5
- package/dist/cli/cli-subscriber.d.ts +4 -0
- package/dist/cli/cli-subscriber.d.ts.map +1 -1
- package/dist/cli/cli-subscriber.js +40 -2
- package/dist/cli/commands/create-app.d.ts +16 -14
- package/dist/cli/commands/create-app.d.ts.map +1 -1
- package/dist/cli/commands/create-app.js +626 -102
- package/dist/cli/commands/create-image.d.ts +7 -0
- package/dist/cli/commands/create-image.d.ts.map +1 -0
- package/dist/cli/commands/create-image.js +201 -0
- package/dist/cli/commands/helpers/formatters.js +7 -7
- package/dist/cli/commands/index.d.ts +2 -1
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +2 -1
- package/dist/cli/commands/init-app.js +7 -7
- package/dist/cli/commands/install.d.ts +0 -3
- package/dist/cli/commands/install.d.ts.map +1 -1
- package/dist/cli/commands/install.js +10 -35
- package/dist/cli/commands/interactive-commands/command-parser.js +7 -7
- package/dist/cli/commands/interactive-commands/general-commands.js +1 -1
- package/dist/cli/commands/interactive-commands/prompt-commands.js +11 -11
- package/dist/cli/commands/interactive-commands/system/system-commands.js +3 -3
- package/dist/cli/commands/list-agents.js +2 -2
- package/dist/cli/commands/session-commands.js +16 -16
- package/dist/cli/commands/setup.d.ts +13 -5
- package/dist/cli/commands/setup.d.ts.map +1 -1
- package/dist/cli/commands/setup.js +860 -65
- package/dist/cli/commands/which.js +1 -1
- package/dist/cli/ink-cli/components/ApprovalPrompt.d.ts +2 -0
- package/dist/cli/ink-cli/components/ApprovalPrompt.d.ts.map +1 -1
- package/dist/cli/ink-cli/components/ApprovalPrompt.js +29 -7
- package/dist/cli/ink-cli/components/CustomInput.js +1 -1
- package/dist/cli/ink-cli/components/EditableMultiLineInput.js +4 -4
- package/dist/cli/ink-cli/components/ElicitationForm.d.ts.map +1 -1
- package/dist/cli/ink-cli/components/ElicitationForm.js +6 -6
- package/dist/cli/ink-cli/components/ErrorBoundary.d.ts.map +1 -1
- package/dist/cli/ink-cli/components/ErrorBoundary.js +1 -1
- package/dist/cli/ink-cli/components/Footer.d.ts.map +1 -1
- package/dist/cli/ink-cli/components/Footer.js +1 -1
- package/dist/cli/ink-cli/components/HistorySearchBar.d.ts.map +1 -1
- package/dist/cli/ink-cli/components/HistorySearchBar.js +1 -1
- package/dist/cli/ink-cli/components/MultiLineInput.d.ts.map +1 -1
- package/dist/cli/ink-cli/components/MultiLineInput.js +3 -3
- package/dist/cli/ink-cli/components/ResourceAutocomplete.d.ts.map +1 -1
- package/dist/cli/ink-cli/components/ResourceAutocomplete.js +4 -4
- package/dist/cli/ink-cli/components/SlashCommandAutocomplete.js +3 -3
- package/dist/cli/ink-cli/components/StatusBar.d.ts.map +1 -1
- package/dist/cli/ink-cli/components/StatusBar.js +7 -5
- package/dist/cli/ink-cli/components/TextBufferInput.d.ts.map +1 -1
- package/dist/cli/ink-cli/components/TextBufferInput.js +6 -6
- package/dist/cli/ink-cli/components/base/BaseAutocomplete.js +4 -4
- package/dist/cli/ink-cli/components/base/BaseSelector.js +2 -2
- package/dist/cli/ink-cli/components/chat/Footer.js +1 -1
- package/dist/cli/ink-cli/components/chat/Header.d.ts.map +1 -1
- package/dist/cli/ink-cli/components/chat/Header.js +2 -4
- package/dist/cli/ink-cli/components/chat/MessageItem.d.ts.map +1 -1
- package/dist/cli/ink-cli/components/chat/MessageItem.js +5 -5
- package/dist/cli/ink-cli/components/chat/MessageList.js +1 -1
- package/dist/cli/ink-cli/components/chat/QueuedMessagesDisplay.js +1 -1
- package/dist/cli/ink-cli/components/chat/ToolIcon.d.ts +1 -1
- package/dist/cli/ink-cli/components/chat/ToolIcon.js +4 -4
- package/dist/cli/ink-cli/components/chat/styled-boxes/ConfigBox.js +1 -1
- package/dist/cli/ink-cli/components/chat/styled-boxes/HelpBox.js +1 -1
- package/dist/cli/ink-cli/components/chat/styled-boxes/LogConfigBox.js +2 -2
- package/dist/cli/ink-cli/components/chat/styled-boxes/SessionHistoryBox.js +5 -5
- package/dist/cli/ink-cli/components/chat/styled-boxes/SessionListBox.js +2 -2
- package/dist/cli/ink-cli/components/chat/styled-boxes/ShortcutsBox.js +1 -1
- package/dist/cli/ink-cli/components/chat/styled-boxes/StatsBox.js +1 -1
- package/dist/cli/ink-cli/components/chat/styled-boxes/StyledBox.js +2 -2
- package/dist/cli/ink-cli/components/modes/AlternateBufferCLI.d.ts.map +1 -1
- package/dist/cli/ink-cli/components/modes/AlternateBufferCLI.js +1 -1
- package/dist/cli/ink-cli/components/modes/StaticCLI.d.ts.map +1 -1
- package/dist/cli/ink-cli/components/modes/StaticCLI.js +1 -1
- package/dist/cli/ink-cli/components/overlays/ApiKeyInput.js +1 -1
- package/dist/cli/ink-cli/components/overlays/CustomModelWizard.d.ts +10 -2
- package/dist/cli/ink-cli/components/overlays/CustomModelWizard.d.ts.map +1 -1
- package/dist/cli/ink-cli/components/overlays/CustomModelWizard.js +198 -89
- package/dist/cli/ink-cli/components/overlays/LogLevelSelector.d.ts.map +1 -1
- package/dist/cli/ink-cli/components/overlays/LogLevelSelector.js +2 -2
- package/dist/cli/ink-cli/components/overlays/McpAddChoice.d.ts.map +1 -1
- package/dist/cli/ink-cli/components/overlays/McpAddChoice.js +1 -1
- package/dist/cli/ink-cli/components/overlays/McpAddSelector.js +1 -1
- package/dist/cli/ink-cli/components/overlays/McpCustomTypeSelector.d.ts.map +1 -1
- package/dist/cli/ink-cli/components/overlays/McpCustomTypeSelector.js +2 -2
- package/dist/cli/ink-cli/components/overlays/McpCustomWizard.js +1 -1
- package/dist/cli/ink-cli/components/overlays/McpRemoveSelector.d.ts.map +1 -1
- package/dist/cli/ink-cli/components/overlays/McpRemoveSelector.js +1 -1
- package/dist/cli/ink-cli/components/overlays/McpSelector.d.ts.map +1 -1
- package/dist/cli/ink-cli/components/overlays/McpSelector.js +1 -1
- package/dist/cli/ink-cli/components/overlays/McpServerActions.d.ts.map +1 -1
- package/dist/cli/ink-cli/components/overlays/McpServerActions.js +2 -2
- package/dist/cli/ink-cli/components/overlays/McpServerList.d.ts.map +1 -1
- package/dist/cli/ink-cli/components/overlays/McpServerList.js +1 -1
- package/dist/cli/ink-cli/components/overlays/ModelSelectorRefactored.d.ts +5 -5
- package/dist/cli/ink-cli/components/overlays/ModelSelectorRefactored.d.ts.map +1 -1
- package/dist/cli/ink-cli/components/overlays/ModelSelectorRefactored.js +222 -68
- package/dist/cli/ink-cli/components/overlays/PromptAddChoice.d.ts.map +1 -1
- package/dist/cli/ink-cli/components/overlays/PromptAddChoice.js +2 -2
- package/dist/cli/ink-cli/components/overlays/PromptAddWizard.js +1 -1
- package/dist/cli/ink-cli/components/overlays/PromptDeleteSelector.d.ts.map +1 -1
- package/dist/cli/ink-cli/components/overlays/PromptDeleteSelector.js +2 -2
- package/dist/cli/ink-cli/components/overlays/PromptList.d.ts.map +1 -1
- package/dist/cli/ink-cli/components/overlays/PromptList.js +2 -2
- package/dist/cli/ink-cli/components/overlays/SearchOverlay.d.ts.map +1 -1
- package/dist/cli/ink-cli/components/overlays/SearchOverlay.js +4 -4
- package/dist/cli/ink-cli/components/overlays/SessionSubcommandSelector.d.ts.map +1 -1
- package/dist/cli/ink-cli/components/overlays/SessionSubcommandSelector.js +1 -1
- package/dist/cli/ink-cli/components/overlays/StreamSelector.d.ts.map +1 -1
- package/dist/cli/ink-cli/components/overlays/StreamSelector.js +1 -1
- package/dist/cli/ink-cli/components/overlays/ToolBrowser.js +12 -12
- package/dist/cli/ink-cli/components/overlays/custom-model-wizard/LocalModelWizard.d.ts +25 -0
- package/dist/cli/ink-cli/components/overlays/custom-model-wizard/LocalModelWizard.d.ts.map +1 -0
- package/dist/cli/ink-cli/components/overlays/custom-model-wizard/LocalModelWizard.js +609 -0
- package/dist/cli/ink-cli/components/overlays/custom-model-wizard/index.d.ts +15 -0
- package/dist/cli/ink-cli/components/overlays/custom-model-wizard/index.d.ts.map +1 -0
- package/dist/cli/ink-cli/components/overlays/custom-model-wizard/index.js +14 -0
- package/dist/cli/ink-cli/components/overlays/custom-model-wizard/provider-config.d.ts +33 -0
- package/dist/cli/ink-cli/components/overlays/custom-model-wizard/provider-config.d.ts.map +1 -0
- package/dist/cli/ink-cli/components/overlays/custom-model-wizard/provider-config.js +419 -0
- package/dist/cli/ink-cli/components/overlays/custom-model-wizard/shared/ApiKeyStep.d.ts +25 -0
- package/dist/cli/ink-cli/components/overlays/custom-model-wizard/shared/ApiKeyStep.d.ts.map +1 -0
- package/dist/cli/ink-cli/components/overlays/custom-model-wizard/shared/ApiKeyStep.js +29 -0
- package/dist/cli/ink-cli/components/overlays/custom-model-wizard/shared/ProviderSelector.d.ts +17 -0
- package/dist/cli/ink-cli/components/overlays/custom-model-wizard/shared/ProviderSelector.d.ts.map +1 -0
- package/dist/cli/ink-cli/components/overlays/custom-model-wizard/shared/ProviderSelector.js +11 -0
- package/dist/cli/ink-cli/components/overlays/custom-model-wizard/shared/SetupInfoBanner.d.ts +20 -0
- package/dist/cli/ink-cli/components/overlays/custom-model-wizard/shared/SetupInfoBanner.d.ts.map +1 -0
- package/dist/cli/ink-cli/components/overlays/custom-model-wizard/shared/SetupInfoBanner.js +10 -0
- package/dist/cli/ink-cli/components/overlays/custom-model-wizard/shared/WizardStepInput.d.ts +30 -0
- package/dist/cli/ink-cli/components/overlays/custom-model-wizard/shared/WizardStepInput.d.ts.map +1 -0
- package/dist/cli/ink-cli/components/overlays/custom-model-wizard/shared/WizardStepInput.js +13 -0
- package/dist/cli/ink-cli/components/overlays/custom-model-wizard/shared/index.d.ts +8 -0
- package/dist/cli/ink-cli/components/overlays/custom-model-wizard/shared/index.d.ts.map +1 -0
- package/dist/cli/ink-cli/components/overlays/custom-model-wizard/shared/index.js +7 -0
- package/dist/cli/ink-cli/components/overlays/custom-model-wizard/types.d.ts +79 -0
- package/dist/cli/ink-cli/components/overlays/custom-model-wizard/types.d.ts.map +1 -0
- package/dist/cli/ink-cli/components/overlays/custom-model-wizard/types.js +38 -0
- package/dist/cli/ink-cli/components/renderers/DiffRenderer.js +2 -2
- package/dist/cli/ink-cli/components/renderers/FilePreviewRenderer.js +1 -1
- package/dist/cli/ink-cli/components/renderers/FileRenderer.js +4 -4
- package/dist/cli/ink-cli/components/renderers/GenericRenderer.js +2 -2
- package/dist/cli/ink-cli/components/renderers/SearchRenderer.js +1 -1
- package/dist/cli/ink-cli/components/renderers/ShellRenderer.js +3 -3
- package/dist/cli/ink-cli/components/renderers/diff-shared.js +1 -1
- package/dist/cli/ink-cli/components/shared/MarkdownText.d.ts.map +1 -1
- package/dist/cli/ink-cli/components/shared/MarkdownText.js +8 -6
- package/dist/cli/ink-cli/containers/InputContainer.d.ts.map +1 -1
- package/dist/cli/ink-cli/containers/InputContainer.js +23 -1
- package/dist/cli/ink-cli/containers/OverlayContainer.d.ts.map +1 -1
- package/dist/cli/ink-cli/containers/OverlayContainer.js +80 -24
- package/dist/cli/ink-cli/hooks/useAgentEvents.d.ts +1 -1
- package/dist/cli/ink-cli/hooks/useAgentEvents.d.ts.map +1 -1
- package/dist/cli/ink-cli/hooks/useAgentEvents.js +5 -1
- package/dist/cli/ink-cli/hooks/useCLIState.d.ts +1 -1
- package/dist/cli/ink-cli/hooks/useCLIState.d.ts.map +1 -1
- package/dist/cli/ink-cli/hooks/useCLIState.js +4 -2
- package/dist/cli/ink-cli/services/processStream.d.ts.map +1 -1
- package/dist/cli/ink-cli/services/processStream.js +77 -9
- package/dist/cli/ink-cli/state/types.d.ts +3 -2
- package/dist/cli/ink-cli/state/types.d.ts.map +1 -1
- package/dist/cli/ink-cli/utils/messageFormatting.d.ts +5 -0
- package/dist/cli/ink-cli/utils/messageFormatting.d.ts.map +1 -1
- package/dist/cli/ink-cli/utils/messageFormatting.js +59 -1
- package/dist/cli/ink-cli/utils/toolUtils.d.ts.map +1 -1
- package/dist/cli/ink-cli/utils/toolUtils.js +2 -0
- package/dist/cli/utils/api-key-setup.d.ts +54 -4
- package/dist/cli/utils/api-key-setup.d.ts.map +1 -1
- package/dist/cli/utils/api-key-setup.js +433 -107
- package/dist/cli/utils/api-key-verification.d.ts +17 -0
- package/dist/cli/utils/api-key-verification.d.ts.map +1 -0
- package/dist/cli/utils/api-key-verification.js +211 -0
- package/dist/cli/utils/config-validation.d.ts +22 -2
- package/dist/cli/utils/config-validation.d.ts.map +1 -1
- package/dist/cli/utils/config-validation.js +354 -25
- package/dist/cli/utils/local-model-setup.d.ts +46 -0
- package/dist/cli/utils/local-model-setup.d.ts.map +1 -0
- package/dist/cli/utils/local-model-setup.js +662 -0
- package/dist/cli/utils/options.js +1 -1
- package/dist/cli/utils/prompt-helpers.d.ts +47 -0
- package/dist/cli/utils/prompt-helpers.d.ts.map +1 -0
- package/dist/cli/utils/prompt-helpers.js +66 -0
- package/dist/cli/utils/provider-setup.d.ts +66 -8
- package/dist/cli/utils/provider-setup.d.ts.map +1 -1
- package/dist/cli/utils/provider-setup.js +324 -84
- package/dist/cli/utils/scaffolding-utils.d.ts +76 -0
- package/dist/cli/utils/scaffolding-utils.d.ts.map +1 -0
- package/dist/cli/utils/scaffolding-utils.js +246 -0
- package/dist/cli/utils/setup-utils.d.ts +16 -0
- package/dist/cli/utils/setup-utils.d.ts.map +1 -1
- package/dist/cli/utils/setup-utils.js +72 -21
- package/dist/cli/utils/template-engine.d.ts +65 -0
- package/dist/cli/utils/template-engine.d.ts.map +1 -0
- package/dist/cli/utils/template-engine.js +1089 -0
- package/dist/config/cli-overrides.d.ts +44 -1
- package/dist/config/cli-overrides.d.ts.map +1 -1
- package/dist/config/cli-overrides.js +102 -0
- package/dist/index.js +315 -53
- package/dist/webui/assets/index-8j-KMkX1.js +2054 -0
- package/dist/webui/assets/index-c_AX24V4.css +1 -0
- package/dist/webui/index.html +3 -9
- package/dist/webui/logos/aws-color.svg +1 -0
- package/dist/webui/logos/dexto/dexto_logo.svg +1 -1
- package/dist/webui/logos/dexto/dexto_logo_light.svg +6 -6
- package/dist/webui/logos/glama.svg +7 -0
- package/dist/webui/logos/litellm.svg +7 -0
- package/dist/webui/logos/openrouter.svg +1 -0
- package/package.json +8 -7
- package/dist/webui/assets/index-BkwPkZpd.css +0 -1
- 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.
|
|
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)}`));
|