@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,197 @@
1
+ import inquirer from 'inquirer';
2
+ import chalk from 'chalk';
3
+ import { Tool } from './Tool';
4
+
5
+ /**
6
+ * Question types supported by AskQuestionTool
7
+ */
8
+ export type QuestionType = 'text' | 'confirm' | 'list' | 'checkbox';
9
+
10
+ /**
11
+ * Question definition for the AI to create
12
+ */
13
+ interface QuestionDef {
14
+ type: QuestionType;
15
+ question: string;
16
+ options?: Array<{ label: string; description?: string }>;
17
+ default?: any;
18
+ }
19
+
20
+ /**
21
+ * AskQuestionTool - Allows the AI to ask clarifying questions
22
+ * This enables interactive plan mode where AI can gather requirements
23
+ */
24
+ export class AskQuestionTool implements Tool {
25
+ name = 'ask_question';
26
+ description = 'Ask the user a clarifying question. Use this in plan mode to gather requirements before implementation. Supports: text, confirm (yes/no), list (single choice), checkbox (multi-select).';
27
+ parameters = {
28
+ type: 'object',
29
+ properties: {
30
+ question: {
31
+ type: 'string',
32
+ description: 'The question to ask the user'
33
+ },
34
+ type: {
35
+ type: 'string',
36
+ enum: ['text', 'confirm', 'list', 'checkbox'],
37
+ description: 'Type of question: text (free input), confirm (yes/no), list (single choice), checkbox (multi-select)'
38
+ },
39
+ options: {
40
+ type: 'array',
41
+ items: {
42
+ type: 'object',
43
+ properties: {
44
+ label: { type: 'string', description: 'Display text for the option' },
45
+ description: { type: 'string', description: 'Additional context (optional)' }
46
+ }
47
+ },
48
+ description: 'Options for list/checkbox questions. Required for list/checkbox types.'
49
+ },
50
+ default: {
51
+ oneOf: [{ type: 'string' }, { type: 'boolean' }, { type: 'array' }],
52
+ description: 'Default value (optional)'
53
+ }
54
+ },
55
+ required: ['question', 'type']
56
+ };
57
+
58
+ /**
59
+ * Execute the question and return the user's answer
60
+ */
61
+ async execute(args: QuestionDef & { question: string }): Promise<string> {
62
+ const questionType = args.type || 'text';
63
+
64
+ // Show question header
65
+ console.log('');
66
+ console.log(chalk.cyan('🤔 Question from AI:'));
67
+ console.log(chalk.gray('─'.repeat(60)));
68
+
69
+ try {
70
+ let result: any;
71
+
72
+ switch (questionType) {
73
+ case 'text':
74
+ result = await this.askText(args.question, args.default);
75
+ break;
76
+
77
+ case 'confirm':
78
+ result = await this.askConfirm(args.question, args.default);
79
+ break;
80
+
81
+ case 'list':
82
+ if (!args.options || args.options.length === 0) {
83
+ return 'Error: list questions require options';
84
+ }
85
+ result = await this.askList(args.question, args.options, args.default);
86
+ break;
87
+
88
+ case 'checkbox':
89
+ if (!args.options || args.options.length === 0) {
90
+ return 'Error: checkbox questions require options';
91
+ }
92
+ result = await this.askCheckbox(args.question, args.options, args.default);
93
+ break;
94
+
95
+ default:
96
+ return `Error: Unknown question type: ${questionType}`;
97
+ }
98
+
99
+ console.log(chalk.gray('─'.repeat(60)));
100
+
101
+ // Format result as string for return to LLM
102
+ return this.formatResult(result, questionType);
103
+
104
+ } catch (error: any) {
105
+ return `Error asking question: ${error.message}`;
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Ask a free-text question
111
+ */
112
+ private async askText(question: string, defaultValue?: string): Promise<string> {
113
+ const { answer } = await inquirer.prompt([
114
+ {
115
+ type: 'input',
116
+ name: 'answer',
117
+ message: question,
118
+ default: defaultValue
119
+ }
120
+ ]);
121
+ return answer;
122
+ }
123
+
124
+ /**
125
+ * Ask a yes/no confirmation
126
+ */
127
+ private async askConfirm(question: string, defaultValue?: boolean): Promise<boolean> {
128
+ const { answer } = await inquirer.prompt([
129
+ {
130
+ type: 'confirm',
131
+ name: 'answer',
132
+ message: question,
133
+ default: defaultValue ?? true
134
+ }
135
+ ]);
136
+ return answer;
137
+ }
138
+
139
+ /**
140
+ * Ask a single-choice list question
141
+ */
142
+ private async askList(question: string, options: Array<{ label: string; description?: string }>, defaultValue?: string): Promise<string> {
143
+ const { answer } = await inquirer.prompt([
144
+ {
145
+ type: 'list',
146
+ name: 'answer',
147
+ message: question,
148
+ choices: options.map((opt, i) => ({
149
+ name: opt.label,
150
+ value: opt.label,
151
+ short: opt.label
152
+ })),
153
+ default: defaultValue
154
+ }
155
+ ]);
156
+ return answer;
157
+ }
158
+
159
+ /**
160
+ * Ask a multi-select checkbox question
161
+ */
162
+ private async askCheckbox(question: string, options: Array<{ label: string; description?: string }>, defaultValue?: string[]): Promise<string[]> {
163
+ const { answer } = await inquirer.prompt([
164
+ {
165
+ type: 'checkbox',
166
+ name: 'answer',
167
+ message: question,
168
+ choices: options.map((opt, i) => ({
169
+ name: opt.label,
170
+ value: opt.label,
171
+ checked: defaultValue?.includes(opt.label),
172
+ short: opt.label
173
+ }))
174
+ }
175
+ ]);
176
+ return answer;
177
+ }
178
+
179
+ /**
180
+ * Format the result for return to the LLM
181
+ */
182
+ private formatResult(result: any, questionType: QuestionType): string {
183
+ switch (questionType) {
184
+ case 'confirm':
185
+ return result === true ? 'Yes' : 'No';
186
+
187
+ case 'checkbox':
188
+ if (Array.isArray(result) && result.length > 0) {
189
+ return `Selected: ${result.join(', ')}`;
190
+ }
191
+ return 'None selected';
192
+
193
+ default:
194
+ return String(result);
195
+ }
196
+ }
197
+ }
@@ -0,0 +1,172 @@
1
+ import { Tool } from './Tool';
2
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
3
+ import { resolve, dirname } from 'path';
4
+ import { fileURLToPath } from 'url';
5
+
6
+ /**
7
+ * EditFileTool - Performs string replacement in files (like Claude's Edit tool)
8
+ * Returns a unified diff preview instead of writing immediately
9
+ */
10
+ export class EditFileTool implements Tool {
11
+ name = 'edit_file';
12
+ description = 'Make targeted edits to files using string replacement. Returns diff preview. Requires approval before writing.';
13
+ parameters = {
14
+ type: 'object',
15
+ properties: {
16
+ file_path: {
17
+ type: 'string',
18
+ description: 'The path to the file to edit'
19
+ },
20
+ old_string: {
21
+ type: 'string',
22
+ description: 'The exact string to replace. Must match exactly (including whitespace).'
23
+ },
24
+ new_string: {
25
+ type: 'string',
26
+ description: 'The new string to replace old_string with.'
27
+ },
28
+ auto_format: {
29
+ type: 'boolean',
30
+ description: 'Auto-format code after edit (default: false)'
31
+ }
32
+ },
33
+ required: ['file_path', 'old_string', 'new_string']
34
+ };
35
+
36
+ /**
37
+ * Execute the edit and return a diff preview
38
+ * Note: This does NOT write the file - it returns what WOULD change
39
+ * The caller (ReplManager) should handle approval before calling applyEdit()
40
+ */
41
+ async execute(args: {
42
+ file_path: string;
43
+ old_string: string;
44
+ new_string: string;
45
+ auto_format?: boolean;
46
+ }): Promise<string> {
47
+ const filePath = resolve(process.cwd(), args.file_path);
48
+
49
+ if (!existsSync(filePath)) {
50
+ return `Error: File not found: ${args.file_path}`;
51
+ }
52
+
53
+ const originalContent = readFileSync(filePath, 'utf-8');
54
+
55
+ if (!originalContent.includes(args.old_string)) {
56
+ return `Error: old_string not found in file. The string must match exactly (including whitespace and indentation).`;
57
+ }
58
+
59
+ // Count occurrences
60
+ const occurrences = (originalContent.match(new RegExp(this.escapeRegex(args.old_string), 'g')) || []).length;
61
+
62
+ if (occurrences > 1) {
63
+ return `Warning: old_string found ${occurrences} times. All occurrences will be replaced.\n\n${this.generateDiff(originalContent, args.old_string, args.new_string, args.file_path)}`;
64
+ }
65
+
66
+ // Generate and return diff
67
+ return this.generateDiff(originalContent, args.old_string, args.new_string, args.file_path);
68
+ }
69
+
70
+ /**
71
+ * Apply the edit after approval
72
+ * This should be called after user approves the diff
73
+ */
74
+ applyEdit(args: {
75
+ file_path: string;
76
+ old_string: string;
77
+ new_string: string;
78
+ }): { success: boolean; message: string } {
79
+ const filePath = resolve(process.cwd(), args.file_path);
80
+
81
+ if (!existsSync(filePath)) {
82
+ return { success: false, message: `File not found: ${args.file_path}` };
83
+ }
84
+
85
+ const originalContent = readFileSync(filePath, 'utf-8');
86
+
87
+ if (!originalContent.includes(args.old_string)) {
88
+ return { success: false, message: 'old_string not found in file' };
89
+ }
90
+
91
+ const newContent = originalContent.replace(args.old_string, args.new_string);
92
+
93
+ writeFileSync(filePath, newContent, 'utf-8');
94
+
95
+ return {
96
+ success: true,
97
+ message: `Successfully edited ${args.file_path}`
98
+ };
99
+ }
100
+
101
+ /**
102
+ * Generate a unified diff preview
103
+ */
104
+ private generateDiff(content: string, oldString: string, newString: string, filePath: string): string {
105
+ const lines = content.split('\n');
106
+ const oldLines = oldString.split('\n');
107
+ const newLines = newString.split('\n');
108
+
109
+ // Find the line number where old_string starts
110
+ let startLine = -1;
111
+ for (let i = 0; i <= lines.length - oldLines.length; i++) {
112
+ let match = true;
113
+ for (let j = 0; j < oldLines.length; j++) {
114
+ if (lines[i + j] !== oldLines[j]) {
115
+ match = false;
116
+ break;
117
+ }
118
+ }
119
+ if (match) {
120
+ startLine = i;
121
+ break;
122
+ }
123
+ }
124
+
125
+ if (startLine === -1) {
126
+ return 'Error: Could not locate old_string in file';
127
+ }
128
+
129
+ // Build unified diff
130
+ let diff = `\n${'─'.repeat(60)}\n`;
131
+ diff += `📝 Edit Preview: ${filePath}\n`;
132
+ diff += `${'─'.repeat(60)}\n`;
133
+ diff += `Line ${startLine + 1}:\n\n`;
134
+
135
+ // Show context (2 lines before)
136
+ const contextStart = Math.max(0, startLine - 2);
137
+ if (contextStart < startLine) {
138
+ for (let i = contextStart; i < startLine; i++) {
139
+ diff += ` ${lines[i]}\n`;
140
+ }
141
+ }
142
+
143
+ // Show removed lines (red)
144
+ for (const line of oldLines) {
145
+ diff += `\x1b[31m- ${line}\x1b[0m\n`;
146
+ }
147
+
148
+ // Show added lines (green)
149
+ for (const line of newLines) {
150
+ diff += `\x1b[32m+ ${line}\x1b[0m\n`;
151
+ }
152
+
153
+ // Show context (2 lines after)
154
+ const contextEnd = Math.min(lines.length, startLine + oldLines.length + 2);
155
+ if (startLine + oldLines.length < contextEnd) {
156
+ for (let i = startLine + oldLines.length; i < contextEnd; i++) {
157
+ diff += ` ${lines[i]}\n`;
158
+ }
159
+ }
160
+
161
+ diff += `${'─'.repeat(60)}\n`;
162
+
163
+ return diff;
164
+ }
165
+
166
+ /**
167
+ * Escape special regex characters
168
+ */
169
+ private escapeRegex(str: string): string {
170
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
171
+ }
172
+ }
@@ -1,6 +1,9 @@
1
1
  import fs from 'fs-extra';
2
2
  import path from 'path';
3
3
  import { Tool } from './Tool';
4
+ export { EditFileTool } from './EditFileTool';
5
+ export { AskQuestionTool } from './AskQuestionTool';
6
+ export { PlanModeTool } from './PlanModeTool';
4
7
 
5
8
  export class WriteFileTool implements Tool {
6
9
  name = 'write_file';
@@ -0,0 +1,50 @@
1
+ import inquirer from 'inquirer';
2
+ import chalk from 'chalk';
3
+ import { Tool } from './Tool';
4
+ import { PlanModeUI } from '../ui/PlanModeUI';
5
+
6
+ /**
7
+ * PlanModeTool - Allows AI to suggest switching to plan mode
8
+ * Use this when the task is complex, requires architecture design, or needs requirements gathering
9
+ */
10
+ export class PlanModeTool implements Tool {
11
+ name = 'enter_plan_mode';
12
+ description = 'Suggest switching to plan mode for complex tasks. Call this when you need to gather requirements, design architecture, or break down a complex implementation before coding.';
13
+ parameters = {
14
+ type: 'object',
15
+ properties: {
16
+ reason: {
17
+ type: 'string',
18
+ description: 'Explain why plan mode is recommended for this task'
19
+ }
20
+ },
21
+ required: ['reason']
22
+ };
23
+
24
+ /**
25
+ * Execute - Ask user if they want to switch to plan mode
26
+ */
27
+ async execute(args: { reason: string }): Promise<string> {
28
+ console.log('');
29
+ console.log(chalk.cyan('🎯 AI suggests entering PLAN MODE'));
30
+ console.log(chalk.gray('─'.repeat(60)));
31
+ console.log(chalk.dim(`Reason: ${args.reason}`));
32
+ console.log(chalk.gray('─'.repeat(60)));
33
+
34
+ const { confirm } = await inquirer.prompt([
35
+ {
36
+ type: 'confirm',
37
+ name: 'confirm',
38
+ message: 'Switch to plan mode?',
39
+ default: true
40
+ }
41
+ ]);
42
+
43
+ if (confirm) {
44
+ PlanModeUI.showPlanHeader();
45
+ return 'User approved. Switching to plan mode for requirements gathering and planning.';
46
+ }
47
+
48
+ return 'User declined. Continuing in current mode.';
49
+ }
50
+ }