@indiccoder/mentis-cli 1.1.3 → 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 (82) 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/ARCHITECTURE.md +267 -0
  14. package/CONTRIBUTING.md +209 -0
  15. package/README.md +17 -0
  16. package/dist/checkpoint/CheckpointManager.js +92 -0
  17. package/dist/commands/Command.js +15 -1
  18. package/dist/commands/CommandManager.js +30 -5
  19. package/dist/commands/__tests__/CommandManager.test.js +70 -0
  20. package/dist/debug_google.js +61 -0
  21. package/dist/debug_lite.js +49 -0
  22. package/dist/debug_lite_headers.js +57 -0
  23. package/dist/debug_search.js +16 -0
  24. package/dist/index.js +33 -0
  25. package/dist/mcp/JsonRpcClient.js +16 -0
  26. package/dist/mcp/McpConfig.js +132 -0
  27. package/dist/mcp/McpManager.js +189 -0
  28. package/dist/repl/PersistentShell.js +20 -1
  29. package/dist/repl/ReplManager.js +410 -138
  30. package/dist/skills/Skill.js +17 -2
  31. package/dist/skills/SkillsManager.js +28 -3
  32. package/dist/skills/__tests__/SkillsManager.test.js +62 -0
  33. package/dist/tools/AskQuestionTool.js +172 -0
  34. package/dist/tools/EditFileTool.js +141 -0
  35. package/dist/tools/FileTools.js +7 -1
  36. package/dist/tools/PlanModeTool.js +53 -0
  37. package/dist/tools/WebSearchTool.js +190 -27
  38. package/dist/ui/DiffViewer.js +110 -0
  39. package/dist/ui/InputBox.js +16 -2
  40. package/dist/ui/MultiFileSelector.js +123 -0
  41. package/dist/ui/PlanModeUI.js +105 -0
  42. package/dist/ui/ToolExecutor.js +154 -0
  43. package/dist/ui/UIManager.js +12 -2
  44. package/dist/utils/__mocks__/chalk.js +20 -0
  45. package/dist/utils/__tests__/ContextVisualizer.test.js +95 -0
  46. package/docs/MCP_INTEGRATION.md +290 -0
  47. package/google_dump.html +18 -0
  48. package/lite_dump.html +176 -0
  49. package/lite_headers_dump.html +176 -0
  50. package/package.json +34 -2
  51. package/scripts/test_exa_mcp.ts +90 -0
  52. package/src/checkpoint/CheckpointManager.ts +102 -0
  53. package/src/commands/Command.ts +64 -13
  54. package/src/commands/CommandManager.ts +26 -5
  55. package/src/commands/__tests__/CommandManager.test.ts +83 -0
  56. package/src/debug_google.ts +30 -0
  57. package/src/debug_lite.ts +18 -0
  58. package/src/debug_lite_headers.ts +25 -0
  59. package/src/debug_search.ts +18 -0
  60. package/src/index.ts +45 -1
  61. package/src/mcp/JsonRpcClient.ts +19 -0
  62. package/src/mcp/McpConfig.ts +153 -0
  63. package/src/mcp/McpManager.ts +224 -0
  64. package/src/repl/PersistentShell.ts +24 -1
  65. package/src/repl/ReplManager.ts +1521 -1204
  66. package/src/skills/Skill.ts +91 -11
  67. package/src/skills/SkillsManager.ts +25 -3
  68. package/src/skills/__tests__/SkillsManager.test.ts +73 -0
  69. package/src/tools/AskQuestionTool.ts +197 -0
  70. package/src/tools/EditFileTool.ts +172 -0
  71. package/src/tools/FileTools.ts +3 -0
  72. package/src/tools/PlanModeTool.ts +50 -0
  73. package/src/tools/WebSearchTool.ts +235 -63
  74. package/src/ui/DiffViewer.ts +117 -0
  75. package/src/ui/InputBox.ts +17 -2
  76. package/src/ui/MultiFileSelector.ts +135 -0
  77. package/src/ui/PlanModeUI.ts +121 -0
  78. package/src/ui/ToolExecutor.ts +182 -0
  79. package/src/ui/UIManager.ts +15 -2
  80. package/src/utils/__mocks__/chalk.ts +19 -0
  81. package/src/utils/__tests__/ContextVisualizer.test.ts +118 -0
  82. package/console.log(tick) +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indiccoder/mentis-cli",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -12,7 +12,9 @@
