josephrw commited on
Commit
f7adc47
·
verified ·
1 Parent(s): 740072c

Restore Giant GPT Terminal Kernel v3.1.1 — GPT relay

Browse files
Files changed (4) hide show
  1. .gitignore +5 -0
  2. Dockerfile +1 -1
  3. README.md +4 -4
  4. app.py +444 -129
.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ *.db
5
+ data/
Dockerfile CHANGED
@@ -2,7 +2,7 @@ FROM python:3.11-slim
2
 
3
  WORKDIR /app
4
 
5
- RUN pip install --no-cache-dir fastapi uvicorn[standard] jinja2
6
 
7
  COPY . /app
8
 
 
2
 
3
  WORKDIR /app
4
 
5
+ RUN pip install --no-cache-dir fastapi uvicorn[standard] pydantic
6
 
7
  COPY . /app
8
 
README.md CHANGED
@@ -1,8 +1,8 @@
1
  ---
2
- title: MasseurBoost Endpoint
3
- emoji: 💆
4
- colorFrom: purple
5
- colorTo: pink
6
  sdk: docker
7
  app_port: 7860
8
  pinned: false
 
1
  ---
2
+ title: GPT Relay Endpoint
3
+ emoji: 🚀
4
+ colorFrom: gray
5
+ colorTo: blue
6
  sdk: docker
7
  app_port: 7860
8
  pinned: false
app.py CHANGED
@@ -1,170 +1,485 @@
1
- """josephrw-endpoint HF SpaceMasseurBoost API endpoint.
2
 
3
- Exposes the SaaS app on Hugging Face Spaces with a lightweight
4
- read-only mode (no local DB writes, no Selenium).
5
  """
6
  import os
7
- import sys
8
  import json
 
 
 
9
  import sqlite3
 
 
10
  from pathlib import Path
11
- from fastapi import FastAPI, Request, Query
12
- from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
 
 
 
13
  from fastapi.staticfiles import StaticFiles
14
- from fastapi.templating import Jinja2Templates
 
 
 
 
 
 
 
15
 
16
- # ─── Config ──────────────────────────────────────────────────
17
- # The DB is bundled in the Space repo under /data/masseurs.db
18
- # Falls back to a small demo DB if not present.
19
- DB_PATH = Path(os.environ.get("DB_PATH", "/data/masseurs.db"))
20
- if not DB_PATH.exists():
21
- DB_PATH = Path(__file__).parent / "data" / "masseurs.db"
22
 
23
- BASE_DIR = Path(__file__).parent
24
- TEMPLATES_DIR = BASE_DIR / "templates"
25
- STATIC_DIR = BASE_DIR / "static"
26
 
27
- app = FastAPI(title="MasseurBoost Endpoint", docs_url="/api/docs")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
- if TEMPLATES_DIR.exists():
30
- templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
31
- if STATIC_DIR.exists():
32
- app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
33
 
34
- RENTMASSEUR_BASE = "https://rentmasseur.com"
35
 
36
 
37
- def get_db():
38
- conn = sqlite3.connect(str(DB_PATH))
39
  conn.row_factory = sqlite3.Row
40
  return conn
41
 
42
 
43
- # ─── Health ──────────────────────────────────────────────────
44
- @app.get("/api/health")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  async def health():
46
- return {"status": "ok", "db": str(DB_PATH), "db_exists": DB_PATH.exists()}
47
 
48
 
49
- # ─── Market stats ────────────────────────────────────────────
50
- @app.get("/api/market-stats")
51
- async def market_stats():
52
- if not DB_PATH.exists():
53
- return {"error": "DB not found", "total_masseurs": 0}
54
- conn = get_db()
55
- c = conn.cursor()
56
- total = c.execute("SELECT COUNT(*) FROM masseurs").fetchone()[0]
57
- cities = c.execute("SELECT COUNT(DISTINCT city) FROM masseurs").fetchone()[0]
58
- gold = c.execute("SELECT COUNT(*) FROM masseurs WHERE is_gold=1").fetchone()[0]
59
- avg_vpd = c.execute("SELECT AVG(views_per_day) FROM masseurs WHERE views_per_day > 0").fetchone()[0]
60
- conn.close()
61
- return {
62
- "total_masseurs": total,
63
- "cities_covered": cities,
64
- "gold_members": gold,
65
- "avg_views_per_day": round(avg_vpd or 0, 1),
66
- }
67
 
