@indiccoder/mentis-cli 1.0.8 ā 1.0.9
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/dist/repl/ReplManager.js +142 -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/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 +3 -2
- package/src/repl/ReplManager.ts +174 -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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SkillCreator - Interactive wizard for creating new skills
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import inquirer from 'inquirer';
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import * as os from 'os';
|
|
9
|
+
import { SkillsManager } from './SkillsManager';
|
|
10
|
+
|
|
11
|
+
export class SkillCreator {
|
|
12
|
+
private skillsManager: SkillsManager;
|
|
13
|
+
|
|
14
|
+
constructor(skillsManager?: SkillsManager) {
|
|
15
|
+
this.skillsManager = skillsManager || new SkillsManager();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Run the interactive skill creation wizard
|
|
20
|
+
*/
|
|
21
|
+
async run(name?: string): Promise<boolean> {
|
|
22
|
+
console.log('\nš Create a new Skill\n');
|
|
23
|
+
|
|
24
|
+
let skillName: string;
|
|
25
|
+
let skillType: 'personal' | 'project';
|
|
26
|
+
let description: string;
|
|
27
|
+
let allowedTools: string[] | undefined;
|
|
28
|
+
|
|
29
|
+
// Step 1: Skill Name
|
|
30
|
+
if (name) {
|
|
31
|
+
skillName = name;
|
|
32
|
+
} else {
|
|
33
|
+
const { name: inputName } = await inquirer.prompt([
|
|
34
|
+
{
|
|
35
|
+
type: 'input',
|
|
36
|
+
name: 'name',
|
|
37
|
+
message: 'Skill name (lowercase, numbers, hyphens only):',
|
|
38
|
+
validate: (input: string) => {
|
|
39
|
+
if (!input) return 'Name is required';
|
|
40
|
+
if (!/^[a-z0-9-]+$/.test(input)) {
|
|
41
|
+
return 'Name must contain only lowercase letters, numbers, and hyphens';
|
|
42
|
+
}
|
|
43
|
+
if (input.length > 64) return 'Name must be 64 characters or less';
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
]);
|
|
48
|
+
skillName = inputName;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Step 2: Skill Type
|
|
52
|
+
const { type } = await inquirer.prompt([
|
|
53
|
+
{
|
|
54
|
+
type: 'list',
|
|
55
|
+
name: 'type',
|
|
56
|
+
message: 'Skill type:',
|
|
57
|
+
choices: [
|
|
58
|
+
{ name: 'Personal (available in all projects)', value: 'personal' },
|
|
59
|
+
{ name: 'Project (shared with team via git)', value: 'project' }
|
|
60
|
+
],
|
|
61
|
+
default: 'personal'
|
|
62
|
+
}
|
|
63
|
+
]);
|
|
64
|
+
skillType = type;
|
|
65
|
+
|
|
66
|
+
// Step 3: Description
|
|
67
|
+
const { desc } = await inquirer.prompt([
|
|
68
|
+
{
|
|
69
|
+
type: 'input',
|
|
70
|
+
name: 'desc',
|
|
71
|
+
message: 'Description (what it does + when to use it):',
|
|
72
|
+
validate: (input: string) => {
|
|
73
|
+
if (!input) return 'Description is required';
|
|
74
|
+
if (input.length > 1024) return 'Description must be 1024 characters or less';
|
|
75
|
+
if (!input.toLowerCase().includes('use when') && !input.toLowerCase().includes('use for')) {
|
|
76
|
+
return 'Tip: Include when to use this skill (e.g., "Use when...")';
|
|
77
|
+
}
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
]);
|
|
82
|
+
description = desc;
|
|
83
|
+
|
|
84
|
+
// Step 4: Allowed Tools (optional)
|
|
85
|
+
const { useAllowedTools } = await inquirer.prompt([
|
|
86
|
+
{
|
|
87
|
+
type: 'confirm',
|
|
88
|
+
name: 'useAllowedTools',
|
|
89
|
+
message: 'Restrict which tools this skill can use?',
|
|
90
|
+
default: false
|
|
91
|
+
}
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
if (useAllowedTools) {
|
|
95
|
+
const { tools } = await inquirer.prompt([
|
|
96
|
+
{
|
|
97
|
+
type: 'checkbox',
|
|
98
|
+
name: 'tools',
|
|
99
|
+
message: 'Select allowed tools:',
|
|
100
|
+
choices: [
|
|
101
|
+
{ name: 'Read (read_file)', value: 'Read' },
|
|
102
|
+
{ name: 'Write (write_file)', value: 'Write' },
|
|
103
|
+
{ name: 'Edit (edit_file)', value: 'Edit' },
|
|
104
|
+
{ name: 'Grep (search files)', value: 'Grep' },
|
|
105
|
+
{ name: 'Glob (find files)', value: 'Glob' },
|
|
106
|
+
{ name: 'ListDir (list directory)', value: 'ListDir' },
|
|
107
|
+
{ name: 'SearchFile (search in files)', value: 'SearchFile' },
|
|
108
|
+
{ name: 'RunShell (run shell command)', value: 'RunShell' },
|
|
109
|
+
{ name: 'WebSearch (web search)', value: 'WebSearch' },
|
|
110
|
+
{ name: 'GitStatus', value: 'GitStatus' },
|
|
111
|
+
{ name: 'GitDiff', value: 'GitDiff' },
|
|
112
|
+
{ name: 'GitCommit', value: 'GitCommit' },
|
|
113
|
+
{ name: 'GitPush', value: 'GitPush' },
|
|
114
|
+
{ name: 'GitPull', value: 'GitPull' }
|
|
115
|
+
]
|
|
116
|
+
}
|
|
117
|
+
]);
|
|
118
|
+
allowedTools = tools.length > 0 ? tools : undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Step 5: Create the skill
|
|
122
|
+
return this.createSkill(skillName, skillType, description, allowedTools);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Create the skill file and directory
|
|
127
|
+
*/
|
|
128
|
+
async createSkill(
|
|
129
|
+
name: string,
|
|
130
|
+
type: 'personal' | 'project',
|
|
131
|
+
description: string,
|
|
132
|
+
allowedTools?: string[]
|
|
133
|
+
): Promise<boolean> {
|
|
134
|
+
const baseDir = type === 'personal'
|
|
135
|
+
? path.join(os.homedir(), '.mentis', 'skills')
|
|
136
|
+
: path.join(process.cwd(), '.mentis', 'skills');
|
|
137
|
+
|
|
138
|
+
const skillDir = path.join(baseDir, name);
|
|
139
|
+
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
140
|
+
|
|
141
|
+
// Check if skill already exists
|
|
142
|
+
if (fs.existsSync(skillFile)) {
|
|
143
|
+
const { overwrite } = await inquirer.prompt([
|
|
144
|
+
{
|
|
145
|
+
type: 'confirm',
|
|
146
|
+
name: 'overwrite',
|
|
147
|
+
message: `Skill "${name}" already exists. Overwrite?`,
|
|
148
|
+
default: false
|
|
149
|
+
}
|
|
150
|
+
]);
|
|
151
|
+
|
|
152
|
+
if (!overwrite) {
|
|
153
|
+
console.log('Cancelled.');
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Create directory
|
|
159
|
+
if (!fs.existsSync(skillDir)) {
|
|
160
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Generate SKILL.md content
|
|
164
|
+
let content = `---\nname: ${name}\ndescription: ${description}\n`;
|
|
165
|
+
|
|
166
|
+
if (allowedTools && allowedTools.length > 0) {
|
|
167
|
+
content += `allowed-tools: [${allowedTools.map(t => `"${t}"`).join(', ')}]\n`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
content += `---\n\n# ${this.formatTitle(name)}\n\n`;
|
|
171
|
+
|
|
172
|
+
content += `## Overview\n\n`;
|
|
173
|
+
content += `This skill provides...\n\n`;
|
|
174
|
+
content += `## Instructions\n\n`;
|
|
175
|
+
content += `### Step 1: ...\n\n`;
|
|
176
|
+
content += `### Step 2: ...\n\n`;
|
|
177
|
+
content += `## Examples\n\n`;
|
|
178
|
+
content += `\`\`\`\n`;
|
|
179
|
+
content += `// Example usage\n`;
|
|
180
|
+
content += `\`\`\`\n`;
|
|
181
|
+
|
|
182
|
+
// Write SKILL.md
|
|
183
|
+
fs.writeFileSync(skillFile, content, 'utf-8');
|
|
184
|
+
|
|
185
|
+
console.log(`\nā Skill created at: ${skillFile}`);
|
|
186
|
+
console.log(`\nNext steps:`);
|
|
187
|
+
console.log(` 1. Edit ${skillFile} to add instructions`);
|
|
188
|
+
console.log(` 2. Add supporting files (reference.md, examples.md, scripts/) as needed`);
|
|
189
|
+
console.log(` 3. Restart Mentis to load the new skill`);
|
|
190
|
+
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Format skill name to title case
|
|
196
|
+
*/
|
|
197
|
+
private formatTitle(name: string): string {
|
|
198
|
+
return name
|
|
199
|
+
.split('-')
|
|
200
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
201
|
+
.join(' ');
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Validate skills and show results
|
|
207
|
+
*/
|
|
208
|
+
export async function validateSkills(skillsManager: SkillsManager): Promise<void> {
|
|
209
|
+
const results = skillsManager.validateAllSkills();
|
|
210
|
+
|
|
211
|
+
console.log('\nš Skill Validation Results\n');
|
|
212
|
+
|
|
213
|
+
let hasErrors = false;
|
|
214
|
+
let hasWarnings = false;
|
|
215
|
+
|
|
216
|
+
for (const [name, result] of results) {
|
|
217
|
+
if (result.isValid) {
|
|
218
|
+
console.log(`ā ${name}`);
|
|
219
|
+
} else {
|
|
220
|
+
console.log(`ā ${name}`);
|
|
221
|
+
hasErrors = true;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (result.errors.length > 0) {
|
|
225
|
+
result.errors.forEach(err => console.log(` ERROR: ${err}`));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (result.warnings.length > 0) {
|
|
229
|
+
hasWarnings = true;
|
|
230
|
+
result.warnings.forEach(warn => console.log(` WARNING: ${warn}`));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (!hasErrors && !hasWarnings) {
|
|
235
|
+
console.log('\nā All skills are valid!');
|
|
236
|
+
}
|
|
237
|
+
}
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SkillsManager - Core module for Agent Skills system
|
|
3
|
+
* Handles discovery, loading, and validation of skills
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import * as os from 'os';
|
|
9
|
+
import { glob } from 'fast-glob';
|
|
10
|
+
import YAML from 'yaml';
|
|
11
|
+
import {
|
|
12
|
+
Skill,
|
|
13
|
+
SkillMetadata,
|
|
14
|
+
SkillFrontmatter,
|
|
15
|
+
SkillValidationResult,
|
|
16
|
+
SkillContext,
|
|
17
|
+
SkillDiscoveryOptions
|
|
18
|
+
} from './Skill';
|
|
19
|
+
|
|
20
|
+
// Re-export Skill for use in other modules
|
|
21
|
+
export type { Skill } from './Skill';
|
|
22
|
+
|
|
23
|
+
export class SkillsManager {
|
|
24
|
+
private skills: Map<string, Skill> = new Map();
|
|
25
|
+
private personalSkillsDir: string;
|
|
26
|
+
private projectSkillsDir: string;
|
|
27
|
+
|
|
28
|
+
constructor(cwd: string = process.cwd()) {
|
|
29
|
+
this.personalSkillsDir = path.join(os.homedir(), '.mentis', 'skills');
|
|
30
|
+
this.projectSkillsDir = path.join(cwd, '.mentis', 'skills');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Discover all skills from configured directories
|
|
35
|
+
*/
|
|
36
|
+
async discoverSkills(options: SkillDiscoveryOptions = {}): Promise<Skill[]> {
|
|
37
|
+
const {
|
|
38
|
+
includePersonal = true,
|
|
39
|
+
includeProject = true,
|
|
40
|
+
includePlugin = false // Plugin skills not implemented yet
|
|
41
|
+
} = options;
|
|
42
|
+
|
|
43
|
+
const discovered: Skill[] = [];
|
|
44
|
+
|
|
45
|
+
if (includePersonal) {
|
|
46
|
+
discovered.push(...await this.discoverSkillsInDirectory(this.personalSkillsDir, 'personal'));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (includeProject) {
|
|
50
|
+
discovered.push(...await this.discoverSkillsInDirectory(this.projectSkillsDir, 'project'));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Store skills in map for quick lookup
|
|
54
|
+
for (const skill of discovered) {
|
|
55
|
+
this.skills.set(skill.name, skill);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return Array.from(this.skills.values());
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Discover skills in a specific directory
|
|
63
|
+
*/
|
|
64
|
+
private async discoverSkillsInDirectory(dir: string, type: 'personal' | 'project' | 'plugin'): Promise<Skill[]> {
|
|
65
|
+
if (!fs.existsSync(dir)) {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const skills: Skill[] = [];
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
// Find all SKILL.md files in subdirectories
|
|
73
|
+
const skillFiles = await glob('**/SKILL.md', {
|
|
74
|
+
cwd: dir,
|
|
75
|
+
absolute: true,
|
|
76
|
+
onlyFiles: true
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
for (const skillFile of skillFiles) {
|
|
80
|
+
const skillDir = path.dirname(skillFile);
|
|
81
|
+
const skillName = path.basename(skillDir);
|
|
82
|
+
|
|
83
|
+
const skill = await this.loadSkillMetadata(skillFile, skillDir, type);
|
|
84
|
+
if (skill) {
|
|
85
|
+
skills.push(skill);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} catch (error: any) {
|
|
89
|
+
console.error(`Error discovering skills in ${dir}: ${error.message}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return skills;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Load only the metadata (YAML frontmatter) from a SKILL.md file
|
|
97
|
+
* This is used for progressive disclosure - only name/description loaded at startup
|
|
98
|
+
*/
|
|
99
|
+
async loadSkillMetadata(skillPath: string, skillDir: string, type: 'personal' | 'project' | 'plugin'): Promise<Skill | null> {
|
|
100
|
+
try {
|
|
101
|
+
const content = fs.readFileSync(skillPath, 'utf-8');
|
|
102
|
+
const frontmatter = this.extractFrontmatter(content);
|
|
103
|
+
|
|
104
|
+
if (!frontmatter) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Convert SkillFrontmatter to SkillMetadata for validation
|
|
109
|
+
const metadata: SkillMetadata = {
|
|
110
|
+
name: frontmatter.name,
|
|
111
|
+
description: frontmatter.description,
|
|
112
|
+
allowedTools: frontmatter['allowed-tools']
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const validation = this.validateSkillMetadata(metadata);
|
|
116
|
+
const skill: Skill = {
|
|
117
|
+
name: frontmatter.name,
|
|
118
|
+
description: frontmatter.description,
|
|
119
|
+
allowedTools: frontmatter['allowed-tools'],
|
|
120
|
+
path: skillPath,
|
|
121
|
+
directory: skillDir,
|
|
122
|
+
type,
|
|
123
|
+
isValid: validation.isValid,
|
|
124
|
+
errors: validation.errors.length > 0 ? validation.errors : undefined
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
return skill;
|
|
128
|
+
} catch (error: any) {
|
|
129
|
+
console.error(`Error loading skill metadata from ${skillPath}: ${error.message}`);
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Load the full content of a skill (for progressive disclosure)
|
|
136
|
+
*/
|
|
137
|
+
async loadFullSkill(name: string): Promise<Skill | null> {
|
|
138
|
+
const skill = this.skills.get(name);
|
|
139
|
+
if (!skill) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const content = fs.readFileSync(skill.path, 'utf-8');
|
|
145
|
+
skill.content = content;
|
|
146
|
+
return skill;
|
|
147
|
+
} catch (error: any) {
|
|
148
|
+
console.error(`Error loading full skill ${name}: ${error.message}`);
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Extract YAML frontmatter from markdown content
|
|
155
|
+
*/
|
|
156
|
+
private extractFrontmatter(content: string): SkillFrontmatter | null {
|
|
157
|
+
const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n/;
|
|
158
|
+
const match = content.match(frontmatterRegex);
|
|
159
|
+
|
|
160
|
+
if (!match) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const parsed = YAML.parse(match[1]) as SkillFrontmatter;
|
|
166
|
+
return parsed;
|
|
167
|
+
} catch (error) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Validate skill metadata according to Claude Code spec
|
|
174
|
+
*/
|
|
175
|
+
validateSkillMetadata(metadata: SkillMetadata): SkillValidationResult {
|
|
176
|
+
const errors: string[] = [];
|
|
177
|
+
const warnings: string[] = [];
|
|
178
|
+
|
|
179
|
+
// Check required fields
|
|
180
|
+
if (!metadata.name) {
|
|
181
|
+
errors.push('Missing required field: name');
|
|
182
|
+
} else {
|
|
183
|
+
// Name validation: lowercase letters, numbers, hyphens only, max 64 chars
|
|
184
|
+
const nameRegex = /^[a-z0-9-]+$/;
|
|
185
|
+
if (!nameRegex.test(metadata.name)) {
|
|
186
|
+
errors.push('Name must contain only lowercase letters, numbers, and hyphens');
|
|
187
|
+
}
|
|
188
|
+
if (metadata.name.length > 64) {
|
|
189
|
+
errors.push('Name must be 64 characters or less');
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!metadata.description) {
|
|
194
|
+
errors.push('Missing required field: description');
|
|
195
|
+
} else {
|
|
196
|
+
if (metadata.description.length > 1024) {
|
|
197
|
+
errors.push('Description must be 1024 characters or less');
|
|
198
|
+
}
|
|
199
|
+
// Check if description mentions when to use
|
|
200
|
+
if (!metadata.description.toLowerCase().includes('use when') &&
|
|
201
|
+
!metadata.description.toLowerCase().includes('use for')) {
|
|
202
|
+
warnings.push('Description should include when to use this skill (e.g., "Use when...")');
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Validate allowed-tools if present
|
|
207
|
+
if (metadata.allowedTools) {
|
|
208
|
+
const validTools = [
|
|
209
|
+
'Read', 'Write', 'Edit', 'Grep', 'Glob', 'Bash', 'WebSearch',
|
|
210
|
+
'GitStatus', 'GitDiff', 'GitCommit', 'GitPush', 'GitPull',
|
|
211
|
+
'ListDir', 'SearchFile', 'RunShell'
|
|
212
|
+
];
|
|
213
|
+
const invalidTools = metadata.allowedTools.filter(t => !validTools.includes(t));
|
|
214
|
+
if (invalidTools.length > 0) {
|
|
215
|
+
warnings.push(`Unknown tools in allowed-tools: ${invalidTools.join(', ')}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
isValid: errors.length === 0,
|
|
221
|
+
errors,
|
|
222
|
+
warnings
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Get skill by name
|
|
228
|
+
*/
|
|
229
|
+
getSkill(name: string): Skill | undefined {
|
|
230
|
+
return this.skills.get(name);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Get all skills
|
|
235
|
+
*/
|
|
236
|
+
getAllSkills(): Skill[] {
|
|
237
|
+
return Array.from(this.skills.values());
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Get skills formatted for model context injection
|
|
242
|
+
* This provides only metadata for progressive disclosure
|
|
243
|
+
*/
|
|
244
|
+
getSkillsContext(): string {
|
|
245
|
+
const skills = this.getAllSkills().filter(s => s.isValid);
|
|
246
|
+
|
|
247
|
+
if (skills.length === 0) {
|
|
248
|
+
return '';
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const context = skills.map(skill => {
|
|
252
|
+
return `- ${skill.name}: ${skill.description}`;
|
|
253
|
+
}).join('\n');
|
|
254
|
+
|
|
255
|
+
return `Available Skills:\n${context}`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Get skills as SkillContext array
|
|
260
|
+
*/
|
|
261
|
+
getSkillsContextArray(): SkillContext[] {
|
|
262
|
+
return this.getAllSkills()
|
|
263
|
+
.filter(s => s.isValid)
|
|
264
|
+
.map(s => ({
|
|
265
|
+
name: s.name,
|
|
266
|
+
description: s.description
|
|
267
|
+
}));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Validate all loaded skills
|
|
272
|
+
*/
|
|
273
|
+
validateAllSkills(): Map<string, SkillValidationResult> {
|
|
274
|
+
const results = new Map<string, SkillValidationResult>();
|
|
275
|
+
|
|
276
|
+
for (const [name, skill] of this.skills) {
|
|
277
|
+
const metadata: SkillMetadata = {
|
|
278
|
+
name: skill.name,
|
|
279
|
+
description: skill.description,
|
|
280
|
+
allowedTools: skill.allowedTools
|
|
281
|
+
};
|
|
282
|
+
results.set(name, this.validateSkillMetadata(metadata));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return results;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Read a supporting file from a skill directory
|
|
290
|
+
* Used for progressive disclosure of skill resources
|
|
291
|
+
*/
|
|
292
|
+
readSkillFile(skillName: string, fileName: string): string | null {
|
|
293
|
+
const skill = this.skills.get(skillName);
|
|
294
|
+
if (!skill) {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const filePath = path.join(skill.directory, fileName);
|
|
299
|
+
if (!fs.existsSync(filePath)) {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
305
|
+
} catch (error) {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* List supporting files in a skill directory
|
|
312
|
+
*/
|
|
313
|
+
listSkillFiles(skillName: string): string[] {
|
|
314
|
+
const skill = this.skills.get(skillName);
|
|
315
|
+
if (!skill) {
|
|
316
|
+
return [];
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (!fs.existsSync(skill.directory)) {
|
|
320
|
+
return [];
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
try {
|
|
324
|
+
const files = fs.readdirSync(skill.directory);
|
|
325
|
+
// Exclude SKILL.md as it's the main file
|
|
326
|
+
return files.filter(f => f !== 'SKILL.md');
|
|
327
|
+
} catch (error) {
|
|
328
|
+
return [];
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Create skills directories if they don't exist
|
|
334
|
+
*/
|
|
335
|
+
ensureDirectoriesExist(): void {
|
|
336
|
+
if (!fs.existsSync(this.personalSkillsDir)) {
|
|
337
|
+
fs.mkdirSync(this.personalSkillsDir, { recursive: true });
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Get personal skills directory path
|
|
343
|
+
*/
|
|
344
|
+
getPersonalSkillsDir(): string {
|
|
345
|
+
return this.personalSkillsDir;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Get project skills directory path
|
|
350
|
+
*/
|
|
351
|
+
getProjectSkillsDir(): string {
|
|
352
|
+
return this.projectSkillsDir;
|
|
353
|
+
}
|
|
354
|
+
}
|