12
12
  "scripts": {
13
13
  "build": "tsc",
14
14
  "start": "node dist/index.js",
15
- "test": "echo \"Error: no test specified\" && exit 1"
15
+ "test": "jest",
16
+ "test:watch": "jest --watch",
17
+ "test:coverage": "jest --coverage"
16
18
  },
17
19
  "keywords": [
18
20
  "cli",
@@ -36,6 +38,7 @@
36
38
  "cli-cursor": "^5.0.0",
37
39
  "cli-highlight": "^2.1.11",
38
40
  "commander": "^14.0.2",
41
+ "diff": "^8.0.3",
39
42
  "dotenv": "^17.2.3",
40
43
  "duck-duck-scrape": "^2.2.7",
41
44
  "fast-glob": "^3.3.3",
@@ -60,7 +63,36 @@
60
63
  "@types/fs-extra": "^11.0.4",
61
64
  "@types/gradient-string": "^1.1.6",
62
65
  "@types/inquirer": "^9.0.9",
66
+ "@types/jest": "^30.0.0",
63
67
  "@types/node": "^25.0.2",
68
+ "jest": "^30.2.0",
69
+ "ts-jest": "^29.4.6",
64
70
  "typescript": "^5.9.3"
71
+ },
72
+ "jest": {
73
+ "preset": "ts-jest",
74
+ "testEnvironment": "node",
75
+ "roots": [
76
+ "<rootDir>/src"
77
+ ],
78
+ "testMatch": [
79
+ "**/__tests__/**/*.ts",
80
+ "**/?(*.)+(spec|test).ts"
81
+ ],
82
+ "collectCoverageFrom": [
83
+ "src/**/*.ts",
84
+ "!src/**/*.d.ts"
85
+ ],
86
+ "transformIgnorePatterns": [
87
+ "node_modules/(?!(chalk)/)"
88
+ ],
89
+ "coverageThreshold": {
90
+ "global": {
91
+ "branches": 20,
92
+ "functions": 30,
93
+ "lines": 30,
94
+ "statements": 30
95
+ }
96
+ }
65
97
  }
