@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 +6 -0
- package/dist/repl/ReplManager.js +66 -29
- package/dist/ui/UIManager.js +46 -8
- package/dist/utils/UpdateManager.js +81 -0
- package/package.json +1 -1
- package/scripts/test_config_update.ts +63 -0
- package/src/index.ts +7 -0
- package/src/repl/ReplManager.ts +73 -29
- package/src/ui/UIManager.ts +61 -15
- package/src/utils/UpdateManager.ts +83 -0
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
|
}
|
package/dist/repl/ReplManager.js
CHANGED
|
@@ -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.
|
|
127
|
-
|
|
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();
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
//
|
|
144
|
-
|
|
145
|
-
const promptText =
|
|
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
|
|
522
|
-
//
|
|
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[
|
|
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 ${
|
|
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 (
|
|
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 (
|
|
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 (
|
|
556
|
+
else if (selectedProvider === 'openai') {
|
|
540
557
|
models = ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'Other...'];
|
|
541
558
|
}
|
|
542
|
-
else if (
|
|
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:
|
|
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
|
|
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(`\
|
|
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) {
|
package/dist/ui/UIManager.js
CHANGED
|
@@ -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',
|
|
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.
|
|
22
|
+
console.log(chalk_1.default.gray(' v1.0.5 - AI Coding Agent'));
|
|
23
23
|
console.log('');
|
|
24
24
|
}
|
|
25
|
-
static
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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:
|
|
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
|
@@ -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
|
}
|
package/src/repl/ReplManager.ts
CHANGED
|
@@ -99,28 +99,28 @@ export class ReplManager {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
public async start() {
|
|
102
|
-
UIManager.
|
|
103
|
-
|
|
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();
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
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
|
|
542
|
+
const currentProvider = config.defaultProvider || 'ollama';
|
|
537
543
|
|
|
538
|
-
//
|
|
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[
|
|
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 ${
|
|
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 (
|
|
571
|
+
if (selectedProvider === 'gemini') {
|
|
551
572
|
models = ['gemini-2.5-flash', 'gemini-1.5-pro', 'gemini-1.0-pro', 'Other...'];
|
|
552
|
-
} else if (
|
|
553
|
-
models = ['llama3:latest', 'deepseek-r1:latest', 'mistral:latest', 'Other...'];
|
|
554
|
-
} else if (
|
|
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 (
|
|
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:
|
|
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
|
|
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(`\
|
|
633
|
+
console.log(chalk.green(`\nSwitched to ${chalk.bold(provider)} (${model})!`));
|
|
590
634
|
}
|
|
591
635
|
|
|
592
636
|
private async handleConnectCommand(args: string[]) {
|
package/src/ui/UIManager.ts
CHANGED
|
@@ -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',
|
|
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.
|
|
17
|
+
console.log(chalk.gray(' v1.0.5 - AI Coding Agent'));
|
|
18
18
|
console.log('');
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
public static
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
+
}
|