import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js"; import { installSkill } from "../../agents/skills-install.js"; import { buildWorkspaceSkillStatus } from "../../agents/skills-status.js"; import { loadWorkspaceSkillEntries, type SkillEntry } from "../../agents/skills.js"; import type { ClawdbotConfig } from "../../config/config.js"; import { loadConfig, writeConfigFile } from "../../config/config.js"; import { getRemoteSkillEligibility } from "../../infra/skills-remote.js"; import { ErrorCodes, errorShape, formatValidationErrors, validateSkillsBinsParams, validateSkillsInstallParams, validateSkillsStatusParams, validateSkillsUpdateParams, } from "../protocol/index.js"; import type { GatewayRequestHandlers } from "./types.js"; function listWorkspaceDirs(cfg: ClawdbotConfig): string[] { const dirs = new Set(); const list = cfg.agents?.list; if (Array.isArray(list)) { for (const entry of list) { if (entry && typeof entry === "object" && typeof entry.id === "string") { dirs.add(resolveAgentWorkspaceDir(cfg, entry.id)); } } } dirs.add(resolveAgentWorkspaceDir(cfg, resolveDefaultAgentId(cfg))); return [...dirs]; } function collectSkillBins(entries: SkillEntry[]): string[] { const bins = new Set(); for (const entry of entries) { const required = entry.clawdbot?.requires?.bins ?? []; const anyBins = entry.clawdbot?.requires?.anyBins ?? []; const install = entry.clawdbot?.install ?? []; for (const bin of required) { const trimmed = bin.trim(); if (trimmed) bins.add(trimmed); } for (const bin of anyBins) { const trimmed = bin.trim(); if (trimmed) bins.add(trimmed); } for (const spec of install) { const specBins = spec?.bins ?? []; for (const bin of specBins) { const trimmed = String(bin).trim(); if (trimmed) bins.add(trimmed); } } } return [...bins].sort(); } export const skillsHandlers: GatewayRequestHandlers = { "skills.status": ({ params, respond }) => { if (!validateSkillsStatusParams(params)) { respond( false, undefined, errorShape( ErrorCodes.INVALID_REQUEST, `invalid skills.status params: ${formatValidationErrors(validateSkillsStatusParams.errors)}`, ), ); return; } const cfg = loadConfig(); const workspaceDir = resolveAgentWorkspaceDir(cfg, resolveDefaultAgentId(cfg)); const report = buildWorkspaceSkillStatus(workspaceDir, { config: cfg, eligibility: { remote: getRemoteSkillEligibility() }, }); respond(true, report, undefined); }, "skills.bins": ({ params, respond }) => { if (!validateSkillsBinsParams(params)) { respond( false, undefined, errorShape( ErrorCodes.INVALID_REQUEST, `invalid skills.bins params: ${formatValidationErrors(validateSkillsBinsParams.errors)}`, ), ); return; } const cfg = loadConfig(); const workspaceDirs = listWorkspaceDirs(cfg); const bins = new Set(); for (const workspaceDir of workspaceDirs) { const entries = loadWorkspaceSkillEntries(workspaceDir, { config: cfg }); for (const bin of collectSkillBins(entries)) bins.add(bin); } respond(true, { bins: [...bins].sort() }, undefined); }, "skills.install": async ({ params, respond }) => { if (!validateSkillsInstallParams(params)) { respond( false, undefined, errorShape( ErrorCodes.INVALID_REQUEST, `invalid skills.install params: ${formatValidationErrors(validateSkillsInstallParams.errors)}`, ), ); return; } const p = params as { name: string; installId: string; timeoutMs?: number; }; const cfg = loadConfig(); const workspaceDirRaw = resolveAgentWorkspaceDir(cfg, resolveDefaultAgentId(cfg)); const result = await installSkill({ workspaceDir: workspaceDirRaw, skillName: p.name, installId: p.installId, timeoutMs: p.timeoutMs, config: cfg, }); respond( result.ok, result, result.ok ? undefined : errorShape(ErrorCodes.UNAVAILABLE, result.message), ); }, "skills.update": async ({ params, respond }) => { if (!validateSkillsUpdateParams(params)) { respond( false, undefined, errorShape( ErrorCodes.INVALID_REQUEST, `invalid skills.update params: ${formatValidationErrors(validateSkillsUpdateParams.errors)}`, ), ); return; } const p = params as { skillKey: string; enabled?: boolean; apiKey?: string; env?: Record; }; const cfg = loadConfig(); const skills = cfg.skills ? { ...cfg.skills } : {}; const entries = skills.entries ? { ...skills.entries } : {}; const current = entries[p.skillKey] ? { ...entries[p.skillKey] } : {}; if (typeof p.enabled === "boolean") { current.enabled = p.enabled; } if (typeof p.apiKey === "string") { const trimmed = p.apiKey.trim(); if (trimmed) current.apiKey = trimmed; else delete current.apiKey; } if (p.env && typeof p.env === "object") { const nextEnv = current.env ? { ...current.env } : {}; for (const [key, value] of Object.entries(p.env)) { const trimmedKey = key.trim(); if (!trimmedKey) continue; const trimmedVal = value.trim(); if (!trimmedVal) delete nextEnv[trimmedKey]; else nextEnv[trimmedKey] = trimmedVal; } current.env = nextEnv; } entries[p.skillKey] = current; skills.entries = entries; const nextConfig: ClawdbotConfig = { ...cfg, skills, }; await writeConfigFile(nextConfig); respond(true, { ok: true, skillKey: p.skillKey, config: current }, undefined); }, };