diff --git a/web/app.py b/web/app.py index 0e3c098..86acdbe 100644 --- a/web/app.py +++ b/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. diff --git a/web/static/index.html b/web/static/index.html index a3e5008..a945b81 100644 --- a/web/static/index.html +++ b/web/static/index.html @@ -1293,6 +1293,16 @@ async function loadSkillList() { const updatedStr = s.updated_at ? new Date(s.updated_at).toLocaleDateString('he-IL') : '—'; const inDb = s.db_markdown_chars > 0; const onDisk = s.disk_exists; + // Action buttons + const actions = []; + if (onDisk && !inDb) { + actions.push(``); + } else if (onDisk && inDb) { + actions.push(``); + } + if (inDb) { + actions.push(``); + } return `