Add DELETE endpoint for company skills and fix skills.sh URL resolution

- Add DELETE /api/companies/:companyId/skills/:skillId endpoint with same
  permission model as other skill mutations. Deleting a skill removes it
  from the DB, cleans up materialized runtime files, and automatically
  strips it from any agent desiredSkills that reference it.
- Fix parseSkillImportSourceInput to detect skills.sh URLs
  (e.g. https://skills.sh/org/repo/skill) and resolve them to the
  underlying GitHub repo + skill slug, instead of fetching the HTML page.
- Add tests for skills.sh URL resolution with and without skill slug.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
dotta
2026-03-19 12:05:27 -05:00
parent 8edff22c0b
commit ce69ebd2ec
3 changed files with 102 additions and 1 deletions
+55 -1
View File
@@ -5,7 +5,7 @@ import { fileURLToPath } from "node:url";
import { and, asc, eq } from "drizzle-orm";
import type { Db } from "@paperclipai/db";
import { companySkills } from "@paperclipai/db";
import { readPaperclipSkillSyncPreference } from "@paperclipai/adapter-utils/server-utils";
import { readPaperclipSkillSyncPreference, writePaperclipSkillSyncPreference } from "@paperclipai/adapter-utils/server-utils";
import type { PaperclipSkillEntry } from "@paperclipai/adapter-utils/server-utils";
import type {
CompanySkill,
@@ -578,6 +578,17 @@ export function parseSkillImportSourceInput(rawInput: string): ParsedSkillImport
};
}
// Detect skills.sh URLs and resolve to GitHub: https://skills.sh/org/repo/skill → org/repo/skill key
const skillsShMatch = normalizedSource.match(/^https?:\/\/(?:www\.)?skills\.sh\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)(?:\/([A-Za-z0-9_.-]+))?(?:[?#].*)?$/i);
if (skillsShMatch) {
const [, owner, repo, skillSlugRaw] = skillsShMatch;
return {
resolvedSource: `https://github.com/${owner}/${repo}`,
requestedSkillSlug: skillSlugRaw ? normalizeSkillSlug(skillSlugRaw) : requestedSkillSlug,
warnings,
};
}
return {
resolvedSource: normalizedSource,
requestedSkillSlug,
@@ -2195,6 +2206,48 @@ export function companySkillService(db: Db) {
return { imported, warnings };
}
async function deleteSkill(companyId: string, skillId: string): Promise<CompanySkill | null> {
const row = await db
.select()
.from(companySkills)
.where(and(eq(companySkills.id, skillId), eq(companySkills.companyId, companyId)))
.then((rows) => rows[0] ?? null);
if (!row) return null;
const skill = toCompanySkill(row);
// Remove from any agent desiredSkills that reference this skill
const agentRows = await agents.list(companyId);
const allSkills = await listFull(companyId);
for (const agent of agentRows) {
const config = agent.adapterConfig as Record<string, unknown>;
const preference = readPaperclipSkillSyncPreference(config);
const referencesSkill = preference.desiredSkills.some((ref) => {
const resolved = resolveSkillReference(allSkills, ref);
return resolved.skill?.id === skillId;
});
if (referencesSkill) {
const filtered = preference.desiredSkills.filter((ref) => {
const resolved = resolveSkillReference(allSkills, ref);
return resolved.skill?.id !== skillId;
});
await agents.update(agent.id, {
adapterConfig: writePaperclipSkillSyncPreference(config, filtered),
});
}
}
// Delete DB row
await db
.delete(companySkills)
.where(eq(companySkills.id, skillId));
// Clean up materialized runtime files
await fs.rm(resolveRuntimeSkillMaterializedPath(companyId, skill), { recursive: true, force: true });
return skill;
}
return {
list,
listFull,
@@ -2209,6 +2262,7 @@ export function companySkillService(db: Db) {
readFile,
updateFile,
createLocalSkill,
deleteSkill,
importFromSource,
scanProjectWorkspaces,
importPackageFiles,