@indiccoder/mentis-cli 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +90 -0
- package/console.log(tick) +0 -0
- package/debug_fs.js +12 -0
- package/dist/checkpoint/CheckpointManager.js +53 -0
- package/dist/config/ConfigManager.js +55 -0
- package/dist/context/ContextManager.js +55 -0
- package/dist/context/RepoMapper.js +112 -0
- package/dist/index.js +12 -0
- package/dist/llm/AnthropicClient.js +70 -0
- package/dist/llm/ModelInterface.js +2 -0
- package/dist/llm/OpenAIClient.js +58 -0
- package/dist/mcp/JsonRpcClient.js +117 -0
- package/dist/mcp/McpClient.js +59 -0
- package/dist/repl/PersistentShell.js +75 -0
- package/dist/repl/ReplManager.js +813 -0
- package/dist/tools/FileTools.js +100 -0
- package/dist/tools/GitTools.js +127 -0
- package/dist/tools/PersistentShellTool.js +30 -0
- package/dist/tools/SearchTools.js +83 -0
- package/dist/tools/Tool.js +2 -0
- package/dist/tools/WebSearchTool.js +60 -0
- package/dist/ui/UIManager.js +40 -0
- package/package.json +63 -0
- package/screenshot_1765779883482_9b30.png +0 -0
- package/scripts/test_features.ts +48 -0
- package/scripts/test_glm.ts +53 -0
- package/scripts/test_models.ts +38 -0
- package/src/checkpoint/CheckpointManager.ts +61 -0
- package/src/config/ConfigManager.ts +77 -0
- package/src/context/ContextManager.ts +63 -0
- package/src/context/RepoMapper.ts +119 -0
- package/src/index.ts +12 -0
- package/src/llm/ModelInterface.ts +47 -0
- package/src/llm/OpenAIClient.ts +64 -0
- package/src/mcp/JsonRpcClient.ts +103 -0
- package/src/mcp/McpClient.ts +75 -0
- package/src/repl/PersistentShell.ts +85 -0
- package/src/repl/ReplManager.ts +842 -0
- package/src/tools/FileTools.ts +89 -0
- package/src/tools/GitTools.ts +113 -0
- package/src/tools/PersistentShellTool.ts +32 -0
- package/src/tools/SearchTools.ts +74 -0
- package/src/tools/Tool.ts +6 -0
- package/src/tools/WebSearchTool.ts +63 -0
- package/src/ui/UIManager.ts +41 -0
- package/tsconfig.json +21 -0
package/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Mentis-CLI π§
|
|
2
|
+
> **An Agentic, Multi-Model CLI Coding Assistant.**
|
|
3
|
+
|
|
4
|
+
Mentis is a powerful terminal-based AI coding assistant that lives in your command line. It supports multiple LLM providers (Gemini, Ollama, OpenAI), agentic file operations, MCP (Model Context Protocol), and more.
|
|
5
|
+
|
|
6
|
+
## β¨ Features
|
|
7
|
+
|
|
8
|
+
* **π€ Multi-Model Support**: Switch seamlessly between **Gemini**, **Ollama** (Local), and **OpenAI**.
|
|
9
|
+
* **π οΈ Agentic Capabilities**: Mentis can read, write, list files, and search your codebase to understand context.
|
|
10
|
+
* **π Web Intelligence**: Hybrid **Google** + **DuckDuckGo** search for documentation and error fixing (Zero-Config).
|
|
11
|
+
* **πΊοΈ Smart Context**: Automatically maps your repository structure for better spatial awareness.
|
|
12
|
+
* **π° Cost Awareness**: Real-time token usage and cost estimation displayed after every response.
|
|
13
|
+
* **π§ Persistent Memory**: Auto-saves sessions (`/resume`) and supports manual checkpoints (`/checkpoint`).
|
|
14
|
+
* **π MCP Support**: Full [Model Context Protocol](https://github.com/modelcontextprotocol) client. Connect external tools like databases or memory servers.
|
|
15
|
+
* **π Codebase Search**: Built-in `grep` tool to find code across your project.
|
|
16
|
+
* **π Shell Integration**: Run shell commands (`/run`) and commit changes (`/commit`) directly from the agent.
|
|
17
|
+
* **π¨ Premium UI**: Beautiful terminal interface with modes (`PLAN` vs `BUILD`).
|
|
18
|
+
|
|
19
|
+
## π Installation
|
|
20
|
+
|
|
21
|
+
### From GitHub (Recommended)
|
|
22
|
+
You can install Mentis directly from the repository, ensuring you always get the latest version (bypassing NPM delays).
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install -g git+https://github.com/CoderVLSI/Mentis-CLI.git
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### From Source (Dev)
|
|
29
|
+
```bash
|
|
30
|
+
git clone https://github.com/CoderVLSI/Mentis-CLI.git
|
|
31
|
+
cd Mentis-CLI
|
|
32
|
+
npm install
|
|
33
|
+
npm run build
|
|
34
|
+
npm link
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## βοΈ Configuration
|
|
38
|
+
|
|
39
|
+
Start Mentis by typing:
|
|
40
|
+
```bash
|
|
41
|
+
mentis
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Setting up Models
|
|
45
|
+
Type `/model` inside the CLI to launch the interactive configuration wizard:
|
|
46
|
+
1. **Select Provider**: Gemini, Ollama, or OpenAI.
|
|
47
|
+
2. **Select Model**: e.g., `gemini-2.5-flash`, `llama3`.
|
|
48
|
+
3. **Enter Credentials**: API Key (for cloud) or Base URL (for local).
|
|
49
|
+
|
|
50
|
+
*Credentials are stored securely in `~/.mentisrc`.*
|
|
51
|
+
|
|
52
|
+
## π Usage
|
|
53
|
+
|
|
54
|
+
### Modes
|
|
55
|
+
* **/plan**: Switch to high-level planning mode (Architecture, requirements).
|
|
56
|
+
* **/build**: Switch to code generation mode (Implementation).
|
|
57
|
+
|
|
58
|
+
### Commands
|
|
59
|
+
| Command | Description |
|
|
60
|
+
| :--- | :--- |
|
|
61
|
+
| `/help` | Show available commands |
|
|
62
|
+
| `/model` | Configure AI provider and model (Interactive) |
|
|
63
|
+
| `/resume` | Resume the last session |
|
|
64
|
+
| `/checkpoint` | Manage saved sessions (`save`, `load`, `list`) |
|
|
65
|
+
| `/search <query>` | Search for code in the current directory |
|
|
66
|
+
| `/mcp connect <cmd>` | Connect an MCP Server (e.g., `npx -y @modelcontextprotocol/server-time`) |
|
|
67
|
+
| `/run <cmd>` | Execute a shell command |
|
|
68
|
+
| `/commit [msg]` | Stage and commit changes to Git |
|
|
69
|
+
| `/add <file>` | Add a specific file to context |
|
|
70
|
+
| `/drop <file>` | Remove a file from context |
|
|
71
|
+
| `/clear` | Clear chat history |
|
|
72
|
+
| `/exit` | Save and exit |
|
|
73
|
+
|
|
74
|
+
### Example Workflow
|
|
75
|
+
```text
|
|
76
|
+
Mentis > /plan
|
|
77
|
+
[PLAN] > Analyze this project structure and suggest improvements.
|
|
78
|
+
|
|
79
|
+
Mentis > /build
|
|
80
|
+
[BUILD] > Implement the FolderManager class in src/utils.ts.
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## π Safety
|
|
84
|
+
Mentis asks for **explicit approval** before writing or modifying any files, keeping you in control.
|
|
85
|
+
|
|
86
|
+
## π€ Contributing
|
|
87
|
+
Pull requests are welcome!
|
|
88
|
+
|
|
89
|
+
## π License
|
|
90
|
+
ISC
|
|
File without changes
|
package/debug_fs.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
const target = path.join(os.homedir(), 'mentis_debug_write_test.txt');
|
|
6
|
+
console.log(`Attempting to write to: ${target}`);
|
|
7
|
+
try {
|
|
8
|
+
fs.writeFileSync(target, 'Hello world');
|
|
9
|
+
console.log('Success!');
|
|
10
|
+
} catch (e) {
|
|
11
|
+
console.error('Failed:', e);
|
|
12
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
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.CheckpointManager = void 0;
|
|
7
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const os_1 = __importDefault(require("os"));
|
|
10
|
+
class CheckpointManager {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.checkpointDir = path_1.default.join(os_1.default.homedir(), '.mentis', 'checkpoints');
|
|
13
|
+
fs_extra_1.default.ensureDirSync(this.checkpointDir);
|
|
14
|
+
}
|
|
15
|
+
save(name, history, files) {
|
|
16
|
+
const checkpoint = {
|
|
17
|
+
timestamp: Date.now(),
|
|
18
|
+
name,
|
|
19
|
+
history,
|
|
20
|
+
files
|
|
21
|
+
};
|
|
22
|
+
const filePath = path_1.default.join(this.checkpointDir, `${name}.json`);
|
|
23
|
+
fs_extra_1.default.writeJsonSync(filePath, checkpoint, { spaces: 2 });
|
|
24
|
+
return filePath;
|
|
25
|
+
}
|
|
26
|
+
load(name) {
|
|
27
|
+
const filePath = path_1.default.join(this.checkpointDir, `${name}.json`);
|
|
28
|
+
if (fs_extra_1.default.existsSync(filePath)) {
|
|
29
|
+
return fs_extra_1.default.readJsonSync(filePath);
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
list() {
|
|
34
|
+
if (!fs_extra_1.default.existsSync(this.checkpointDir))
|
|
35
|
+
return [];
|
|
36
|
+
return fs_extra_1.default.readdirSync(this.checkpointDir)
|
|
37
|
+
.filter(f => f.endsWith('.json'))
|
|
38
|
+
.map(f => f.replace('.json', ''));
|
|
39
|
+
}
|
|
40
|
+
delete(name) {
|
|
41
|
+
const filePath = path_1.default.join(this.checkpointDir, `${name}.json`);
|
|
42
|
+
if (fs_extra_1.default.existsSync(filePath)) {
|
|
43
|
+
fs_extra_1.default.removeSync(filePath);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
exists(name) {
|
|
49
|
+
const filePath = path_1.default.join(this.checkpointDir, `${name}.json`);
|
|
50
|
+
return fs_extra_1.default.existsSync(filePath);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
exports.CheckpointManager = CheckpointManager;
|
|
@@ -0,0 +1,55 @@
|
|
|
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.ConfigManager = void 0;
|
|
7
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const os_1 = __importDefault(require("os"));
|
|
10
|
+
class ConfigManager {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.configPath = path_1.default.join(os_1.default.homedir(), '.mentisrc');
|
|
13
|
+
this.config = {
|
|
14
|
+
defaultProvider: 'ollama',
|
|
15
|
+
ollama: {
|
|
16
|
+
baseUrl: 'http://localhost:11434/v1',
|
|
17
|
+
model: 'llama3:latest'
|
|
18
|
+
},
|
|
19
|
+
gemini: {
|
|
20
|
+
model: 'gemini-2.5-flash'
|
|
21
|
+
},
|
|
22
|
+
glm: {
|
|
23
|
+
model: 'glm-4.6',
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
this.loadConfig();
|
|
27
|
+
}
|
|
28
|
+
loadConfig() {
|
|
29
|
+
try {
|
|
30
|
+
if (fs_extra_1.default.existsSync(this.configPath)) {
|
|
31
|
+
const fileContent = fs_extra_1.default.readFileSync(this.configPath, 'utf-8');
|
|
32
|
+
this.config = { ...this.config, ...JSON.parse(fileContent) };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
console.error('Error loading config:', error);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
saveConfig() {
|
|
40
|
+
try {
|
|
41
|
+
fs_extra_1.default.writeFileSync(this.configPath, JSON.stringify(this.config, null, 2));
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
console.error('Error saving config:', error);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
getConfig() {
|
|
48
|
+
return this.config;
|
|
49
|
+
}
|
|
50
|
+
updateConfig(newConfig) {
|
|
51
|
+
this.config = { ...this.config, ...newConfig };
|
|
52
|
+
this.saveConfig();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
exports.ConfigManager = ConfigManager;
|
|
@@ -0,0 +1,55 @@
|
|
|
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.ContextManager = void 0;
|
|
7
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const RepoMapper_1 = require("./RepoMapper");
|
|
10
|
+
class ContextManager {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.files = new Map();
|
|
13
|
+
this.repoMapper = new RepoMapper_1.RepoMapper(process.cwd());
|
|
14
|
+
}
|
|
15
|
+
async addFile(filePath) {
|
|
16
|
+
try {
|
|
17
|
+
const absolutePath = path_1.default.resolve(process.cwd(), filePath);
|
|
18
|
+
if (!fs_extra_1.default.existsSync(absolutePath)) {
|
|
19
|
+
throw new Error(`File not found: ${filePath}`);
|
|
20
|
+
}
|
|
21
|
+
const content = await fs_extra_1.default.readFile(absolutePath, 'utf-8');
|
|
22
|
+
this.files.set(absolutePath, content);
|
|
23
|
+
return `Added ${filePath} to context.`;
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
return `Error adding file: ${error.message}`;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
removeFile(filePath) {
|
|
30
|
+
const absolutePath = path_1.default.resolve(process.cwd(), filePath);
|
|
31
|
+
if (this.files.has(absolutePath)) {
|
|
32
|
+
this.files.delete(absolutePath);
|
|
33
|
+
return `Removed ${filePath} from context.`;
|
|
34
|
+
}
|
|
35
|
+
return `File not in context: ${filePath}`;
|
|
36
|
+
}
|
|
37
|
+
clear() {
|
|
38
|
+
this.files.clear();
|
|
39
|
+
}
|
|
40
|
+
getContextString() {
|
|
41
|
+
const repoMap = this.repoMapper.generateTree();
|
|
42
|
+
let context = `Repository Structure:\n${repoMap}\n\n`;
|
|
43
|
+
if (this.files.size > 0) {
|
|
44
|
+
context += 'Current File Context:\n\n';
|
|
45
|
+
for (const [filePath, content] of this.files.entries()) {
|
|
46
|
+
context += `--- File: ${path_1.default.basename(filePath)} ---\n${content}\n\n`;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return context;
|
|
50
|
+
}
|
|
51
|
+
getFiles() {
|
|
52
|
+
return Array.from(this.files.keys());
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
exports.ContextManager = ContextManager;
|
|
@@ -0,0 +1,112 @@
|
|
|
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.RepoMapper = void 0;
|
|
7
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const child_process_1 = require("child_process");
|
|
10
|
+
class RepoMapper {
|
|
11
|
+
constructor(rootDir) {
|
|
12
|
+
this.rootDir = rootDir;
|
|
13
|
+
this.ignorePatterns = new Set(['.git', 'node_modules', 'dist', 'coverage', '.DS_Store']);
|
|
14
|
+
}
|
|
15
|
+
generateTree() {
|
|
16
|
+
try {
|
|
17
|
+
// Try using git first for speed
|
|
18
|
+
const stdout = (0, child_process_1.execSync)('git ls-files --cached --others --exclude-standard', {
|
|
19
|
+
cwd: this.rootDir,
|
|
20
|
+
encoding: 'utf-8',
|
|
21
|
+
stdio: ['ignore', 'pipe', 'ignore'] // Ignore stderr to avoid noise
|
|
22
|
+
});
|
|
23
|
+
const files = stdout.split('\n').filter(Boolean);
|
|
24
|
+
return this.buildTreeFromPaths(files);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
// Fallback to manual walk if not a git repo or git fails
|
|
28
|
+
return this.walk(this.rootDir, '');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
buildTreeFromPaths(paths) {
|
|
32
|
+
const tree = {};
|
|
33
|
+
// Build object tree
|
|
34
|
+
for (const p of paths) {
|
|
35
|
+
const parts = p.split('/');
|
|
36
|
+
let current = tree;
|
|
37
|
+
for (const part of parts) {
|
|
38
|
+
if (!current[part]) {
|
|
39
|
+
current[part] = {};
|
|
40
|
+
}
|
|
41
|
+
current = current[part];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Convert object tree to string
|
|
45
|
+
return this.renderTree(tree, '');
|
|
46
|
+
}
|
|
47
|
+
renderTree(node, indent) {
|
|
48
|
+
let result = '';
|
|
49
|
+
const keys = Object.keys(node).sort((a, b) => {
|
|
50
|
+
const aIsLeaf = Object.keys(node[a]).length === 0;
|
|
51
|
+
const bIsLeaf = Object.keys(node[b]).length === 0;
|
|
52
|
+
// Dirs first
|
|
53
|
+
if (!aIsLeaf && bIsLeaf)
|
|
54
|
+
return -1;
|
|
55
|
+
if (aIsLeaf && !bIsLeaf)
|
|
56
|
+
return 1;
|
|
57
|
+
return a.localeCompare(b);
|
|
58
|
+
});
|
|
59
|
+
keys.forEach((key, index) => {
|
|
60
|
+
const isLast = index === keys.length - 1;
|
|
61
|
+
const isDir = Object.keys(node[key]).length > 0;
|
|
62
|
+
const prefix = isLast ? 'βββ ' : 'βββ ';
|
|
63
|
+
const childIndent = isLast ? ' ' : 'β ';
|
|
64
|
+
result += `${indent}${prefix}${key}${isDir ? '/' : ''}\n`;
|
|
65
|
+
if (isDir) {
|
|
66
|
+
result += this.renderTree(node[key], indent + childIndent);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
walk(currentPath, indent) {
|
|
72
|
+
try {
|
|
73
|
+
const files = fs_extra_1.default.readdirSync(currentPath);
|
|
74
|
+
let result = '';
|
|
75
|
+
// Cache stats to avoid repeated calls during sort
|
|
76
|
+
const fileStats = files.map(file => {
|
|
77
|
+
try {
|
|
78
|
+
return {
|
|
79
|
+
name: file,
|
|
80
|
+
isDir: fs_extra_1.default.statSync(path_1.default.join(currentPath, file)).isDirectory()
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}).filter(f => f);
|
|
87
|
+
// Sort: Directories first, then files
|
|
88
|
+
fileStats.sort((a, b) => {
|
|
89
|
+
if (a.isDir && !b.isDir)
|
|
90
|
+
return -1;
|
|
91
|
+
if (!a.isDir && b.isDir)
|
|
92
|
+
return 1;
|
|
93
|
+
return a.name.localeCompare(b.name);
|
|
94
|
+
});
|
|
95
|
+
const filteredFiles = fileStats.filter(f => !this.ignorePatterns.has(f.name));
|
|
96
|
+
filteredFiles.forEach((fileObj, index) => {
|
|
97
|
+
const isLast = index === filteredFiles.length - 1;
|
|
98
|
+
const prefix = isLast ? 'βββ ' : 'βββ ';
|
|
99
|
+
const childIndent = isLast ? ' ' : 'β ';
|
|
100
|
+
result += `${indent}${prefix}${fileObj.name}${fileObj.isDir ? '/' : ''}\n`;
|
|
101
|
+
if (fileObj.isDir) {
|
|
102
|
+
result += this.walk(path_1.default.join(currentPath, fileObj.name), indent + childIndent);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
return `${indent}Error reading directory: ${e}\n`;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
exports.RepoMapper = RepoMapper;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const ReplManager_1 = require("./repl/ReplManager");
|
|
5
|
+
async function main() {
|
|
6
|
+
const repl = new ReplManager_1.ReplManager();
|
|
7
|
+
await repl.start();
|
|
8
|
+
}
|
|
9
|
+
main().catch((error) => {
|
|
10
|
+
console.error('Fatal error:', error);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
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.AnthropicClient = void 0;
|
|
7
|
+
const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
|
|
8
|
+
class AnthropicClient {
|
|
9
|
+
constructor(apiKey, model, baseUrl) {
|
|
10
|
+
this.client = new sdk_1.default({
|
|
11
|
+
apiKey: apiKey,
|
|
12
|
+
baseURL: baseUrl, // Optional, allows pointing to Z.ai or other proxies
|
|
13
|
+
});
|
|
14
|
+
this.model = model;
|
|
15
|
+
}
|
|
16
|
+
async chat(messages, tools) {
|
|
17
|
+
try {
|
|
18
|
+
// Convert ChatMessage (system, user, assistant) to Anthropic format
|
|
19
|
+
// Anthropic handles 'system' prompt via a separate parameter, not in the messages array.
|
|
20
|
+
let systemPrompt = undefined;
|
|
21
|
+
const anthropicMessages = [];
|
|
22
|
+
for (const msg of messages) {
|
|
23
|
+
if (msg.role === 'system') {
|
|
24
|
+
systemPrompt = msg.content;
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
anthropicMessages.push({
|
|
28
|
+
role: msg.role,
|
|
29
|
+
content: msg.content
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// If no system prompt found in messages, use default
|
|
34
|
+
if (!systemPrompt) {
|
|
35
|
+
systemPrompt = 'You are Mentis, an expert AI coding assistant. You help users write code, debug issues, and explain concepts.';
|
|
36
|
+
}
|
|
37
|
+
const params = {
|
|
38
|
+
model: this.model,
|
|
39
|
+
messages: anthropicMessages,
|
|
40
|
+
system: systemPrompt,
|
|
41
|
+
max_tokens: 4096,
|
|
42
|
+
};
|
|
43
|
+
// TODO: Map tools if supported (Z.ai docs say they support tools)
|
|
44
|
+
// For now, simplicity first.
|
|
45
|
+
const response = await this.client.messages.create(params);
|
|
46
|
+
// Calculate usage (approximate if not provided, but Anthropic provides it)
|
|
47
|
+
const inputTokens = response.usage.input_tokens;
|
|
48
|
+
const outputTokens = response.usage.output_tokens;
|
|
49
|
+
// Extract content (handling text blocks)
|
|
50
|
+
let content = '';
|
|
51
|
+
for (const block of response.content) {
|
|
52
|
+
if (block.type === 'text') {
|
|
53
|
+
content += block.text;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
content: content,
|
|
58
|
+
usage: {
|
|
59
|
+
input_tokens: inputTokens,
|
|
60
|
+
output_tokens: outputTokens
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
console.error('Error calling Anthropic API:', error.message);
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
exports.AnthropicClient = AnthropicClient;
|
|
@@ -0,0 +1,58 @@
|
|
|
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.OpenAIClient = void 0;
|
|
7
|
+
const axios_1 = __importDefault(require("axios"));
|
|
8
|
+
class OpenAIClient {
|
|
9
|
+
constructor(baseUrl, apiKey, model) {
|
|
10
|
+
this.baseUrl = baseUrl.replace(/\/$/, ''); // Remove trailing slash
|
|
11
|
+
this.apiKey = apiKey;
|
|
12
|
+
this.model = model;
|
|
13
|
+
}
|
|
14
|
+
async chat(messages, tools, signal) {
|
|
15
|
+
try {
|
|
16
|
+
const requestBody = {
|
|
17
|
+
model: this.model,
|
|
18
|
+
messages: [
|
|
19
|
+
{ role: 'system', content: 'You are Mentis, an expert AI coding assistant. You help users write code, debug issues, and explain concepts. You are concise, accurate, and professional.' },
|
|
20
|
+
...messages
|
|
21
|
+
],
|
|
22
|
+
temperature: 0.7,
|
|
23
|
+
};
|
|
24
|
+
if (tools && tools.length > 0) {
|
|
25
|
+
requestBody.tools = tools;
|
|
26
|
+
requestBody.tool_choice = 'auto';
|
|
27
|
+
}
|
|
28
|
+
const response = await axios_1.default.post(`${this.baseUrl}/chat/completions`, requestBody, {
|
|
29
|
+
headers: {
|
|
30
|
+
'Content-Type': 'application/json',
|
|
31
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
32
|
+
},
|
|
33
|
+
signal: signal // Pass AbortSignal to Axios
|
|
34
|
+
});
|
|
35
|
+
const choice = response.data.choices[0];
|
|
36
|
+
return {
|
|
37
|
+
content: response.data.choices[0].message.content,
|
|
38
|
+
tool_calls: response.data.choices[0].message.tool_calls,
|
|
39
|
+
usage: {
|
|
40
|
+
input_tokens: response.data.usage?.prompt_tokens || 0,
|
|
41
|
+
output_tokens: response.data.usage?.completion_tokens || 0
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (axios_1.default.isCancel(error) || error.name === 'AbortError' || error.code === 'ERR_CANCELED') {
|
|
47
|
+
// Rethrow as specific cancellation to be handled by caller
|
|
48
|
+
throw new Error('Request cancelled by user');
|
|
49
|
+
}
|
|
50
|
+
console.error('Error calling model API:', error.message);
|
|
51
|
+
if (error.response) {
|
|
52
|
+
console.error('Response data:', error.response.data);
|
|
53
|
+
}
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
exports.OpenAIClient = OpenAIClient;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.JsonRpcClient = void 0;
|
|
37
|
+
const child_process_1 = require("child_process");
|
|
38
|
+
const readline = __importStar(require("readline"));
|
|
39
|
+
class JsonRpcClient {
|
|
40
|
+
constructor(command, args) {
|
|
41
|
+
this.sequence = 0;
|
|
42
|
+
this.pendingRequests = new Map();
|
|
43
|
+
this.process = (0, child_process_1.spawn)(command, args, {
|
|
44
|
+
stdio: ['pipe', 'pipe', 'inherit'], // stdin, stdout, stderr
|
|
45
|
+
});
|
|
46
|
+
if (!this.process.stdout) {
|
|
47
|
+
throw new Error('Failed to open stdout for MCP process');
|
|
48
|
+
}
|
|
49
|
+
const rl = readline.createInterface({ input: this.process.stdout });
|
|
50
|
+
rl.on('line', (line) => this.handleMessage(line));
|
|
51
|
+
this.process.on('error', (err) => console.error('MCP Process Error:', err));
|
|
52
|
+
this.process.on('exit', (code) => console.log('MCP Process Exited:', code));
|
|
53
|
+
}
|
|
54
|
+
handleMessage(line) {
|
|
55
|
+
try {
|
|
56
|
+
if (!line.trim())
|
|
57
|
+
return;
|
|
58
|
+
const message = JSON.parse(line);
|
|
59
|
+
// Handle Response
|
|
60
|
+
if (message.id !== undefined && (message.result !== undefined || message.error !== undefined)) {
|
|
61
|
+
const handler = this.pendingRequests.get(message.id);
|
|
62
|
+
if (handler) {
|
|
63
|
+
this.pendingRequests.delete(message.id);
|
|
64
|
+
if (message.error) {
|
|
65
|
+
handler.reject(new Error(message.error.message));
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
handler.resolve(message.result);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
// Handle Notification or Request from server (not implemented deep yet)
|
|
74
|
+
// console.log('Received notification from MCP:', message);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
console.error('Failed to parse MCP message:', line, e);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async sendRequest(method, params) {
|
|
82
|
+
const id = this.sequence++;
|
|
83
|
+
const request = {
|
|
84
|
+
jsonrpc: '2.0',
|
|
85
|
+
method,
|
|
86
|
+
params,
|
|
87
|
+
id,
|
|
88
|
+
};
|
|
89
|
+
return new Promise((resolve, reject) => {
|
|
90
|
+
this.pendingRequests.set(id, { resolve, reject });
|
|
91
|
+
try {
|
|
92
|
+
if (!this.process.stdin)
|
|
93
|
+
throw new Error('Stdin not available');
|
|
94
|
+
const data = JSON.stringify(request) + '\n';
|
|
95
|
+
this.process.stdin.write(data);
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
this.pendingRequests.delete(id);
|
|
99
|
+
reject(e);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
sendNotification(method, params) {
|
|
104
|
+
if (!this.process.stdin)
|
|
105
|
+
return;
|
|
106
|
+
const notification = {
|
|
107
|
+
jsonrpc: '2.0',
|
|
108
|
+
method,
|
|
109
|
+
params,
|
|
110
|
+
};
|
|
111
|
+
this.process.stdin.write(JSON.stringify(notification) + '\n');
|
|
112
|
+
}
|
|
113
|
+
disconnect() {
|
|
114
|
+
this.process.kill();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
exports.JsonRpcClient = JsonRpcClient;
|