66
98
  }
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Test script for Exa MCP server integration
5
+ * This script demonstrates how to use the Exa MCP server for web search
6
+ */
7
+
8
+ import { McpManager } from '../src/mcp/McpManager';
9
+ import chalk from 'chalk';
10
+
11
+ async function testExaMcp() {
12
+ console.log(chalk.cyan.bold('Testing Exa MCP Server Integration\n'));
13
+
14
+ const mcpManager = new McpManager();
15
+
16
+ try {
17
+ // Check if Exa API key is available
18
+ if (!process.env.EXA_API_KEY) {
19
+ console.log(chalk.yellow('⚠️ EXA_API_KEY environment variable not set.'));
20
+ console.log(chalk.dim('Set it with: export EXA_API_KEY=your_api_key'));
21
+ console.log(chalk.dim('Or the MCP server will prompt you for it.\n'));
22
+ }
23
+
24
+ // Connect to Exa Search server
25
+ console.log(chalk.blue('Connecting to Exa Search MCP server...'));
26
+ const connection = await mcpManager.connectToServer('Exa Search');
27
+
28
+ if (!connection) {
29
+ console.log(chalk.red('❌ Failed to connect to Exa Search server'));
30
+ return;
31
+ }
32
+
33
+ console.log(chalk.green('✅ Successfully connected to Exa Search!'));
34
+
35
+ // List available tools
36
+ const tools = connection.tools;
37
+ console.log(chalk.blue(`\n📦 Available tools (${tools.length}):`));
38
+
39
+ for (const tool of tools) {
40
+ console.log(chalk.dim(` • ${chalk.cyan(tool.name)}: ${tool.description}`));
41
+ }
42
+
43
+ // Test a search if search tool is available
44
+ const searchTool = tools.find(t => t.name.toLowerCase().includes('search'));
45
+ if (searchTool) {
46
+ console.log(chalk.blue(`\n🔍 Testing ${searchTool.name}...`));
47
+
48
+ try {
49
+ const result = await searchTool.execute({
50
+ query: 'Mentis CLI MCP integration',
51
+ count: 3
52
+ });
53
+
54
+ console.log(chalk.green('✅ Search executed successfully!'));
55
+ console.log(chalk.dim('\nSearch results preview:'));
56
+ console.log(result.substring(0, 500) + (result.length > 500 ? '...' : ''));
57
+ } catch (error: any) {
58
+ console.log(chalk.red(`❌ Search failed: ${error.message}`));
59
+ }
60
+ }
61
+
62
+ // List all configured servers
63
+ console.log(chalk.blue('\n📋 All configured MCP servers:'));
64
+ await mcpManager.listServers();
65
+
66
+ // Test connection health
67
+ console.log(chalk.blue('\n🏥 Testing connection health...'));
68
+ const isHealthy = await mcpManager.testConnection('Exa Search');
69
+ console.log(isHealthy ?
70
+ chalk.green('✅ Connection is healthy') :
71
+ chalk.red('❌ Connection test failed')
72
+ );
73
+
74
+ // Disconnect
75
+ console.log(chalk.blue('\n🔌 Disconnecting...'));
76
+ await mcpManager.disconnectFromServer('Exa Search');
77
+ console.log(chalk.green('✅ Disconnected successfully'));
78
+
79
+ } catch (error: any) {
80
+ console.error(chalk.red(`❌ Error: ${error.message}`));
81
+ process.exit(1);
82
+ }
83
+ }
84
+
85
+ // Run the test
86
+ if (require.main === module) {
87
+ testExaMcp().catch(console.error);
88
+ }
89
+
90
+ export { testExaMcp };
@@ -58,4 +58,106 @@ export class CheckpointManager {
58
58
  const filePath = path.join(this.checkpointDir, `${name}.json`);
59
59
  return fs.existsSync(filePath);
60
60
  }
