From 0f4c77adc905da5b295eaa2c2dc2a719609d4b25 Mon Sep 17 00:00:00 2001 From: PeninsulaInd Date: Thu, 19 Feb 2026 20:09:00 -0600 Subject: [PATCH] Add link building API endpoint and skill file - /api/linkbuilding/status endpoint returns pending, in-progress, completed, and failed pipeline states for dashboard consumption - skills/linkbuilding.md with YAML frontmatter linking tools and agents - Skill body documents workflow, triggers, default flags, and ClickUp fields Co-Authored-By: Claude Opus 4.6 --- cheddahbot/__main__.py | 111 ++++++++++++++++++++++++++++++++++++++--- skills/linkbuilding.md | 48 ++++++++++++++++++ 2 files changed, 152 insertions(+), 7 deletions(-) create mode 100644 skills/linkbuilding.md diff --git a/cheddahbot/__main__.py b/cheddahbot/__main__.py index 1144a23..e010e57 100644 --- a/cheddahbot/__main__.py +++ b/cheddahbot/__main__.py @@ -1,6 +1,8 @@ """Entry point: python -m cheddahbot""" +import json import logging +from pathlib import Path from .agent import Agent from .agent_registry import AgentRegistry @@ -131,13 +133,108 @@ def main(): log.warning("Scheduler not available: %s", e) log.info("Launching Gradio UI on %s:%s...", config.host, config.port) - app = create_ui(registry, config, default_llm, notification_bus=notification_bus) - app.launch( - server_name=config.host, - server_port=config.port, - pwa=True, - show_error=True, - ) + blocks = create_ui(registry, config, default_llm, notification_bus=notification_bus) + + # Build a parent FastAPI app so we can mount the dashboard alongside Gradio. + # Inserting routes into blocks.app before launch() doesn't work because + # launch()/mount_gradio_app() replaces the internal App instance. + import gradio as gr + import uvicorn + from fastapi import FastAPI + from fastapi.responses import RedirectResponse + from starlette.staticfiles import StaticFiles + + fastapi_app = FastAPI() + + # Mount the dashboard as static files (must come before Gradio's catch-all) + dashboard_dir = Path(__file__).resolve().parent.parent / "dashboard" + if dashboard_dir.is_dir(): + # Redirect /dashboard (no trailing slash) → /dashboard/ + @fastapi_app.get("/dashboard") + async def _dashboard_redirect(): + return RedirectResponse(url="/dashboard/") + + fastapi_app.mount( + "/dashboard", + StaticFiles(directory=str(dashboard_dir), html=True), + name="dashboard", + ) + log.info("Dashboard mounted at /dashboard/ (serving %s)", dashboard_dir) + + # Link building status API endpoint (for dashboard consumption) + @fastapi_app.get("/api/linkbuilding/status") + async def linkbuilding_status(): + """Return link building pipeline status for dashboard consumption.""" + result = { + "pending_cora_runs": [], + "in_progress": [], + "completed": [], + "failed": [], + } + + # Query KV store for tracked link building states + try: + for key, value in db.kv_scan("linkbuilding:watched:"): + try: + state = json.loads(value) + entry = { + "filename": state.get("filename", key.split(":")[-1]), + "status": state.get("status", "unknown"), + "task_id": state.get("task_id", ""), + } + if state.get("status") == "completed": + entry["completed_at"] = state.get("completed_at", "") + result["completed"].append(entry) + elif state.get("status") == "failed": + entry["error"] = state.get("error", "") + entry["failed_at"] = state.get("failed_at", "") + result["failed"].append(entry) + elif state.get("status") == "processing": + entry["started_at"] = state.get("started_at", "") + result["in_progress"].append(entry) + except json.JSONDecodeError: + pass + except Exception as e: + log.warning("Error reading linkbuilding KV state: %s", e) + + # Query ClickUp for pending tasks (to do + Link Building + Cora Backlinks) + if config.clickup.enabled: + try: + from .clickup import ClickUpClient + + cu = ClickUpClient( + api_token=config.clickup.api_token, + workspace_id=config.clickup.workspace_id, + task_type_field_name=config.clickup.task_type_field_name, + ) + try: + tasks = cu.get_tasks_from_space( + config.clickup.space_id, statuses=["to do"] + ) + for task in tasks: + if task.task_type != "Link Building": + continue + lb_method = task.custom_fields.get("LB Method", "") + if lb_method and lb_method != "Cora Backlinks": + continue + result["pending_cora_runs"].append({ + "keyword": task.custom_fields.get("Keyword", ""), + "url": task.custom_fields.get("IMSURL", ""), + "client": task.custom_fields.get("Client", ""), + "task_id": task.id, + "task_name": task.name, + }) + finally: + cu.close() + except Exception as e: + log.warning("Error querying ClickUp for pending link building: %s", e) + + return result + + # Mount Gradio at the root + gr.mount_gradio_app(fastapi_app, blocks, path="/", pwa=True, show_error=True) + + uvicorn.run(fastapi_app, host=config.host, port=config.port) if __name__ == "__main__": diff --git a/skills/linkbuilding.md b/skills/linkbuilding.md new file mode 100644 index 0000000..b722d39 --- /dev/null +++ b/skills/linkbuilding.md @@ -0,0 +1,48 @@ +--- +name: link-building +description: Automated link building pipeline using Big-Link-Man CLI. Orchestrates CORA report ingestion and content generation for tiered link building campaigns. Use when the user asks about link building, Cora backlinks, content generation for SEO, or managing the link building pipeline. +tools: [run_link_building, run_cora_backlinks, blm_ingest_cora, blm_generate_batch, scan_cora_folder] +agents: [link_builder, default] +--- + +# Link Building Pipeline + +This skill automates the link building workflow using the Big-Link-Man CLI tool. It handles CORA report ingestion and batch content generation for tiered backlink campaigns. + +## Workflow + +### Full Pipeline (Cora Backlinks) + +1. **Ingest CORA Report**: Parse a `.xlsx` CORA report to create a project with keyword data, entities, and related searches +2. **Generate Content Batch**: Produce tiered content articles based on the ingested project data + +### Triggers + +- **Folder Watcher**: Automatically picks up new `.xlsx` files from `Z:/cora-inbox` and matches them to ClickUp tasks by keyword +- **Chat Command**: User can directly request link building via chat (e.g., "Run link building for precision-cnc-machining.xlsx") +- **ClickUp Task**: Tasks with Work Category = "Link Building" are tracked and executed when a matching CORA file appears + +### Default Flags + +- `-m` (money site URL): Always passed to prevent interactive prompts +- `-bp` (branded plus ratio): Defaults to 0.7 unless overridden +- `--continue-on-error`: Always set on generate-batch + +### Available Tools + +- `run_link_building` — Orchestrator that routes to the correct pipeline based on LB Method +- `run_cora_backlinks` — Full pipeline: ingest + generate +- `blm_ingest_cora` — Standalone ingest (creates project, returns job file) +- `blm_generate_batch` — Standalone generate (processes existing job file) +- `scan_cora_folder` — Check what's in the watch folder and their processing status + +### ClickUp Integration + +Link building tasks use these custom fields: +- **LB Method**: Pipeline type (currently "Cora Backlinks") +- **Keyword**: Target keyword (used for file matching) +- **IMSURL**: Money site URL +- **CustomAnchors**: Comma-separated anchor text overrides +- **BrandedPlusRatio**: Override for branded+ ratio +- **CLIFlags**: Additional CLI flags +- **CoraFile**: Path to .xlsx file (set by agent after match)