morpheus-cli 0.9.5 → 0.9.7
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 +63 -43
- package/dist/channels/discord.js +71 -21
- package/dist/channels/telegram.js +73 -19
- package/dist/cli/commands/restart.js +15 -0
- package/dist/cli/commands/start.js +18 -0
- package/dist/config/manager.js +61 -0
- package/dist/config/paths.js +1 -0
- package/dist/config/schemas.js +11 -3
- package/dist/http/api.js +3 -0
- package/dist/http/routers/link.js +239 -0
- package/dist/http/routers/skills.js +1 -8
- package/dist/runtime/apoc.js +1 -1
- package/dist/runtime/audit/repository.js +1 -1
- package/dist/runtime/link-chunker.js +214 -0
- package/dist/runtime/link-repository.js +301 -0
- package/dist/runtime/link-search.js +298 -0
- package/dist/runtime/link-worker.js +284 -0
- package/dist/runtime/link.js +295 -0
- package/dist/runtime/memory/sati/service.js +1 -1
- package/dist/runtime/memory/sqlite.js +52 -0
- package/dist/runtime/neo.js +1 -1
- package/dist/runtime/oracle.js +81 -44
- package/dist/runtime/scaffold.js +4 -17
- package/dist/runtime/skills/__tests__/loader.test.js +7 -10
- package/dist/runtime/skills/__tests__/registry.test.js +2 -18
- package/dist/runtime/skills/__tests__/tool.test.js +55 -224
- package/dist/runtime/skills/index.js +1 -2
- package/dist/runtime/skills/loader.js +0 -2
- package/dist/runtime/skills/registry.js +8 -20
- package/dist/runtime/skills/schema.js +0 -4
- package/dist/runtime/skills/tool.js +42 -209
- package/dist/runtime/smiths/delegator.js +1 -1
- package/dist/runtime/smiths/registry.js +1 -1
- package/dist/runtime/tasks/worker.js +12 -44
- package/dist/runtime/trinity.js +1 -1
- package/dist/types/config.js +14 -0
- package/dist/ui/assets/AuditDashboard-93LCGHG1.js +1 -0
- package/dist/ui/assets/{Chat-BNtutgja.js → Chat-CK5sNcQ1.js} +8 -8
- package/dist/ui/assets/{Chronos-3C8RPZcl.js → Chronos-m2h--GEe.js} +1 -1
- package/dist/ui/assets/{ConfirmationModal-ZQPBeJ2Z.js → ConfirmationModal-Dd5pUJme.js} +1 -1
- package/dist/ui/assets/{Dashboard-CqkHzr2F.js → Dashboard-ODwl7d-a.js} +1 -1
- package/dist/ui/assets/{DeleteConfirmationModal-CioxFWn_.js → DeleteConfirmationModal-CCcojDmr.js} +1 -1
- package/dist/ui/assets/Documents-dWnSoxFO.js +7 -0
- package/dist/ui/assets/{Logs-DBVanS0O.js → Logs-Dc9Z2LBj.js} +1 -1
- package/dist/ui/assets/{MCPManager-vXfL3P2U.js → MCPManager-CMkb8vMn.js} +1 -1
- package/dist/ui/assets/{ModelPricing-DyfdunLT.js → ModelPricing-DtHPPbEQ.js} +1 -1
- package/dist/ui/assets/{Notifications-VL-vep6d.js → Notifications-BPvo-DWP.js} +1 -1
- package/dist/ui/assets/{Pagination-oTGieBLM.js → Pagination-BHZKk42X.js} +1 -1
- package/dist/ui/assets/{SatiMemories-jaadkW0U.js → SatiMemories-BUPu1Lxr.js} +1 -1
- package/dist/ui/assets/SessionAudit-CFKF4DA8.js +9 -0
- package/dist/ui/assets/Settings-C4JrXfsR.js +47 -0
- package/dist/ui/assets/{Skills-DE3zziXL.js → Skills-BUlvJgJ4.js} +1 -1
- package/dist/ui/assets/{Smiths-pmogN1mU.js → Smiths-CDtJdY0I.js} +1 -1
- package/dist/ui/assets/{Tasks-Bs8s34Jc.js → Tasks-DK_cOsNK.js} +1 -1
- package/dist/ui/assets/{TrinityDatabases-D7uihcdp.js → TrinityDatabases-X07by-19.js} +1 -1
- package/dist/ui/assets/{UsageStats-B9gePLZ0.js → UsageStats-dYcgckLq.js} +1 -1
- package/dist/ui/assets/{WebhookManager-B2L3rCLM.js → WebhookManager-DDw5eX2R.js} +1 -1
- package/dist/ui/assets/{audit-Cggeu9mM.js → audit-DZ5WLUEm.js} +1 -1
- package/dist/ui/assets/{chronos-D3-sWhfU.js → chronos-B_HI4mlq.js} +1 -1
- package/dist/ui/assets/{config-CBqRUPgn.js → config-B-YxlVrc.js} +1 -1
- package/dist/ui/assets/index-DVjwJ8jT.css +1 -0
- package/dist/ui/assets/{index-zKplfrXZ.js → index-DfJwcKqG.js} +5 -5
- package/dist/ui/assets/{mcp-uL1R9hyA.js → mcp-k-_pwbqA.js} +1 -1
- package/dist/ui/assets/{skills-jmw8yTJs.js → skills-xMXangks.js} +1 -1
- package/dist/ui/assets/{stats-HOms6GnM.js → stats-C4QZIv5O.js} +1 -1
- package/dist/ui/assets/{vendor-icons-DMd9RGvJ.js → vendor-icons-NHF9HNeN.js} +1 -1
- package/dist/ui/index.html +3 -3
- package/dist/ui/sw.js +1 -1
- package/package.json +3 -1
- package/dist/runtime/__tests__/keymaker.test.js +0 -148
- package/dist/runtime/keymaker.js +0 -157
- package/dist/ui/assets/AuditDashboard-DliJ1CX0.js +0 -1
- package/dist/ui/assets/SessionAudit-BsXrWlwz.js +0 -9
- package/dist/ui/assets/Settings-B4eezRcg.js +0 -47
- package/dist/ui/assets/index-D4fzIKy1.css +0 -1
package/dist/runtime/scaffold.js
CHANGED
|
@@ -22,7 +22,6 @@ This folder contains custom skills for Morpheus.
|
|
|
22
22
|
---
|
|
23
23
|
name: my-skill
|
|
24
24
|
description: What this skill does (max 500 chars)
|
|
25
|
-
execution_mode: sync
|
|
26
25
|
version: 1.0.0
|
|
27
26
|
author: your-name
|
|
28
27
|
tags:
|
|
@@ -33,7 +32,7 @@ This folder contains custom skills for Morpheus.
|
|
|
33
32
|
|
|
34
33
|
# My Skill
|
|
35
34
|
|
|
36
|
-
Instructions for
|
|
35
|
+
Instructions for Oracle to follow when handling this skill.
|
|
37
36
|
|
|
38
37
|
## Steps
|
|
39
38
|
1. First step
|
|
@@ -43,23 +42,11 @@ This folder contains custom skills for Morpheus.
|
|
|
43
42
|
How to format the result.
|
|
44
43
|
\`\`\`
|
|
45
44
|
|
|
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
45
|
## How It Works
|
|
57
46
|
|
|
58
47
|
- Oracle lists available skills in its system prompt
|
|
59
|
-
- When a request matches a
|
|
60
|
-
-
|
|
61
|
-
- Keymaker has access to ALL tools (filesystem, shell, git, MCP, databases)
|
|
62
|
-
- Keymaker follows SKILL.md instructions to complete the task
|
|
48
|
+
- When a request matches a skill, Oracle calls \`load_skill\` to load its instructions
|
|
49
|
+
- Oracle then follows the skill instructions using its existing tools (DevKit, MCP, databases, etc.)
|
|
63
50
|
|
|
64
51
|
## Frontmatter Schema
|
|
65
52
|
|
|
@@ -67,7 +54,6 @@ This folder contains custom skills for Morpheus.
|
|
|
67
54
|
|-------|----------|---------|-------------|
|
|
68
55
|
| name | Yes | - | Unique identifier (a-z, 0-9, hyphens) |
|
|
69
56
|
| description | Yes | - | Short description (max 500 chars) |
|
|
70
|
-
| execution_mode | No | sync | sync or async |
|
|
71
57
|
| version | No | - | Semver (e.g., 1.0.0) |
|
|
72
58
|
| author | No | - | Your name |
|
|
73
59
|
| enabled | No | true | true/false |
|
|
@@ -85,6 +71,7 @@ export async function scaffold() {
|
|
|
85
71
|
fs.ensureDir(PATHS.cache),
|
|
86
72
|
fs.ensureDir(PATHS.commands),
|
|
87
73
|
fs.ensureDir(PATHS.skills),
|
|
74
|
+
fs.ensureDir(PATHS.docs),
|
|
88
75
|
]);
|
|
89
76
|
// Migrate config.yaml -> zaion.yaml if needed
|
|
90
77
|
await migrateConfigFile();
|
|
@@ -53,7 +53,6 @@ describe('SkillLoader', () => {
|
|
|
53
53
|
version: '1.0.0',
|
|
54
54
|
author: 'Test Author',
|
|
55
55
|
enabled: true,
|
|
56
|
-
execution_mode: 'sync',
|
|
57
56
|
tags: ['test', 'unit'],
|
|
58
57
|
examples: ['do something', 'do another thing'],
|
|
59
58
|
}, '# Test Skill\n\nInstructions here.');
|
|
@@ -66,7 +65,6 @@ describe('SkillLoader', () => {
|
|
|
66
65
|
expect(skill.version).toBe('1.0.0');
|
|
67
66
|
expect(skill.author).toBe('Test Author');
|
|
68
67
|
expect(skill.enabled).toBe(true);
|
|
69
|
-
expect(skill.execution_mode).toBe('sync');
|
|
70
68
|
expect(skill.tags).toEqual(['test', 'unit']);
|
|
71
69
|
expect(skill.examples).toEqual(['do something', 'do another thing']);
|
|
72
70
|
expect(skill.content).toBe('# Test Skill\n\nInstructions here.');
|
|
@@ -84,7 +82,6 @@ describe('SkillLoader', () => {
|
|
|
84
82
|
const skill = result.skills[0];
|
|
85
83
|
expect(skill.name).toBe('minimal-skill');
|
|
86
84
|
expect(skill.enabled).toBe(true); // default
|
|
87
|
-
expect(skill.execution_mode).toBe('sync'); // default
|
|
88
85
|
});
|
|
89
86
|
it('should report error for missing SKILL.md', async () => {
|
|
90
87
|
const skillDir = path.join(testDir, 'no-md-skill');
|
|
@@ -152,17 +149,17 @@ describe('SkillLoader', () => {
|
|
|
152
149
|
expect(result.skills).toHaveLength(0);
|
|
153
150
|
expect(result.errors).toHaveLength(0);
|
|
154
151
|
});
|
|
155
|
-
it('should load
|
|
156
|
-
const skillDir = path.join(testDir, '
|
|
152
|
+
it('should load skill with tags correctly', async () => {
|
|
153
|
+
const skillDir = path.join(testDir, 'tagged-skill');
|
|
157
154
|
fs.ensureDirSync(skillDir);
|
|
158
155
|
createSkillMd(skillDir, {
|
|
159
|
-
name: '
|
|
160
|
-
description: '
|
|
161
|
-
|
|
162
|
-
}, '
|
|
156
|
+
name: 'tagged-skill',
|
|
157
|
+
description: 'A tagged skill',
|
|
158
|
+
tags: ['ops', 'deploy'],
|
|
159
|
+
}, 'Tagged task instructions');
|
|
163
160
|
const result = await loader.scan();
|
|
164
161
|
expect(result.skills).toHaveLength(1);
|
|
165
|
-
expect(result.skills[0].
|
|
162
|
+
expect(result.skills[0].tags).toEqual(['ops', 'deploy']);
|
|
166
163
|
});
|
|
167
164
|
});
|
|
168
165
|
describe('content handling', () => {
|
|
@@ -129,13 +129,12 @@ describe('SkillRegistry', () => {
|
|
|
129
129
|
});
|
|
130
130
|
});
|
|
131
131
|
describe('getSystemPromptSection()', () => {
|
|
132
|
-
it('should generate prompt section with
|
|
132
|
+
it('should generate prompt section with skills', async () => {
|
|
133
133
|
const skillDir = path.join(TEST_DIR, 'prompt-skill');
|
|
134
134
|
fs.ensureDirSync(skillDir);
|
|
135
135
|
createSkillMd(skillDir, {
|
|
136
136
|
name: 'prompt-skill',
|
|
137
137
|
description: 'A skill for prompts',
|
|
138
|
-
execution_mode: 'sync',
|
|
139
138
|
examples: ['example usage'],
|
|
140
139
|
}, 'Instructions for prompt skill');
|
|
141
140
|
const registry = SkillRegistry.getInstance();
|
|
@@ -144,22 +143,7 @@ describe('SkillRegistry', () => {
|
|
|
144
143
|
expect(section).toContain('Available Skills');
|
|
145
144
|
expect(section).toContain('prompt-skill');
|
|
146
145
|
expect(section).toContain('A skill for prompts');
|
|
147
|
-
expect(section).toContain('
|
|
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');
|
|
146
|
+
expect(section).toContain('load_skill');
|
|
163
147
|
});
|
|
164
148
|
it('should return empty string when no skills', async () => {
|
|
165
149
|
const registry = SkillRegistry.getInstance();
|
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
2
|
// Use vi.hoisted to define mocks before they're used in vi.mock calls
|
|
3
|
-
const { mockRegistry,
|
|
3
|
+
const { mockRegistry, mockAudit, mockContext } = vi.hoisted(() => ({
|
|
4
4
|
mockRegistry: {
|
|
5
5
|
get: vi.fn(),
|
|
6
6
|
getEnabled: vi.fn(() => []),
|
|
7
|
-
getContent: vi.fn(() => null),
|
|
8
7
|
},
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
},
|
|
12
|
-
mockDisplay: {
|
|
13
|
-
log: vi.fn(),
|
|
8
|
+
mockAudit: {
|
|
9
|
+
insert: vi.fn(),
|
|
14
10
|
},
|
|
15
11
|
mockContext: {
|
|
16
12
|
get: vi.fn(() => ({
|
|
@@ -19,13 +15,6 @@ const { mockRegistry, mockRepository, mockDisplay, mockContext, mockKeymaker } =
|
|
|
19
15
|
origin_message_id: '123',
|
|
20
16
|
origin_user_id: 'user-1',
|
|
21
17
|
})),
|
|
22
|
-
findDuplicateDelegation: vi.fn(() => null),
|
|
23
|
-
canEnqueueDelegation: vi.fn(() => true),
|
|
24
|
-
setDelegationAck: vi.fn(),
|
|
25
|
-
},
|
|
26
|
-
mockKeymaker: {
|
|
27
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
28
|
-
executeKeymakerTask: vi.fn((_skill, _obj, _ctx) => Promise.resolve('Keymaker result')),
|
|
29
18
|
},
|
|
30
19
|
}));
|
|
31
20
|
vi.mock('../registry.js', () => ({
|
|
@@ -33,234 +22,76 @@ vi.mock('../registry.js', () => ({
|
|
|
33
22
|
getInstance: () => mockRegistry,
|
|
34
23
|
},
|
|
35
24
|
}));
|
|
36
|
-
vi.mock('../../
|
|
37
|
-
|
|
38
|
-
getInstance: () =>
|
|
25
|
+
vi.mock('../../audit/repository.js', () => ({
|
|
26
|
+
AuditRepository: {
|
|
27
|
+
getInstance: () => mockAudit,
|
|
39
28
|
},
|
|
40
29
|
}));
|
|
41
30
|
vi.mock('../../tasks/context.js', () => ({
|
|
42
31
|
TaskRequestContext: mockContext,
|
|
43
32
|
}));
|
|
44
|
-
vi.mock('../../display.js', () => ({
|
|
45
|
-
DisplayManager: {
|
|
46
|
-
getInstance: () => mockDisplay,
|
|
47
|
-
},
|
|
48
|
-
}));
|
|
49
|
-
vi.mock('../../keymaker.js', () => ({
|
|
50
|
-
executeKeymakerTask: (skillName, objective, context) => mockKeymaker.executeKeymakerTask(skillName, objective, context),
|
|
51
|
-
}));
|
|
52
33
|
// Now import the module under test
|
|
53
|
-
import {
|
|
54
|
-
describe('
|
|
34
|
+
import { createLoadSkillTool } from '../tool.js';
|
|
35
|
+
describe('load_skill tool', () => {
|
|
36
|
+
let loadSkillTool;
|
|
55
37
|
beforeEach(() => {
|
|
56
38
|
vi.clearAllMocks();
|
|
57
39
|
mockRegistry.get.mockReset();
|
|
58
40
|
mockRegistry.getEnabled.mockReset();
|
|
59
|
-
mockKeymaker.executeKeymakerTask.mockReset();
|
|
60
|
-
mockKeymaker.executeKeymakerTask.mockResolvedValue('Keymaker result');
|
|
61
41
|
mockRegistry.getEnabled.mockReturnValue([]);
|
|
42
|
+
loadSkillTool = createLoadSkillTool();
|
|
62
43
|
});
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
]);
|
|
70
|
-
const description = getSkillExecuteDescription();
|
|
71
|
-
expect(description).toContain('code-review: Review code for issues');
|
|
72
|
-
expect(description).toContain('git-ops: Git operations helper');
|
|
73
|
-
expect(description).not.toContain('deploy');
|
|
74
|
-
});
|
|
75
|
-
it('should show no sync skills message when none enabled', () => {
|
|
76
|
-
mockRegistry.getEnabled.mockReturnValue([]);
|
|
77
|
-
const description = getSkillExecuteDescription();
|
|
78
|
-
expect(description).toContain('(no sync skills enabled)');
|
|
44
|
+
it('should return skill content for valid enabled skill', async () => {
|
|
45
|
+
mockRegistry.get.mockReturnValue({
|
|
46
|
+
name: 'test-skill',
|
|
47
|
+
description: 'Test',
|
|
48
|
+
enabled: true,
|
|
49
|
+
content: '# Instructions\n\nDo the thing.',
|
|
79
50
|
});
|
|
51
|
+
const result = await loadSkillTool.invoke({ skillName: 'test-skill' });
|
|
52
|
+
expect(result).toContain('Loaded skill: test-skill');
|
|
53
|
+
expect(result).toContain('# Instructions');
|
|
54
|
+
expect(result).toContain('Do the thing.');
|
|
80
55
|
});
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
execution_mode: 'sync',
|
|
88
|
-
content: 'Instructions here',
|
|
89
|
-
});
|
|
90
|
-
mockRegistry.getEnabled.mockReturnValue([{ name: 'test-skill', execution_mode: 'sync' }]);
|
|
91
|
-
const result = await SkillExecuteTool.invoke({
|
|
92
|
-
skillName: 'test-skill',
|
|
93
|
-
objective: 'do the thing',
|
|
94
|
-
});
|
|
95
|
-
expect(mockKeymaker.executeKeymakerTask).toHaveBeenCalledWith('test-skill', 'do the thing', expect.objectContaining({
|
|
96
|
-
origin_channel: 'telegram',
|
|
97
|
-
session_id: 'test-session',
|
|
98
|
-
}));
|
|
99
|
-
expect(result).toBe('Keymaker result');
|
|
100
|
-
});
|
|
101
|
-
it('should return error for non-existent skill', async () => {
|
|
102
|
-
mockRegistry.get.mockReturnValue(undefined);
|
|
103
|
-
mockRegistry.getEnabled.mockReturnValue([
|
|
104
|
-
{ name: 'other-skill', execution_mode: 'sync' },
|
|
105
|
-
]);
|
|
106
|
-
const result = await SkillExecuteTool.invoke({
|
|
107
|
-
skillName: 'non-existent',
|
|
108
|
-
objective: 'do something',
|
|
109
|
-
});
|
|
110
|
-
expect(result).toContain('Error');
|
|
111
|
-
expect(result).toContain('not found');
|
|
112
|
-
expect(result).toContain('other-skill');
|
|
113
|
-
});
|
|
114
|
-
it('should return error for async skill', async () => {
|
|
115
|
-
mockRegistry.get.mockReturnValue({
|
|
116
|
-
name: 'async-skill',
|
|
117
|
-
description: 'Async only',
|
|
118
|
-
enabled: true,
|
|
119
|
-
execution_mode: 'async',
|
|
120
|
-
});
|
|
121
|
-
const result = await SkillExecuteTool.invoke({
|
|
122
|
-
skillName: 'async-skill',
|
|
123
|
-
objective: 'do something',
|
|
124
|
-
});
|
|
125
|
-
expect(result).toContain('Error');
|
|
126
|
-
expect(result).toContain('async-only');
|
|
127
|
-
expect(result).toContain('skill_delegate');
|
|
56
|
+
it('should emit skill_loaded audit event', async () => {
|
|
57
|
+
mockRegistry.get.mockReturnValue({
|
|
58
|
+
name: 'test-skill',
|
|
59
|
+
description: 'Test',
|
|
60
|
+
enabled: true,
|
|
61
|
+
content: 'Instructions',
|
|
128
62
|
});
|
|
63
|
+
await loadSkillTool.invoke({ skillName: 'test-skill' });
|
|
64
|
+
expect(mockAudit.insert).toHaveBeenCalledWith(expect.objectContaining({
|
|
65
|
+
event_type: 'skill_loaded',
|
|
66
|
+
agent: 'oracle',
|
|
67
|
+
tool_name: 'test-skill',
|
|
68
|
+
status: 'success',
|
|
69
|
+
session_id: 'test-session',
|
|
70
|
+
}));
|
|
129
71
|
});
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
mockContext.findDuplicateDelegation.mockReturnValue(null);
|
|
139
|
-
mockContext.canEnqueueDelegation.mockReturnValue(true);
|
|
140
|
-
mockRegistry.getEnabled.mockReturnValue([]);
|
|
72
|
+
it('should return error for non-existent skill', async () => {
|
|
73
|
+
mockRegistry.get.mockReturnValue(undefined);
|
|
74
|
+
mockRegistry.getEnabled.mockReturnValue([
|
|
75
|
+
{ name: 'other-skill', description: 'Other' },
|
|
76
|
+
]);
|
|
77
|
+
const result = await loadSkillTool.invoke({ skillName: 'non-existent' });
|
|
78
|
+
expect(result).toContain('not found');
|
|
79
|
+
expect(result).toContain('other-skill');
|
|
141
80
|
});
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
{ name: 'code-review', description: 'Review code', execution_mode: 'sync' }, // should not appear
|
|
148
|
-
]);
|
|
149
|
-
const description = getSkillDelegateDescription();
|
|
150
|
-
expect(description).toContain('deploy-staging: Deploy to staging');
|
|
151
|
-
expect(description).toContain('batch-process: Process batch jobs');
|
|
152
|
-
expect(description).not.toContain('code-review');
|
|
153
|
-
});
|
|
154
|
-
it('should show no async skills message when none enabled', () => {
|
|
155
|
-
mockRegistry.getEnabled.mockReturnValue([]);
|
|
156
|
-
const description = getSkillDelegateDescription();
|
|
157
|
-
expect(description).toContain('(no async skills enabled)');
|
|
81
|
+
it('should return error for disabled skill', async () => {
|
|
82
|
+
mockRegistry.get.mockReturnValue({
|
|
83
|
+
name: 'disabled-skill',
|
|
84
|
+
description: 'Disabled',
|
|
85
|
+
enabled: false,
|
|
158
86
|
});
|
|
87
|
+
const result = await loadSkillTool.invoke({ skillName: 'disabled-skill' });
|
|
88
|
+
expect(result).toContain('disabled');
|
|
159
89
|
});
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
execution_mode: 'async',
|
|
167
|
-
});
|
|
168
|
-
mockRegistry.getEnabled.mockReturnValue([{ name: 'deploy-staging', execution_mode: 'async' }]);
|
|
169
|
-
mockRepository.createTask.mockReturnValue({
|
|
170
|
-
id: 'task-123',
|
|
171
|
-
agent: 'keymaker',
|
|
172
|
-
status: 'pending',
|
|
173
|
-
});
|
|
174
|
-
const result = await SkillDelegateTool.invoke({
|
|
175
|
-
skillName: 'deploy-staging',
|
|
176
|
-
objective: 'deploy to staging',
|
|
177
|
-
});
|
|
178
|
-
expect(mockRepository.createTask).toHaveBeenCalledWith(expect.objectContaining({
|
|
179
|
-
agent: 'keymaker',
|
|
180
|
-
input: 'deploy to staging',
|
|
181
|
-
context: JSON.stringify({ skill: 'deploy-staging' }),
|
|
182
|
-
origin_channel: 'telegram',
|
|
183
|
-
session_id: 'test-session',
|
|
184
|
-
}));
|
|
185
|
-
expect(result).toContain('task-123');
|
|
186
|
-
expect(result).toContain('queued');
|
|
187
|
-
});
|
|
188
|
-
it('should return error for sync skill', async () => {
|
|
189
|
-
mockRegistry.get.mockReturnValue({
|
|
190
|
-
name: 'sync-skill',
|
|
191
|
-
description: 'Sync',
|
|
192
|
-
enabled: true,
|
|
193
|
-
execution_mode: 'sync',
|
|
194
|
-
});
|
|
195
|
-
const result = await SkillDelegateTool.invoke({
|
|
196
|
-
skillName: 'sync-skill',
|
|
197
|
-
objective: 'do something',
|
|
198
|
-
});
|
|
199
|
-
expect(result).toContain('Error');
|
|
200
|
-
expect(result).toContain('sync');
|
|
201
|
-
expect(result).toContain('skill_execute');
|
|
202
|
-
expect(mockRepository.createTask).not.toHaveBeenCalled();
|
|
203
|
-
});
|
|
204
|
-
it('should return error for non-existent skill', async () => {
|
|
205
|
-
mockRegistry.get.mockReturnValue(undefined);
|
|
206
|
-
mockRegistry.getEnabled.mockReturnValue([
|
|
207
|
-
{ name: 'other-skill', execution_mode: 'async' },
|
|
208
|
-
]);
|
|
209
|
-
const result = await SkillDelegateTool.invoke({
|
|
210
|
-
skillName: 'non-existent',
|
|
211
|
-
objective: 'do something',
|
|
212
|
-
});
|
|
213
|
-
expect(result).toContain('Error');
|
|
214
|
-
expect(result).toContain('not found');
|
|
215
|
-
expect(mockRepository.createTask).not.toHaveBeenCalled();
|
|
216
|
-
});
|
|
217
|
-
it('should return error for disabled skill', async () => {
|
|
218
|
-
mockRegistry.get.mockReturnValue({
|
|
219
|
-
name: 'disabled-skill',
|
|
220
|
-
description: 'Disabled',
|
|
221
|
-
enabled: false,
|
|
222
|
-
execution_mode: 'async',
|
|
223
|
-
});
|
|
224
|
-
const result = await SkillDelegateTool.invoke({
|
|
225
|
-
skillName: 'disabled-skill',
|
|
226
|
-
objective: 'do something',
|
|
227
|
-
});
|
|
228
|
-
expect(result).toContain('Error');
|
|
229
|
-
expect(result).toContain('disabled');
|
|
230
|
-
expect(mockRepository.createTask).not.toHaveBeenCalled();
|
|
231
|
-
});
|
|
232
|
-
it('should deduplicate delegation requests', async () => {
|
|
233
|
-
mockRegistry.get.mockReturnValue({
|
|
234
|
-
name: 'dup-skill',
|
|
235
|
-
enabled: true,
|
|
236
|
-
execution_mode: 'async',
|
|
237
|
-
});
|
|
238
|
-
mockContext.findDuplicateDelegation.mockReturnValue({
|
|
239
|
-
task_id: 'existing-task',
|
|
240
|
-
agent: 'keymaker',
|
|
241
|
-
task: 'dup-skill:objective',
|
|
242
|
-
});
|
|
243
|
-
const result = await SkillDelegateTool.invoke({
|
|
244
|
-
skillName: 'dup-skill',
|
|
245
|
-
objective: 'objective',
|
|
246
|
-
});
|
|
247
|
-
expect(result).toContain('existing-task');
|
|
248
|
-
expect(result).toContain('already queued');
|
|
249
|
-
expect(mockRepository.createTask).not.toHaveBeenCalled();
|
|
250
|
-
});
|
|
251
|
-
it('should block when delegation limit reached', async () => {
|
|
252
|
-
mockRegistry.get.mockReturnValue({
|
|
253
|
-
name: 'limit-skill',
|
|
254
|
-
enabled: true,
|
|
255
|
-
execution_mode: 'async',
|
|
256
|
-
});
|
|
257
|
-
mockContext.canEnqueueDelegation.mockReturnValue(false);
|
|
258
|
-
const result = await SkillDelegateTool.invoke({
|
|
259
|
-
skillName: 'limit-skill',
|
|
260
|
-
objective: 'objective',
|
|
261
|
-
});
|
|
262
|
-
expect(result).toContain('limit reached');
|
|
263
|
-
expect(mockRepository.createTask).not.toHaveBeenCalled();
|
|
264
|
-
});
|
|
90
|
+
it('should show no skills available when none enabled', async () => {
|
|
91
|
+
mockRegistry.get.mockReturnValue(undefined);
|
|
92
|
+
mockRegistry.getEnabled.mockReturnValue([]);
|
|
93
|
+
const result = await loadSkillTool.invoke({ skillName: 'anything' });
|
|
94
|
+
expect(result).toContain('not found');
|
|
95
|
+
expect(result).toContain('none');
|
|
265
96
|
});
|
|
266
97
|
});
|
|
@@ -4,5 +4,4 @@
|
|
|
4
4
|
export { SkillRegistry } from './registry.js';
|
|
5
5
|
export { SkillLoader } from './loader.js';
|
|
6
6
|
export { SkillMetadataSchema } from './schema.js';
|
|
7
|
-
export {
|
|
8
|
-
} from './tool.js';
|
|
7
|
+
export { createLoadSkillTool } from './tool.js';
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
* ---
|
|
7
7
|
* name: my-skill
|
|
8
8
|
* description: What this skill does
|
|
9
|
-
* execution_mode: sync
|
|
10
9
|
* ---
|
|
11
10
|
*
|
|
12
11
|
* # Skill Instructions...
|
|
@@ -200,7 +199,6 @@ export class SkillLoader {
|
|
|
200
199
|
dirName,
|
|
201
200
|
content: content.trim(),
|
|
202
201
|
enabled: metadata.enabled ?? true,
|
|
203
|
-
execution_mode: metadata.execution_mode ?? 'sync',
|
|
204
202
|
};
|
|
205
203
|
return { skill };
|
|
206
204
|
}
|
|
@@ -107,26 +107,14 @@ export class SkillRegistry {
|
|
|
107
107
|
if (enabled.length === 0) {
|
|
108
108
|
return '';
|
|
109
109
|
}
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
lines.push('');
|
|
119
|
-
}
|
|
120
|
-
if (asyncSkills.length > 0) {
|
|
121
|
-
lines.push('### Async Skills (background task via skill_delegate)');
|
|
122
|
-
for (const s of asyncSkills) {
|
|
123
|
-
lines.push(`- **${s.name}**: ${s.description}`);
|
|
124
|
-
}
|
|
125
|
-
lines.push('');
|
|
126
|
-
}
|
|
127
|
-
lines.push('Use `skill_execute(skillName, objective)` for sync skills — result returned immediately.');
|
|
128
|
-
lines.push('Use `skill_delegate(skillName, objective)` for async skills — runs in background, notifies when done.');
|
|
129
|
-
return lines.join('\n');
|
|
110
|
+
const skillList = enabled
|
|
111
|
+
.map(s => `- **${s.name}**: ${s.description}`)
|
|
112
|
+
.join('\n');
|
|
113
|
+
return `## Available Skills
|
|
114
|
+
|
|
115
|
+
${skillList}
|
|
116
|
+
|
|
117
|
+
Use the \`load_skill\` tool when you need detailed instructions for handling a specific type of request.`;
|
|
130
118
|
}
|
|
131
119
|
/**
|
|
132
120
|
* Get skill names for tool description
|
|
@@ -15,10 +15,6 @@ export const SkillMetadataSchema = z.object({
|
|
|
15
15
|
.string()
|
|
16
16
|
.min(1, 'Description is required')
|
|
17
17
|
.max(500, 'Description must be at most 500 characters'),
|
|
18
|
-
execution_mode: z
|
|
19
|
-
.enum(['sync', 'async'])
|
|
20
|
-
.default('sync')
|
|
21
|
-
.describe('Execution mode: sync returns result inline, async creates background task'),
|
|
22
18
|
version: z
|
|
23
19
|
.string()
|
|
24
20
|
.regex(/^\d+\.\d+\.\d+$/, 'Version must be semver format (e.g., 1.0.0)')
|