@indiccoder/mentis-cli 1.1.4 → 1.1.5

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.
Files changed (64) hide show
  1. package/.claude/settings.local.json +8 -0
  2. package/.mentis/session.json +15 -0
  3. package/.mentis/sessions/1769189035730.json +23 -0
  4. package/.mentis/sessions/1769189569160.json +23 -0
  5. package/.mentis/sessions/1769767538672.json +23 -0
  6. package/.mentis/sessions/1769767785155.json +23 -0
  7. package/.mentis/sessions/1769768745802.json +23 -0
  8. package/.mentis/sessions/1769769600884.json +31 -0
  9. package/.mentis/sessions/1769770030160.json +31 -0
  10. package/.mentis/sessions/1769770606004.json +78 -0
  11. package/.mentis/sessions/1769771084515.json +141 -0
  12. package/.mentis/sessions/1769881926630.json +57 -0
  13. package/README.md +17 -0
  14. package/dist/checkpoint/CheckpointManager.js +92 -0
  15. package/dist/debug_google.js +61 -0
  16. package/dist/debug_lite.js +49 -0
  17. package/dist/debug_lite_headers.js +57 -0
  18. package/dist/debug_search.js +16 -0
  19. package/dist/index.js +10 -0
  20. package/dist/mcp/JsonRpcClient.js +16 -0
  21. package/dist/mcp/McpConfig.js +132 -0
  22. package/dist/mcp/McpManager.js +189 -0
  23. package/dist/repl/PersistentShell.js +20 -1
  24. package/dist/repl/ReplManager.js +410 -138
  25. package/dist/tools/AskQuestionTool.js +172 -0
  26. package/dist/tools/EditFileTool.js +141 -0
  27. package/dist/tools/FileTools.js +7 -1
  28. package/dist/tools/PlanModeTool.js +53 -0
  29. package/dist/tools/WebSearchTool.js +190 -27
  30. package/dist/ui/DiffViewer.js +110 -0
  31. package/dist/ui/InputBox.js +16 -2
  32. package/dist/ui/MultiFileSelector.js +123 -0
  33. package/dist/ui/PlanModeUI.js +105 -0
  34. package/dist/ui/ToolExecutor.js +154 -0
  35. package/dist/ui/UIManager.js +12 -2
  36. package/docs/MCP_INTEGRATION.md +290 -0
  37. package/google_dump.html +18 -0
  38. package/lite_dump.html +176 -0
  39. package/lite_headers_dump.html +176 -0
  40. package/package.json +16 -5
  41. package/scripts/test_exa_mcp.ts +90 -0
  42. package/src/checkpoint/CheckpointManager.ts +102 -0
  43. package/src/debug_google.ts +30 -0
  44. package/src/debug_lite.ts +18 -0
  45. package/src/debug_lite_headers.ts +25 -0
  46. package/src/debug_search.ts +18 -0
  47. package/src/index.ts +12 -0
  48. package/src/mcp/JsonRpcClient.ts +19 -0
  49. package/src/mcp/McpConfig.ts +153 -0
  50. package/src/mcp/McpManager.ts +224 -0
  51. package/src/repl/PersistentShell.ts +24 -1
  52. package/src/repl/ReplManager.ts +1521 -1204
  53. package/src/tools/AskQuestionTool.ts +197 -0
  54. package/src/tools/EditFileTool.ts +172 -0
  55. package/src/tools/FileTools.ts +3 -0
  56. package/src/tools/PlanModeTool.ts +50 -0
  57. package/src/tools/WebSearchTool.ts +235 -63
  58. package/src/ui/DiffViewer.ts +117 -0
  59. package/src/ui/InputBox.ts +17 -2
  60. package/src/ui/MultiFileSelector.ts +135 -0
  61. package/src/ui/PlanModeUI.ts +121 -0
  62. package/src/ui/ToolExecutor.ts +182 -0
  63. package/src/ui/UIManager.ts +15 -2
  64. package/console.log(tick) +0 -0
