forked from farhoodlabs/paperclip
fix(skills): prune skills removed from GitHub/sks_sh sources on re-scan
When re-scanning existing sources, diff skills in the DB against the current source manifest. Skills no longer in the source are either: - Added to conflicts (if still used by agents) - Deleted via deleteSkill (if orphaned), added to skipped Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2016,7 +2016,7 @@ export function companySkillService(db: Db) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-scan GitHub/sks_sh sources to pick up newly added skills
|
// Re-scan GitHub/sks_sh sources to pick up newly added skills and prune removed ones
|
||||||
const sourceLocators = new Set<string>();
|
const sourceLocators = new Set<string>();
|
||||||
for (const skill of acceptedSkills) {
|
for (const skill of acceptedSkills) {
|
||||||
if (skill.sourceType !== "github" && skill.sourceType !== "skills_sh") continue;
|
if (skill.sourceType !== "github" && skill.sourceType !== "skills_sh") continue;
|
||||||
@@ -2026,6 +2026,9 @@ export function companySkillService(db: Db) {
|
|||||||
for (const sourceLocator of sourceLocators) {
|
for (const sourceLocator of sourceLocators) {
|
||||||
try {
|
try {
|
||||||
const result = await readUrlSkillImports(companyId, sourceLocator, null);
|
const result = await readUrlSkillImports(companyId, sourceLocator, null);
|
||||||
|
const currentSlugs = new Set(result.skills.map((s) => s.slug));
|
||||||
|
|
||||||
|
// Upsert any new skills found in the source
|
||||||
for (const nextSkill of result.skills) {
|
for (const nextSkill of result.skills) {
|
||||||
if (acceptedSkills.some((s) => s.slug === nextSkill.slug)) continue;
|
if (acceptedSkills.some((s) => s.slug === nextSkill.slug)) continue;
|
||||||
const persisted = (await upsertImportedSkills(companyId, [nextSkill]))[0];
|
const persisted = (await upsertImportedSkills(companyId, [nextSkill]))[0];
|
||||||
@@ -2034,6 +2037,25 @@ export function companySkillService(db: Db) {
|
|||||||
upsertAcceptedSkill(persisted);
|
upsertAcceptedSkill(persisted);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prune skills that are no longer in the source
|
||||||
|
const skillsAtSource = acceptedSkills.filter((s) => s.sourceLocator === sourceLocator);
|
||||||
|
for (const skill of skillsAtSource) {
|
||||||
|
if (currentSlugs.has(skill.slug)) continue;
|
||||||
|
const usedByAgents = await usage(companyId, skill.key);
|
||||||
|
if (usedByAgents.length > 0) {
|
||||||
|
conflicts.push({
|
||||||
|
path: sourceLocator,
|
||||||
|
existingSkillId: skill.id,
|
||||||
|
existingSkillKey: skill.key,
|
||||||
|
existingSourceLocator: sourceLocator,
|
||||||
|
reason: `Skill "${skill.slug}" was removed from ${sourceLocator} but is still used by ${usedByAgents.map((a) => a.name).join(", ")}. Detach it from those agents first.`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const deleted = await deleteSkill(companyId, skill.id);
|
||||||
|
if (deleted) skipped.push(deleted);
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Best-effort: don't fail the whole scan if one source fails
|
// Best-effort: don't fail the whole scan if one source fails
|
||||||
warnings.push(`Could not re-scan source ${sourceLocator} — skipping.`);
|
warnings.push(`Could not re-scan source ${sourceLocator} — skipping.`);
|
||||||
|
|||||||
Reference in New Issue
Block a user