@indiccoder/mentis-cli 1.0.9 ā 1.1.1
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/index.js +52 -2
- package/dist/repl/ReplManager.js +120 -4
- package/dist/ui/UIManager.js +2 -2
- package/dist/utils/ContextVisualizer.js +92 -0
- package/dist/utils/ConversationCompacter.js +101 -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/index.ts +62 -2
- package/src/repl/ReplManager.ts +152 -4
- package/src/ui/UIManager.ts +2 -2
- package/src/utils/ContextVisualizer.ts +105 -0
- package/src/utils/ConversationCompacter.ts +128 -0
- package/src/utils/ProjectInitializer.ts +170 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: List all files in current directory
|
|
3
|
+
argument-hint: "[directory]"
|
|
4
|
+
---
|
|
5
|
+
## Usage
|
|
6
|
+
Use this command to list files: /ls [directory]
|
|
7
|
+
|
|
8
|
+
## Instructions
|
|
9
|
+
List all files using the ls command. If $1 is provided, use it as the directory.
|
|
10
|
+
|
|
11
|
+
!`ls -la $ARGUMENTS`
|
|
12
|
+
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* CommandCreator - Interactive wizard for creating new custom slash commands
|
|
4
|
+
*/
|
|
5
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
6
|
+
if (k2 === undefined) k2 = k;
|
|
7
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
8
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
9
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
10
|
+
}
|
|
11
|
+
Object.defineProperty(o, k2, desc);
|
|
12
|
+
}) : (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
o[k2] = m[k];
|
|
15
|
+
}));
|
|
16
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
17
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
18
|
+
}) : function(o, v) {
|
|
19
|
+
o["default"] = v;
|
|
20
|
+
});
|
|
21
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
22
|
+
var ownKeys = function(o) {
|
|
23
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
24
|
+
var ar = [];
|
|
25
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
26
|
+
return ar;
|
|
27
|
+
};
|
|
28
|
+
return ownKeys(o);
|
|
29
|
+
};
|
|
30
|
+
return function (mod) {
|
|
31
|
+
if (mod && mod.__esModule) return mod;
|
|
32
|
+
var result = {};
|
|
33
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
34
|
+
__setModuleDefault(result, mod);
|
|
35
|
+
return result;
|
|
36
|
+
};
|
|
37
|
+
})();
|
|
38
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
39
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
40
|
+
};
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.CommandCreator = void 0;
|
|
43
|
+
exports.validateCommands = validateCommands;
|
|
44
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
45
|
+
const fs = __importStar(require("fs"));
|
|
46
|
+
const path = __importStar(require("path"));
|
|
47
|
+
const os = __importStar(require("os"));
|
|
48
|
+
const CommandManager_1 = require("./CommandManager");
|
|
49
|
+
class CommandCreator {
|
|
50
|
+
constructor(commandManager) {
|
|
51
|
+
this.commandManager = commandManager || new CommandManager_1.CommandManager();
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Run the interactive command creation wizard
|
|
55
|
+
*/
|
|
56
|
+
async run(name) {
|
|
57
|
+
console.log('\nš Create a new Custom Slash Command\n');
|
|
58
|
+
let commandName;
|
|
59
|
+
let commandType;
|
|
60
|
+
let description;
|
|
61
|
+
let allowedTools;
|
|
62
|
+
let argumentHint;
|
|
63
|
+
let namespace;
|
|
64
|
+
// Step 1: Command Name
|
|
65
|
+
if (name) {
|
|
66
|
+
commandName = name;
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
const { name: inputName } = await inquirer_1.default.prompt([
|
|
70
|
+
{
|
|
71
|
+
type: 'input',
|
|
72
|
+
name: 'name',
|
|
73
|
+
message: 'Command name (lowercase, numbers, hyphens only):',
|
|
74
|
+
validate: (input) => {
|
|
75
|
+
if (!input)
|
|
76
|
+
return 'Name is required';
|
|
77
|
+
if (!/^[a-z0-9-]+$/.test(input)) {
|
|
78
|
+
return 'Name must contain only lowercase letters, numbers, and hyphens';
|
|
79
|
+
}
|
|
80
|
+
if (input.length > 64)
|
|
81
|
+
return 'Name must be 64 characters or less';
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
]);
|
|
86
|
+
commandName = inputName;
|
|
87
|
+
}
|
|
88
|
+
// Step 2: Command Type
|
|
89
|
+
const { type } = await inquirer_1.default.prompt([
|
|
90
|
+
{
|
|
91
|
+
type: 'list',
|
|
92
|
+
name: 'type',
|
|
93
|
+
message: 'Command type:',
|
|
94
|
+
choices: [
|
|
95
|
+
{ name: 'Personal (available in all projects)', value: 'personal' },
|
|
96
|
+
{ name: 'Project (shared with team via git)', value: 'project' }
|
|
97
|
+
],
|
|
98
|
+
default: 'personal'
|
|
99
|
+
}
|
|
100
|
+
]);
|
|
101
|
+
commandType = type;
|
|
102
|
+
// Step 3: Namespace (optional, for grouping)
|
|
103
|
+
const { useNamespace } = await inquirer_1.default.prompt([
|
|
104
|
+
{
|
|
105
|
+
type: 'confirm',
|
|
106
|
+
name: 'useNamespace',
|
|
107
|
+
message: 'Add a namespace for grouping?',
|
|
108
|
+
default: false
|
|
109
|
+
}
|
|
110
|
+
]);
|
|
111
|
+
if (useNamespace) {
|
|
112
|
+
const { ns } = await inquirer_1.default.prompt([
|
|
113
|
+
{
|
|
114
|
+
type: 'input',
|
|
115
|
+
name: 'ns',
|
|
116
|
+
message: 'Namespace (e.g., "git", "review", "test"):',
|
|
117
|
+
validate: (input) => {
|
|
118
|
+
if (!input)
|
|
119
|
+
return 'Namespace is required';
|
|
120
|
+
if (!/^[a-z0-9-]+$/.test(input)) {
|
|
121
|
+
return 'Namespace must contain only lowercase letters, numbers, and hyphens';
|
|
122
|
+
}
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
]);
|
|
127
|
+
namespace = ns;
|
|
128
|
+
}
|
|
129
|
+
// Step 4: Description
|
|
130
|
+
const { desc } = await inquirer_1.default.prompt([
|
|
131
|
+
{
|
|
132
|
+
type: 'input',
|
|
133
|
+
name: 'desc',
|
|
134
|
+
message: 'Description:',
|
|
135
|
+
validate: (input) => {
|
|
136
|
+
if (!input)
|
|
137
|
+
return 'Description is required';
|
|
138
|
+
if (input.length > 1024)
|
|
139
|
+
return 'Description must be 1024 characters or less';
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
]);
|
|
144
|
+
description = desc;
|
|
145
|
+
// Step 5: Arguments (optional)
|
|
146
|
+
const { useArgs } = await inquirer_1.default.prompt([
|
|
147
|
+
{
|
|
148
|
+
type: 'confirm',
|
|
149
|
+
name: 'useArgs',
|
|
150
|
+
message: 'Does this command accept arguments?',
|
|
151
|
+
default: false
|
|
152
|
+
}
|
|
153
|
+
]);
|
|
154
|
+
if (useArgs) {
|
|
155
|
+
const { hint } = await inquirer_1.default.prompt([
|
|
156
|
+
{
|
|
157
|
+
type: 'input',
|
|
158
|
+
name: 'hint',
|
|
159
|
+
message: 'Argument hint (e.g., "<file>", "[options]"):',
|
|
160
|
+
validate: (input) => {
|
|
161
|
+
if (!input)
|
|
162
|
+
return 'Argument hint is required';
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
]);
|
|
167
|
+
argumentHint = hint;
|
|
168
|
+
}
|
|
169
|
+
// Step 6: Allowed Tools (optional)
|
|
170
|
+
const { useAllowedTools } = await inquirer_1.default.prompt([
|
|
171
|
+
{
|
|
172
|
+
type: 'confirm',
|
|
173
|
+
name: 'useAllowedTools',
|
|
174
|
+
message: 'Restrict which tools this command can use?',
|
|
175
|
+
default: false
|
|
176
|
+
}
|
|
177
|
+
]);
|
|
178
|
+
if (useAllowedTools) {
|
|
179
|
+
const { tools } = await inquirer_1.default.prompt([
|
|
180
|
+
{
|
|
181
|
+
type: 'checkbox',
|
|
182
|
+
name: 'tools',
|
|
183
|
+
message: 'Select allowed tools:',
|
|
184
|
+
choices: [
|
|
185
|
+
{ name: 'Read (read_file)', value: 'Read' },
|
|
186
|
+
{ name: 'Write (write_file)', value: 'Write' },
|
|
187
|
+
{ name: 'Edit (edit_file)', value: 'Edit' },
|
|
188
|
+
{ name: 'Grep (search files)', value: 'Grep' },
|
|
189
|
+
{ name: 'Glob (find files)', value: 'Glob' },
|
|
190
|
+
{ name: 'ListDir (list directory)', value: 'ListDir' },
|
|
191
|
+
{ name: 'SearchFile (search in files)', value: 'SearchFile' },
|
|
192
|
+
{ name: 'RunShell (run shell command)', value: 'RunShell' },
|
|
193
|
+
{ name: 'WebSearch (web search)', value: 'WebSearch' },
|
|
194
|
+
{ name: 'GitStatus', value: 'GitStatus' },
|
|
195
|
+
{ name: 'GitDiff', value: 'GitDiff' },
|
|
196
|
+
{ name: 'GitCommit', value: 'GitCommit' },
|
|
197
|
+
{ name: 'GitPush', value: 'GitPush' },
|
|
198
|
+
{ name: 'GitPull', value: 'GitPull' }
|
|
199
|
+
]
|
|
200
|
+
}
|
|
201
|
+
]);
|
|
202
|
+
allowedTools = tools.length > 0 ? tools : undefined;
|
|
203
|
+
}
|
|
204
|
+
// Step 7: Create the command
|
|
205
|
+
return this.createCommand(commandName, commandType, description, allowedTools, argumentHint, namespace);
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Create the command file
|
|
209
|
+
*/
|
|
210
|
+
async createCommand(name, type, description, allowedTools, argumentHint, namespace) {
|
|
211
|
+
const baseDir = type === 'personal'
|
|
212
|
+
? path.join(os.homedir(), '.mentis', 'commands')
|
|
213
|
+
: path.join(process.cwd(), '.mentis', 'commands');
|
|
214
|
+
const commandDir = namespace ? path.join(baseDir, namespace) : baseDir;
|
|
215
|
+
const commandFile = path.join(commandDir, `${name}.md`);
|
|
216
|
+
// Check if command already exists
|
|
217
|
+
if (fs.existsSync(commandFile)) {
|
|
218
|
+
const { overwrite } = await inquirer_1.default.prompt([
|
|
219
|
+
{
|
|
220
|
+
type: 'confirm',
|
|
221
|
+
name: 'overwrite',
|
|
222
|
+
message: `Command "${name}" already exists. Overwrite?`,
|
|
223
|
+
default: false
|
|
224
|
+
}
|
|
225
|
+
]);
|
|
226
|
+
if (!overwrite) {
|
|
227
|
+
console.log('Cancelled.');
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// Create directory
|
|
232
|
+
if (!fs.existsSync(commandDir)) {
|
|
233
|
+
fs.mkdirSync(commandDir, { recursive: true });
|
|
234
|
+
}
|
|
235
|
+
// Generate command content
|
|
236
|
+
let content = `---\ndescription: ${description}\n`;
|
|
237
|
+
if (allowedTools && allowedTools.length > 0) {
|
|
238
|
+
content += `allowed-tools: [${allowedTools.map(t => `"${t}"`).join(', ')}]\n`;
|
|
239
|
+
}
|
|
240
|
+
if (argumentHint) {
|
|
241
|
+
content += `argument-hint: "${argumentHint}"\n`;
|
|
242
|
+
}
|
|
243
|
+
content += `---\n\n`;
|
|
244
|
+
// Add usage instructions in markdown
|
|
245
|
+
content += `## Usage\n\n`;
|
|
246
|
+
content += `Use this command by typing: /${name}${argumentHint ? ` ${argumentHint}` : ''}\n\n`;
|
|
247
|
+
content += `## Instructions\n\n`;
|
|
248
|
+
content += `Add your instructions here. You can use:\n`;
|
|
249
|
+
content += `- \`$1\`, \`$2\`, etc. for positional arguments\n`;
|
|
250
|
+
content += `- \`$ARGUMENTS\` for all arguments\n`;
|
|
251
|
+
content += `- \`\!\\\`command\`\` for bash commands\n`;
|
|
252
|
+
content += `- \`@file\` for file references\n\n`;
|
|
253
|
+
// Write command file
|
|
254
|
+
fs.writeFileSync(commandFile, content, 'utf-8');
|
|
255
|
+
console.log(`\nā Command created at: ${commandFile}`);
|
|
256
|
+
console.log(`\nNext steps:`);
|
|
257
|
+
console.log(` 1. Edit ${commandFile} to add instructions`);
|
|
258
|
+
console.log(` 2. Restart Mentis or use /commands validate to load the new command`);
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
exports.CommandCreator = CommandCreator;
|
|
263
|
+
/**
|
|
264
|
+
* Validate commands and show results
|
|
265
|
+
*/
|
|
266
|
+
async function validateCommands(commandManager) {
|
|
267
|
+
const commands = commandManager.getAllCommands();
|
|
268
|
+
console.log('\nš Command Validation Results\n');
|
|
269
|
+
if (commands.length === 0) {
|
|
270
|
+
console.log('No custom commands to validate.');
|
|
271
|
+
console.log('Create commands with: /commands create');
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
for (const cmd of commands) {
|
|
275
|
+
const isValid = cmd.name && cmd.name.length > 0 && cmd.content.length > 0;
|
|
276
|
+
const icon = isValid ? 'ā' : 'ā';
|
|
277
|
+
console.log(`${icon} /${cmd.name} (${cmd.type})`);
|
|
278
|
+
if (!isValid) {
|
|
279
|
+
console.log(` ERROR: Invalid command structure`);
|
|
280
|
+
}
|
|
281
|
+
if (cmd.frontmatter['allowed-tools'] && cmd.frontmatter['allowed-tools'].length > 0) {
|
|
282
|
+
console.log(` Allowed tools: ${cmd.frontmatter['allowed-tools'].join(', ')}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
console.log(`\nā Validated ${commands.length} commands`);
|
|
286
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* CommandManager - Discover, parse, and execute custom slash commands
|
|
4
|
+
*/
|
|
5
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
6
|
+
if (k2 === undefined) k2 = k;
|
|
7
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
8
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
9
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
10
|
+
}
|
|
11
|
+
Object.defineProperty(o, k2, desc);
|
|
12
|
+
}) : (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
o[k2] = m[k];
|
|
15
|
+
}));
|
|
16
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
17
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
18
|
+
}) : function(o, v) {
|
|
19
|
+
o["default"] = v;
|
|
20
|
+
});
|
|
21
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
22
|
+
var ownKeys = function(o) {
|
|
23
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
24
|
+
var ar = [];
|
|
25
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
26
|
+
return ar;
|
|
27
|
+
};
|
|
28
|
+
return ownKeys(o);
|
|
29
|
+
};
|
|
30
|
+
return function (mod) {
|
|
31
|
+
if (mod && mod.__esModule) return mod;
|
|
32
|
+
var result = {};
|
|
33
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
34
|
+
__setModuleDefault(result, mod);
|
|
35
|
+
return result;
|
|
36
|
+
};
|
|
37
|
+
})();
|
|
38
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
39
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
40
|
+
};
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.CommandManager = void 0;
|
|
43
|
+
const fs = __importStar(require("fs"));
|
|
44
|
+
const path = __importStar(require("path"));
|
|
45
|
+
const os = __importStar(require("os"));
|
|
46
|
+
const fast_glob_1 = require("fast-glob");
|
|
47
|
+
const yaml_1 = __importDefault(require("yaml"));
|
|
48
|
+
class CommandManager {
|
|
49
|
+
constructor(cwd = process.cwd()) {
|
|
50
|
+
this.commands = new Map();
|
|
51
|
+
this.personalCommandsDir = path.join(os.homedir(), '.mentis', 'commands');
|
|
52
|
+
this.projectCommandsDir = path.join(cwd, '.mentis', 'commands');
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Discover all commands from configured directories
|
|
56
|
+
*/
|
|
57
|
+
async discoverCommands() {
|
|
58
|
+
const discovered = [];
|
|
59
|
+
// Personal commands
|
|
60
|
+
discovered.push(...await this.discoverCommandsInDirectory(this.personalCommandsDir, 'personal'));
|
|
61
|
+
// Project commands
|
|
62
|
+
discovered.push(...await this.discoverCommandsInDirectory(this.projectCommandsDir, 'project'));
|
|
63
|
+
// Store commands in map (project commands override personal)
|
|
64
|
+
for (const command of discovered) {
|
|
65
|
+
this.commands.set(command.name, command);
|
|
66
|
+
}
|
|
67
|
+
return Array.from(this.commands.values());
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Discover commands in a specific directory
|
|
71
|
+
*/
|
|
72
|
+
async discoverCommandsInDirectory(dir, type) {
|
|
73
|
+
if (!fs.existsSync(dir)) {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
const commands = [];
|
|
77
|
+
try {
|
|
78
|
+
// Find all .md files in subdirectories
|
|
79
|
+
const commandFiles = await (0, fast_glob_1.glob)('**/*.md', {
|
|
80
|
+
cwd: dir,
|
|
81
|
+
absolute: true,
|
|
82
|
+
onlyFiles: true
|
|
83
|
+
});
|
|
84
|
+
for (const commandFile of commandFiles) {
|
|
85
|
+
const command = await this.parseCommandFile(commandFile, type);
|
|
86
|
+
if (command) {
|
|
87
|
+
commands.push(command);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
console.error(`Error discovering commands in ${dir}: ${error.message}`);
|
|
93
|
+
}
|
|
94
|
+
return commands;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Parse a command file
|
|
98
|
+
*/
|
|
99
|
+
async parseCommandFile(commandPath, type) {
|
|
100
|
+
try {
|
|
101
|
+
const content = fs.readFileSync(commandPath, 'utf-8');
|
|
102
|
+
const frontmatter = this.extractFrontmatter(content);
|
|
103
|
+
const commandName = this.getCommandName(commandPath, type);
|
|
104
|
+
if (!commandName) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
// Get namespace (subdirectory)
|
|
108
|
+
const relativePath = path.relative(type === 'personal' ? this.personalCommandsDir : this.projectCommandsDir, commandPath);
|
|
109
|
+
const namespace = path.dirname(relativePath) !== '.' ? path.dirname(relativePath) : '';
|
|
110
|
+
const description = frontmatter.description || this.extractFirstLine(content);
|
|
111
|
+
const command = {
|
|
112
|
+
name: commandName,
|
|
113
|
+
type,
|
|
114
|
+
path: commandPath,
|
|
115
|
+
directory: path.dirname(commandPath),
|
|
116
|
+
frontmatter,
|
|
117
|
+
content: content,
|
|
118
|
+
description: namespace ? `${description} (${type}:${namespace})` : description,
|
|
119
|
+
hasParameters: content.includes('$1') || content.includes('$ARGUMENTS')
|
|
120
|
+
};
|
|
121
|
+
return command;
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
console.error(`Error parsing command ${commandPath}: ${error.message}`);
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Get command name from file path
|
|
130
|
+
*/
|
|
131
|
+
getCommandName(commandPath, type) {
|
|
132
|
+
const relativePath = path.relative(type === 'personal' ? this.personalCommandsDir : this.projectCommandsDir, commandPath);
|
|
133
|
+
const nameWithExt = path.basename(relativePath); // e.g., "review.md"
|
|
134
|
+
const name = nameWithExt.replace(/\.md$/, ''); // e.g., "review"
|
|
135
|
+
if (!name || name.startsWith('.')) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
return name;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Extract YAML frontmatter from markdown content
|
|
142
|
+
*/
|
|
143
|
+
extractFrontmatter(content) {
|
|
144
|
+
const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n/;
|
|
145
|
+
const match = content.match(frontmatterRegex);
|
|
146
|
+
if (!match) {
|
|
147
|
+
return {};
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
const parsed = yaml_1.default.parse(match[1]);
|
|
151
|
+
return parsed;
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
return {};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Extract first line of content as description
|
|
159
|
+
*/
|
|
160
|
+
extractFirstLine(content) {
|
|
161
|
+
// Remove frontmatter
|
|
162
|
+
const withoutFrontmatter = content.replace(/^---\s*\n[\s\S]*?\n---\s*\n/, '');
|
|
163
|
+
// Get first non-empty line
|
|
164
|
+
const lines = withoutFrontmatter.split('\n');
|
|
165
|
+
for (const line of lines) {
|
|
166
|
+
const trimmed = line.trim();
|
|
167
|
+
if (trimmed && !trimmed.startsWith('#')) {
|
|
168
|
+
return trimmed;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return 'No description';
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Get command by name
|
|
175
|
+
*/
|
|
176
|
+
getCommand(name) {
|
|
177
|
+
return this.commands.get(name);
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Get all commands
|
|
181
|
+
*/
|
|
182
|
+
getAllCommands() {
|
|
183
|
+
return Array.from(this.commands.values());
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Get commands context for system prompt injection
|
|
187
|
+
*/
|
|
188
|
+
getCommandsContext() {
|
|
189
|
+
const commands = this.getAllCommands();
|
|
190
|
+
if (commands.length === 0) {
|
|
191
|
+
return '';
|
|
192
|
+
}
|
|
193
|
+
const context = commands.map(cmd => {
|
|
194
|
+
let line = `/${cmd.name}`;
|
|
195
|
+
if (cmd.frontmatter['argument-hint']) {
|
|
196
|
+
line += ` ${cmd.frontmatter['argument-hint']}`;
|
|
197
|
+
}
|
|
198
|
+
line += `: ${cmd.description.replace(/\s*\([^)]+\)/, '')}`;
|
|
199
|
+
return line;
|
|
200
|
+
}).join('\n');
|
|
201
|
+
return `Available Custom Commands:\n${context}`;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Parse command content and execute substitutions
|
|
205
|
+
*/
|
|
206
|
+
async parseCommand(command, args) {
|
|
207
|
+
let content = command.content;
|
|
208
|
+
const bashCommands = [];
|
|
209
|
+
const fileReferences = [];
|
|
210
|
+
// Remove frontmatter from content
|
|
211
|
+
content = content.replace(/^---\s*\n[\s\S]*?\n---\s*\n/, '');
|
|
212
|
+
// Substitute $1, $2, etc.
|
|
213
|
+
content = this.substitutePositionalArgs(content, args);
|
|
214
|
+
// Substitute $ARGUMENTS
|
|
215
|
+
content = this.substituteAllArgs(content, args);
|
|
216
|
+
// Extract and collect bash commands (!`cmd`)
|
|
217
|
+
const bashRegex = /!`([^`]+)`/g;
|
|
218
|
+
let bashMatch;
|
|
219
|
+
while ((bashMatch = bashRegex.exec(content)) !== null) {
|
|
220
|
+
bashCommands.push(bashMatch[1]);
|
|
221
|
+
}
|
|
222
|
+
// Remove bash command markers
|
|
223
|
+
content = content.replace(/!`[^`]+`/g, '[BASH_OUTPUT]');
|
|
224
|
+
// Extract and collect file references (@file)
|
|
225
|
+
const fileRegex = /@([^\s]+)/g;
|
|
226
|
+
let fileMatch;
|
|
227
|
+
while ((fileMatch = fileRegex.exec(content)) !== null) {
|
|
228
|
+
fileReferences.push(fileMatch[1]);
|
|
229
|
+
}
|
|
230
|
+
return { content, bashCommands, fileReferences };
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Substitute positional arguments ($1, $2, etc.)
|
|
234
|
+
*/
|
|
235
|
+
substitutePositionalArgs(content, args) {
|
|
236
|
+
return content.replace(/\$(\d+)/g, (match, index) => {
|
|
237
|
+
const argIndex = parseInt(index) - 1;
|
|
238
|
+
return args[argIndex] || '';
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Substitute $ARGUMENTS placeholder
|
|
243
|
+
*/
|
|
244
|
+
substituteAllArgs(content, args) {
|
|
245
|
+
return content.replace(/\$ARGUMENTS/g, args.join(' '));
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Ensure commands directories exist
|
|
249
|
+
*/
|
|
250
|
+
ensureDirectoriesExist() {
|
|
251
|
+
if (!fs.existsSync(this.personalCommandsDir)) {
|
|
252
|
+
fs.mkdirSync(this.personalCommandsDir, { recursive: true });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Get personal commands directory path
|
|
257
|
+
*/
|
|
258
|
+
getPersonalCommandsDir() {
|
|
259
|
+
return this.personalCommandsDir;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Get project commands directory path
|
|
263
|
+
*/
|
|
264
|
+
getProjectCommandsDir() {
|
|
265
|
+
return this.projectCommandsDir;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
exports.CommandManager = CommandManager;
|