morpheus-cli 0.7.2 → 0.7.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/README.md +119 -0
- package/dist/channels/discord.js +109 -0
- package/dist/channels/telegram.js +94 -0
- package/dist/cli/commands/start.js +12 -0
- package/dist/config/manager.js +3 -0
- package/dist/config/paths.js +1 -0
- package/dist/config/schemas.js +11 -2
- package/dist/http/api.js +3 -0
- package/dist/http/routers/skills.js +291 -0
- package/dist/runtime/__tests__/keymaker.test.js +145 -0
- package/dist/runtime/keymaker.js +162 -0
- package/dist/runtime/oracle.js +15 -4
- package/dist/runtime/scaffold.js +75 -0
- package/dist/runtime/skills/__tests__/loader.test.js +187 -0
- package/dist/runtime/skills/__tests__/registry.test.js +201 -0
- package/dist/runtime/skills/__tests__/tool.test.js +266 -0
- package/dist/runtime/skills/index.js +8 -0
- package/dist/runtime/skills/loader.js +213 -0
- package/dist/runtime/skills/registry.js +141 -0
- package/dist/runtime/skills/schema.js +30 -0
- package/dist/runtime/skills/tool.js +204 -0
- package/dist/runtime/skills/types.js +7 -0
- package/dist/runtime/tasks/context.js +16 -0
- package/dist/runtime/tasks/worker.js +22 -0
- package/dist/runtime/tools/apoc-tool.js +28 -1
- package/dist/runtime/tools/morpheus-tools.js +3 -0
- package/dist/runtime/tools/neo-tool.js +32 -0
- package/dist/runtime/tools/trinity-tool.js +27 -0
- package/dist/types/config.js +3 -0
- package/dist/ui/assets/index-CsMDzmtQ.js +117 -0
- package/dist/ui/assets/index-Dz_qYlIb.css +1 -0
- package/dist/ui/index.html +2 -2
- package/dist/ui/sw.js +1 -1
- package/package.json +4 -1
- package/dist/ui/assets/index-7e8TCoiy.js +0 -111
- package/dist/ui/assets/index-B9ngtbja.css +0 -1
package/dist/runtime/scaffold.js
CHANGED
|
@@ -1,10 +1,79 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
2
3
|
import { PATHS } from '../config/paths.js';
|
|
3
4
|
import { ConfigManager } from '../config/manager.js';
|
|
4
5
|
import { DEFAULT_MCP_TEMPLATE } from '../types/mcp.js';
|
|
5
6
|
import chalk from 'chalk';
|
|
6
7
|
import ora from 'ora';
|
|
7
8
|
import { migrateConfigFile } from './migration.js';
|
|
9
|
+
const SKILLS_README = `# Morpheus Skills
|
|
10
|
+
|
|
11
|
+
This folder contains custom skills for Morpheus.
|
|
12
|
+
|
|
13
|
+
## Creating a Skill
|
|
14
|
+
|
|
15
|
+
1. Create a folder with your skill name (lowercase, hyphens allowed):
|
|
16
|
+
\`\`\`
|
|
17
|
+
mkdir my-skill
|
|
18
|
+
\`\`\`
|
|
19
|
+
|
|
20
|
+
2. Create \`SKILL.md\` with YAML frontmatter + instructions:
|
|
21
|
+
\`\`\`markdown
|
|
22
|
+
---
|
|
23
|
+
name: my-skill
|
|
24
|
+
description: What this skill does (max 500 chars)
|
|
25
|
+
execution_mode: sync
|
|
26
|
+
version: 1.0.0
|
|
27
|
+
author: your-name
|
|
28
|
+
tags:
|
|
29
|
+
- category
|
|
30
|
+
examples:
|
|
31
|
+
- "Example request that triggers this skill"
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
# My Skill
|
|
35
|
+
|
|
36
|
+
Instructions for Keymaker to follow when executing this skill.
|
|
37
|
+
|
|
38
|
+
## Steps
|
|
39
|
+
1. First step
|
|
40
|
+
2. Second step
|
|
41
|
+
|
|
42
|
+
## Output Format
|
|
43
|
+
How to format the result.
|
|
44
|
+
\`\`\`
|
|
45
|
+
|
|
46
|
+
## Execution Modes
|
|
47
|
+
|
|
48
|
+
| Mode | Tool | Description |
|
|
49
|
+
|------|------|-------------|
|
|
50
|
+
| sync | skill_execute | Result returned immediately (default) |
|
|
51
|
+
| async | skill_delegate | Runs in background, notifies when done |
|
|
52
|
+
|
|
53
|
+
**sync** (default): Best for quick tasks like code review, analysis.
|
|
54
|
+
**async**: Best for long-running tasks like builds, deployments.
|
|
55
|
+
|
|
56
|
+
## How It Works
|
|
57
|
+
|
|
58
|
+
- Oracle lists available skills in its system prompt
|
|
59
|
+
- When a request matches a sync skill, Oracle calls \`skill_execute\`
|
|
60
|
+
- When a request matches an async skill, Oracle calls \`skill_delegate\`
|
|
61
|
+
- Keymaker has access to ALL tools (filesystem, shell, git, MCP, databases)
|
|
62
|
+
- Keymaker follows SKILL.md instructions to complete the task
|
|
63
|
+
|
|
64
|
+
## Frontmatter Schema
|
|
65
|
+
|
|
66
|
+
| Field | Required | Default | Description |
|
|
67
|
+
|-------|----------|---------|-------------|
|
|
68
|
+
| name | Yes | - | Unique identifier (a-z, 0-9, hyphens) |
|
|
69
|
+
| description | Yes | - | Short description (max 500 chars) |
|
|
70
|
+
| execution_mode | No | sync | sync or async |
|
|
71
|
+
| version | No | - | Semver (e.g., 1.0.0) |
|
|
72
|
+
| author | No | - | Your name |
|
|
73
|
+
| enabled | No | true | true/false |
|
|
74
|
+
| tags | No | - | Array of tags (max 10) |
|
|
75
|
+
| examples | No | - | Example requests (max 5) |
|
|
76
|
+
`;
|
|
8
77
|
export async function scaffold() {
|
|
9
78
|
const spinner = ora('Ensuring Morpheus environment...').start();
|
|
10
79
|
try {
|
|
@@ -15,6 +84,7 @@ export async function scaffold() {
|
|
|
15
84
|
fs.ensureDir(PATHS.memory),
|
|
16
85
|
fs.ensureDir(PATHS.cache),
|
|
17
86
|
fs.ensureDir(PATHS.commands),
|
|
87
|
+
fs.ensureDir(PATHS.skills),
|
|
18
88
|
]);
|
|
19
89
|
// Migrate config.yaml -> zaion.yaml if needed
|
|
20
90
|
await migrateConfigFile();
|
|
@@ -30,6 +100,11 @@ export async function scaffold() {
|
|
|
30
100
|
if (!(await fs.pathExists(PATHS.mcps))) {
|
|
31
101
|
await fs.writeJson(PATHS.mcps, DEFAULT_MCP_TEMPLATE, { spaces: 2 });
|
|
32
102
|
}
|
|
103
|
+
// Create skills README if not exists
|
|
104
|
+
const skillsReadme = path.join(PATHS.skills, 'README.md');
|
|
105
|
+
if (!(await fs.pathExists(skillsReadme))) {
|
|
106
|
+
await fs.writeFile(skillsReadme, SKILLS_README, 'utf-8');
|
|
107
|
+
}
|
|
33
108
|
spinner.succeed('Morpheus environment ready at ' + chalk.cyan(PATHS.root));
|
|
34
109
|
}
|
|
35
110
|
catch (error) {
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { SkillLoader } from '../loader.js';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
/**
|
|
6
|
+
* Helper to create SKILL.md with YAML frontmatter
|
|
7
|
+
*/
|
|
8
|
+
function createSkillMd(dir, frontmatter, content = '') {
|
|
9
|
+
const lines = [];
|
|
10
|
+
for (const [key, value] of Object.entries(frontmatter)) {
|
|
11
|
+
if (Array.isArray(value)) {
|
|
12
|
+
lines.push(`${key}:`);
|
|
13
|
+
for (const item of value) {
|
|
14
|
+
lines.push(` - ${item}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
lines.push(`${key}: ${value}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const yaml = lines.join('\n');
|
|
22
|
+
const md = `---\n${yaml}\n---\n${content}`;
|
|
23
|
+
fs.writeFileSync(path.join(dir, 'SKILL.md'), md);
|
|
24
|
+
}
|
|
25
|
+
describe('SkillLoader', () => {
|
|
26
|
+
const testDir = path.join(process.cwd(), 'test-skills');
|
|
27
|
+
let loader;
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
fs.ensureDirSync(testDir);
|
|
30
|
+
loader = new SkillLoader(testDir);
|
|
31
|
+
});
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
fs.removeSync(testDir);
|
|
34
|
+
});
|
|
35
|
+
describe('scan()', () => {
|
|
36
|
+
it('should return empty list for non-existent directory', async () => {
|
|
37
|
+
fs.removeSync(testDir);
|
|
38
|
+
const result = await loader.scan();
|
|
39
|
+
expect(result.skills).toHaveLength(0);
|
|
40
|
+
expect(result.errors).toHaveLength(0);
|
|
41
|
+
});
|
|
42
|
+
it('should return empty list for empty directory', async () => {
|
|
43
|
+
const result = await loader.scan();
|
|
44
|
+
expect(result.skills).toHaveLength(0);
|
|
45
|
+
expect(result.errors).toHaveLength(0);
|
|
46
|
+
});
|
|
47
|
+
it('should load valid skill with all metadata', async () => {
|
|
48
|
+
const skillDir = path.join(testDir, 'test-skill');
|
|
49
|
+
fs.ensureDirSync(skillDir);
|
|
50
|
+
createSkillMd(skillDir, {
|
|
51
|
+
name: 'test-skill',
|
|
52
|
+
description: 'A test skill for unit testing',
|
|
53
|
+
version: '1.0.0',
|
|
54
|
+
author: 'Test Author',
|
|
55
|
+
enabled: true,
|
|
56
|
+
execution_mode: 'sync',
|
|
57
|
+
tags: ['test', 'unit'],
|
|
58
|
+
examples: ['do something', 'do another thing'],
|
|
59
|
+
}, '# Test Skill\n\nInstructions here.');
|
|
60
|
+
const result = await loader.scan();
|
|
61
|
+
expect(result.skills).toHaveLength(1);
|
|
62
|
+
expect(result.errors).toHaveLength(0);
|
|
63
|
+
const skill = result.skills[0];
|
|
64
|
+
expect(skill.name).toBe('test-skill');
|
|
65
|
+
expect(skill.description).toBe('A test skill for unit testing');
|
|
66
|
+
expect(skill.version).toBe('1.0.0');
|
|
67
|
+
expect(skill.author).toBe('Test Author');
|
|
68
|
+
expect(skill.enabled).toBe(true);
|
|
69
|
+
expect(skill.execution_mode).toBe('sync');
|
|
70
|
+
expect(skill.tags).toEqual(['test', 'unit']);
|
|
71
|
+
expect(skill.examples).toEqual(['do something', 'do another thing']);
|
|
72
|
+
expect(skill.content).toBe('# Test Skill\n\nInstructions here.');
|
|
73
|
+
});
|
|
74
|
+
it('should load skill with minimal metadata (defaults)', async () => {
|
|
75
|
+
const skillDir = path.join(testDir, 'minimal-skill');
|
|
76
|
+
fs.ensureDirSync(skillDir);
|
|
77
|
+
createSkillMd(skillDir, {
|
|
78
|
+
name: 'minimal-skill',
|
|
79
|
+
description: 'A minimal skill',
|
|
80
|
+
}, 'Minimal instructions');
|
|
81
|
+
const result = await loader.scan();
|
|
82
|
+
expect(result.skills).toHaveLength(1);
|
|
83
|
+
expect(result.errors).toHaveLength(0);
|
|
84
|
+
const skill = result.skills[0];
|
|
85
|
+
expect(skill.name).toBe('minimal-skill');
|
|
86
|
+
expect(skill.enabled).toBe(true); // default
|
|
87
|
+
expect(skill.execution_mode).toBe('sync'); // default
|
|
88
|
+
});
|
|
89
|
+
it('should report error for missing SKILL.md', async () => {
|
|
90
|
+
const skillDir = path.join(testDir, 'no-md-skill');
|
|
91
|
+
fs.ensureDirSync(skillDir);
|
|
92
|
+
// No SKILL.md file created
|
|
93
|
+
const result = await loader.scan();
|
|
94
|
+
expect(result.skills).toHaveLength(0);
|
|
95
|
+
expect(result.errors).toHaveLength(1);
|
|
96
|
+
expect(result.errors[0].directory).toBe('no-md-skill');
|
|
97
|
+
expect(result.errors[0].message).toContain('Missing SKILL.md');
|
|
98
|
+
});
|
|
99
|
+
it('should report error for SKILL.md without frontmatter', async () => {
|
|
100
|
+
const skillDir = path.join(testDir, 'no-frontmatter');
|
|
101
|
+
fs.ensureDirSync(skillDir);
|
|
102
|
+
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), '# No Frontmatter\n\nJust plain markdown.');
|
|
103
|
+
const result = await loader.scan();
|
|
104
|
+
expect(result.skills).toHaveLength(0);
|
|
105
|
+
expect(result.errors).toHaveLength(1);
|
|
106
|
+
expect(result.errors[0].directory).toBe('no-frontmatter');
|
|
107
|
+
expect(result.errors[0].message).toContain('Invalid format');
|
|
108
|
+
});
|
|
109
|
+
it('should report error for schema validation failure', async () => {
|
|
110
|
+
const skillDir = path.join(testDir, 'bad-schema');
|
|
111
|
+
fs.ensureDirSync(skillDir);
|
|
112
|
+
// Missing required 'description' field
|
|
113
|
+
createSkillMd(skillDir, {
|
|
114
|
+
name: 'bad-schema',
|
|
115
|
+
}, 'No description provided');
|
|
116
|
+
const result = await loader.scan();
|
|
117
|
+
expect(result.skills).toHaveLength(0);
|
|
118
|
+
expect(result.errors).toHaveLength(1);
|
|
119
|
+
expect(result.errors[0].message).toContain('Schema validation failed');
|
|
120
|
+
});
|
|
121
|
+
it('should reject invalid skill name format', async () => {
|
|
122
|
+
const skillDir = path.join(testDir, 'invalid-name');
|
|
123
|
+
fs.ensureDirSync(skillDir);
|
|
124
|
+
createSkillMd(skillDir, {
|
|
125
|
+
name: 'Invalid Name With Spaces!',
|
|
126
|
+
description: 'Should fail validation',
|
|
127
|
+
}, 'Content');
|
|
128
|
+
const result = await loader.scan();
|
|
129
|
+
expect(result.skills).toHaveLength(0);
|
|
130
|
+
expect(result.errors).toHaveLength(1);
|
|
131
|
+
expect(result.errors[0].message).toContain('Schema validation failed');
|
|
132
|
+
});
|
|
133
|
+
it('should load multiple skills', async () => {
|
|
134
|
+
// Create skill 1
|
|
135
|
+
const skill1Dir = path.join(testDir, 'skill-one');
|
|
136
|
+
fs.ensureDirSync(skill1Dir);
|
|
137
|
+
createSkillMd(skill1Dir, { name: 'skill-one', description: 'First skill' }, 'Instructions 1');
|
|
138
|
+
// Create skill 2
|
|
139
|
+
const skill2Dir = path.join(testDir, 'skill-two');
|
|
140
|
+
fs.ensureDirSync(skill2Dir);
|
|
141
|
+
createSkillMd(skill2Dir, { name: 'skill-two', description: 'Second skill' }, 'Instructions 2');
|
|
142
|
+
const result = await loader.scan();
|
|
143
|
+
expect(result.skills).toHaveLength(2);
|
|
144
|
+
expect(result.errors).toHaveLength(0);
|
|
145
|
+
const names = result.skills.map(s => s.name).sort();
|
|
146
|
+
expect(names).toEqual(['skill-one', 'skill-two']);
|
|
147
|
+
});
|
|
148
|
+
it('should ignore non-directory entries', async () => {
|
|
149
|
+
// Create a file instead of directory
|
|
150
|
+
fs.writeFileSync(path.join(testDir, 'not-a-dir.yaml'), 'name: test');
|
|
151
|
+
const result = await loader.scan();
|
|
152
|
+
expect(result.skills).toHaveLength(0);
|
|
153
|
+
expect(result.errors).toHaveLength(0);
|
|
154
|
+
});
|
|
155
|
+
it('should load async skill correctly', async () => {
|
|
156
|
+
const skillDir = path.join(testDir, 'async-skill');
|
|
157
|
+
fs.ensureDirSync(skillDir);
|
|
158
|
+
createSkillMd(skillDir, {
|
|
159
|
+
name: 'async-skill',
|
|
160
|
+
description: 'An async skill',
|
|
161
|
+
execution_mode: 'async',
|
|
162
|
+
}, 'Long-running task instructions');
|
|
163
|
+
const result = await loader.scan();
|
|
164
|
+
expect(result.skills).toHaveLength(1);
|
|
165
|
+
expect(result.skills[0].execution_mode).toBe('async');
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
describe('content handling', () => {
|
|
169
|
+
it('should include content in skill object', async () => {
|
|
170
|
+
const skillDir = path.join(testDir, 'content-skill');
|
|
171
|
+
fs.ensureDirSync(skillDir);
|
|
172
|
+
const mdContent = '# Content Skill\n\nThis is the instruction content.';
|
|
173
|
+
createSkillMd(skillDir, { name: 'content-skill', description: 'Test' }, mdContent);
|
|
174
|
+
const result = await loader.scan();
|
|
175
|
+
expect(result.skills).toHaveLength(1);
|
|
176
|
+
expect(result.skills[0].content).toBe(mdContent);
|
|
177
|
+
});
|
|
178
|
+
it('should handle empty content after frontmatter', async () => {
|
|
179
|
+
const skillDir = path.join(testDir, 'empty-content');
|
|
180
|
+
fs.ensureDirSync(skillDir);
|
|
181
|
+
createSkillMd(skillDir, { name: 'empty-content', description: 'Test' }, '');
|
|
182
|
+
const result = await loader.scan();
|
|
183
|
+
expect(result.skills).toHaveLength(1);
|
|
184
|
+
expect(result.skills[0].content).toBe('');
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
});
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
// Define test directory as a static string path
|
|
5
|
+
const TEST_DIR = process.cwd() + '/test-skills-registry';
|
|
6
|
+
// Mock PATHS to use test directory
|
|
7
|
+
vi.mock('../../../config/paths.js', () => ({
|
|
8
|
+
PATHS: {
|
|
9
|
+
skills: process.cwd() + '/test-skills-registry',
|
|
10
|
+
},
|
|
11
|
+
}));
|
|
12
|
+
// Import after mock setup
|
|
13
|
+
import { SkillRegistry } from '../registry.js';
|
|
14
|
+
/**
|
|
15
|
+
* Helper to create SKILL.md with YAML frontmatter
|
|
16
|
+
*/
|
|
17
|
+
function createSkillMd(dir, frontmatter, content = '') {
|
|
18
|
+
const lines = [];
|
|
19
|
+
for (const [key, value] of Object.entries(frontmatter)) {
|
|
20
|
+
if (Array.isArray(value)) {
|
|
21
|
+
lines.push(`${key}:`);
|
|
22
|
+
for (const item of value) {
|
|
23
|
+
lines.push(` - ${item}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
else if (typeof value === 'boolean') {
|
|
27
|
+
lines.push(`${key}: ${value ? 'true' : 'false'}`);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
lines.push(`${key}: ${value}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const yaml = lines.join('\n');
|
|
34
|
+
const md = `---\n${yaml}\n---\n${content}`;
|
|
35
|
+
fs.writeFileSync(path.join(dir, 'SKILL.md'), md);
|
|
36
|
+
}
|
|
37
|
+
describe('SkillRegistry', () => {
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
SkillRegistry.resetInstance();
|
|
40
|
+
fs.ensureDirSync(TEST_DIR);
|
|
41
|
+
});
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
fs.removeSync(TEST_DIR);
|
|
44
|
+
});
|
|
45
|
+
describe('singleton pattern', () => {
|
|
46
|
+
it('should return the same instance', () => {
|
|
47
|
+
const instance1 = SkillRegistry.getInstance();
|
|
48
|
+
const instance2 = SkillRegistry.getInstance();
|
|
49
|
+
expect(instance1).toBe(instance2);
|
|
50
|
+
});
|
|
51
|
+
it('should reset instance correctly', () => {
|
|
52
|
+
const instance1 = SkillRegistry.getInstance();
|
|
53
|
+
SkillRegistry.resetInstance();
|
|
54
|
+
const instance2 = SkillRegistry.getInstance();
|
|
55
|
+
expect(instance1).not.toBe(instance2);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
describe('load()', () => {
|
|
59
|
+
it('should load skills from directory', async () => {
|
|
60
|
+
const skillDir = path.join(TEST_DIR, 'test-skill');
|
|
61
|
+
fs.ensureDirSync(skillDir);
|
|
62
|
+
createSkillMd(skillDir, { name: 'test-skill', description: 'Test skill' }, 'Instructions');
|
|
63
|
+
const registry = SkillRegistry.getInstance();
|
|
64
|
+
await registry.load();
|
|
65
|
+
expect(registry.getAll()).toHaveLength(1);
|
|
66
|
+
expect(registry.get('test-skill')).toBeDefined();
|
|
67
|
+
});
|
|
68
|
+
it('should clear previous skills on reload', async () => {
|
|
69
|
+
const skillDir = path.join(TEST_DIR, 'skill-a');
|
|
70
|
+
fs.ensureDirSync(skillDir);
|
|
71
|
+
createSkillMd(skillDir, { name: 'skill-a', description: 'Skill A' }, 'Instructions');
|
|
72
|
+
const registry = SkillRegistry.getInstance();
|
|
73
|
+
await registry.load();
|
|
74
|
+
expect(registry.getAll()).toHaveLength(1);
|
|
75
|
+
// Remove the skill and reload
|
|
76
|
+
fs.removeSync(skillDir);
|
|
77
|
+
await registry.reload();
|
|
78
|
+
expect(registry.getAll()).toHaveLength(0);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
describe('enable() / disable()', () => {
|
|
82
|
+
it('should enable a disabled skill', async () => {
|
|
83
|
+
const skillDir = path.join(TEST_DIR, 'toggle-skill');
|
|
84
|
+
fs.ensureDirSync(skillDir);
|
|
85
|
+
createSkillMd(skillDir, { name: 'toggle-skill', description: 'Toggle test', enabled: false }, 'Instructions');
|
|
86
|
+
const registry = SkillRegistry.getInstance();
|
|
87
|
+
await registry.load();
|
|
88
|
+
expect(registry.get('toggle-skill')?.enabled).toBe(false);
|
|
89
|
+
expect(registry.getEnabled()).toHaveLength(0);
|
|
90
|
+
const result = registry.enable('toggle-skill');
|
|
91
|
+
expect(result).toBe(true);
|
|
92
|
+
expect(registry.get('toggle-skill')?.enabled).toBe(true);
|
|
93
|
+
expect(registry.getEnabled()).toHaveLength(1);
|
|
94
|
+
});
|
|
95
|
+
it('should disable an enabled skill', async () => {
|
|
96
|
+
const skillDir = path.join(TEST_DIR, 'toggle-skill');
|
|
97
|
+
fs.ensureDirSync(skillDir);
|
|
98
|
+
createSkillMd(skillDir, { name: 'toggle-skill', description: 'Toggle test', enabled: true }, 'Instructions');
|
|
99
|
+
const registry = SkillRegistry.getInstance();
|
|
100
|
+
await registry.load();
|
|
101
|
+
expect(registry.get('toggle-skill')?.enabled).toBe(true);
|
|
102
|
+
const result = registry.disable('toggle-skill');
|
|
103
|
+
expect(result).toBe(true);
|
|
104
|
+
expect(registry.get('toggle-skill')?.enabled).toBe(false);
|
|
105
|
+
expect(registry.getEnabled()).toHaveLength(0);
|
|
106
|
+
});
|
|
107
|
+
it('should return false for non-existent skill', async () => {
|
|
108
|
+
const registry = SkillRegistry.getInstance();
|
|
109
|
+
await registry.load();
|
|
110
|
+
expect(registry.enable('non-existent')).toBe(false);
|
|
111
|
+
expect(registry.disable('non-existent')).toBe(false);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
describe('getEnabled()', () => {
|
|
115
|
+
it('should return only enabled skills', async () => {
|
|
116
|
+
// Create enabled skill
|
|
117
|
+
const enabledDir = path.join(TEST_DIR, 'enabled-skill');
|
|
118
|
+
fs.ensureDirSync(enabledDir);
|
|
119
|
+
createSkillMd(enabledDir, { name: 'enabled-skill', description: 'Enabled', enabled: true }, 'Instructions');
|
|
120
|
+
// Create disabled skill
|
|
121
|
+
const disabledDir = path.join(TEST_DIR, 'disabled-skill');
|
|
122
|
+
fs.ensureDirSync(disabledDir);
|
|
123
|
+
createSkillMd(disabledDir, { name: 'disabled-skill', description: 'Disabled', enabled: false }, 'Instructions');
|
|
124
|
+
const registry = SkillRegistry.getInstance();
|
|
125
|
+
await registry.load();
|
|
126
|
+
expect(registry.getAll()).toHaveLength(2);
|
|
127
|
+
expect(registry.getEnabled()).toHaveLength(1);
|
|
128
|
+
expect(registry.getEnabled()[0].name).toBe('enabled-skill');
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
describe('getSystemPromptSection()', () => {
|
|
132
|
+
it('should generate prompt section with sync skills', async () => {
|
|
133
|
+
const skillDir = path.join(TEST_DIR, 'prompt-skill');
|
|
134
|
+
fs.ensureDirSync(skillDir);
|
|
135
|
+
createSkillMd(skillDir, {
|
|
136
|
+
name: 'prompt-skill',
|
|
137
|
+
description: 'A skill for prompts',
|
|
138
|
+
execution_mode: 'sync',
|
|
139
|
+
examples: ['example usage'],
|
|
140
|
+
}, 'Instructions for prompt skill');
|
|
141
|
+
const registry = SkillRegistry.getInstance();
|
|
142
|
+
await registry.load();
|
|
143
|
+
const section = registry.getSystemPromptSection();
|
|
144
|
+
expect(section).toContain('Available Skills');
|
|
145
|
+
expect(section).toContain('prompt-skill');
|
|
146
|
+
expect(section).toContain('A skill for prompts');
|
|
147
|
+
expect(section).toContain('skill_execute');
|
|
148
|
+
});
|
|
149
|
+
it('should generate prompt section with async skills', async () => {
|
|
150
|
+
const skillDir = path.join(TEST_DIR, 'async-skill');
|
|
151
|
+
fs.ensureDirSync(skillDir);
|
|
152
|
+
createSkillMd(skillDir, {
|
|
153
|
+
name: 'async-skill',
|
|
154
|
+
description: 'An async skill',
|
|
155
|
+
execution_mode: 'async',
|
|
156
|
+
}, 'Instructions for async skill');
|
|
157
|
+
const registry = SkillRegistry.getInstance();
|
|
158
|
+
await registry.load();
|
|
159
|
+
const section = registry.getSystemPromptSection();
|
|
160
|
+
expect(section).toContain('Async Skills');
|
|
161
|
+
expect(section).toContain('async-skill');
|
|
162
|
+
expect(section).toContain('skill_delegate');
|
|
163
|
+
});
|
|
164
|
+
it('should return empty string when no skills', async () => {
|
|
165
|
+
const registry = SkillRegistry.getInstance();
|
|
166
|
+
await registry.load();
|
|
167
|
+
const section = registry.getSystemPromptSection();
|
|
168
|
+
expect(section).toBe('');
|
|
169
|
+
});
|
|
170
|
+
it('should not include disabled skills', async () => {
|
|
171
|
+
const skillDir = path.join(TEST_DIR, 'disabled-prompt');
|
|
172
|
+
fs.ensureDirSync(skillDir);
|
|
173
|
+
createSkillMd(skillDir, {
|
|
174
|
+
name: 'disabled-prompt',
|
|
175
|
+
description: 'Disabled skill',
|
|
176
|
+
enabled: false,
|
|
177
|
+
}, 'Instructions');
|
|
178
|
+
const registry = SkillRegistry.getInstance();
|
|
179
|
+
await registry.load();
|
|
180
|
+
const section = registry.getSystemPromptSection();
|
|
181
|
+
expect(section).toBe('');
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
describe('getContent()', () => {
|
|
185
|
+
it('should return skill content from loaded skill', async () => {
|
|
186
|
+
const skillDir = path.join(TEST_DIR, 'content-skill');
|
|
187
|
+
fs.ensureDirSync(skillDir);
|
|
188
|
+
createSkillMd(skillDir, { name: 'content-skill', description: 'Test' }, '# Instructions\n\nDo the thing.');
|
|
189
|
+
const registry = SkillRegistry.getInstance();
|
|
190
|
+
await registry.load();
|
|
191
|
+
const content = registry.getContent('content-skill');
|
|
192
|
+
expect(content).toBe('# Instructions\n\nDo the thing.');
|
|
193
|
+
});
|
|
194
|
+
it('should return null for non-existent skill', async () => {
|
|
195
|
+
const registry = SkillRegistry.getInstance();
|
|
196
|
+
await registry.load();
|
|
197
|
+
const content = registry.getContent('non-existent');
|
|
198
|
+
expect(content).toBeNull();
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
});
|