feat(11-03): add skill groups frontend API client and query keys

- Create ui/src/api/skillGroups.ts with skillGroupsApi object
- All 14 methods covering group CRUD, members, export/import, agent assignments
- removeGroup uses raw fetch for DELETE-with-body (api.delete has no body support)
- Add skillGroups namespace to ui/src/lib/queryKeys.ts with 6 key factories
This commit is contained in:
Mikkel Georgsen 2026-04-01 03:32:17 +02:00 committed by Nexus Dev
parent 155b973e03
commit bd38700876
2 changed files with 101 additions and 0 deletions

93
ui/src/api/skillGroups.ts Normal file
View file

@ -0,0 +1,93 @@
import { api, ApiError } from "./client";
export type SkillGroupRow = {
id: string;
name: string;
description: string | null;
isBuiltin: number;
createdAt: number;
updatedAt: number;
};
export type GroupMemberRow = {
groupId: string;
skillId: string;
addedAt: number;
};
export type AssignResult = {
installed: string[];
skipped: string[];
pendingPlugin: string[];
};
export type GroupExport = {
version: "1";
group: {
id: string;
name: string;
description: string | null;
members: string[];
parents: string[];
};
};
export const skillGroupsApi = {
listGroups: () => api.get<SkillGroupRow[]>("/skill-registry/groups"),
getGroup: (groupId: string) =>
api.get<SkillGroupRow>(`/skill-registry/groups/${groupId}`),
createGroup: (input: { name: string; description?: string }) =>
api.post<SkillGroupRow>("/skill-registry/groups", input),
updateGroup: (groupId: string, patch: { name?: string; description?: string }) =>
api.patch<SkillGroupRow>(`/skill-registry/groups/${groupId}`, patch),
deleteGroup: (groupId: string) =>
api.delete<void>(`/skill-registry/groups/${groupId}`),
listMembers: (groupId: string) =>
api.get<GroupMemberRow[]>(`/skill-registry/groups/${groupId}/members`),
addMember: (groupId: string, skillId: string) =>
api.post<{ ok: boolean }>(`/skill-registry/groups/${groupId}/members`, { skillId }),
removeMember: (groupId: string, skillId: string) =>
api.delete<void>(`/skill-registry/groups/${groupId}/members/${skillId}`),
exportGroup: (groupId: string) =>
api.get<GroupExport>(`/skill-registry/groups/${groupId}/export`),
importGroup: (data: GroupExport) =>
api.post<SkillGroupRow>("/skill-registry/groups/import", data),
listAgentGroups: (agentId: string) =>
api.get<SkillGroupRow[]>(`/skill-registry/agents/${agentId}/groups`),
assignGroup: (agentId: string, groupId: string, agentSkillsDir: string) =>
api.post<AssignResult>(`/skill-registry/agents/${agentId}/groups`, {
groupId,
agentSkillsDir,
}),
removeGroup: async (agentId: string, groupId: string, agentSkillsDir: string): Promise<void> => {
const res = await fetch(`/api/skill-registry/agents/${agentId}/groups/${groupId}`, {
method: "DELETE",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ agentSkillsDir }),
});
if (!res.ok && res.status !== 204) {
const errorBody = await res.json().catch(() => null);
throw new ApiError(
(errorBody as { error?: string } | null)?.error ?? `Request failed: ${res.status}`,
res.status,
errorBody,
);
}
},
listAgentSkills: (agentId: string) =>
api.get<string[]>(`/skill-registry/agents/${agentId}/skills`),
};

View file

@ -139,6 +139,14 @@ export const queryKeys = {
detail: (skillId: string) => ["skill-registry", "skills", skillId] as const,
versions: (skillId: string) => ["skill-registry", "skills", skillId, "versions"] as const,
},
skillGroups: {
list: ["skill-groups"] as const,
detail: (groupId: string) => ["skill-groups", groupId] as const,
members: (groupId: string) => ["skill-groups", groupId, "members"] as const,
agentGroups: (agentId: string) => ["skill-groups", "agent", agentId] as const,
agentSkills: (agentId: string) => ["skill-groups", "agent", agentId, "skills"] as const,
agentEffective: (agentId: string) => ["skill-groups", "agent", agentId, "effective"] as const,
},
plugins: {
all: ["plugins"] as const,
examples: ["plugins", "examples"] as const,