multica/apps/desktop/electron/ipc/skills.ts
Jiang Bohan 2a4fcded03 feat(desktop): add IPC handlers for Hub, Tools, and Skills management
- Create hub.ts IPC handlers for Hub initialization and agent management
- Create agent.ts IPC handlers for tools list, toggle, setStatus, reload
- Create skills.ts IPC handlers for skills list, get, toggle, add, remove
- Expose typed electronAPI via preload.ts with contextBridge
- Add TypeScript definitions in electron-env.d.ts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 18:25:20 +08:00

278 lines
7.2 KiB
TypeScript

/**
* Skills IPC handlers for Electron main process.
*
* These handlers get skill information from the real Agent instance
* managed by the Hub.
*/
import { ipcMain } from 'electron'
import { getCurrentHub } from './hub.js'
/**
* Skill info returned to renderer.
*/
export interface SkillInfo {
id: string
name: string
description: string
version: string
enabled: boolean
source: 'bundled' | 'global' | 'profile'
triggers: string[]
}
/**
* Get the default agent from Hub.
*/
function getDefaultAgent() {
const hub = getCurrentHub()
if (!hub) return null
const agentIds = hub.listAgents()
if (agentIds.length === 0) return null
return hub.getAgent(agentIds[0]) ?? null
}
/**
* Get default bundled skills (fallback when no agent).
*/
function getDefaultSkills(): SkillInfo[] {
return [
{
id: 'commit',
name: 'Git Commit Helper',
description: 'Create well-formatted git commits following conventional commit standards',
version: '1.0.0',
enabled: true,
source: 'bundled',
triggers: ['/commit'],
},
{
id: 'code-review',
name: 'Code Review',
description: 'Review code for bugs, security issues, and best practices',
version: '1.0.0',
enabled: true,
source: 'bundled',
triggers: ['/review'],
},
{
id: 'skill-creator',
name: 'Skill Creator',
description: 'Create, edit, and manage custom skills',
version: '1.0.0',
enabled: true,
source: 'bundled',
triggers: ['/skill'],
},
{
id: 'profile-setup',
name: 'Profile Setup',
description: 'Interactive setup wizard to personalize your agent profile',
version: '1.0.0',
enabled: true,
source: 'bundled',
triggers: ['/profile'],
},
]
}
/**
* Register all Skills-related IPC handlers.
*/
export function registerSkillsIpcHandlers(): void {
/**
* Get list of all skills with their status.
* Returns skills from the real Agent instance.
*/
ipcMain.handle('skills:list', async () => {
const agent = getDefaultAgent()
if (!agent) {
// Fallback: return default skills when no agent
console.log('[IPC] skills:list - No agent available, returning defaults')
return getDefaultSkills()
}
try {
const skillsWithStatus = agent.getSkillsWithStatus()
// Transform to SkillInfo format
const skills: SkillInfo[] = skillsWithStatus.map((skill) => ({
id: skill.id,
name: skill.name,
description: skill.description,
version: '1.0.0', // Skills don't have version in current implementation
enabled: skill.eligible,
source: skill.source as 'bundled' | 'global' | 'profile',
triggers: [`/${skill.id}`], // Default trigger is /<skill-id>
}))
console.log(`[IPC] skills:list - Returning ${skills.length} skills from agent`)
return skills
} catch (err) {
console.error('[IPC] skills:list - Error getting skills from agent:', err)
return getDefaultSkills()
}
})
/**
* Toggle a skill's enabled status.
* NOTE: Skills eligibility is determined by requirements (env vars, binaries, etc.)
* This handler reports the current eligibility status.
*/
ipcMain.handle('skills:toggle', async (_event, skillId: string) => {
console.log(`[IPC] skills:toggle called for: ${skillId}`)
const agent = getDefaultAgent()
if (!agent) {
return { error: 'No agent available' }
}
const skillsWithStatus = agent.getSkillsWithStatus()
const skill = skillsWithStatus.find((s) => s.id === skillId)
if (!skill) {
return { error: `Skill not found: ${skillId}` }
}
// Skills can't be manually toggled - eligibility is based on requirements
// Return current status
return {
id: skillId,
enabled: skill.eligible,
reasons: skill.reasons,
}
})
/**
* Set a skill's enabled status explicitly.
* NOTE: Skills eligibility is automatic based on requirements.
* This handler is a no-op but returns current status.
*/
ipcMain.handle('skills:setStatus', async (_event, skillId: string, enabled: boolean) => {
console.log(`[IPC] skills:setStatus called for: ${skillId}, enabled: ${enabled}`)
const agent = getDefaultAgent()
if (!agent) {
return { error: 'No agent available' }
}
const skillsWithStatus = agent.getSkillsWithStatus()
const skill = skillsWithStatus.find((s) => s.id === skillId)
if (!skill) {
return { error: `Skill not found: ${skillId}` }
}
// TODO: Implement skill disable via config
// For now, just return current eligibility status
return {
id: skillId,
enabled: skill.eligible,
reasons: skill.reasons,
}
})
/**
* Get skill details by ID.
*/
ipcMain.handle('skills:get', async (_event, skillId: string) => {
const agent = getDefaultAgent()
if (!agent) {
// Fallback: check default skills
const defaults = getDefaultSkills()
const skill = defaults.find((s) => s.id === skillId)
if (skill) return skill
return { error: `Skill not found: ${skillId}` }
}
const skillsWithStatus = agent.getSkillsWithStatus()
const skill = skillsWithStatus.find((s) => s.id === skillId)
if (!skill) {
return { error: `Skill not found: ${skillId}` }
}
return {
id: skill.id,
name: skill.name,
description: skill.description,
version: '1.0.0',
enabled: skill.eligible,
source: skill.source as 'bundled' | 'global' | 'profile',
triggers: [`/${skill.id}`],
reasons: skill.reasons,
}
})
/**
* Reload skills from disk.
*/
ipcMain.handle('skills:reload', async () => {
const agent = getDefaultAgent()
if (!agent) {
return { error: 'No agent available' }
}
agent.reloadSkills()
console.log('[IPC] skills:reload - Skills reloaded')
return { ok: true }
})
/**
* Add a skill from GitHub repository.
* Source formats: owner/repo, owner/repo/skill-name, or full GitHub URL
*/
ipcMain.handle(
'skills:add',
async (
_event,
source: string,
options?: { name?: string; force?: boolean },
) => {
console.log(`[IPC] skills:add called: source=${source}, options=${JSON.stringify(options)}`)
const { addSkill } = await import('../../../../src/agent/skills/add.js')
const result = await addSkill({
source,
name: options?.name,
force: options?.force,
})
console.log(`[IPC] skills:add result: ${result.message}`)
// Reload skills in agent if available
const agent = getDefaultAgent()
if (agent && result.ok) {
agent.reloadSkills()
}
return result
},
)
/**
* Remove an installed skill by name.
*/
ipcMain.handle('skills:remove', async (_event, name: string) => {
console.log(`[IPC] skills:remove called: name=${name}`)
const { removeSkill } = await import('../../../../src/agent/skills/add.js')
const result = await removeSkill(name)
console.log(`[IPC] skills:remove result: ${result.message}`)
// Reload skills in agent if available
const agent = getDefaultAgent()
if (agent && result.ok) {
agent.reloadSkills()
}
return result
})
}