68
 
69
- # ─── Search masseurs (with profile URLs) ─────────────────────
70
- @app.get("/api/search")
71
- async def search_masseurs(
72
- city: str = Query(""),
73
- limit: int = Query(20, le=100),
74
- offset: int = Query(0),
75
- sort: str = Query("views_per_day"),
76
- min_vpd: float = Query(0),
77
- ):
78
- if not DB_PATH.exists():
79
- return {"error": "DB not found", "total": 0, "results": []}
80
- conn = get_db()
81
- c = conn.cursor()
 
 
 
 
 
 
 
 
82
 
83
- where = "WHERE views_per_day >= ?"
84
- params = [min_vpd]
85
- if city:
86
- where += " AND city = ?"
87
- params.append(city)
88
 
89
- valid_sorts = {"views_per_day", "visits", "reviews_count", "rating", "bio"}
90
- sort = sort if sort in valid_sorts else "views_per_day"
 
 
 
 
 
 
 
 
 
 
 
 
91
 
92
- total = c.execute(f"SELECT COUNT(*) FROM masseurs {where}", params).fetchone()[0]
93
- rows = c.execute(
94
- f"SELECT username, city, views_per_day, visits, member_since, rating, reviews_count, is_gold, photo_count FROM masseurs {where} ORDER BY {sort} DESC LIMIT ? OFFSET ?",
95
- params + [limit, offset],
96
- ).fetchall()
97
- conn.close()
98
 
99
- return {
100
- "total": total,
101
- "results": [
102
- {
103
- "username": r[0],
104
- "city": r[1],
105
- "vpd": r[2],
106
- "visits": r[3],
107
- "since": r[4],
108
- "rating": r[5],
109
- "reviews": r[6],
110
- "gold": bool(r[7]),
111
- "photos": r[8],
112
- "profile_url": f"{RENTMASSEUR_BASE}/{r[0]}",
113
- "internal_url": f"/profile/{r[0]}",
114
- }
115
- for r in rows
116
- ],
117
- }
118
 
119
 
120
- # ─── Analyze a profile ───────────────────────────────────────
121
- @app.get("/api/analyze/{username}")
122
- async def analyze_profile(username: str):
123
- if not DB_PATH.exists():
124
- return {"error": "DB not found"}
125
- conn = get_db()
126
- c = conn.cursor()
127
- row = c.execute("SELECT * FROM masseurs WHERE username = ?", (username,)).fetchone()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  if not row:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  conn.close()
130
- return {"error": "Profile not found", "username": username}
131
 
132
- profile = dict(row)
133
- total = c.execute("SELECT COUNT(*) FROM masseurs").fetchone()[0]
134
- rank = c.execute("SELECT COUNT(*) FROM masseurs WHERE views_per_day > ?", (profile["views_per_day"],)).fetchone()[0] + 1
135
- percentile = round((1 - rank / total) * 100, 1)
136
- conn.close()
137
 
138
- return {
139
- "username": username,
140
- "rank": rank,
141
- "total": total,
142
- "percentile": percentile,
143
- "views_per_day": profile["views_per_day"],
144
- "total_visits": profile["visits"],
145
- "member_since": profile["member_since"],
146
- "rating": profile["rating"],
147
- "reviews_count": profile["reviews_count"],
148
- "is_gold": bool(profile["is_gold"]),
149
- "photo_count": profile["photo_count"],
150
- "bio_length": len(profile.get("bio", "") or ""),
151
- "profile_url": f"{RENTMASSEUR_BASE}/{username}",
152
- }
153
 
154
 
155
- # ─── Root ────────────────────────────────────────────────────
 
 
156
  @app.get("/")