61
+
62
+ // ─── Per-Directory Local Session Methods ───────────────────────────────
63
+
64
+ private getLocalSessionsDir(cwd: string): string {
65
+ return path.join(cwd, '.mentis', 'sessions');
66
+ }
67
+
68
+ public saveLocalSession(cwd: string, history: ChatMessage[], files: string[]): string {
69
+ const timestamp = Date.now();
70
+ const checkpoint: Checkpoint = {
71
+ timestamp,
72
+ name: `session-${timestamp}`,
73
+ history,
74
+ files
75
+ };
76
+ const sessionsDir = this.getLocalSessionsDir(cwd);
77
+ fs.ensureDirSync(sessionsDir);
78
+ const filePath = path.join(sessionsDir, `${timestamp}.json`);
79
+ fs.writeJsonSync(filePath, checkpoint, { spaces: 2 });
80
+ return filePath;
81
+ }
82
+
83
+ public loadLocalSession(cwd: string, sessionId?: string): Checkpoint | null {
84
+ const sessionsDir = this.getLocalSessionsDir(cwd);
85
+ if (!fs.existsSync(sessionsDir)) return null;
86
+
87
+ let filePath: string;
88
+ if (sessionId) {
89
+ filePath = path.join(sessionsDir, `${sessionId}.json`);
90
+ } else {
91
+ // Load most recent
92
+ const sessions = this.listLocalSessions(cwd);
93
+ if (sessions.length === 0) return null;
94
+ filePath = path.join(sessionsDir, `${sessions[0].id}.json`);
95
+ }
96
+
97
+ if (fs.existsSync(filePath)) {
98
+ return fs.readJsonSync(filePath) as Checkpoint;
99
+ }
100
+ return null;
101
+ }
102
+
103
+ public listLocalSessions(cwd: string): Array<{ id: string; timestamp: number; messageCount: number; preview: string }> {
104
+ const sessionsDir = this.getLocalSessionsDir(cwd);
105
+ if (!fs.existsSync(sessionsDir)) return [];
106
+
107
+ const files = fs.readdirSync(sessionsDir);
108
+
109
+ return files
110
+ .filter(f => f.endsWith('.json'))
111
+ .map(f => {
112
+ const filePath = path.join(sessionsDir, f);
113
+ try {
114
+ const data = fs.readJsonSync(filePath) as Checkpoint;
115
+ // Extract meaningful preview from LAST user message (to show current state)
116
+ const lastUserMsg = [...(data.history || [])].reverse().find(m => m.role === 'user');
117
+ let preview = 'No preview';
118
+ if (lastUserMsg?.content) {
119
+ let clean = lastUserMsg.content;
120
+
121
+ // 1. Remove Repository Structure (explicitly)
122
+ clean = clean.replace(/Repository Structure:[\s\S]*?(?=\n\n|$)/g, '');
123
+
124
+ // 2. Remove "User Question:" label (if context insertion logic used it)
125
+ clean = clean.replace(/User Question:\s*/g, '');
126
+
127
+ // 3. Remove "Available Custom Commands" block
128
+ clean = clean.replace(/Available Custom Commands:[\s\S]*?(?=\n\n|$)/g, '');
129
+
130
+ // 4. Remove "Available Skills" block (if present)
131
+ clean = clean.replace(/Available Skills \([\s\S]*?(?=\n\n|$)/g, '');
132
+ clean = clean.replace(/Available Skills:[\s\S]*?(?=\n\n|$)/g, '');
133
+
134
+ // 5. Remove [SYSTEM: ...] instructions at the end
135
+ clean = clean.replace(/\n\[SYSTEM:[\s\S]*?\]$/g, '');
136
+
137
+ // 6. Clean up code blocks references/markdown if they were just context
138
+ const text = clean.trim();
139
+ if (text) {
140
+ // Take the first 50 chars of what's left
141
+ preview = text.substring(0, 50).replace(/\n/g, ' ');
142
+ } else {
143
+ preview = 'New Session';
144
+ }
145
+ }
146
+ return {
147
+ id: f.replace('.json', ''),
148
+ timestamp: data.timestamp,
149
+ messageCount: data.history?.length || 0,
150
+ preview: preview.length >= 50 ? preview + '...' : preview
151
+ };
152
+ } catch {
153
+ return null;
154
+ }
155
+ })
156
+ .filter((s): s is { id: string; timestamp: number; messageCount: number; preview: string } => s !== null)
157
+ .sort((a, b) => b.timestamp - a.timestamp); // Most recent first
158
+ }
159
+
160
+ public localSessionExists(cwd: string): boolean {
161
+ return this.listLocalSessions(cwd).length > 0;
162
+ }
61
163
  }
@@ -1,40 +1,91 @@
1
1
  /**
2
2
  * Custom Slash Commands System
3
- * Users can define their own slash commands as markdown files
3
+ *
4
+ * Users can define their own slash commands as markdown files with YAML frontmatter.
5
+ * Commands support parameter substitution, bash execution, and file references.
6
+ *
7
+ * @packageDocumentation
8
+ *
9
+ * @example
10
+ * ```markdown
11
+ * ---
12
+ * description: Run tests and show coverage
13
+ * argument-hint: [test-pattern]
14
+ * ---
15
+ *
16
+ * Run tests with !`npm test $1`
17
+ * ```
4
18
  */
5
19
 
20
+ /**
21
+ * YAML frontmatter options for custom commands
22
+ */
6
23
  export interface CommandFrontmatter {
24
+ /** Human-readable description of what the command does */
7
25
  description?: string;
26
+ /** Restrict which tools the AI can use when executing this command */
8
27
  'allowed-tools'?: string[];
28
+ /** Hint shown to user about expected arguments (e.g., "[pattern]") */
9
29
  'argument-hint'?: string;
30
+ /** Specific model to use for this command */
10
31
  model?: string;
32
+ /** Disable AI model invocation (execute bash/reads only) */
11
33
  'disable-model-invocation'?: boolean;
12
34
  }
