@defai.digital/ax-cli 4.4.19 → 5.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/dist/config.d.ts +31 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +43 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +36 -36
- package/dist/index.js.map +1 -1
- package/dist/setup.d.ts +14 -2
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +333 -331
- package/dist/setup.js.map +1 -1
- package/package.json +11 -11
package/dist/setup.js
CHANGED
|
@@ -17,161 +17,114 @@
|
|
|
17
17
|
import chalk from 'chalk';
|
|
18
18
|
import { select, confirm, input } from '@inquirer/prompts';
|
|
19
19
|
import ora from 'ora';
|
|
20
|
-
import { existsSync
|
|
21
|
-
import { homedir } from 'os';
|
|
22
|
-
import { join } from 'path';
|
|
20
|
+
import { existsSync } from 'fs';
|
|
23
21
|
import { execSync, spawnSync } from 'child_process';
|
|
22
|
+
import { AX_CLI_PROVIDER } from '@defai.digital/ax-core';
|
|
23
|
+
import { AX_CLI_CONFIG_FILE, deleteConfig, loadConfig, saveConfig, } from './config.js';
|
|
24
|
+
const DEFAULT_LOCAL_BASE_URL = AX_CLI_PROVIDER.defaultBaseURL || 'http://localhost:11434/v1';
|
|
24
25
|
// ═══════════════════════════════════════════════════════════════════
|
|
25
|
-
//
|
|
26
|
+
// MODEL DATA - Derived from AX_CLI_PROVIDER (single source of truth)
|
|
26
27
|
// ═══════════════════════════════════════════════════════════════════
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
// TIER 3: DeepSeek-Coder V2 - Best speed (9.3/10)
|
|
79
|
-
// → Best for quick iterations, patches, linting
|
|
80
|
-
// Note: For cloud DeepSeek, a future ax-deepseek package will be available
|
|
81
|
-
const LOCAL_DEEPSEEK_MODELS = [
|
|
82
|
-
{ id: 'deepseek-coder-v2:16b', name: 'DeepSeek-Coder-V2 16B', description: 'SPEED: Fast iterations, patches' },
|
|
83
|
-
{ id: 'deepseek-coder-v2:7b', name: 'DeepSeek-Coder-V2 7B', description: 'SPEED: 7B rivals 13B, edge-friendly' },
|
|
84
|
-
{ id: 'deepseek-v3', name: 'DeepSeek V3', description: 'Latest general + coding model' },
|
|
85
|
-
{ id: 'deepseek-coder:33b', name: 'DeepSeek-Coder 33B', description: 'Strong coding model' },
|
|
86
|
-
{ id: 'deepseek-coder:6.7b', name: 'DeepSeek-Coder 6.7B', description: 'Efficient, low memory' },
|
|
87
|
-
];
|
|
88
|
-
// TIER 4: Codestral/Mistral - C++/Rust niche (8.4/10)
|
|
89
|
-
const LOCAL_CODESTRAL_MODELS = [
|
|
90
|
-
{ id: 'codestral:22b', name: 'Codestral 22B', description: 'C++/RUST: Systems programming niche' },
|
|
91
|
-
{ id: 'mistral:7b', name: 'Mistral 7B', description: 'Good speed/accuracy balance' },
|
|
92
|
-
{ id: 'mistral-nemo:12b', name: 'Mistral Nemo 12B', description: 'Compact but capable' },
|
|
93
|
-
];
|
|
94
|
-
// TIER 5: Llama - Best fallback/compatibility (8.1/10)
|
|
95
|
-
const LOCAL_LLAMA_MODELS = [
|
|
96
|
-
{ id: 'llama3.1:70b', name: 'Llama 3.1 70B', description: 'FALLBACK: Best framework compatibility' },
|
|
97
|
-
{ id: 'llama3.1:8b', name: 'Llama 3.1 8B', description: 'FALLBACK: Fast, stable' },
|
|
98
|
-
{ id: 'llama3.2:11b', name: 'Llama 3.2 11B', description: 'Vision support' },
|
|
99
|
-
{ id: 'codellama:34b', name: 'Code Llama 34B', description: 'Code specialist' },
|
|
100
|
-
{ id: 'codellama:7b', name: 'Code Llama 7B', description: 'Efficient code model' },
|
|
101
|
-
];
|
|
102
|
-
// All local models combined for offline setup (ordered by tier)
|
|
103
|
-
const ALL_LOCAL_MODELS = [
|
|
104
|
-
// Tier 1: Qwen (PRIMARY - recommended for most coding tasks)
|
|
105
|
-
...LOCAL_QWEN_MODELS.map(m => ({ ...m, name: `[T1-Qwen] ${m.name}` })),
|
|
106
|
-
// Tier 2: GLM-4.6 (REFACTOR - best for large-scale refactoring + docs)
|
|
107
|
-
...LOCAL_GLM_MODELS.map(m => ({ ...m, name: `[T2-GLM] ${m.name}` })),
|
|
108
|
-
// Tier 3: DeepSeek (SPEED - best for quick iterations)
|
|
109
|
-
...LOCAL_DEEPSEEK_MODELS.map(m => ({ ...m, name: `[T3-DeepSeek] ${m.name}` })),
|
|
110
|
-
// Tier 4: Codestral (C++/RUST - systems programming)
|
|
111
|
-
...LOCAL_CODESTRAL_MODELS.map(m => ({ ...m, name: `[T4-Codestral] ${m.name}` })),
|
|
112
|
-
// Tier 5: Llama (FALLBACK - compatibility)
|
|
113
|
-
...LOCAL_LLAMA_MODELS.map(m => ({ ...m, name: `[T5-Llama] ${m.name}` })),
|
|
114
|
-
];
|
|
28
|
+
/**
|
|
29
|
+
* Model tier configuration for categorization and display
|
|
30
|
+
* Single source of truth for tier metadata (colors, ratings, labels)
|
|
31
|
+
*/
|
|
32
|
+
const MODEL_TIERS = {
|
|
33
|
+
T1: { prefix: 'T1-Qwen', pattern: /qwen/i, rating: '9.6/10', label: 'PRIMARY', displayName: 'Qwen 3', description: 'Best overall, coding leader', color: chalk.green },
|
|
34
|
+
T2: { prefix: 'T2-GLM', pattern: /glm|codegeex|chatglm/i, rating: '9.4/10', label: 'REFACTOR', displayName: 'GLM', description: 'Large-scale refactor + docs', color: chalk.magenta, isNew: true },
|
|
35
|
+
T3: { prefix: 'T3-DeepSeek', pattern: /deepseek/i, rating: '9.3/10', label: 'SPEED', displayName: 'DeepSeek', description: 'Quick patches, linting', color: chalk.blue },
|
|
36
|
+
T4: { prefix: 'T4-Codestral', pattern: /codestral|mistral/i, rating: '8.4/10', label: 'C++/RUST', displayName: 'Codestral', description: 'Systems programming', color: chalk.cyan },
|
|
37
|
+
T5: { prefix: 'T5-Llama', pattern: /llama|codellama/i, rating: '8.1/10', label: 'FALLBACK', displayName: 'Llama', description: 'Best compatibility', color: chalk.gray },
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Convert provider model config to setup ModelInfo format
|
|
41
|
+
*/
|
|
42
|
+
function providerModelsToModelInfo() {
|
|
43
|
+
return Object.entries(AX_CLI_PROVIDER.models).map(([id, config]) => ({
|
|
44
|
+
id,
|
|
45
|
+
name: config.name,
|
|
46
|
+
description: config.description,
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
const PROVIDER_MODEL_INFOS = providerModelsToModelInfo();
|
|
50
|
+
/**
|
|
51
|
+
* Get the tier for a model ID
|
|
52
|
+
*/
|
|
53
|
+
function getModelTier(modelId) {
|
|
54
|
+
const lower = modelId.toLowerCase();
|
|
55
|
+
for (const [tier, config] of Object.entries(MODEL_TIERS)) {
|
|
56
|
+
if (config.pattern.test(lower)) {
|
|
57
|
+
return { tier, label: config.prefix };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return { tier: 'T5', label: 'Other' };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Get models grouped by tier with prefixes
|
|
64
|
+
*/
|
|
65
|
+
function getModelsWithTierPrefix(models = PROVIDER_MODEL_INFOS) {
|
|
66
|
+
return models.map(m => {
|
|
67
|
+
const { label } = getModelTier(m.id);
|
|
68
|
+
return { ...m, name: label !== 'Other' ? `[${label}] ${m.name}` : m.name };
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Get models for a specific tier
|
|
73
|
+
*/
|
|
74
|
+
function getModelsByTier(tier, models = PROVIDER_MODEL_INFOS) {
|
|
75
|
+
return models.filter(m => getModelTier(m.id).tier === tier);
|
|
76
|
+
}
|
|
77
|
+
// Derived model lists from single source of truth
|
|
78
|
+
const ALL_LOCAL_MODELS = getModelsWithTierPrefix();
|
|
115
79
|
// ═══════════════════════════════════════════════════════════════════
|
|
116
80
|
// PROVIDER CONFIGURATION - LOCAL/OFFLINE ONLY
|
|
117
81
|
// ═══════════════════════════════════════════════════════════════════
|
|
118
82
|
const PROVIDER_INFO = {
|
|
119
|
-
name:
|
|
120
|
-
description:
|
|
121
|
-
cliName:
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
defaultModel: 'qwen3:14b', // Tier 1: Best overall
|
|
125
|
-
apiKeyEnvVar: '', // No API key for local
|
|
83
|
+
name: AX_CLI_PROVIDER.displayName,
|
|
84
|
+
description: AX_CLI_PROVIDER.branding.description,
|
|
85
|
+
cliName: AX_CLI_PROVIDER.branding.cliName,
|
|
86
|
+
defaultBaseURL: DEFAULT_LOCAL_BASE_URL,
|
|
87
|
+
defaultModel: AX_CLI_PROVIDER.defaultModel,
|
|
126
88
|
website: 'https://ollama.ai',
|
|
127
89
|
models: ALL_LOCAL_MODELS,
|
|
128
90
|
};
|
|
129
91
|
// Well-known local server ports
|
|
130
92
|
const LOCAL_SERVERS = [
|
|
131
|
-
{ name: 'Ollama', url:
|
|
93
|
+
{ name: 'Ollama', url: DEFAULT_LOCAL_BASE_URL, port: 11434 },
|
|
132
94
|
{ name: 'LM Studio', url: 'http://localhost:1234/v1', port: 1234 },
|
|
133
95
|
{ name: 'vLLM', url: 'http://localhost:8000/v1', port: 8000 },
|
|
134
96
|
{ name: 'LocalAI', url: 'http://localhost:8080/v1', port: 8080 },
|
|
135
97
|
];
|
|
136
98
|
// Config paths
|
|
137
|
-
const AX_CLI_CONFIG_DIR = join(homedir(), '.ax-cli');
|
|
138
|
-
const AX_CLI_CONFIG_FILE = join(AX_CLI_CONFIG_DIR, 'config.json');
|
|
139
99
|
/**
|
|
140
|
-
*
|
|
100
|
+
* Normalize base URLs to avoid trailing slash issues
|
|
141
101
|
*/
|
|
142
|
-
function
|
|
102
|
+
function normalizeBaseURL(baseURL) {
|
|
143
103
|
try {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
104
|
+
const parsed = new URL(baseURL);
|
|
105
|
+
parsed.pathname = parsed.pathname.replace(/\/+$/, '');
|
|
106
|
+
return parsed.toString().replace(/\/$/, '');
|
|
147
107
|
}
|
|
148
108
|
catch {
|
|
149
|
-
|
|
109
|
+
return baseURL.replace(/\/+$/, '');
|
|
150
110
|
}
|
|
151
|
-
return {};
|
|
152
111
|
}
|
|
153
112
|
/**
|
|
154
|
-
*
|
|
113
|
+
* Build models endpoint from a base URL
|
|
155
114
|
*/
|
|
156
|
-
function
|
|
157
|
-
|
|
158
|
-
mkdirSync(AX_CLI_CONFIG_DIR, { recursive: true });
|
|
159
|
-
}
|
|
160
|
-
writeFileSync(AX_CLI_CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
115
|
+
function buildModelsEndpoint(baseURL) {
|
|
116
|
+
return `${normalizeBaseURL(baseURL)}/models`;
|
|
161
117
|
}
|
|
162
118
|
/**
|
|
163
|
-
*
|
|
119
|
+
* Run an async task with a spinner, ensuring cleanup even on failure
|
|
164
120
|
*/
|
|
165
|
-
function
|
|
166
|
-
const
|
|
121
|
+
async function withSpinner(text, task) {
|
|
122
|
+
const spinner = ora(text).start();
|
|
167
123
|
try {
|
|
168
|
-
|
|
169
|
-
unlinkSync(AX_CLI_CONFIG_FILE);
|
|
170
|
-
}
|
|
171
|
-
return true;
|
|
124
|
+
return await task();
|
|
172
125
|
}
|
|
173
|
-
|
|
174
|
-
|
|
126
|
+
finally {
|
|
127
|
+
spinner.stop();
|
|
175
128
|
}
|
|
176
129
|
}
|
|
177
130
|
/**
|
|
@@ -195,13 +148,13 @@ function getAutomatosXStatus() {
|
|
|
195
148
|
}
|
|
196
149
|
}
|
|
197
150
|
/**
|
|
198
|
-
*
|
|
151
|
+
* Execute a command with a timeout and inherited stdio
|
|
199
152
|
*/
|
|
200
|
-
|
|
153
|
+
function runCommand(command, timeoutMs) {
|
|
201
154
|
try {
|
|
202
|
-
execSync(
|
|
155
|
+
execSync(command, {
|
|
203
156
|
stdio: 'inherit',
|
|
204
|
-
timeout:
|
|
157
|
+
timeout: timeoutMs
|
|
205
158
|
});
|
|
206
159
|
return true;
|
|
207
160
|
}
|
|
@@ -209,34 +162,30 @@ async function installAutomatosX() {
|
|
|
209
162
|
return false;
|
|
210
163
|
}
|
|
211
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* Install AutomatosX globally
|
|
167
|
+
*/
|
|
168
|
+
function installAutomatosX() {
|
|
169
|
+
return runCommand('npm install -g @defai.digital/automatosx', 180000);
|
|
170
|
+
}
|
|
212
171
|
/**
|
|
213
172
|
* Run AutomatosX setup with force flag
|
|
214
173
|
*/
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
execSync('ax setup -f', {
|
|
218
|
-
stdio: 'inherit',
|
|
219
|
-
timeout: 120000 // 2 minutes timeout
|
|
220
|
-
});
|
|
221
|
-
return true;
|
|
222
|
-
}
|
|
223
|
-
catch {
|
|
224
|
-
return false;
|
|
225
|
-
}
|
|
174
|
+
function runAutomatosXSetup() {
|
|
175
|
+
return runCommand('ax setup -f', 120000);
|
|
226
176
|
}
|
|
227
177
|
/**
|
|
228
178
|
* Fetch with timeout - reduces duplication in network calls
|
|
229
179
|
*/
|
|
230
180
|
async function fetchWithTimeout(url, timeoutMs, options = {}) {
|
|
181
|
+
const controller = new AbortController();
|
|
182
|
+
const timeoutHandle = setTimeout(() => controller.abort(), timeoutMs);
|
|
231
183
|
try {
|
|
232
|
-
const controller = new AbortController();
|
|
233
|
-
const timeoutHandle = setTimeout(() => controller.abort(), timeoutMs);
|
|
234
184
|
const response = await fetch(url, {
|
|
235
185
|
...options,
|
|
236
186
|
signal: controller.signal,
|
|
237
187
|
headers: { 'Content-Type': 'application/json', ...options.headers },
|
|
238
188
|
});
|
|
239
|
-
clearTimeout(timeoutHandle);
|
|
240
189
|
if (!response.ok) {
|
|
241
190
|
return { ok: false };
|
|
242
191
|
}
|
|
@@ -246,19 +195,22 @@ async function fetchWithTimeout(url, timeoutMs, options = {}) {
|
|
|
246
195
|
catch {
|
|
247
196
|
return { ok: false };
|
|
248
197
|
}
|
|
198
|
+
finally {
|
|
199
|
+
clearTimeout(timeoutHandle);
|
|
200
|
+
}
|
|
249
201
|
}
|
|
250
202
|
/**
|
|
251
203
|
* Check if a local server is running on a given port
|
|
252
204
|
*/
|
|
253
205
|
async function checkLocalServer(url) {
|
|
254
|
-
const result = await fetchWithTimeout(
|
|
206
|
+
const result = await fetchWithTimeout(buildModelsEndpoint(url), 2000);
|
|
255
207
|
return result.ok;
|
|
256
208
|
}
|
|
257
209
|
/**
|
|
258
210
|
* Fetch models from a local server
|
|
259
211
|
*/
|
|
260
212
|
async function fetchLocalModels(baseURL) {
|
|
261
|
-
const result = await fetchWithTimeout(
|
|
213
|
+
const result = await fetchWithTimeout(buildModelsEndpoint(baseURL), 5000);
|
|
262
214
|
if (!result.ok || !result.data?.data) {
|
|
263
215
|
return [];
|
|
264
216
|
}
|
|
@@ -272,10 +224,14 @@ async function fetchLocalModels(baseURL) {
|
|
|
272
224
|
* Detect running local servers
|
|
273
225
|
*/
|
|
274
226
|
async function detectLocalServers() {
|
|
275
|
-
const results = await Promise.all(LOCAL_SERVERS.map(async (server) =>
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
227
|
+
const results = await Promise.all(LOCAL_SERVERS.map(async (server) => {
|
|
228
|
+
const normalizedURL = normalizeBaseURL(server.url);
|
|
229
|
+
return {
|
|
230
|
+
...server,
|
|
231
|
+
url: normalizedURL,
|
|
232
|
+
available: await checkLocalServer(normalizedURL),
|
|
233
|
+
};
|
|
234
|
+
}));
|
|
279
235
|
return results;
|
|
280
236
|
}
|
|
281
237
|
/**
|
|
@@ -290,32 +246,15 @@ function validateURL(value) {
|
|
|
290
246
|
return 'Please enter a valid URL';
|
|
291
247
|
}
|
|
292
248
|
}
|
|
293
|
-
/**
|
|
294
|
-
* Categorize model by tier (2025 rankings)
|
|
295
|
-
*/
|
|
296
|
-
function categorizeModel(id) {
|
|
297
|
-
const lower = id.toLowerCase();
|
|
298
|
-
if (lower.includes('qwen'))
|
|
299
|
-
return { tier: 'T1', label: 'T1-Qwen' };
|
|
300
|
-
if (lower.includes('glm') || lower.includes('codegeex') || lower.includes('chatglm'))
|
|
301
|
-
return { tier: 'T2', label: 'T2-GLM' };
|
|
302
|
-
if (lower.includes('deepseek'))
|
|
303
|
-
return { tier: 'T3', label: 'T3-DeepSeek' };
|
|
304
|
-
if (lower.includes('codestral') || lower.includes('mistral'))
|
|
305
|
-
return { tier: 'T4', label: 'T4-Codestral' };
|
|
306
|
-
if (lower.includes('llama') || lower.includes('codellama'))
|
|
307
|
-
return { tier: 'T5', label: 'T5-Llama' };
|
|
308
|
-
return { tier: 'T6', label: 'Other' };
|
|
309
|
-
}
|
|
310
249
|
/**
|
|
311
250
|
* Sort models by tier priority
|
|
312
251
|
*/
|
|
313
252
|
function sortModelsByTier(models) {
|
|
314
|
-
const tierOrder = ['T1', 'T2', 'T3', 'T4', 'T5'
|
|
253
|
+
const tierOrder = ['T1', 'T2', 'T3', 'T4', 'T5'];
|
|
315
254
|
return [...models].sort((a, b) => {
|
|
316
|
-
const
|
|
317
|
-
const
|
|
318
|
-
return
|
|
255
|
+
const tierA = tierOrder.indexOf(getModelTier(a.id).tier);
|
|
256
|
+
const tierB = tierOrder.indexOf(getModelTier(b.id).tier);
|
|
257
|
+
return (tierA === -1 ? 99 : tierA) - (tierB === -1 ? 99 : tierB);
|
|
319
258
|
});
|
|
320
259
|
}
|
|
321
260
|
/**
|
|
@@ -323,7 +262,7 @@ function sortModelsByTier(models) {
|
|
|
323
262
|
*/
|
|
324
263
|
function buildModelChoices(models, addTierPrefix = true) {
|
|
325
264
|
const choices = models.map(m => {
|
|
326
|
-
const { label } =
|
|
265
|
+
const { label } = getModelTier(m.id);
|
|
327
266
|
const prefix = addTierPrefix && label !== 'Other' ? `[${label}] ` : '';
|
|
328
267
|
return {
|
|
329
268
|
name: `${prefix}${m.name} - ${m.description}`,
|
|
@@ -365,136 +304,148 @@ function printBoxHeader(title, width = 50) {
|
|
|
365
304
|
console.log(chalk.cyan(` └${'─'.repeat(width)}┘\n`));
|
|
366
305
|
}
|
|
367
306
|
/**
|
|
368
|
-
*
|
|
307
|
+
* Get display limit for a tier (how many models to show)
|
|
369
308
|
*/
|
|
370
|
-
|
|
309
|
+
function getTierDisplayLimit(tier) {
|
|
310
|
+
return tier === 'T4' ? 1 : tier === 'T5' ? 2 : 3;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Print model recommendations by tier
|
|
314
|
+
*/
|
|
315
|
+
function printModelRecommendations() {
|
|
316
|
+
console.log(' Recommended models by tier (Updated Dec 2025):\n');
|
|
317
|
+
for (const [tier, config] of Object.entries(MODEL_TIERS)) {
|
|
318
|
+
const models = getModelsByTier(tier).slice(0, getTierDisplayLimit(tier));
|
|
319
|
+
if (models.length === 0)
|
|
320
|
+
continue;
|
|
321
|
+
console.log(config.color.bold(` ${config.prefix} (${config.rating}) - ${config.label}:`));
|
|
322
|
+
models.forEach(m => {
|
|
323
|
+
console.log(` ${config.color('•')} ${m.id}: ${chalk.dim(m.description)}`);
|
|
324
|
+
});
|
|
325
|
+
console.log();
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Print shared cloud provider guidance
|
|
330
|
+
*/
|
|
331
|
+
function printCloudProviderInfo() {
|
|
332
|
+
console.log(chalk.dim(' For cloud providers, use dedicated CLIs:'));
|
|
333
|
+
console.log(chalk.dim(' • GLM (Z.AI): npm install -g @defai.digital/ax-glm && ax-glm setup'));
|
|
334
|
+
console.log(chalk.dim(' • Grok (xAI): npm install -g @defai.digital/ax-grok && ax-grok setup'));
|
|
335
|
+
console.log(chalk.dim(' • DeepSeek: (coming soon) @defai.digital/ax-deepseek'));
|
|
336
|
+
console.log();
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Print welcome banner
|
|
340
|
+
*/
|
|
341
|
+
function printWelcomeBanner() {
|
|
371
342
|
console.log(chalk.cyan('\n ╔════════════════════════════════════════════════╗'));
|
|
372
343
|
console.log(chalk.cyan(' ║ Welcome to ax-cli Setup Wizard ║'));
|
|
373
344
|
console.log(chalk.cyan(' ║ LOCAL/OFFLINE AI Assistant ║'));
|
|
374
345
|
console.log(chalk.cyan(' ╚════════════════════════════════════════════════╝\n'));
|
|
375
346
|
console.log(' ax-cli is designed for LOCAL/OFFLINE AI inference.');
|
|
376
347
|
console.log(' Run AI models locally without sending data to the cloud.\n');
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
console.log(chalk.red(' ✗ Failed to delete existing configuration\n'));
|
|
390
|
-
process.exit(1);
|
|
391
|
-
}
|
|
348
|
+
printCloudProviderInfo();
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Print tier rankings (derived from MODEL_TIERS)
|
|
352
|
+
*/
|
|
353
|
+
function printTierRankings() {
|
|
354
|
+
console.log(chalk.bold(' 2025 Offline Coding LLM Rankings (Updated Dec 2025):'));
|
|
355
|
+
for (const [tier, config] of Object.entries(MODEL_TIERS)) {
|
|
356
|
+
const newBadge = 'isNew' in config && config.isNew ? ' ' + chalk.yellow('★NEW') : '';
|
|
357
|
+
console.log(config.color(` ${tier} ${config.displayName}`) +
|
|
358
|
+
chalk.dim(` (${config.rating})`) +
|
|
359
|
+
` - ${config.label}: ${config.description}${newBadge}`);
|
|
392
360
|
}
|
|
393
|
-
// Load existing config
|
|
394
|
-
const existingConfig = loadConfig();
|
|
395
361
|
console.log();
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
console.log(chalk.
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Handle force flag - delete existing config
|
|
365
|
+
*/
|
|
366
|
+
function handleForceFlag(force) {
|
|
367
|
+
if (!force || !existsSync(AX_CLI_CONFIG_FILE))
|
|
368
|
+
return;
|
|
369
|
+
console.log(chalk.yellow(' ⚠ Force flag detected - deleting existing configuration...'));
|
|
370
|
+
const deleted = deleteConfig();
|
|
371
|
+
if (deleted) {
|
|
372
|
+
console.log(chalk.green(' ✓ Existing configuration deleted\n'));
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
console.log(chalk.red(' ✗ Failed to delete existing configuration\n'));
|
|
376
|
+
process.exit(1);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Step 1: Select local server
|
|
381
|
+
*/
|
|
382
|
+
async function selectLocalServer(existingConfig) {
|
|
407
383
|
console.log(chalk.bold.cyan('\n Step 1/3 — Local Server\n'));
|
|
408
|
-
const
|
|
409
|
-
const detectedServers = await detectLocalServers();
|
|
384
|
+
const detectedServers = await withSpinner('Detecting local inference servers...', detectLocalServers);
|
|
410
385
|
const availableServers = detectedServers.filter(s => s.available);
|
|
411
|
-
|
|
412
|
-
let selectedBaseURL;
|
|
386
|
+
const defaultServerURL = normalizeBaseURL(existingConfig.baseURL || DEFAULT_LOCAL_BASE_URL);
|
|
413
387
|
if (availableServers.length > 0) {
|
|
414
388
|
console.log(chalk.green(` ✓ Found ${availableServers.length} running server(s)\n`));
|
|
415
389
|
const serverChoices = [
|
|
416
390
|
...availableServers.map(s => ({
|
|
417
391
|
name: `${chalk.green('●')} ${s.name} - ${s.url}`,
|
|
418
|
-
value: s.url,
|
|
392
|
+
value: normalizeBaseURL(s.url),
|
|
419
393
|
})),
|
|
420
|
-
{
|
|
421
|
-
name: `${chalk.dim('○')} Enter custom URL...`,
|
|
422
|
-
value: '__custom__',
|
|
423
|
-
},
|
|
394
|
+
{ name: `${chalk.dim('○')} Enter custom URL...`, value: '__custom__' },
|
|
424
395
|
];
|
|
425
396
|
const serverSelection = await select({
|
|
426
397
|
message: 'Select your local server:',
|
|
427
398
|
choices: serverChoices,
|
|
428
|
-
default: existingConfig.baseURL || availableServers[0]?.url,
|
|
399
|
+
default: normalizeBaseURL(existingConfig.baseURL || availableServers[0]?.url),
|
|
429
400
|
});
|
|
430
401
|
if (serverSelection === '__custom__') {
|
|
431
|
-
|
|
402
|
+
return normalizeBaseURL(await input({
|
|
432
403
|
message: 'Enter server URL:',
|
|
433
|
-
default:
|
|
404
|
+
default: defaultServerURL,
|
|
434
405
|
validate: validateURL,
|
|
435
|
-
});
|
|
436
|
-
}
|
|
437
|
-
else {
|
|
438
|
-
selectedBaseURL = serverSelection;
|
|
406
|
+
}));
|
|
439
407
|
}
|
|
408
|
+
return normalizeBaseURL(serverSelection);
|
|
440
409
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
410
|
+
// No servers detected
|
|
411
|
+
console.log(chalk.yellow(' No running servers detected.\n'));
|
|
412
|
+
console.log(' Common local server URLs:');
|
|
413
|
+
LOCAL_SERVERS.forEach(s => {
|
|
414
|
+
console.log(` ${chalk.dim('•')} ${s.name}: ${chalk.dim(s.url)}`);
|
|
415
|
+
});
|
|
416
|
+
console.log();
|
|
417
|
+
console.log(chalk.dim(' Tip: Start Ollama with: ollama serve'));
|
|
418
|
+
console.log();
|
|
419
|
+
return normalizeBaseURL(await input({
|
|
420
|
+
message: 'Enter your server URL:',
|
|
421
|
+
default: defaultServerURL,
|
|
422
|
+
validate: validateURL,
|
|
423
|
+
}));
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Step 2: Select model
|
|
427
|
+
*/
|
|
428
|
+
async function selectModel(baseURL, existingConfig) {
|
|
459
429
|
console.log(chalk.bold.cyan('\n Step 2/3 — Choose Model\n'));
|
|
460
|
-
|
|
461
|
-
let availableModels = await fetchLocalModels(selectedBaseURL);
|
|
462
|
-
modelSpinner.stop();
|
|
463
|
-
let selectedModel;
|
|
430
|
+
let availableModels = await withSpinner('Fetching available models...', () => fetchLocalModels(baseURL));
|
|
464
431
|
if (availableModels.length > 0) {
|
|
465
432
|
console.log(chalk.green(` ✓ Found ${availableModels.length} model(s) on server\n`));
|
|
466
433
|
const sortedModels = sortModelsByTier(availableModels);
|
|
467
434
|
const modelChoices = buildModelChoices(sortedModels, true);
|
|
468
|
-
selectedModel = await selectModelWithCustom(modelChoices, existingConfig.defaultModel || sortedModels[0]?.id, PROVIDER_INFO.defaultModel);
|
|
435
|
+
const selectedModel = await selectModelWithCustom(modelChoices, existingConfig.defaultModel || sortedModels[0]?.id, PROVIDER_INFO.defaultModel);
|
|
436
|
+
return { model: selectedModel, models: availableModels };
|
|
469
437
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
console.log(chalk.bold.blue('\n T3 DeepSeek (9.3/10) - SPEED:'));
|
|
482
|
-
LOCAL_DEEPSEEK_MODELS.slice(0, 2).forEach(m => {
|
|
483
|
-
console.log(` ${chalk.blue('•')} ${m.id}: ${chalk.dim(m.description)}`);
|
|
484
|
-
});
|
|
485
|
-
console.log(chalk.bold.gray('\n T5 Llama (8.1/10) - FALLBACK:'));
|
|
486
|
-
LOCAL_LLAMA_MODELS.slice(0, 2).forEach(m => {
|
|
487
|
-
console.log(` ${chalk.gray('•')} ${m.id}: ${chalk.dim(m.description)}`);
|
|
488
|
-
});
|
|
489
|
-
console.log();
|
|
490
|
-
// ALL_LOCAL_MODELS already has tier prefixes from mapping, so don't add again
|
|
491
|
-
const modelChoices = buildModelChoices(ALL_LOCAL_MODELS, false);
|
|
492
|
-
selectedModel = await selectModelWithCustom(modelChoices, existingConfig.defaultModel || PROVIDER_INFO.defaultModel, PROVIDER_INFO.defaultModel);
|
|
493
|
-
availableModels = PROVIDER_INFO.models; // Fallback to predefined local models
|
|
494
|
-
}
|
|
495
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
496
|
-
// STEP 3: Quick Setup Option
|
|
497
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
438
|
+
// No models from server - use defaults
|
|
439
|
+
console.log(chalk.yellow(' Could not fetch models from server.\n'));
|
|
440
|
+
printModelRecommendations();
|
|
441
|
+
const modelChoices = buildModelChoices(ALL_LOCAL_MODELS, false);
|
|
442
|
+
const selectedModel = await selectModelWithCustom(modelChoices, existingConfig.defaultModel || PROVIDER_INFO.defaultModel, PROVIDER_INFO.defaultModel);
|
|
443
|
+
return { model: selectedModel, models: PROVIDER_INFO.models };
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Step 3: Quick setup confirmation
|
|
447
|
+
*/
|
|
448
|
+
async function confirmQuickSetup(baseURL) {
|
|
498
449
|
console.log(chalk.bold.cyan('\n Step 3/3 — Quick Setup\n'));
|
|
499
450
|
const useDefaults = await confirm({
|
|
500
451
|
message: 'Use default settings for everything else? (Recommended)',
|
|
@@ -502,50 +453,56 @@ export async function runSetup(options = {}) {
|
|
|
502
453
|
});
|
|
503
454
|
if (useDefaults) {
|
|
504
455
|
console.log(chalk.green('\n ✓ Using default settings\n'));
|
|
456
|
+
return true;
|
|
457
|
+
}
|
|
458
|
+
// Validate server connection
|
|
459
|
+
const validateSpinner = ora('Validating local server connection...').start();
|
|
460
|
+
const isValid = await checkLocalServer(baseURL);
|
|
461
|
+
if (isValid) {
|
|
462
|
+
validateSpinner.succeed('Local server connection validated!');
|
|
505
463
|
}
|
|
506
464
|
else {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
const isValid = await checkLocalServer(selectedBaseURL);
|
|
510
|
-
if (isValid) {
|
|
511
|
-
validateSpinner.succeed('Local server connection validated!');
|
|
512
|
-
}
|
|
513
|
-
else {
|
|
514
|
-
validateSpinner.warn('Server not responding (will save anyway)');
|
|
515
|
-
console.log(chalk.dim('\n Tip: Make sure your local server is running before using ax-cli'));
|
|
516
|
-
}
|
|
465
|
+
validateSpinner.warn('Server not responding (will save anyway)');
|
|
466
|
+
console.log(chalk.dim('\n Tip: Make sure your local server is running before using ax-cli'));
|
|
517
467
|
}
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
console.log(chalk.
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Print setup summary
|
|
472
|
+
*/
|
|
473
|
+
function printSetupSummary(config) {
|
|
474
|
+
printBoxHeader('Configuration Summary', 41);
|
|
475
|
+
console.log(` Provider: ${config._provider}`);
|
|
476
|
+
console.log(` Server: ${config.baseURL}`);
|
|
477
|
+
console.log(` Model: ${config.defaultModel}`);
|
|
478
|
+
console.log(` Config: ${AX_CLI_CONFIG_FILE}`);
|
|
479
|
+
console.log();
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Print next steps
|
|
483
|
+
*/
|
|
484
|
+
function printNextSteps() {
|
|
485
|
+
printBoxHeader('Next Steps', 41);
|
|
486
|
+
console.log(` 1. Run ${chalk.bold('ax-cli')} to start`);
|
|
487
|
+
console.log(` 2. Run ${chalk.bold('ax-cli --help')} for all options`);
|
|
488
|
+
console.log();
|
|
489
|
+
console.log(chalk.dim(' Note: Make sure your local server is running before using ax-cli'));
|
|
490
|
+
console.log();
|
|
491
|
+
printCloudProviderInfo();
|
|
492
|
+
console.log(chalk.green(' ✓ Setup complete! Happy coding!\n'));
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Handle AutomatosX integration
|
|
496
|
+
*/
|
|
497
|
+
function handleAutomatosXIntegration(useQuickSetup) {
|
|
498
|
+
if (useQuickSetup) {
|
|
542
499
|
let axStatus = getAutomatosXStatus();
|
|
543
500
|
if (!axStatus.installed) {
|
|
544
501
|
const installSpinner = ora('Installing AutomatosX for multi-agent AI orchestration...').start();
|
|
545
|
-
const installed =
|
|
502
|
+
const installed = installAutomatosX();
|
|
546
503
|
if (installed) {
|
|
547
504
|
installSpinner.succeed('AutomatosX installed successfully!');
|
|
548
|
-
axStatus = getAutomatosXStatus();
|
|
505
|
+
axStatus = getAutomatosXStatus();
|
|
549
506
|
}
|
|
550
507
|
else {
|
|
551
508
|
installSpinner.stop();
|
|
@@ -556,10 +513,9 @@ export async function runSetup(options = {}) {
|
|
|
556
513
|
else {
|
|
557
514
|
console.log(chalk.green(` ✓ AutomatosX detected${axStatus.version ? ` (v${axStatus.version})` : ''}`));
|
|
558
515
|
}
|
|
559
|
-
// Run ax setup -f to configure AutomatosX with defaults
|
|
560
516
|
if (axStatus.installed) {
|
|
561
517
|
const setupSpinner = ora('Configuring AutomatosX...').start();
|
|
562
|
-
const setupSuccess =
|
|
518
|
+
const setupSuccess = runAutomatosXSetup();
|
|
563
519
|
if (setupSuccess) {
|
|
564
520
|
setupSpinner.succeed('AutomatosX configured successfully!');
|
|
565
521
|
}
|
|
@@ -569,37 +525,83 @@ export async function runSetup(options = {}) {
|
|
|
569
525
|
console.log(chalk.dim(' Configure manually later: ax setup -f'));
|
|
570
526
|
}
|
|
571
527
|
}
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
// Non-quick setup - just show info
|
|
531
|
+
const axStatus = getAutomatosXStatus();
|
|
532
|
+
if (axStatus.installed) {
|
|
533
|
+
console.log(chalk.green(` ✓ AutomatosX detected${axStatus.version ? ` (v${axStatus.version})` : ''}`));
|
|
572
534
|
}
|
|
573
535
|
else {
|
|
574
|
-
|
|
575
|
-
const axStatus = getAutomatosXStatus();
|
|
576
|
-
if (axStatus.installed) {
|
|
577
|
-
console.log(chalk.green(` ✓ AutomatosX detected${axStatus.version ? ` (v${axStatus.version})` : ''}`));
|
|
578
|
-
}
|
|
579
|
-
else {
|
|
580
|
-
console.log(chalk.dim(' AutomatosX not installed. Install later: npm install -g @defai.digital/automatosx'));
|
|
581
|
-
}
|
|
536
|
+
console.log(chalk.dim(' AutomatosX not installed. Install later: npm install -g @defai.digital/automatosx'));
|
|
582
537
|
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Build and save configuration
|
|
541
|
+
*/
|
|
542
|
+
function buildAndSaveConfig(existingConfig, baseURL, model, models) {
|
|
543
|
+
const normalizedBaseURL = normalizeBaseURL(baseURL);
|
|
544
|
+
const newConfig = {
|
|
545
|
+
...existingConfig,
|
|
546
|
+
selectedProvider: 'local',
|
|
547
|
+
serverType: 'local',
|
|
548
|
+
apiKey: '',
|
|
549
|
+
baseURL: normalizedBaseURL,
|
|
550
|
+
defaultModel: model,
|
|
551
|
+
currentModel: model,
|
|
552
|
+
maxTokens: existingConfig.maxTokens ?? 8192,
|
|
553
|
+
temperature: existingConfig.temperature ?? 0.7,
|
|
554
|
+
models: Array.from(new Set([...models.map(m => m.id), model])),
|
|
555
|
+
_provider: PROVIDER_INFO.name,
|
|
556
|
+
_website: PROVIDER_INFO.website,
|
|
557
|
+
_isLocalServer: true,
|
|
558
|
+
};
|
|
559
|
+
saveConfig(newConfig);
|
|
560
|
+
console.log(chalk.green('\n ✓ Configuration saved!\n'));
|
|
561
|
+
return newConfig;
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Run the setup wizard
|
|
565
|
+
*
|
|
566
|
+
* Refactored to use extracted helper functions for clarity:
|
|
567
|
+
* - printWelcomeBanner() - Welcome message and cloud provider info
|
|
568
|
+
* - handleForceFlag() - Handle --force config reset
|
|
569
|
+
* - printTierRankings() - Display LLM tier rankings
|
|
570
|
+
* - selectLocalServer() - Step 1: Server selection
|
|
571
|
+
* - selectModel() - Step 2: Model selection
|
|
572
|
+
* - confirmQuickSetup() - Step 3: Quick/detailed setup
|
|
573
|
+
* - buildAndSaveConfig() - Save configuration
|
|
574
|
+
* - handleAutomatosXIntegration() - AutomatosX setup
|
|
575
|
+
* - printSetupSummary() - Configuration summary
|
|
576
|
+
* - printNextSteps() - Next steps and completion
|
|
577
|
+
*/
|
|
578
|
+
export async function runSetup(options = {}) {
|
|
579
|
+
// Welcome banner
|
|
580
|
+
printWelcomeBanner();
|
|
581
|
+
// Handle --force flag
|
|
582
|
+
handleForceFlag(options.force ?? false);
|
|
583
|
+
// Load existing config
|
|
584
|
+
const existingConfig = loadConfig();
|
|
585
|
+
// Show setup header with tier rankings
|
|
597
586
|
console.log();
|
|
598
|
-
|
|
599
|
-
console.log(
|
|
600
|
-
|
|
587
|
+
printBoxHeader('Local/Offline Setup (Ollama, LMStudio, vLLM)', 53);
|
|
588
|
+
console.log(' Run AI models locally without an API key.\n');
|
|
589
|
+
printTierRankings();
|
|
590
|
+
// Step 1: Select local server
|
|
591
|
+
const selectedBaseURL = await selectLocalServer(existingConfig);
|
|
592
|
+
const normalizedBaseURL = normalizeBaseURL(selectedBaseURL);
|
|
593
|
+
// Step 2: Select model
|
|
594
|
+
const { model: selectedModel, models: availableModels } = await selectModel(normalizedBaseURL, existingConfig);
|
|
595
|
+
// Step 3: Quick setup confirmation
|
|
596
|
+
const useQuickSetup = await confirmQuickSetup(normalizedBaseURL);
|
|
597
|
+
// Save configuration
|
|
598
|
+
const newConfig = buildAndSaveConfig(existingConfig, normalizedBaseURL, selectedModel, availableModels);
|
|
599
|
+
// AutomatosX integration
|
|
600
|
+
handleAutomatosXIntegration(useQuickSetup);
|
|
601
|
+
// Show summary and next steps
|
|
602
|
+
printSetupSummary(newConfig);
|
|
601
603
|
console.log();
|
|
602
|
-
|
|
604
|
+
printNextSteps();
|
|
603
605
|
}
|
|
604
606
|
/**
|
|
605
607
|
* Get the selected provider from config
|