157
  async def root():
158
  return {
159
- "service": "MasseurBoost Endpoint",
160
- "spaces": [
161
- "https://josephrw-endpoint.hf.space",
162
- "https://josephrw-csc-engine.hf.space",
163
- ],
164
  "endpoints": [
165
- "/api/health",
166
- "/api/market-stats",
167
- "/api/search",
168
- "/api/analyze/{username}",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
  ],
170
  }
 
1
+ """Giant GPT Terminal Kernel josephrw-endpoint HF Space.
2
 
3
+ GPT Actions compatible external terminal, Python, file, artifact URL,
4
+ dynamic tool, memory, and receipt kernel. Hardened to avoid post-execution 500s.
5
  """
6
  import os
 
7
  import json
8
+ import time
9
+ import uuid
10
+ import hashlib
11
  import sqlite3
12
+ import subprocess
13
+ import threading
14
  from pathlib import Path
15
+ from datetime import datetime, timezone
16
+ from typing import Optional
17
+
18
+ from fastapi import FastAPI, Request, Query, HTTPException
19
+ from fastapi.responses import JSONResponse, PlainTextResponse
20
  from fastapi.staticfiles import StaticFiles
21
+ from pydantic import BaseModel, Field
22
+
23
+ app = FastAPI(
24
+ title="Giant GPT Terminal Kernel",
25
+ version="3.1.1",
26
+ docs_url="/docs",
27
+ openapi_url="/openapi.json",
28
+ )
29
 
30
+ # ─── Paths ────────────────────────────────────────────────────
31
+ WORKSPACE_ROOT = Path(os.environ.get("WORKSPACE_ROOT", str(Path(__file__).parent / "data" / "workspaces")))
32
+ ARTIFACTS_DIR = Path(os.environ.get("ARTIFACTS_DIR", str(Path(__file__).parent / "data" / "artifacts")))
33
+ DB_PATH = Path(os.environ.get("KERNEL_DB", str(Path(__file__).parent / "data" / "kernel.db")))
 
 
34
 
35
+ WORKSPACE_ROOT.mkdir(parents=True, exist_ok=True)
36
+ ARTIFACTS_DIR.mkdir(parents=True, exist_ok=True)
 
37
 
38
+ # ─── SQLite for receipts, memory, tools, sessions ─────────────
39
+ def _init_db():
40
+ conn = sqlite3.connect(str(DB_PATH))
41
+ c = conn.cursor()
42
+ c.execute("""CREATE TABLE IF NOT EXISTS receipts (
43
+ id TEXT PRIMARY KEY, kind TEXT, workspace TEXT, summary TEXT,
44
+ sha256 TEXT, created_at TEXT, data_json TEXT
45
+ )""")
46
+ c.execute("""CREATE TABLE IF NOT EXISTS memory (
47
+ id TEXT PRIMARY KEY, topic TEXT, content TEXT, utility REAL,
48
+ tags TEXT, created_at TEXT
49
+ )""")
50
+ c.execute("""CREATE TABLE IF NOT EXISTS tools (
51
+ name TEXT PRIMARY KEY, description TEXT, mode TEXT,
52
+ command_template TEXT, schema_data TEXT, enabled INTEGER,
53
+ created_at TEXT
54
+ )""")
55
+ c.execute("""CREATE TABLE IF NOT EXISTS sessions (
56
+ session_id TEXT PRIMARY KEY, workspace TEXT, cwd TEXT,
57
+ created_at TEXT, active INTEGER
58
+ )""")
59
+ conn.commit()
60
+ conn.close()
61
 
62
+ _init_db()
 
 
 
63
 
64
+ DB_LOCK = threading.Lock()
65
 
66
 
67
+ def _db():
68
+ conn = sqlite3.connect(str(DB_PATH), timeout=10)
69
  conn.row_factory = sqlite3.Row
70
  return conn
71
 
72
 
