@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.
@@ -0,0 +1,88 @@
1
+ ---
2
+ name: code-reviewer
3
+ description: Reviews code for best practices, potential bugs, security issues, and improvements. Use when reviewing code, checking pull requests, or analyzing code quality.
4
+ allowed-tools: ["Read", "Grep", "Glob"]
5
+ ---
6
+
7
+ # Code Reviewer
8
+
9
+ This skill provides systematic code review to identify issues and suggest improvements.
10
+
11
+ ## Review Checklist
12
+
13
+ ### 1. Code Organization
14
+ - [ ] Single Responsibility Principle followed
15
+ - [ ] Clear and descriptive names
16
+ - [ ] Proper separation of concerns
17
+ - [ ] Appropriate abstraction levels
18
+
19
+ ### 2. Error Handling
20
+ - [ ] Proper error handling for all operations
21
+ - [ ] Meaningful error messages
22
+ - [ ] Graceful degradation
23
+ - [ ] No silent failures
24
+
25
+ ### 3. Performance
26
+ - [ ] No obvious performance issues
27
+ - [ ] Appropriate data structures used
28
+ - [ ] Caching where applicable
29
+ - [ ] Resource cleanup (no memory leaks)
30
+
31
+ ### 4. Security
32
+ - [ ] Input validation
33
+ - [ ] No hardcoded credentials
34
+ - [ ] SQL injection prevention
35
+ - [ ] XSS prevention (web apps)
36
+ - [ ] Proper authentication/authorization
37
+
38
+ ### 5. Testing
39
+ - [ ] Test coverage adequate
40
+ - [ ] Edge cases considered
41
+ - [ ] Error scenarios tested
42
+
43
+ ### 6. Documentation
44
+ - [ ] Complex logic explained
45
+ - [ ] Public API documented
46
+ - [ ] Usage examples provided
47
+
48
+ ## How to Review
49
+
50
+ 1. **Understand the purpose**: What is this code supposed to do?
51
+ 2. **Read the code**: Follow the execution flow
52
+ 3. **Check against checklist**: Go through each category
53
+ 4. **Provide feedback**:
54
+ - **Critical**: Must fix before merge
55
+ - **Important**: Should fix
56
+ - **Suggestion**: Nice to have
57
+ 5. **Explain why**: Don't just point out problems
58
+
59
+ ## Feedback Format
60
+
61
+ ```markdown
62
+ ## Critical Issues
63
+ - Issue description
64
+ - Location: file.ts:123
65
+ - Why: [reason]
66
+ - Suggestion: [fix]
67
+
68
+ ## Important Notes
69
+ - Note description
70
+ - Location: file.ts:456
71
+
72
+ ## Suggestions
73
+ - Suggestion description
74
+ - Could improve [aspect]
75
+ ```
76
+
77
+ ## Common Issues to Look For
78
+
79
+ | Issue | Example |
80
+ |-------|---------|
81
+ | Unhandled promises | No `.catch()` or `await` without try/catch |
82
+ | Missing null checks | Accessing properties without null check |
83
+ | Race conditions | Async operations without proper ordering |
84
+ | Resource leaks | File handles, connections not closed |
85
+ | Type coercion | Using `==` instead of `===` |
86
+ | Magic numbers | Unexplained numeric literals |
87
+ | Large functions | Functions > 50 lines |
88
+ | Deep nesting | More than 3 levels of nesting |
@@ -0,0 +1,66 @@
1
+ ---
2
+ name: commit-helper
3
+ description: Generates clear, conventional commit messages from git diffs. Use when writing commit messages, reviewing staged changes, or when the user asks for help with git commits.
4
+ allowed-tools: ["GitStatus", "GitDiff", "GitCommit"]
5
+ ---
6
+
7
+ # Commit Message Helper
8
+
9
+ This skill helps you write clear, informative git commit messages following conventional commit format.
10
+
11
+ ## Instructions
12
+
13
+ When the user wants to commit changes:
14
+
15
+ 1. **Check git status** to see what files are staged
16
+ 2. **Review the diff** to understand what changed
17
+ 3. **Generate a commit message** with:
18
+ - **Type**: One of `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`
19
+ - **Scope**: (optional) The component or module affected
20
+ - **Summary**: Brief description under 50 characters
21
+ - **Body**: Detailed explanation of what and why (not how)
22
+ - **Footer**: (optional) Breaking changes or references
23
+
24
+ ## Commit Types
25
+
26
+ | Type | Description |
27
+ |------|-------------|
28
+ | `feat` | New feature |
29
+ | `fix` | Bug fix |
30
+ | `docs` | Documentation changes |
31
+ | `style` | Code style changes (formatting, etc.) |
32
+ | `refactor` | Code refactoring |
33
+ | `test` | Adding or updating tests |
34
+ | `chore` | Maintenance tasks |
35
+
36
+ ## Examples
37
+
38
+ ### Feature Addition
39
+ ```
40
+ feat(api): add user authentication endpoint
41
+
42
+ Add OAuth2 authentication for the REST API.
43
+ Implements login, logout, and token refresh.
44
+ ```
45
+
46
+ ### Bug Fix
47
+ ```
48
+ fix(cli): prevent crash when config file is missing
49
+
50
+ Check for config file existence before reading.
51
+ Show helpful error message if file not found.
52
+ ```
53
+
54
+ ### Documentation
55
+ ```
56
+ docs: update README with new installation instructions
57
+
58
+ Clarify NPM installation steps and add troubleshooting section.
59
+ ```
60
+
61
+ ## Best Practices
62
+
63
+ - Use present tense ("add" not "added")
64
+ - Explain what and why, not how
65
+ - Keep summary under 50 characters
66
+ - Reference issues in footer: `Closes #123`
@@ -0,0 +1,108 @@
1
+ ---
2
+ name: pdf-processing
3
+ description: Extract text, fill forms, merge PDFs, and manipulate PDF documents. Use when working with PDF files, forms, or document extraction. Requires pdf-parse package.
4
+ ---
5
+
6
+ # PDF Processing
7
+
8
+ This skill provides PDF manipulation capabilities for text extraction, form filling, and document operations.
9
+
10
+ ## Prerequisites
11
+
12
+ Install required packages:
13
+ ```bash
14
+ npm install pdf-parse
15
+ # or
16
+ pnpm add pdf-parse
17
+ # or
18
+ yarn add pdf-parse
19
+ ```
20
+
21
+ ## Capabilities
22
+
23
+ ### 1. Text Extraction
24
+ Extract plain text from PDF files with page-by-page information.
25
+
26
+ ### 2. Form Field Detection
27
+ Identify and extract form fields from PDF documents.
28
+
29
+ ### 3. Metadata Reading
30
+ Access PDF metadata like author, creation date, title.
31
+
32
+ ### 4. Page Count & Info
33
+ Get total pages and document dimensions.
34
+
35
+ ## Common Operations
36
+
37
+ ### Extract All Text
38
+ ```typescript
39
+ import fs from 'fs';
40
+ import pdf from 'pdf-parse';
41
+
42
+ const dataBuffer = fs.readFileSync('document.pdf');
43
+ const data = await pdf(dataBuffer);
44
+
45
+ console.log(data.text); // Full text content
46
+ console.log(data.numpages); // Number of pages
47
+ console.log(data.info); // PDF metadata
48
+ ```
49
+
50
+ ### Extract Text by Page
51
+ ```typescript
52
+ const data = await pdf(dataBuffer);
53
+ // Text includes page breaks
54
+ // Split by page markers if needed
55
+ ```
56
+
57
+ ### Get PDF Info
58
+ ```typescript
59
+ const data = await pdf(dataBuffer);
60
+ console.log({
61
+ pages: data.numpages,
62
+ info: data.info,
63
+ metadata: data.metadata
64
+ });
65
+ ```
66
+
67
+ ## Troubleshooting
68
+
69
+ ### "pdf-parse not found"
70
+ Install the package: `npm install pdf-parse`
71
+
72
+ ### Scanned PDFs return no text
73
+ Scanned PDFs are images. Use OCR (tesseract.js) instead.
74
+
75
+ ### Encrypted PDFs
76
+ Password-protected PDFs cannot be read. Remove password first.
77
+
78
+ ### Memory Issues with Large Files
79
+ For very large PDFs (>100MB), process in chunks or use streaming.
80
+
81
+ ## Advanced Topics
82
+
83
+ For advanced PDF operations like filling forms, merging, or creating PDFs, see [ADVANCED.md](ADVANCED.md).
84
+
85
+ ## Examples
86
+
87
+ ### Quick Text Extraction
88
+ ```bash
89
+ # User asks: "Extract text from this PDF"
90
+ # 1. Read the PDF file
91
+ # 2. Use pdf-parse to extract text
92
+ # 3. Return the text content
93
+ ```
94
+
95
+ ### Get Page Count
96
+ ```bash
97
+ # User asks: "How many pages in this PDF?"
98
+ # 1. Parse the PDF
99
+ # 2. Return numpages value
100
+ ```
101
+
102
+ ### Form Analysis
103
+ ```bash
104
+ # User asks: "What fields are in this PDF form?"
105
+ # 1. Parse the PDF
106
+ # 2. Look for form field patterns
107
+ # 3. List detected fields
108
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indiccoder/mentis-cli",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -50,7 +50,8 @@
50
50
  "screenshot-desktop": "^1.15.3",
51
51
  "uuid": "^13.0.0",
52
52
  "vscode-ripgrep": "^1.13.2",
53
- "xlsx": "^0.18.5"
53
+ "xlsx": "^0.18.5",
54
+ "yaml": "^2.7.0"
54
55
  },
55
56
  "devDependencies": {
56
57
  "@types/figlet": "^1.7.0",
@@ -17,6 +17,8 @@ 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';
20
22
  import * as readline from 'readline';
21
23
  import * as fs from 'fs';
22
24
  import * as path from 'path';
@@ -31,18 +33,23 @@ export class ReplManager {
31
33
  private modelClient!: ModelClient;
32
34
  private contextManager: ContextManager;
33
35
  private checkpointManager: CheckpointManager;
36
+ private skillsManager: SkillsManager;
34
37
  private history: ChatMessage[] = [];
35
38
  private mode: 'PLAN' | 'BUILD' = 'BUILD';
36
39
  private tools: Tool[] = [];
37
40
  private mcpClients: McpClient[] = [];
38
41
  private shell: PersistentShell;
39
42
  private currentModelName: string = 'Unknown';
43
+ private activeSkill: string | null = null; // Track currently active skill for allowed-tools
40
44
 
41
45
  constructor() {
42
46
  this.configManager = new ConfigManager();
43
47
  this.contextManager = new ContextManager();
44
48
  this.checkpointManager = new CheckpointManager();
49
+ this.skillsManager = new SkillsManager();
45
50
  this.shell = new PersistentShell();
51
+
52
+ // Create tools array without skill tools first
46
53
  this.tools = [
47
54
  new WriteFileTool(),
48
55
  new ReadFileTool(),
@@ -64,6 +71,67 @@ export class ReplManager {
64
71
  });
65
72
  // Default to Ollama if not specified, assuming compatible endpoint
66
73
  this.initializeClient();
74
+
75
+ // Initialize skills system after client is ready
76
+ this.initializeSkills();
77
+ }
78
+
79
+ /**
80
+ * Initialize the skills system
81
+ */
82
+ private async initializeSkills() {
83
+ this.skillsManager.ensureDirectoriesExist();
84
+ await this.skillsManager.discoverSkills();
85
+
86
+ // Add skill tools to the tools list
87
+ // Pass callback to LoadSkillTool to track active skill
88
+ this.tools.push(
89
+ new LoadSkillTool(this.skillsManager, (skill) => {
90
+ this.activeSkill = skill ? skill.name : null;
91
+ }),
92
+ new ListSkillsTool(this.skillsManager),
93
+ new ReadSkillFileTool(this.skillsManager)
94
+ );
95
+ }
96
+
97
+ /**
98
+ * Check if a tool is allowed by the currently active skill
99
+ * Returns true if tool is allowed, false if it requires confirmation
100
+ */
101
+ private isToolAllowedBySkill(toolName: string): boolean {
102
+ if (!this.activeSkill) {
103
+ // No active skill, all tools require confirmation as per normal flow
104
+ return false;
105
+ }
106
+
107
+ const skill = this.skillsManager.getSkill(this.activeSkill);
108
+ if (!skill || !skill.allowedTools || skill.allowedTools.length === 0) {
109
+ // No skill or no allowed-tools restriction
110
+ return false;
111
+ }
112
+
113
+ // Map tool names to allowed tool names
114
+ const toolMapping: Record<string, string> = {
115
+ 'write_file': 'Write',
116
+ 'read_file': 'Read',
117
+ 'edit_file': 'Edit',
118
+ 'search_files': 'Grep',
119
+ 'list_dir': 'ListDir',
120
+ 'search_file': 'SearchFile',
121
+ 'run_shell': 'RunShell',
122
+ 'web_search': 'WebSearch',
123
+ 'git_status': 'GitStatus',
124
+ 'git_diff': 'GitDiff',
125
+ 'git_commit': 'GitCommit',
126
+ 'git_push': 'GitPush',
127
+ 'git_pull': 'GitPull',
128
+ 'load_skill': 'Read',
129
+ 'list_skills': 'Read',
130
+ 'read_skill_file': 'Read'
131
+ };
132
+
133
+ const mappedToolName = toolMapping[toolName] || toolName;
134
+ return skill.allowedTools.includes(mappedToolName);
67
135
  }
68
136
 
69
137
  private initializeClient() {
@@ -184,11 +252,10 @@ export class ReplManager {
184
252
  console.log(' /drop <file> - Remove file from context');
185
253
  console.log(' /plan - Switch to PLAN mode');
186
254
  console.log(' /build - Switch to BUILD mode');
187
- console.log(' /plan - Switch to PLAN mode');
188
- console.log(' /build - Switch to BUILD mode');
189
255
  console.log(' /model - Interactively select Provider & Model');
190
256
  console.log(' /use <provider> [model] - Quick switch (legacy)');
191
257
  console.log(' /mcp <cmd> - Manage MCP servers');
258
+ console.log(' /skills <list|show|create|validate> - Manage Agent Skills');
192
259
  console.log(' /resume - Resume last session');
193
260
  console.log(' /checkpoint <save|load|list> [name] - Manage checkpoints');
194
261
  console.log(' /search <query> - Search codebase');
@@ -263,6 +330,9 @@ export class ReplManager {
263
330
  const updater = new UpdateManager();
264
331
  await updater.checkAndPerformUpdate(true);
265
332
  break;
333
+ case '/skills':
334
+ await this.handleSkillsCommand(args);
335
+ break;
266
336
  default:
267
337
  console.log(chalk.red(`Unknown command: ${command}`));
268
338
  }
@@ -270,6 +340,7 @@ export class ReplManager {
270
340
 
271
341
  private async handleChat(input: string) {
272
342
  const context = this.contextManager.getContextString();
343
+ const skillsContext = this.skillsManager.getSkillsContext();
273
344
  let fullInput = input;
274
345
 
275
346
  let modeInstruction = '';
@@ -281,6 +352,11 @@ export class ReplManager {
281
352
 
282
353
  fullInput = `${input}${modeInstruction}`;
283
354
 
355
+ // Add skills context if available
356
+ if (skillsContext) {
357
+ fullInput = `${skillsContext}\n\n${fullInput}`;
358
+ }
359
+
284
360
  if (context) {
285
361
  fullInput = `${context}\n\nUser Question: ${fullInput}`;
286
362
  }
@@ -343,7 +419,8 @@ export class ReplManager {
343
419
  console.log(chalk.dim(` [Action] ${toolName}(${displayArgs})`));
344
420
 
345
421
  // Safety check for write_file
346
- if (toolName === 'write_file') {
422
+ // Skip confirmation if tool is allowed by active skill
423
+ if (toolName === 'write_file' && !this.isToolAllowedBySkill('Write')) {
347
424
  // Pause cancellation listener during user interaction
348
425
  if (process.stdin.isTTY) {
349
426
  process.stdin.removeListener('keypress', keyListener);
@@ -863,6 +940,100 @@ export class ReplManager {
863
940
  }
864
941
  }
865
942
 
943
+ private async handleSkillsCommand(args: string[]) {
944
+ const { SkillCreator, validateSkills } = await import('../skills/SkillCreator');
945
+
946
+ if (args.length < 1) {
947
+ // Show skills list by default
948
+ await this.handleSkillsCommand(['list']);
949
+ return;
950
+ }
951
+
952
+ const action = args[0];
953
+
954
+ switch (action) {
955
+ case 'list':
956
+ await this.handleSkillsList();
957
+ break;
958
+ case 'show':
959
+ if (args.length < 2) {
960
+ console.log(chalk.red('Usage: /skills show <name>'));
961
+ return;
962
+ }
963
+ await this.handleSkillsShow(args[1]);
964
+ break;
965
+ case 'create':
966
+ const creator = new SkillCreator(this.skillsManager);
967
+ await creator.run(args[1]);
968
+ // Re-discover skills after creation
969
+ await this.skillsManager.discoverSkills();
970
+ break;
971
+ case 'validate':
972
+ await validateSkills(this.skillsManager);
973
+ break;
974
+ default:
975
+ console.log(chalk.red(`Unknown skills action: ${action}`));
976
+ console.log(chalk.yellow('Available actions: list, show, create, validate'));
977
+ }
978
+ }
979
+
980
+ private async handleSkillsList(): Promise<void> {
981
+ const skills = this.skillsManager.getAllSkills();
982
+
983
+ if (skills.length === 0) {
984
+ console.log(chalk.yellow('No skills available.'));
985
+ console.log(chalk.dim('Create skills with: /skills create'));
986
+ console.log(chalk.dim('Add skills to: ~/.mentis/skills/ or .mentis/skills/'));
987
+ return;
988
+ }
989
+
990
+ console.log(chalk.cyan(`\nAvailable Skills (${skills.length}):\n`));
991
+
992
+ for (const skill of skills) {
993
+ const statusIcon = skill.isValid ? '✓' : '✗';
994
+ const typeLabel = skill.type === 'personal' ? 'Personal' : 'Project';
995
+
996
+ console.log(`${statusIcon} ${chalk.bold(skill.name)} (${typeLabel})`);
997
+ console.log(` ${skill.description}`);
998
+
999
+ if (skill.allowedTools && skill.allowedTools.length > 0) {
1000
+ console.log(chalk.dim(` Allowed tools: ${skill.allowedTools.join(', ')}`));
1001
+ }
1002
+
1003
+ if (!skill.isValid && skill.errors) {
1004
+ console.log(chalk.red(` Errors: ${skill.errors.join(', ')}`));
1005
+ }
1006
+
1007
+ console.log('');
1008
+ }
1009
+ }
1010
+
1011
+ private async handleSkillsShow(name: string): Promise<void> {
1012
+ const skill = await this.skillsManager.loadFullSkill(name);
1013
+
1014
+ if (!skill) {
1015
+ console.log(chalk.red(`Skill "${name}" not found.`));
1016
+ return;
1017
+ }
1018
+
1019
+ console.log(chalk.cyan(`\n# ${skill.name}\n`));
1020
+ console.log(chalk.dim(`Type: ${skill.type}`));
1021
+ console.log(chalk.dim(`Path: ${skill.path}`));
1022
+
1023
+ if (skill.allowedTools && skill.allowedTools.length > 0) {
1024
+ console.log(chalk.dim(`Allowed tools: ${skill.allowedTools.join(', ')}`));
1025
+ }
1026
+
1027
+ console.log('');
1028
+ console.log(skill.content || 'No content available');
1029
+
1030
+ // List supporting files
1031
+ const files = this.skillsManager.listSkillFiles(name);
1032
+ if (files.length > 0) {
1033
+ console.log(chalk.dim(`\nSupporting files: ${files.join(', ')}`));
1034
+ }
1035
+ }
1036
+
866
1037
  private estimateCost(input: number, output: number): number {
867
1038
  const config = this.configManager.getConfig();
868
1039
  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
+ }