@@ -0,0 +1,18 @@
1
+
2
+ import { execSync } from 'child_process';
3
+ import * as fs from 'fs';
4
+
5
+ const query = "expo go latest sdk";
6
+ const url = `https://lite.duckduckgo.com/lite/?q=${encodeURIComponent(query)}`;
7
+ console.log(`Fetching Lite DDG: ${url}`);
8
+
9
+ try {
10
+ const cmd = `curl -s -L -A "Mozilla/5.0" "${url}"`;
11
+ const html = execSync(cmd, { encoding: 'utf-8' });
12
+
13
+ fs.writeFileSync('lite_dump.html', html);
14
+ console.log('Dumped HTML to lite_dump.html');
15
+
16
+ } catch (e: any) {
17
+ console.error('Error:', e.message);
18
+ }
@@ -0,0 +1,25 @@
1
+
2
+ import { execSync } from 'child_process';
3
+ import * as fs from 'fs';
4
+
5
+ const query = "expo go latest sdk";
6
+ const url = `https://lite.duckduckgo.com/lite/?q=${encodeURIComponent(query)}`;
7
+ console.log(`Testing Lite Headers: ${url}`);
8
+
9
+ try {
10
+ const cmd = `curl -s -L -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" -H "Referer: https://duckduckgo.com/" -H "Accept-Language: en-US,en;q=0.9" "${url}"`;
11
+ const html = execSync(cmd, { encoding: 'utf-8' });
12
+
13
+ fs.writeFileSync('lite_headers_dump.html', html);
14
+
15
+ if (html.includes('anomaly-modal')) {
16
+ console.log('STILL BLOCKED by Captcha');
17
+ } else if (html.includes('result-link')) {
18
+ console.log('SUCCESS! Found result links.');
19
+ } else {
20
+ console.log('Unknown response. Check dump.');
21
+ }
22
+
23
+ } catch (e: any) {
24
+ console.error('Error:', e.message);
25
+ }
@@ -0,0 +1,18 @@
1
+
2
+ import { execSync } from 'child_process';
3
+
4
+ const query = "test";
5
+ const url = `https://lite.duckduckgo.com/lite/?q=${encodeURIComponent(query)}`;
6
+ console.log(`Testing Lite DDG: ${url}`);
7
+
8
+ try {
9
+ const cmd = `curl -s -L -A "Mozilla/5.0" "${url}"`;
10
+ const html = execSync(cmd, { encoding: 'utf-8' });
11
+
12
+ console.log('HTML Length:', html.length);
13
+ console.log('Snippet (first 2000 chars):');
14
+ console.log(html.substring(0, 2000));
15
+
16
+ } catch (e: any) {
17
+ console.error('Error:', e.message);
18
+ }
package/src/index.ts CHANGED
@@ -116,6 +116,18 @@ async function main(): Promise<void> {
116
116
  await repl.start();
117
117
  }
118
118
 
119
+ // Global error handlers to prevent silent crashes
120
+ process.on('uncaughtException', (error) => {
121
+ console.error('Uncaught Exception:', error.message);
122
+ console.error(error.stack);
123
+ process.exit(1);
124
+ });
125
+
126
+ process.on('unhandledRejection', (reason, promise) => {
127
+ console.error('Unhandled Rejection at:', promise, 'reason:', reason);
128
+ process.exit(1);
129
+ });
130
+
119
131
  // Start the application