73
+ # ─── Helpers ──────────────────────────────────────────────────
74
+ def _ws_path(workspace: str, path: str = ".") -> Path:
75
+ base = WORKSPACE_ROOT / workspace
76
+ base.mkdir(parents=True, exist_ok=True)
77
+ resolved = (base / path).resolve()
78
+ if not str(resolved).startswith(str(base.resolve())):
79
+ raise HTTPException(status_code=400, detail="Path traversal denied")
80
+ return resolved
81
+
82
+
83
+ def _truncate(text: str, limit: int = 50000) -> tuple:
84
+ if len(text) > limit:
85
+ return text[:limit], True
86
+ return text, False
87
+
88
+
89
+ def _receipt(kind: str, summary: str, data: dict, workspace: Optional[str] = None) -> dict:
90
+ rid = uuid.uuid4().hex[:16]
91
+ sha = hashlib.sha256(json.dumps(data, sort_keys=True, default=str).encode()).hexdigest()
92
+ now = datetime.now(timezone.utc).isoformat()
93
+ with DB_LOCK:
94
+ conn = _db()
95
+ conn.execute(
96
+ "INSERT INTO receipts VALUES (?,?,?,?,?,?,?)",
97
+ (rid, kind, workspace, summary, sha, now, json.dumps(data, default=str)),
98
+ )
99
+ conn.commit()
100
+ conn.close()
101
+ return {"id": rid, "kind": kind, "workspace": workspace, "summary": summary, "sha256": sha, "created_at": now}
102
+
103
+
104
+ def _safe_run(cmd: list, cwd: Path, timeout: int) -> dict:
105
+ try:
106
+ result = subprocess.run(
107
+ cmd, cwd=str(cwd), capture_output=True, text=True, timeout=timeout,
108
+ )
109
+ stdout, truncated = _truncate(result.stdout)
110
+ stderr, _ = _truncate(result.stderr)
111
+ return {"returncode": result.returncode, "stdout": stdout, "stderr": stderr, "truncated": truncated}
112
+ except subprocess.TimeoutExpired:
113
+ return {"returncode": -1, "stdout": "", "stderr": f"Timed out after {timeout}s", "truncated": False}
114
+ except Exception as e:
115
+ return {"returncode": -1, "stdout": "", "stderr": str(e), "truncated": False}
116
+
117
+
118
+ # ═══════════════════════════════════════════════════════════════
119
+ # MODELS
120
+ # ═══════════════════════════════════════════════════════════════
121
+ class TerminalRunRequest(BaseModel):
122
+ workspace: str = "default"
123
+ command: str
124
+ timeout_seconds: int = Field(default=10, ge=1, le=30)
125
+ cwd: str = "."
126
+ create_receipt: bool = True
127
+
128
+
129
+ class PythonRunRequest(BaseModel):
130
+ workspace: str = "default"
131
+ code: str
132
+ timeout_seconds: int = Field(default=10, ge=1, le=30)
133
+ create_receipt: bool = True
134
+
135
+
136
+ class SessionCreateRequest(BaseModel):
137
+ workspace: str = "default"
138
+ cwd: str = "."
139
+
140
+
141
+ class SessionRunRequest(BaseModel):
142
+ session_id: str
143
+ command: str
144
+ timeout_seconds: int = Field(default=10, ge=1, le=30)
145
+
146
+
147
+ class FileWriteRequest(BaseModel):
148
+ workspace: str = "default"
149
+ path: str
150
+ content: str
151
+ encoding: str = "utf-8"
152
+
153
+
154
+ class FileReadRequest(BaseModel):
155
+ workspace: str = "default"
156
+ path: str
157
+ max_bytes: int = Field(default=120000, ge=1, le=20000000)
158
+
159
+
160
+ class ListRequest(BaseModel):
161
+ workspace: str = "default"
162
+ path: str = "."
163
+ max_items: int = Field(default=250, ge=1, le=2000)
164
+
165
+
166
+ class ArtifactRequest(BaseModel):
167
+ workspace: str = "default"
168
+ filename: str
169
+ content: str
170
+ content_type: str = "text/plain"
171
+
172
+
173
+ class LearnRequest(BaseModel):
174
+ topic: str = "general"
175
+ content: str
176
+ utility: float = Field(default=0.5, ge=0, le=1)
177
+ tags: list = []
178
+
179
+
180
+ class RecallRequest(BaseModel):
181
+ query: str
182
+ limit: int = Field(default=8, ge=1, le=50)
183
+
184
+
185
+ class ToolRegisterRequest(BaseModel):
186
+ name: str
187
+ description: str = ""
188
+ mode: str = "command"
189
+ command_template: str = ""
190
+ schema_data: dict = {}
191
+ enabled: bool = True
192
+
193
+
194
+ class ToolInvokeRequest(BaseModel):
195
+ workspace: str = "default"
196
+ args: dict = {}
197
+ timeout_seconds: int = Field(default=10, ge=1, le=30)
198
+
199
+
200
+ # ═══════════════════════════════════════════════════════════════
201
+ # HEALTH
202
+ # ═══════════════════════════════════════════════════════════════
203
+ @app.get("/health")
204
  async def health():
