@indiccoder/mentis-cli 1.1.3 → 1.1.4
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/ARCHITECTURE.md +267 -0
- package/CONTRIBUTING.md +209 -0
- package/dist/commands/Command.js +15 -1
- package/dist/commands/CommandManager.js +30 -5
- package/dist/commands/__tests__/CommandManager.test.js +70 -0
- package/dist/index.js +23 -0
- package/dist/skills/Skill.js +17 -2
- package/dist/skills/SkillsManager.js +28 -3
- package/dist/skills/__tests__/SkillsManager.test.js +62 -0
- package/dist/utils/__mocks__/chalk.js +20 -0
- package/dist/utils/__tests__/ContextVisualizer.test.js +95 -0
- package/package.json +23 -2
- package/src/commands/Command.ts +64 -13
- package/src/commands/CommandManager.ts +26 -5
- package/src/commands/__tests__/CommandManager.test.ts +83 -0
- package/src/index.ts +33 -1
- package/src/skills/Skill.ts +91 -11
- package/src/skills/SkillsManager.ts +25 -3
- package/src/skills/__tests__/SkillsManager.test.ts +73 -0
- package/src/utils/__mocks__/chalk.ts +19 -0
- package/src/utils/__tests__/ContextVisualizer.test.ts +118 -0
package/src/skills/Skill.ts
CHANGED
|
@@ -1,26 +1,96 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Agent Skills System
|
|
3
|
+
*
|
|
4
|
+
* Based on Claude Code's Agent Skills format. Skills are reusable AI agent
|
|
5
|
+
* configurations stored as SKILL.md files in dedicated directories.
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```markdown
|
|
11
|
+
* ---
|
|
12
|
+
* name: code-reviewer
|
|
13
|
+
* description: Use when the user asks for a code review. Examines code for bugs, style issues, and improvements.
|
|
14
|
+
* allowed-tools: [Read, Grep, Glob]
|
|
15
|
+
* ---
|
|
16
|
+
*
|
|
17
|
+
* Review the code for...
|
|
18
|
+
* ```
|
|
4
19
|
*/
|
|
5
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Core metadata for an Agent Skill
|
|
23
|
+
*
|
|
24
|
+
* @remarks
|
|
25
|
+
* Skills define specialized AI behaviors with optional tool restrictions.
|
|
26
|
+
* The description should include when to use the skill (e.g., "Use when...").
|
|
27
|
+
*/
|
|
6
28
|
export interface SkillMetadata {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Lowercase skill name with numbers and hyphens only (max 64 chars)
|
|
31
|
+
*
|
|
32
|
+
* @pattern ^[a-z0-9-]+$
|
|
33
|
+
* @maxLength 64
|
|
34
|
+
*/
|
|
35
|
+
name: string;
|
|
36
|
+
/**
|
|
37
|
+
* What the skill does and when to use it (max 1024 chars)
|
|
38
|
+
*
|
|
39
|
+
* @remarks
|
|
40
|
+
* Should include phrases like "Use when..." or "Use for..."
|
|
41
|
+
* to help the AI understand when to invoke this skill.
|
|
42
|
+
*
|
|
43
|
+
* @maxLength 1024
|
|
44
|
+
*/
|
|
45
|
+
description: string;
|
|
46
|
+
/**
|
|
47
|
+
* Optional list of tools the skill is allowed to use
|
|
48
|
+
*
|
|
49
|
+
* @remarks
|
|
50
|
+
* If specified, the AI will be restricted to only these tools
|
|
51
|
+
* when executing this skill.
|
|
52
|
+
*/
|
|
53
|
+
allowedTools?: string[];
|
|
10
54
|
}
|
|
11
55
|
|
|
56
|
+
/**
|
|
57
|
+
* A fully loaded Agent Skill
|
|
58
|
+
*/
|
|
12
59
|
export interface Skill extends SkillMetadata {
|
|
13
|
-
|
|
60
|
+
/** Absolute path to the SKILL.md file */
|
|
61
|
+
path: string;
|
|
62
|
+
/** Whether this is personal, project, or plugin skill */
|
|
14
63
|
type: 'personal' | 'project' | 'plugin';
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Full skill content (loaded on demand)
|
|
66
|
+
*
|
|
67
|
+
* @remarks
|
|
68
|
+
* For progressive disclosure, only metadata is loaded initially.
|
|
69
|
+
* The full content is loaded only when the skill is invoked.
|
|
70
|
+
*/
|
|
71
|
+
content?: string;
|
|
72
|
+
/** Directory containing the skill (for resolving supporting files) */
|
|
73
|
+
directory: string;
|
|
74
|
+
/** Whether the skill passes validation */
|
|
75
|
+
isValid: boolean;
|
|
76
|
+
/** Validation errors if any */
|
|
77
|
+
errors?: string[];
|
|
19
78
|
}
|
|
20
79
|
|
|
80
|
+
/**
|
|
81
|
+
* YAML frontmatter parsed from SKILL.md
|
|
82
|
+
*/
|
|
21
83
|
export interface SkillFrontmatter {
|
|
84
|
+
/** Skill name */
|
|
22
85
|
name: string;
|
|
86
|
+
/** Skill description */
|
|
23
87
|
description: string;
|
|
88
|
+
/**
|
|
89
|
+
* Optional tool restrictions
|
|
90
|
+
*
|
|
91
|
+
* @remarks
|
|
92
|
+
* YAML key uses kebab-case: allowed-tools
|
|
93
|
+
*/
|
|
24
94
|
'allowed-tools'?: string[];
|
|
25
95
|
}
|
|
26
96
|
|
|
@@ -28,13 +98,20 @@ export interface SkillFrontmatter {
|
|
|
28
98
|
* Validation result for a skill
|
|
29
99
|
*/
|
|
30
100
|
export interface SkillValidationResult {
|
|
101
|
+
/** Whether the skill is valid */
|
|
31
102
|
isValid: boolean;
|
|
103
|
+
/** Validation errors that must be fixed */
|
|
32
104
|
errors: string[];
|
|
105
|
+
/** Warnings that don't block usage */
|
|
33
106
|
warnings: string[];
|
|
34
107
|
}
|
|
35
108
|
|
|
36
109
|
/**
|
|
37
|
-
*
|
|
110
|
+
* Minimal skill context for model injection
|
|
111
|
+
*
|
|
112
|
+
* @remarks
|
|
113
|
+
* This is the format used when injecting skills into the system prompt.
|
|
114
|
+
* Only name and description are included (progressive disclosure).
|
|
38
115
|
*/
|
|
39
116
|
export interface SkillContext {
|
|
40
117
|
name: string;
|
|
@@ -45,7 +122,10 @@ export interface SkillContext {
|
|
|
45
122
|
* Options for skill discovery
|
|
46
123
|
*/
|
|
47
124
|
export interface SkillDiscoveryOptions {
|
|
125
|
+
/** Include personal skills from ~/.mentis/skills */
|
|
48
126
|
includePersonal?: boolean;
|
|
127
|
+
/** Include project skills from .mentis/skills */
|
|
49
128
|
includeProject?: boolean;
|
|
129
|
+
/** Include plugin skills (not yet implemented) */
|
|
50
130
|
includePlugin?: boolean;
|
|
51
131
|
}
|
|
@@ -43,11 +43,19 @@ export class SkillsManager {
|
|
|
43
43
|
const discovered: Skill[] = [];
|
|
44
44
|
|
|
45
45
|
if (includePersonal) {
|
|
46
|
-
|
|
46
|
+
try {
|
|
47
|
+
discovered.push(...await this.discoverSkillsInDirectory(this.personalSkillsDir, 'personal'));
|
|
48
|
+
} catch (error: any) {
|
|
49
|
+
console.warn(`Warning: Failed to load personal skills from ${this.personalSkillsDir}: ${error.message}`);
|
|
50
|
+
}
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
if (includeProject) {
|
|
50
|
-
|
|
54
|
+
try {
|
|
55
|
+
discovered.push(...await this.discoverSkillsInDirectory(this.projectSkillsDir, 'project'));
|
|
56
|
+
} catch (error: any) {
|
|
57
|
+
console.warn(`Warning: Failed to load project skills from ${this.projectSkillsDir}: ${error.message}`);
|
|
58
|
+
}
|
|
51
59
|
}
|
|
52
60
|
|
|
53
61
|
// Store skills in map for quick lookup
|
|
@@ -102,6 +110,7 @@ export class SkillsManager {
|
|
|
102
110
|
const frontmatter = this.extractFrontmatter(content);
|
|
103
111
|
|
|
104
112
|
if (!frontmatter) {
|
|
113
|
+
console.warn(`Warning: Invalid or missing frontmatter in ${skillPath} (skipping)`);
|
|
105
114
|
return null;
|
|
106
115
|
}
|
|
107
116
|
|
|
@@ -124,9 +133,22 @@ export class SkillsManager {
|
|
|
124
133
|
errors: validation.errors.length > 0 ? validation.errors : undefined
|
|
125
134
|
};
|
|
126
135
|
|
|
136
|
+
// Log validation warnings
|
|
137
|
+
if (validation.warnings.length > 0) {
|
|
138
|
+
for (const warning of validation.warnings) {
|
|
139
|
+
console.warn(`Warning (${skill.name}): ${warning}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
127
143
|
return skill;
|
|
128
144
|
} catch (error: any) {
|
|
129
|
-
|
|
145
|
+
if (error.code === 'ENOENT') {
|
|
146
|
+
console.warn(`Warning: Skill file not found: ${skillPath}`);
|
|
147
|
+
} else if (error.code === 'EACCES') {
|
|
148
|
+
console.warn(`Warning: Permission denied reading skill: ${skillPath}`);
|
|
149
|
+
} else {
|
|
150
|
+
console.error(`Error loading skill metadata from ${skillPath}: ${error.message}`);
|
|
151
|
+
}
|
|
130
152
|
return null;
|
|
131
153
|
}
|
|
132
154
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for SkillsManager
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { SkillsManager } from '../SkillsManager';
|
|
6
|
+
import { Skill } from '../Skill';
|
|
7
|
+
|
|
8
|
+
describe('SkillsManager', () => {
|
|
9
|
+
let manager: SkillsManager;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
manager = new SkillsManager();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('getAllSkills', () => {
|
|
16
|
+
it('should return empty array initially', () => {
|
|
17
|
+
const skills = manager.getAllSkills();
|
|
18
|
+
expect(skills).toEqual([]);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('getSkill', () => {
|
|
23
|
+
it('should return undefined for non-existent skill', () => {
|
|
24
|
+
const skill = manager.getSkill('non-existent');
|
|
25
|
+
expect(skill).toBeUndefined();
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('getSkillsContext', () => {
|
|
30
|
+
it('should return empty string when no skills', () => {
|
|
31
|
+
const context = manager.getSkillsContext();
|
|
32
|
+
expect(context).toBe('');
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('addSkill', () => {
|
|
37
|
+
it('should add skill to manager', () => {
|
|
38
|
+
const skill: Skill = {
|
|
39
|
+
name: 'test-skill',
|
|
40
|
+
description: 'Test skill',
|
|
41
|
+
path: '/skills/test.md',
|
|
42
|
+
type: 'personal',
|
|
43
|
+
directory: '/skills',
|
|
44
|
+
isValid: true,
|
|
45
|
+
content: 'Test content'
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
manager['skills'].set('test-skill', skill);
|
|
49
|
+
const retrieved = manager.getSkill('test-skill');
|
|
50
|
+
|
|
51
|
+
expect(retrieved).toBeDefined();
|
|
52
|
+
expect(retrieved?.name).toBe('test-skill');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should return skills context with skills', () => {
|
|
56
|
+
const skill: Skill = {
|
|
57
|
+
name: 'reviewer',
|
|
58
|
+
description: 'Code reviewer skill',
|
|
59
|
+
path: '/skills/reviewer.md',
|
|
60
|
+
type: 'personal',
|
|
61
|
+
directory: '/skills',
|
|
62
|
+
isValid: true,
|
|
63
|
+
content: 'Review code'
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
manager['skills'].set('reviewer', skill);
|
|
67
|
+
const context = manager.getSkillsContext();
|
|
68
|
+
|
|
69
|
+
expect(context).toContain('Available Skills');
|
|
70
|
+
expect(context).toContain('reviewer');
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Manual mock for chalk to avoid ESM issues in Jest
|
|
2
|
+
module.exports = {
|
|
3
|
+
dim: (str: string) => str,
|
|
4
|
+
green: (str: string) => str,
|
|
5
|
+
yellow: (str: string) => str,
|
|
6
|
+
red: (str: string) => str,
|
|
7
|
+
gray: (str: string) => str,
|
|
8
|
+
bold: (str: string) => str,
|
|
9
|
+
cyan: (str: string) => str,
|
|
10
|
+
default: {
|
|
11
|
+
dim: (str: string) => str,
|
|
12
|
+
green: (str: string) => str,
|
|
13
|
+
yellow: (str: string) => str,
|
|
14
|
+
red: (str: string) => str,
|
|
15
|
+
gray: (str: string) => str,
|
|
16
|
+
bold: (str: string) => str,
|
|
17
|
+
cyan: (str: string) => str
|
|
18
|
+
}
|
|
19
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for ContextVisualizer
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { ContextVisualizer } from '../ContextVisualizer';
|
|
6
|
+
import { ChatMessage } from '../../llm/ModelInterface';
|
|
7
|
+
|
|
8
|
+
describe('ContextVisualizer', () => {
|
|
9
|
+
let visualizer: ContextVisualizer;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
visualizer = new ContextVisualizer();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('calculateUsage', () => {
|
|
16
|
+
it('should handle empty history', () => {
|
|
17
|
+
const history: ChatMessage[] = [];
|
|
18
|
+
const usage = visualizer.calculateUsage(history);
|
|
19
|
+
|
|
20
|
+
// Includes 2000 char overhead = 500 tokens
|
|
21
|
+
expect(usage.tokens).toBe(500);
|
|
22
|
+
expect(usage.maxTokens).toBe(128000);
|
|
23
|
+
expect(usage).toHaveProperty('percentage');
|
|
24
|
+
expect(usage).toHaveProperty('tokens');
|
|
25
|
+
expect(usage).toHaveProperty('maxTokens');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should calculate tokens for messages', () => {
|
|
29
|
+
const history: ChatMessage[] = [
|
|
30
|
+
{ role: 'system', content: 'You are a helpful assistant.' },
|
|
31
|
+
{ role: 'user', content: 'Hello' },
|
|
32
|
+
{ role: 'assistant', content: 'Hi there!' }
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const usage = visualizer.calculateUsage(history);
|
|
36
|
+
|
|
37
|
+
expect(usage.tokens).toBeGreaterThan(500);
|
|
38
|
+
expect(usage.tokens).toBeLessThan(1000);
|
|
39
|
+
expect(usage.maxTokens).toBe(128000);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should handle large messages', () => {
|
|
43
|
+
const largeContent = 'x'.repeat(10000);
|
|
44
|
+
const history: ChatMessage[] = [
|
|
45
|
+
{ role: 'user', content: largeContent }
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const usage = visualizer.calculateUsage(history);
|
|
49
|
+
|
|
50
|
+
expect(usage.tokens).toBeGreaterThan(1500);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('formatBar', () => {
|
|
55
|
+
it('should format bar at low usage', () => {
|
|
56
|
+
const usage = { tokens: 1000, percentage: 5, maxTokens: 128000 };
|
|
57
|
+
const bar = visualizer.formatBar(usage);
|
|
58
|
+
|
|
59
|
+
// Check that bar contains expected data (without chalk dependency)
|
|
60
|
+
expect(bar).toContain('5');
|
|
61
|
+
expect(bar).toContain('1k');
|
|
62
|
+
expect(bar).toContain('128');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should format bar at medium usage', () => {
|
|
66
|
+
const usage = { tokens: 50000, percentage: 40, maxTokens: 128000 };
|
|
67
|
+
const bar = visualizer.formatBar(usage);
|
|
68
|
+
|
|
69
|
+
expect(bar).toContain('40');
|
|
70
|
+
expect(bar).toContain('50k');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should format bar at high usage', () => {
|
|
74
|
+
const usage = { tokens: 100000, percentage: 80, maxTokens: 128000 };
|
|
75
|
+
const bar = visualizer.formatBar(usage);
|
|
76
|
+
|
|
77
|
+
expect(bar).toContain('80');
|
|
78
|
+
expect(bar).toContain('100k');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('shouldCompact', () => {
|
|
83
|
+
it('should return false for low percentage', () => {
|
|
84
|
+
const history: ChatMessage[] = [
|
|
85
|
+
{ role: 'user', content: 'small message' }
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
const shouldCompact = visualizer.shouldCompact(history);
|
|
89
|
+
expect(shouldCompact).toBe(false);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should return true at 80% threshold', () => {
|
|
93
|
+
// Create enough content to exceed 80%
|
|
94
|
+
// 80% of 128000 tokens = 102400 tokens = ~409600 chars
|
|
95
|
+
// Subtract 2000 overhead = ~407400 chars needed
|
|
96
|
+
const largeContent = 'x'.repeat(410000);
|
|
97
|
+
const history: ChatMessage[] = [
|
|
98
|
+
{ role: 'system', content: largeContent },
|
|
99
|
+
{ role: 'user', content: largeContent }
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
const shouldCompact = visualizer.shouldCompact(history);
|
|
103
|
+
expect(shouldCompact).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('setMaxTokens', () => {
|
|
108
|
+
it('should update max tokens', () => {
|
|
109
|
+
visualizer.setMaxTokens(32000);
|
|
110
|
+
|
|
111
|
+
const history: ChatMessage[] = [];
|
|
112
|
+
const usage = visualizer.calculateUsage(history);
|
|
113
|
+
|
|
114
|
+
expect(usage.maxTokens).toBe(32000);
|
|
115
|
+
expect(usage.percentage).toBeGreaterThan(1); // Should be higher percentage with smaller max
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
});
|