Skip to content

Commit ed9350b

Browse files
author
Ubuntu
committed
feat: Implement Agent Skills MVP
Implements issue #155 - Agent Skills system following agentskills.io standard. ## Core Implementation ### Main Process - Add skillsService with discovery, loading, enable/disable - Add IPC handlers for all skills operations - Integrate skills context into system prompt builder - Auto-create ~/levante/skills/ directory on startup ### Types - Add Skill, SkillMetadata, SkillsConfig types - Add SkillValidationResult for validation ### Preload - Add skillsApi with list, get, enable, disable, toggle, refresh - Expose skills path getter ### Renderer - Add skillsStore (Zustand) for state management - Add SkillsPage with grid view of skills - Add SkillCard component with toggle switch - Add Skills item to sidebar navigation - Add en/es translations for skills UI ### Dependencies - Add gray-matter for YAML frontmatter parsing ## How It Works 1. Skills are folders in ~/levante/skills/ with a SKILL.md file 2. SKILL.md uses YAML frontmatter for metadata (name, description, etc) 3. Enabled skills are injected into the AI system prompt 4. Toggle skills on/off from the Skills page ## Skill Format (SKILL.md) ```yaml --- name: my-skill description: What this skill does version: 1.0.0 author: Author Name --- # Skill Instructions Markdown content that gets injected into the AI context. ``` Closes #155
1 parent a957391 commit ed9350b

File tree

20 files changed

+1138
-1
lines changed

20 files changed

+1138
-1
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
"electron-store": "^11.0.2",
8787
"embla-carousel-react": "^8.6.0",
8888
"fix-path": "^5.0.0",
89+
"gray-matter": "^4.0.3",
8990
"i18next": "^25.7.2",
9091
"iceberg-js": "^0.8.1",
9192
"input-otp": "^1.4.2",

src/main/ipc/skillsHandlers.ts

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
/**
2+
* Skills IPC Handlers
3+
*
4+
* Handles IPC communication for the skills system.
5+
*/
6+
7+
import { ipcMain } from 'electron';
8+
import { skillsService } from '../services/skills';
9+
import { getLogger } from '../services/logging';
10+
11+
const logger = getLogger();
12+
13+
export function registerSkillsHandlers(): void {
14+
logger.core.debug('Registering skills IPC handlers');
15+
16+
// List all skills
17+
ipcMain.handle('levante/skills/list', async () => {
18+
try {
19+
const skills = skillsService.getSkills();
20+
return { success: true, data: skills };
21+
} catch (error) {
22+
logger.core.error('Failed to list skills', {
23+
error: error instanceof Error ? error.message : error
24+
});
25+
return {
26+
success: false,
27+
error: error instanceof Error ? error.message : 'Failed to list skills'
28+
};
29+
}
30+
});
31+
32+
// Get a single skill
33+
ipcMain.handle('levante/skills/get', async (_, id: string) => {
34+
try {
35+
const skill = skillsService.getSkill(id);
36+
if (!skill) {
37+
return { success: false, error: 'Skill not found' };
38+
}
39+
return { success: true, data: skill };
40+
} catch (error) {
41+
logger.core.error('Failed to get skill', {
42+
id,
43+
error: error instanceof Error ? error.message : error
44+
});
45+
return {
46+
success: false,
47+
error: error instanceof Error ? error.message : 'Failed to get skill'
48+
};
49+
}
50+
});
51+
52+
// Get enabled skills
53+
ipcMain.handle('levante/skills/enabled', async () => {
54+
try {
55+
const skills = skillsService.getEnabledSkills();
56+
return { success: true, data: skills };
57+
} catch (error) {
58+
logger.core.error('Failed to get enabled skills', {
59+
error: error instanceof Error ? error.message : error
60+
});
61+
return {
62+
success: false,
63+
error: error instanceof Error ? error.message : 'Failed to get enabled skills'
64+
};
65+
}
66+
});
67+
68+
// Enable a skill
69+
ipcMain.handle('levante/skills/enable', async (_, id: string) => {
70+
try {
71+
const result = await skillsService.enableSkill(id);
72+
return { success: result };
73+
} catch (error) {
74+
logger.core.error('Failed to enable skill', {
75+
id,
76+
error: error instanceof Error ? error.message : error
77+
});
78+
return {
79+
success: false,
80+
error: error instanceof Error ? error.message : 'Failed to enable skill'
81+
};
82+
}
83+
});
84+
85+
// Disable a skill
86+
ipcMain.handle('levante/skills/disable', async (_, id: string) => {
87+
try {
88+
const result = await skillsService.disableSkill(id);
89+
return { success: result };
90+
} catch (error) {
91+
logger.core.error('Failed to disable skill', {
92+
id,
93+
error: error instanceof Error ? error.message : error
94+
});
95+
return {
96+
success: false,
97+
error: error instanceof Error ? error.message : 'Failed to disable skill'
98+
};
99+
}
100+
});
101+
102+
// Toggle a skill
103+
ipcMain.handle('levante/skills/toggle', async (_, id: string) => {
104+
try {
105+
const result = await skillsService.toggleSkill(id);
106+
return { success: result };
107+
} catch (error) {
108+
logger.core.error('Failed to toggle skill', {
109+
id,
110+
error: error instanceof Error ? error.message : error
111+
});
112+
return {
113+
success: false,
114+
error: error instanceof Error ? error.message : 'Failed to toggle skill'
115+
};
116+
}
117+
});
118+
119+
// Refresh skills (re-discover)
120+
ipcMain.handle('levante/skills/refresh', async () => {
121+
try {
122+
const skills = await skillsService.refresh();
123+
return { success: true, data: skills };
124+
} catch (error) {
125+
logger.core.error('Failed to refresh skills', {
126+
error: error instanceof Error ? error.message : error
127+
});
128+
return {
129+
success: false,
130+
error: error instanceof Error ? error.message : 'Failed to refresh skills'
131+
};
132+
}
133+
});
134+
135+
// Get skills path
136+
ipcMain.handle('levante/skills/path', async () => {
137+
try {
138+
const path = skillsService.getSkillsPath();
139+
return { success: true, data: path };
140+
} catch (error) {
141+
return {
142+
success: false,
143+
error: error instanceof Error ? error.message : 'Failed to get skills path'
144+
};
145+
}
146+
});
147+
148+
// Validate a skill
149+
ipcMain.handle('levante/skills/validate', async (_, id: string) => {
150+
try {
151+
const skill = skillsService.getSkill(id);
152+
if (!skill) {
153+
return { success: false, error: 'Skill not found' };
154+
}
155+
const result = skillsService.validateSkill(skill);
156+
return { success: true, data: result };
157+
} catch (error) {
158+
logger.core.error('Failed to validate skill', {
159+
id,
160+
error: error instanceof Error ? error.message : error
161+
});
162+
return {
163+
success: false,
164+
error: error instanceof Error ? error.message : 'Failed to validate skill'
165+
};
166+
}
167+
});
168+
169+
logger.core.debug('Skills IPC handlers registered');
170+
}

