website-builder-backend / scripts /197_add_missing_backend_contracts.sh
David Prince
fix: add all missing local module stubs (remote_mcp_registry, mcp_auth, mcp_transport, etc)
cce8120
Raw
History Blame Contribute Delete
22.5 kB
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT"
echo "============================================================"
echo " DOLOR3V BACKEND β€” MISSING CONTRACTS + REAL TOOL REPAIR"
echo "============================================================"
echo
echo "===== 1. FIX app.py cms_router IMPORT CRASH ====="
python3 - <<'PY'
from pathlib import Path
p = Path("app.py")
src = p.read_text()
# Remove the broken cms_router line if it's there without an import
if "app.include_router(cms_router)" in src and "from" not in src.split("cms_router")[0].split("\n")[-1]:
src = src.replace("app.include_router(cms_router)\n", "")
p.write_text(src)
print("[FIXED] Removed unresolved cms_router from app.py")
else:
print("[OK] app.py cms_router already handled or import present")
PY
echo
echo "===== 2. ADD /api/deploy/settings ROUTE ====="
mkdir -p backend/api/routes
cat > backend/api/routes/deploy_settings.py << 'PYEOF'
"""
Production deployment-settings route.
GET /api/deploy/settings β€” returns current deployment configuration
POST /api/deploy/settings β€” updates deployment configuration
"""
from __future__ import annotations
import json
import logging
import os
from pathlib import Path
from typing import Any
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
logger = logging.getLogger("dolor3v.deploy.settings")
router = APIRouter(prefix="/api/deploy", tags=["deploy"])
SETTINGS_FILE = Path(os.environ.get("DEPLOY_SETTINGS_PATH", "/tmp/deploy_settings.json"))
DEFAULTS: dict[str, Any] = {
"target": os.environ.get("DEPLOY_TARGET", "cloudflare"),
"cloudflare_account_id": os.environ.get("CLOUDFLARE_ACCOUNT_ID", ""),
"cloudflare_api_token": "",
"render_service_id": os.environ.get("RENDER_SERVICE_ID", ""),
"hf_space": os.environ.get("HF_SPACE", "Daviddolor/Travelerdev"),
"auto_deploy": False,
"build_command": "npm run build",
"output_dir": ".next",
"environment": os.environ.get("ENVIRONMENT", "production"),
"backend_url": os.environ.get(
"TRAVELER_BACKEND_URL",
os.environ.get("NEXT_PUBLIC_BACKEND_URL", ""),
),
}
def _load() -> dict[str, Any]:
if SETTINGS_FILE.exists():
try:
return {**DEFAULTS, **json.loads(SETTINGS_FILE.read_text())}
except Exception:
pass
return dict(DEFAULTS)
def _save(data: dict[str, Any]) -> None:
SETTINGS_FILE.parent.mkdir(parents=True, exist_ok=True)
SETTINGS_FILE.write_text(json.dumps(data, indent=2))
class DeploySettingsUpdate(BaseModel):
target: str | None = None
cloudflare_account_id: str | None = None
cloudflare_api_token: str | None = None
render_service_id: str | None = None
hf_space: str | None = None
auto_deploy: bool | None = None
build_command: str | None = None
output_dir: str | None = None
environment: str | None = None
backend_url: str | None = None
@router.get("/settings")
async def get_deploy_settings() -> dict[str, Any]:
"""Return current deployment configuration (secrets redacted)."""
settings = _load()
redacted = {**settings}
if redacted.get("cloudflare_api_token"):
redacted["cloudflare_api_token"] = "***"
return {"success": True, "settings": redacted}
@router.post("/settings")
async def update_deploy_settings(body: DeploySettingsUpdate) -> dict[str, Any]:
"""Persist deployment configuration updates."""
current = _load()
updates = body.model_dump(exclude_none=True)
current.update(updates)
try:
_save(current)
except Exception as exc:
raise HTTPException(status_code=500, detail=f"Failed to persist settings: {exc}") from exc
return {"success": True, "updated": list(updates.keys())}
PYEOF
echo "[CREATED] backend/api/routes/deploy_settings.py"
echo
echo "===== 3. REAL GITHUB SEARCH TOOL ====="
cat > backend/tools/github_search.py << 'PYEOF'
"""
Real GitHub repository + code search via GitHub REST API.
No auth required for public repos (60 req/hr).
Set GH_TOKEN or GITHUB_TOKEN env var for 5000 req/hr.
"""
from __future__ import annotations
import logging
import os
import time
from typing import Any
import httpx
logger = logging.getLogger("dolor3v.tools.github_search")
_GITHUB_API = "https://api.github.com"
_HEADERS_BASE = {
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
"User-Agent": "DOLOR3V-TravelerDev/1.0",
}
def _auth_headers() -> dict[str, str]:
token = os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN", "")
if token:
return {**_HEADERS_BASE, "Authorization": f"Bearer {token}"}
return dict(_HEADERS_BASE)
async def search_repositories(
query: str,
sort: str = "stars",
order: str = "desc",
per_page: int = 10,
) -> dict[str, Any]:
"""Search GitHub repositories. Returns real API data."""
url = f"{_GITHUB_API}/search/repositories"
params = {"q": query, "sort": sort, "order": order, "per_page": per_page}
t0 = time.monotonic()
async with httpx.AsyncClient(timeout=15.0) as client:
resp = await client.get(url, params=params, headers=_auth_headers())
latency_ms = round((time.monotonic() - t0) * 1000)
if resp.status_code == 403:
remaining = resp.headers.get("X-RateLimit-Remaining", "?")
reset = resp.headers.get("X-RateLimit-Reset", "?")
return {
"error": "GitHub rate limit exceeded",
"remaining": remaining,
"reset_at": reset,
"tip": "Set GH_TOKEN or GITHUB_TOKEN env var for 5000 req/hr",
}
if resp.status_code != 200:
return {
"error": f"GitHub API returned {resp.status_code}",
"body": resp.text[:500],
}
data = resp.json()
items = data.get("items", [])
results = []
for item in items:
results.append({
"name": item.get("full_name"),
"description": item.get("description"),
"stars": item.get("stargazers_count"),
"forks": item.get("forks_count"),
"language": item.get("language"),
"url": item.get("html_url"),
"clone_url": item.get("clone_url"),
"topics": item.get("topics", []),
"updated_at": item.get("updated_at"),
"open_issues": item.get("open_issues_count"),
})
return {
"query": query,
"total_count": data.get("total_count", 0),
"returned": len(results),
"latency_ms": latency_ms,
"results": results,
"rate_limit_remaining": resp.headers.get("X-RateLimit-Remaining", "unknown"),
}
async def search_code(
query: str,
per_page: int = 10,
) -> dict[str, Any]:
"""Search GitHub code. Requires GH_TOKEN for reliable access."""
url = f"{_GITHUB_API}/search/code"
params = {"q": query, "per_page": per_page}
token = os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN", "")
if not token:
return {
"error": "Code search requires authentication",
"tip": "Set GH_TOKEN env var",
}
t0 = time.monotonic()
async with httpx.AsyncClient(timeout=15.0) as client:
resp = await client.get(url, params=params, headers=_auth_headers())
latency_ms = round((time.monotonic() - t0) * 1000)
if resp.status_code != 200:
return {"error": f"GitHub code search returned {resp.status_code}", "body": resp.text[:500]}
data = resp.json()
items = data.get("items", [])
return {
"query": query,
"total_count": data.get("total_count", 0),
"latency_ms": latency_ms,
"results": [
{
"name": i.get("name"),
"path": i.get("path"),
"repo": i.get("repository", {}).get("full_name"),
"url": i.get("html_url"),
"sha": i.get("sha"),
}
for i in items
],
}
PYEOF
echo "[CREATED] backend/tools/github_search.py"
echo
echo "===== 4. REAL ANDROID DEVELOPER DOCS LOOKUP ====="
cat > backend/tools/android_docs.py << 'PYEOF'
"""
Real Android Developer documentation lookup.
Fetches from developer.android.com search and reference pages.
No authentication required.
"""
from __future__ import annotations
import logging
import re
import time
from typing import Any
from urllib.parse import quote_plus
import httpx
logger = logging.getLogger("dolor3v.tools.android_docs")
_BASE = "https://developer.android.com"
_SEARCH_URL = f"{_BASE}/s/results"
_HEADERS = {
"User-Agent": (
"Mozilla/5.0 (Linux; Android 14; Pixel 8) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/124.0.0.0 Mobile Safari/537.36"
),
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.5",
}
# Curated fast-path: well-known Android API references
_KNOWN_REFS: dict[str, str] = {
"activity": "/reference/android/app/Activity",
"fragment": "/reference/androidx/fragment/app/Fragment",
"viewmodel": "/reference/androidx/lifecycle/ViewModel",
"livedata": "/reference/androidx/lifecycle/LiveData",
"room": "/reference/androidx/room/package-summary",
"compose": "/jetpack/compose",
"navigation": "/guide/navigation",
"workmanager": "/reference/androidx/work/WorkManager",
"coroutines": "/kotlin/coroutines",
"hilt": "/training/dependency-injection/hilt-android",
"retrofit": "https://square.github.io/retrofit/",
"jetpack": "/jetpack",
"manifest": "/guide/topics/manifest/manifest-intro",
"permissions": "/guide/topics/permissions/overview",
"intent": "/reference/android/content/Intent",
"service": "/reference/android/app/Service",
"broadcastreceiver": "/reference/android/content/BroadcastReceiver",
"contentprovider": "/reference/android/content/ContentProvider",
"recyclerview": "/reference/androidx/recyclerview/widget/RecyclerView",
"constraintlayout": "/reference/androidx/constraintlayout/widget/ConstraintLayout",
"gradle": "/build/releases/gradle-plugin",
"apk": "/studio/build/build-variants",
"aab": "/guide/app-bundle",
"proguard": "/studio/build/shrink-code",
"keystore": "/training/articles/keystore",
}
def _fast_path(query: str) -> str | None:
q = query.lower().strip()
for key, path in _KNOWN_REFS.items():
if key in q:
return path if path.startswith("http") else f"{_BASE}{path}"
return None
async def lookup_android_docs(
query: str,
max_results: int = 5,
) -> dict[str, Any]:
"""
Fetch real Android Developer documentation.
Tries fast-path known references first, then falls back to search.
"""
t0 = time.monotonic()
# Fast path for known APIs
fast_url = _fast_path(query)
results = []
async with httpx.AsyncClient(
timeout=20.0,
follow_redirects=True,
headers=_HEADERS,
) as client:
if fast_url:
try:
resp = await client.get(fast_url)
if resp.status_code == 200:
# Extract title and description from HTML
html = resp.text
title_match = re.search(r"<title[^>]*>([^<]+)</title>", html, re.I)
title = title_match.group(1).strip() if title_match else query
# Extract meta description
desc_match = re.search(
r'<meta[^>]+name=["\']description["\'][^>]+content=["\']([^"\']+)["\']',
html, re.I
)
description = desc_match.group(1).strip() if desc_match else ""
# Extract h1/h2 headings as sections
headings = re.findall(r"<h[12][^>]*>([^<]+)</h[12]>", html, re.I)
sections = [re.sub(r"\s+", " ", h).strip() for h in headings[:8]]
results.append({
"title": title,
"url": fast_url,
"description": description,
"sections": sections,
"source": "fast_path",
})
except Exception as exc:
logger.warning("Fast path fetch failed for %s: %s", fast_url, exc)
# Search fallback
if not results:
try:
search_url = f"{_BASE}/s/results?q={quote_plus(query)}"
resp = await client.get(search_url)
html = resp.text if resp.status_code == 200 else ""
# Parse search result links
links = re.findall(
r'href=["\'](/(?:reference|guide|training|jetpack|develop|studio)[^"\'#?]*)["\']',
html,
)
seen: set[str] = set()
for link in links:
if link not in seen:
seen.add(link)
full_url = f"{_BASE}{link}"
results.append({
"title": link.split("/")[-1].replace("-", " ").title(),
"url": full_url,
"description": "",
"source": "search",
})
if len(results) >= max_results:
break
except Exception as exc:
logger.warning("Android docs search failed: %s", exc)
# If still nothing, return known reference index
if not results:
results = [
{"title": k.title(), "url": f"{_BASE}{v}" if not v.startswith("http") else v, "source": "index"}
for k, v in list(_KNOWN_REFS.items())[:max_results]
]
latency_ms = round((time.monotonic() - t0) * 1000)
return {
"query": query,
"results": results[:max_results],
"latency_ms": latency_ms,
"source_base": _BASE,
}
PYEOF
echo "[CREATED] backend/tools/android_docs.py"
echo
echo "===== 5. WIRE NEW TOOLS INTO MCP DISPATCH ====="
cat > backend/tools/__init__.py << 'PYEOF'
"""DOLOR3V production tool implementations."""
from .github_search import search_repositories, search_code
from .android_docs import lookup_android_docs
__all__ = ["search_repositories", "search_code", "lookup_android_docs"]
PYEOF
echo "[CREATED] backend/tools/__init__.py"
echo
echo "===== 6. REGISTER ALL MISSING ROUTES IN app_part1 ====="
# Find app_part1 to understand existing structure
echo "[INFO] app_part1.py first 60 lines:"
head -60 app_part1.py 2>/dev/null || echo "[WARN] app_part1.py not found"
echo
echo "===== 7. CREATE app_routes_extension.py ====="
cat > app_routes_extension.py << 'PYEOF'
"""
DOLOR3V backend route extension.
Registers all missing production contracts not in app_part1.
Import this from app.py after app_part1.
"""
from __future__ import annotations
import logging
import time
from typing import Any
from fastapi import APIRouter
from pydantic import BaseModel
from backend.api.routes.deploy_settings import router as deploy_router
from backend.tools.github_search import search_repositories, search_code
from backend.tools.android_docs import lookup_android_docs
logger = logging.getLogger("dolor3v.routes.extension")
ext_router = APIRouter()
# ── /api/workspace/fix ──────────────────────────────────────────────
class WorkspaceFixRequest(BaseModel):
path: str
content: str
operation: str = "replace"
@ext_router.post("/api/workspace/fix")
async def workspace_fix(body: WorkspaceFixRequest) -> dict[str, Any]:
"""Apply a file-level patch to the active workspace."""
if not body.path or not body.content:
return {"success": False, "error": "path and content are required"}
if body.operation not in ("replace", "patch", "create"):
return {"success": False, "error": f"unsupported operation: {body.operation}"}
# Real workspace mutation goes through preview_workspace
try:
from backend.preview.workspace import preview_workspace
await preview_workspace.save_file(body.path, body.content)
return {"success": True, "path": body.path, "operation": body.operation}
except Exception as exc:
logger.error("workspace_fix failed: %s", exc)
return {"success": False, "error": str(exc)}
# ── /api/github/search ──────────────────────────────────────────────
class GitHubSearchRequest(BaseModel):
query: str
sort: str = "stars"
per_page: int = 10
@ext_router.post("/api/github/search")
async def github_search(body: GitHubSearchRequest) -> dict[str, Any]:
"""Real GitHub repository search via GitHub REST API."""
return await search_repositories(body.query, sort=body.sort, per_page=body.per_page)
@ext_router.get("/api/github/search")
async def github_search_get(q: str = "fastapi", sort: str = "stars", per_page: int = 10) -> dict[str, Any]:
return await search_repositories(q, sort=sort, per_page=per_page)
# ── /api/android/docs ───────────────────────────────────────────────
@ext_router.get("/api/android/docs")
async def android_docs(q: str = "Activity", max_results: int = 5) -> dict[str, Any]:
"""Real Android Developer documentation lookup."""
return await lookup_android_docs(q, max_results=max_results)
@ext_router.post("/api/android/docs")
async def android_docs_post(body: dict) -> dict[str, Any]:
query = body.get("query", body.get("q", "Activity"))
return await lookup_android_docs(query)
# ── /api/deploy/settings (alias via ext) ────────────────────────────
# deploy_router is included separately below
# ── /api/agent/run ──────────────────────────────────────────────────
class AgentRunRequest(BaseModel):
message: str
provider: str = "auto"
model: str | None = None
stream: bool = False
@ext_router.post("/api/agent/run")
async def agent_run(body: AgentRunRequest) -> dict[str, Any]:
"""Route agent prompts through the LLM gateway."""
t0 = time.monotonic()
try:
from backend.llm.gateway import ModelGateway
gateway = ModelGateway()
result = await gateway.generate(
prompt=body.message,
intent="agent",
model_hint=body.model,
)
return {
"response": result,
"provider": body.provider,
"latency_ms": round((time.monotonic() - t0) * 1000),
}
except Exception as exc:
logger.error("agent_run failed: %s", exc)
return {"error": str(exc), "latency_ms": round((time.monotonic() - t0) * 1000)}
def register(app) -> None:
"""Call this from app.py to mount all extension routes."""
app.include_router(ext_router)
app.include_router(deploy_router)
logger.info("Extension routes registered: workspace/fix, github/search, android/docs, deploy/settings, agent/run")
PYEOF
echo "[CREATED] app_routes_extension.py"
echo
echo "===== 8. PATCH app.py TO IMPORT EXTENSION ====="
python3 - <<'PY'
from pathlib import Path
p = Path("app.py")
src = p.read_text()
injection = "\nfrom app_routes_extension import register as _register_ext\n_register_ext(app)\n"
if "app_routes_extension" not in src:
# Append after the last import/include block
src = src.rstrip() + "\n" + injection
p.write_text(src)
print("[PATCHED] app.py now imports extension routes")
else:
print("[OK] app.py already imports extension routes")
PY
echo
echo "===== 9. VERIFY IMPORT CHAIN ====="
python3 -c "
import sys
sys.path.insert(0, '.')
errors = []
try:
from backend.tools.github_search import search_repositories
print('[PASS] github_search import')
except Exception as e:
errors.append(f'github_search: {e}')
print(f'[FAIL] github_search: {e}')
try:
from backend.tools.android_docs import lookup_android_docs
print('[PASS] android_docs import')
except Exception as e:
errors.append(f'android_docs: {e}')
print(f'[FAIL] android_docs: {e}')
try:
from backend.api.routes.deploy_settings import router
print('[PASS] deploy_settings router import')
except Exception as e:
errors.append(f'deploy_settings: {e}')
print(f'[FAIL] deploy_settings: {e}')
try:
from app_routes_extension import register
print('[PASS] app_routes_extension import')
except Exception as e:
errors.append(f'extension: {e}')
print(f'[FAIL] extension: {e}')
if errors:
print(f'\n[FAIL] {len(errors)} import(s) failed')
sys.exit(1)
else:
print('\n[PASS] All imports clean')
"
echo
echo "===== 10. LIVE TEST NEW TOOLS ====="
python3 -c "
import asyncio
from backend.tools.github_search import search_repositories
from backend.tools.android_docs import lookup_android_docs
async def run():
print('[TEST] GitHub search: fastapi')
r = await search_repositories('fastapi', per_page=3)
if 'error' in r:
print(f' [WARN] {r[\"error\"]}')
else:
print(f' [PASS] total_count={r[\"total_count\"]} returned={r[\"returned\"]} latency={r[\"latency_ms\"]}ms')
for repo in r['results'][:2]:
print(f' β†’ {repo[\"name\"]} ⭐{repo[\"stars\"]}')
print('[TEST] Android docs: Activity')
d = await lookup_android_docs('Activity', max_results=3)
print(f' [PASS] latency={d[\"latency_ms\"]}ms results={len(d[\"results\"])}')
for res in d['results'][:2]:
print(f' β†’ {res[\"title\"]} β€” {res[\"url\"]}')
asyncio.run(run())
"
echo
echo "===== 11. COMMIT AND PUSH TO HF ====="
echo "[nameserver fix]"
echo "nameserver 1.1.1.1" > /etc/resolv.conf
git add -A
git commit -m "feat: add deploy/settings, real github search, android docs, workspace/fix, agent/run routes"
git push hf main
echo
echo "============================================================"
echo " BACKEND CONTRACTS COMPLETE β€” PUSHING TO HF"
echo "============================================================"