Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline (Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/* packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
308 lines
9.5 KiB
TypeScript
308 lines
9.5 KiB
TypeScript
import { ORPCError } from "@orpc/server";
|
|
import { BUILTIN_AGENT_SKILLS } from "@rakazo/adapters";
|
|
import type { Actor, AgentSkill, AgentSkillSource } from "@rakazo/contracts";
|
|
import {
|
|
buildSkillMd,
|
|
findSkillByName,
|
|
isSkillReadOnly,
|
|
mergeBuiltinSkills,
|
|
parseSkillMd,
|
|
type SkillSource,
|
|
} from "@rakazo/core";
|
|
import { IsolationError, type PrismaClient } from "@rakazo/db";
|
|
|
|
type AgentSkillRow = {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
content: string;
|
|
source: string;
|
|
createdAt: Date;
|
|
updatedAt: Date;
|
|
};
|
|
|
|
function asSource(value: string): AgentSkillSource {
|
|
if (value === "builtin" || value === "plugin" || value === "user") return value;
|
|
return "user";
|
|
}
|
|
|
|
export function mapAgentSkill(row: AgentSkillRow): AgentSkill {
|
|
const source = asSource(row.source);
|
|
return {
|
|
id: row.id,
|
|
name: row.name,
|
|
description: row.description,
|
|
content: row.content,
|
|
source,
|
|
readOnly: isSkillReadOnly(source as SkillSource),
|
|
createdAt: row.createdAt.toISOString(),
|
|
updatedAt: row.updatedAt.toISOString(),
|
|
};
|
|
}
|
|
|
|
function builtinCatalog(): AgentSkill[] {
|
|
return BUILTIN_AGENT_SKILLS.map((skill) => ({
|
|
id: `builtin:${skill.name}`,
|
|
name: skill.name,
|
|
description: skill.description,
|
|
content: skill.content,
|
|
source: "builtin" as const,
|
|
readOnly: true,
|
|
createdAt: new Date(0).toISOString(),
|
|
updatedAt: new Date(0).toISOString(),
|
|
}));
|
|
}
|
|
|
|
export function resolveSkillContent(input: {
|
|
content?: string;
|
|
name?: string;
|
|
description?: string;
|
|
body?: string;
|
|
prior?: { content: string };
|
|
}): { name: string; description: string; content: string } {
|
|
const ensureContentLimit = (content: string): string => {
|
|
if (content.length > 100_000) {
|
|
throw new ORPCError("BAD_REQUEST", {
|
|
message: "Skill content must be at most 100000 characters.",
|
|
});
|
|
}
|
|
return content;
|
|
};
|
|
|
|
if (input.content?.trim()) {
|
|
const parsed = parseSkillMd(input.content);
|
|
if ("error" in parsed) {
|
|
throw new ORPCError("BAD_REQUEST", { message: parsed.error });
|
|
}
|
|
return {
|
|
name: parsed.name,
|
|
description: parsed.description,
|
|
content: ensureContentLimit(buildSkillMd(parsed)),
|
|
};
|
|
}
|
|
|
|
const priorParsed = input.prior ? parseSkillMd(input.prior.content) : null;
|
|
if (priorParsed && "error" in priorParsed) {
|
|
throw new ORPCError("BAD_REQUEST", { message: priorParsed.error });
|
|
}
|
|
|
|
const name = (input.name ?? priorParsed?.name ?? "").trim();
|
|
const description = (input.description ?? priorParsed?.description ?? "").trim();
|
|
const body = input.body ?? priorParsed?.body ?? "";
|
|
if (!name || !description) {
|
|
throw new ORPCError("BAD_REQUEST", {
|
|
message: "Provide content (SKILL.md) or name + description (+ optional body)",
|
|
});
|
|
}
|
|
let content: string;
|
|
try {
|
|
content = buildSkillMd({
|
|
name,
|
|
description,
|
|
body,
|
|
frontmatter: priorParsed && !("error" in priorParsed) ? priorParsed.frontmatter : undefined,
|
|
});
|
|
} catch (error) {
|
|
throw new ORPCError("BAD_REQUEST", {
|
|
message: error instanceof Error ? error.message : "Invalid skill fields.",
|
|
});
|
|
}
|
|
const validated = parseSkillMd(content);
|
|
if ("error" in validated) {
|
|
throw new ORPCError("BAD_REQUEST", { message: validated.error });
|
|
}
|
|
return {
|
|
name: validated.name,
|
|
description: validated.description,
|
|
content: ensureContentLimit(buildSkillMd(validated)),
|
|
};
|
|
}
|
|
|
|
export function createAgentSkillsService(prisma: PrismaClient) {
|
|
async function owned(actor: Actor, skillId: string) {
|
|
const row = await prisma.agentSkill.findFirst({
|
|
where: {
|
|
id: skillId,
|
|
spaceId: actor.spaceId,
|
|
userId: actor.userId,
|
|
},
|
|
});
|
|
if (!row) throw new IsolationError();
|
|
return row;
|
|
}
|
|
|
|
return {
|
|
async list(actor: Actor): Promise<Omit<AgentSkill, "content">[]> {
|
|
const rows = await prisma.agentSkill.findMany({
|
|
where: { spaceId: actor.spaceId, userId: actor.userId },
|
|
orderBy: [{ name: "asc" }, { id: "asc" }],
|
|
});
|
|
return mergeBuiltinSkills(builtinCatalog(), rows.map(mapAgentSkill)).map(
|
|
({ content: _content, ...entry }) => entry,
|
|
);
|
|
},
|
|
|
|
async listWithContent(actor: Actor): Promise<AgentSkill[]> {
|
|
const rows = await prisma.agentSkill.findMany({
|
|
where: { spaceId: actor.spaceId, userId: actor.userId },
|
|
orderBy: [{ name: "asc" }, { id: "asc" }],
|
|
});
|
|
return mergeBuiltinSkills(builtinCatalog(), rows.map(mapAgentSkill));
|
|
},
|
|
|
|
async get(actor: Actor, input: { skillId?: string; name?: string }): Promise<AgentSkill> {
|
|
if (input.skillId?.startsWith("builtin:")) {
|
|
const builtin = builtinCatalog().find((skill) => skill.id === input.skillId);
|
|
if (!builtin) throw new IsolationError();
|
|
return builtin;
|
|
}
|
|
if (input.skillId) {
|
|
return mapAgentSkill(await owned(actor, input.skillId));
|
|
}
|
|
const name = input.name?.trim() ?? "";
|
|
const rows = await prisma.agentSkill.findMany({
|
|
where: {
|
|
spaceId: actor.spaceId,
|
|
userId: actor.userId,
|
|
},
|
|
orderBy: [{ name: "asc" }, { id: "asc" }],
|
|
});
|
|
const row = findSkillByName(rows, name);
|
|
if (row) return mapAgentSkill(row);
|
|
const builtin = findSkillByName(builtinCatalog(), name);
|
|
if (!builtin) throw new IsolationError();
|
|
return builtin;
|
|
},
|
|
|
|
async create(
|
|
actor: Actor,
|
|
input: { content?: string; name?: string; description?: string; body?: string },
|
|
): Promise<AgentSkill> {
|
|
const resolved = resolveSkillContent(input);
|
|
const clash = await prisma.agentSkill.findFirst({
|
|
where: {
|
|
spaceId: actor.spaceId,
|
|
userId: actor.userId,
|
|
name: { equals: resolved.name, mode: "insensitive" },
|
|
},
|
|
});
|
|
if (
|
|
clash ||
|
|
builtinCatalog().some((s) => s.name.toLowerCase() === resolved.name.toLowerCase())
|
|
) {
|
|
throw new ORPCError("CONFLICT", { message: "A skill with that name already exists." });
|
|
}
|
|
try {
|
|
const row = await prisma.agentSkill.create({
|
|
data: {
|
|
spaceId: actor.spaceId,
|
|
userId: actor.userId,
|
|
name: resolved.name,
|
|
description: resolved.description,
|
|
content: resolved.content,
|
|
source: "user",
|
|
},
|
|
});
|
|
return mapAgentSkill(row);
|
|
} catch (error) {
|
|
if (
|
|
error instanceof Error &&
|
|
"code" in error &&
|
|
(error as { code?: string }).code === "P2002"
|
|
) {
|
|
throw new ORPCError("CONFLICT", { message: "A skill with that name already exists." });
|
|
}
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
async update(
|
|
actor: Actor,
|
|
input: {
|
|
skillId: string;
|
|
content?: string;
|
|
name?: string;
|
|
description?: string;
|
|
body?: string;
|
|
},
|
|
): Promise<AgentSkill> {
|
|
const existing = await owned(actor, input.skillId);
|
|
if (isSkillReadOnly(asSource(existing.source) as SkillSource)) {
|
|
throw new ORPCError("BAD_REQUEST", { message: "Builtin and plugin skills are read-only." });
|
|
}
|
|
const resolved = resolveSkillContent({ ...input, prior: existing });
|
|
if (!findSkillByName([existing], resolved.name)) {
|
|
const clash = await prisma.agentSkill.findFirst({
|
|
where: {
|
|
spaceId: actor.spaceId,
|
|
userId: actor.userId,
|
|
name: { equals: resolved.name, mode: "insensitive" },
|
|
NOT: { id: existing.id },
|
|
},
|
|
});
|
|
if (
|
|
clash ||
|
|
builtinCatalog().some((s) => s.name.toLowerCase() === resolved.name.toLowerCase())
|
|
) {
|
|
throw new ORPCError("CONFLICT", { message: "A skill with that name already exists." });
|
|
}
|
|
}
|
|
// Mutate only owner-scoped user rows (never builtin/plugin), even if source was tampered.
|
|
try {
|
|
const updated = await prisma.agentSkill.updateMany({
|
|
where: {
|
|
id: existing.id,
|
|
spaceId: actor.spaceId,
|
|
userId: actor.userId,
|
|
source: "user",
|
|
},
|
|
data: {
|
|
name: resolved.name,
|
|
description: resolved.description,
|
|
content: resolved.content,
|
|
},
|
|
});
|
|
if (updated.count !== 1) throw new IsolationError();
|
|
} catch (error) {
|
|
if (error instanceof IsolationError) throw error;
|
|
if (
|
|
error instanceof Error &&
|
|
"code" in error &&
|
|
(error as { code?: string }).code === "P2002"
|
|
) {
|
|
throw new ORPCError("CONFLICT", { message: "A skill with that name already exists." });
|
|
}
|
|
throw error;
|
|
}
|
|
const row = await prisma.agentSkill.findFirst({
|
|
where: {
|
|
id: existing.id,
|
|
spaceId: actor.spaceId,
|
|
userId: actor.userId,
|
|
},
|
|
});
|
|
if (!row) throw new IsolationError();
|
|
return mapAgentSkill(row);
|
|
},
|
|
|
|
async remove(actor: Actor, skillId: string): Promise<{ ok: true }> {
|
|
const existing = await owned(actor, skillId);
|
|
if (isSkillReadOnly(asSource(existing.source) as SkillSource)) {
|
|
throw new ORPCError("BAD_REQUEST", { message: "Builtin and plugin skills are read-only." });
|
|
}
|
|
const deleted = await prisma.agentSkill.deleteMany({
|
|
where: {
|
|
id: existing.id,
|
|
spaceId: actor.spaceId,
|
|
userId: actor.userId,
|
|
source: "user",
|
|
},
|
|
});
|
|
if (deleted.count !== 1) throw new IsolationError();
|
|
return { ok: true };
|
|
},
|
|
};
|
|
}
|
|
|
|
export type AgentSkillsService = ReturnType<typeof createAgentSkillsService>;
|