@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.
@@ -1,26 +1,96 @@
1
1
  /**
2
- * Skill data structure for Agent Skills system
3
- * Based on Claude Code's Agent Skills format
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
- 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
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
- path: string; // Path to SKILL.md
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
- 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
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
- * Skill context format for model injection
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
- discovered.push(...await this.discoverSkillsInDirectory(this.personalSkillsDir, 'personal'));
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
- discovered.push(...await this.discoverSkillsInDirectory(this.projectSkillsDir, 'project'));
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
- console.error(`Error loading skill metadata from ${skillPath}: ${error.message}`);
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
+ });