120
132
  main().catch((error) => {
121
133
  console.error('Fatal error:', error);
@@ -23,6 +23,7 @@ export class JsonRpcClient {
23
23
  private process: ChildProcess;
24
24
  private sequence = 0;
25
25
  private pendingRequests = new Map<number | string, { resolve: Function; reject: Function }>();
26
+ private readonly REQUEST_TIMEOUT = 30000; // 30 seconds timeout
26
27
 
27
28
  constructor(command: string, args: string[]) {
28
29
  this.process = spawn(command, args, {
@@ -50,6 +51,10 @@ export class JsonRpcClient {
50
51
  const handler = this.pendingRequests.get(message.id);
51
52
  if (handler) {
52
53
  this.pendingRequests.delete(message.id);
54
+ // Clear timeout if present
55
+ if ((handler as any).timeout) {
56
+ clearTimeout((handler as any).timeout);
57
+ }
53
58
  if (message.error) {
54
59
  handler.reject(new Error(message.error.message));
55
60
  } else {
@@ -76,12 +81,26 @@ export class JsonRpcClient {
76
81
 
77
82
  return new Promise((resolve, reject) => {
78
83
  this.pendingRequests.set(id, { resolve, reject });
84
+
85
+ // Set timeout to prevent indefinite hangs
86
+ const timeout = setTimeout(() => {
87
+ this.pendingRequests.delete(id);
88
+ reject(new Error(`MCP request timeout after ${this.REQUEST_TIMEOUT}ms`));
89
+ }, this.REQUEST_TIMEOUT);
90
+
91
+ // Store timeout with the request for cleanup
92
+ const handler = this.pendingRequests.get(id);
93
+ if (handler) {
94
+ (handler as any).timeout = timeout;
95
+ }
96
+
79
97
  try {
80
98
  if (!this.process.stdin) throw new Error('Stdin not available');
81
99
  const data = JSON.stringify(request) + '\n';
82
100
  this.process.stdin.write(data);
83
101
  } catch (e) {
84
102
  this.pendingRequests.delete(id);
103
+ clearTimeout(timeout);
85
104
  reject(e);
86
105
  }
87
106
  });
@@ -0,0 +1,153 @@
1
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
2
+ import { homedir } from 'os';
3
+ import { join } from 'path';
4
+
5
+ export interface McpServerConfig {
6
+ name: string;
7
+ command: string;
8
+ args: string[];
9
+ description?: string;
10
+ autoConnect?: boolean;
11
+ env?: Record<string, string>;
12
+ }
13
+
14
+ export interface McpConfig {
15
+ servers: McpServerConfig[];
16
+ }
17
+
18
+ export class McpConfigManager {
19
+ private configPath: string;
20
+ private config: McpConfig;
21
+
22
+ constructor() {
23
+ // Config path: ~/.mentis/mcp.json
24
+ this.configPath = join(homedir(), '.mentis', 'mcp.json');
25
+ this.config = this.loadConfig();
26
+ }
27
+
28
+ private loadConfig(): McpConfig {
29
+ if (existsSync(this.configPath)) {
30
+ try {
31
+ const content = readFileSync(this.configPath, 'utf-8');
32
+ return JSON.parse(content);
33
+ } catch (error) {
34
+ console.warn('Failed to load MCP config, using defaults:', error);
35
+ }
36
+ }
37
+
38
+ // Default configuration with popular MCP servers
39
+ return this.getDefaultConfig();
40
+ }
41
+
42
+ private getDefaultConfig(): McpConfig {
43
+ return {
44
+ servers: [
45
+ {
46
+ name: 'Exa Search',
47
+ command: 'npx',
48
+ args: ['-y', '@exa-labs/mcp-server-exa'],
49
+ description: 'Web search via Exa API (requires EXA_API_KEY)',
50
+ autoConnect: false,
51
+ env: {
52
+ EXA_API_KEY: process.env.EXA_API_KEY || ''
53
+ }
54
+ },
55
+ {
56
+ name: 'Memory',
57
+ command: 'npx',
58
+ args: ['-y', '@modelcontextprotocol/server-memory'],
59
+ description: 'Persistent memory storage for conversations',
60
+ autoConnect: false
61
+ },
62
+ {
63
+ name: 'Filesystem',
64
+ command: 'npx',
65
+ args: ['-y', '@modelcontextprotocol/server-filesystem', process.cwd()],
66
+ description: 'Enhanced filesystem operations',
67
+ autoConnect: false
68
+ },
69
+ {
70
+ name: 'GitHub',
71
+ command: 'npx',
72
+ args: ['-y', '@modelcontextprotocol/server-github'],
73
+ description: 'GitHub repository management and operations',
74
+ autoConnect: false,
75
+ env: {
76
+ GITHUB_PERSONAL_ACCESS_TOKEN: process.env.GITHUB_PERSONAL_ACCESS_TOKEN || ''
77
+ }
78
+ },
79
+ {
80
+ name: 'Puppeteer',
81
+ command: 'npx',
82
+ args: ['-y', '@modelcontextprotocol/server-puppeteer'],
83
+ description: 'Web browser automation and scraping',
84
+ autoConnect: false
85
+ },
86
+ {
87
+ name: 'Brave Search',
88
+ command: 'npx',
89
+ args: ['-y', '@modelcontextprotocol/server-brave-search'],
90
+ description: 'Web search via Brave Search API',
91
+ autoConnect: false,
92
+ env: {
93
+ BRAVE_API_KEY: process.env.BRAVE_API_KEY || ''
94
+ }
95
+ },
96
+ {
97
+ name: 'Slack',
98
+ command: 'npx',
99
+ args: ['-y', '@modelcontextprotocol/server-slack'],
100
+ description: 'Slack workspace integration',
101
+ autoConnect: false,
102
+ env: {
103
+ SLACK_BOT_TOKEN: process.env.SLACK_BOT_TOKEN || ''
104
+ }
105
+ }
106
+ ]
107
+ };
108
+ }
109
+
110
+ public getConfig(): McpConfig {
111
+ return this.config;
112
+ }
113
+
114
+ public saveConfig(): void {
115
+ try {
116
+ const dir = join(homedir(), '.mentis');
117
+ if (!existsSync(dir)) {
118
+ require('fs').mkdirSync(dir, { recursive: true });
119
+ }
120
+ writeFileSync(this.configPath, JSON.stringify(this.config, null, 2));
121
+ } catch (error) {
122
+ console.error('Failed to save MCP config:', error);
123
+ }
124
+ }
125
+
126
+ public addServer(server: McpServerConfig): void {
127
+ // Remove existing server with same name
128
+ this.config.servers = this.config.servers.filter(s => s.name !== server.name);
129
+ this.config.servers.push(server);
130
+ this.saveConfig();
131
+ }
132
+
133
+ public removeServer(name: string): void {
134
+ this.config.servers = this.config.servers.filter(s => s.name !== name);
135
+ this.saveConfig();
136
+ }
137
+
138
+ public getServer(name: string): McpServerConfig | undefined {
139
+ return this.config.servers.find(s => s.name === name);
140
+ }
141
+
142
+ public getAutoConnectServers(): McpServerConfig[] {
143
+ return this.config.servers.filter(s => s.autoConnect === true);
144
+ }
145
+
146
+ public updateServer(name: string, updates: Partial<McpServerConfig>): void {
147
+ const server = this.config.servers.find(s => s.name === name);
148
+ if (server) {
149
+ Object.assign(server, updates);
150
+ this.saveConfig();
151
+ }
152
+ }
153
+ }
@@ -0,0 +1,224 @@
1
+ import { McpClient } from './McpClient';
2
+ import { McpConfigManager, McpServerConfig } from './McpConfig';
3
+ import { Tool } from '../tools/Tool';
4
+ import ora from 'ora';
5
+ import chalk from 'chalk';
6
+
7
+ export interface McpConnection {
8
+ client: McpClient;
9
+ config: McpServerConfig;
10
+ tools: Tool[];
11
+ connectedAt: Date;
12
+ }
13
+
14
+ export class McpManager {
15
+ private configManager: McpConfigManager;
16
+ private connections: Map<string, McpConnection> = new Map();
17
+
18
+ constructor() {
19
+ this.configManager = new McpConfigManager();
20
+ }
21
+
22
+ public getConfig(): McpConfigManager {
23
+ return this.configManager;
24
+ }
25
+
26
+ public async connectToServer(serverName: string): Promise<McpConnection | null> {
27
+ const config = this.configManager.getServer(serverName);
28
+ if (!config) {
29
+ console.error(chalk.red(`MCP server "${serverName}" not found in configuration`));
30
+ return null;
31
+ }
32
+
33
+ // Check if already connected
34
+ if (this.connections.has(serverName)) {
35
+ console.log(chalk.yellow(`Already connected to ${serverName}`));
36
+ return this.connections.get(serverName)!;
37
+ }
38
+
39
+ const spinner = ora(`Connecting to MCP server: ${serverName}...`).start();
40
+
41
+ try {
42
+ // Set environment variables if specified
43
+ if (config.env) {
44
+ for (const [key, value] of Object.entries(config.env)) {
45
+ if (value) {
46
+ process.env[key] = value;
47
+ } else if (!process.env[key]) {
48
+ spinner.warn(chalk.yellow(`Environment variable ${key} is required for ${serverName}`));
49
+ console.log(chalk.dim(`Set it with: export ${key}=your_key`));
50
+ console.log(chalk.dim(`Or add it to your MCP configuration`));
51
+ }
52
+ }
53
+ }
54
+
55
+ const client = new McpClient(config.command, config.args);
56
+ await client.initialize();
57
+ const tools = await client.listTools();
58
+
59
+ const connection: McpConnection = {
60
+ client,
61
+ config,
62
+ tools,
63
+ connectedAt: new Date()
64
+ };
65
+
66
+ this.connections.set(serverName, connection);
67
+
68
+ spinner.succeed(chalk.green(`Connected to ${client.serverName}!`));
69
+
70
+ if (tools.length > 0) {
71
+ console.log(chalk.green(`Added ${tools.length} tools:`));
72
+ tools.forEach(t => {
73
+ const description = t.description.length > 60
74
+ ? t.description.substring(0, 60) + '...'
75
+ : t.description;
76
+ console.log(chalk.dim(` - ${chalk.cyan(t.name)}: ${description}`));
77
+ });
78
+ } else {
79
+ console.log(chalk.yellow('No tools available from this server'));
80
+ }
81
+
82
+ return connection;
83
+
84
+ } catch (error: any) {
85
+ spinner.fail(chalk.red(`Failed to connect to ${serverName}: ${error.message}`));
86
+ return null;
87
+ }
88
+ }
89
+
90
+ public async disconnectFromServer(serverName: string): Promise<boolean> {
91
+ const connection = this.connections.get(serverName);
92
+ if (!connection) {
93
+ console.log(chalk.yellow(`Not connected to ${serverName}`));
94
+ return false;
95
+ }
96
+
97
+ try {
98
+ connection.client.disconnect();
99
+ this.connections.delete(serverName);
100
+ console.log(chalk.green(`Disconnected from ${serverName}`));
101
+ return true;
102
+ } catch (error: any) {
103
+ console.error(chalk.red(`Error disconnecting from ${serverName}: ${error.message}`));
104
+ return false;
105
+ }
106
+ }
107
+
108
+ public disconnectAll(): void {
109
+ const serverNames = Array.from(this.connections.keys());
110
+ for (const name of serverNames) {
111
+ this.disconnectFromServer(name);
112
+ }
113
+ }
114
+
115
+ public getConnections(): McpConnection[] {
116
+ return Array.from(this.connections.values());
117
+ }
118
+
119
+ public getConnection(name: string): McpConnection | undefined {
120
+ return this.connections.get(name);
121
+ }
122
+
123
+ public getAllTools(): Tool[] {
124
+ const allTools: Tool[] = [];
125
+ for (const connection of this.connections.values()) {
126
+ allTools.push(...connection.tools);
127
+ }
128
+ return allTools;
129
+ }
130
+
131
+ public getServerNames(): string[] {
132
+ return Array.from(this.connections.keys());
133
+ }
134
+
135
+ public getAvailableServers(): McpServerConfig[] {
136
+ return this.configManager.getConfig().servers;
137
+ }
138
+
139
+ public async autoConnect(): Promise<void> {
140
+ const autoConnectServers = this.configManager.getAutoConnectServers();
141
+
142
+ if (autoConnectServers.length === 0) {
143
+ return;
144
+ }
145
+
146
+ console.log(chalk.blue(`\nAuto-connecting to ${autoConnectServers.length} MCP servers...`));
147
+
148
+ for (const config of autoConnectServers) {
149
+ await this.connectToServer(config.name);
150
+ }
151
+ }
152
+
153
+ public async listServers(): Promise<void> {
154
+ const availableServers = this.configManager.getConfig().servers;
155
+ const connectedServers = this.getServerNames();
156
+
157
+ if (availableServers.length === 0) {
158
+ console.log(chalk.yellow('No MCP servers configured.'));
159
+ return;
160
+ }
161
+
162
+ console.log(chalk.cyan('\nMCP Servers:\n'));
163
+
164
+ for (const server of availableServers) {
165
+ const isConnected = connectedServers.includes(server.name);
166
+ const status = isConnected ? chalk.green('● Connected') : chalk.gray('○ Disconnected');
167
+ const auto = server.autoConnect ? chalk.dim('[auto]') : '';
168
+
169
+ console.log(`${status} ${chalk.bold(server.name)} ${auto}`);
170
+ if (server.description) {
171
+ console.log(chalk.dim(` ${server.description}`));
172
+ }
173
+
174
+ if (isConnected) {
175
+ const connection = this.getConnection(server.name);
176
+ if (connection && connection.tools.length > 0) {
177
+ console.log(chalk.dim(` Tools: ${connection.tools.map(t => t.name).join(', ')}`));
178
+ }
179
+ }
180
+ console.log('');
181
+ }
182
+ }
183
+
184
+ public async addServer(name: string, command: string, args: string[], description?: string): Promise<void> {
185
+ const serverConfig: McpServerConfig = {
186
+ name,
187
+ command,
188
+ args,
189
+ description,
190
+ autoConnect: false
191
+ };
192
+
193
+ this.configManager.addServer(serverConfig);
194
+ console.log(chalk.green(`Added MCP server "${name}" to configuration`));
195
+ }
196
+
197
+ public async removeServer(name: string): Promise<void> {
198
+ // Disconnect if connected
199
+ if (this.connections.has(name)) {
200
+ await this.disconnectFromServer(name);
201
+ }
202
+
203
+ this.configManager.removeServer(name);
204
+ console.log(chalk.green(`Removed MCP server "${name}" from configuration`));
205
+ }
206
+
207
+ public async testConnection(serverName: string): Promise<boolean> {
208
+ const connection = this.getConnection(serverName);
209
+ if (!connection) {
210
+ console.log(chalk.red(`Not connected to ${serverName}`));
211
+ return false;
212
+ }
213
+
214
+ try {
215
+ // Try to list tools as a test
216
+ await connection.client.listTools();
217
+ console.log(chalk.green(`${serverName} connection is healthy`));
218
+ return true;
219
+ } catch (error: any) {
220
+ console.log(chalk.red(`${serverName} connection test failed: ${error.message}`));
221
+ return false;
222
+ }
223
+ }
224
+ }
@@ -7,6 +7,7 @@ export class PersistentShell {
7
7
  private delimiter: string = 'MENTIS_SHELL_DELIMITER';
8
8
  private resolveCallback: ((output: string) => void) | null = null;
9
9
  private rejectCallback: ((error: Error) => void) | null = null;
10
+ private readonly COMMAND_TIMEOUT = 120000; // 2 minutes timeout for shell commands
10
11
 
11
12
  constructor() {
12
13
  // Lazy init: Do not spawn here.
@@ -68,7 +69,29 @@ export class PersistentShell {
68
69
  this.rejectCallback = reject;
69
70
  this.buffer = '';
70
71
 
71
- const cleanCommand = command.replace(/\n/g, '; ');
72
+ // Set timeout to prevent indefinite hangs
73
+ const timeout = setTimeout(() => {
74
+ this.resolveCallback = null;
75
+ this.rejectCallback = null;
76
+ reject(new Error(`Shell command timeout after ${this.COMMAND_TIMEOUT}ms`));
77
+ }, this.COMMAND_TIMEOUT);
78
+
79
+ // Store timeout for cleanup
80
+ const originalResolve = resolve;
81
+ const wrappedResolve = (output: string) => {
82
+ clearTimeout(timeout);
83
+ originalResolve(output);
84
+ };
85
+
86
+ this.resolveCallback = wrappedResolve;
87
+
88
+ let cleanCommand = command.replace(/\n/g, '; ');
89
+
90
+ // Fix: PowerShell alias 'curl' -> 'Invoke-WebRequest' causes issues for linux-style curl commands.
91
+ // Force usage of 'curl.exe' if on Windows.
92
+ if (os.platform() === 'win32') {
93
+ cleanCommand = cleanCommand.replace(/(^|[\s|;&])curl(\s|$)/g, '$1curl.exe$2');
94
+ }
72
95
 
73
96
  const fullCommand = `${cleanCommand}; echo "${this.delimiter}"\n`;
74
97