src/main/lifecycle/initialization.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import { setupAttachmentHandlers } from "../ipc/attachmentHandlers";
3030
import { registerAnalyticsHandlers } from "../ipc/analyticsHandlers";
3131
import { setupWidgetHandlers } from "../ipc/widgetHandlers";
3232
import { setupAnnouncementHandlers } from "../ipc/announcementHandlers";
33+
import { registerSkillsHandlers } from "../ipc/skillsHandlers";
34+
import { skillsService } from "../services/skills";
3335

3436
const logger = getLogger();
3537

@@ -104,6 +106,17 @@ export async function initializeServices(): Promise<void> {
104106
error: error instanceof Error ? error.message : error,
105107
});
106108
}
109+
110+
// 6. Initialize skills service
111+
try {
112+
await skillsService.initialize();
113+
logger.core.info("Skills service initialized successfully");
114+
} catch (error) {
115+
logger.core.error("Failed to initialize skills service", {
116+
error: error instanceof Error ? error.message : error,
117+
});
118+
// Continue with degraded functionality - skills are optional
119+
}
107120
}
108121

109122
/**
@@ -131,6 +144,7 @@ export async function registerIPCHandlers(getMainWindow: () => BrowserWindow | n
131144
setupOAuthHandlers();
132145
setupWidgetHandlers();
133146
setupAnnouncementHandlers();
147+
registerSkillsHandlers();
134148

135149
logger.core.info("All IPC handlers registered successfully");
136150
}

src/main/services/ai/systemPromptBuilder.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,22 @@ Example response format:
204204
Click the button above to add it. Once configured, I'll be able to help you with GitHub operations."`;
205205
}
206206

207+
// Add enabled skills context
208+
try {
209+
const { skillsService } = await import('../skills');
210+
const skillsContext = skillsService.getSkillsForPrompt();
211+
if (skillsContext) {
212+
systemPrompt += skillsContext;
213+
logger.aiSdk.debug('Skills context added to system prompt', {
214+
enabledSkillsCount: skillsService.getEnabledSkills().length
215+
});
216+
}
217+
} catch (error) {
218+
logger.aiSdk.debug('Failed to load skills context (skills may not be initialized)', {
219+
error: error instanceof Error ? error.message : error
220+
});
221+
}
222+
207223
// Debug log for final system prompt
208224
logger.aiSdk.debug('Final system prompt generated', {
209225
enabled: personalization?.enabled || false,

src/main/services/skills/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/**
2+
* Skills Service Exports
3+
*/
4+
5+
export { skillsService } from './skillsService';

0 commit comments

Comments
 (0)