205
+ return {"status": "ok", "version": "3.1.1", "workspaces": len(list(WORKSPACE_ROOT.iterdir()))}
206
 
207
 
208
+ # ═══════════════════════════════════════════════════════════════
209
+ # TERMINAL RUN
210
+ # ═══════════════════════════════════════════════════════════════
211
+ @app.post("/terminal/run")
212
+ async def run_terminal(req: TerminalRunRequest):
213
+ cwd = _ws_path(req.workspace, req.cwd)
214
+ result = _safe_run(["sh", "-c", req.command], cwd, req.timeout_seconds)
215
+ receipt = None
216
+ receipt_error = None
217
+ if req.create_receipt:
218
+ try:
219
+ receipt = _receipt("terminal_run", req.command[:80], result, req.workspace)
220
+ except Exception as e:
221
+ receipt_error = str(e)
222
+ return {"workspace": req.workspace, "cwd": req.cwd, "command": req.command, **result, "receipt": receipt, "receipt_error": receipt_error}
 
 
 
223
 
224
 
225
+ # ═══════════════════════════════════════════════════════════════
226
+ # PYTHON RUN
227
+ # ═══════════════════════════════════════════════════════════════
228
+ @app.post("/python/run")
229
+ async def run_python(req: PythonRunRequest):
230
+ ws_base = _ws_path(req.workspace)
231
+ script_path = ws_base / f"_run_{uuid.uuid4().hex[:8]}.py"
232
+ script_path.write_text(req.code)
233
+ result = _safe_run(["python3", str(script_path)], ws_base, req.timeout_seconds)
234
+ try:
235
+ script_path.unlink(missing_ok=True)
236
+ except Exception:
237
+ pass
238
+ receipt = None
239
+ receipt_error = None
240
+ if req.create_receipt:
241
+ try:
242
+ receipt = _receipt("python_run", req.code[:80], result, req.workspace)
243
+ except Exception as e:
244
+ receipt_error = str(e)
245
+ return {"workspace": req.workspace, "script": script_path.name, **result, "receipt": receipt, "receipt_error": receipt_error}
246
 
 
 
 
 
 
247
 
248
+ # ═══════════════════════════════════════════════════════════════
249
+ # SESSIONS
250
+ # ═══════════════════════════════════════════════════════════════
251
+ @app.post("/session/create")
252
+ async def create_session(req: SessionCreateRequest):
253
+ sid = uuid.uuid4().hex[:12]
254
+ _ws_path(req.workspace, req.cwd)
255
+ now = datetime.now(timezone.utc).isoformat()
256
+ with DB_LOCK:
257
+ conn = _db()
258
+ conn.execute("INSERT INTO sessions VALUES (?,?,?,?,1)", (sid, req.workspace, req.cwd, now))
259
+ conn.commit()
260
+ conn.close()
261
+ return {"session_id": sid, "workspace": req.workspace, "cwd": req.cwd, "created_at": now}
262
 
 
 
 
 
 
 
263
 
