@indiccoder/mentis-cli 1.0.8 → 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 +245 -3
- package/dist/skills/LoadSkillTool.js +133 -0
- package/dist/skills/Skill.js +6 -0
- package/dist/skills/SkillCreator.js +247 -0
- package/dist/skills/SkillsManager.js +337 -0
- 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/docs/SKILLS.md +319 -0
- package/examples/skills/code-reviewer/SKILL.md +88 -0
- package/examples/skills/commit-helper/SKILL.md +66 -0
- package/examples/skills/pdf-processing/SKILL.md +108 -0
- package/package.json +4 -3
- 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 +302 -3
- package/src/skills/LoadSkillTool.ts +168 -0
- package/src/skills/Skill.ts +51 -0
- package/src/skills/SkillCreator.ts +237 -0
- package/src/skills/SkillsManager.ts +354 -0
- 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
package/src/repl/ReplManager.ts
CHANGED
|
@@ -17,6 +17,13 @@ import { Tool } from '../tools/Tool';
|
|
|
17
17
|
import { McpClient } from '../mcp/McpClient';
|
|
18
18
|
|
|
19
19
|
import { CheckpointManager } from '../checkpoint/CheckpointManager';
|
|
20
|
+
import { SkillsManager } from '../skills/SkillsManager';
|
|
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';
|
|
20
27
|
import * as readline from 'readline';
|
|
21
28
|
import * as fs from 'fs';
|
|
22
29
|
import * as path from 'path';
|
|
@@ -31,18 +38,29 @@ export class ReplManager {
|
|
|
31
38
|
private modelClient!: ModelClient;
|
|
32
39
|
private contextManager: ContextManager;
|
|
33
40
|
private checkpointManager: CheckpointManager;
|
|
41
|
+
private skillsManager: SkillsManager;
|
|
42
|
+
private contextVisualizer: ContextVisualizer;
|
|
43
|
+
private conversationCompacter: ConversationCompacter;
|
|
44
|
+
private commandManager: CommandManager;
|
|
34
45
|
private history: ChatMessage[] = [];
|
|
35
46
|
private mode: 'PLAN' | 'BUILD' = 'BUILD';
|
|
36
47
|
private tools: Tool[] = [];
|
|
37
48
|
private mcpClients: McpClient[] = [];
|
|
38
49
|
private shell: PersistentShell;
|
|
39
50
|
private currentModelName: string = 'Unknown';
|
|
51
|
+
private activeSkill: string | null = null; // Track currently active skill for allowed-tools
|
|
40
52
|
|
|
41
53
|
constructor() {
|
|
42
54
|
this.configManager = new ConfigManager();
|
|
43
55
|
this.contextManager = new ContextManager();
|
|
44
56
|
this.checkpointManager = new CheckpointManager();
|
|
57
|
+
this.skillsManager = new SkillsManager();
|
|
58
|
+
this.contextVisualizer = new ContextVisualizer();
|
|
59
|
+
this.conversationCompacter = new ConversationCompacter();
|
|
60
|
+
this.commandManager = new CommandManager();
|
|
45
61
|
this.shell = new PersistentShell();
|
|
62
|
+
|
|
63
|
+
// Create tools array without skill tools first
|
|
46
64
|
this.tools = [
|
|
47
65
|
new WriteFileTool(),
|
|
48
66
|
new ReadFileTool(),
|
|
@@ -64,6 +82,76 @@ export class ReplManager {
|
|
|
64
82
|
});
|
|
65
83
|
// Default to Ollama if not specified, assuming compatible endpoint
|
|
66
84
|
this.initializeClient();
|
|
85
|
+
|
|
86
|
+
// Initialize skills system after client is ready
|
|
87
|
+
this.initializeSkills();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Initialize the skills and custom commands system
|
|
92
|
+
*/
|
|
93
|
+
private async initializeSkills() {
|
|
94
|
+
// Initialize skills
|
|
95
|
+
this.skillsManager.ensureDirectoriesExist();
|
|
96
|
+
await this.skillsManager.discoverSkills();
|
|
97
|
+
|
|
98
|
+
// Initialize custom commands
|
|
99
|
+
this.commandManager.ensureDirectoriesExist();
|
|
100
|
+
await this.commandManager.discoverCommands();
|
|
101
|
+
|
|
102
|
+
// Add skill tools to the tools list
|
|
103
|
+
// Pass callback to LoadSkillTool to track active skill
|
|
104
|
+
this.tools.push(
|
|
105
|
+
new LoadSkillTool(this.skillsManager, (skill) => {
|
|
106
|
+
this.activeSkill = skill ? skill.name : null;
|
|
107
|
+
}),
|
|
108
|
+
new ListSkillsTool(this.skillsManager),
|
|
109
|
+
new ReadSkillFileTool(this.skillsManager),
|
|
110
|
+
new SlashCommandTool(this.commandManager),
|
|
111
|
+
new ListCommandsTool(this.commandManager)
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Check if a tool is allowed by the currently active skill
|
|
117
|
+
* Returns true if tool is allowed, false if it requires confirmation
|
|
118
|
+
*/
|
|
119
|
+
private isToolAllowedBySkill(toolName: string): boolean {
|
|
120
|
+
if (!this.activeSkill) {
|
|
121
|
+
// No active skill, all tools require confirmation as per normal flow
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const skill = this.skillsManager.getSkill(this.activeSkill);
|
|
126
|
+
if (!skill || !skill.allowedTools || skill.allowedTools.length === 0) {
|
|
127
|
+
// No skill or no allowed-tools restriction
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Map tool names to allowed tool names
|
|
132
|
+
const toolMapping: Record<string, string> = {
|
|
133
|
+
'write_file': 'Write',
|
|
134
|
+
'read_file': 'Read',
|
|
135
|
+
'edit_file': 'Edit',
|
|
136
|
+
'search_files': 'Grep',
|
|
137
|
+
'list_dir': 'ListDir',
|
|
138
|
+
'search_file': 'SearchFile',
|
|
139
|
+
'run_shell': 'RunShell',
|
|
140
|
+
'web_search': 'WebSearch',
|
|
141
|
+
'git_status': 'GitStatus',
|
|
142
|
+
'git_diff': 'GitDiff',
|
|
143
|
+
'git_commit': 'GitCommit',
|
|
144
|
+
'git_push': 'GitPush',
|
|
145
|
+
'git_pull': 'GitPull',
|
|
146
|
+
'load_skill': 'Read',
|
|
147
|
+
'list_skills': 'Read',
|
|
148
|
+
'read_skill_file': 'Read',
|
|
149
|
+
'slash_command': 'Read',
|
|
150
|
+
'list_commands': 'Read'
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const mappedToolName = toolMapping[toolName] || toolName;
|
|
154
|
+
return skill.allowedTools.includes(mappedToolName);
|
|
67
155
|
}
|
|
68
156
|
|
|
69
157
|
private initializeClient() {
|
|
@@ -184,16 +272,17 @@ export class ReplManager {
|
|
|
184
272
|
console.log(' /drop <file> - Remove file from context');
|
|
185
273
|
console.log(' /plan - Switch to PLAN mode');
|
|
186
274
|
console.log(' /build - Switch to BUILD mode');
|
|
187
|
-
console.log(' /plan - Switch to PLAN mode');
|
|
188
|
-
console.log(' /build - Switch to BUILD mode');
|
|
189
275
|
console.log(' /model - Interactively select Provider & Model');
|
|
190
276
|
console.log(' /use <provider> [model] - Quick switch (legacy)');
|
|
191
277
|
console.log(' /mcp <cmd> - Manage MCP servers');
|
|
278
|
+
console.log(' /skills <list|show|create|validate> - Manage Agent Skills');
|
|
279
|
+
console.log(' /commands <list|create|validate> - Manage Custom Commands');
|
|
192
280
|
console.log(' /resume - Resume last session');
|
|
193
281
|
console.log(' /checkpoint <save|load|list> [name] - Manage checkpoints');
|
|
194
282
|
console.log(' /search <query> - Search codebase');
|
|
195
283
|
console.log(' /run <cmd> - Run shell command');
|
|
196
284
|
console.log(' /commit [msg] - Git commit all changes');
|
|
285
|
+
console.log(' /init - Initialize project with .mentis.md');
|
|
197
286
|
break;
|
|
198
287
|
case '/plan':
|
|
199
288
|
this.mode = 'PLAN';
|
|
@@ -263,6 +352,15 @@ export class ReplManager {
|
|
|
263
352
|
const updater = new UpdateManager();
|
|
264
353
|
await updater.checkAndPerformUpdate(true);
|
|
265
354
|
break;
|
|
355
|
+
case '/init':
|
|
356
|
+
await this.handleInitCommand();
|
|
357
|
+
break;
|
|
358
|
+
case '/skills':
|
|
359
|
+
await this.handleSkillsCommand(args);
|
|
360
|
+
break;
|
|
361
|
+
case '/commands':
|
|
362
|
+
await this.handleCommandsCommand(args);
|
|
363
|
+
break;
|
|
266
364
|
default:
|
|
267
365
|
console.log(chalk.red(`Unknown command: ${command}`));
|
|
268
366
|
}
|
|
@@ -270,6 +368,8 @@ export class ReplManager {
|
|
|
270
368
|
|
|
271
369
|
private async handleChat(input: string) {
|
|
272
370
|
const context = this.contextManager.getContextString();
|
|
371
|
+
const skillsContext = this.skillsManager.getSkillsContext();
|
|
372
|
+
const commandsContext = this.commandManager.getCommandsContext();
|
|
273
373
|
let fullInput = input;
|
|
274
374
|
|
|
275
375
|
let modeInstruction = '';
|
|
@@ -281,6 +381,16 @@ export class ReplManager {
|
|
|
281
381
|
|
|
282
382
|
fullInput = `${input}${modeInstruction}`;
|
|
283
383
|
|
|
384
|
+
// Add skills context if available
|
|
385
|
+
if (skillsContext) {
|
|
386
|
+
fullInput = `${skillsContext}\n\n${fullInput}`;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Add commands context if available
|
|
390
|
+
if (commandsContext) {
|
|
391
|
+
fullInput = `${commandsContext}\n\n${fullInput}`;
|
|
392
|
+
}
|
|
393
|
+
|
|
284
394
|
if (context) {
|
|
285
395
|
fullInput = `${context}\n\nUser Question: ${fullInput}`;
|
|
286
396
|
}
|
|
@@ -343,7 +453,8 @@ export class ReplManager {
|
|
|
343
453
|
console.log(chalk.dim(` [Action] ${toolName}(${displayArgs})`));
|
|
344
454
|
|
|
345
455
|
// Safety check for write_file
|
|
346
|
-
if
|
|
456
|
+
// Skip confirmation if tool is allowed by active skill
|
|
457
|
+
if (toolName === 'write_file' && !this.isToolAllowedBySkill('Write')) {
|
|
347
458
|
// Pause cancellation listener during user interaction
|
|
348
459
|
if (process.stdin.isTTY) {
|
|
349
460
|
process.stdin.removeListener('keypress', keyListener);
|
|
@@ -441,8 +552,22 @@ export class ReplManager {
|
|
|
441
552
|
console.log(chalk.dim(`\n(Tokens: ${input_tokens} in / ${output_tokens} out | Est. Cost: $${totalCost.toFixed(5)})`));
|
|
442
553
|
}
|
|
443
554
|
|
|
555
|
+
// Display context bar
|
|
556
|
+
const contextBar = this.contextVisualizer.getContextBar(this.history);
|
|
557
|
+
console.log(chalk.dim(`\n${contextBar}`));
|
|
558
|
+
|
|
444
559
|
console.log('');
|
|
445
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
|
+
}
|
|
446
571
|
}
|
|
447
572
|
} catch (error: any) {
|
|
448
573
|
spinner.stop();
|
|
@@ -863,6 +988,180 @@ export class ReplManager {
|
|
|
863
988
|
}
|
|
864
989
|
}
|
|
865
990
|
|
|
991
|
+
private async handleSkillsCommand(args: string[]) {
|
|
992
|
+
const { SkillCreator, validateSkills } = await import('../skills/SkillCreator');
|
|
993
|
+
|
|
994
|
+
if (args.length < 1) {
|
|
995
|
+
// Show skills list by default
|
|
996
|
+
await this.handleSkillsCommand(['list']);
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
const action = args[0];
|
|
1001
|
+
|
|
1002
|
+
switch (action) {
|
|
1003
|
+
case 'list':
|
|
1004
|
+
await this.handleSkillsList();
|
|
1005
|
+
break;
|
|
1006
|
+
case 'show':
|
|
1007
|
+
if (args.length < 2) {
|
|
1008
|
+
console.log(chalk.red('Usage: /skills show <name>'));
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
await this.handleSkillsShow(args[1]);
|
|
1012
|
+
break;
|
|
1013
|
+
case 'create':
|
|
1014
|
+
const creator = new SkillCreator(this.skillsManager);
|
|
1015
|
+
await creator.run(args[1]);
|
|
1016
|
+
// Re-discover skills after creation
|
|
1017
|
+
await this.skillsManager.discoverSkills();
|
|
1018
|
+
break;
|
|
1019
|
+
case 'validate':
|
|
1020
|
+
await validateSkills(this.skillsManager);
|
|
1021
|
+
break;
|
|
1022
|
+
default:
|
|
1023
|
+
console.log(chalk.red(`Unknown skills action: ${action}`));
|
|
1024
|
+
console.log(chalk.yellow('Available actions: list, show, create, validate'));
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
private async handleSkillsList(): Promise<void> {
|
|
1029
|
+
const skills = this.skillsManager.getAllSkills();
|
|
1030
|
+
|
|
1031
|
+
if (skills.length === 0) {
|
|
1032
|
+
console.log(chalk.yellow('No skills available.'));
|
|
1033
|
+
console.log(chalk.dim('Create skills with: /skills create'));
|
|
1034
|
+
console.log(chalk.dim('Add skills to: ~/.mentis/skills/ or .mentis/skills/'));
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
console.log(chalk.cyan(`\nAvailable Skills (${skills.length}):\n`));
|
|
1039
|
+
|
|
1040
|
+
for (const skill of skills) {
|
|
1041
|
+
const statusIcon = skill.isValid ? '✓' : '✗';
|
|
1042
|
+
const typeLabel = skill.type === 'personal' ? 'Personal' : 'Project';
|
|
1043
|
+
|
|
1044
|
+
console.log(`${statusIcon} ${chalk.bold(skill.name)} (${typeLabel})`);
|
|
1045
|
+
console.log(` ${skill.description}`);
|
|
1046
|
+
|
|
1047
|
+
if (skill.allowedTools && skill.allowedTools.length > 0) {
|
|
1048
|
+
console.log(chalk.dim(` Allowed tools: ${skill.allowedTools.join(', ')}`));
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
if (!skill.isValid && skill.errors) {
|
|
1052
|
+
console.log(chalk.red(` Errors: ${skill.errors.join(', ')}`));
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
console.log('');
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
private async handleSkillsShow(name: string): Promise<void> {
|
|
1060
|
+
const skill = await this.skillsManager.loadFullSkill(name);
|
|
1061
|
+
|
|
1062
|
+
if (!skill) {
|
|
1063
|
+
console.log(chalk.red(`Skill "${name}" not found.`));
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
console.log(chalk.cyan(`\n# ${skill.name}\n`));
|
|
1068
|
+
console.log(chalk.dim(`Type: ${skill.type}`));
|
|
1069
|
+
console.log(chalk.dim(`Path: ${skill.path}`));
|
|
1070
|
+
|
|
1071
|
+
if (skill.allowedTools && skill.allowedTools.length > 0) {
|
|
1072
|
+
console.log(chalk.dim(`Allowed tools: ${skill.allowedTools.join(', ')}`));
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
console.log('');
|
|
1076
|
+
console.log(skill.content || 'No content available');
|
|
1077
|
+
|
|
1078
|
+
// List supporting files
|
|
1079
|
+
const files = this.skillsManager.listSkillFiles(name);
|
|
1080
|
+
if (files.length > 0) {
|
|
1081
|
+
console.log(chalk.dim(`\nSupporting files: ${files.join(', ')}`));
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
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
|
+
|
|
866
1165
|
private estimateCost(input: number, output: number): number {
|
|
867
1166
|
const config = this.configManager.getConfig();
|
|
868
1167
|
const provider = config.defaultProvider;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LoadSkillTool - Tool for model-invoked skill loading
|
|
3
|
+
* The model can call this tool when it determines a skill is relevant to the current task
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Tool } from '../tools/Tool';
|
|
7
|
+
import { SkillsManager, Skill } from './SkillsManager';
|
|
8
|
+
|
|
9
|
+
interface LoadSkillArgs {
|
|
10
|
+
name: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type SkillLoadedCallback = (skill: Skill | null) => void;
|
|
14
|
+
|
|
15
|
+
export class LoadSkillTool implements Tool {
|
|
16
|
+
name = 'load_skill';
|
|
17
|
+
description = 'Load the full content of a skill by name. Use this when you need detailed instructions from a skill. Available skills can be seen in the system prompt. Example: load_skill({ name: "commit-helper" })';
|
|
18
|
+
parameters = {
|
|
19
|
+
type: 'object',
|
|
20
|
+
properties: {
|
|
21
|
+
name: {
|
|
22
|
+
type: 'string',
|
|
23
|
+
description: 'The name of the skill to load (e.g., "commit-helper", "pdf-processing")'
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
required: ['name']
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
private skillsManager: SkillsManager;
|
|
30
|
+
private onSkillLoaded?: SkillLoadedCallback;
|
|
31
|
+
|
|
32
|
+
constructor(skillsManager: SkillsManager, onSkillLoaded?: SkillLoadedCallback) {
|
|
33
|
+
this.skillsManager = skillsManager;
|
|
34
|
+
this.onSkillLoaded = onSkillLoaded;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async execute(args: LoadSkillArgs): Promise<string> {
|
|
38
|
+
const { name } = args;
|
|
39
|
+
|
|
40
|
+
if (!name) {
|
|
41
|
+
return 'Error: Skill name is required';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const skill = this.skillsManager.getSkill(name);
|
|
45
|
+
if (!skill) {
|
|
46
|
+
const availableSkills = this.skillsManager.getAllSkills().map(s => s.name).join(', ');
|
|
47
|
+
return `Error: Skill "${name}" not found. Available skills: ${availableSkills || 'none'}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Load full skill content
|
|
51
|
+
const fullSkill = await this.skillsManager.loadFullSkill(name);
|
|
52
|
+
if (!fullSkill || !fullSkill.content) {
|
|
53
|
+
return `Error: Failed to load content for skill "${name}"`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Notify callback that skill was loaded
|
|
57
|
+
if (this.onSkillLoaded) {
|
|
58
|
+
this.onSkillLoaded(fullSkill);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Format response with skill content
|
|
62
|
+
let response = `# Loaded Skill: ${skill.name}\n\n`;
|
|
63
|
+
response += `**Type**: ${skill.type}\n`;
|
|
64
|
+
response += `**Description**: ${skill.description}\n`;
|
|
65
|
+
if (skill.allowedTools && skill.allowedTools.length > 0) {
|
|
66
|
+
response += `**Allowed Tools**: ${skill.allowedTools.join(', ')}\n`;
|
|
67
|
+
}
|
|
68
|
+
response += `\n---\n\n`;
|
|
69
|
+
response += fullSkill.content;
|
|
70
|
+
|
|
71
|
+
return response;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* ListSkillsTool - Tool for listing available skills
|
|
77
|
+
*/
|
|
78
|
+
export class ListSkillsTool implements Tool {
|
|
79
|
+
name = 'list_skills';
|
|
80
|
+
description = 'List all available skills with their descriptions. Use this to see what skills are available.';
|
|
81
|
+
parameters = {
|
|
82
|
+
type: 'object',
|
|
83
|
+
properties: {},
|
|
84
|
+
required: []
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
private skillsManager: SkillsManager;
|
|
88
|
+
|
|
89
|
+
constructor(skillsManager: SkillsManager) {
|
|
90
|
+
this.skillsManager = skillsManager;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async execute(): Promise<string> {
|
|
94
|
+
const skills = this.skillsManager.getAllSkills();
|
|
95
|
+
|
|
96
|
+
if (skills.length === 0) {
|
|
97
|
+
return 'No skills available. Add skills to ~/.mentis/skills/ or .mentis/skills/';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let response = `# Available Skills (${skills.length})\n\n`;
|
|
101
|
+
|
|
102
|
+
for (const skill of skills) {
|
|
103
|
+
const statusIcon = skill.isValid ? '✓' : '✗';
|
|
104
|
+
response += `**${statusIcon} ${skill.name}** (${skill.type})\n`;
|
|
105
|
+
response += ` ${skill.description}\n`;
|
|
106
|
+
|
|
107
|
+
if (skill.allowedTools && skill.allowedTools.length > 0) {
|
|
108
|
+
response += ` Allowed tools: ${skill.allowedTools.join(', ')}\n`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!skill.isValid && skill.errors) {
|
|
112
|
+
response += ` Errors: ${skill.errors.join(', ')}\n`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
response += '\n';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return response;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* ReadSkillFileTool - Tool for reading supporting files within a skill
|
|
124
|
+
* Used for progressive disclosure of skill resources
|
|
125
|
+
*/
|
|
126
|
+
export class ReadSkillFileTool implements Tool {
|
|
127
|
+
name = 'read_skill_file';
|
|
128
|
+
description = 'Read a supporting file from within a skill directory. Use this when a skill references additional files like [reference.md](reference.md). Example: read_skill_file({ skill: "pdf-processing", file: "reference.md" })';
|
|
129
|
+
parameters = {
|
|
130
|
+
type: 'object',
|
|
131
|
+
properties: {
|
|
132
|
+
skill: {
|
|
133
|
+
type: 'string',
|
|
134
|
+
description: 'The name of the skill'
|
|
135
|
+
},
|
|
136
|
+
file: {
|
|
137
|
+
type: 'string',
|
|
138
|
+
description: 'The filename within the skill directory (e.g., "reference.md", "examples.md")'
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
required: ['skill', 'file']
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
private skillsManager: SkillsManager;
|
|
145
|
+
|
|
146
|
+
constructor(skillsManager: SkillsManager) {
|
|
147
|
+
this.skillsManager = skillsManager;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async execute(args: { skill: string; file: string }): Promise<string> {
|
|
151
|
+
const { skill, file } = args;
|
|
152
|
+
|
|
153
|
+
if (!skill || !file) {
|
|
154
|
+
return 'Error: Both skill and file parameters are required';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const content = this.skillsManager.readSkillFile(skill, file);
|
|
158
|
+
if (content === null) {
|
|
159
|
+
const availableFiles = this.skillsManager.listSkillFiles(skill);
|
|
160
|
+
if (availableFiles.length === 0) {
|
|
161
|
+
return `Error: Skill "${skill}" has no supporting files`;
|
|
162
|
+
}
|
|
163
|
+
return `Error: File "${file}" not found in skill "${skill}". Available files: ${availableFiles.join(', ')}`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return content;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill data structure for Agent Skills system
|
|
3
|
+
* Based on Claude Code's Agent Skills format
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface SkillMetadata {
|
|
7
|
+
name: string; // Lowercase, numbers, hyphens only (max 64 chars)
|
|
8
|
+
description: string; // What it does + when to use it (max 1024 chars)
|
|
9
|
+
allowedTools?: string[]; // Optional tool restrictions
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface Skill extends SkillMetadata {
|
|
13
|
+
path: string; // Path to SKILL.md
|
|
14
|
+
type: 'personal' | 'project' | 'plugin';
|
|
15
|
+
content?: string; // Loaded on demand (progressive disclosure)
|
|
16
|
+
directory: string; // Path to skill directory (for resolving supporting files)
|
|
17
|
+
isValid: boolean; // Whether skill passes validation
|
|
18
|
+
errors?: string[]; // Validation errors if any
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SkillFrontmatter {
|
|
22
|
+
name: string;
|
|
23
|
+
description: string;
|
|
24
|
+
'allowed-tools'?: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Validation result for a skill
|
|
29
|
+
*/
|
|
30
|
+
export interface SkillValidationResult {
|
|
31
|
+
isValid: boolean;
|
|
32
|
+
errors: string[];
|
|
33
|
+
warnings: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Skill context format for model injection
|
|
38
|
+
*/
|
|
39
|
+
export interface SkillContext {
|
|
40
|
+
name: string;
|
|
41
|
+
description: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Options for skill discovery
|
|
46
|
+
*/
|
|
47
|
+
export interface SkillDiscoveryOptions {
|
|
48
|
+
includePersonal?: boolean;
|
|
49
|
+
includeProject?: boolean;
|
|
50
|
+
includePlugin?: boolean;
|
|
51
|
+
}
|