@indiccoder/mentis-cli 1.0.9 → 1.1.0
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/.mentis/commands/ls.md +12 -0
- package/dist/commands/Command.js +6 -0
- package/dist/commands/CommandCreator.js +286 -0
- package/dist/commands/CommandManager.js +268 -0
- package/dist/commands/SlashCommandTool.js +160 -0
- package/dist/repl/ReplManager.js +106 -3
- package/dist/ui/UIManager.js +2 -2
- package/dist/utils/ContextVisualizer.js +92 -0
- package/dist/utils/ConversationCompacter.js +98 -0
- package/dist/utils/ProjectInitializer.js +181 -0
- package/package.json +2 -2
- package/src/commands/Command.ts +40 -0
- package/src/commands/CommandCreator.ts +281 -0
- package/src/commands/CommandManager.ts +280 -0
- package/src/commands/SlashCommandTool.ts +152 -0
- package/src/repl/ReplManager.ts +131 -3
- package/src/ui/UIManager.ts +2 -2
- package/src/utils/ContextVisualizer.ts +105 -0
- package/src/utils/ConversationCompacter.ts +124 -0
- package/src/utils/ProjectInitializer.ts +170 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CommandManager - Discover, parse, and execute custom slash commands
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import * as os from 'os';
|
|
8
|
+
import { glob } from 'fast-glob';
|
|
9
|
+
import YAML from 'yaml';
|
|
10
|
+
import {
|
|
11
|
+
Command,
|
|
12
|
+
CommandFrontmatter,
|
|
13
|
+
CommandExecutionContext,
|
|
14
|
+
ParsedCommand
|
|
15
|
+
} from './Command';
|
|
16
|
+
|
|
17
|
+
export class CommandManager {
|
|
18
|
+
private commands: Map<string, Command> = new Map();
|
|
19
|
+
private personalCommandsDir: string;
|
|
20
|
+
private projectCommandsDir: string;
|
|
21
|
+
|
|
22
|
+
constructor(cwd: string = process.cwd()) {
|
|
23
|
+
this.personalCommandsDir = path.join(os.homedir(), '.mentis', 'commands');
|
|
24
|
+
this.projectCommandsDir = path.join(cwd, '.mentis', 'commands');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Discover all commands from configured directories
|
|
29
|
+
*/
|
|
30
|
+
async discoverCommands(): Promise<Command[]> {
|
|
31
|
+
const discovered: Command[] = [];
|
|
32
|
+
|
|
33
|
+
// Personal commands
|
|
34
|
+
discovered.push(...await this.discoverCommandsInDirectory(this.personalCommandsDir, 'personal'));
|
|
35
|
+
|
|
36
|
+
// Project commands
|
|
37
|
+
discovered.push(...await this.discoverCommandsInDirectory(this.projectCommandsDir, 'project'));
|
|
38
|
+
|
|
39
|
+
// Store commands in map (project commands override personal)
|
|
40
|
+
for (const command of discovered) {
|
|
41
|
+
this.commands.set(command.name, command);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return Array.from(this.commands.values());
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Discover commands in a specific directory
|
|
49
|
+
*/
|
|
50
|
+
private async discoverCommandsInDirectory(dir: string, type: 'personal' | 'project'): Promise<Command[]> {
|
|
51
|
+
if (!fs.existsSync(dir)) {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const commands: Command[] = [];
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
// Find all .md files in subdirectories
|
|
59
|
+
const commandFiles = await glob('**/*.md', {
|
|
60
|
+
cwd: dir,
|
|
61
|
+
absolute: true,
|
|
62
|
+
onlyFiles: true
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
for (const commandFile of commandFiles) {
|
|
66
|
+
const command = await this.parseCommandFile(commandFile, type);
|
|
67
|
+
if (command) {
|
|
68
|
+
commands.push(command);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} catch (error: any) {
|
|
72
|
+
console.error(`Error discovering commands in ${dir}: ${error.message}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return commands;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Parse a command file
|
|
80
|
+
*/
|
|
81
|
+
private async parseCommandFile(commandPath: string, type: 'personal' | 'project'): Promise<Command | null> {
|
|
82
|
+
try {
|
|
83
|
+
const content = fs.readFileSync(commandPath, 'utf-8');
|
|
84
|
+
const frontmatter = this.extractFrontmatter(content);
|
|
85
|
+
const commandName = this.getCommandName(commandPath, type);
|
|
86
|
+
|
|
87
|
+
if (!commandName) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Get namespace (subdirectory)
|
|
92
|
+
const relativePath = path.relative(type === 'personal' ? this.personalCommandsDir : this.projectCommandsDir, commandPath);
|
|
93
|
+
const namespace = path.dirname(relativePath) !== '.' ? path.dirname(relativePath) : '';
|
|
94
|
+
|
|
95
|
+
const description = frontmatter.description || this.extractFirstLine(content);
|
|
96
|
+
|
|
97
|
+
const command: Command = {
|
|
98
|
+
name: commandName,
|
|
99
|
+
type,
|
|
100
|
+
path: commandPath,
|
|
101
|
+
directory: path.dirname(commandPath),
|
|
102
|
+
frontmatter,
|
|
103
|
+
content: content,
|
|
104
|
+
description: namespace ? `${description} (${type}:${namespace})` : description,
|
|
105
|
+
hasParameters: content.includes('$1') || content.includes('$ARGUMENTS')
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
return command;
|
|
109
|
+
} catch (error: any) {
|
|
110
|
+
console.error(`Error parsing command ${commandPath}: ${error.message}`);
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get command name from file path
|
|
117
|
+
*/
|
|
118
|
+
private getCommandName(commandPath: string, type: 'personal' | 'project'): string | null {
|
|
119
|
+
const relativePath = path.relative(type === 'personal' ? this.personalCommandsDir : this.projectCommandsDir, commandPath);
|
|
120
|
+
const nameWithExt = path.basename(relativePath); // e.g., "review.md"
|
|
121
|
+
const name = nameWithExt.replace(/\.md$/, ''); // e.g., "review"
|
|
122
|
+
|
|
123
|
+
if (!name || name.startsWith('.')) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return name;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Extract YAML frontmatter from markdown content
|
|
132
|
+
*/
|
|
133
|
+
private extractFrontmatter(content: string): CommandFrontmatter {
|
|
134
|
+
const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n/;
|
|
135
|
+
const match = content.match(frontmatterRegex);
|
|
136
|
+
|
|
137
|
+
if (!match) {
|
|
138
|
+
return {};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const parsed = YAML.parse(match[1]) as CommandFrontmatter;
|
|
143
|
+
return parsed;
|
|
144
|
+
} catch (error) {
|
|
145
|
+
return {};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Extract first line of content as description
|
|
151
|
+
*/
|
|
152
|
+
private extractFirstLine(content: string): string {
|
|
153
|
+
// Remove frontmatter
|
|
154
|
+
const withoutFrontmatter = content.replace(/^---\s*\n[\s\S]*?\n---\s*\n/, '');
|
|
155
|
+
|
|
156
|
+
// Get first non-empty line
|
|
157
|
+
const lines = withoutFrontmatter.split('\n');
|
|
158
|
+
for (const line of lines) {
|
|
159
|
+
const trimmed = line.trim();
|
|
160
|
+
if (trimmed && !trimmed.startsWith('#')) {
|
|
161
|
+
return trimmed;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return 'No description';
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Get command by name
|
|
170
|
+
*/
|
|
171
|
+
getCommand(name: string): Command | undefined {
|
|
172
|
+
return this.commands.get(name);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Get all commands
|
|
177
|
+
*/
|
|
178
|
+
getAllCommands(): Command[] {
|
|
179
|
+
return Array.from(this.commands.values());
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Get commands context for system prompt injection
|
|
184
|
+
*/
|
|
185
|
+
getCommandsContext(): string {
|
|
186
|
+
const commands = this.getAllCommands();
|
|
187
|
+
|
|
188
|
+
if (commands.length === 0) {
|
|
189
|
+
return '';
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const context = commands.map(cmd => {
|
|
193
|
+
let line = `/${cmd.name}`;
|
|
194
|
+
if (cmd.frontmatter['argument-hint']) {
|
|
195
|
+
line += ` ${cmd.frontmatter['argument-hint']}`;
|
|
196
|
+
}
|
|
197
|
+
line += `: ${cmd.description.replace(/\s*\([^)]+\)/, '')}`;
|
|
198
|
+
return line;
|
|
199
|
+
}).join('\n');
|
|
200
|
+
|
|
201
|
+
return `Available Custom Commands:\n${context}`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Parse command content and execute substitutions
|
|
206
|
+
*/
|
|
207
|
+
async parseCommand(command: Command, args: string[]): Promise<ParsedCommand> {
|
|
208
|
+
let content = command.content;
|
|
209
|
+
const bashCommands: string[] = [];
|
|
210
|
+
const fileReferences: string[] = [];
|
|
211
|
+
|
|
212
|
+
// Remove frontmatter from content
|
|
213
|
+
content = content.replace(/^---\s*\n[\s\S]*?\n---\s*\n/, '');
|
|
214
|
+
|
|
215
|
+
// Substitute $1, $2, etc.
|
|
216
|
+
content = this.substitutePositionalArgs(content, args);
|
|
217
|
+
|
|
218
|
+
// Substitute $ARGUMENTS
|
|
219
|
+
content = this.substituteAllArgs(content, args);
|
|
220
|
+
|
|
221
|
+
// Extract and collect bash commands (!`cmd`)
|
|
222
|
+
const bashRegex = /!`([^`]+)`/g;
|
|
223
|
+
let bashMatch;
|
|
224
|
+
while ((bashMatch = bashRegex.exec(content)) !== null) {
|
|
225
|
+
bashCommands.push(bashMatch[1]);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Remove bash command markers
|
|
229
|
+
content = content.replace(/!`[^`]+`/g, '[BASH_OUTPUT]');
|
|
230
|
+
|
|
231
|
+
// Extract and collect file references (@file)
|
|
232
|
+
const fileRegex = /@([^\s]+)/g;
|
|
233
|
+
let fileMatch;
|
|
234
|
+
while ((fileMatch = fileRegex.exec(content)) !== null) {
|
|
235
|
+
fileReferences.push(fileMatch[1]);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return { content, bashCommands, fileReferences };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Substitute positional arguments ($1, $2, etc.)
|
|
243
|
+
*/
|
|
244
|
+
private substitutePositionalArgs(content: string, args: string[]): string {
|
|
245
|
+
return content.replace(/\$(\d+)/g, (match, index) => {
|
|
246
|
+
const argIndex = parseInt(index) - 1;
|
|
247
|
+
return args[argIndex] || '';
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Substitute $ARGUMENTS placeholder
|
|
253
|
+
*/
|
|
254
|
+
private substituteAllArgs(content: string, args: string[]): string {
|
|
255
|
+
return content.replace(/\$ARGUMENTS/g, args.join(' '));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Ensure commands directories exist
|
|
260
|
+
*/
|
|
261
|
+
ensureDirectoriesExist(): void {
|
|
262
|
+
if (!fs.existsSync(this.personalCommandsDir)) {
|
|
263
|
+
fs.mkdirSync(this.personalCommandsDir, { recursive: true });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Get personal commands directory path
|
|
269
|
+
*/
|
|
270
|
+
getPersonalCommandsDir(): string {
|
|
271
|
+
return this.personalCommandsDir;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Get project commands directory path
|
|
276
|
+
*/
|
|
277
|
+
getProjectCommandsDir(): string {
|
|
278
|
+
return this.projectCommandsDir;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SlashCommandTool - Tool for model-invoked custom command execution
|
|
3
|
+
* The model can call this tool when it determines a custom command should be run
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Tool } from '../tools/Tool';
|
|
7
|
+
import { CommandManager } from './CommandManager';
|
|
8
|
+
import { Command } from './Command';
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
import { exec } from 'child_process';
|
|
11
|
+
import { promisify } from 'util';
|
|
12
|
+
|
|
13
|
+
const execAsync = promisify(exec);
|
|
14
|
+
|
|
15
|
+
interface SlashCommandArgs {
|
|
16
|
+
name: string;
|
|
17
|
+
arguments?: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class SlashCommandTool implements Tool {
|
|
21
|
+
name = 'slash_command';
|
|
22
|
+
description = 'Execute a custom slash command. Available commands can be seen in /help. Example: slash_command({ name: "review", arguments: ["src/file.ts"] })';
|
|
23
|
+
parameters = {
|
|
24
|
+
type: 'object',
|
|
25
|
+
properties: {
|
|
26
|
+
name: {
|
|
27
|
+
type: 'string',
|
|
28
|
+
description: 'The name of the custom command to execute'
|
|
29
|
+
},
|
|
30
|
+
arguments: {
|
|
31
|
+
type: 'array',
|
|
32
|
+
items: { type: 'string' },
|
|
33
|
+
description: 'Arguments to pass to the command'
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
required: ['name']
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
private commandManager: CommandManager;
|
|
40
|
+
|
|
41
|
+
constructor(commandManager: CommandManager) {
|
|
42
|
+
this.commandManager = commandManager;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async execute(args: SlashCommandArgs): Promise<string> {
|
|
46
|
+
const { name, arguments: cmdArgs = [] } = args;
|
|
47
|
+
|
|
48
|
+
if (!name) {
|
|
49
|
+
return 'Error: Command name is required';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const command = this.commandManager.getCommand(name);
|
|
53
|
+
if (!command) {
|
|
54
|
+
const availableCommands = this.commandManager.getAllCommands().map(c => c.name).join(', ');
|
|
55
|
+
return `Error: Command "${name}" not found. Available commands: ${availableCommands || 'none'}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
// Parse command and handle substitutions
|
|
60
|
+
const parsed = await this.commandManager.parseCommand(command, cmdArgs);
|
|
61
|
+
let content = parsed.content;
|
|
62
|
+
|
|
63
|
+
// Execute bash commands and collect results
|
|
64
|
+
const bashResults: string[] = [];
|
|
65
|
+
for (const bashCmd of parsed.bashCommands) {
|
|
66
|
+
try {
|
|
67
|
+
const result = await execAsync(bashCmd);
|
|
68
|
+
bashResults.push(`${bashCmd}\n${result.stdout}`);
|
|
69
|
+
} catch (error: any) {
|
|
70
|
+
bashResults.push(`${bashCmd}\nError: ${error.message}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Substitute bash outputs into content
|
|
75
|
+
let bashIndex = 0;
|
|
76
|
+
content = content.replace(/\[BASH_OUTPUT\]/g, () => {
|
|
77
|
+
return bashResults[bashIndex++] || '';
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Read file references
|
|
81
|
+
const fileContents: string[] = [];
|
|
82
|
+
for (const filePath of parsed.fileReferences) {
|
|
83
|
+
try {
|
|
84
|
+
const fileContent = fs.readFileSync(filePath, 'utf-8');
|
|
85
|
+
fileContents.push(`\n=== File: ${filePath} ===\n${fileContent}\n=== End of ${filePath} ===\n`);
|
|
86
|
+
} catch (error: any) {
|
|
87
|
+
fileContents.push(`\n=== File: ${filePath} ===\nError: ${error.message}\n=== End of ${filePath} ===\n`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Substitute file references
|
|
92
|
+
let fileIndex = 0;
|
|
93
|
+
content = content.replace(/@[^\s]+/g, () => {
|
|
94
|
+
return fileContents[fileIndex++] || '';
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return `# Executing: /${name}${cmdArgs.length ? ' ' + cmdArgs.join(' ') : ''}\n\n${content}`;
|
|
98
|
+
} catch (error: any) {
|
|
99
|
+
return `Error executing command "${name}": ${error.message}`;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* ListCommandsTool - Tool for listing custom commands
|
|
106
|
+
*/
|
|
107
|
+
export class ListCommandsTool implements Tool {
|
|
108
|
+
name = 'list_commands';
|
|
109
|
+
description = 'List all available custom slash commands with their descriptions';
|
|
110
|
+
parameters = {
|
|
111
|
+
type: 'object',
|
|
112
|
+
properties: {},
|
|
113
|
+
required: []
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
private commandManager: CommandManager;
|
|
117
|
+
|
|
118
|
+
constructor(commandManager: CommandManager) {
|
|
119
|
+
this.commandManager = commandManager;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async execute(): Promise<string> {
|
|
123
|
+
const commands = this.commandManager.getAllCommands();
|
|
124
|
+
|
|
125
|
+
if (commands.length === 0) {
|
|
126
|
+
return 'No custom commands available. Add commands to ~/.mentis/commands/ or .mentis/commands/';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let response = `# Custom Commands (${commands.length})\n\n`;
|
|
130
|
+
|
|
131
|
+
// Group by namespace
|
|
132
|
+
const grouped = new Map<string, Command[]>();
|
|
133
|
+
for (const cmd of commands) {
|
|
134
|
+
const ns = cmd.description.match(/\(([^)]+)\)/)?.[1] || cmd.type;
|
|
135
|
+
if (!grouped.has(ns)) {
|
|
136
|
+
grouped.set(ns, []);
|
|
137
|
+
}
|
|
138
|
+
grouped.get(ns)!.push(cmd);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
for (const [namespace, cmds] of grouped) {
|
|
142
|
+
response += `## ${namespace}\n\n`;
|
|
143
|
+
for (const cmd of cmds) {
|
|
144
|
+
const params = cmd.frontmatter['argument-hint'] ? ` ${cmd.frontmatter['argument-hint']}` : '';
|
|
145
|
+
response += `**/${cmd.name}${params}**\n`;
|
|
146
|
+
response += `${cmd.description}\n\n`;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return response;
|
|
151
|
+
}
|
|
152
|
+
}
|
package/src/repl/ReplManager.ts
CHANGED
|
@@ -19,6 +19,11 @@ import { McpClient } from '../mcp/McpClient';
|
|
|
19
19
|
import { CheckpointManager } from '../checkpoint/CheckpointManager';
|
|
20
20
|
import { SkillsManager } from '../skills/SkillsManager';
|
|
21
21
|
import { LoadSkillTool, ListSkillsTool, ReadSkillFileTool } from '../skills/LoadSkillTool';
|
|
22
|
+
import { ContextVisualizer } from '../utils/ContextVisualizer';
|
|
23
|
+
import { ProjectInitializer } from '../utils/ProjectInitializer';
|
|
24
|
+
import { ConversationCompacter } from '../utils/ConversationCompacter';
|
|
25
|
+
import { CommandManager } from '../commands/CommandManager';
|
|
26
|
+
import { SlashCommandTool, ListCommandsTool } from '../commands/SlashCommandTool';
|
|
22
27
|
import * as readline from 'readline';
|
|
23
28
|
import * as fs from 'fs';
|
|
24
29
|
import * as path from 'path';
|
|
@@ -34,6 +39,9 @@ export class ReplManager {
|
|
|
34
39
|
private contextManager: ContextManager;
|
|
35
40
|
private checkpointManager: CheckpointManager;
|
|
36
41
|
private skillsManager: SkillsManager;
|
|
42
|
+
private contextVisualizer: ContextVisualizer;
|
|
43
|
+
private conversationCompacter: ConversationCompacter;
|
|
44
|
+
private commandManager: CommandManager;
|
|
37
45
|
private history: ChatMessage[] = [];
|
|
38
46
|
private mode: 'PLAN' | 'BUILD' = 'BUILD';
|
|
39
47
|
private tools: Tool[] = [];
|
|
@@ -47,6 +55,9 @@ export class ReplManager {
|
|
|
47
55
|
this.contextManager = new ContextManager();
|
|
48
56
|
this.checkpointManager = new CheckpointManager();
|
|
49
57
|
this.skillsManager = new SkillsManager();
|
|
58
|
+
this.contextVisualizer = new ContextVisualizer();
|
|
59
|
+
this.conversationCompacter = new ConversationCompacter();
|
|
60
|
+
this.commandManager = new CommandManager();
|
|
50
61
|
this.shell = new PersistentShell();
|
|
51
62
|
|
|
52
63
|
// Create tools array without skill tools first
|
|
@@ -77,12 +88,17 @@ export class ReplManager {
|
|
|
77
88
|
}
|
|
78
89
|
|
|
79
90
|
/**
|
|
80
|
-
* Initialize the skills system
|
|
91
|
+
* Initialize the skills and custom commands system
|
|
81
92
|
*/
|
|
82
93
|
private async initializeSkills() {
|
|
94
|
+
// Initialize skills
|
|
83
95
|
this.skillsManager.ensureDirectoriesExist();
|
|
84
96
|
await this.skillsManager.discoverSkills();
|
|
85
97
|
|
|
98
|
+
// Initialize custom commands
|
|
99
|
+
this.commandManager.ensureDirectoriesExist();
|
|
100
|
+
await this.commandManager.discoverCommands();
|
|
101
|
+
|
|
86
102
|
// Add skill tools to the tools list
|
|
87
103
|
// Pass callback to LoadSkillTool to track active skill
|
|
88
104
|
this.tools.push(
|
|
@@ -90,7 +106,9 @@ export class ReplManager {
|
|
|
90
106
|
this.activeSkill = skill ? skill.name : null;
|
|
91
107
|
}),
|
|
92
108
|
new ListSkillsTool(this.skillsManager),
|
|
93
|
-
new ReadSkillFileTool(this.skillsManager)
|
|
109
|
+
new ReadSkillFileTool(this.skillsManager),
|
|
110
|
+
new SlashCommandTool(this.commandManager),
|
|
111
|
+
new ListCommandsTool(this.commandManager)
|
|
94
112
|
);
|
|
95
113
|
}
|
|
96
114
|
|
|
@@ -127,7 +145,9 @@ export class ReplManager {
|
|
|
127
145
|
'git_pull': 'GitPull',
|
|
128
146
|
'load_skill': 'Read',
|
|
129
147
|
'list_skills': 'Read',
|
|
130
|
-
'read_skill_file': 'Read'
|
|
148
|
+
'read_skill_file': 'Read',
|
|
149
|
+
'slash_command': 'Read',
|
|
150
|
+
'list_commands': 'Read'
|
|
131
151
|
};
|
|
132
152
|
|
|
133
153
|
const mappedToolName = toolMapping[toolName] || toolName;
|
|
@@ -256,11 +276,13 @@ export class ReplManager {
|
|
|
256
276
|
console.log(' /use <provider> [model] - Quick switch (legacy)');
|
|
257
277
|
console.log(' /mcp <cmd> - Manage MCP servers');
|
|
258
278
|
console.log(' /skills <list|show|create|validate> - Manage Agent Skills');
|
|
279
|
+
console.log(' /commands <list|create|validate> - Manage Custom Commands');
|
|
259
280
|
console.log(' /resume - Resume last session');
|
|
260
281
|
console.log(' /checkpoint <save|load|list> [name] - Manage checkpoints');
|
|
261
282
|
console.log(' /search <query> - Search codebase');
|
|
262
283
|
console.log(' /run <cmd> - Run shell command');
|
|
263
284
|
console.log(' /commit [msg] - Git commit all changes');
|
|
285
|
+
console.log(' /init - Initialize project with .mentis.md');
|
|
264
286
|
break;
|
|
265
287
|
case '/plan':
|
|
266
288
|
this.mode = 'PLAN';
|
|
@@ -330,9 +352,15 @@ export class ReplManager {
|
|
|
330
352
|
const updater = new UpdateManager();
|
|
331
353
|
await updater.checkAndPerformUpdate(true);
|
|
332
354
|
break;
|
|
355
|
+
case '/init':
|
|
356
|
+
await this.handleInitCommand();
|
|
357
|
+
break;
|
|
333
358
|
case '/skills':
|
|
334
359
|
await this.handleSkillsCommand(args);
|
|
335
360
|
break;
|
|
361
|
+
case '/commands':
|
|
362
|
+
await this.handleCommandsCommand(args);
|
|
363
|
+
break;
|
|
336
364
|
default:
|
|
337
365
|
console.log(chalk.red(`Unknown command: ${command}`));
|
|
338
366
|
}
|
|
@@ -341,6 +369,7 @@ export class ReplManager {
|
|
|
341
369
|
private async handleChat(input: string) {
|
|
342
370
|
const context = this.contextManager.getContextString();
|
|
343
371
|
const skillsContext = this.skillsManager.getSkillsContext();
|
|
372
|
+
const commandsContext = this.commandManager.getCommandsContext();
|
|
344
373
|
let fullInput = input;
|
|
345
374
|
|
|
346
375
|
let modeInstruction = '';
|
|
@@ -357,6 +386,11 @@ export class ReplManager {
|
|
|
357
386
|
fullInput = `${skillsContext}\n\n${fullInput}`;
|
|
358
387
|
}
|
|
359
388
|
|
|
389
|
+
// Add commands context if available
|
|
390
|
+
if (commandsContext) {
|
|
391
|
+
fullInput = `${commandsContext}\n\n${fullInput}`;
|
|
392
|
+
}
|
|
393
|
+
|
|
360
394
|
if (context) {
|
|
361
395
|
fullInput = `${context}\n\nUser Question: ${fullInput}`;
|
|
362
396
|
}
|
|
@@ -518,8 +552,22 @@ export class ReplManager {
|
|
|
518
552
|
console.log(chalk.dim(`\n(Tokens: ${input_tokens} in / ${output_tokens} out | Est. Cost: $${totalCost.toFixed(5)})`));
|
|
519
553
|
}
|
|
520
554
|
|
|
555
|
+
// Display context bar
|
|
556
|
+
const contextBar = this.contextVisualizer.getContextBar(this.history);
|
|
557
|
+
console.log(chalk.dim(`\n${contextBar}`));
|
|
558
|
+
|
|
521
559
|
console.log('');
|
|
522
560
|
this.history.push({ role: 'assistant', content: response.content });
|
|
561
|
+
|
|
562
|
+
// Auto-compact prompt when context is at 80%
|
|
563
|
+
const usage = this.contextVisualizer.calculateUsage(this.history);
|
|
564
|
+
if (usage.percentage >= 80) {
|
|
565
|
+
this.history = await this.conversationCompacter.promptIfCompactNeeded(
|
|
566
|
+
usage.percentage,
|
|
567
|
+
this.history,
|
|
568
|
+
this.modelClient
|
|
569
|
+
);
|
|
570
|
+
}
|
|
523
571
|
}
|
|
524
572
|
} catch (error: any) {
|
|
525
573
|
spinner.stop();
|
|
@@ -1034,6 +1082,86 @@ export class ReplManager {
|
|
|
1034
1082
|
}
|
|
1035
1083
|
}
|
|
1036
1084
|
|
|
1085
|
+
private async handleInitCommand(): Promise<void> {
|
|
1086
|
+
const initializer = new ProjectInitializer();
|
|
1087
|
+
await initializer.run();
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
private async handleCommandsCommand(args: string[]) {
|
|
1091
|
+
if (args.length < 1) {
|
|
1092
|
+
// Show commands list by default
|
|
1093
|
+
await this.handleCommandsCommand(['list']);
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
const action = args[0];
|
|
1098
|
+
|
|
1099
|
+
switch (action) {
|
|
1100
|
+
case 'list':
|
|
1101
|
+
await this.handleCommandsList();
|
|
1102
|
+
break;
|
|
1103
|
+
case 'create':
|
|
1104
|
+
await this.handleCommandsCreate(args[1]);
|
|
1105
|
+
break;
|
|
1106
|
+
case 'validate':
|
|
1107
|
+
await this.handleCommandsValidate();
|
|
1108
|
+
break;
|
|
1109
|
+
default:
|
|
1110
|
+
console.log(chalk.red(`Unknown commands action: ${action}`));
|
|
1111
|
+
console.log(chalk.yellow('Available actions: list, create, validate'));
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
private async handleCommandsList(): Promise<void> {
|
|
1116
|
+
const commands = this.commandManager.getAllCommands();
|
|
1117
|
+
|
|
1118
|
+
if (commands.length === 0) {
|
|
1119
|
+
console.log(chalk.yellow('No custom commands available.'));
|
|
1120
|
+
console.log(chalk.dim('Create commands with: /commands create'));
|
|
1121
|
+
console.log(chalk.dim('Add commands to: ~/.mentis/commands/ or .mentis/commands/'));
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
console.log(chalk.cyan(`\nCustom Commands (${commands.length}):\n`));
|
|
1126
|
+
|
|
1127
|
+
// Group by namespace
|
|
1128
|
+
const grouped = new Map<string, any[]>();
|
|
1129
|
+
for (const cmd of commands) {
|
|
1130
|
+
const ns = cmd.description.match(/\(([^)]+)\)/)?.[1] || cmd.type;
|
|
1131
|
+
if (!grouped.has(ns)) {
|
|
1132
|
+
grouped.set(ns, []);
|
|
1133
|
+
}
|
|
1134
|
+
grouped.get(ns)!.push(cmd);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
for (const [namespace, cmds] of grouped) {
|
|
1138
|
+
console.log(chalk.bold(`\n${namespace}`));
|
|
1139
|
+
for (const cmd of cmds) {
|
|
1140
|
+
const params = cmd.frontmatter['argument-hint'] ? ` ${cmd.frontmatter['argument-hint']}` : '';
|
|
1141
|
+
console.log(` /${cmd.name}${params}`);
|
|
1142
|
+
console.log(` ${cmd.description.replace(/\s*\([^)]+\)/, '')}`);
|
|
1143
|
+
|
|
1144
|
+
if (cmd.frontmatter['allowed-tools'] && cmd.frontmatter['allowed-tools'].length > 0) {
|
|
1145
|
+
console.log(chalk.dim(` Allowed tools: ${cmd.frontmatter['allowed-tools'].join(', ')}`));
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
console.log('');
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
private async handleCommandsCreate(name?: string): Promise<void> {
|
|
1153
|
+
const { CommandCreator } = await import('../commands/CommandCreator');
|
|
1154
|
+
const creator = new CommandCreator(this.commandManager);
|
|
1155
|
+
await creator.run(name);
|
|
1156
|
+
// Re-discover commands after creation
|
|
1157
|
+
await this.commandManager.discoverCommands();
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
private async handleCommandsValidate(): Promise<void> {
|
|
1161
|
+
const { validateCommands } = await import('../commands/CommandCreator');
|
|
1162
|
+
await validateCommands(this.commandManager);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1037
1165
|
private estimateCost(input: number, output: number): number {
|
|
1038
1166
|
const config = this.configManager.getConfig();
|
|
1039
1167
|
const provider = config.defaultProvider;
|
package/src/ui/UIManager.ts
CHANGED
|
@@ -14,13 +14,13 @@ export class UIManager {
|
|
|
14
14
|
whitespaceBreak: true,
|
|
15
15
|
});
|
|
16
16
|
console.log(gradient.pastel.multiline(logoText));
|
|
17
|
-
console.log(chalk.gray(' v1.0
|
|
17
|
+
console.log(chalk.gray(' v1.1.0 - AI Coding Agent'));
|
|
18
18
|
console.log('');
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
public static renderDashboard(config: { model: string, mode: string, cwd: string }) {
|
|
22
22
|
const { model, cwd } = config;
|
|
23
|
-
const version = 'v1.0
|
|
23
|
+
const version = 'v1.1.0';
|
|
24
24
|
|
|
25
25
|
// Layout: Left (Status/Welcome) | Right (Tips/Activity)
|
|
26
26
|
// Total width ~80 chars.
|