264
+ @app.post("/session/run")
265
+ async def run_session_command(req: SessionRunRequest):
266
+ with DB_LOCK:
267
+ conn = _db()
268
+ row = conn.execute("SELECT * FROM sessions WHERE session_id=? AND active=1", (req.session_id,)).fetchone()
269
+ conn.close()
270
+ if not row:
271
+ raise HTTPException(status_code=404, detail="Session not found or inactive")
272
+ cwd = _ws_path(row["workspace"], row["cwd"])
273
+ result = _safe_run(["sh", "-c", req.command], cwd, req.timeout_seconds)
274
+ return {"workspace": row["workspace"], "cwd": row["cwd"], "command": req.command, **result, "receipt": _receipt("session_run", req.command[:80], result, row["workspace"]), "receipt_error": None}
 
 
 
 
 
 
 
 
275
 
276
 
277
+ # ═══════════════════════════════════════════════════════════════
278
+ # FILES
279
+ # ═══════════════════════════════════════════════════════════════
280
+ @app.post("/files/write")
281
+ async def write_file(req: FileWriteRequest):
282
+ target = _ws_path(req.workspace, req.path)
283
+ target.parent.mkdir(parents=True, exist_ok=True)
284
+ target.write_text(req.content, encoding=req.encoding)
285
+ receipt = _receipt("file_write", req.path, {"bytes": len(req.content)}, req.workspace)
286
+ return {"status": "written", "path": req.path, "bytes": len(req.content), "receipt": receipt}
287
+
288
+
289
+ @app.post("/files/read")
290
+ async def read_file(req: FileReadRequest):
291
+ target = _ws_path(req.workspace, req.path)
292
+ if not target.exists():
293
+ raise HTTPException(status_code=404, detail="File not found")
294
+ if target.is_dir():
295
+ raise HTTPException(status_code=400, detail="Path is a directory")
296
+ data = target.read_bytes()[:req.max_bytes]
297
+ try:
298
+ text = data.decode("utf-8")
299
+ return {"path": req.path, "content": text, "bytes": len(data), "truncated": target.stat().st_size > req.max_bytes}
300
+ except UnicodeDecodeError:
301
+ import base64
302
+ return {"path": req.path, "content_base64": base64.b64encode(data).decode(), "bytes": len(data), "truncated": target.stat().st_size > req.max_bytes}
303
+
304
+
305
+ @app.post("/files/list")
306
+ async def list_files(req: ListRequest):
307
+ target = _ws_path(req.workspace, req.path)
308
+ if not target.exists():
309
+ return {"path": req.path, "entries": []}
310
+ entries = []
311
+ if target.is_dir():
312
+ for item in sorted(target.iterdir())[:req.max_items]:
313
+ entries.append({"name": item.name, "type": "dir" if item.is_dir() else "file", "size": item.stat().st_size if item.is_file() else None})
314
+ return {"path": req.path, "entries": entries}
315
+
316
+
317
+ @app.get("/workspace/tree")
318
+ async def get_workspace_tree(workspace: str = "default", max_items: int = 500):
319
+ base = _ws_path(workspace)
320
+ lines = []
321
+ count = 0
322
+ for p in sorted(base.rglob("*")):
323
+ if count >= max_items:
324
+ lines.append("... (truncated)")
325
+ break
326
+ rel = p.relative_to(base)
327
+ indent = " " * (len(rel.parts) - 1)
328
+ marker = "/" if p.is_dir() else ""
329
+ lines.append(f"{indent}{p.name}{marker}")
330
+ count += 1
331
+ return PlainTextResponse("\n".join(lines) if lines else "(empty)")
332
+
333
+
334
+ # ═══════════════════════════════════════════════════════════════
335
+ # ARTIFACTS
336
+ # ═══════════════════════════════════════════════════════════════
337
+ app.mount("/artifact", StaticFiles(directory=str(ARTIFACTS_DIR)), name="artifacts")
338
+
339
+
340
+ @app.post("/artifact/compile")
341
+ async def compile_artifact(req: ArtifactRequest):
342
+ artifact_hash = hashlib.sha256(req.content.encode()).hexdigest()[:16]
343
+ artifact_dir = ARTIFACTS_DIR / artifact_hash
344
+ artifact_dir.mkdir(parents=True, exist_ok=True)
345
+ (artifact_dir / req.filename).write_text(req.content)
346
+ (artifact_dir / "meta.json").write_text(json.dumps({"filename": req.filename, "content_type": req.content_type, "workspace": req.workspace, "sha256": artifact_hash, "created_at": datetime.now(timezone.utc).isoformat()}, indent=2))
347
+ receipt = _receipt("artifact", req.filename, {"hash": artifact_hash}, req.workspace)
348
+ return {"hash": artifact_hash, "filename": req.filename, "url": f"/artifact/{artifact_hash}/{req.filename}", "receipt": receipt}
349
+
350
+
351
+ @app.post("/url/issue")
352
+ async def issue_url(req: ArtifactRequest):
353
+ artifact_hash = hashlib.sha256(req.content.encode()).hexdigest()[:16]
354
+ artifact_dir = ARTIFACTS_DIR / artifact_hash
355
+ artifact_dir.mkdir(parents=True, exist_ok=True)
356
+ (artifact_dir / req.filename).write_text(req.content)
357
+ url = f"https://josephrw-endpoint.hf.space/artifact/{artifact_hash}/{req.filename}"
358
+ receipt = _receipt("url_issue", req.filename, {"url": url, "hash": artifact_hash}, req.workspace)
359
+ return {"url": url, "hash": artifact_hash, "filename": req.filename, "receipt": receipt}
360
+
361
+
362
+ # ═══════════════════════════════════════════════════════════════
363
+ # MEMORY
364
+ # ════════════════════════════════════════════════════════════��══
365
+ @app.post("/learn")
366
+ async def learn_memory(req: LearnRequest):
367
+ mid = uuid.uuid4().hex[:16]
368
+ now = datetime.now(timezone.utc).isoformat()
369
+ with DB_LOCK:
370
+ conn = _db()
371
+ conn.execute("INSERT INTO memory VALUES (?,?,?,?,?,?)", (mid, req.topic, req.content, req.utility, json.dumps(req.tags), now))
372
+ conn.commit()
373
+ conn.close()
374
+ return {"status": "learned", "id": mid, "topic": req.topic}
375
+
376
+
377
+ @app.post("/recall")
378
+ async def recall_memory(req: RecallRequest):
379
+ with DB_LOCK:
380
+ conn = _db()
381
+ rows = conn.execute("SELECT * FROM memory WHERE content LIKE ? OR topic LIKE ? ORDER BY utility DESC LIMIT ?", (f"%{req.query}%", f"%{req.query}%", req.limit)).fetchall()
382
+ conn.close()
383
+ return {"results": [{"id": r["id"], "topic": r["topic"], "content": r["content"], "utility": r["utility"], "tags": json.loads(r["tags"]), "created_at": r["created_at"]} for r in rows]}
384
+
385
+
386
+ # ═══════════════════════════════════════════════════════════════
387
+ # DYNAMIC TOOLS
388
+ # ═══════════════════════════════════════════════════════════════
389
+ @app.post("/tool/register")
390
+ async def register_tool(req: ToolRegisterRequest):
391
+ now = datetime.now(timezone.utc).isoformat()
392
+ with DB_LOCK:
393
+ conn = _db()
394
+ conn.execute("INSERT OR REPLACE INTO tools VALUES (?,?,?,?,?,?,?)", (req.name, req.description, req.mode, req.command_template, json.dumps(req.schema_data), int(req.enabled), now))
395
+ conn.commit()
396
+ conn.close()
397
+ return {"status": "registered", "name": req.name, "mode": req.mode}
398
+
399
+
400
+ @app.get("/tools")
401
+ async def list_tools():
402
+ with DB_LOCK:
403
+ conn = _db()
404
+ rows = conn.execute("SELECT * FROM tools WHERE enabled=1").fetchall()
405
+ conn.close()
406
+ return {"tools": [{"name": r["name"], "description": r["description"], "mode": r["mode"], "command_template": r["command_template"], "schema": json.loads(r["schema_data"])} for r in rows]}
407
+
408
+
409
+ @app.post("/tool/{name}")
410
+ async def invoke_tool(name: str, req: ToolInvokeRequest):
411
+ with DB_LOCK:
412
+ conn = _db()
413
+ row = conn.execute("SELECT * FROM tools WHERE name=? AND enabled=1", (name,)).fetchone()
414
+ conn.close()
415
  if not row:
