@indiccoder/mentis-cli 1.1.2 → 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/repl/ReplManager.js +19 -31
- 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/ui/InputBox.js +127 -0
- package/dist/ui/UIManager.js +2 -2
- package/dist/utils/__mocks__/chalk.js +20 -0
- package/dist/utils/__tests__/ContextVisualizer.test.js +95 -0
- package/package.json +25 -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/repl/ReplManager.ts +19 -33
- 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/ui/InputBox.ts +145 -0
- package/src/ui/UIManager.ts +2 -2
- package/src/utils/__mocks__/chalk.ts +19 -0
- package/src/utils/__tests__/ContextVisualizer.test.ts +118 -0
|
@@ -31,10 +31,18 @@ export class CommandManager {
|
|
|
31
31
|
const discovered: Command[] = [];
|
|
32
32
|
|
|
33
33
|
// Personal commands
|
|
34
|
-
|
|
34
|
+
try {
|
|
35
|
+
discovered.push(...await this.discoverCommandsInDirectory(this.personalCommandsDir, 'personal'));
|
|
36
|
+
} catch (error: any) {
|
|
37
|
+
console.warn(`Warning: Failed to load personal commands from ${this.personalCommandsDir}: ${error.message}`);
|
|
38
|
+
}
|
|
35
39
|
|
|
36
40
|
// Project commands
|
|
37
|
-
|
|
41
|
+
try {
|
|
42
|
+
discovered.push(...await this.discoverCommandsInDirectory(this.projectCommandsDir, 'project'));
|
|
43
|
+
} catch (error: any) {
|
|
44
|
+
console.warn(`Warning: Failed to load project commands from ${this.projectCommandsDir}: ${error.message}`);
|
|
45
|
+
}
|
|
38
46
|
|
|
39
47
|
// Store commands in map (project commands override personal)
|
|
40
48
|
for (const command of discovered) {
|
|
@@ -85,6 +93,7 @@ export class CommandManager {
|
|
|
85
93
|
const commandName = this.getCommandName(commandPath, type);
|
|
86
94
|
|
|
87
95
|
if (!commandName) {
|
|
96
|
+
console.warn(`Warning: Invalid command name in ${commandPath} (skipping)`);
|
|
88
97
|
return null;
|
|
89
98
|
}
|
|
90
99
|
|
|
@@ -107,7 +116,13 @@ export class CommandManager {
|
|
|
107
116
|
|
|
108
117
|
return command;
|
|
109
118
|
} catch (error: any) {
|
|
110
|
-
|
|
119
|
+
if (error.code === 'ENOENT') {
|
|
120
|
+
console.warn(`Warning: Command file not found: ${commandPath}`);
|
|
121
|
+
} else if (error.code === 'EACCES') {
|
|
122
|
+
console.warn(`Warning: Permission denied reading command: ${commandPath}`);
|
|
123
|
+
} else {
|
|
124
|
+
console.error(`Error parsing command ${commandPath}: ${error.message}`);
|
|
125
|
+
}
|
|
111
126
|
return null;
|
|
112
127
|
}
|
|
113
128
|
}
|
|
@@ -222,7 +237,10 @@ export class CommandManager {
|
|
|
222
237
|
const bashRegex = /!`([^`]+)`/g;
|
|
223
238
|
let bashMatch;
|
|
224
239
|
while ((bashMatch = bashRegex.exec(content)) !== null) {
|
|
225
|
-
|
|
240
|
+
const bashCommand = bashMatch[1].trim();
|
|
241
|
+
if (bashCommand) {
|
|
242
|
+
bashCommands.push(bashCommand);
|
|
243
|
+
}
|
|
226
244
|
}
|
|
227
245
|
|
|
228
246
|
// Remove bash command markers
|
|
@@ -232,7 +250,10 @@ export class CommandManager {
|
|
|
232
250
|
const fileRegex = /@([^\s]+)/g;
|
|
233
251
|
let fileMatch;
|
|
234
252
|
while ((fileMatch = fileRegex.exec(content)) !== null) {
|
|
235
|
-
|
|
253
|
+
const fileRef = fileMatch[1].trim();
|
|
254
|
+
if (fileRef && !fileReferences.includes(fileRef)) {
|
|
255
|
+
fileReferences.push(fileRef);
|
|
256
|
+
}
|
|
236
257
|
}
|
|
237
258
|
|
|
238
259
|
return { content, bashCommands, fileReferences };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for CommandManager
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { CommandManager } from '../CommandManager';
|
|
6
|
+
import { Command } from '../Command';
|
|
7
|
+
|
|
8
|
+
describe('CommandManager', () => {
|
|
9
|
+
let manager: CommandManager;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
manager = new CommandManager();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('getAllCommands', () => {
|
|
16
|
+
it('should return empty array initially', () => {
|
|
17
|
+
const commands = manager.getAllCommands();
|
|
18
|
+
expect(commands).toEqual([]);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('getCommandsContext', () => {
|
|
23
|
+
it('should return empty string when no commands', () => {
|
|
24
|
+
const context = manager.getCommandsContext();
|
|
25
|
+
expect(context).toBe('');
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('parseCommand', () => {
|
|
30
|
+
it('should replace $ARGUMENTS placeholder', async () => {
|
|
31
|
+
const command: Command = {
|
|
32
|
+
name: 'echo',
|
|
33
|
+
type: 'personal',
|
|
34
|
+
path: '/echo.md',
|
|
35
|
+
directory: '/commands',
|
|
36
|
+
description: 'Echo arguments',
|
|
37
|
+
frontmatter: {},
|
|
38
|
+
content: 'You said: $ARGUMENTS',
|
|
39
|
+
hasParameters: true
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const parsed = await manager.parseCommand(command, ['hello', 'world']);
|
|
43
|
+
|
|
44
|
+
expect(parsed.content).toContain('hello world');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should replace $1, $2 placeholders', async () => {
|
|
48
|
+
const command: Command = {
|
|
49
|
+
name: 'greet',
|
|
50
|
+
type: 'personal',
|
|
51
|
+
path: '/greet.md',
|
|
52
|
+
directory: '/commands',
|
|
53
|
+
description: 'Greet user',
|
|
54
|
+
frontmatter: {},
|
|
55
|
+
content: 'Hello $1, welcome to $2',
|
|
56
|
+
hasParameters: true
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const parsed = await manager.parseCommand(command, ['Alice', 'Wonderland']);
|
|
60
|
+
|
|
61
|
+
expect(parsed.content).toContain('Hello Alice');
|
|
62
|
+
expect(parsed.content).toContain('welcome to Wonderland');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should extract bash commands from content', async () => {
|
|
66
|
+
const command: Command = {
|
|
67
|
+
name: 'run-test',
|
|
68
|
+
type: 'personal',
|
|
69
|
+
path: '/run-test.md',
|
|
70
|
+
directory: '/commands',
|
|
71
|
+
description: 'Run tests',
|
|
72
|
+
frontmatter: {},
|
|
73
|
+
content: 'Run tests with !`npm test`',
|
|
74
|
+
hasParameters: false
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const parsed = await manager.parseCommand(command, []);
|
|
78
|
+
|
|
79
|
+
expect(parsed.bashCommands).toHaveLength(1);
|
|
80
|
+
expect(parsed.bashCommands[0]).toBe('npm test');
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -1,13 +1,39 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mentis CLI - An Agentic, Multi-Model CLI Coding Assistant
|
|
5
|
+
*
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
*/
|
|
8
|
+
|
|
2
9
|
import { ReplManager } from './repl/ReplManager';
|
|
3
10
|
|
|
11
|
+
/**
|
|
12
|
+
* CLI options for controlling Mentis behavior
|
|
13
|
+
*/
|
|
4
14
|
interface CliOptions {
|
|
15
|
+
/** Resume from the last saved checkpoint */
|
|
5
16
|
resume: boolean;
|
|
17
|
+
/** Auto-confirm all prompts (skip confirmations) */
|
|
6
18
|
yolo: boolean;
|
|
19
|
+
/** Run in headless (non-interactive) mode */
|
|
7
20
|
headless: boolean;
|
|
21
|
+
/** Prompt to execute in headless mode */
|
|
8
22
|
headlessPrompt?: string;
|
|
9
23
|
}
|
|
10
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Parse command line arguments
|
|
27
|
+
*
|
|
28
|
+
* @returns Parsed command and options
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```bash
|
|
32
|
+
* mentis --resume
|
|
33
|
+
* mentis -p "fix the bug"
|
|
34
|
+
* mentis --yolo
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
11
37
|
function parseArgs(): { command: string | null, options: CliOptions } {
|
|
12
38
|
const args = process.argv.slice(2);
|
|
13
39
|
const options: CliOptions = {
|
|
@@ -69,7 +95,12 @@ Commands (in REPL):
|
|
|
69
95
|
return { command, options };
|
|
70
96
|
}
|
|
71
97
|
|
|
72
|
-
|
|
98
|
+
/**
|
|
99
|
+
* Main entry point for Mentis CLI
|
|
100
|
+
*
|
|
101
|
+
* Parses arguments and starts the REPL or update manager
|
|
102
|
+
*/
|
|
103
|
+
async function main(): Promise<void> {
|
|
73
104
|
const { command, options } = parseArgs();
|
|
74
105
|
|
|
75
106
|
// Handle update command
|
|
@@ -85,6 +116,7 @@ async function main() {
|
|
|
85
116
|
await repl.start();
|
|
86
117
|
}
|
|
87
118
|
|
|
119
|
+
// Start the application
|
|
88
120
|
main().catch((error) => {
|
|
89
121
|
console.error('Fatal error:', error);
|
|
90
122
|
process.exit(1);
|
package/src/repl/ReplManager.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { OpenAIClient } from '../llm/OpenAIClient';
|
|
|
7
7
|
|
|
8
8
|
import { ContextManager } from '../context/ContextManager';
|
|
9
9
|
import { UIManager } from '../ui/UIManager';
|
|
10
|
+
import { InputBox } from '../ui/InputBox';
|
|
10
11
|
import { WriteFileTool, ReadFileTool, ListDirTool } from '../tools/FileTools';
|
|
11
12
|
import { SearchFileTool } from '../tools/SearchTools';
|
|
12
13
|
import { PersistentShellTool } from '../tools/PersistentShellTool';
|
|
@@ -229,53 +230,38 @@ export class ReplManager {
|
|
|
229
230
|
} catch (e) { }
|
|
230
231
|
}
|
|
231
232
|
|
|
233
|
+
// Initialize InputBox with history
|
|
234
|
+
const inputBox = new InputBox(commandHistory);
|
|
235
|
+
|
|
232
236
|
while (true) {
|
|
233
|
-
//
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
// Hint (Claude style puts it below, we put it above for standard terminal compatibility)
|
|
237
|
-
console.log(chalk.dim(' ? for shortcuts'));
|
|
238
|
-
|
|
239
|
-
const promptText = `> `; // Clean prompt
|
|
240
|
-
|
|
241
|
-
// Use readline for basic input to support history
|
|
242
|
-
const answer = await new Promise<string>((resolve) => {
|
|
243
|
-
const rl = readline.createInterface({
|
|
244
|
-
input: process.stdin,
|
|
245
|
-
output: process.stdout,
|
|
246
|
-
history: commandHistory,
|
|
247
|
-
historySize: 1000,
|
|
248
|
-
prompt: promptText
|
|
249
|
-
});
|
|
237
|
+
// Calculate context usage for display
|
|
238
|
+
const usage = this.contextVisualizer.calculateUsage(this.history);
|
|
250
239
|
|
|
251
|
-
|
|
240
|
+
// Display enhanced input frame
|
|
241
|
+
inputBox.displayFrame({
|
|
242
|
+
messageCount: this.history.length,
|
|
243
|
+
contextPercent: usage.percentage
|
|
244
|
+
});
|
|
252
245
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
246
|
+
// Get styled input
|
|
247
|
+
const answer = await inputBox.prompt({
|
|
248
|
+
showHint: this.history.length === 0,
|
|
249
|
+
hint: 'Type your message or /help for commands'
|
|
257
250
|
});
|
|
258
251
|
|
|
259
|
-
// Update history manually or grab from rl?
|
|
260
|
-
// rl.history gets updated when user hits enter.
|
|
261
|
-
// But we closed rl. We should manually save the input to our tracking array and file.
|
|
262
252
|
const input = answer.trim();
|
|
263
253
|
|
|
264
254
|
if (input) {
|
|
265
|
-
// Update
|
|
266
|
-
|
|
267
|
-
// Avoid duplicates if needed, but standard shell keeps them.
|
|
268
|
-
if (commandHistory[0] !== input) {
|
|
269
|
-
commandHistory.unshift(input);
|
|
270
|
-
}
|
|
255
|
+
// Update history via InputBox
|
|
256
|
+
inputBox.addToHistory(input);
|
|
271
257
|
|
|
272
|
-
// Append to file
|
|
258
|
+
// Append to file
|
|
273
259
|
try {
|
|
274
260
|
fs.appendFileSync(HISTORY_FILE, input + '\n');
|
|
275
261
|
} catch (e) { }
|
|
276
262
|
}
|
|
277
263
|
|
|
278
|
-
if (!
|
|
264
|
+
if (!input) continue;
|
|
279
265
|
|
|
280
266
|
if (input.startsWith('/')) {
|
|
281
267
|
await this.handleCommand(input);
|
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
|
+
});
|