13
35
 
36
+ /**
37
+ * A custom slash command
38
+ */
14
39
  export interface Command {
15
- name: string; // Command name (from filename)
16
- type: 'personal' | 'project'; // Personal or project command
17
- path: string; // Path to command file
18
- directory: string; // Directory containing the command
40
+ /** Command name (derived from filename, without .md extension) */
41
+ name: string;
42
+ /** Whether this is a personal or project-level command */
43
+ type: 'personal' | 'project';
44
+ /** Absolute path to the command's .md file */
45
+ path: string;
46
+ /** Directory containing the command file */
47
+ directory: string;
48
+ /** Parsed YAML frontmatter from the command file */
19
49
  frontmatter: CommandFrontmatter;
20
- content: string; // Command content (markdown)
21
- description: string; // Command description
22
- hasParameters: boolean; // Whether command uses parameters
50
+ /** Raw markdown content of the command */
51
+ content: string;
52
+ /** Human-readable description for display */
53
+ description: string;
54
+ /** Whether the command uses $1, $2, or $ARGUMENTS placeholders */
55
+ hasParameters: boolean;
23
56
  }
24
57
 
58
+ /**
59
+ * Execution context for running a custom command
60
+ */
25
61
  export interface CommandExecutionContext {
62
+ /** The command being executed */
26
63
  command: Command;
27
- args: string[]; // Command arguments
64
+ /** Arguments passed to the command */
65
+ args: string[];
66
+ /** Function to substitute $1, $2, $ARGUMENTS placeholders */
28
67
  substitutePlaceholders: (content: string, args: string[]) => string;
68
+ /** Function to execute bash commands */
29
69
  executeBash: (bashCommand: string) => Promise<string>;
70
+ /** Function to read file contents */
30
71
  readFile: (filePath: string) => Promise<string>;
31
72
  }
32
73
 
33
74
  /**
34
- * Parsed command with substitutions applied
75
+ * Parsed command with all substitutions applied
76
+ *
77
+ * @remarks
78
+ * After parsing, the command content will have:
79
+ * - $1, $2, etc. replaced with positional arguments
80
+ * - $ARGUMENTS replaced with all arguments joined by spaces
81
+ * - !`cmd` patterns extracted to bashCommands and replaced with [BASH_OUTPUT]
82
+ * - @file patterns extracted to fileReferences
35
83
  */
36
84
  export interface ParsedCommand {
37
- content: string; // Content with substitutions applied
38
- bashCommands: string[]; // Bash commands to execute
39
- fileReferences: string[]; // Files to read
85
+ /** Content with all substitutions applied */
86
+ content: string;
87
+ /** Bash commands extracted from !`cmd` patterns */
88
+ bashCommands: string[];
89
+ /** File paths extracted from @file patterns */
90
+ fileReferences: string[];
40
91
  }
