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:
88
web/app.py
88
web/app.py
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user