@defai.digital/ax-cli 4.4.18 → 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/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, mkdirSync, readFileSync, writeFileSync } from 'fs';
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
- // 2025 OFFLINE CODING LLM RANKINGS (Updated December 2025)
26
+ // MODEL DATA - Derived from AX_CLI_PROVIDER (single source of truth)
26
27
  // ═══════════════════════════════════════════════════════════════════
27
- //
28
- // Tier 1: Qwen 3 (32B/72B) - 9.6/10 - BEST OVERALL (PRIMARY)
29
- // - Multi-task consistency and stability still best
30
- // - Best for large codebase analysis (especially 32B+)
31
- // - Highest agentic tool integration (AX-CLI)
32
- // - Best Claude Code alternative for offline
33
- // → Recommended as PRIMARY model
34
- //
35
- // Tier 2: GLM-4.6 (9B/32B) - 9.4/10 - BEST REFACTOR + DOCS (MAJOR UPGRADE!)
36
- // - GLM-4.6-Coder coding significantly upgraded, cross-language excellent
37
- // - 9B performance rivals Qwen 14B / DeepSeek 16B
38
- // - Superior for large-scale refactor + documentation generation
39
- // - Beats DeepSeek Coder V2 on long context reasoning
40
- // - Best bilingual (Chinese + English) understanding
41
- // → Recommended as SECONDARY core model
42
- //
43
- // Tier 3: DeepSeek-Coder V2 (7B/16B) - 9.3/10 - BEST SPEED
44
- // - #1 inference speed
45
- // - Easiest edge deployment (Jetson/Mac)
46
- // - Best small model efficiency
47
- // - BUT: GLM-4.6 9B > DeepSeek 16B on large codebase reasoning
48
- // → Best for: quick patches, linting, small refactors
49
- //
50
- // Tier 4: Codestral/Mistral - 8.4/10 - C++/RUST NICHE
51
- // - Niche advantage in C++/Rust
52
- // - Overall capabilities superseded by Qwen/GLM/DeepSeek
53
- //
54
- // Tier 5: Llama 3.1/CodeLlama - 8.1/10 - FALLBACK
55
- // - Wide ecosystem, best framework support
56
- // - Stable and robust
57
- // - No longer leading in capability
58
- // ═══════════════════════════════════════════════════════════════════
59
- // TIER 1: Qwen 3 - Best overall offline coding model (9.6/10)
60
- // → PRIMARY model for most coding tasks
61
- const LOCAL_QWEN_MODELS = [
62
- { id: 'qwen3:72b', name: 'Qwen 3 72B', description: 'PRIMARY: Most capable, 128K context' },
63
- { id: 'qwen3:32b', name: 'Qwen 3 32B', description: 'PRIMARY: Best for large codebase analysis' },
64
- { id: 'qwen3:14b', name: 'Qwen 3 14B', description: 'PRIMARY: Balanced performance (recommended)' },
65
- { id: 'qwen3:8b', name: 'Qwen 3 8B', description: 'PRIMARY: Efficient, great for most tasks' },
66
- { id: 'qwen2.5-coder:32b', name: 'Qwen2.5-Coder 32B', description: 'Excellent coding specialist' },
67
- { id: 'qwen2.5-coder:14b', name: 'Qwen2.5-Coder 14B', description: 'Balanced coding model' },
68
- ];
69
- // TIER 2: GLM-4.6 - Best for refactor + docs (9.4/10) - MAJOR UPGRADE!
70
- // → SECONDARY core model, excellent for architecture refactoring
71
- // Note: For cloud GLM with web search/vision, use ax-glm
72
- const LOCAL_GLM_MODELS = [
73
- { id: 'glm-4.6:32b', name: 'GLM-4.6 32B', description: 'REFACTOR: Large-scale refactor + multi-file editing' },
74
- { id: 'glm-4.6:9b', name: 'GLM-4.6 9B', description: 'REFACTOR: Rivals Qwen 14B, excellent long context' },
75
- { id: 'codegeex4', name: 'CodeGeeX4', description: 'DOCS: Best for documentation generation' },
76
- { id: 'glm4:9b', name: 'GLM-4 9B', description: 'Bilingual code understanding' },
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: 'Local/Offline (Ollama, LMStudio, vLLM)',
120
- description: 'Run models locally - Qwen 3 recommended (best offline coding model)',
121
- cliName: 'ax-cli',
122
- package: '@ax-cli/cli',
123
- defaultBaseURL: 'http://localhost:11434/v1',
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: 'http://localhost:11434/v1', port: 11434 },
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
- * Load ax-cli config
100
+ * Normalize base URLs to avoid trailing slash issues
141
101
  */
