#!/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"]*>([^<]+)", html, re.I) title = title_match.group(1).strip() if title_match else query # Extract meta description desc_match = re.search( r']+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"]*>([^<]+)", 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 "============================================================"