import { Router, type Request } from "express"; import type { Db } from "@paperclipai/db"; import { companySkillCreateSchema, companySkillFileUpdateSchema, companySkillImportSchema, companySkillProjectScanRequestSchema, } from "@paperclipai/shared"; import { validate } from "../middleware/validate.js"; import { accessService, agentService, companySkillService, logActivity } from "../services/index.js"; import { forbidden } from "../errors.js"; import { assertCompanyAccess, getActorInfo } from "./authz.js"; export function companySkillRoutes(db: Db) { const router = Router(); const agents = agentService(db); const access = accessService(db); const svc = companySkillService(db); function canCreateAgents(agent: { permissions: Record | null | undefined }) { if (!agent.permissions || typeof agent.permissions !== "object") return false; return Boolean((agent.permissions as Record).canCreateAgents); } async function assertCanMutateCompanySkills(req: Request, companyId: string) { assertCompanyAccess(req, companyId); if (req.actor.type === "board") { if (req.actor.source === "local_implicit" || req.actor.isInstanceAdmin) return; const allowed = await access.canUser(companyId, req.actor.userId, "agents:create"); if (!allowed) { throw forbidden("Missing permission: agents:create"); } return; } if (!req.actor.agentId) { throw forbidden("Agent authentication required"); } const actorAgent = await agents.getById(req.actor.agentId); if (!actorAgent || actorAgent.companyId !== companyId) { throw forbidden("Agent key cannot access another company"); } const allowedByGrant = await access.hasPermission(companyId, "agent", actorAgent.id, "agents:create"); if (allowedByGrant || canCreateAgents(actorAgent)) { return; } throw forbidden("Missing permission: can create agents"); } router.get("/companies/:companyId/skills", async (req, res) => { const companyId = req.params.companyId as string; assertCompanyAccess(req, companyId); const result = await svc.list(companyId); res.json(result); }); router.get("/companies/:companyId/skills/:skillId", async (req, res) => { const companyId = req.params.companyId as string; const skillId = req.params.skillId as string; assertCompanyAccess(req, companyId); const result = await svc.detail(companyId, skillId); if (!result) { res.status(404).json({ error: "Skill not found" }); return; } res.json(result); }); router.get("/companies/:companyId/skills/:skillId/update-status", async (req, res) => { const companyId = req.params.companyId as string; const skillId = req.params.skillId as string; assertCompanyAccess(req, companyId); const result = await svc.updateStatus(companyId, skillId); if (!result) { res.status(404).json({ error: "Skill not found" }); return; } res.json(result); }); router.get("/companies/:companyId/skills/:skillId/files", async (req, res) => { const companyId = req.params.companyId as string; const skillId = req.params.skillId as string; const relativePath = String(req.query.path ?? "SKILL.md"); assertCompanyAccess(req, companyId); const result = await svc.readFile(companyId, skillId, relativePath); if (!result) { res.status(404).json({ error: "Skill not found" }); return; } res.json(result); }); router.post( "/companies/:companyId/skills", validate(companySkillCreateSchema), async (req, res) => { const companyId = req.params.companyId as string; await assertCanMutateCompanySkills(req, companyId); const result = await svc.createLocalSkill(companyId, req.body); const actor = getActorInfo(req); await logActivity(db, { companyId, actorType: actor.actorType, actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, action: "company.skill_created", entityType: "company_skill", entityId: result.id, details: { slug: result.slug, name: result.name, }, }); res.status(201).json(result); }, ); router.patch( "/companies/:companyId/skills/:skillId/files", validate(companySkillFileUpdateSchema), async (req, res) => { const companyId = req.params.companyId as string; const skillId = req.params.skillId as string; await assertCanMutateCompanySkills(req, companyId); const result = await svc.updateFile( companyId, skillId, String(req.body.path ?? ""), String(req.body.content ?? ""), ); const actor = getActorInfo(req); await logActivity(db, { companyId, actorType: actor.actorType, actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, action: "company.skill_file_updated", entityType: "company_skill", entityId: skillId, details: { path: result.path, markdown: result.markdown, }, }); res.json(result); }, ); router.post( "/companies/:companyId/skills/import", validate(companySkillImportSchema), async (req, res) => { const companyId = req.params.companyId as string; await assertCanMutateCompanySkills(req, companyId); const source = String(req.body.source ?? ""); const result = await svc.importFromSource(companyId, source); const actor = getActorInfo(req); await logActivity(db, { companyId, actorType: actor.actorType, actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, action: "company.skills_imported", entityType: "company", entityId: companyId, details: { source, importedCount: result.imported.length, importedSlugs: result.imported.map((skill) => skill.slug), warningCount: result.warnings.length, }, }); res.status(201).json(result); }, ); router.post( "/companies/:companyId/skills/scan-projects", validate(companySkillProjectScanRequestSchema), async (req, res) => { const companyId = req.params.companyId as string; await assertCanMutateCompanySkills(req, companyId); const result = await svc.scanProjectWorkspaces(companyId, req.body); const actor = getActorInfo(req); await logActivity(db, { companyId, actorType: actor.actorType, actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, action: "company.skills_scanned", entityType: "company", entityId: companyId, details: { scannedProjects: result.scannedProjects, scannedWorkspaces: result.scannedWorkspaces, discovered: result.discovered, importedCount: result.imported.length, updatedCount: result.updated.length, conflictCount: result.conflicts.length, warningCount: result.warnings.length, }, }); res.json(result); }, ); router.delete("/companies/:companyId/skills/:skillId", async (req, res) => { const companyId = req.params.companyId as string; const skillId = req.params.skillId as string; await assertCanMutateCompanySkills(req, companyId); const result = await svc.deleteSkill(companyId, skillId); if (!result) { res.status(404).json({ error: "Skill not found" }); return; } const actor = getActorInfo(req); await logActivity(db, { companyId, actorType: actor.actorType, actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, action: "company.skill_deleted", entityType: "company_skill", entityId: result.id, details: { slug: result.slug, name: result.name, }, }); res.json(result); }); router.post("/companies/:companyId/skills/:skillId/install-update", async (req, res) => { const companyId = req.params.companyId as string; const skillId = req.params.skillId as string; await assertCanMutateCompanySkills(req, companyId); const result = await svc.installUpdate(companyId, skillId); if (!result) { res.status(404).json({ error: "Skill not found" }); return; } const actor = getActorInfo(req); await logActivity(db, { companyId, actorType: actor.actorType, actorId: actor.actorId, agentId: actor.agentId, runId: actor.runId, action: "company.skill_update_installed", entityType: "company_skill", entityId: result.id, details: { slug: result.slug, sourceRef: result.sourceRef, }, }); res.json(result); }); return router; }