142
- function loadConfig() {
102
+ function normalizeBaseURL(baseURL) {
143
103
  try {
144
- if (existsSync(AX_CLI_CONFIG_FILE)) {
145
- return JSON.parse(readFileSync(AX_CLI_CONFIG_FILE, 'utf-8'));
146
- }
104
+ const parsed = new URL(baseURL);
105
+ parsed.pathname = parsed.pathname.replace(/\/+$/, '');
106
+ return parsed.toString().replace(/\/$/, '');
147
107
  }
148
108
  catch {
149
- // Ignore errors
109
+ return baseURL.replace(/\/+$/, '');
150
110
  }
151
- return {};
152
111
  }
153
112
  /**
154
- * Save ax-cli config
113
+ * Build models endpoint from a base URL
155
114
  */
156
- function saveConfig(config) {
157
- if (!existsSync(AX_CLI_CONFIG_DIR)) {
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
- * Delete ax-cli config (for --force flag)
119
+ * Run an async task with a spinner, ensuring cleanup even on failure
164
120
  */
165
- function deleteConfig() {
166
- const { unlinkSync } = require('fs');
121
+ async function withSpinner(text, task) {
122
+ const spinner = ora(text).start();
167
123
  try {
168
- if (existsSync(AX_CLI_CONFIG_FILE)) {
169
- unlinkSync(AX_CLI_CONFIG_FILE);
170
- }
171
- return true;
124
+ return await task();
172
125
  }
173
- catch {
174
- return false;
126
+ finally {
127
+ spinner.stop();
175
128
  }
176
129
  }
177
130
  /**
@@ -195,13 +148,13 @@ function getAutomatosXStatus() {
195
148
  }
196
149
  }
197
150
  /**
198
- * Install AutomatosX globally
151
+ * Execute a command with a timeout and inherited stdio
199
152
  */
200
- async function installAutomatosX() {
153
+ function runCommand(command, timeoutMs) {
201
154
  try {
202
- execSync('npm install -g @defai.digital/automatosx', {
155
+ execSync(command, {
203
156
  stdio: 'inherit',
204
- timeout: 180000 // 3 minutes 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
- async function runAutomatosXSetup() {
216
- try {
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(`${url}/models`, 2000);
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(`${baseURL}/models`, 5000);
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
- ...server,
277
- available: await checkLocalServer(server.url),
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', 'T6'];
253
+ const tierOrder = ['T1', 'T2', 'T3', 'T4', 'T5'];
315
254
  return [...models].sort((a, b) => {
316
- const catA = tierOrder.indexOf(categorizeModel(a.id).tier);
317
- const catB = tierOrder.indexOf(categorizeModel(b.id).tier);
318
- return catA - catB;
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 } = categorizeModel(m.id);
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
- * Run the setup wizard
307
+ * Get display limit for a tier (how many models to show)
369
308
  */
370
- export async function runSetup(options = {}) {
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
- console.log(chalk.dim(' For cloud providers, use dedicated CLIs:'));
378
- console.log(chalk.dim(' • GLM (Z.AI): npm install -g @defai.digital/ax-glm'));
379
- console.log(chalk.dim(' • Grok (xAI): npm install -g @defai.digital/ax-grok'));
380
- console.log(chalk.dim(' • DeepSeek: (coming soon) @defai.digital/ax-deepseek\n'));
381
- // Handle --force flag: delete existing config
382
- if (options.force && existsSync(AX_CLI_CONFIG_FILE)) {
383
- console.log(chalk.yellow(' Force flag detected - deleting existing configuration...'));
384
- const deleted = deleteConfig();
385
- if (deleted) {
386
- console.log(chalk.green(' ✓ Existing configuration deleted\n'));
387
- }
388
- else {
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
- printBoxHeader('Local/Offline Setup (Ollama, LMStudio, vLLM)', 53);
397
- console.log(' Run AI models locally without an API key.\n');
398
- console.log(chalk.bold(' 2025 Offline Coding LLM Rankings (Updated Dec 2025):'));
399
- console.log(chalk.green(' T1 Qwen 3') + chalk.dim(' (9.6/10)') + ' - PRIMARY: Best overall, coding leader');
400
- console.log(chalk.magenta(' T2 GLM-4.6') + chalk.dim(' (9.4/10)') + ' - REFACTOR: Large-scale refactor + docs ' + chalk.yellow('★NEW'));
401
- console.log(chalk.blue(' T3 DeepSeek') + chalk.dim(' (9.3/10)') + ' - SPEED: Quick patches, linting');
402
- console.log(chalk.cyan(' T4 Codestral') + chalk.dim(' (8.4/10)') + ' - C++/RUST: Systems programming');
403
- console.log(chalk.gray(' T5 Llama') + chalk.dim(' (8.1/10)') + ' - FALLBACK: Best compatibility\n');
404
- // ═══════════════════════════════════════════════════════════════════
405
- // STEP 1: Local Server Detection
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 detectSpinner = ora('Detecting local inference servers...').start();
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
- detectSpinner.stop();
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
- selectedBaseURL = await input({
402
+ return normalizeBaseURL(await input({
432
403
  message: 'Enter server URL:',
433
- default: 'http://localhost:11434/v1',
404
+ default: defaultServerURL,
434
405
  validate: validateURL,
435
- });
436
- }
437
- else {
438
- selectedBaseURL = serverSelection;
406
+ }));
439
407
  }
408
+ return normalizeBaseURL(serverSelection);
440
409
  }
441
- else {
442
- console.log(chalk.yellow(' No running servers detected.\n'));
443
- console.log(' Common local server URLs:');
444
- LOCAL_SERVERS.forEach(s => {
445
- console.log(` ${chalk.dim('•')} ${s.name}: ${chalk.dim(s.url)}`);
446
- });
447
- console.log();
448
- console.log(chalk.dim(' Tip: Start Ollama with: ollama serve'));
449
- console.log();
450
- selectedBaseURL = await input({
451
- message: 'Enter your server URL:',
452
- default: existingConfig.baseURL || 'http://localhost:11434/v1',
453
- validate: validateURL,
454
- });
455
- }
456
- // ═══════════════════════════════════════════════════════════════════
457
- // STEP 2: Model Selection (fetch from server or use defaults)
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
- const modelSpinner = ora('Fetching available models...').start();
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
- else {
471
- console.log(chalk.yellow(' Could not fetch models from server.\n'));
472
- console.log(' Recommended models by tier (Updated Dec 2025):\n');
473
- console.log(chalk.bold.green(' T1 Qwen 3 (9.6/10) - PRIMARY:'));
474
- LOCAL_QWEN_MODELS.slice(0, 3).forEach(m => {
475
- console.log(` ${chalk.green('•')} ${m.id}: ${chalk.dim(m.description)}`);
476
- });
477
- console.log(chalk.bold.magenta('\n T2 GLM-4.6 (9.4/10) - REFACTOR + DOCS ★NEW:'));
478
- LOCAL_GLM_MODELS.slice(0, 3).forEach(m => {
479
- console.log(` ${chalk.magenta('•')} ${m.id}: ${chalk.dim(m.description)}`);
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
- // Only validate if user wants detailed setup
508
- const validateSpinner = ora('Validating local server connection...').start();
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
- // Save configuration
519
- const newConfig = {
520
- ...existingConfig,
521
- selectedProvider: 'local',
522
- serverType: 'local',
523
- apiKey: '', // No API key needed for local
524
- baseURL: selectedBaseURL,
525
- defaultModel: selectedModel,
526
- currentModel: selectedModel,
527
- maxTokens: existingConfig.maxTokens ?? 8192,
528
- temperature: existingConfig.temperature ?? 0.7,
529
- // Ensure the selected model is always persisted, even if it wasn't in the fetched list
530
- models: Array.from(new Set([...availableModels.map(m => m.id), selectedModel])),
531
- _provider: PROVIDER_INFO.name,
532
- _website: PROVIDER_INFO.website,
533
- _isLocalServer: true,
534
- };
535
- saveConfig(newConfig);
536
- console.log(chalk.green('\n ✓ Configuration saved!\n'));
537
- // ═══════════════════════════════════════════════════════════════════
538
- // AutomatosX Integration (quick setup installs and configures by default)
539
- // ═══════════════════════════════════════════════════════════════════
540
- if (useDefaults) {
541
- // Quick setup - install AutomatosX and run ax setup -f by default
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 = await installAutomatosX();
502
+ const installed = installAutomatosX();
546
503
  if (installed) {
547
504
  installSpinner.succeed('AutomatosX installed successfully!');
548
- axStatus = getAutomatosXStatus(); // Refresh status after install
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 = await runAutomatosXSetup();
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
- // Detailed setup - just show info
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
- // Show summary
584
- printBoxHeader('Configuration Summary', 41);
585
- console.log(` Provider: ${newConfig._provider}`);
586
- console.log(` Server: ${newConfig.baseURL}`);
587
- console.log(` Model: ${newConfig.defaultModel}`);
588
- console.log(` Config: ${AX_CLI_CONFIG_FILE}`);
589
- console.log();
590
- // Show next steps
591
- console.log();
592
- printBoxHeader('Next Steps', 41);
593
- console.log(` 1. Run ${chalk.bold('ax-cli')} to start`);
594
- console.log(` 2. Run ${chalk.bold('ax-cli --help')} for all options`);
595
- console.log();
596
- console.log(chalk.dim(' Note: Make sure your local server is running before using ax-cli'));
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
- console.log(chalk.dim(' For cloud providers:'));
599
- console.log(chalk.dim(' GLM (Z.AI): ax-glm setup'));
600
- console.log(chalk.dim(' • Grok (xAI): ax-grok setup'));
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
- console.log(chalk.green(' ✓ Setup complete! Happy coding!\n'));
604
+ printNextSteps();
603
605
  }
604
606
  /**
605
607
  * Get the selected provider from config