@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.
- package/.claude/settings.local.json +8 -0
- package/.mentis/session.json +15 -0
- package/.mentis/sessions/1769189035730.json +23 -0
- package/.mentis/sessions/1769189569160.json +23 -0
- package/.mentis/sessions/1769767538672.json +23 -0
- package/.mentis/sessions/1769767785155.json +23 -0
- package/.mentis/sessions/1769768745802.json +23 -0
- package/.mentis/sessions/1769769600884.json +31 -0
- package/.mentis/sessions/1769770030160.json +31 -0
- package/.mentis/sessions/1769770606004.json +78 -0
- package/.mentis/sessions/1769771084515.json +141 -0
- package/.mentis/sessions/1769881926630.json +57 -0
- package/ARCHITECTURE.md +267 -0
- package/CONTRIBUTING.md +209 -0
- package/README.md +17 -0
- package/dist/checkpoint/CheckpointManager.js +92 -0
- package/dist/commands/Command.js +15 -1
- package/dist/commands/CommandManager.js +30 -5
- package/dist/commands/__tests__/CommandManager.test.js +70 -0
- package/dist/debug_google.js +61 -0
- package/dist/debug_lite.js +49 -0
- package/dist/debug_lite_headers.js +57 -0
- package/dist/debug_search.js +16 -0
- package/dist/index.js +33 -0
- package/dist/mcp/JsonRpcClient.js +16 -0
- package/dist/mcp/McpConfig.js +132 -0
- package/dist/mcp/McpManager.js +189 -0
- package/dist/repl/PersistentShell.js +20 -1
- package/dist/repl/ReplManager.js +410 -138
- package/dist/skills/Skill.js +17 -2
- package/dist/skills/SkillsManager.js +28 -3
- package/dist/skills/__tests__/SkillsManager.test.js +62 -0
- package/dist/tools/AskQuestionTool.js +172 -0
- package/dist/tools/EditFileTool.js +141 -0
- package/dist/tools/FileTools.js +7 -1
- package/dist/tools/PlanModeTool.js +53 -0
- package/dist/tools/WebSearchTool.js +190 -27
- package/dist/ui/DiffViewer.js +110 -0
- package/dist/ui/InputBox.js +16 -2
- package/dist/ui/MultiFileSelector.js +123 -0
- package/dist/ui/PlanModeUI.js +105 -0
- package/dist/ui/ToolExecutor.js +154 -0
- package/dist/ui/UIManager.js +12 -2
- package/dist/utils/__mocks__/chalk.js +20 -0
- package/dist/utils/__tests__/ContextVisualizer.test.js +95 -0
- package/docs/MCP_INTEGRATION.md +290 -0
- package/google_dump.html +18 -0
- package/lite_dump.html +176 -0
- package/lite_headers_dump.html +176 -0
- package/package.json +34 -2
- package/scripts/test_exa_mcp.ts +90 -0
- package/src/checkpoint/CheckpointManager.ts +102 -0
- package/src/commands/Command.ts +64 -13
- package/src/commands/CommandManager.ts +26 -5
- package/src/commands/__tests__/CommandManager.test.ts +83 -0
- package/src/debug_google.ts +30 -0
- package/src/debug_lite.ts +18 -0
- package/src/debug_lite_headers.ts +25 -0
- package/src/debug_search.ts +18 -0
- package/src/index.ts +45 -1
- package/src/mcp/JsonRpcClient.ts +19 -0
- package/src/mcp/McpConfig.ts +153 -0
- package/src/mcp/McpManager.ts +224 -0
- package/src/repl/PersistentShell.ts +24 -1
- package/src/repl/ReplManager.ts +1521 -1204
- package/src/skills/Skill.ts +91 -11
- package/src/skills/SkillsManager.ts +25 -3
- package/src/skills/__tests__/SkillsManager.test.ts +73 -0
- package/src/tools/AskQuestionTool.ts +197 -0
- package/src/tools/EditFileTool.ts +172 -0
- package/src/tools/FileTools.ts +3 -0
- package/src/tools/PlanModeTool.ts +50 -0
- package/src/tools/WebSearchTool.ts +235 -63
- package/src/ui/DiffViewer.ts +117 -0
- package/src/ui/InputBox.ts +17 -2
- package/src/ui/MultiFileSelector.ts +135 -0
- package/src/ui/PlanModeUI.ts +121 -0
- package/src/ui/ToolExecutor.ts +182 -0
- package/src/ui/UIManager.ts +15 -2
- package/src/utils/__mocks__/chalk.ts +19 -0
- package/src/utils/__tests__/ContextVisualizer.test.ts +118 -0
- 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
|
+
"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": "
|
|
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
|
}
|
package/src/commands/Command.ts
CHANGED
|
@@ -1,40 +1,91 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Custom Slash Commands System
|
|
3
|
-
*
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|