Spaces:
Sleeping
Sleeping
Update main.py
Browse files
main.py
CHANGED
|
@@ -57,8 +57,59 @@ ALLOWED_BINS = {
|
|
| 57 |
"ls": ["ls"],
|
| 58 |
"find": ["find"],
|
| 59 |
}
|
| 60 |
-
EXEC_TIMEOUT
|
| 61 |
-
MAX_OUTPUT
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
|
| 63 |
# ββ Meta / stats ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 64 |
def load_meta():
|
|
@@ -387,7 +438,24 @@ async def exec_code(request: Request):
|
|
| 387 |
env = data.get("env",{})
|
| 388 |
timeout = int(data.get("timeout", EXEC_TIMEOUT))
|
| 389 |
if not code.strip(): raise HTTPException(400, "code required")
|
| 390 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 391 |
return jresp(result)
|
| 392 |
|
| 393 |
@app.get("/api/exec/stream")
|
|
@@ -442,6 +510,175 @@ async def stats():
|
|
| 442 |
"total_writes":META["total_writes"],"total_execs":META["total_execs"],
|
| 443 |
"by_ext":dict(sorted(by_ext.items(),key=lambda x:-x[1])[:10])})
|
| 444 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 445 |
# ββ MCP βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 446 |
MCP_TOOLS = [
|
| 447 |
{"name":"vault_read","description":"Read a file from the workspace",
|
|
@@ -476,6 +713,14 @@ MCP_TOOLS = [
|
|
| 476 |
"src":{"type":"string"},"dst":{"type":"string"}}}},
|
| 477 |
{"name":"vault_stats","description":"Get workspace statistics",
|
| 478 |
"inputSchema":{"type":"object","properties":{}}},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 479 |
]
|
| 480 |
|
| 481 |
async def mcp_call(name, args):
|
|
@@ -529,6 +774,36 @@ async def mcp_call(name, args):
|
|
| 529 |
if p.is_file(): total_files+=1; total_size+=p.stat().st_size
|
| 530 |
return json.dumps({"total_files":total_files,"total_size":total_size,
|
| 531 |
"total_writes":META["total_writes"],"total_execs":META["total_execs"]})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 532 |
return json.dumps({"error": f"unknown tool: {name}"})
|
| 533 |
|
| 534 |
@app.get("/mcp/sse")
|
|
@@ -871,6 +1146,7 @@ body::after{content:'';position:fixed;inset:0;pointer-events:none;
|
|
| 871 |
<div class="tab-item" id="tab-editor" >✎ Editor</div>
|
| 872 |
<div class="tab-item" id="tab-terminal">> Terminal</div>
|
| 873 |
<div class="tab-item" id="tab-exec-log">⚙ Exec Log</div>
|
|
|
|
| 874 |
</div>
|
| 875 |
|
| 876 |
<!-- BROWSER -->
|
|
@@ -924,6 +1200,74 @@ body::after{content:'';position:fixed;inset:0;pointer-events:none;
|
|
| 924 |
<!-- EXEC LOG -->
|
| 925 |
<div id="exec-log-view" style="display:none;flex:1;overflow-y:auto;padding:.6rem .9rem;"></div>
|
| 926 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 927 |
<!-- VERSIONS PANEL -->
|
| 928 |
<div id="versions-panel">
|
| 929 |
<div id="vp-hdr">
|
|
@@ -1308,17 +1652,118 @@ function showTab(t){
|
|
| 1308 |
document.getElementById('tab-editor').className='tab-item'+(t==='editor'?' on':'');
|
| 1309 |
document.getElementById('tab-terminal').className='tab-item'+(t==='terminal'?' on':'');
|
| 1310 |
document.getElementById('tab-exec-log').className='tab-item'+(t==='exec-log'?' on':'');
|
|
|
|
| 1311 |
document.getElementById('browser-view').style.display=t==='browser'?'block':'none';
|
| 1312 |
document.getElementById('editor-wrap').style.display=t==='editor'?'flex':'none';
|
| 1313 |
document.getElementById('terminal-wrap').style.display=t==='terminal'?'flex':'none';
|
| 1314 |
document.getElementById('exec-log-view').style.display=t==='exec-log'?'block':'none';
|
|
|
|
| 1315 |
if(t==='exec-log') loadExecLog();
|
|
|
|
| 1316 |
}
|
| 1317 |
document.getElementById('tab-browser').addEventListener('click',function(){showTab('browser');});
|
| 1318 |
document.getElementById('tab-editor').addEventListener('click',function(){showTab('editor');});
|
| 1319 |
document.getElementById('tab-terminal').addEventListener('click',function(){showTab('terminal');});
|
| 1320 |
document.getElementById('tab-exec-log').addEventListener('click',function(){showTab('exec-log');});
|
| 1321 |
document.getElementById('btn-open-term').addEventListener('click',function(){showTab('terminal');});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1322 |
|
| 1323 |
// ββ View toggle βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1324 |
document.getElementById('vt-list').addEventListener('click',function(){
|
|
@@ -1427,4 +1872,4 @@ loadTree();
|
|
| 1427 |
loadBrowser('');
|
| 1428 |
</script>
|
| 1429 |
</body>
|
| 1430 |
-
</html>"""
|
|
|
|
| 57 |
"ls": ["ls"],
|
| 58 |
"find": ["find"],
|
| 59 |
}
|
| 60 |
+
EXEC_TIMEOUT = int(os.environ.get("VAULT_EXEC_TIMEOUT", "30"))
|
| 61 |
+
MAX_OUTPUT = 64 * 1024 # 64KB output cap
|
| 62 |
+
|
| 63 |
+
# ββ Sprint 6 β security + AI integrations βββββββββββββββββββββββββ
|
| 64 |
+
APPROVE_URL = os.environ.get("APPROVE_URL", "")
|
| 65 |
+
HARNESS_URL = os.environ.get("HARNESS_URL", "")
|
| 66 |
+
ANTHROPIC_KEY = os.environ.get("ANTHROPIC_API_KEY", "")
|
| 67 |
+
GH_TOKEN = os.environ.get("GH_TOKEN", "")
|
| 68 |
+
VAULT_KEY = os.environ.get("VAULT_KEY", "")
|
| 69 |
+
|
| 70 |
+
# Patterns that require human approval before exec
|
| 71 |
+
RISKY_EXEC_PATTERNS = [
|
| 72 |
+
"rm -rf", "rm -f /", "dd if=", "mkfs", "> /dev/",
|
| 73 |
+
":(){ :|:& };:", "chmod -R 777", "wget.*| bash", "curl.*| sh",
|
| 74 |
+
]
|
| 75 |
+
RISKY_GIT_PATTERNS = ["push --force", "push -f", "reset --hard", "clean -fd"]
|
| 76 |
+
|
| 77 |
+
import httpx as _httpx # sync httpx for gate helpers (called from sync context)
|
| 78 |
+
|
| 79 |
+
def _approve_sync(tool: str, args: dict, risk: str = "high") -> tuple[bool, str]:
|
| 80 |
+
"""Synchronous approval gate β polls up to 90s."""
|
| 81 |
+
if not APPROVE_URL:
|
| 82 |
+
return True, "approve_url_missing"
|
| 83 |
+
try:
|
| 84 |
+
r = _httpx.post(f"{APPROVE_URL}/api/approval/request",
|
| 85 |
+
json={"agent": "vault", "tool": tool, "args": args,
|
| 86 |
+
"risk": risk, "auto_timeout": 120}, timeout=6)
|
| 87 |
+
if r.status_code == 200:
|
| 88 |
+
aid = r.json().get("id")
|
| 89 |
+
for _ in range(18):
|
| 90 |
+
import time as _t; _t.sleep(5)
|
| 91 |
+
pr = _httpx.get(f"{APPROVE_URL}/api/approval/{aid}", timeout=4)
|
| 92 |
+
if pr.status_code == 200:
|
| 93 |
+
st = pr.json().get("status")
|
| 94 |
+
if st == "approved": return True, "human_approved"
|
| 95 |
+
if st in ("rejected","expired"): return False, st
|
| 96 |
+
return False, "timeout"
|
| 97 |
+
except Exception as e:
|
| 98 |
+
return False, f"approve_error: {e}"
|
| 99 |
+
|
| 100 |
+
def _harness_scan(tool: str, content: str) -> str:
|
| 101 |
+
"""Scan exec output through harness before returning. Pass-through on failure."""
|
| 102 |
+
if not HARNESS_URL:
|
| 103 |
+
return content
|
| 104 |
+
try:
|
| 105 |
+
r = _httpx.post(f"{HARNESS_URL}/api/scan/output",
|
| 106 |
+
json={"agent": "vault", "tool": tool, "content": content},
|
| 107 |
+
timeout=4)
|
| 108 |
+
if r.status_code == 200:
|
| 109 |
+
return r.json().get("sanitised", content)
|
| 110 |
+
except Exception:
|
| 111 |
+
pass
|
| 112 |
+
return content
|
| 113 |
|
| 114 |
# ββ Meta / stats ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 115 |
def load_meta():
|
|
|
|
| 438 |
env = data.get("env",{})
|
| 439 |
timeout = int(data.get("timeout", EXEC_TIMEOUT))
|
| 440 |
if not code.strip(): raise HTTPException(400, "code required")
|
| 441 |
+
|
| 442 |
+
# Approval gate: risky bash/shell patterns
|
| 443 |
+
if runtime in ("bash","sh"):
|
| 444 |
+
if any(p in code for p in RISKY_EXEC_PATTERNS):
|
| 445 |
+
ok, reason = _approve_sync("vault_exec",
|
| 446 |
+
{"runtime": runtime, "code": code[:300], "cwd": cwd},
|
| 447 |
+
risk="critical")
|
| 448 |
+
if not ok:
|
| 449 |
+
return jresp({"ok": False, "exit_code": -1,
|
| 450 |
+
"output": f"[VAULT] BLOCKED by approval gate: {reason}",
|
| 451 |
+
"ms": 0})
|
| 452 |
+
|
| 453 |
+
result = await exec_command(runtime, code, cwd, env, min(timeout, 60))
|
| 454 |
+
|
| 455 |
+
# Harness: scan output before returning
|
| 456 |
+
if result.get("output"):
|
| 457 |
+
result["output"] = _harness_scan("vault_exec", result["output"])
|
| 458 |
+
|
| 459 |
return jresp(result)
|
| 460 |
|
| 461 |
@app.get("/api/exec/stream")
|
|
|
|
| 510 |
"total_writes":META["total_writes"],"total_execs":META["total_execs"],
|
| 511 |
"by_ext":dict(sorted(by_ext.items(),key=lambda x:-x[1])[:10])})
|
| 512 |
|
| 513 |
+
# ββ Git API (Sprint 6) ββββββββββββββββββββββββββββββββββββββββββββ
|
| 514 |
+
|
| 515 |
+
@app.get("/api/git/status")
|
| 516 |
+
async def git_status(cwd: str = "code"):
|
| 517 |
+
result = await exec_command("git", "status --short", cwd)
|
| 518 |
+
staged = await exec_command("git", "diff --cached --name-only", cwd)
|
| 519 |
+
log = await exec_command("git", "log --oneline -5", cwd)
|
| 520 |
+
return jresp({
|
| 521 |
+
"status": result.get("output","").strip(),
|
| 522 |
+
"staged": staged.get("output","").strip(),
|
| 523 |
+
"log": log.get("output","").strip(),
|
| 524 |
+
"ok": result.get("ok", False),
|
| 525 |
+
})
|
| 526 |
+
|
| 527 |
+
@app.get("/api/git/log")
|
| 528 |
+
async def git_log(cwd: str = "code", limit: int = 20):
|
| 529 |
+
result = await exec_command("git",
|
| 530 |
+
f"log --oneline --graph --decorate -n {min(limit,50)}", cwd)
|
| 531 |
+
return jresp({"log": result.get("output","").strip(), "ok": result.get("ok")})
|
| 532 |
+
|
| 533 |
+
@app.post("/api/git/init")
|
| 534 |
+
async def git_init(request: Request):
|
| 535 |
+
data = await request.json()
|
| 536 |
+
cwd = data.get("cwd","code")
|
| 537 |
+
result = await exec_command("git", "init", cwd)
|
| 538 |
+
if result.get("ok"):
|
| 539 |
+
await exec_command("git", 'config user.email "vault@ki-fusion-labs.de"', cwd)
|
| 540 |
+
await exec_command("git", 'config user.name "FORGE Vault"', cwd)
|
| 541 |
+
return jresp(result)
|
| 542 |
+
|
| 543 |
+
@app.post("/api/git/commit")
|
| 544 |
+
async def git_commit(request: Request):
|
| 545 |
+
data = await request.json()
|
| 546 |
+
message = data.get("message","auto: vault commit").strip() or "auto: vault commit"
|
| 547 |
+
cwd = data.get("cwd","code")
|
| 548 |
+
files = data.get("files", []) # empty = git add -A
|
| 549 |
+
|
| 550 |
+
# Stage files
|
| 551 |
+
if files:
|
| 552 |
+
for f in files:
|
| 553 |
+
await exec_command("git", f"add {f}", cwd)
|
| 554 |
+
else:
|
| 555 |
+
await exec_command("git", "add -A", cwd)
|
| 556 |
+
|
| 557 |
+
# Commit
|
| 558 |
+
result = await exec_command("git", f'commit -m "{message}"', cwd)
|
| 559 |
+
return jresp({
|
| 560 |
+
"ok": result.get("ok"),
|
| 561 |
+
"output": result.get("output","").strip(),
|
| 562 |
+
"ms": result.get("ms",0),
|
| 563 |
+
})
|
| 564 |
+
|
| 565 |
+
@app.post("/api/git/push")
|
| 566 |
+
async def git_push(request: Request):
|
| 567 |
+
data = await request.json()
|
| 568 |
+
remote = data.get("remote","origin")
|
| 569 |
+
branch = data.get("branch","main")
|
| 570 |
+
force = bool(data.get("force", False))
|
| 571 |
+
cwd = data.get("cwd","code")
|
| 572 |
+
|
| 573 |
+
# Always require approval for git push
|
| 574 |
+
ok, reason = _approve_sync(
|
| 575 |
+
"vault_git_push",
|
| 576 |
+
{"remote": remote, "branch": branch, "force": force, "cwd": cwd},
|
| 577 |
+
risk="high" if not force else "critical",
|
| 578 |
+
)
|
| 579 |
+
if not ok:
|
| 580 |
+
return jresp({"ok": False, "output": f"[VAULT] git push BLOCKED: {reason}", "ms": 0})
|
| 581 |
+
|
| 582 |
+
force_flag = "--force" if force else ""
|
| 583 |
+
env_extra = {"GH_TOKEN": GH_TOKEN} if GH_TOKEN else {}
|
| 584 |
+
cmd = f"push {remote} {branch} {force_flag}".strip()
|
| 585 |
+
result = await exec_command("git", cmd, cwd, env_extra)
|
| 586 |
+
return jresp({
|
| 587 |
+
"ok": result.get("ok"),
|
| 588 |
+
"output": result.get("output","").strip(),
|
| 589 |
+
"ms": result.get("ms",0),
|
| 590 |
+
})
|
| 591 |
+
|
| 592 |
+
@app.post("/api/git/clone")
|
| 593 |
+
async def git_clone(request: Request):
|
| 594 |
+
data = await request.json()
|
| 595 |
+
url = data.get("url","")
|
| 596 |
+
dest = data.get("dest","code")
|
| 597 |
+
if not url: raise HTTPException(400,"url required")
|
| 598 |
+
env_extra = {"GH_TOKEN": GH_TOKEN} if GH_TOKEN else {}
|
| 599 |
+
result = await exec_command("bash", f"git clone {url} {WS_ROOT}/{dest}/repo",
|
| 600 |
+
"", env_extra, timeout=60)
|
| 601 |
+
return jresp(result)
|
| 602 |
+
|
| 603 |
+
|
| 604 |
+
# ββ Anthropic API proxy (OpenClaw skill, Sprint 6) ββββββββββββββββ
|
| 605 |
+
# Streams Claude responses. Agents call this directly for AI-assisted
|
| 606 |
+
# code review, documentation, analysis within the vault context.
|
| 607 |
+
|
| 608 |
+
@app.post("/api/anthropic")
|
| 609 |
+
async def anthropic_proxy(request: Request):
|
| 610 |
+
"""
|
| 611 |
+
Anthropic API proxy with vault context injection.
|
| 612 |
+
Body: { model, max_tokens, messages, system, vault_context_path }
|
| 613 |
+
If vault_context_path set, injects file content into system prompt.
|
| 614 |
+
Streams raw SSE from Anthropic back to caller.
|
| 615 |
+
"""
|
| 616 |
+
if not ANTHROPIC_KEY:
|
| 617 |
+
raise HTTPException(503, "ANTHROPIC_API_KEY not configured")
|
| 618 |
+
|
| 619 |
+
data = await request.json()
|
| 620 |
+
model = data.get("model","claude-sonnet-4-20250514")
|
| 621 |
+
max_tokens = int(data.get("max_tokens", 4096))
|
| 622 |
+
messages = data.get("messages", [])
|
| 623 |
+
system = data.get("system","You are a helpful AI assistant with access to the FORGE vault workspace.")
|
| 624 |
+
stream = bool(data.get("stream", True))
|
| 625 |
+
ctx_path = data.get("vault_context_path","")
|
| 626 |
+
|
| 627 |
+
# Inject vault file context if requested
|
| 628 |
+
if ctx_path:
|
| 629 |
+
try:
|
| 630 |
+
p = safe_path(ctx_path)
|
| 631 |
+
ctx_content = p.read_text(errors="replace")[:8000]
|
| 632 |
+
system = system + f"\n\nVAULT FILE CONTEXT ({ctx_path}):\n```\n{ctx_content}\n```"
|
| 633 |
+
except Exception as e:
|
| 634 |
+
system = system + f"\n\n[vault_context_path error: {e}]"
|
| 635 |
+
|
| 636 |
+
payload = {
|
| 637 |
+
"model": model,
|
| 638 |
+
"max_tokens": max_tokens,
|
| 639 |
+
"system": system,
|
| 640 |
+
"messages": messages,
|
| 641 |
+
"stream": stream,
|
| 642 |
+
}
|
| 643 |
+
|
| 644 |
+
async def _stream_anthropic():
|
| 645 |
+
async with _httpx.AsyncClient(timeout=120) as c:
|
| 646 |
+
async with c.stream(
|
| 647 |
+
"POST",
|
| 648 |
+
"https://api.anthropic.com/v1/messages",
|
| 649 |
+
json=payload,
|
| 650 |
+
headers={
|
| 651 |
+
"x-api-key": ANTHROPIC_KEY,
|
| 652 |
+
"anthropic-version": "2023-06-01",
|
| 653 |
+
"content-type": "application/json",
|
| 654 |
+
}
|
| 655 |
+
) as resp:
|
| 656 |
+
async for chunk in resp.aiter_text():
|
| 657 |
+
if chunk:
|
| 658 |
+
yield chunk
|
| 659 |
+
|
| 660 |
+
if stream:
|
| 661 |
+
return StreamingResponse(
|
| 662 |
+
_stream_anthropic(),
|
| 663 |
+
media_type="text/event-stream",
|
| 664 |
+
headers={"Cache-Control":"no-cache","X-Accel-Buffering":"no"},
|
| 665 |
+
)
|
| 666 |
+
|
| 667 |
+
# Non-stream: collect and return
|
| 668 |
+
async with _httpx.AsyncClient(timeout=120) as c:
|
| 669 |
+
resp = await c.post(
|
| 670 |
+
"https://api.anthropic.com/v1/messages",
|
| 671 |
+
json={**payload, "stream": False},
|
| 672 |
+
headers={
|
| 673 |
+
"x-api-key": ANTHROPIC_KEY,
|
| 674 |
+
"anthropic-version": "2023-06-01",
|
| 675 |
+
"content-type": "application/json",
|
| 676 |
+
}
|
| 677 |
+
)
|
| 678 |
+
return JSONResponse(resp.json())
|
| 679 |
+
|
| 680 |
+
|
| 681 |
+
|
| 682 |
# ββ MCP βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 683 |
MCP_TOOLS = [
|
| 684 |
{"name":"vault_read","description":"Read a file from the workspace",
|
|
|
|
| 713 |
"src":{"type":"string"},"dst":{"type":"string"}}}},
|
| 714 |
{"name":"vault_stats","description":"Get workspace statistics",
|
| 715 |
"inputSchema":{"type":"object","properties":{}}},
|
| 716 |
+
{"name":"vault_git_status","description":"Git status, staged files and recent log.",
|
| 717 |
+
"inputSchema":{"type":"object","properties":{"cwd":{"type":"string"}}}},
|
| 718 |
+
{"name":"vault_git_commit","description":"Stage all and commit.",
|
| 719 |
+
"inputSchema":{"type":"object","properties":{"message":{"type":"string"},"cwd":{"type":"string"},"files":{"type":"array","items":{"type":"string"}}}}},
|
| 720 |
+
{"name":"vault_git_push","description":"Push to remote (requires human approval).",
|
| 721 |
+
"inputSchema":{"type":"object","properties":{"remote":{"type":"string"},"branch":{"type":"string"},"force":{"type":"boolean"},"cwd":{"type":"string"}}}},
|
| 722 |
+
{"name":"vault_anthropic","description":"Call Claude API with optional vault file context injection.",
|
| 723 |
+
"inputSchema":{"type":"object","properties":{"messages":{"type":"array"},"system":{"type":"string"},"vault_context_path":{"type":"string"},"max_tokens":{"type":"integer"}}}},
|
| 724 |
]
|
| 725 |
|
| 726 |
async def mcp_call(name, args):
|
|
|
|
| 774 |
if p.is_file(): total_files+=1; total_size+=p.stat().st_size
|
| 775 |
return json.dumps({"total_files":total_files,"total_size":total_size,
|
| 776 |
"total_writes":META["total_writes"],"total_execs":META["total_execs"]})
|
| 777 |
+
if name == "vault_git_status":
|
| 778 |
+
import asyncio as _aio
|
| 779 |
+
r = _aio.get_event_loop().run_until_complete(exec_command("git","status --short",a.get("cwd","code")))
|
| 780 |
+
lg = _aio.get_event_loop().run_until_complete(exec_command("git","log --oneline -5",a.get("cwd","code")))
|
| 781 |
+
return json.dumps({"status":r.get("output","").strip(),"log":lg.get("output","").strip()})
|
| 782 |
+
if name == "vault_git_commit":
|
| 783 |
+
import urllib.request as _ur
|
| 784 |
+
body = json.dumps({"message":a.get("message","auto commit"),"cwd":a.get("cwd","code"),"files":a.get("files",[])}).encode()
|
| 785 |
+
req = _ur.Request("http://localhost:7860/api/git/commit",data=body,headers={"Content-Type":"application/json"},method="POST")
|
| 786 |
+
try:
|
| 787 |
+
with _ur.urlopen(req,timeout=30) as resp: d=json.loads(resp.read())
|
| 788 |
+
except Exception as e: d={"ok":False,"output":str(e)}
|
| 789 |
+
return json.dumps(d)
|
| 790 |
+
if name == "vault_git_push":
|
| 791 |
+
import urllib.request as _ur
|
| 792 |
+
body = json.dumps({"remote":a.get("remote","origin"),"branch":a.get("branch","main"),"force":bool(a.get("force",False)),"cwd":a.get("cwd","code")}).encode()
|
| 793 |
+
req = _ur.Request("http://localhost:7860/api/git/push",data=body,headers={"Content-Type":"application/json"},method="POST")
|
| 794 |
+
try:
|
| 795 |
+
with _ur.urlopen(req,timeout=120) as resp: d=json.loads(resp.read())
|
| 796 |
+
except Exception as e: d={"ok":False,"output":str(e)}
|
| 797 |
+
return json.dumps(d)
|
| 798 |
+
if name == "vault_anthropic":
|
| 799 |
+
import urllib.request as _ur
|
| 800 |
+
body = json.dumps({**a,"stream":False}).encode()
|
| 801 |
+
req = _ur.Request("http://localhost:7860/api/anthropic",data=body,headers={"Content-Type":"application/json"},method="POST")
|
| 802 |
+
try:
|
| 803 |
+
with _ur.urlopen(req,timeout=120) as resp: d=json.loads(resp.read())
|
| 804 |
+
text = d.get("content",[{}])[0].get("text","") if isinstance(d.get("content"),list) else str(d)
|
| 805 |
+
except Exception as e: text=str(e)
|
| 806 |
+
return json.dumps({"text":text[:2000]})
|
| 807 |
return json.dumps({"error": f"unknown tool: {name}"})
|
| 808 |
|
| 809 |
@app.get("/mcp/sse")
|
|
|
|
| 1146 |
<div class="tab-item" id="tab-editor" >✎ Editor</div>
|
| 1147 |
<div class="tab-item" id="tab-terminal">> Terminal</div>
|
| 1148 |
<div class="tab-item" id="tab-exec-log">⚙ Exec Log</div>
|
| 1149 |
+
<div class="tab-item" id="tab-git">🔗 Git</div>
|
| 1150 |
</div>
|
| 1151 |
|
| 1152 |
<!-- BROWSER -->
|
|
|
|
| 1200 |
<!-- EXEC LOG -->
|
| 1201 |
<div id="exec-log-view" style="display:none;flex:1;overflow-y:auto;padding:.6rem .9rem;"></div>
|
| 1202 |
|
| 1203 |
+
<!-- GIT PANEL -->
|
| 1204 |
+
<div id="git-view" style="display:none;flex:1;overflow-y:auto;padding:.9rem;font-family:'DM Mono',monospace">
|
| 1205 |
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;margin-bottom:1rem">
|
| 1206 |
+
<div>
|
| 1207 |
+
<div style="font-size:.68rem;color:var(--muted);text-transform:uppercase;letter-spacing:.12em;margin-bottom:.4rem">Working Directory</div>
|
| 1208 |
+
<select id="git-cwd" style="background:var(--surface2);border:1px solid var(--border);color:var(--text);padding:.35rem .6rem;border-radius:4px;font-family:'DM Mono',monospace;font-size:.78rem;width:100%">
|
| 1209 |
+
<option value="code">code/</option>
|
| 1210 |
+
<option value="reports">reports/</option>
|
| 1211 |
+
<option value="scratch">scratch/</option>
|
| 1212 |
+
<option value="shared">shared/</option>
|
| 1213 |
+
</select>
|
| 1214 |
+
</div>
|
| 1215 |
+
<div style="display:flex;align-items:flex-end;gap:.5rem">
|
| 1216 |
+
<button id="git-refresh-btn" class="btn btn-secondary" style="font-size:.75rem;padding:.35rem .8rem">↻ Status</button>
|
| 1217 |
+
<button id="git-init-btn" class="btn btn-secondary" style="font-size:.75rem;padding:.35rem .8rem">⚙ Init</button>
|
| 1218 |
+
</div>
|
| 1219 |
+
</div>
|
| 1220 |
+
|
| 1221 |
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;margin-bottom:1rem">
|
| 1222 |
+
<div>
|
| 1223 |
+
<div style="font-size:.68rem;color:var(--muted);text-transform:uppercase;letter-spacing:.12em;margin-bottom:.4rem">Status</div>
|
| 1224 |
+
<pre id="git-status-out" style="background:var(--surface2);border:1px solid var(--border);border-radius:4px;padding:.6rem;font-size:.75rem;line-height:1.5;min-height:80px;overflow-x:auto;white-space:pre-wrap;color:var(--green)">β</pre>
|
| 1225 |
+
</div>
|
| 1226 |
+
<div>
|
| 1227 |
+
<div style="font-size:.68rem;color:var(--muted);text-transform:uppercase;letter-spacing:.12em;margin-bottom:.4rem">Recent Log</div>
|
| 1228 |
+
<pre id="git-log-out" style="background:var(--surface2);border:1px solid var(--border);border-radius:4px;padding:.6rem;font-size:.75rem;line-height:1.5;min-height:80px;overflow-x:auto;white-space:pre-wrap;color:var(--cyan)">β</pre>
|
| 1229 |
+
</div>
|
| 1230 |
+
</div>
|
| 1231 |
+
|
| 1232 |
+
<div style="background:var(--surface2);border:1px solid var(--border);border-radius:6px;padding:1rem;margin-bottom:1rem">
|
| 1233 |
+
<div style="font-size:.72rem;color:var(--accent);font-weight:600;margin-bottom:.75rem">✓ Commit</div>
|
| 1234 |
+
<input id="git-commit-msg" class="search-input" placeholder="Commit message…" style="width:100%;margin-bottom:.6rem;font-family:'DM Mono',monospace;font-size:.8rem">
|
| 1235 |
+
<button id="git-commit-btn" class="btn btn-primary" style="font-size:.78rem">✍ Commit (stage all)</button>
|
| 1236 |
+
<span id="git-commit-result" style="margin-left:.75rem;font-size:.75rem;color:var(--muted)"></span>
|
| 1237 |
+
</div>
|
| 1238 |
+
|
| 1239 |
+
<div style="background:var(--surface2);border:1px solid var(--border);border-radius:6px;padding:1rem;margin-bottom:1rem">
|
| 1240 |
+
<div style="font-size:.72rem;color:var(--accent);font-weight:600;margin-bottom:.75rem">🚀 Push (requires approval)</div>
|
| 1241 |
+
<div style="display:grid;grid-template-columns:1fr 1fr auto;gap:.5rem;align-items:end">
|
| 1242 |
+
<div>
|
| 1243 |
+
<div style="font-size:.65rem;color:var(--muted);margin-bottom:.25rem">Remote</div>
|
| 1244 |
+
<input id="git-remote" class="search-input" value="origin" style="width:100%;font-size:.78rem">
|
| 1245 |
+
</div>
|
| 1246 |
+
<div>
|
| 1247 |
+
<div style="font-size:.65rem;color:var(--muted);margin-bottom:.25rem">Branch</div>
|
| 1248 |
+
<input id="git-branch" class="search-input" value="main" style="width:100%;font-size:.78rem">
|
| 1249 |
+
</div>
|
| 1250 |
+
<button id="git-push-btn" class="btn btn-primary" style="font-size:.78rem;background:var(--purple)">⇧ Push</button>
|
| 1251 |
+
</div>
|
| 1252 |
+
<label style="display:flex;align-items:center;gap:.4rem;margin-top:.6rem;font-size:.72rem;color:var(--muted);cursor:pointer">
|
| 1253 |
+
<input type="checkbox" id="git-force" style="accent-color:var(--red)"> Force push (--force)
|
| 1254 |
+
</label>
|
| 1255 |
+
<div id="git-push-result" style="margin-top:.5rem;font-size:.75rem;color:var(--muted)"></div>
|
| 1256 |
+
</div>
|
| 1257 |
+
|
| 1258 |
+
<!-- Claude assistant -->
|
| 1259 |
+
<div style="background:var(--surface2);border:1px solid var(--border);border-left:3px solid var(--accent);border-radius:6px;padding:1rem">
|
| 1260 |
+
<div style="font-size:.72rem;color:var(--accent);font-weight:600;margin-bottom:.75rem">🤖 Claude Code Review</div>
|
| 1261 |
+
<div style="font-size:.7rem;color:var(--muted);margin-bottom:.5rem">Paste code or leave blank to use the open editor file</div>
|
| 1262 |
+
<textarea id="claude-input" class="search-input" rows="4" placeholder="Code to review, or press Run to use the open file…" style="width:100%;font-family:'DM Mono',monospace;font-size:.78rem;resize:vertical;min-height:80px;margin-bottom:.5rem"></textarea>
|
| 1263 |
+
<div style="display:flex;gap:.5rem;align-items:center">
|
| 1264 |
+
<button id="claude-run-btn" class="btn btn-primary" style="font-size:.78rem">▶ Ask Claude</button>
|
| 1265 |
+
<span style="font-size:.7rem;color:var(--muted)">streams via /api/anthropic</span>
|
| 1266 |
+
</div>
|
| 1267 |
+
<pre id="claude-out" style="margin-top:.75rem;background:var(--surface);border:1px solid var(--border);border-radius:4px;padding:.75rem;font-size:.75rem;line-height:1.6;min-height:60px;overflow-x:auto;white-space:pre-wrap;color:var(--text);display:none"></pre>
|
| 1268 |
+
</div>
|
| 1269 |
+
</div>
|
| 1270 |
+
|
| 1271 |
<!-- VERSIONS PANEL -->
|
| 1272 |
<div id="versions-panel">
|
| 1273 |
<div id="vp-hdr">
|
|
|
|
| 1652 |
document.getElementById('tab-editor').className='tab-item'+(t==='editor'?' on':'');
|
| 1653 |
document.getElementById('tab-terminal').className='tab-item'+(t==='terminal'?' on':'');
|
| 1654 |
document.getElementById('tab-exec-log').className='tab-item'+(t==='exec-log'?' on':'');
|
| 1655 |
+
document.getElementById('tab-git').className='tab-item'+(t==='git'?' on':'');
|
| 1656 |
document.getElementById('browser-view').style.display=t==='browser'?'block':'none';
|
| 1657 |
document.getElementById('editor-wrap').style.display=t==='editor'?'flex':'none';
|
| 1658 |
document.getElementById('terminal-wrap').style.display=t==='terminal'?'flex':'none';
|
| 1659 |
document.getElementById('exec-log-view').style.display=t==='exec-log'?'block':'none';
|
| 1660 |
+
document.getElementById('git-view').style.display=t==='git'?'flex':'none';
|
| 1661 |
if(t==='exec-log') loadExecLog();
|
| 1662 |
+
if(t==='git') loadGitStatus();
|
| 1663 |
}
|
| 1664 |
document.getElementById('tab-browser').addEventListener('click',function(){showTab('browser');});
|
| 1665 |
document.getElementById('tab-editor').addEventListener('click',function(){showTab('editor');});
|
| 1666 |
document.getElementById('tab-terminal').addEventListener('click',function(){showTab('terminal');});
|
| 1667 |
document.getElementById('tab-exec-log').addEventListener('click',function(){showTab('exec-log');});
|
| 1668 |
document.getElementById('btn-open-term').addEventListener('click',function(){showTab('terminal');});
|
| 1669 |
+
document.getElementById('tab-git').addEventListener('click',function(){showTab('git');});
|
| 1670 |
+
|
| 1671 |
+
// ββ Git tab JS βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1672 |
+
function gitCwd(){ return document.getElementById('git-cwd').value; }
|
| 1673 |
+
|
| 1674 |
+
function loadGitStatus(){
|
| 1675 |
+
fetch('/api/git/status?cwd='+encodeURIComponent(gitCwd()))
|
| 1676 |
+
.then(function(r){return r.json();}).then(function(d){
|
| 1677 |
+
document.getElementById('git-status-out').textContent = d.status || '(clean)';
|
| 1678 |
+
document.getElementById('git-log-out').textContent = d.log || '(no commits)';
|
| 1679 |
+
}).catch(function(e){ document.getElementById('git-status-out').textContent='Error: '+e; });
|
| 1680 |
+
}
|
| 1681 |
+
|
| 1682 |
+
document.getElementById('git-refresh-btn').addEventListener('click', loadGitStatus);
|
| 1683 |
+
document.getElementById('git-init-btn').addEventListener('click', function(){
|
| 1684 |
+
fetch('/api/git/init',{method:'POST',headers:{'Content-Type':'application/json'},
|
| 1685 |
+
body:JSON.stringify({cwd:gitCwd()})})
|
| 1686 |
+
.then(function(r){return r.json();}).then(function(d){
|
| 1687 |
+
toast(d.ok?'Git repo initialized':'Init failed: '+(d.output||'?'), d.ok?'ok':'err');
|
| 1688 |
+
loadGitStatus();
|
| 1689 |
+
});
|
| 1690 |
+
});
|
| 1691 |
+
|
| 1692 |
+
document.getElementById('git-commit-btn').addEventListener('click', function(){
|
| 1693 |
+
var msg = document.getElementById('git-commit-msg').value.trim() || 'auto: vault commit';
|
| 1694 |
+
var btn = document.getElementById('git-commit-btn');
|
| 1695 |
+
btn.disabled=true; btn.textContent='Committing…';
|
| 1696 |
+
fetch('/api/git/commit',{method:'POST',headers:{'Content-Type':'application/json'},
|
| 1697 |
+
body:JSON.stringify({message:msg,cwd:gitCwd(),files:[]})})
|
| 1698 |
+
.then(function(r){return r.json();}).then(function(d){
|
| 1699 |
+
var el = document.getElementById('git-commit-result');
|
| 1700 |
+
el.style.color = d.ok ? 'var(--green)' : 'var(--red)';
|
| 1701 |
+
el.textContent = d.ok ? '✓ Committed' : '✗ '+(d.output||'failed').slice(0,80);
|
| 1702 |
+
if(d.ok){ document.getElementById('git-commit-msg').value=''; loadGitStatus(); }
|
| 1703 |
+
btn.disabled=false; btn.textContent='✍ Commit (stage all)';
|
| 1704 |
+
});
|
| 1705 |
+
});
|
| 1706 |
+
|
| 1707 |
+
document.getElementById('git-push-btn').addEventListener('click', function(){
|
| 1708 |
+
var btn = document.getElementById('git-push-btn');
|
| 1709 |
+
btn.disabled=true; btn.textContent='Waiting approval…';
|
| 1710 |
+
document.getElementById('git-push-result').textContent = 'Awaiting Telegram approval from Christof...';
|
| 1711 |
+
fetch('/api/git/push',{method:'POST',headers:{'Content-Type':'application/json'},
|
| 1712 |
+
body:JSON.stringify({
|
| 1713 |
+
remote: document.getElementById('git-remote').value||'origin',
|
| 1714 |
+
branch: document.getElementById('git-branch').value||'main',
|
| 1715 |
+
force: document.getElementById('git-force').checked,
|
| 1716 |
+
cwd: gitCwd()
|
| 1717 |
+
})})
|
| 1718 |
+
.then(function(r){return r.json();}).then(function(d){
|
| 1719 |
+
var el = document.getElementById('git-push-result');
|
| 1720 |
+
el.style.color = d.ok ? 'var(--green)' : 'var(--red)';
|
| 1721 |
+
el.textContent = d.ok ? '✓ Pushed: '+(d.output||'').slice(0,100) : '✗ '+(d.output||'blocked').slice(0,120);
|
| 1722 |
+
btn.disabled=false; btn.textContent='⇧ Push';
|
| 1723 |
+
if(d.ok) loadGitStatus();
|
| 1724 |
+
});
|
| 1725 |
+
});
|
| 1726 |
+
|
| 1727 |
+
// Claude code review
|
| 1728 |
+
document.getElementById('claude-run-btn').addEventListener('click', function(){
|
| 1729 |
+
var code = document.getElementById('claude-input').value.trim();
|
| 1730 |
+
var out = document.getElementById('claude-out');
|
| 1731 |
+
out.style.display='block'; out.textContent='Calling Claude…';
|
| 1732 |
+
var btn = document.getElementById('claude-run-btn');
|
| 1733 |
+
btn.disabled=true;
|
| 1734 |
+
// If no code pasted, include open file path for context
|
| 1735 |
+
var ctxPath = (!code && OPEN_FILE) ? OPEN_FILE : '';
|
| 1736 |
+
var userMsg = code ? 'Please review this code and suggest improvements:\n\n```\n'+code+'\n```'
|
| 1737 |
+
: 'Please review the file at the vault context path and suggest improvements.';
|
| 1738 |
+
fetch('/api/anthropic',{method:'POST',headers:{'Content-Type':'application/json'},
|
| 1739 |
+
body:JSON.stringify({
|
| 1740 |
+
messages:[{role:'user',content:userMsg}],
|
| 1741 |
+
system:'You are a senior software engineer doing code review. Be concise and specific.',
|
| 1742 |
+
vault_context_path: ctxPath,
|
| 1743 |
+
max_tokens:1024,
|
| 1744 |
+
stream:true
|
| 1745 |
+
})})
|
| 1746 |
+
.then(function(resp){
|
| 1747 |
+
var reader=resp.body.getReader(); var decoder=new TextDecoder(); out.textContent='';
|
| 1748 |
+
function read(){
|
| 1749 |
+
reader.read().then(function(a){
|
| 1750 |
+
if(a.done){btn.disabled=false;return;}
|
| 1751 |
+
var chunk=decoder.decode(a.value);
|
| 1752 |
+
// Parse SSE data lines for text delta
|
| 1753 |
+
chunk.split('\n').forEach(function(line){
|
| 1754 |
+
if(!line.startsWith('data: ')) return;
|
| 1755 |
+
try{
|
| 1756 |
+
var ev=JSON.parse(line.slice(6));
|
| 1757 |
+
if(ev.type==='content_block_delta'&&ev.delta&&ev.delta.text)
|
| 1758 |
+
out.textContent+=ev.delta.text;
|
| 1759 |
+
}catch(e){}
|
| 1760 |
+
});
|
| 1761 |
+
read();
|
| 1762 |
+
});
|
| 1763 |
+
}
|
| 1764 |
+
read();
|
| 1765 |
+
}).catch(function(e){ out.textContent='Error: '+e; btn.disabled=false; });
|
| 1766 |
+
});
|
| 1767 |
|
| 1768 |
// ββ View toggle βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1769 |
document.getElementById('vt-list').addEventListener('click',function(){
|
|
|
|
| 1872 |
loadBrowser('');
|
| 1873 |
</script>
|
| 1874 |
</body>
|
| 1875 |
+
</html>"""
|