@@ -31,10 +31,18 @@ export class CommandManager {
31
31
  const discovered: Command[] = [];
32
32
 
33
33
  // Personal commands
34
- discovered.push(...await this.discoverCommandsInDirectory(this.personalCommandsDir, 'personal'));
34
+ try {
35
+ discovered.push(...await this.discoverCommandsInDirectory(this.personalCommandsDir, 'personal'));
36
+ } catch (error: any) {
37
+ console.warn(`Warning: Failed to load personal commands from ${this.personalCommandsDir}: ${error.message}`);
38
+ }
35
39
 
36
40
  // Project commands
37
- discovered.push(...await this.discoverCommandsInDirectory(this.projectCommandsDir, 'project'));
41
+ try {
42
+ discovered.push(...await this.discoverCommandsInDirectory(this.projectCommandsDir, 'project'));
43
+ } catch (error: any) {
44
+ console.warn(`Warning: Failed to load project commands from ${this.projectCommandsDir}: ${error.message}`);
45
+ }
38
46
 
39
47
  // Store commands in map (project commands override personal)
40
48
  for (const command of discovered) {
@@ -85,6 +93,7 @@ export class CommandManager {
85
93
  const commandName = this.getCommandName(commandPath, type);
86
94
 
87
95
  if (!commandName) {
96
+ console.warn(`Warning: Invalid command name in ${commandPath} (skipping)`);
88
97
  return null;
89
98
  }
90
99
 
@@ -107,7 +116,13 @@ export class CommandManager {
107
116
 
108
117
  return command;
109
118
  } catch (error: any) {
110
- console.error(`Error parsing command ${commandPath}: ${error.message}`);
119
+ if (error.code === 'ENOENT') {
120
+ console.warn(`Warning: Command file not found: ${commandPath}`);
121
+ } else if (error.code === 'EACCES') {
122
+ console.warn(`Warning: Permission denied reading command: ${commandPath}`);
123
+ } else {
124
+ console.error(`Error parsing command ${commandPath}: ${error.message}`);
125
+ }
111
126
  return null;
112
127
  }
113
128
  }
@@ -222,7 +237,10 @@ export class CommandManager {
222
237
  const bashRegex = /!`([^`]+)`/g;
223
238
  let bashMatch;
224
239
  while ((bashMatch = bashRegex.exec(content)) !== null) {
225
- bashCommands.push(bashMatch[1]);
240
+ const bashCommand = bashMatch[1].trim();
241
+ if (bashCommand) {
242
+ bashCommands.push(bashCommand);
243
+ }
226
244
  }
227
245
 
228
246
  // Remove bash command markers
@@ -232,7 +250,10 @@ export class CommandManager {
232
250
  const fileRegex = /@([^\s]+)/g;
233
251
  let fileMatch;
234
252
  while ((fileMatch = fileRegex.exec(content)) !== null) {
235
- fileReferences.push(fileMatch[1]);
253
+ const fileRef = fileMatch[1].trim();
254
+ if (fileRef && !fileReferences.includes(fileRef)) {
255
+ fileReferences.push(fileRef);
256
+ }
236
257
  }
237
258
 
238
259
  return { content, bashCommands, fileReferences };
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Tests for CommandManager
3
+ */
4
+
5
+ import { CommandManager } from '../CommandManager';
6
+ import { Command } from '../Command';
7
+
8
+ describe('CommandManager', () => {
9
+ let manager: CommandManager;
10
+
11
+ beforeEach(() => {
12
+ manager = new CommandManager();
13
+ });
14
+
15
+ describe('getAllCommands', () => {
16
+ it('should return empty array initially', () => {
17
+ const commands = manager.getAllCommands();
18
+ expect(commands).toEqual([]);
19
+ });
20
+ });
21
+
22
+ describe('getCommandsContext', () => {
23
+ it('should return empty string when no commands', () => {
24
+ const context = manager.getCommandsContext();
25
+ expect(context).toBe('');
26
+ });
27
+ });
28
+
29
+ describe('parseCommand', () => {
30
+ it('should replace $ARGUMENTS placeholder', async () => {
31
+ const command: Command = {
32
+ name: 'echo',
33
+ type: 'personal',
34
+ path: '/echo.md',
35
+ directory: '/commands',
36
+ description: 'Echo arguments',
37
+ frontmatter: {},
38
+ content: 'You said: $ARGUMENTS',
39
+ hasParameters: true
40
+ };
41
+
42
+ const parsed = await manager.parseCommand(command, ['hello', 'world']);
43
+
44
+ expect(parsed.content).toContain('hello world');
45
+ });
46
+
47
+ it('should replace $1, $2 placeholders', async () => {
48
+ const command: Command = {
49
+ name: 'greet',
50
+ type: 'personal',
51
+ path: '/greet.md',
52
+ directory: '/commands',
53
+ description: 'Greet user',
54
+ frontmatter: {},
55
+ content: 'Hello $1, welcome to $2',
56
+ hasParameters: true
57
+ };
58
+
59
+ const parsed = await manager.parseCommand(command, ['Alice', 'Wonderland']);
60
+
61
+ expect(parsed.content).toContain('Hello Alice');
62
+ expect(parsed.content).toContain('welcome to Wonderland');
63
+ });
64
+
65
+ it('should extract bash commands from content', async () => {
66
+ const command: Command = {
67
+ name: 'run-test',
68
+ type: 'personal',
69
+ path: '/run-test.md',
70
+ directory: '/commands',
71
+ description: 'Run tests',
72
+ frontmatter: {},
73
+ content: 'Run tests with !`npm test`',
74
+ hasParameters: false
75
+ };
76
+
77
+ const parsed = await manager.parseCommand(command, []);
78
+
79
+ expect(parsed.bashCommands).toHaveLength(1);
80
+ expect(parsed.bashCommands[0]).toBe('npm test');
81
+ });
82
+ });
83
+ });
@@ -0,0 +1,30 @@
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://www.google.com/search?q=${encodeURIComponent(query)}&hl=en`;
7
+ console.log(`Fetching Google: ${url}`);
8
+
9
+ try {
10
+ // Mimic standard browser
11
+ const userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
12
+ const cmd = `curl -s -L -A "${userAgent}" "${url}"`;
13
+ const html = execSync(cmd, { encoding: 'utf-8' });
14
+
15
+ fs.writeFileSync('google_dump.html', html);
16
+ console.log('Dumped HTML to google_dump.html. Length:', html.length);
17
+
18
+ if (html.includes('Captcha') || html.includes('unusual traffic')) {
19
+ console.log('BLOCKED by Google Captcha');
20
+ } else {
21
+ console.log('Seems OK? Checking for result markers...');
22
+ // Google uses complex class names, but often "h3" is title
23
+ if (html.includes('<h3')) {
24
+ console.log('Found h3 tags (likely titles).');
25
+ }
26
+ }
27
+
28
+ } catch (e: any) {
29
+ console.error('Error:', e.message);
30
+ }
@@ -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
@@ -1,13 +1,39 @@
1
1
  #!/usr/bin/env node
2
+
3
+ /**
4
+ * Mentis CLI - An Agentic, Multi-Model CLI Coding Assistant
5
+ *
6
+ * @packageDocumentation
7
+ */
8
+
2
9
  import { ReplManager } from './repl/ReplManager';
3
10
 
11
+ /**
12
+ * CLI options for controlling Mentis behavior
13
+ */
4
14
  interface CliOptions {
15
+ /** Resume from the last saved checkpoint */
5
16
  resume: boolean;
17
+ /** Auto-confirm all prompts (skip confirmations) */
6
18
  yolo: boolean;
19
+ /** Run in headless (non-interactive) mode */
7
20
  headless: boolean;
21
+ /** Prompt to execute in headless mode */
8
22
  headlessPrompt?: string;
9
23
  }
10
24
 
25
+ /**
26
+ * Parse command line arguments
27
+ *
28
+ * @returns Parsed command and options
29
+ *
30
+ * @example
31
+ * ```bash
32
+ * mentis --resume
33
+ * mentis -p "fix the bug"
34
+ * mentis --yolo
35
+ * ```
36
+ */
11
37
  function parseArgs(): { command: string | null, options: CliOptions } {
12
38
  const args = process.argv.slice(2);
13
39
  const options: CliOptions = {
@@ -69,7 +95,12 @@ Commands (in REPL):
69
95
  return { command, options };
70
96
  }
71
97
 
72
- async function main() {
98
+ /**
99
+ * Main entry point for Mentis CLI
100
+ *
101
+ * Parses arguments and starts the REPL or update manager
102
+ */
103
+ async function main(): Promise<void> {
73
104
  const { command, options } = parseArgs();
74
105
 
75
106
  // Handle update command
@@ -85,6 +116,19 @@ async function main() {
85
116
  await repl.start();
86
117
  }
87
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
+
131
+ // Start the application
88
132
  main().catch((error) => {
89
133
  console.error('Fatal error:', error);
90
134
  process.exit(1);