416
+ raise HTTPException(status_code=404, detail=f"Tool '{name}' not found")
417
+ template = row["command_template"]
418
+ cmd = template
419
+ for k, v in req.args.items():
420
+ cmd = cmd.replace(f"{{{{{k}}}}}", str(v))
421
+ if row["mode"] == "python":
422
+ ws_base = _ws_path(req.workspace)
423
+ script = ws_base / f"_tool_{uuid.uuid4().hex[:8]}.py"
424
+ script.write_text(cmd)
425
+ result = _safe_run(["python3", str(script)], ws_base, req.timeout_seconds)
426
+ script.unlink(missing_ok=True)
427
+ else:
428
+ cwd = _ws_path(req.workspace)
429
+ result = _safe_run(["sh", "-c", cmd], cwd, req.timeout_seconds)
430
+ receipt = _receipt("tool_invoke", name, result, req.workspace)
431
+ return {"tool": name, **result, "receipt": receipt}
432
+
433
+
434
+ # ═══════════════════════════════════════════════════════════════
435
+ # RECEIPTS / LEDGER
436
+ # ═══════════════════════════════════════════════════════════════
437
+ @app.get("/ledger/recent")
438
+ async def get_recent_ledger(limit: int = 50):
439
+ with DB_LOCK:
440
+ conn = _db()
441
+ rows = conn.execute("SELECT id, kind, workspace, summary, sha256, created_at FROM receipts ORDER BY created_at DESC LIMIT ?", (limit,)).fetchall()
442
  conn.close()
