Spaces:
Sleeping
Sleeping
David Prince
fix: add all missing local module stubs (remote_mcp_registry, mcp_auth, mcp_transport, etc)
cce8120 | 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 "============================================================" | |