Commit ·
81c99eb
0
Parent(s):
Fix model selection and add tools passthrough for function-calling
Browse files- .gitignore +9 -0
- Dockerfile +14 -0
- gateway.py +1490 -0
- load_master_prompt.py +9 -0
- master_builder_prompt.txt +67 -0
- requirements.txt +7 -0
.gitignore
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.env
|
| 2 |
+
*.pyc
|
| 3 |
+
__pycache__/
|
| 4 |
+
projects/
|
| 5 |
+
data/
|
| 6 |
+
*.log
|
| 7 |
+
memory.json
|
| 8 |
+
node_modules/
|
| 9 |
+
.next/
|
Dockerfile
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
RUN useradd -m -u 1000 user
|
| 4 |
+
WORKDIR /app
|
| 5 |
+
|
| 6 |
+
COPY --chown=user requirements.txt requirements.txt
|
| 7 |
+
RUN pip install --no-cache-dir --upgrade -r requirements.txt
|
| 8 |
+
|
| 9 |
+
COPY --chown=user . /app
|
| 10 |
+
|
| 11 |
+
ENV PORT=7860
|
| 12 |
+
USER user
|
| 13 |
+
|
| 14 |
+
CMD ["uvicorn", "gateway:app", "--host", "0.0.0.0", "--port", "7860"]
|
gateway.py
ADDED
|
@@ -0,0 +1,1490 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# ============================================================
|
| 3 |
+
# DOLOR3V GATEWAY v6 – Claude Code features
|
| 4 |
+
# ============================================================
|
| 5 |
+
from dotenv import load_dotenv
|
| 6 |
+
load_dotenv('/opt/dolor3v/.env')
|
| 7 |
+
import asyncio
|
| 8 |
+
import os, sys, json, time, re, math, subprocess, threading, hashlib, itertools
|
| 9 |
+
import urllib.request, urllib.parse, urllib.error
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
from collections import defaultdict
|
| 12 |
+
from datetime import datetime
|
| 13 |
+
from fastapi import FastAPI, Request, HTTPException
|
| 14 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 15 |
+
import uvicorn, requests
|
| 16 |
+
from bs4 import BeautifulSoup
|
| 17 |
+
from load_master_prompt import load_master_prompt
|
| 18 |
+
from colorthief import ColorThief
|
| 19 |
+
|
| 20 |
+
PORT = int(os.environ.get("PORT", 8080))
|
| 21 |
+
DOLOR3V_KEY = os.environ.get("DOLOR3V_KEY", "d3v-master-dolordprince-2026")
|
| 22 |
+
GROQ_KEYS = [k for k in [os.environ.get("GROQ_API_KEY",""), os.environ.get("GROQ_API_KEY_2",""), os.environ.get("GROQ_API_KEY_3","")] if k]
|
| 23 |
+
GROQ_KEY = GROQ_KEYS[0] if GROQ_KEYS else ""
|
| 24 |
+
_groq_idx = 0
|
| 25 |
+
OPENROUTER_KEY= os.environ.get("OPENROUTER_KEY","")
|
| 26 |
+
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY","")
|
| 27 |
+
CEREBRAS_API_KEY = os.environ.get("CEREBRAS_API_KEY","")
|
| 28 |
+
ZAI_API_KEY = os.environ.get("ZAI_API_KEY","")
|
| 29 |
+
GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN", "")
|
| 30 |
+
print("GitHub token loaded: " + str(bool(GITHUB_TOKEN)))
|
| 31 |
+
GITHUB_USER = os.environ.get("GITHUB_USER", "dolordprince")
|
| 32 |
+
SURGE_TOKEN = os.environ.get("SURGE_TOKEN", "")
|
| 33 |
+
SURGE_EMAIL = os.environ.get("SURGE_EMAIL", "personaldolor@gmail.com")
|
| 34 |
+
HF_TOKEN = os.environ.get("HF_TOKEN", "")
|
| 35 |
+
if not HF_TOKEN:
|
| 36 |
+
try:
|
| 37 |
+
with open("/opt/dolor3v/.env") as f:
|
| 38 |
+
for line in f:
|
| 39 |
+
if line.startswith("HF_TOKEN="):
|
| 40 |
+
HF_TOKEN = line.strip().split("=",1)[1].strip('\"')
|
| 41 |
+
break
|
| 42 |
+
except:
|
| 43 |
+
pass
|
| 44 |
+
print("HF token loaded:", bool(HF_TOKEN))
|
| 45 |
+
if not HF_TOKEN:
|
| 46 |
+
try:
|
| 47 |
+
with open('/opt/dolor3v/.env') as f:
|
| 48 |
+
for line in f:
|
| 49 |
+
if line.startswith('HF_TOKEN='):
|
| 50 |
+
HF_TOKEN = line.strip().split('=',1)[1].strip('"').strip("'")
|
| 51 |
+
break
|
| 52 |
+
except:
|
| 53 |
+
pass
|
| 54 |
+
if not HF_TOKEN:
|
| 55 |
+
try:
|
| 56 |
+
with open('/opt/dolor3v/.env') as f:
|
| 57 |
+
for line in f:
|
| 58 |
+
if line.startswith('HF_TOKEN='):
|
| 59 |
+
HF_TOKEN = line.strip().split('=',1)[1].strip('"').strip("'")
|
| 60 |
+
break
|
| 61 |
+
except: pass
|
| 62 |
+
print("HF token loaded: " + str(bool(HF_TOKEN)))
|
| 63 |
+
HF_USER = os.environ.get("HF_USER", "Daviddolor")
|
| 64 |
+
OLLAMA_URL = "http://localhost:11434"
|
| 65 |
+
TABBY_URL = "http://localhost:9090"
|
| 66 |
+
EVENT_BUS_URL = "http://localhost:9091"
|
| 67 |
+
PROJECTS_DIR = "/opt/dolor3v/projects"
|
| 68 |
+
MEMORY_DB = "/opt/dolor3v/memory.json"
|
| 69 |
+
LOG_FILE = "/tmp/dolor3v/mcp.log"
|
| 70 |
+
SAFE_MODE = os.environ.get("DOLOR3V_SAFE_MODE", "true").lower() not in ("0","false","no")
|
| 71 |
+
MAX_CONTEXT_CHARS = 4000 # smaller limit to avoid 413 from Groq
|
| 72 |
+
|
| 73 |
+
os.makedirs(PROJECTS_DIR, exist_ok=True)
|
| 74 |
+
os.makedirs("/tmp/dolor3v", exist_ok=True)
|
| 75 |
+
os.makedirs("/opt/dolor3v/data", exist_ok=True)
|
| 76 |
+
|
| 77 |
+
def log(msg):
|
| 78 |
+
ts = datetime.now().strftime("%H:%M:%S")
|
| 79 |
+
line = f"[{ts}] {msg}"
|
| 80 |
+
print(line, flush=True)
|
| 81 |
+
try:
|
| 82 |
+
with open(LOG_FILE,"a") as f: f.write(line+"\n")
|
| 83 |
+
except: pass
|
| 84 |
+
|
| 85 |
+
app = FastAPI(title="DOLOR3V MCP Gateway v6.0")
|
| 86 |
+
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
|
| 87 |
+
|
| 88 |
+
class Memory:
|
| 89 |
+
_lock = threading.Lock()
|
| 90 |
+
@staticmethod
|
| 91 |
+
def _load():
|
| 92 |
+
try:
|
| 93 |
+
with open(MEMORY_DB) as f: return json.load(f)
|
| 94 |
+
except: return {"kv":{}, "docs":[]}
|
| 95 |
+
@staticmethod
|
| 96 |
+
def _save(data):
|
| 97 |
+
with open(MEMORY_DB,"w") as f: json.dump(data, f, indent=2)
|
| 98 |
+
@staticmethod
|
| 99 |
+
def set(key, value):
|
| 100 |
+
with Memory._lock:
|
| 101 |
+
d = Memory._load()
|
| 102 |
+
d["kv"][key] = {"value": value, "ts": time.time()}
|
| 103 |
+
Memory._save(d)
|
| 104 |
+
@staticmethod
|
| 105 |
+
def get(key):
|
| 106 |
+
d = Memory._load()
|
| 107 |
+
entry = d["kv"].get(key)
|
| 108 |
+
return entry["value"] if entry else None
|
| 109 |
+
@staticmethod
|
| 110 |
+
def ingest(text, source="user"):
|
| 111 |
+
with Memory._lock:
|
| 112 |
+
d = Memory._load()
|
| 113 |
+
d.setdefault("docs", [])
|
| 114 |
+
d["docs"].append({"id": hashlib.md5(text.encode()).hexdigest()[:8],
|
| 115 |
+
"text": text[:2000], "source": source, "ts": time.time()})
|
| 116 |
+
if len(d["docs"]) > 200: d["docs"] = d["docs"][-200:]
|
| 117 |
+
Memory._save(d)
|
| 118 |
+
@staticmethod
|
| 119 |
+
def search(query, top_k=3):
|
| 120 |
+
d = Memory._load()
|
| 121 |
+
docs = d.get("docs", [])
|
| 122 |
+
if not docs: return []
|
| 123 |
+
def tokenize(t): return re.findall(r'\w+', t.lower())
|
| 124 |
+
q_tokens = set(tokenize(query))
|
| 125 |
+
N = len(docs)
|
| 126 |
+
df = defaultdict(int)
|
| 127 |
+
for doc in docs:
|
| 128 |
+
for tok in set(tokenize(doc["text"])):
|
| 129 |
+
df[tok] += 1
|
| 130 |
+
scores = []
|
| 131 |
+
for doc in docs:
|
| 132 |
+
tokens = tokenize(doc["text"])
|
| 133 |
+
tf = defaultdict(int)
|
| 134 |
+
for t in tokens: tf[t] += 1
|
| 135 |
+
score = 0.0
|
| 136 |
+
for tok in q_tokens:
|
| 137 |
+
if tok in tf:
|
| 138 |
+
tfidf = (tf[tok]/len(tokens)) * math.log((N+1)/(df[tok]+1)+1)
|
| 139 |
+
score += tfidf
|
| 140 |
+
scores.append((score, doc))
|
| 141 |
+
scores.sort(key=lambda x: x[0], reverse=True)
|
| 142 |
+
return [d for _, d in scores[:top_k] if _ > 0]
|
| 143 |
+
@staticmethod
|
| 144 |
+
def all_kv():
|
| 145 |
+
return Memory._load().get("kv", {})
|
| 146 |
+
|
| 147 |
+
class Firewall:
|
| 148 |
+
UA = "Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36"
|
| 149 |
+
@staticmethod
|
| 150 |
+
def search(query, max_results=5):
|
| 151 |
+
try:
|
| 152 |
+
q = urllib.parse.quote_plus(query)
|
| 153 |
+
url = f"https://html.duckduckgo.com/html/?q={q}"
|
| 154 |
+
req = urllib.request.Request(url, headers={"User-Agent": Firewall.UA})
|
| 155 |
+
with urllib.request.urlopen(req, timeout=12) as r:
|
| 156 |
+
html = r.read().decode("utf-8", errors="ignore")
|
| 157 |
+
snippets = re.findall(r'class="result__snippet"[^>]*>(.*?)</a>', html, re.DOTALL)[:max_results]
|
| 158 |
+
titles = re.findall(r'class="result__a"[^>]*>(.*?)</a>', html, re.DOTALL)[:max_results]
|
| 159 |
+
urls = re.findall(r'class="result__url"[^>]*>(.*?)</span>', html, re.DOTALL)[:max_results]
|
| 160 |
+
results = []
|
| 161 |
+
for i in range(len(snippets)):
|
| 162 |
+
results.append({"title": re.sub(r"<[^>]+>","", titles[i] if i<len(titles) else ""),
|
| 163 |
+
"snippet": re.sub(r"<[^>]+>","", snippets[i]),
|
| 164 |
+
"url": urls[i].strip() if i<len(urls) else ""})
|
| 165 |
+
for r in results:
|
| 166 |
+
Memory.ingest(f"{r['title']}: {r['snippet']}", "web_search")
|
| 167 |
+
return results
|
| 168 |
+
except Exception as e:
|
| 169 |
+
log(f"Search error: {e}")
|
| 170 |
+
return []
|
| 171 |
+
@staticmethod
|
| 172 |
+
def fetch(url):
|
| 173 |
+
try:
|
| 174 |
+
req = urllib.request.Request(url, headers={"User-Agent": Firewall.UA})
|
| 175 |
+
with urllib.request.urlopen(req, timeout=15) as r:
|
| 176 |
+
html = r.read().decode("utf-8", errors="ignore")
|
| 177 |
+
text = re.sub(r"<style[^>]*>.*?</style>","", html, flags=re.DOTALL)
|
| 178 |
+
text = re.sub(r"<script[^>]*>.*?</script>","", text, flags=re.DOTALL)
|
| 179 |
+
text = re.sub(r"<[^>]+>"," ", text)
|
| 180 |
+
text = re.sub(r"\s+"," ", text).strip()
|
| 181 |
+
Memory.ingest(text[:1000], f"fetch:{url}")
|
| 182 |
+
return text[:5000]
|
| 183 |
+
except Exception as e:
|
| 184 |
+
return f"Fetch error: {e}"
|
| 185 |
+
@staticmethod
|
| 186 |
+
def gather_context(prompt):
|
| 187 |
+
keywords = re.sub(r'\b(make|build|create|a|an|the|for|with|and|to)\b','', prompt.lower())[:80]
|
| 188 |
+
results = Firewall.search(f"{keywords} production best practices 2025")
|
| 189 |
+
ctx = f"[FIREWALL: {keywords}]\n"
|
| 190 |
+
for r in results[:3]:
|
| 191 |
+
ctx += f"- {r['title']}: {r['snippet']}\n"
|
| 192 |
+
return ctx
|
| 193 |
+
def _http_post(url, data, headers=None, timeout=120):
|
| 194 |
+
body = json.dumps(data).encode()
|
| 195 |
+
hdrs = {"Content-Type":"application/json"}
|
| 196 |
+
if headers: hdrs.update(headers)
|
| 197 |
+
req = urllib.request.Request(url, data=body, headers=hdrs)
|
| 198 |
+
with urllib.request.urlopen(req, timeout=timeout) as r:
|
| 199 |
+
return json.loads(r.read())
|
| 200 |
+
|
| 201 |
+
def llm_ollama(messages, model="dolor3v_coder:turbo"):
|
| 202 |
+
result = _http_post(f"{OLLAMA_URL}/api/chat",
|
| 203 |
+
{"model": model, "messages": messages, "stream": False,
|
| 204 |
+
"options": {"num_ctx":1024,"temperature":0.2}})
|
| 205 |
+
return result["message"]["content"]
|
| 206 |
+
|
| 207 |
+
def llm_groq(messages, model="llama-3.3-70b-versatile", tools=None, tool_choice=None):
|
| 208 |
+
global _groq_idx
|
| 209 |
+
last_err = None
|
| 210 |
+
tries = len(GROQ_KEYS) or 1
|
| 211 |
+
backoff = 4 # start with 4 seconds for 429
|
| 212 |
+
for attempt in range(tries):
|
| 213 |
+
key = GROQ_KEYS[_groq_idx % len(GROQ_KEYS)] if GROQ_KEYS else ""
|
| 214 |
+
_groq_idx += 1
|
| 215 |
+
try:
|
| 216 |
+
payload = {"model": model, "messages": messages, "temperature": 0.2, "max_tokens": 4096}
|
| 217 |
+
if tools:
|
| 218 |
+
payload["tools"] = tools
|
| 219 |
+
if tool_choice:
|
| 220 |
+
payload["tool_choice"] = tool_choice
|
| 221 |
+
result = _http_post("https://api.groq.com/openai/v1/chat/completions",
|
| 222 |
+
payload,
|
| 223 |
+
{"Authorization": f"Bearer {key}", "User-Agent": "Mozilla/5.0 (compatible; dolor3v/1.0)"})
|
| 224 |
+
return result["choices"][0]["message"]
|
| 225 |
+
except urllib.error.HTTPError as e:
|
| 226 |
+
if e.code == 429:
|
| 227 |
+
log(f"Groq rate‑limited, sleeping {backoff}s")
|
| 228 |
+
time.sleep(backoff)
|
| 229 |
+
backoff = min(backoff * 2, 30)
|
| 230 |
+
last_err = e
|
| 231 |
+
# stay on the same key, just wait
|
| 232 |
+
else:
|
| 233 |
+
last_err = e
|
| 234 |
+
log(f"Groq key failed, rotating: {e}")
|
| 235 |
+
continue
|
| 236 |
+
except Exception as e:
|
| 237 |
+
last_err = e
|
| 238 |
+
log(f"Groq key failed, rotating: {e}")
|
| 239 |
+
continue
|
| 240 |
+
raise last_err if last_err else Exception("No Groq keys configured")
|
| 241 |
+
|
| 242 |
+
def llm_openrouter(messages, model="nvidia/nemotron-super-49b-v1:free", tools=None, tool_choice=None):
|
| 243 |
+
payload = {"model": model, "messages": messages, "temperature": 0.2, "max_tokens": 4096}
|
| 244 |
+
if tools:
|
| 245 |
+
payload["tools"] = tools
|
| 246 |
+
if tool_choice:
|
| 247 |
+
payload["tool_choice"] = tool_choice
|
| 248 |
+
result = _http_post("https://openrouter.ai/api/v1/chat/completions",
|
| 249 |
+
payload,
|
| 250 |
+
{"Authorization": f"Bearer {OPENROUTER_KEY}", "HTTP-Referer": "https://dolor3v.com"})
|
| 251 |
+
return result["choices"][0]["message"]
|
| 252 |
+
|
| 253 |
+
def llm_openai(messages, model="gpt-4o-mini"):
|
| 254 |
+
result = _http_post("https://api.openai.com/v1/chat/completions",
|
| 255 |
+
{"model": model, "messages": messages, "temperature": 0.2, "max_tokens": 4096},
|
| 256 |
+
{"Authorization": f"Bearer {OPENAI_API_KEY}"})
|
| 257 |
+
return result["choices"][0]["message"]["content"]
|
| 258 |
+
|
| 259 |
+
def llm_cerebras(messages, model="gpt-oss-120b"):
|
| 260 |
+
result = _http_post("https://api.cerebras.ai/v1/chat/completions",
|
| 261 |
+
{"model": model, "messages": messages, "temperature": 0.2, "max_tokens": 4096},
|
| 262 |
+
{"Authorization": f"Bearer {CEREBRAS_API_KEY}", "User-Agent": "Mozilla/5.0 (compatible; dolor3v/1.0)"})
|
| 263 |
+
msg = result["choices"][0]["message"]
|
| 264 |
+
content = msg.get("content") or msg.get("reasoning") or ""
|
| 265 |
+
if not content:
|
| 266 |
+
raise Exception("Cerebras returned no usable content")
|
| 267 |
+
return content
|
| 268 |
+
|
| 269 |
+
def llm_glm(messages, model="glm-4.7-flash"):
|
| 270 |
+
result = _http_post("https://api.z.ai/api/paas/v4/chat/completions",
|
| 271 |
+
{"model": model, "messages": messages, "temperature": 0.2, "max_tokens": 4096},
|
| 272 |
+
{"Authorization": f"Bearer {ZAI_API_KEY}", "User-Agent": "Mozilla/5.0 (compatible; dolor3v/1.0)"})
|
| 273 |
+
msg = result["choices"][0]["message"]
|
| 274 |
+
content = msg.get("content") or ""
|
| 275 |
+
if not content:
|
| 276 |
+
raise Exception("GLM returned no usable content")
|
| 277 |
+
return content
|
| 278 |
+
|
| 279 |
+
def llm_route(messages, model=None, tools=None, tool_choice=None):
|
| 280 |
+
query = " ".join(m.get("content","") for m in messages if m.get("role")=="user")[-200:]
|
| 281 |
+
try:
|
| 282 |
+
mem_docs = Memory.search(query)
|
| 283 |
+
except Exception as e:
|
| 284 |
+
log(f"Memory.search failed, continuing without context: {e}")
|
| 285 |
+
mem_docs = None
|
| 286 |
+
if mem_docs:
|
| 287 |
+
context_text = "\n".join([d["text"][:200] for d in mem_docs])
|
| 288 |
+
messages = [{"role":"system","content":context_text}] + messages
|
| 289 |
+
|
| 290 |
+
if tools:
|
| 291 |
+
providers = [
|
| 292 |
+
("groq", lambda: llm_groq(messages, model=model or "llama-3.3-70b-versatile", tools=tools, tool_choice=tool_choice)),
|
| 293 |
+
("openrouter", lambda: llm_openrouter(messages, model=model or "nvidia/nemotron-super-49b-v1:free", tools=tools, tool_choice=tool_choice)),
|
| 294 |
+
]
|
| 295 |
+
else:
|
| 296 |
+
providers = [
|
| 297 |
+
("ollama", lambda: {"role":"assistant","content": llm_ollama(messages, model=model or "dolor3v_coder:turbo")}),
|
| 298 |
+
("groq", lambda: llm_groq(messages, model=model or "llama-3.3-70b-versatile")),
|
| 299 |
+
("cerebras", lambda: {"role":"assistant","content": llm_cerebras(messages, model=model or "gpt-oss-120b")}),
|
| 300 |
+
("glm", lambda: {"role":"assistant","content": llm_glm(messages, model=model or "glm-4.7-flash")}),
|
| 301 |
+
("openrouter", lambda: llm_openrouter(messages, model=model or "nvidia/nemotron-super-49b-v1:free")),
|
| 302 |
+
("openai", lambda: {"role":"assistant","content": llm_openai(messages, model=model or "gpt-4o-mini")}),
|
| 303 |
+
]
|
| 304 |
+
|
| 305 |
+
for name, fn in providers:
|
| 306 |
+
try:
|
| 307 |
+
log(f"LLM → {name}")
|
| 308 |
+
message = fn()
|
| 309 |
+
content_preview = message.get("content") or ""
|
| 310 |
+
try:
|
| 311 |
+
Memory.ingest(f"Q:{query[:100]} A:{content_preview[:200]}", "llm")
|
| 312 |
+
except Exception as e:
|
| 313 |
+
log(f"Memory.ingest failed, ignoring: {e}")
|
| 314 |
+
emit_event("custom", {"provider":name,"tokens":len(content_preview)}, "mcp-llm")
|
| 315 |
+
return message, name
|
| 316 |
+
except Exception as e:
|
| 317 |
+
log(f"LLM {name} failed: {e}")
|
| 318 |
+
continue
|
| 319 |
+
return {"role":"assistant","content":"All LLM providers failed"}, "none"
|
| 320 |
+
|
| 321 |
+
def llm_route_quality(messages, model=None):
|
| 322 |
+
query = " ".join(m.get("content","") for m in messages if m.get("role")=="user")[-200:]
|
| 323 |
+
mem_docs = Memory.search(query)
|
| 324 |
+
if mem_docs:
|
| 325 |
+
context_text = "\n".join([d["text"][:200] for d in mem_docs])
|
| 326 |
+
messages = [{"role":"system","content":context_text}] + messages
|
| 327 |
+
providers = [
|
| 328 |
+
("cerebras", lambda: llm_cerebras(messages)),
|
| 329 |
+
("groq", lambda: llm_groq(messages)),
|
| 330 |
+
("glm", lambda: llm_glm(messages)),
|
| 331 |
+
("openrouter", lambda: llm_openrouter(messages)),
|
| 332 |
+
("openai", lambda: llm_openai(messages)),
|
| 333 |
+
("ollama", lambda: llm_ollama(messages))
|
| 334 |
+
]
|
| 335 |
+
for name, fn in providers:
|
| 336 |
+
try:
|
| 337 |
+
log(f"LLM(quality) \u2192 {name}")
|
| 338 |
+
result = fn()
|
| 339 |
+
Memory.ingest(f"Q:{query[:100]} A:{result[:200]}", "llm")
|
| 340 |
+
emit_event("custom", {"provider":name,"tokens":len(result)}, "mcp-llm")
|
| 341 |
+
return result, name
|
| 342 |
+
except Exception as e:
|
| 343 |
+
log(f"LLM(quality) {name} failed: {e}")
|
| 344 |
+
continue
|
| 345 |
+
return "All LLM providers failed", "none"
|
| 346 |
+
|
| 347 |
+
def emit_event(etype, payload, source="mcp"):
|
| 348 |
+
try:
|
| 349 |
+
_http_post(f"{EVENT_BUS_URL}/emit",
|
| 350 |
+
{"type":etype,"payload":payload,"source":source}, timeout=3)
|
| 351 |
+
except: pass
|
| 352 |
+
|
| 353 |
+
class GitHub:
|
| 354 |
+
@staticmethod
|
| 355 |
+
def create_repo(name, desc="DOLOR3V project"):
|
| 356 |
+
data = json.dumps({"name": name, "description": desc, "private": False, "auto_init": True}).encode()
|
| 357 |
+
req = urllib.request.Request("https://api.github.com/user/repos", data=data,
|
| 358 |
+
headers={"Authorization": f"token {GITHUB_TOKEN}",
|
| 359 |
+
"Accept": "application/vnd.github.v3+json",
|
| 360 |
+
"Content-Type": "application/json",
|
| 361 |
+
"User-Agent": "DOLOR3V-MCP"})
|
| 362 |
+
with urllib.request.urlopen(req, timeout=15) as r:
|
| 363 |
+
result = json.loads(r.read())
|
| 364 |
+
return result.get("clone_url",""), result.get("html_url","")
|
| 365 |
+
@staticmethod
|
| 366 |
+
def push(project_path, repo_name, msg="DOLOR3V build"):
|
| 367 |
+
if not GITHUB_TOKEN: return "❌ GITHUB_TOKEN not set"
|
| 368 |
+
try:
|
| 369 |
+
clone_url, html_url = GitHub.create_repo(repo_name)
|
| 370 |
+
if not clone_url: return "❌ Could not create repo"
|
| 371 |
+
auth_url = clone_url.replace("https://", f"https://{GITHUB_TOKEN}@")
|
| 372 |
+
cmds = f"cd {project_path} && git init -q && git config user.email '{SURGE_EMAIL}' && git config user.name '{GITHUB_USER}' && git add -A && git commit -q -m '{msg}' && git branch -M main && git remote add origin {auth_url} 2>/dev/null || git remote set-url origin {auth_url} && git push -u origin main --force -q"
|
| 373 |
+
r = subprocess.run(cmds, shell=True, capture_output=True, text=True, timeout=60)
|
| 374 |
+
if r.returncode == 0:
|
| 375 |
+
emit_event("deploy", {"github":html_url,"path":project_path}, "mcp-github")
|
| 376 |
+
return f"✅ {html_url}"
|
| 377 |
+
return f"❌ {r.stderr[:200]}"
|
| 378 |
+
except Exception as e:
|
| 379 |
+
return f"❌ GitHub error: {e}"
|
| 380 |
+
@staticmethod
|
| 381 |
+
def push_source():
|
| 382 |
+
return GitHub.push("/opt/dolor3v", "dolor3v-engine", "DOLOR3V: engine update")
|
| 383 |
+
|
| 384 |
+
class Surge:
|
| 385 |
+
@staticmethod
|
| 386 |
+
def deploy(project_path, subdomain=None):
|
| 387 |
+
if not subdomain:
|
| 388 |
+
slug = re.sub(r'[^a-z0-9-]','-', Path(project_path).name.lower())
|
| 389 |
+
subdomain = f"{slug}-{int(time.time())}"
|
| 390 |
+
domain = f"{subdomain}.surge.sh"
|
| 391 |
+
env = os.environ.copy()
|
| 392 |
+
if SURGE_TOKEN: env["SURGE_TOKEN"] = SURGE_TOKEN
|
| 393 |
+
try:
|
| 394 |
+
r = subprocess.run(["surge", project_path, domain], capture_output=True, text=True, env=env, timeout=120)
|
| 395 |
+
if r.returncode == 0 or "Success" in r.stdout:
|
| 396 |
+
url = f"https://{domain}"
|
| 397 |
+
emit_event("deploy", {"url":url}, "mcp-surge")
|
| 398 |
+
return url, None
|
| 399 |
+
return None, r.stderr[:300] or r.stdout[:300]
|
| 400 |
+
except FileNotFoundError:
|
| 401 |
+
return None, "surge not installed"
|
| 402 |
+
except Exception as e:
|
| 403 |
+
return None, str(e)
|
| 404 |
+
|
| 405 |
+
def tabby_complete(prefix, suffix="", lang="python"):
|
| 406 |
+
try:
|
| 407 |
+
result = _http_post(f"{TABBY_URL}/v1beta/completions",
|
| 408 |
+
{"language": lang, "segments": {"prefix":prefix,"suffix":suffix}}, timeout=60)
|
| 409 |
+
return result.get("choices",[{}])[0].get("text","")
|
| 410 |
+
except Exception as e:
|
| 411 |
+
return f"Tabby error: {e}"
|
| 412 |
+
TOOL_LIST = [
|
| 413 |
+
"write_file","read_file","list_files","delete_file",
|
| 414 |
+
"shell_exec","web_search","web_fetch","firewall_context",
|
| 415 |
+
"memory_save","memory_recall","memory_search","memory_ingest",
|
| 416 |
+
"github_push","github_push_source","git_commit",
|
| 417 |
+
"surge_deploy","tabby_complete","hf_deploy",
|
| 418 |
+
"watchdog_ping","code_parse","clone_website",
|
| 419 |
+
"project_index","grep_code","spawn_agent","qa_review","build_project"
|
| 420 |
+
]
|
| 421 |
+
|
| 422 |
+
DANGEROUS_PATTERNS = [
|
| 423 |
+
r'\brm\s+-rf\b', r'\brm\s+-r\b', r'\brm\s+.*-f\b',
|
| 424 |
+
r'>\s*/dev/sd', r'\bmkfs\.', r'\bdd\s+if=',
|
| 425 |
+
r'git\s+push\s+--force', r'git\s+push\s+-f',
|
| 426 |
+
r'\bchmod\s+777', r'\bchown\s+-R',
|
| 427 |
+
r':(){ :|:& };:'
|
| 428 |
+
]
|
| 429 |
+
|
| 430 |
+
def is_dangerous(cmd):
|
| 431 |
+
return any(re.search(pat, cmd) for pat in DANGEROUS_PATTERNS)
|
| 432 |
+
|
| 433 |
+
def retry_tool(tool_func, *args, max_retries=2):
|
| 434 |
+
for attempt in range(max_retries + 1):
|
| 435 |
+
try:
|
| 436 |
+
return tool_func(*args)
|
| 437 |
+
except Exception as e:
|
| 438 |
+
if attempt == max_retries:
|
| 439 |
+
raise
|
| 440 |
+
time.sleep(1)
|
| 441 |
+
log(f"Retry {attempt+1}/{max_retries} for tool: {e}")
|
| 442 |
+
|
| 443 |
+
def project_index(path="."):
|
| 444 |
+
path = os.path.expanduser(path)
|
| 445 |
+
summary = []
|
| 446 |
+
summary.append(f"Project root: {os.path.abspath(path)}")
|
| 447 |
+
try:
|
| 448 |
+
r = subprocess.run(["find", path, "-maxdepth", "2", "-not", "-path", "*/node_modules/*", "-not", "-path", "*/.git/*"],
|
| 449 |
+
capture_output=True, text=True, timeout=10)
|
| 450 |
+
tree = r.stdout.strip()
|
| 451 |
+
summary.append(f"File tree (depth 2):\n{tree[:2000]}")
|
| 452 |
+
except:
|
| 453 |
+
pass
|
| 454 |
+
|
| 455 |
+
# Collect up to 15 important source files and include their first 50 lines
|
| 456 |
+
source_files = []
|
| 457 |
+
for root, dirs, files in os.walk(path):
|
| 458 |
+
dirs[:] = [d for d in dirs if d not in ('.git', 'node_modules', '__pycache__')]
|
| 459 |
+
for f in files:
|
| 460 |
+
if f.endswith(('.py', '.js', '.ts', '.sh', '.yaml', '.yml', '.json', '.toml')):
|
| 461 |
+
source_files.append(os.path.join(root, f))
|
| 462 |
+
if len(source_files) >= 20:
|
| 463 |
+
break
|
| 464 |
+
for fpath in sorted(source_files)[:15]:
|
| 465 |
+
try:
|
| 466 |
+
with open(fpath) as f:
|
| 467 |
+
flines = f.readlines()[:50]
|
| 468 |
+
header = ''.join(flines)
|
| 469 |
+
rel = os.path.relpath(fpath, path)
|
| 470 |
+
summary.append(f"\n--- {rel} (first 50 lines) ---\n{header}")
|
| 471 |
+
except:
|
| 472 |
+
pass
|
| 473 |
+
|
| 474 |
+
manifests = {
|
| 475 |
+
"package.json": None,
|
| 476 |
+
"requirements.txt": None,
|
| 477 |
+
"Pipfile": None,
|
| 478 |
+
"pyproject.toml": None,
|
| 479 |
+
"go.mod": None,
|
| 480 |
+
"Cargo.toml": None,
|
| 481 |
+
"Makefile": None,
|
| 482 |
+
"README.md": None,
|
| 483 |
+
".env.example": None,
|
| 484 |
+
"docker-compose.yml": None,
|
| 485 |
+
}
|
| 486 |
+
for fname in manifests:
|
| 487 |
+
fpath = os.path.join(path, fname)
|
| 488 |
+
if os.path.isfile(fpath):
|
| 489 |
+
try:
|
| 490 |
+
with open(fpath) as f:
|
| 491 |
+
mcontent = f.read(2000)
|
| 492 |
+
manifests[fname] = mcontent
|
| 493 |
+
except:
|
| 494 |
+
pass
|
| 495 |
+
|
| 496 |
+
for name, mcontent in manifests.items():
|
| 497 |
+
if mcontent:
|
| 498 |
+
summary.append(f"\n--- {name} ---\n{mcontent}")
|
| 499 |
+
|
| 500 |
+
try:
|
| 501 |
+
r = subprocess.run(["git", "-C", path, "status", "--short"], capture_output=True, text=True, timeout=5)
|
| 502 |
+
git_stat = r.stdout.strip()
|
| 503 |
+
if git_stat:
|
| 504 |
+
summary.append(f"\n--- git status ---\n{git_stat}")
|
| 505 |
+
except:
|
| 506 |
+
pass
|
| 507 |
+
|
| 508 |
+
return "\n".join(summary)
|
| 509 |
+
|
| 510 |
+
def grep_code(directory, pattern, file_filter="*"):
|
| 511 |
+
cmd = f"grep -rn --include='{file_filter}' '{pattern}' {directory}"
|
| 512 |
+
r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=15)
|
| 513 |
+
return r.stdout.strip() or "No matches found."
|
| 514 |
+
|
| 515 |
+
def spawn_agent(prompt, max_steps=3):
|
| 516 |
+
# AGENT_SYSTEM_PROMPT will be defined later; we'll handle that after function definition
|
| 517 |
+
msgs = [{"role":"system","content":AGENT_SYSTEM_PROMPT},
|
| 518 |
+
{"role":"user","content":prompt}]
|
| 519 |
+
for _ in range(max_steps):
|
| 520 |
+
try:
|
| 521 |
+
reply, _ = llm_route(msgs)
|
| 522 |
+
except:
|
| 523 |
+
return "Sub-agent failed: no LLM response."
|
| 524 |
+
line = reply.strip()
|
| 525 |
+
if line.startswith("{") and '"tool"' in line:
|
| 526 |
+
try:
|
| 527 |
+
tc = json.loads(line)
|
| 528 |
+
tool_res = execute_tool(tc["tool"], tc.get("args",{}))
|
| 529 |
+
msgs.append({"role":"assistant","content":line})
|
| 530 |
+
msgs.append({"role":"user","content":f"Tool result:\n{tool_res}"})
|
| 531 |
+
continue
|
| 532 |
+
except:
|
| 533 |
+
return reply
|
| 534 |
+
return reply
|
| 535 |
+
return "Sub-agent reached max steps."
|
| 536 |
+
|
| 537 |
+
# AGENT_SYSTEM_PROMPT placeholder – real definition will be in part 5
|
| 538 |
+
AGENT_SYSTEM_PROMPT = ""
|
| 539 |
+
|
| 540 |
+
def execute_tool(tool, args):
|
| 541 |
+
log(f"Tool: {tool} args:{str(args)[:80]}")
|
| 542 |
+
# Safety checks
|
| 543 |
+
if SAFE_MODE and tool == "shell_exec":
|
| 544 |
+
cmd = args.get("command","")
|
| 545 |
+
if is_dangerous(cmd):
|
| 546 |
+
return "🚫 SAFE MODE: Dangerous command blocked. Set DOLOR3V_SAFE_MODE=false to disable."
|
| 547 |
+
if SAFE_MODE and tool == "delete_file":
|
| 548 |
+
path = args.get("path","")
|
| 549 |
+
if any(re.search(pat, path) for pat in [r'\/$', r'^\/(etc|boot|bin|sbin|lib|sys|dev|proc)']):
|
| 550 |
+
return "🚫 SAFE MODE: Deletion of system path blocked."
|
| 551 |
+
|
| 552 |
+
# ----- standard tools -----
|
| 553 |
+
if tool == "write_file":
|
| 554 |
+
filename = os.path.basename(args.get("path",""))
|
| 555 |
+
file_path_arg = args.get("path", args.get("file_path",""))
|
| 556 |
+
|
| 557 |
+
# Block dangerous system paths
|
| 558 |
+
BLOCKED_PATHS = ["/etc/","/boot/","/bin/","/sbin/","/lib/","/sys/","/dev/","/proc/"]
|
| 559 |
+
if any(file_path_arg.startswith(bp) for bp in BLOCKED_PATHS):
|
| 560 |
+
return {"error": "BLOCKED", "message": f"System path blocked: {file_path_arg}"}
|
| 561 |
+
|
| 562 |
+
# Allow all legitimate web/app file extensions
|
| 563 |
+
ALLOWED_EXTENSIONS = {
|
| 564 |
+
".html",".css",".js",".ts",".tsx",".jsx",".json",".svg",
|
| 565 |
+
".md",".mdx",".txt",".env",".yaml",".yml",".toml",".lock",
|
| 566 |
+
".png",".jpg",".jpeg",".gif",".webp",".ico",".woff",".woff2"
|
| 567 |
+
}
|
| 568 |
+
ext = os.path.splitext(filename)[1].lower()
|
| 569 |
+
if ext and ext not in ALLOWED_EXTENSIONS:
|
| 570 |
+
return {"error": "BLOCKED", "message": f"File extension not allowed: {ext}", "blocked_file": filename}
|
| 571 |
+
|
| 572 |
+
path = os.path.expanduser(args.get("path", args.get("file_path","")))
|
| 573 |
+
content = args.get("content","")
|
| 574 |
+
os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
|
| 575 |
+
with open(path,"w") as f: f.write(content)
|
| 576 |
+
return f"✅ Written: {path} ({len(content)} bytes)"
|
| 577 |
+
elif tool == "read_file":
|
| 578 |
+
path = os.path.expanduser(args.get("path", args.get("file_path","")))
|
| 579 |
+
with open(path) as f: return f.read()[:2000][:3000]
|
| 580 |
+
elif tool == "list_files":
|
| 581 |
+
path = os.path.expanduser(args.get("path","."))
|
| 582 |
+
items = list(Path(path).iterdir())
|
| 583 |
+
return "\n".join(f"{'D' if i.is_dir() else 'F'} {i.name}" for i in sorted(items))
|
| 584 |
+
elif tool == "delete_file":
|
| 585 |
+
path = os.path.expanduser(args.get("path", args.get("file_path","")))
|
| 586 |
+
os.remove(path)
|
| 587 |
+
return f"✅ Deleted: {path}"
|
| 588 |
+
elif tool == "shell_exec":
|
| 589 |
+
cmd = args.get("command","")
|
| 590 |
+
r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=120)
|
| 591 |
+
out = (r.stdout + r.stderr).strip()
|
| 592 |
+
return out[:3000] or "✅ Done (no output)"
|
| 593 |
+
elif tool == "web_search":
|
| 594 |
+
results = Firewall.search(args.get("query",""), args.get("max_results",5))
|
| 595 |
+
return json.dumps(results, indent=2)
|
| 596 |
+
elif tool == "web_fetch":
|
| 597 |
+
return Firewall.fetch(args.get("url",""))
|
| 598 |
+
elif tool == "firewall_context":
|
| 599 |
+
return Firewall.gather_context(args.get("prompt",""))
|
| 600 |
+
elif tool == "memory_save":
|
| 601 |
+
Memory.set(args["key"], args["value"])
|
| 602 |
+
return f"✅ Saved: {args['key']}"
|
| 603 |
+
elif tool == "memory_recall":
|
| 604 |
+
val = Memory.get(args["key"])
|
| 605 |
+
return val if val else f"No memory for: {args['key']}"
|
| 606 |
+
elif tool == "memory_search":
|
| 607 |
+
results = Memory.search(args.get("query",""))
|
| 608 |
+
return json.dumps([r["text"][:200] for r in results], indent=2)
|
| 609 |
+
elif tool == "memory_ingest":
|
| 610 |
+
Memory.ingest(args.get("text",""), args.get("source","user"))
|
| 611 |
+
return "✅ Ingested into RAG"
|
| 612 |
+
elif tool == "github_push":
|
| 613 |
+
return GitHub.push(args.get("project_path",""),
|
| 614 |
+
args.get("repo_name", Path(args.get("project_path","x")).name),
|
| 615 |
+
args.get("message","DOLOR3V build"))
|
| 616 |
+
elif tool == "github_push_source":
|
| 617 |
+
return GitHub.push_source()
|
| 618 |
+
elif tool == "git_commit":
|
| 619 |
+
path = args.get("path",".")
|
| 620 |
+
msg = args.get("message","update")
|
| 621 |
+
r = subprocess.run(f"cd {path} && git add -A && git commit -m '{msg}'",
|
| 622 |
+
shell=True, capture_output=True, text=True)
|
| 623 |
+
return r.stdout + r.stderr
|
| 624 |
+
elif tool == "surge_deploy":
|
| 625 |
+
url, err = Surge.deploy(args.get("project_path",""), args.get("subdomain"))
|
| 626 |
+
return f"✅ Live: {url}" if url else f"❌ {err}"
|
| 627 |
+
elif tool == "tabby_complete":
|
| 628 |
+
return tabby_complete(args.get("prefix",""), args.get("suffix",""), args.get("lang","python"))
|
| 629 |
+
elif tool == "hf_deploy":
|
| 630 |
+
space = args.get("space", f"{HF_USER}/dolor3v")
|
| 631 |
+
src = args.get("src_path", "/opt/dolor3v")
|
| 632 |
+
if not HF_TOKEN: return "❌ HF_TOKEN not set"
|
| 633 |
+
hf_url = f"https://user:{HF_TOKEN}@huggingface.co/spaces/{space}"
|
| 634 |
+
r = subprocess.run(f"cd {src} && git init && git add -A && git commit -m 'DOLOR3V deploy' && git remote add hf {hf_url} 2>/dev/null || git remote set-url hf {hf_url} && git push hf main --force",
|
| 635 |
+
shell=True, capture_output=True, text=True, timeout=120)
|
| 636 |
+
return f"✅ Deployed to https://{space.replace('/','--')}.hf.space" if r.returncode==0 else f"❌ {r.stderr[:200]}"
|
| 637 |
+
elif tool == "watchdog_ping":
|
| 638 |
+
service = args.get("service","unknown")
|
| 639 |
+
status = args.get("status","ok")
|
| 640 |
+
emit_event("watchdog", {"service":service,"status":status}, "mcp-watchdog")
|
| 641 |
+
return f"✅ Watchdog: {service}={status}"
|
| 642 |
+
elif tool == "code_parse":
|
| 643 |
+
path = os.path.expanduser(args.get("path", args.get("file_path","")))
|
| 644 |
+
with open(path) as f: content = f.read()
|
| 645 |
+
lines = content.split("\n")
|
| 646 |
+
funcs = [l.strip() for l in lines if l.strip().startswith(("def ","class ","function ","const ","async "))]
|
| 647 |
+
return json.dumps({"lines": len(lines), "size": len(content), "symbols": funcs[:20]}, indent=2)
|
| 648 |
+
elif tool == "clone_website":
|
| 649 |
+
url = args.get("url")
|
| 650 |
+
proj_dir = args.get("project_dir")
|
| 651 |
+
if not url: return "❌ missing url"
|
| 652 |
+
try:
|
| 653 |
+
resp = requests.get(url, headers={"User-Agent": Firewall.UA}, timeout=15)
|
| 654 |
+
soup = BeautifulSoup(resp.text, "html.parser")
|
| 655 |
+
title = soup.title.string.strip() if soup.title else url
|
| 656 |
+
desc = soup.find("meta", attrs={"name": "description"})
|
| 657 |
+
description = desc.get("content","") if desc else ""
|
| 658 |
+
og_image = soup.find("meta", property="og:image")
|
| 659 |
+
hero_url = urllib.parse.urljoin(url, og_image["content"]) if og_image else None
|
| 660 |
+
colors = ["#3B82F6", "#1E3A8A", "#F59E0B"]
|
| 661 |
+
if hero_url:
|
| 662 |
+
img_data = requests.get(hero_url, timeout=10).content
|
| 663 |
+
import tempfile
|
| 664 |
+
with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp:
|
| 665 |
+
tmp.write(img_data)
|
| 666 |
+
tmp_path = tmp.name
|
| 667 |
+
ct = ColorThief(tmp_path)
|
| 668 |
+
palette = ct.get_palette(color_count=3)
|
| 669 |
+
colors = [f"rgb({r},{g},{b})" for (r,g,b) in palette]
|
| 670 |
+
os.unlink(tmp_path)
|
| 671 |
+
html = f"""<!DOCTYPE html>
|
| 672 |
+
<html><head><meta charset="UTF-8"><title>{title}</title>
|
| 673 |
+
<style>body{{font-family:system-ui;margin:0;background:{colors[0]};color:white;text-align:center;}}
|
| 674 |
+
.hero{{height:100vh;display:flex;flex-direction:column;justify-content:center;}}
|
| 675 |
+
button{{background:white;color:{colors[0]};border:none;padding:12px 24px;border-radius:30px;}}</style>
|
| 676 |
+
</head><body><div class="hero"><h1>{title}</h1><p>{description[:200]}</p><button>Explore</button></div></body></html>"""
|
| 677 |
+
if proj_dir:
|
| 678 |
+
os.makedirs(proj_dir, exist_ok=True)
|
| 679 |
+
with open(os.path.join(proj_dir, "index.html"), "w") as f:
|
| 680 |
+
f.write(html)
|
| 681 |
+
if hero_url:
|
| 682 |
+
img_data = requests.get(hero_url, timeout=10).content
|
| 683 |
+
with open(os.path.join(proj_dir, "hero.jpg"), "wb") as f:
|
| 684 |
+
f.write(img_data)
|
| 685 |
+
return {"status": "done", "path": proj_dir}
|
| 686 |
+
return {"html": html}
|
| 687 |
+
except Exception as e:
|
| 688 |
+
return {"error": str(e)}
|
| 689 |
+
# ----- new Claude Code tools -----
|
| 690 |
+
elif tool == "project_index":
|
| 691 |
+
return project_index(args.get("path","."))
|
| 692 |
+
elif tool == "grep_code":
|
| 693 |
+
directory = args.get("directory",".")
|
| 694 |
+
pattern = args.get("pattern","")
|
| 695 |
+
file_filter = args.get("file_filter","*")
|
| 696 |
+
if not pattern: return "❌ missing pattern"
|
| 697 |
+
return grep_code(directory, pattern, file_filter)
|
| 698 |
+
elif tool == "spawn_agent":
|
| 699 |
+
prompt = args.get("prompt","")
|
| 700 |
+
if not prompt: return "❌ missing prompt"
|
| 701 |
+
return spawn_agent(prompt, args.get("max_steps",3))
|
| 702 |
+
elif tool == "build_project":
|
| 703 |
+
build_dir = args.get("path", args.get("project_dir", ""))
|
| 704 |
+
command = args.get("command", "")
|
| 705 |
+
if not build_dir or not command:
|
| 706 |
+
return "❌ build_project requires path and command"
|
| 707 |
+
log_file = os.path.join(build_dir, "build.log")
|
| 708 |
+
os.makedirs(build_dir, exist_ok=True)
|
| 709 |
+
try:
|
| 710 |
+
with open(log_file, "w") as lf:
|
| 711 |
+
r = subprocess.run(
|
| 712 |
+
command, shell=True, cwd=build_dir,
|
| 713 |
+
stdout=lf, stderr=lf, timeout=600
|
| 714 |
+
)
|
| 715 |
+
with open(log_file) as lf:
|
| 716 |
+
output = lf.read()[-3000:]
|
| 717 |
+
status = "✅ Build succeeded" if r.returncode == 0 else f"❌ Build failed (exit {r.returncode})"
|
| 718 |
+
return f"{status}\n{output}"
|
| 719 |
+
except subprocess.TimeoutExpired:
|
| 720 |
+
return "❌ build_project timed out after 600s"
|
| 721 |
+
except Exception as e:
|
| 722 |
+
return f"❌ build_project error: {e}"
|
| 723 |
+
elif tool == "qa_review":
|
| 724 |
+
proj_dir_arg = args.get("path", args.get("project_dir", ""))
|
| 725 |
+
if not proj_dir_arg:
|
| 726 |
+
return "❌ missing path"
|
| 727 |
+
issues = []
|
| 728 |
+
html_path = os.path.join(proj_dir_arg, "index.html")
|
| 729 |
+
css_path = os.path.join(proj_dir_arg, "style.css")
|
| 730 |
+
if os.path.exists(html_path):
|
| 731 |
+
with open(html_path) as f:
|
| 732 |
+
html_content = f.read()
|
| 733 |
+
soup = BeautifulSoup(html_content, "html.parser")
|
| 734 |
+
if not soup.find("title") or not soup.title.string or not soup.title.string.strip():
|
| 735 |
+
issues.append("Missing or empty title tag")
|
| 736 |
+
if not soup.find("meta", attrs={"name": "description"}):
|
| 737 |
+
issues.append("Missing meta description tag")
|
| 738 |
+
if not soup.find("meta", attrs={"name": "viewport"}):
|
| 739 |
+
issues.append("Missing viewport meta tag")
|
| 740 |
+
imgs_missing_alt = [img.get("src","?") for img in soup.find_all("img") if not img.get("alt")]
|
| 741 |
+
if imgs_missing_alt:
|
| 742 |
+
issues.append(f"{len(imgs_missing_alt)} image(s) missing alt text")
|
| 743 |
+
if not soup.find("h1"):
|
| 744 |
+
issues.append("Missing an h1 heading")
|
| 745 |
+
else:
|
| 746 |
+
issues.append("index.html not found")
|
| 747 |
+
if os.path.exists(css_path):
|
| 748 |
+
with open(css_path) as f:
|
| 749 |
+
css_content = f.read()
|
| 750 |
+
if "@media" not in css_content:
|
| 751 |
+
issues.append("No media query found, page may not be responsive")
|
| 752 |
+
else:
|
| 753 |
+
issues.append("style.css not found")
|
| 754 |
+
if not issues:
|
| 755 |
+
return "QA review passed: no issues found"
|
| 756 |
+
return "QA review found issues: " + "; ".join(issues)
|
| 757 |
+
else:
|
| 758 |
+
return f"❌ Unknown tool: {tool}"
|
| 759 |
+
|
| 760 |
+
@app.post("/v1/build/nextjs")
|
| 761 |
+
async def build_nextjs(request: Request):
|
| 762 |
+
body = await request.json()
|
| 763 |
+
prompt = body.get("prompt", "")
|
| 764 |
+
if not prompt:
|
| 765 |
+
return {"ok": False, "error": "prompt required"}
|
| 766 |
+
|
| 767 |
+
slug = re.sub(r'[^a-z0-9-]', '-', prompt.lower())[:30]
|
| 768 |
+
ts = datetime.now().strftime("%m%d%H%M")
|
| 769 |
+
proj_name = f"dolor3v-{slug}-{ts}"
|
| 770 |
+
proj_dir = f"{PROJECTS_DIR}/{proj_name}"
|
| 771 |
+
os.makedirs(proj_dir, exist_ok=True)
|
| 772 |
+
log(f"NextJS build started: {proj_dir}")
|
| 773 |
+
|
| 774 |
+
# ── Phase 1: Research ──────────────────────────────────────────
|
| 775 |
+
research = {}
|
| 776 |
+
|
| 777 |
+
# 1a. Web search for design inspiration
|
| 778 |
+
search_results = Firewall.search(f"{prompt} website design inspiration color palette", 5)
|
| 779 |
+
research["search"] = search_results
|
| 780 |
+
|
| 781 |
+
# 1b. Fetch top result for real copy + colors
|
| 782 |
+
top_url = next((r.get("url") for r in search_results if r.get("url")), None)
|
| 783 |
+
fetched_text = ""
|
| 784 |
+
if top_url:
|
| 785 |
+
try:
|
| 786 |
+
fetched_text = Firewall.fetch(top_url)[:3000]
|
| 787 |
+
research["fetched_url"] = top_url
|
| 788 |
+
research["fetched_text"] = fetched_text
|
| 789 |
+
except Exception as e:
|
| 790 |
+
log(f"web_fetch failed: {e}")
|
| 791 |
+
|
| 792 |
+
# 1c. Extract color palette from a real image
|
| 793 |
+
palette = ["#1a1a2e", "#16213e", "#e94560"]
|
| 794 |
+
try:
|
| 795 |
+
img_keyword = re.sub(r'[^a-z0-9]', '-', prompt.lower())[:20]
|
| 796 |
+
img_url = f"https://picsum.photos/seed/{img_keyword}/800/600"
|
| 797 |
+
img_resp = requests.get(img_url, timeout=10)
|
| 798 |
+
if img_resp.status_code == 200:
|
| 799 |
+
import tempfile
|
| 800 |
+
with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp:
|
| 801 |
+
tmp.write(img_resp.content)
|
| 802 |
+
tmp_path = tmp.name
|
| 803 |
+
ct = ColorThief(tmp_path)
|
| 804 |
+
raw = ct.get_palette(color_count=3, quality=1)
|
| 805 |
+
palette = [f"#{r:02x}{g:02x}{b:02x}" for r,g,b in raw]
|
| 806 |
+
os.unlink(tmp_path)
|
| 807 |
+
research["palette"] = palette
|
| 808 |
+
research["hero_img"] = img_url
|
| 809 |
+
log(f"Extracted palette: {palette}")
|
| 810 |
+
except Exception as e:
|
| 811 |
+
log(f"Color extraction failed: {e}")
|
| 812 |
+
research["palette"] = palette
|
| 813 |
+
|
| 814 |
+
# 1d. Pick font pair from Google Fonts
|
| 815 |
+
font_pairs = [
|
| 816 |
+
("Playfair Display", "Inter"),
|
| 817 |
+
("DM Serif Display", "DM Sans"),
|
| 818 |
+
("Cormorant Garamond", "Nunito Sans"),
|
| 819 |
+
("Libre Baskerville", "Source Sans Pro"),
|
| 820 |
+
("Fraunces", "Outfit"),
|
| 821 |
+
]
|
| 822 |
+
import random
|
| 823 |
+
heading_font, body_font = random.choice(font_pairs)
|
| 824 |
+
research["heading_font"] = heading_font
|
| 825 |
+
research["body_font"] = body_font
|
| 826 |
+
|
| 827 |
+
log(f"Research complete: palette={palette}, fonts={heading_font}/{body_font}")
|
| 828 |
+
|
| 829 |
+
# ── Phase 2: Scaffold Next.js ──────────────────────────────────
|
| 830 |
+
scaffold_log = f"{proj_dir}/scaffold.log"
|
| 831 |
+
scaffold_cmd = (
|
| 832 |
+
f"cd {PROJECTS_DIR} && "
|
| 833 |
+
f"pnpm create next-app {proj_name} --typescript --tailwind --eslint "
|
| 834 |
+
f"--app --no-src-dir --no-git --yes 2>&1"
|
| 835 |
+
)
|
| 836 |
+
log("Scaffolding Next.js...")
|
| 837 |
+
scaffold_result = await asyncio.to_thread(
|
| 838 |
+
subprocess.run, scaffold_cmd, shell=True,
|
| 839 |
+
capture_output=True, text=True, timeout=600
|
| 840 |
+
)
|
| 841 |
+
with open(scaffold_log, "w") as f:
|
| 842 |
+
f.write(scaffold_result.stdout + scaffold_result.stderr)
|
| 843 |
+
|
| 844 |
+
if scaffold_result.returncode != 0:
|
| 845 |
+
return {"ok": False, "error": "Scaffold failed", "log": scaffold_result.stderr[-2000:]}
|
| 846 |
+
|
| 847 |
+
# Write next.config.ts for static export
|
| 848 |
+
next_config = (
|
| 849 |
+
"import type { NextConfig } from 'next';\n"
|
| 850 |
+
"const nextConfig: NextConfig = {\n"
|
| 851 |
+
" output: 'export',\n"
|
| 852 |
+
" images: { unoptimized: true }\n"
|
| 853 |
+
"};\n"
|
| 854 |
+
"export default nextConfig;\n"
|
| 855 |
+
)
|
| 856 |
+
with open(f"{proj_dir}/next.config.ts", "w") as f:
|
| 857 |
+
f.write(next_config)
|
| 858 |
+
|
| 859 |
+
# Install framer-motion
|
| 860 |
+
fm_result = await asyncio.to_thread(
|
| 861 |
+
subprocess.run, f"cd {proj_dir} && pnpm add framer-motion 2>&1",
|
| 862 |
+
shell=True, capture_output=True, text=True, timeout=300
|
| 863 |
+
)
|
| 864 |
+
log(f"framer-motion install: exit {fm_result.returncode}")
|
| 865 |
+
|
| 866 |
+
log("Scaffold complete, handing off to LLM for components...")
|
| 867 |
+
return {
|
| 868 |
+
"ok": True,
|
| 869 |
+
"phase": "scaffolded",
|
| 870 |
+
"proj_dir": proj_dir,
|
| 871 |
+
"research": research,
|
| 872 |
+
"message": "Scaffold complete. Call /v1/build/nextjs/complete to write components and build."
|
| 873 |
+
}
|
| 874 |
+
|
| 875 |
+
|
| 876 |
+
|
| 877 |
+
@app.post("/v1/build/nextjs/complete")
|
| 878 |
+
async def build_nextjs_complete(request: Request):
|
| 879 |
+
body = await request.json()
|
| 880 |
+
prompt = body.get("prompt", "")
|
| 881 |
+
proj_dir = body.get("proj_dir", "")
|
| 882 |
+
research = body.get("research", {})
|
| 883 |
+
if not prompt or not proj_dir:
|
| 884 |
+
return {"ok": False, "error": "prompt and proj_dir required"}
|
| 885 |
+
|
| 886 |
+
palette = research.get("palette", ["#1a1a2e","#16213e","#e94560"])
|
| 887 |
+
heading_font = research.get("heading_font", "Playfair Display")
|
| 888 |
+
body_font = research.get("body_font", "Inter")
|
| 889 |
+
hero_img = research.get("hero_img", "https://picsum.photos/seed/hero/1200/800")
|
| 890 |
+
fetched_text = research.get("fetched_text", "")[:1500]
|
| 891 |
+
search_snippets = " ".join([r.get("snippet","") for r in research.get("search",[])])[:1000]
|
| 892 |
+
|
| 893 |
+
context = f"""
|
| 894 |
+
RESEARCH RESULTS:
|
| 895 |
+
Design inspiration snippets: {search_snippets}
|
| 896 |
+
Fetched content: {fetched_text}
|
| 897 |
+
Color palette (use these exact hex values): {palette[0]} (primary), {palette[1]} (secondary), {palette[2]} (accent)
|
| 898 |
+
Heading font: {heading_font} (load from Google Fonts)
|
| 899 |
+
Body font: {body_font} (load from Google Fonts)
|
| 900 |
+
Hero image URL: {hero_img}
|
| 901 |
+
Project directory: {proj_dir}
|
| 902 |
+
|
| 903 |
+
TASK:
|
| 904 |
+
Write a complete, production-ready Next.js {prompt} website.
|
| 905 |
+
Use the exact colors, fonts, and image URLs from the research above.
|
| 906 |
+
Write ALL of these files using write_file:
|
| 907 |
+
1. {proj_dir}/app/globals.css - Import Google Fonts, define CSS variables for the palette
|
| 908 |
+
2. {proj_dir}/app/layout.tsx - Root layout with metadata, font imports
|
| 909 |
+
3. {proj_dir}/app/page.tsx - Home: hero, features, testimonials (2-3 real quotes), FAQ (3-5 Q&As), CTA
|
| 910 |
+
4. {proj_dir}/app/about/page.tsx - About: brand story, team
|
| 911 |
+
5. {proj_dir}/app/services/page.tsx - Services: detailed cards
|
| 912 |
+
6. {proj_dir}/app/contact/page.tsx - Contact: form, social links
|
| 913 |
+
7. {proj_dir}/components/Navbar.tsx - Responsive nav with mobile menu
|
| 914 |
+
8. {proj_dir}/components/Footer.tsx - Rich footer with links
|
| 915 |
+
|
| 916 |
+
Every image must use a real picsum.photos URL.
|
| 917 |
+
Use Framer Motion for scroll animations and hover effects.
|
| 918 |
+
Make every page fully responsive and accessible.
|
| 919 |
+
"""
|
| 920 |
+
|
| 921 |
+
system = load_master_prompt() + f"""
|
| 922 |
+
You are writing Next.js TypeScript components only.
|
| 923 |
+
Use write_file for every file.
|
| 924 |
+
Do not scaffold, do not install packages, do not build — just write the component files.
|
| 925 |
+
{context}
|
| 926 |
+
After writing ALL files respond with exactly: ◆DONE
|
| 927 |
+
"""
|
| 928 |
+
msgs = [{"role":"system","content":system},{"role":"user","content":prompt}]
|
| 929 |
+
results = []
|
| 930 |
+
response, provider = "", "groq"
|
| 931 |
+
MAX_STEPS = 20
|
| 932 |
+
parse_fail_count = 0
|
| 933 |
+
|
| 934 |
+
for _step in range(MAX_STEPS):
|
| 935 |
+
response, provider = await asyncio.to_thread(llm_route_quality, msgs)
|
| 936 |
+
step_results = []
|
| 937 |
+
decoder = json.JSONDecoder()
|
| 938 |
+
idx = 0
|
| 939 |
+
while idx < len(response):
|
| 940 |
+
brace_idx = response.find("{", idx)
|
| 941 |
+
if brace_idx == -1: break
|
| 942 |
+
try:
|
| 943 |
+
tc, end_idx = decoder.raw_decode(response, brace_idx)
|
| 944 |
+
except json.JSONDecodeError:
|
| 945 |
+
idx = brace_idx + 1
|
| 946 |
+
continue
|
| 947 |
+
idx = end_idx
|
| 948 |
+
if isinstance(tc, dict) and "tool" not in tc:
|
| 949 |
+
preceding = response[max(0,brace_idx-150):brace_idx].lower()
|
| 950 |
+
for tname in TOOL_LIST:
|
| 951 |
+
if tname.lower() in preceding:
|
| 952 |
+
tc = {"tool": tname, "args": tc}
|
| 953 |
+
break
|
| 954 |
+
if isinstance(tc, dict) and "tool" in tc:
|
| 955 |
+
args = tc.get("args", {})
|
| 956 |
+
if tc.get("tool") == "write_file":
|
| 957 |
+
if not args.get("path") and not args.get("file_path"):
|
| 958 |
+
args["path"] = os.path.join(proj_dir, "app/page.tsx")
|
| 959 |
+
content = str(args.get("content",""))
|
| 960 |
+
img_pattern = re.compile(r'src=["\'](?!https?://)(.*?)["\']')
|
| 961 |
+
def fix_img(m):
|
| 962 |
+
kw = re.sub(r'[^a-z0-9]','-',m.group(1).lower())[:20] or "photo"
|
| 963 |
+
return f'src="https://picsum.photos/seed/{kw}/800/600"'
|
| 964 |
+
args["content"] = img_pattern.sub(fix_img, content)
|
| 965 |
+
r = await asyncio.to_thread(retry_tool, execute_tool, tc["tool"], args)
|
| 966 |
+
results.append({"tool":tc["tool"],"result":str(r)[:200]})
|
| 967 |
+
step_results.append({"tool":tc["tool"],"result":str(r)[:1500]})
|
| 968 |
+
|
| 969 |
+
if "◆DONE" in response or "\u25c6DONE" in response:
|
| 970 |
+
break
|
| 971 |
+
if not step_results:
|
| 972 |
+
parse_fail_count += 1
|
| 973 |
+
if parse_fail_count > 2: break
|
| 974 |
+
msgs.append({"role":"assistant","content":response})
|
| 975 |
+
msgs.append({"role":"user","content":"Could not parse tool call. Use ONLY: {\"tool\":\"write_file\",\"args\":{\"path\":\"...\",\"content\":\"...\"}}. No prose."})
|
| 976 |
+
continue
|
| 977 |
+
parse_fail_count = 0
|
| 978 |
+
msgs.append({"role":"assistant","content":response})
|
| 979 |
+
msgs.append({"role":"user","content":"Tool results:\n"+json.dumps(step_results)[:2000]+"\nContinue writing remaining files, or ◆DONE if all done."})
|
| 980 |
+
|
| 981 |
+
# ── Phase 3: Build with auto-fix ──────────────────────────────
|
| 982 |
+
build_result = {"status":"not_run","log":""}
|
| 983 |
+
for attempt in range(3):
|
| 984 |
+
log(f"pnpm build attempt {attempt+1}...")
|
| 985 |
+
br = await asyncio.to_thread(
|
| 986 |
+
subprocess.run,
|
| 987 |
+
f"cd {proj_dir} && pnpm run build 2>&1",
|
| 988 |
+
shell=True, capture_output=True, text=True, timeout=600
|
| 989 |
+
)
|
| 990 |
+
build_log = (br.stdout + br.stderr)[-3000:]
|
| 991 |
+
if br.returncode == 0:
|
| 992 |
+
build_result = {"status":"success","log":build_log}
|
| 993 |
+
log("Build succeeded")
|
| 994 |
+
break
|
| 995 |
+
log(f"Build failed attempt {attempt+1}, searching for fix...")
|
| 996 |
+
err_lines = [l for l in build_log.splitlines() if "error" in l.lower() or "Error" in l][:5]
|
| 997 |
+
err_summary = " ".join(err_lines)[:300]
|
| 998 |
+
fix_search = Firewall.search(f"Next.js TypeScript build error fix: {err_summary}", 3)
|
| 999 |
+
fix_context = " ".join([r.get("snippet","") for r in fix_search])[:1000]
|
| 1000 |
+
fix_msgs = [
|
| 1001 |
+
{"role":"system","content":"You are a Next.js expert. Fix the build error. Use write_file to overwrite the broken file only. Respond with one write_file tool call then ◆DONE."},
|
| 1002 |
+
{"role":"user","content":f"Build error:\n{err_summary}\n\nFix suggestions:\n{fix_context}\n\nProject dir: {proj_dir}"}
|
| 1003 |
+
]
|
| 1004 |
+
fix_resp, _ = await asyncio.to_thread(llm_route_quality, fix_msgs)
|
| 1005 |
+
fix_decoder = json.JSONDecoder()
|
| 1006 |
+
fix_idx = 0
|
| 1007 |
+
while fix_idx < len(fix_resp):
|
| 1008 |
+
bi = fix_resp.find("{", fix_idx)
|
| 1009 |
+
if bi == -1: break
|
| 1010 |
+
try:
|
| 1011 |
+
ftc, fend = fix_decoder.raw_decode(fix_resp, bi)
|
| 1012 |
+
except json.JSONDecodeError:
|
| 1013 |
+
fix_idx = bi + 1
|
| 1014 |
+
continue
|
| 1015 |
+
fix_idx = fend
|
| 1016 |
+
if isinstance(ftc, dict) and ftc.get("tool") == "write_file":
|
| 1017 |
+
await asyncio.to_thread(retry_tool, execute_tool, "write_file", ftc.get("args",{}))
|
| 1018 |
+
results.append({"tool":"write_file","result":"auto-fix applied"})
|
| 1019 |
+
build_result = {"status":f"failed_attempt_{attempt+1}","log":build_log}
|
| 1020 |
+
|
| 1021 |
+
# ── Phase 4: Deploy ───────────────────────────────────────────
|
| 1022 |
+
gh_url, surge_url = "", ""
|
| 1023 |
+
out_dir = f"{proj_dir}/out"
|
| 1024 |
+
if build_result["status"] == "success" and os.path.isdir(out_dir):
|
| 1025 |
+
slug = re.sub(r'[^a-z0-9-]','-',Path(proj_dir).name.lower())
|
| 1026 |
+
if GITHUB_TOKEN:
|
| 1027 |
+
try:
|
| 1028 |
+
gh_url = await asyncio.to_thread(GitHub.push, proj_dir, slug)
|
| 1029 |
+
except Exception as e:
|
| 1030 |
+
gh_url = f"❌ {e}"
|
| 1031 |
+
try:
|
| 1032 |
+
surge_url_r, _ = await asyncio.to_thread(Surge.deploy, out_dir)
|
| 1033 |
+
if surge_url_r: surge_url = surge_url_r
|
| 1034 |
+
except Exception as e:
|
| 1035 |
+
surge_url = f"❌ {e}"
|
| 1036 |
+
else:
|
| 1037 |
+
surge_url = "❌ Build did not produce out/ folder"
|
| 1038 |
+
|
| 1039 |
+
qa_result = execute_tool("qa_review", {"path": proj_dir})
|
| 1040 |
+
|
| 1041 |
+
return {
|
| 1042 |
+
"ok": True,
|
| 1043 |
+
"prompt": prompt,
|
| 1044 |
+
"project": proj_dir,
|
| 1045 |
+
"provider": provider,
|
| 1046 |
+
"tools_run": results,
|
| 1047 |
+
"build": build_result["status"],
|
| 1048 |
+
"build_log": build_result["log"][-500:],
|
| 1049 |
+
"github": gh_url,
|
| 1050 |
+
"surge": surge_url,
|
| 1051 |
+
"qa": qa_result,
|
| 1052 |
+
"response": "◆DONE" if build_result["status"]=="success" else "Build failed"
|
| 1053 |
+
}
|
| 1054 |
+
|
| 1055 |
+
|
| 1056 |
+
# ========== Routes (all blocking calls offloaded to threads) ==========
|
| 1057 |
+
@app.get("/health")
|
| 1058 |
+
async def health():
|
| 1059 |
+
kv = Memory.all_kv()
|
| 1060 |
+
return {
|
| 1061 |
+
"status": "ok",
|
| 1062 |
+
"service": "DOLOR3V MCP Gateway v6.0",
|
| 1063 |
+
"version": "6.0.0",
|
| 1064 |
+
"port": PORT,
|
| 1065 |
+
"tools": TOOL_LIST,
|
| 1066 |
+
"safe_mode": SAFE_MODE,
|
| 1067 |
+
"providers": {
|
| 1068 |
+
"ollama": True,
|
| 1069 |
+
"groq": bool(GROQ_KEY),
|
| 1070 |
+
"openrouter": bool(OPENROUTER_KEY),
|
| 1071 |
+
"github": bool(GITHUB_TOKEN),
|
| 1072 |
+
"surge": bool(SURGE_TOKEN),
|
| 1073 |
+
"hf": bool(HF_TOKEN),
|
| 1074 |
+
"tabby": True,
|
| 1075 |
+
"event_bus": True
|
| 1076 |
+
},
|
| 1077 |
+
"memory_keys": len(kv),
|
| 1078 |
+
"uptime": time.time()
|
| 1079 |
+
}
|
| 1080 |
+
|
| 1081 |
+
@app.get("/v1/tools")
|
| 1082 |
+
async def list_tools():
|
| 1083 |
+
return {"tools": TOOL_LIST}
|
| 1084 |
+
|
| 1085 |
+
@app.get("/v1/memory")
|
| 1086 |
+
async def get_memory():
|
| 1087 |
+
return Memory.all_kv()
|
| 1088 |
+
|
| 1089 |
+
@app.post("/v1/tools/call")
|
| 1090 |
+
async def call_tool(request: Request):
|
| 1091 |
+
body = await request.json()
|
| 1092 |
+
tool = body.get("tool")
|
| 1093 |
+
args = body.get("arguments", body.get("args", {}))
|
| 1094 |
+
t0 = time.time()
|
| 1095 |
+
try:
|
| 1096 |
+
result = await asyncio.to_thread(retry_tool, execute_tool, tool, args)
|
| 1097 |
+
ms = int((time.time()-t0)*1000)
|
| 1098 |
+
emit_event("custom", {"tool":tool,"ms":ms}, "mcp")
|
| 1099 |
+
return {"result": result, "tool": tool, "ms": ms}
|
| 1100 |
+
except Exception as e:
|
| 1101 |
+
raise HTTPException(500, detail=str(e))
|
| 1102 |
+
|
| 1103 |
+
@app.post("/v1/chat/completions")
|
| 1104 |
+
async def chat(request: Request):
|
| 1105 |
+
body = await request.json()
|
| 1106 |
+
messages = body.get("messages", [])
|
| 1107 |
+
model = body.get("model", "")
|
| 1108 |
+
tools = body.get("tools")
|
| 1109 |
+
tool_choice = body.get("tool_choice")
|
| 1110 |
+
message, provider = await asyncio.to_thread(llm_route, messages, model, tools, tool_choice)
|
| 1111 |
+
finish_reason = "tool_calls" if message.get("tool_calls") else "stop"
|
| 1112 |
+
return {
|
| 1113 |
+
"id": f"dolor3v-{int(time.time())}",
|
| 1114 |
+
"object": "chat.completion",
|
| 1115 |
+
"provider": provider,
|
| 1116 |
+
"choices": [{"index": 0, "message": message, "finish_reason": finish_reason}]
|
| 1117 |
+
}
|
| 1118 |
+
|
| 1119 |
+
@app.post("/v1/search")
|
| 1120 |
+
async def search(request: Request):
|
| 1121 |
+
body = await request.json()
|
| 1122 |
+
results = await asyncio.to_thread(Firewall.search, body.get("query",""), body.get("max_results",5))
|
| 1123 |
+
return {"results": results}
|
| 1124 |
+
|
| 1125 |
+
@app.post("/v1/memory")
|
| 1126 |
+
async def memory_ops(request: Request):
|
| 1127 |
+
body = await request.json()
|
| 1128 |
+
action = body.get("action","set")
|
| 1129 |
+
if action == "set":
|
| 1130 |
+
await asyncio.to_thread(Memory.set, body["key"], body["value"])
|
| 1131 |
+
return {"ok": True}
|
| 1132 |
+
elif action == "get":
|
| 1133 |
+
val = await asyncio.to_thread(Memory.get, body["key"])
|
| 1134 |
+
return {"value": val}
|
| 1135 |
+
elif action == "search":
|
| 1136 |
+
r = await asyncio.to_thread(Memory.search, body.get("query",""))
|
| 1137 |
+
return {"results": [x["text"][:300] for x in r]}
|
| 1138 |
+
elif action == "ingest":
|
| 1139 |
+
await asyncio.to_thread(Memory.ingest, body.get("text",""), body.get("source","api"))
|
| 1140 |
+
return {"ok": True}
|
| 1141 |
+
else:
|
| 1142 |
+
raise HTTPException(400, "unknown action")
|
| 1143 |
+
|
| 1144 |
+
@app.post("/v1/deploy/github")
|
| 1145 |
+
async def deploy_github(request: Request):
|
| 1146 |
+
body = await request.json()
|
| 1147 |
+
result = await asyncio.to_thread(GitHub.push, body.get("project_path",""), body.get("repo_name","dolor3v-project"), body.get("message","DOLOR3V build"))
|
| 1148 |
+
return {"result": result}
|
| 1149 |
+
|
| 1150 |
+
@app.post("/v1/deploy/surge")
|
| 1151 |
+
async def deploy_surge(request: Request):
|
| 1152 |
+
body = await request.json()
|
| 1153 |
+
url, err = await asyncio.to_thread(Surge.deploy, body.get("project_path",""), body.get("subdomain"))
|
| 1154 |
+
if url:
|
| 1155 |
+
return {"url": url}
|
| 1156 |
+
else:
|
| 1157 |
+
raise HTTPException(500, detail=err)
|
| 1158 |
+
|
| 1159 |
+
@app.post("/v1/deploy/hf")
|
| 1160 |
+
async def deploy_hf(request: Request):
|
| 1161 |
+
body = await request.json()
|
| 1162 |
+
result = await asyncio.to_thread(execute_tool, "hf_deploy", body)
|
| 1163 |
+
return {"result": result}
|
| 1164 |
+
|
| 1165 |
+
@app.post("/v1/deploy/source")
|
| 1166 |
+
async def deploy_source():
|
| 1167 |
+
result = await asyncio.to_thread(GitHub.push_source)
|
| 1168 |
+
return {"result": result}
|
| 1169 |
+
|
| 1170 |
+
@app.post("/v1/complete")
|
| 1171 |
+
async def complete(request: Request):
|
| 1172 |
+
body = await request.json()
|
| 1173 |
+
result = await asyncio.to_thread(tabby_complete, body.get("prefix",""), body.get("suffix",""), body.get("lang","python"))
|
| 1174 |
+
return {"completion": result}
|
| 1175 |
+
|
| 1176 |
+
@app.post("/v1/firewall")
|
| 1177 |
+
async def firewall(request: Request):
|
| 1178 |
+
body = await request.json()
|
| 1179 |
+
ctx = await asyncio.to_thread(Firewall.gather_context, body.get("prompt",""))
|
| 1180 |
+
return {"context": ctx}
|
| 1181 |
+
|
| 1182 |
+
@app.post("/watchdog")
|
| 1183 |
+
async def watchdog(request: Request):
|
| 1184 |
+
body = await request.json()
|
| 1185 |
+
emit_event("watchdog", body, "mcp-watchdog")
|
| 1186 |
+
return {"ok": True}
|
| 1187 |
+
|
| 1188 |
+
@app.post("/v1/build")
|
| 1189 |
+
async def build(request: Request):
|
| 1190 |
+
body = await request.json()
|
| 1191 |
+
prompt = body.get("prompt","")
|
| 1192 |
+
slug = re.sub(r'[^a-z0-9-]','-', prompt.lower())[:30]
|
| 1193 |
+
ts = datetime.now().strftime("%m%d%H%M")
|
| 1194 |
+
proj_dir = f"{PROJECTS_DIR}/{slug}-{ts}"
|
| 1195 |
+
os.makedirs(proj_dir, exist_ok=True)
|
| 1196 |
+
|
| 1197 |
+
fw_ctx = await asyncio.to_thread(Firewall.gather_context, prompt)
|
| 1198 |
+
system = load_master_prompt() + f"""
|
| 1199 |
+
|
| 1200 |
+
You are DOLOR3V autonomous production web builder.
|
| 1201 |
+
|
| 1202 |
+
Tools available:
|
| 1203 |
+
{', '.join(TOOL_LIST)}
|
| 1204 |
+
|
| 1205 |
+
Project directory:
|
| 1206 |
+
{proj_dir}
|
| 1207 |
+
|
| 1208 |
+
Firewall context:
|
| 1209 |
+
{fw_ctx}
|
| 1210 |
+
|
| 1211 |
+
|
| 1212 |
+
STRICT BUILD RULES:
|
| 1213 |
+
|
| 1214 |
+
1. You are building a real website.
|
| 1215 |
+
2. NEVER create:
|
| 1216 |
+
- config.json
|
| 1217 |
+
- output.txt
|
| 1218 |
+
- output.typescript
|
| 1219 |
+
- package files
|
| 1220 |
+
- random files
|
| 1221 |
+
|
| 1222 |
+
3. ONLY create:
|
| 1223 |
+
index.html
|
| 1224 |
+
style.css
|
| 1225 |
+
script.js
|
| 1226 |
+
|
| 1227 |
+
|
| 1228 |
+
4. Before writing files:
|
| 1229 |
+
- use web_search once
|
| 1230 |
+
- gather design inspiration
|
| 1231 |
+
|
| 1232 |
+
|
| 1233 |
+
5. HTML requirements:
|
| 1234 |
+
- semantic HTML5
|
| 1235 |
+
- SEO meta tags
|
| 1236 |
+
- hero section
|
| 1237 |
+
- features
|
| 1238 |
+
- testimonials
|
| 1239 |
+
- FAQ
|
| 1240 |
+
- footer
|
| 1241 |
+
|
| 1242 |
+
|
| 1243 |
+
6. CSS requirements:
|
| 1244 |
+
- custom color palette
|
| 1245 |
+
- responsive mobile design
|
| 1246 |
+
- animations
|
| 1247 |
+
- modern UI
|
| 1248 |
+
|
| 1249 |
+
|
| 1250 |
+
7. JS requirements:
|
| 1251 |
+
- real interactions
|
| 1252 |
+
- no empty files
|
| 1253 |
+
|
| 1254 |
+
|
| 1255 |
+
8. Use write_file for every file.
|
| 1256 |
+
|
| 1257 |
+
9. After completion reply ONLY:
|
| 1258 |
+
|
| 1259 |
+
◆DONE
|
| 1260 |
+
|
| 1261 |
+
"""
|
| 1262 |
+
msgs = [{"role": "system", "content": system}, {"role": "user", "content": prompt}]
|
| 1263 |
+
|
| 1264 |
+
results = []
|
| 1265 |
+
parse_fail_count = 0
|
| 1266 |
+
response, provider = "", "groq"
|
| 1267 |
+
MAX_BUILD_STEPS = 20
|
| 1268 |
+
for _step in range(MAX_BUILD_STEPS):
|
| 1269 |
+
response, provider = await asyncio.to_thread(llm_route_quality, msgs)
|
| 1270 |
+
|
| 1271 |
+
step_results = []
|
| 1272 |
+
decoder = json.JSONDecoder()
|
| 1273 |
+
idx = 0
|
| 1274 |
+
text_len = len(response)
|
| 1275 |
+
while idx < text_len:
|
| 1276 |
+
brace_idx = response.find("{", idx)
|
| 1277 |
+
if brace_idx == -1:
|
| 1278 |
+
break
|
| 1279 |
+
try:
|
| 1280 |
+
tc, end_idx = decoder.raw_decode(response, brace_idx)
|
| 1281 |
+
except json.JSONDecodeError:
|
| 1282 |
+
idx = brace_idx + 1
|
| 1283 |
+
continue
|
| 1284 |
+
idx = end_idx
|
| 1285 |
+
if isinstance(tc, dict) and "tool" not in tc:
|
| 1286 |
+
preceding = response[max(0, brace_idx-150):brace_idx].lower()
|
| 1287 |
+
inferred_tool = None
|
| 1288 |
+
for tname in TOOL_LIST:
|
| 1289 |
+
if tname.lower() in preceding:
|
| 1290 |
+
inferred_tool = tname
|
| 1291 |
+
break
|
| 1292 |
+
if inferred_tool:
|
| 1293 |
+
tc = {"tool": inferred_tool, "args": tc}
|
| 1294 |
+
if isinstance(tc, dict) and "tool" in tc:
|
| 1295 |
+
args = tc.get("args", {})
|
| 1296 |
+
if tc.get("tool") == "write_file" and not args.get("path") and not args.get("file_path"):
|
| 1297 |
+
content_preview = str(args.get("content", ""))
|
| 1298 |
+
if "<!DOCTYPE" in content_preview or "<html" in content_preview:
|
| 1299 |
+
default_name = "index.html"
|
| 1300 |
+
elif content_preview.strip().startswith("function") or "document." in content_preview[:200]:
|
| 1301 |
+
default_name = "script.js"
|
| 1302 |
+
elif "{" in content_preview[:100] and ":" in content_preview[:100] and "<" not in content_preview[:50]:
|
| 1303 |
+
default_name = "style.css"
|
| 1304 |
+
else:
|
| 1305 |
+
default_name = "output.txt"
|
| 1306 |
+
args["path"] = os.path.join(proj_dir, default_name)
|
| 1307 |
+
r = await asyncio.to_thread(retry_tool, execute_tool, tc["tool"], args)
|
| 1308 |
+
results.append({"tool": tc["tool"], "result": str(r)[:200]})
|
| 1309 |
+
step_results.append({"tool": tc["tool"], "result": str(r)[:1500]})
|
| 1310 |
+
|
| 1311 |
+
if not step_results:
|
| 1312 |
+
parse_fail_count += 1
|
| 1313 |
+
if parse_fail_count > 2:
|
| 1314 |
+
break
|
| 1315 |
+
msgs.append({"role": "assistant", "content": response})
|
| 1316 |
+
msgs.append({"role": "user", "content": "I could not parse a valid tool call from that response. Respond with ONLY one JSON object in the exact shape: {\"tool\": \"<tool_name>\", \"args\": {...}}. No narration, no repeated objects, no other text. Valid tool names: " + ", ".join(TOOL_LIST)})
|
| 1317 |
+
continue
|
| 1318 |
+
|
| 1319 |
+
qa_passed = any(
|
| 1320 |
+
r.get("tool") == "qa_review" and "passed" in str(r.get("result", "")).lower()
|
| 1321 |
+
for r in step_results
|
| 1322 |
+
)
|
| 1323 |
+
if qa_passed:
|
| 1324 |
+
response = "\u25c6DONE"
|
| 1325 |
+
break
|
| 1326 |
+
|
| 1327 |
+
msgs.append({"role": "assistant", "content": response})
|
| 1328 |
+
msgs.append({"role": "user", "content": "Tool results:\n" + json.dumps(step_results)[:3000] + "\nPerform QA review before completion. Check design quality, responsiveness, completeness, and production readiness. Improve if needed before responding \u25c6DONE."})
|
| 1329 |
+
|
| 1330 |
+
wrote_files = any(r.get("tool") == "write_file" for r in results)
|
| 1331 |
+
# If no files were written via tool calls, auto-extract code blocks and save them
|
| 1332 |
+
if not wrote_files:
|
| 1333 |
+
code_blocks = re.findall(r'```(\w+)?\n(.*?)```', response, re.DOTALL)
|
| 1334 |
+
if code_blocks:
|
| 1335 |
+
for i, (lang, code) in enumerate(code_blocks):
|
| 1336 |
+
ext_map = {
|
| 1337 |
+
'html': 'index.html', 'css': 'style.css', 'js': 'script.js',
|
| 1338 |
+
'javascript': 'script.js', 'python': 'main.py', 'py': 'main.py',
|
| 1339 |
+
'sh': 'setup.sh', 'bash': 'setup.sh', 'json': 'config.json',
|
| 1340 |
+
'yaml': 'config.yaml', 'yml': 'config.yaml', '': 'output.txt'
|
| 1341 |
+
}
|
| 1342 |
+
ext = lang.strip() if lang else 'html'
|
| 1343 |
+
fname = ext_map.get(ext, f'output.{ext}')
|
| 1344 |
+
fpath = os.path.join(proj_dir, fname)
|
| 1345 |
+
with open(fpath, 'w') as f:
|
| 1346 |
+
f.write(code.strip())
|
| 1347 |
+
results.append({"tool": "write_file", "result": f"Saved {fname} ({len(code)} bytes)"})
|
| 1348 |
+
log(f"Auto-saved extracted code block: {fname}")
|
| 1349 |
+
else:
|
| 1350 |
+
html_match = re.search(r'(<!DOCTYPE html>.*?</html>|<html[^>]*>.*?</html>)', response, re.DOTALL | re.IGNORECASE)
|
| 1351 |
+
if html_match:
|
| 1352 |
+
extracted_html = html_match.group(1)
|
| 1353 |
+
fpath = os.path.join(proj_dir, 'index.html')
|
| 1354 |
+
with open(fpath, 'w') as f:
|
| 1355 |
+
f.write(extracted_html)
|
| 1356 |
+
results.append({"tool": "write_file", "result": f"Saved index.html extracted from response ({len(extracted_html)} bytes)"})
|
| 1357 |
+
log("Auto-extracted HTML block from response and saved as index.html")
|
| 1358 |
+
else:
|
| 1359 |
+
log("No extractable HTML found in malformed response; nothing written")
|
| 1360 |
+
|
| 1361 |
+
gh_url, surge_url = "", ""
|
| 1362 |
+
if GITHUB_TOKEN:
|
| 1363 |
+
try:
|
| 1364 |
+
gh_url = await asyncio.to_thread(GitHub.push, proj_dir, f"dolor3v-{slug}-{ts}")
|
| 1365 |
+
except:
|
| 1366 |
+
gh_url = "GitHub push failed – check GITHUB_TOKEN"
|
| 1367 |
+
try:
|
| 1368 |
+
surge_url_r, _ = await asyncio.to_thread(Surge.deploy, proj_dir)
|
| 1369 |
+
if surge_url_r: surge_url = surge_url_r
|
| 1370 |
+
except:
|
| 1371 |
+
pass
|
| 1372 |
+
|
| 1373 |
+
return {
|
| 1374 |
+
"ok": True,
|
| 1375 |
+
"prompt": prompt,
|
| 1376 |
+
"project": proj_dir,
|
| 1377 |
+
"provider": provider,
|
| 1378 |
+
"tools_run": results,
|
| 1379 |
+
"github": gh_url,
|
| 1380 |
+
"surge": surge_url,
|
| 1381 |
+
"response": response[:500]
|
| 1382 |
+
}
|
| 1383 |
+
AGENT_SYSTEM_PROMPT = (
|
| 1384 |
+
"You are DOLOR3V Agent, an autonomous coding/ops assistant with real tool access.\n\n"
|
| 1385 |
+
"Available tools: " + ", ".join(TOOL_LIST) + "\n\n"
|
| 1386 |
+
"To use a tool, respond with ONLY one JSON object on its own line and nothing else, in this exact shape:\n"
|
| 1387 |
+
'{"tool": "<tool_name>", "args": {"...": "..."}}\n\n'
|
| 1388 |
+
"After a tool result is returned to you, you may call another tool the same way, "
|
| 1389 |
+
"or give your final answer as plain text with no JSON and no markdown fences. "
|
| 1390 |
+
"Only call a tool when you actually need it; if you already know the answer, just answer directly.\n\n"
|
| 1391 |
+
"🔥 NEW: Use `project_index` to get an overview of the codebase, and `grep_code` to search for patterns. "
|
| 1392 |
+
"You can `spawn_agent` to handle subtasks in parallel. "
|
| 1393 |
+
"Safety: dangerous shell commands (rm -rf, git push --force, etc.) will be blocked unless DOLOR3V_SAFE_MODE=false."
|
| 1394 |
+
)
|
| 1395 |
+
|
| 1396 |
+
def maybe_summarize(messages, max_chars=MAX_CONTEXT_CHARS):
|
| 1397 |
+
"""Keep total payload under limit by keeping system prompt + last few messages only."""
|
| 1398 |
+
# Quick check: if already small enough, return as-is
|
| 1399 |
+
total = sum(len(str(m.get("content",""))) for m in messages)
|
| 1400 |
+
if total <= max_chars:
|
| 1401 |
+
return messages
|
| 1402 |
+
# Always keep system messages (first 2 usually)
|
| 1403 |
+
system_msgs = [m for m in messages if m["role"]=="system"][:2]
|
| 1404 |
+
# Keep the last 6 non-system messages
|
| 1405 |
+
other_msgs = [m for m in messages if m["role"]!="system"]
|
| 1406 |
+
keep = other_msgs[-6:] if len(other_msgs) > 6 else other_msgs
|
| 1407 |
+
# Insert a summary note
|
| 1408 |
+
summary = {"role":"system","content":"[Earlier conversation truncated to fit context window]"}
|
| 1409 |
+
return system_msgs + [summary] + keep
|
| 1410 |
+
|
| 1411 |
+
def agent_llm(messages):
|
| 1412 |
+
return llm_route(messages)
|
| 1413 |
+
|
| 1414 |
+
@app.post("/v1/agent")
|
| 1415 |
+
async def agent(request: Request):
|
| 1416 |
+
body = await request.json()
|
| 1417 |
+
user_prompt = body.get("prompt", "")
|
| 1418 |
+
history = body.get("history", [])
|
| 1419 |
+
max_steps = body.get("max_steps", 6)
|
| 1420 |
+
|
| 1421 |
+
# Automatic project context (Claude Code feature)
|
| 1422 |
+
proj_index = await asyncio.to_thread(project_index, ".")
|
| 1423 |
+
sys_content = AGENT_SYSTEM_PROMPT + "\n\n=== Current project context ===\n" + proj_index
|
| 1424 |
+
|
| 1425 |
+
messages = [{"role":"system","content":sys_content}] + history + [{"role":"user","content":user_prompt}]
|
| 1426 |
+
steps = []
|
| 1427 |
+
final_text = ""
|
| 1428 |
+
provider = "none"
|
| 1429 |
+
|
| 1430 |
+
for _ in range(max_steps):
|
| 1431 |
+
messages = maybe_summarize(messages)
|
| 1432 |
+
# Try primary LLM call, then fallback if it fails
|
| 1433 |
+
reply = None
|
| 1434 |
+
provider = "none"
|
| 1435 |
+
llm_error = ""
|
| 1436 |
+
for attempt in range(2):
|
| 1437 |
+
try:
|
| 1438 |
+
reply, provider = await asyncio.to_thread(agent_llm, messages)
|
| 1439 |
+
break
|
| 1440 |
+
except Exception as e:
|
| 1441 |
+
llm_error = str(e)
|
| 1442 |
+
log(f"Agent LLM attempt {attempt+1} failed: {e}")
|
| 1443 |
+
# On first failure, try forcing the next provider by removing Ollama from list
|
| 1444 |
+
if attempt == 0:
|
| 1445 |
+
messages.insert(0, {"role":"system","content":"Previous LLM failed. Please use a different provider."})
|
| 1446 |
+
if reply is None:
|
| 1447 |
+
final_text = f"All LLM providers failed: {llm_error}"
|
| 1448 |
+
break
|
| 1449 |
+
|
| 1450 |
+
line = reply.strip()
|
| 1451 |
+
if line.startswith("{") and '"tool"' in line:
|
| 1452 |
+
try:
|
| 1453 |
+
tc = json.loads(line)
|
| 1454 |
+
tool_name = tc.get("tool")
|
| 1455 |
+
tool_args = tc.get("args", {})
|
| 1456 |
+
tool_result = await asyncio.to_thread(retry_tool, execute_tool, tool_name, tool_args)
|
| 1457 |
+
steps.append({"tool": tool_name, "args": tool_args, "result": str(tool_result)[:500]})
|
| 1458 |
+
messages.append({"role":"assistant","content":line})
|
| 1459 |
+
messages.append({"role":"user","content":f"Tool result:\n{tool_result}"})
|
| 1460 |
+
continue
|
| 1461 |
+
except Exception:
|
| 1462 |
+
final_text = reply
|
| 1463 |
+
break
|
| 1464 |
+
# small delay to avoid Groq rate limits
|
| 1465 |
+
await asyncio.sleep(2)
|
| 1466 |
+
final_text = reply
|
| 1467 |
+
break
|
| 1468 |
+
else:
|
| 1469 |
+
final_text = "Max steps reached without a final answer."
|
| 1470 |
+
|
| 1471 |
+
emit_event("agent", {"prompt": user_prompt[:100], "steps": len(steps)}, "mcp-agent")
|
| 1472 |
+
|
| 1473 |
+
return {
|
| 1474 |
+
"response": final_text,
|
| 1475 |
+
"provider": provider,
|
| 1476 |
+
"steps": steps
|
| 1477 |
+
}
|
| 1478 |
+
|
| 1479 |
+
if __name__ == "__main__":
|
| 1480 |
+
print(f"""
|
| 1481 |
+
DOLOR3V MCP GATEWAY v6.0
|
| 1482 |
+
Port : {PORT}
|
| 1483 |
+
Tools : {len(TOOL_LIST)}
|
| 1484 |
+
Safe mode: {SAFE_MODE}
|
| 1485 |
+
GitHub : {"✅" if GITHUB_TOKEN else "❌ set GITHUB_TOKEN"}
|
| 1486 |
+
Surge : {"✅" if SURGE_TOKEN else "❌ set SURGE_TOKEN"}
|
| 1487 |
+
HF : {"✅" if HF_TOKEN else "❌ set HF_TOKEN"}
|
| 1488 |
+
Groq : {"✅" if GROQ_KEY else "❌ set GROQ_API_KEY"}
|
| 1489 |
+
""")
|
| 1490 |
+
uvicorn.run(app, host="0.0.0.0", port=PORT)
|
load_master_prompt.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
|
| 3 |
+
MASTER_PROMPT_FILE = "/opt/dolor3v/master_builder_prompt.txt"
|
| 4 |
+
|
| 5 |
+
def load_master_prompt():
|
| 6 |
+
try:
|
| 7 |
+
return Path(MASTER_PROMPT_FILE).read_text(encoding="utf-8")
|
| 8 |
+
except Exception as e:
|
| 9 |
+
return f"MASTER PROMPT LOAD ERROR: {e}"
|
master_builder_prompt.txt
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
You are DOLOR3V Ultra Builder — a senior product designer and staff frontend engineer who builds hyper-realistic, production-ready websites and applications. No demos. No basics. No templates.
|
| 2 |
+
|
| 3 |
+
STACK
|
| 4 |
+
Default stack for every build:
|
| 5 |
+
- Next.js (App Router) + TypeScript
|
| 6 |
+
- Tailwind CSS v4
|
| 7 |
+
- Framer Motion for animations
|
| 8 |
+
- Real working image URLs from https://picsum.photos/seed/keyword/width/height
|
| 9 |
+
- Google Fonts (pair a display font with a clean body font)
|
| 10 |
+
|
| 11 |
+
RESEARCH FIRST
|
| 12 |
+
Before writing any code:
|
| 13 |
+
1. Call web_search to find real design inspiration, color palettes, and copy ideas relevant to the prompt.
|
| 14 |
+
2. Optionally call web_fetch on the most promising result to extract real content and visual direction.
|
| 15 |
+
3. Use memory_search to recall any relevant past context.
|
| 16 |
+
|
| 17 |
+
SCAFFOLD
|
| 18 |
+
After research, scaffold the project:
|
| 19 |
+
{"tool": "build_project", "args": {"path": "PROJECT_DIR", "command": "pnpm create next-app . --typescript --tailwind --eslint --app --no-src-dir --no-git --yes"}}
|
| 20 |
+
|
| 21 |
+
Then install Framer Motion:
|
| 22 |
+
{"tool": "build_project", "args": {"path": "PROJECT_DIR", "command": "pnpm add framer-motion"}}
|
| 23 |
+
|
| 24 |
+
Then configure static export (required for Surge deployment):
|
| 25 |
+
{"tool": "write_file", "args": {"path": "PROJECT_DIR/next.config.ts", "content": "import type { NextConfig } from 'next';
|
| 26 |
+
const nextConfig: NextConfig = { output: 'export', images: { unoptimized: true } };
|
| 27 |
+
export default nextConfig;"}}
|
| 28 |
+
|
| 29 |
+
DESIGN QUALITY BAR
|
| 30 |
+
- Deliberate color palette (2-3 colors) inspired by your web_search results. Never default browser blue or plain white.
|
| 31 |
+
- Glassmorphism, gradients, subtle hover effects, scroll animations via Framer Motion where appropriate.
|
| 32 |
+
- Every image: real working URL from picsum.photos or placehold.co. Never broken or fake src.
|
| 33 |
+
- Typography: pair a Google Fonts display heading font with a clean sans-serif body font.
|
| 34 |
+
|
| 35 |
+
PAGE STRUCTURE (minimum)
|
| 36 |
+
Build these pages as separate files under app/:
|
| 37 |
+
- page.tsx (Home): hero, features, testimonials (2-3 real quotes), FAQ (3-5 Q&As), CTA section
|
| 38 |
+
- about/page.tsx: brand story, team section
|
| 39 |
+
- services/page.tsx: detailed service cards
|
| 40 |
+
- contact/page.tsx: contact form, location, social links
|
| 41 |
+
- Shared components/: Navbar.tsx, Footer.tsx, shared UI components
|
| 42 |
+
|
| 43 |
+
BRANDING
|
| 44 |
+
Give the project a real brand identity:
|
| 45 |
+
- A memorable name fitting the prompt
|
| 46 |
+
- A short punchy tagline
|
| 47 |
+
- Consistent color, font, and tone across all pages
|
| 48 |
+
|
| 49 |
+
BUILD
|
| 50 |
+
After writing all files, build the project:
|
| 51 |
+
{"tool": "build_project", "args": {"path": "PROJECT_DIR", "command": "pnpm run build"}}
|
| 52 |
+
|
| 53 |
+
If the build fails, call web_search for the specific error message, fix the code, and rebuild.
|
| 54 |
+
|
| 55 |
+
DEPLOY
|
| 56 |
+
After a successful build, deploy:
|
| 57 |
+
{"tool": "surge_deploy", "args": {"path": "PROJECT_DIR/out"}}
|
| 58 |
+
{"tool": "github_push", "args": {"path": "PROJECT_DIR", "repo": "REPO_NAME"}}
|
| 59 |
+
|
| 60 |
+
QUALITY CHECK
|
| 61 |
+
After deploying, call qa_review on PROJECT_DIR to check for issues. Fix anything found.
|
| 62 |
+
|
| 63 |
+
TOOL RESULTS
|
| 64 |
+
Tool results are always returned to you automatically in the next message. Never try to read a tool result from a file. Never omit the "tool" key from a tool call.
|
| 65 |
+
|
| 66 |
+
COMPLETION
|
| 67 |
+
Once built, deployed, and qa_review passes, respond with exactly: ◆DONE
|
requirements.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi
|
| 2 |
+
uvicorn
|
| 3 |
+
requests
|
| 4 |
+
beautifulsoup4
|
| 5 |
+
python-dotenv
|
| 6 |
+
colorthief
|
| 7 |
+
pillow
|