Add sync-to-DB and delete-from-DB actions for skills

- POST /api/admin/skills/{slug}/sync — read SKILL.md from disk, insert/update DB
- DELETE /api/admin/skills/{slug} — remove skill from DB (keeps disk files)
- UI: Sync/Re-sync and Delete buttons per skill in the skills list

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-08 17:52:00 +00:00
parent 2d265d2f0e
commit d8e888ad6a
2 changed files with 130 additions and 0 deletions

View File

@@ -994,6 +994,94 @@ async def api_install_skill(file: UploadFile = File(...)):
}
@app.post("/api/admin/skills/{slug}/sync")
async def api_sync_skill(slug: str):
"""Sync a skill from disk into the DB (for skills that exist on disk but not in DB)."""
skill_dir = PAPERCLIP_SKILLS_DIR / SKILLS_COMPANY_ID / slug
if not skill_dir.exists():
raise HTTPException(404, f"Skill directory not found on disk: {slug}")
skill_md_file = skill_dir / "SKILL.md"
if not skill_md_file.exists():
raise HTTPException(400, f"No SKILL.md found in {slug}")
markdown_content = skill_md_file.read_text(encoding="utf-8")
# Build file_inventory from disk
file_inventory = []
for f in sorted(skill_dir.rglob("*")):
if not f.is_file():
continue
rel = str(f.relative_to(skill_dir))
if rel.startswith(".") or "/__MACOSX/" in rel:
continue
if rel == "SKILL.md":
kind = "skill"
elif rel.startswith("scripts/"):
kind = "script"
elif rel.startswith("references/"):
kind = "reference"
elif rel.endswith(".zip"):
kind = "archive"
else:
kind = "resource"
file_inventory.append({"kind": kind, "path": rel})
conn = await asyncpg.connect(PAPERCLIP_DB_URL)
try:
existing = await conn.fetchval(
"SELECT id FROM company_skills WHERE company_id = $1::uuid AND slug = $2",
SKILLS_COMPANY_ID, slug,
)
if existing:
await conn.execute(
"""UPDATE company_skills
SET markdown = $1, file_inventory = $2::jsonb, updated_at = now()
WHERE id = $3""",
markdown_content,
json.dumps(file_inventory, ensure_ascii=False),
existing,
)
action = "updated"
else:
await conn.execute(
"""INSERT INTO company_skills
(company_id, key, slug, name, markdown, source_type, file_inventory)
VALUES ($1::uuid, $2, $3, $4, $5, 'local_path', $6::jsonb)""",
SKILLS_COMPANY_ID, slug, slug, slug,
markdown_content,
json.dumps(file_inventory, ensure_ascii=False),
)
action = "inserted"
finally:
await conn.close()
return {
"slug": slug,
"action": action,
"file_inventory": file_inventory,
"markdown_chars": len(markdown_content),
}
@app.delete("/api/admin/skills/{slug}")
async def api_delete_skill(slug: str):
"""Delete a skill from the DB. Does NOT delete files from disk."""
conn = await asyncpg.connect(PAPERCLIP_DB_URL)
try:
result = await conn.execute(
"DELETE FROM company_skills WHERE company_id = $1::uuid AND slug = $2",
SKILLS_COMPANY_ID, slug,
)
finally:
await conn.close()
if result == "DELETE 0":
raise HTTPException(404, f"Skill '{slug}' not found in DB")
return {"slug": slug, "action": "deleted"}
@app.post("/api/admin/paperclip/restart")
async def api_restart_paperclip():
"""Restart the Paperclip PM2 process.