443
+ return {"receipts": [{"id": r["id"], "kind": r["kind"], "workspace": r["workspace"], "summary": r["summary"], "sha256": r["sha256"], "created_at": r["created_at"]} for r in rows]}
444
 
 
 
 
 
 
445
 
446
+ @app.get("/receipt/{receipt_id}")
447
+ async def get_receipt(receipt_id: str):
448
+ with DB_LOCK:
449
+ conn = _db()
450
+ row = conn.execute("SELECT * FROM receipts WHERE id=?", (receipt_id,)).fetchone()
451
+ conn.close()
452
+ if not row:
453
+ raise HTTPException(status_code=404, detail="Receipt not found")
454
+ return {"id": row["id"], "kind": row["kind"], "workspace": row["workspace"], "summary": row["summary"], "sha256": row["sha256"], "created_at": row["created_at"], "data": json.loads(row["data_json"])}
 
 
 
 
 
 
455
 
456
 
457
+ # ═══════════════════════════════════════════════════════════════
458
+ # ROOT
459
+ # ═══════════════════════════════════════════════════════════════
460
  @app.get("/")
461
  async def root():
462
  return {
463
+ "service": "Giant GPT Terminal Kernel",
464
+ "version": "3.1.1",
 
 
 
465
  "endpoints": [
466
+ "GET /health",
467
+ "POST /terminal/run",
468
+ "POST /python/run",
469
+ "POST /session/create",
470
+ "POST /session/run",
471
+ "POST /files/write",
472
+ "POST /files/read",
473
+ "POST /files/list",
474
+ "GET /workspace/tree",
475
+ "POST /artifact/compile",
476
+ "POST /url/issue",
477
+ "POST /learn",
478
+ "POST /recall",
479
+ "POST /tool/register",
480
+ "GET /tools",
481
+ "POST /tool/{name}",
482
+ "GET /ledger/recent",
483
+ "GET /receipt/{receipt_id}",
484
  ],
485
  }