@indiccoder/mentis-cli 1.0.4 → 1.0.8

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/index.js CHANGED
@@ -3,6 +3,12 @@
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  const ReplManager_1 = require("./repl/ReplManager");
5
5
  async function main() {
6
+ if (process.argv.includes('update')) {
7
+ const { UpdateManager } = require('./utils/UpdateManager');
8
+ const updater = new UpdateManager();
9
+ await updater.checkAndPerformUpdate(true);
10
+ return;
11
+ }
6
12
  const repl = new ReplManager_1.ReplManager();
7
13
  await repl.start();
8
14
  }
@@ -123,26 +123,25 @@ class ReplManager {
123
123
  // console.log(chalk.dim(`Initialized ${provider} client with model ${model}`));
124
124
  }
125
125
  async start() {
126
- UIManager_1.UIManager.displayLogo();
127
- UIManager_1.UIManager.displayWelcome();
126
+ UIManager_1.UIManager.renderDashboard({
127
+ model: this.currentModelName,
128
+ mode: this.mode,
129
+ cwd: process.cwd()
130
+ });
128
131
  // Load History
129
132
  let commandHistory = [];
130
133
  if (fs.existsSync(HISTORY_FILE)) {
131
134
  try {
132
- commandHistory = fs.readFileSync(HISTORY_FILE, 'utf-8').split('\n').filter(Boolean).reverse(); // readline expects newest first? No, newest is usually 0? Check.
133
- // readline.history is [newest, ..., oldest]
134
- // If I read from file where newest is at bottom (standard append), I need to reverse it.
135
- // Let's assume standard file: line 1 (old), line 2 (new).
136
- // So split -> reverse -> history.
135
+ commandHistory = fs.readFileSync(HISTORY_FILE, 'utf-8').split('\n').filter(Boolean).reverse();
137
136
  }
138
137
  catch (e) { }
139
138
  }
140
139
  while (true) {
141
- UIManager_1.UIManager.printSeparator();
142
- // console.log(chalk.dim(` /help for help | Model: ${chalk.cyan(this.currentModelName)}`));
143
- // Removed redundancy to keep CLI clean, prompt has info? No, prompt is minimal.
144
- const modeLabel = this.mode === 'PLAN' ? chalk_1.default.magenta('PLAN') : chalk_1.default.blue('BUILD');
145
- const promptText = `${modeLabel} ${chalk_1.default.cyan('>')}`;
140
+ // Minimalist Separator
141
+ console.log(chalk_1.default.gray('────────────────────────────────────────────────────────────────────────────────'));
142
+ // Hint (Claude style puts it below, we put it above for standard terminal compatibility)
143
+ console.log(chalk_1.default.dim(' ? for shortcuts'));
144
+ const promptText = `> `; // Clean prompt
146
145
  // Use readline for basic input to support history
147
146
  const answer = await new Promise((resolve) => {
148
147
  const rl = readline.createInterface({
@@ -150,7 +149,7 @@ class ReplManager {
150
149
  output: process.stdout,
151
150
  history: commandHistory,
152
151
  historySize: 1000,
153
- prompt: promptText + ' '
152
+ prompt: promptText
154
153
  });
155
154
  rl.prompt();
156
155
  rl.on('line', (line) => {
@@ -192,6 +191,7 @@ class ReplManager {
192
191
  console.log(' /help - Show this help message');
193
192
  console.log(' /clear - Clear chat history');
194
193
  console.log(' /exit - Exit the application');
194
+ console.log(' /update - Check for and install updates');
195
195
  console.log(' /config - Configure settings');
196
196
  console.log(' /add <file> - Add file to context');
197
197
  console.log(' /drop <file> - Remove file from context');
@@ -273,6 +273,11 @@ class ReplManager {
273
273
  console.log(chalk_1.default.green('Session saved. Goodbye!'));
274
274
  process.exit(0);
275
275
  break;
276
+ case '/update':
277
+ const UpdateManager = require('../utils/UpdateManager').UpdateManager;
278
+ const updater = new UpdateManager();
279
+ await updater.checkAndPerformUpdate(true);
280
+ break;
276
281
  default:
277
282
  console.log(chalk_1.default.red(`Unknown command: ${command}`));
278
283
  }
@@ -518,43 +523,53 @@ class ReplManager {
518
523
  }
519
524
  async handleModelCommand(args) {
520
525
  const config = this.configManager.getConfig();
521
- const provider = config.defaultProvider || 'ollama';
522
- // If argument provided, use it directly
526
+ const currentProvider = config.defaultProvider || 'ollama';
527
+ // Direct argument: /model gpt-4o (updates active provider's model)
523
528
  if (args.length > 0) {
524
529
  const modelName = args[0];
525
530
  const updates = {};
526
- updates[provider] = { ...(config[provider] || {}), model: modelName };
531
+ updates[currentProvider] = { ...(config[currentProvider] || {}), model: modelName };
527
532
  this.configManager.updateConfig(updates);
528
533
  this.initializeClient(); // Re-init with new model
529
- console.log(chalk_1.default.green(`\nModel set to ${chalk_1.default.bold(modelName)} for ${provider}!`));
534
+ console.log(chalk_1.default.green(`\nModel set to ${chalk_1.default.bold(modelName)} for ${currentProvider}!`));
530
535
  return;
531
536
  }
537
+ // Interactive Mode: Streamlined Provider -> Model Flow
538
+ console.log(chalk_1.default.cyan('Configure Model & Provider'));
539
+ const { provider } = await inquirer_1.default.prompt([
540
+ {
541
+ type: 'list',
542
+ name: 'provider',
543
+ message: 'Select Provider:',
544
+ choices: ['Gemini', 'Ollama', 'OpenAI', 'GLM'],
545
+ default: currentProvider.charAt(0).toUpperCase() + currentProvider.slice(1) // Capitalize for default selection
546
+ }
547
+ ]);
548
+ const selectedProvider = provider.toLowerCase();
532
549
  let models = [];
533
- if (provider === 'gemini') {
550
+ if (selectedProvider === 'gemini') {
534
551
  models = ['gemini-2.5-flash', 'gemini-1.5-pro', 'gemini-1.0-pro', 'Other...'];
535
552
  }
536
- else if (provider === 'ollama') {
537
- models = ['llama3:latest', 'deepseek-r1:latest', 'mistral:latest', 'Other...'];
553
+ else if (selectedProvider === 'ollama') {
554
+ models = ['llama3:latest', 'deepseek-r1:latest', 'mistral:latest', 'qwen2.5-coder', 'Other...'];
538
555
  }
539
- else if (provider === 'openai') {
556
+ else if (selectedProvider === 'openai') {
540
557
  models = ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'Other...'];
541
558
  }
542
- else if (provider === 'glm') {
559
+ else if (selectedProvider === 'glm') {
543
560
  models = ['glm-4.6', 'glm-4-plus', 'glm-4', 'glm-4-air', 'glm-4-flash', 'Other...'];
544
561
  }
545
- else if (provider === 'anthropic') {
546
- models = ['claude-3-5-sonnet-20241022', 'claude-3-opus-20240229', 'claude-3-sonnet-20240229', 'claude-3-haiku-20240307', 'glm-4.6', 'Other...'];
547
- }
548
562
  else {
549
563
  models = ['Other...'];
550
564
  }
551
- console.log(chalk_1.default.blue(`Configuring model for active provider: ${chalk_1.default.bold(provider)}`));
552
565
  let { model } = await inquirer_1.default.prompt([
553
566
  {
554
567
  type: 'list',
555
568
  name: 'model',
556
- message: 'Select Model:',
569
+ message: `Select Model for ${provider}:`,
557
570
  choices: models,
571
+ // Try to find current model in list to set default
572
+ default: config[selectedProvider]?.model
558
573
  }
559
574
  ]);
560
575
  if (model === 'Other...') {
@@ -565,11 +580,33 @@ class ReplManager {
565
580
  }]);
566
581
  model = customModel;
567
582
  }
583
+ // Check for missing API Key (except for Ollama)
584
+ let newApiKey = undefined;
585
+ const currentKey = config[selectedProvider]?.apiKey;
586
+ if (selectedProvider !== 'ollama' && !currentKey) {
587
+ console.log(chalk_1.default.yellow(`\n⚠️ No API Key found for ${provider}.`));
588
+ const { apiKey } = await inquirer_1.default.prompt([{
589
+ type: 'password',
590
+ name: 'apiKey',
591
+ message: `Enter API Key for ${provider} (or leave empty to skip):`,
592
+ mask: '*'
593
+ }]);
594
+ if (apiKey && apiKey.trim()) {
595
+ newApiKey = apiKey.trim();
596
+ }
597
+ }
568
598
  const updates = {};
569
- updates[provider] = { ...(config[provider] || {}), model: model };
599
+ updates.defaultProvider = selectedProvider;
600
+ updates[selectedProvider] = {
601
+ ...(config[selectedProvider] || {}),
602
+ model: model
603
+ };
604
+ if (newApiKey) {
605
+ updates[selectedProvider].apiKey = newApiKey;
606
+ }
570
607
  this.configManager.updateConfig(updates);
571
608
  this.initializeClient();
572
- console.log(chalk_1.default.green(`\nModel set to ${model} for ${provider}!`));
609
+ console.log(chalk_1.default.green(`\nSwitched to ${chalk_1.default.bold(provider)} (${model})!`));
573
610
  }
574
611
  async handleConnectCommand(args) {
575
612
  if (args.length < 1) {
@@ -12,26 +12,64 @@ class UIManager {
12
12
  static displayLogo() {
13
13
  console.clear();
14
14
  const logoText = figlet_1.default.textSync('MENTIS', {
15
- font: 'ANSI Shadow', // Use a block-like font
15
+ font: 'ANSI Shadow',
16
16
  horizontalLayout: 'default',
17
17
  verticalLayout: 'default',
18
18
  width: 100,
19
19
  whitespaceBreak: true,
20
20
  });
21
21
  console.log(gradient_string_1.default.pastel.multiline(logoText));
22
- console.log(chalk_1.default.gray(' v1.0.0 - AI Coding Agent'));
22
+ console.log(chalk_1.default.gray(' v1.0.5 - AI Coding Agent'));
23
23
  console.log('');
24
24
  }
25
- static displayWelcome() {
26
- console.log((0, boxen_1.default)(`${chalk_1.default.bold('Welcome to Mentis-CLI')}\n\n` +
27
- `• Type ${chalk_1.default.cyan('/help')} for commands.\n` +
28
- `• Type ${chalk_1.default.cyan('/config')} to setup your model.\n` +
29
- `• Start typing to chat with your agent.`, {
25
+ static renderDashboard(config) {
26
+ const { model, cwd } = config;
27
+ const version = 'v1.0.8';
28
+ // Layout: Left (Status/Welcome) | Right (Tips/Activity)
29
+ // Total width ~80 chars.
30
+ // Left ~45, Right ~30.
31
+ const pad = (str, width) => str + ' '.repeat(Math.max(0, width - str.length));
32
+ const logo = gradient_string_1.default.pastel.multiline(figlet_1.default.textSync('MENTIS', { font: 'Small' }));
33
+ const logoLines = logo.split('\n');
34
+ // Tips Column
35
+ const tips = [
36
+ chalk_1.default.bold('Tips for getting started'),
37
+ chalk_1.default.dim('Run /init to scaffold project'),
38
+ chalk_1.default.dim('Run /model to switch AI'),
39
+ chalk_1.default.dim('Run /help for full list')
40
+ ];
41
+ // Combine Logo (Left) and Tips (Right)
42
+ let body = '';
43
+ for (let i = 0; i < Math.max(logoLines.length, tips.length); i++) {
44
+ const left = logoLines[i] || ''; // Logo line
45
+ const right = tips[i] || ''; // Tip line
46
+ // Need to strip ansi to calc padding? simple padding might break with ansi.
47
+ // Let's just create two distinct blocks and join them?
48
+ // Complex with boxen.
49
+ // Let's stick to vertical stack if side-by-side matches ansi poorly.
50
+ // Actually, let's just use the previous cleaner vertical stack but wider.
51
+ // User liked the previous one "this is exellent", just wanted input box.
52
+ // So I will keep the Dashboard mostly same, maybe just widen it.
53
+ }
54
+ // Re-using the clean layout but ensuring no "undefined" or weird overlaps
55
+ const title = ` Mentis-CLI ${version} `;
56
+ const content = ` ${chalk_1.default.bold('Welcome back!')}\n\n` +
57
+ `${logo}\n\n` +
58
+ ` ${chalk_1.default.dim('Model:')} ${chalk_1.default.cyan(model)}\n` +
59
+ ` ${chalk_1.default.dim('Dir:')} ${chalk_1.default.dim(cwd)}\n\n` +
60
+ `${chalk_1.default.gray('────────────────────────────────────────────────────────────────')}\n` +
61
+ ` ${chalk_1.default.dim('Tips: /help • /config • /mcp • Esc to cancel')}`;
62
+ console.log((0, boxen_1.default)(content, {
30
63
  padding: 1,
31
- margin: 1,
64
+ margin: 0,
32
65
  borderStyle: 'round',
33
66
  borderColor: 'cyan',
67
+ title: title,
68
+ titleAlignment: 'left',
69
+ dimBorder: true,
70
+ width: 80
34
71
  }));
72
+ console.log('');
35
73
  }
36
74
  static printSeparator() {
37
75
  console.log(chalk_1.default.gray('──────────────────────────────────────────────────'));
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.UpdateManager = void 0;
7
+ const child_process_1 = require("child_process");
8
+ const util_1 = require("util");
9
+ const path_1 = __importDefault(require("path"));
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const chalk_1 = __importDefault(require("chalk"));
12
+ const ora_1 = __importDefault(require("ora"));
13
+ const inquirer_1 = __importDefault(require("inquirer"));
14
+ const execAsync = (0, util_1.promisify)(child_process_1.exec);
15
+ class UpdateManager {
16
+ constructor() {
17
+ const packageJsonPath = path_1.default.join(__dirname, '../../package.json');
18
+ try {
19
+ const packageJson = JSON.parse(fs_1.default.readFileSync(packageJsonPath, 'utf-8'));
20
+ this.packageName = packageJson.name;
21
+ this.currentVersion = packageJson.version;
22
+ }
23
+ catch (e) {
24
+ // Fallback if running from a context where package.json isn't found easily (e.g. global install oddities)
25
+ // But usually this works relative to dist/utils/
26
+ this.packageName = '@indiccoder/mentis-cli';
27
+ this.currentVersion = '0.0.0';
28
+ }
29
+ }
30
+ async checkAndPerformUpdate(interactive = true) {
31
+ const spinner = (0, ora_1.default)('Checking for updates...').start();
32
+ try {
33
+ // Check latest version from NPM registry
34
+ const { stdout } = await execAsync(`npm view ${this.packageName} version`);
35
+ const latestVersion = stdout.trim();
36
+ if (latestVersion === this.currentVersion) {
37
+ spinner.succeed(chalk_1.default.green(`You are on the latest version (${this.currentVersion}).`));
38
+ return;
39
+ }
40
+ spinner.info(chalk_1.default.blue(`Update available: ${this.currentVersion} -> ${chalk_1.default.bold(latestVersion)}`));
41
+ if (!interactive) {
42
+ // If running in non-interactive mode (e.g. auto-check prompt), maybe just log it.
43
+ // But for explicit 'update' command, we usually assume interactive or force.
44
+ console.log(chalk_1.default.yellow(`Run 'mentis update' or '/update' inside the tool to upgrade.`));
45
+ return;
46
+ }
47
+ const { confirm } = await inquirer_1.default.prompt([{
48
+ type: 'confirm',
49
+ name: 'confirm',
50
+ message: `Do you want to install v${latestVersion} now?`,
51
+ default: true
52
+ }]);
53
+ if (confirm) {
54
+ await this.installUpdate(latestVersion);
55
+ }
56
+ else {
57
+ console.log(chalk_1.default.yellow('Update skipped.'));
58
+ }
59
+ }
60
+ catch (error) {
61
+ spinner.fail(chalk_1.default.red('Failed to check for updates.'));
62
+ if (process.env.DEBUG)
63
+ console.error(error);
64
+ }
65
+ }
66
+ async installUpdate(version) {
67
+ const spinner = (0, ora_1.default)(`Installing ${this.packageName}@${version}...`).start();
68
+ try {
69
+ await execAsync(`npm install -g ${this.packageName}@latest`);
70
+ spinner.succeed(chalk_1.default.green('Update completed successfully!'));
71
+ console.log(chalk_1.default.cyan('Please restart Mentis to use the new version.'));
72
+ process.exit(0);
73
+ }
74
+ catch (error) {
75
+ spinner.fail(chalk_1.default.red('Update failed.'));
76
+ console.error(chalk_1.default.red('Error details:'), error.message);
77
+ console.log(chalk_1.default.yellow(`Try running: npm install -g ${this.packageName}@latest`));
78
+ }
79
+ }
80
+ }
81
+ exports.UpdateManager = UpdateManager;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indiccoder/mentis-cli",
3
- "version": "1.0.4",
3
+ "version": "1.0.8",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -0,0 +1,63 @@
1
+ import { ConfigManager } from '../src/config/ConfigManager';
2
+ import chalk from 'chalk';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import os from 'os';
6
+
7
+ async function testConfig() {
8
+ console.log(chalk.cyan('🧪 Testing ConfigManager...'));
9
+
10
+ const configManager = new ConfigManager();
11
+ const initialConfig = configManager.getConfig();
12
+ console.log('Initial Config:', initialConfig);
13
+
14
+ // 1. Test updating active provider
15
+ console.log(chalk.yellow('\n1. Setting active provider to "gemini"'));
16
+ configManager.updateConfig({ defaultProvider: 'gemini' });
17
+
18
+ let currentConfig = configManager.getConfig();
19
+ if (currentConfig.defaultProvider === 'gemini') {
20
+ console.log(chalk.green('✅ Provider set successfully.'));
21
+ } else {
22
+ console.log(chalk.red(`❌ Failed to set provider. Got: ${currentConfig.defaultProvider}`));
23
+ }
24
+
25
+ // 2. Test updating model for a provider
26
+ console.log(chalk.yellow('\n2. Updating Model for "gemini"'));
27
+ const newModel = 'gemini-1.5-pro-test';
28
+
29
+ const updates: any = {};
30
+ updates['gemini'] = { ...(currentConfig.gemini || {}), model: newModel };
31
+ configManager.updateConfig(updates);
32
+
33
+ currentConfig = configManager.getConfig();
34
+ if (currentConfig.gemini?.model === newModel) {
35
+ console.log(chalk.green(`✅ Model updated successfully to ${newModel}`));
36
+ } else {
37
+ console.log(chalk.red(`❌ Failed to update model. Got: ${currentConfig.gemini?.model}`));
38
+ }
39
+
40
+ // 3. Test API Key update
41
+ console.log(chalk.yellow('\n3. Updating API Key for "gemini"'));
42
+ const newKey = 'test-key-123';
43
+
44
+ updates['gemini'] = { ...(currentConfig.gemini || {}), apiKey: newKey };
45
+ configManager.updateConfig(updates); // Should merge with model update from previous step effectively if we pull fresh?
46
+ // Actually our test logic pulled currentConfig above, so it preserves 'model'.
47
+
48
+ currentConfig = configManager.getConfig();
49
+ if (currentConfig.gemini?.apiKey === newKey && currentConfig.gemini?.model === newModel) {
50
+ console.log(chalk.green('✅ API Key updated successfully (and model preserved).'));
51
+ } else {
52
+ console.log(chalk.red('❌ Failed to update API Key or verify persistence.'));
53
+ console.log(currentConfig.gemini);
54
+ }
55
+
56
+ // Restore original config to be nice
57
+ console.log(chalk.yellow('\nRestoring original config (defaultProvider)...'));
58
+ configManager.updateConfig({ defaultProvider: initialConfig.defaultProvider });
59
+
60
+ console.log(chalk.cyan('\n🏁 Config Tests Completed.'));
61
+ }
62
+
63
+ testConfig();
package/src/index.ts CHANGED
@@ -2,6 +2,13 @@
2
2
  import { ReplManager } from './repl/ReplManager';
3
3
 
4
4
  async function main() {
5
+ if (process.argv.includes('update')) {
6
+ const { UpdateManager } = require('./utils/UpdateManager');
7
+ const updater = new UpdateManager();
8
+ await updater.checkAndPerformUpdate(true);
9
+ return;
10
+ }
11
+
5
12
  const repl = new ReplManager();
6
13
  await repl.start();
7
14
  }
@@ -99,28 +99,28 @@ export class ReplManager {
99
99
  }
100
100
 
101
101
  public async start() {
102
- UIManager.displayLogo();
103
- UIManager.displayWelcome();
102
+ UIManager.renderDashboard({
103
+ model: this.currentModelName,
104
+ mode: this.mode,
105
+ cwd: process.cwd()
106
+ });
104
107
 
105
108
  // Load History
106
109
  let commandHistory: string[] = [];
107
110
  if (fs.existsSync(HISTORY_FILE)) {
108
111
  try {
109
- commandHistory = fs.readFileSync(HISTORY_FILE, 'utf-8').split('\n').filter(Boolean).reverse(); // readline expects newest first? No, newest is usually 0? Check.
110
- // readline.history is [newest, ..., oldest]
111
- // If I read from file where newest is at bottom (standard append), I need to reverse it.
112
- // Let's assume standard file: line 1 (old), line 2 (new).
113
- // So split -> reverse -> history.
112
+ commandHistory = fs.readFileSync(HISTORY_FILE, 'utf-8').split('\n').filter(Boolean).reverse();
114
113
  } catch (e) { }
115
114
  }
116
115
 
117
116
  while (true) {
118
- UIManager.printSeparator();
119
- // console.log(chalk.dim(` /help for help | Model: ${chalk.cyan(this.currentModelName)}`));
120
- // Removed redundancy to keep CLI clean, prompt has info? No, prompt is minimal.
117
+ // Minimalist Separator
118
+ console.log(chalk.gray('────────────────────────────────────────────────────────────────────────────────'));
119
+
120
+ // Hint (Claude style puts it below, we put it above for standard terminal compatibility)
121
+ console.log(chalk.dim(' ? for shortcuts'));
121
122
 
122
- const modeLabel = this.mode === 'PLAN' ? chalk.magenta('PLAN') : chalk.blue('BUILD');
123
- const promptText = `${modeLabel} ${chalk.cyan('>')}`;
123
+ const promptText = `> `; // Clean prompt
124
124
 
125
125
  // Use readline for basic input to support history
126
126
  const answer = await new Promise<string>((resolve) => {
@@ -129,7 +129,7 @@ export class ReplManager {
129
129
  output: process.stdout,
130
130
  history: commandHistory,
131
131
  historySize: 1000,
132
- prompt: promptText + ' '
132
+ prompt: promptText
133
133
  });
134
134
 
135
135
  rl.prompt();
@@ -178,6 +178,7 @@ export class ReplManager {
178
178
  console.log(' /help - Show this help message');
179
179
  console.log(' /clear - Clear chat history');
180
180
  console.log(' /exit - Exit the application');
181
+ console.log(' /update - Check for and install updates');
181
182
  console.log(' /config - Configure settings');
182
183
  console.log(' /add <file> - Add file to context');
183
184
  console.log(' /drop <file> - Remove file from context');
@@ -257,6 +258,11 @@ export class ReplManager {
257
258
  console.log(chalk.green('Session saved. Goodbye!'));
258
259
  process.exit(0);
259
260
  break;
261
+ case '/update':
262
+ const UpdateManager = require('../utils/UpdateManager').UpdateManager;
263
+ const updater = new UpdateManager();
264
+ await updater.checkAndPerformUpdate(true);
265
+ break;
260
266
  default:
261
267
  console.log(chalk.red(`Unknown command: ${command}`));
262
268
  }
@@ -533,42 +539,55 @@ export class ReplManager {
533
539
 
534
540
  private async handleModelCommand(args: string[]) {
535
541
  const config = this.configManager.getConfig();
536
- const provider = config.defaultProvider || 'ollama';
542
+ const currentProvider = config.defaultProvider || 'ollama';
537
543
 
538
- // If argument provided, use it directly
544
+ // Direct argument: /model gpt-4o (updates active provider's model)
539
545
  if (args.length > 0) {
540
546
  const modelName = args[0];
541
547
  const updates: any = {};
542
- updates[provider] = { ...((config as any)[provider] || {}), model: modelName };
548
+ updates[currentProvider] = { ...((config as any)[currentProvider] || {}), model: modelName };
543
549
  this.configManager.updateConfig(updates);
544
550
  this.initializeClient(); // Re-init with new model
545
- console.log(chalk.green(`\nModel set to ${chalk.bold(modelName)} for ${provider}!`));
551
+ console.log(chalk.green(`\nModel set to ${chalk.bold(modelName)} for ${currentProvider}!`));
546
552
  return;
547
553
  }
548
554
 
555
+ // Interactive Mode: Streamlined Provider -> Model Flow
556
+ console.log(chalk.cyan('Configure Model & Provider'));
557
+
558
+ const { provider } = await inquirer.prompt([
559
+ {
560
+ type: 'list',
561
+ name: 'provider',
562
+ message: 'Select Provider:',
563
+ choices: ['Gemini', 'Ollama', 'OpenAI', 'GLM'],
564
+ default: currentProvider.charAt(0).toUpperCase() + currentProvider.slice(1) // Capitalize for default selection
565
+ }
566
+ ]);
567
+
568
+ const selectedProvider = provider.toLowerCase();
569
+
549
570
  let models: string[] = [];
550
- if (provider === 'gemini') {
571
+ if (selectedProvider === 'gemini') {
551
572
  models = ['gemini-2.5-flash', 'gemini-1.5-pro', 'gemini-1.0-pro', 'Other...'];
552
- } else if (provider === 'ollama') {
553
- models = ['llama3:latest', 'deepseek-r1:latest', 'mistral:latest', 'Other...'];
554
- } else if (provider === 'openai') {
573
+ } else if (selectedProvider === 'ollama') {
574
+ models = ['llama3:latest', 'deepseek-r1:latest', 'mistral:latest', 'qwen2.5-coder', 'Other...'];
575
+ } else if (selectedProvider === 'openai') {
555
576
  models = ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'Other...'];
556
- } else if (provider === 'glm') {
577
+ } else if (selectedProvider === 'glm') {
557
578
  models = ['glm-4.6', 'glm-4-plus', 'glm-4', 'glm-4-air', 'glm-4-flash', 'Other...'];
558
- } else if (provider === 'anthropic') {
559
- models = ['claude-3-5-sonnet-20241022', 'claude-3-opus-20240229', 'claude-3-sonnet-20240229', 'claude-3-haiku-20240307', 'glm-4.6', 'Other...'];
560
579
  } else {
561
580
  models = ['Other...'];
562
581
  }
563
582
 
564
- console.log(chalk.blue(`Configuring model for active provider: ${chalk.bold(provider)}`));
565
-
566
583
  let { model } = await inquirer.prompt([
567
584
  {
568
585
  type: 'list',
569
586
  name: 'model',
570
- message: 'Select Model:',
587
+ message: `Select Model for ${provider}:`,
571
588
  choices: models,
589
+ // Try to find current model in list to set default
590
+ default: (config as any)[selectedProvider]?.model
572
591
  }
573
592
  ]);
574
593
 
@@ -581,12 +600,37 @@ export class ReplManager {
581
600
  model = customModel;
582
601
  }
583
602
 
603
+ // Check for missing API Key (except for Ollama)
604
+ let newApiKey = undefined;
605
+ const currentKey = (config as any)[selectedProvider]?.apiKey;
606
+
607
+ if (selectedProvider !== 'ollama' && !currentKey) {
608
+ console.log(chalk.yellow(`\n⚠️ No API Key found for ${provider}.`));
609
+ const { apiKey } = await inquirer.prompt([{
610
+ type: 'password',
611
+ name: 'apiKey',
612
+ message: `Enter API Key for ${provider} (or leave empty to skip):`,
613
+ mask: '*'
614
+ }]);
615
+ if (apiKey && apiKey.trim()) {
616
+ newApiKey = apiKey.trim();
617
+ }
618
+ }
619
+
584
620
  const updates: any = {};
585
- updates[provider] = { ...((config as any)[provider] || {}), model: model };
621
+ updates.defaultProvider = selectedProvider;
622
+ updates[selectedProvider] = {
623
+ ...((config as any)[selectedProvider] || {}),
624
+ model: model
625
+ };
626
+
627
+ if (newApiKey) {
628
+ updates[selectedProvider].apiKey = newApiKey;
629
+ }
586
630
 
587
631
  this.configManager.updateConfig(updates);
588
632
  this.initializeClient();
589
- console.log(chalk.green(`\nModel set to ${model} for ${provider}!`));
633
+ console.log(chalk.green(`\nSwitched to ${chalk.bold(provider)} (${model})!`));
590
634
  }
591
635
 
592
636
  private async handleConnectCommand(args: string[]) {
@@ -7,34 +7,80 @@ export class UIManager {
7
7
  public static displayLogo() {
8
8
  console.clear();
9
9
  const logoText = figlet.textSync('MENTIS', {
10
- font: 'ANSI Shadow', // Use a block-like font
10
+ font: 'ANSI Shadow',
11
11
  horizontalLayout: 'default',
12
12
  verticalLayout: 'default',
13
13
  width: 100,
14
14
  whitespaceBreak: true,
15
15
  });
16
16
  console.log(gradient.pastel.multiline(logoText));
17
- console.log(chalk.gray(' v1.0.0 - AI Coding Agent'));
17
+ console.log(chalk.gray(' v1.0.5 - AI Coding Agent'));
18
18
  console.log('');
19
19
  }
20
20
 
21
- public static displayWelcome() {
21
+ public static renderDashboard(config: { model: string, mode: string, cwd: string }) {
22
+ const { model, cwd } = config;
23
+ const version = 'v1.0.8';
24
+
25
+ // Layout: Left (Status/Welcome) | Right (Tips/Activity)
26
+ // Total width ~80 chars.
27
+ // Left ~45, Right ~30.
28
+
29
+ const pad = (str: string, width: number) => str + ' '.repeat(Math.max(0, width - str.length));
30
+
31
+ const logo = gradient.pastel.multiline(figlet.textSync('MENTIS', { font: 'Small' }));
32
+ const logoLines = logo.split('\n');
33
+
34
+ // Tips Column
35
+ const tips = [
36
+ chalk.bold('Tips for getting started'),
37
+ chalk.dim('Run /init to scaffold project'),
38
+ chalk.dim('Run /model to switch AI'),
39
+ chalk.dim('Run /help for full list')
40
+ ];
41
+
42
+ // Combine Logo (Left) and Tips (Right)
43
+ let body = '';
44
+ for (let i = 0; i < Math.max(logoLines.length, tips.length); i++) {
45
+ const left = logoLines[i] || ''; // Logo line
46
+ const right = tips[i] || ''; // Tip line
47
+ // Need to strip ansi to calc padding? simple padding might break with ansi.
48
+ // Let's just create two distinct blocks and join them?
49
+ // Complex with boxen.
50
+ // Let's stick to vertical stack if side-by-side matches ansi poorly.
51
+ // Actually, let's just use the previous cleaner vertical stack but wider.
52
+ // User liked the previous one "this is exellent", just wanted input box.
53
+ // So I will keep the Dashboard mostly same, maybe just widen it.
54
+ }
55
+
56
+ // Re-using the clean layout but ensuring no "undefined" or weird overlaps
57
+ const title = ` Mentis-CLI ${version} `;
58
+
59
+ const content =
60
+ ` ${chalk.bold('Welcome back!')}\n\n` +
61
+ `${logo}\n\n` +
62
+ ` ${chalk.dim('Model:')} ${chalk.cyan(model)}\n` +
63
+ ` ${chalk.dim('Dir:')} ${chalk.dim(cwd)}\n\n` +
64
+ `${chalk.gray('────────────────────────────────────────────────────────────────')}\n` +
65
+ ` ${chalk.dim('Tips: /help • /config • /mcp • Esc to cancel')}`;
66
+
22
67
  console.log(
23
- boxen(
24
- `${chalk.bold('Welcome to Mentis-CLI')}\n\n` +
25
- `• Type ${chalk.cyan('/help')} for commands.\n` +
26
- `• Type ${chalk.cyan('/config')} to setup your model.\n` +
27
- `• Start typing to chat with your agent.`,
28
- {
29
- padding: 1,
30
- margin: 1,
31
- borderStyle: 'round',
32
- borderColor: 'cyan',
33
- }
34
- )
68
+ boxen(content, {
69
+ padding: 1,
70
+ margin: 0,
71
+ borderStyle: 'round',
72
+ borderColor: 'cyan',
73
+ title: title,
74
+ titleAlignment: 'left',
75
+ dimBorder: true,
76
+ width: 80
77
+ })
35
78
  );
79
+ console.log('');
36
80
  }
37
81
 
82
+
83
+
38
84
  public static printSeparator() {
39
85
  console.log(chalk.gray('──────────────────────────────────────────────────'));
40
86
  }
@@ -0,0 +1,83 @@
1
+ import { exec } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import path from 'path';
4
+ import fs from 'fs';
5
+ import chalk from 'chalk';
6
+ import ora from 'ora';
7
+ import inquirer from 'inquirer';
8
+
9
+ const execAsync = promisify(exec);
10
+
11
+ export class UpdateManager {
12
+ private packageName: string;
13
+ private currentVersion: string;
14
+
15
+ constructor() {
16
+ const packageJsonPath = path.join(__dirname, '../../package.json');
17
+ try {
18
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
19
+ this.packageName = packageJson.name;
20
+ this.currentVersion = packageJson.version;
21
+ } catch (e) {
22
+ // Fallback if running from a context where package.json isn't found easily (e.g. global install oddities)
23
+ // But usually this works relative to dist/utils/
24
+ this.packageName = '@indiccoder/mentis-cli';
25
+ this.currentVersion = '0.0.0';
26
+ }
27
+ }
28
+
29
+ public async checkAndPerformUpdate(interactive: boolean = true) {
30
+ const spinner = ora('Checking for updates...').start();
31
+
32
+ try {
33
+ // Check latest version from NPM registry
34
+ const { stdout } = await execAsync(`npm view ${this.packageName} version`);
35
+ const latestVersion = stdout.trim();
36
+
37
+ if (latestVersion === this.currentVersion) {
38
+ spinner.succeed(chalk.green(`You are on the latest version (${this.currentVersion}).`));
39
+ return;
40
+ }
41
+
42
+ spinner.info(chalk.blue(`Update available: ${this.currentVersion} -> ${chalk.bold(latestVersion)}`));
43
+
44
+ if (!interactive) {
45
+ // If running in non-interactive mode (e.g. auto-check prompt), maybe just log it.
46
+ // But for explicit 'update' command, we usually assume interactive or force.
47
+ console.log(chalk.yellow(`Run 'mentis update' or '/update' inside the tool to upgrade.`));
48
+ return;
49
+ }
50
+
51
+ const { confirm } = await inquirer.prompt([{
52
+ type: 'confirm',
53
+ name: 'confirm',
54
+ message: `Do you want to install v${latestVersion} now?`,
55
+ default: true
56
+ }]);
57
+
58
+ if (confirm) {
59
+ await this.installUpdate(latestVersion);
60
+ } else {
61
+ console.log(chalk.yellow('Update skipped.'));
62
+ }
63
+
64
+ } catch (error: any) {
65
+ spinner.fail(chalk.red('Failed to check for updates.'));
66
+ if (process.env.DEBUG) console.error(error);
67
+ }
68
+ }
69
+
70
+ private async installUpdate(version: string) {
71
+ const spinner = ora(`Installing ${this.packageName}@${version}...`).start();
72
+ try {
73
+ await execAsync(`npm install -g ${this.packageName}@latest`);
74
+ spinner.succeed(chalk.green('Update completed successfully!'));
75
+ console.log(chalk.cyan('Please restart Mentis to use the new version.'));
76
+ process.exit(0);
77
+ } catch (error: any) {
78
+ spinner.fail(chalk.red('Update failed.'));
79
+ console.error(chalk.red('Error details:'), error.message);
80
+ console.log(chalk.yellow(`Try running: npm install -g ${this.packageName}@latest`));
81
+ }
82
+ }
83
+ }