Spaces:
Sleeping
Sleeping
Upload 4 files
Browse files- Dockerfile +22 -0
- README.md +121 -8
- main.py +1430 -0
- requirements.txt +3 -0
Dockerfile
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
WORKDIR /app
|
| 3 |
+
|
| 4 |
+
RUN useradd -m -u 1000 user
|
| 5 |
+
|
| 6 |
+
# Dev tools agents will exec
|
| 7 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 8 |
+
bash git curl nodejs npm \
|
| 9 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 10 |
+
|
| 11 |
+
COPY requirements.txt .
|
| 12 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 13 |
+
|
| 14 |
+
COPY . .
|
| 15 |
+
|
| 16 |
+
# Create workspace dirs
|
| 17 |
+
RUN mkdir -p workspace/code workspace/reports workspace/scratch workspace/shared \
|
| 18 |
+
.vault_history && chown -R user:user /app
|
| 19 |
+
|
| 20 |
+
USER user
|
| 21 |
+
EXPOSE 7860
|
| 22 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
CHANGED
|
@@ -1,13 +1,126 @@
|
|
| 1 |
---
|
| 2 |
title: Agent Vault
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
-
sdk:
|
| 7 |
-
sdk_version: 6.9.0
|
| 8 |
-
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
-
short_description:
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
title: Agent Vault
|
| 3 |
+
emoji: ποΈ
|
| 4 |
+
colorFrom: red
|
| 5 |
+
colorTo: yellow
|
| 6 |
+
sdk: docker
|
|
|
|
|
|
|
| 7 |
pinned: false
|
| 8 |
+
short_description: Agent Workspace Manager & Remote Execution Environment
|
| 9 |
---
|
| 10 |
|
| 11 |
+
# ποΈ VAULT β Agent Workspace Manager & Execution Environment
|
| 12 |
+
|
| 13 |
+
File manager, version history, and remote code execution for AI agents.
|
| 14 |
+
|
| 15 |
+
## Workspaces
|
| 16 |
+
|
| 17 |
+
```
|
| 18 |
+
workspace/
|
| 19 |
+
code/ β scripts, modules, packages
|
| 20 |
+
reports/ β generated docs, analysis outputs
|
| 21 |
+
scratch/ β temp, experiments, throwaway
|
| 22 |
+
shared/ β cross-agent shared artifacts
|
| 23 |
+
```
|
| 24 |
+
|
| 25 |
+
## Execution Runtimes
|
| 26 |
+
|
| 27 |
+
| Runtime | Command | Use |
|
| 28 |
+
|---|---|---|
|
| 29 |
+
| `bash` | `bash -c` | Shell scripts, system ops |
|
| 30 |
+
| `python3` | `python3 -c` | Data analysis, ML, scripting |
|
| 31 |
+
| `node` | `node -e` | JS/TS execution |
|
| 32 |
+
| `npm` | `npm ...` | Package management |
|
| 33 |
+
| `pip` | `pip install` | Python packages |
|
| 34 |
+
| `git` | `git ...` | Version control |
|
| 35 |
+
| `go` | `go run` | Go programs |
|
| 36 |
+
| `cargo` | `cargo ...` | Rust programs |
|
| 37 |
+
|
| 38 |
+
## MCP Config
|
| 39 |
+
|
| 40 |
+
```json
|
| 41 |
+
{
|
| 42 |
+
"mcpServers": {
|
| 43 |
+
"vault": {
|
| 44 |
+
"command": "npx",
|
| 45 |
+
"args": ["-y", "mcp-remote", "https://chris4k-agent-vault.hf.space/mcp/sse"]
|
| 46 |
+
}
|
| 47 |
+
}
|
| 48 |
+
}
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
## MCP Tools
|
| 52 |
+
|
| 53 |
+
| Tool | Description |
|
| 54 |
+
|---|---|
|
| 55 |
+
| `vault_read` | Read file content |
|
| 56 |
+
| `vault_write` | Write/create file (auto-snapshots) |
|
| 57 |
+
| `vault_list` | List directory contents |
|
| 58 |
+
| `vault_exec` | Execute code (bash/python/node/npm/pip/git) |
|
| 59 |
+
| `vault_diff` | Diff current file vs previous version |
|
| 60 |
+
| `vault_versions` | List version history |
|
| 61 |
+
| `vault_search` | Search by filename or content |
|
| 62 |
+
| `vault_delete` | Delete file or directory |
|
| 63 |
+
| `vault_mkdir` | Create directory |
|
| 64 |
+
| `vault_move` | Move/rename file |
|
| 65 |
+
| `vault_stats` | Workspace statistics |
|
| 66 |
+
|
| 67 |
+
## REST API
|
| 68 |
+
|
| 69 |
+
```
|
| 70 |
+
GET /api/ls?path=code List directory
|
| 71 |
+
GET /api/read?path=code/x.py Read file
|
| 72 |
+
POST /api/write Write file
|
| 73 |
+
POST /api/mkdir Create directory
|
| 74 |
+
DELETE /api/delete Delete
|
| 75 |
+
POST /api/move Move/rename
|
| 76 |
+
POST /api/copy Copy
|
| 77 |
+
GET /api/search?q=hello Search
|
| 78 |
+
GET /api/versions?path=... List versions
|
| 79 |
+
GET /api/version?path=...&vid= Get specific version
|
| 80 |
+
GET /api/diff?path=... Diff vs last version
|
| 81 |
+
POST /api/exec Execute code
|
| 82 |
+
GET /api/exec/stream SSE streaming execution
|
| 83 |
+
GET /api/exec/log Execution log
|
| 84 |
+
GET /api/runtimes Available runtimes
|
| 85 |
+
GET /api/stats Workspace stats
|
| 86 |
+
```
|
| 87 |
+
|
| 88 |
+
## Agent Usage
|
| 89 |
+
|
| 90 |
+
```python
|
| 91 |
+
import requests
|
| 92 |
+
BASE = "https://chris4k-agent-vault.hf.space"
|
| 93 |
+
|
| 94 |
+
# Write a file
|
| 95 |
+
requests.post(f"{BASE}/api/write", json={
|
| 96 |
+
"path": "code/analysis.py",
|
| 97 |
+
"content": "import json\nprint(json.dumps({'result': 42}))",
|
| 98 |
+
"agent": "researcher"
|
| 99 |
+
})
|
| 100 |
+
|
| 101 |
+
# Execute it
|
| 102 |
+
r = requests.post(f"{BASE}/api/exec", json={
|
| 103 |
+
"runtime": "python3",
|
| 104 |
+
"code": "import json\nprint(json.dumps({'result': 42}))",
|
| 105 |
+
"cwd": "code"
|
| 106 |
+
})
|
| 107 |
+
print(r.json()["output"])
|
| 108 |
+
|
| 109 |
+
# Read back a file
|
| 110 |
+
content = requests.get(f"{BASE}/api/read?path=reports/output.json").json()["content"]
|
| 111 |
+
|
| 112 |
+
# Version history
|
| 113 |
+
versions = requests.get(f"{BASE}/api/versions?path=code/analysis.py").json()
|
| 114 |
+
print(versions["versions"])
|
| 115 |
+
|
| 116 |
+
# Run npm install
|
| 117 |
+
r = requests.post(f"{BASE}/api/exec", json={
|
| 118 |
+
"runtime": "npm", "code": "install lodash", "cwd": "code"
|
| 119 |
+
})
|
| 120 |
+
```
|
| 121 |
+
|
| 122 |
+
## Version System
|
| 123 |
+
|
| 124 |
+
Every `vault_write` auto-snapshots the previous content. Up to 20 versions per file. Diff view shows unified diff between any two versions.
|
| 125 |
+
|
| 126 |
+
*Chris4K Β· ki-fusion-labs.de*
|
main.py
ADDED
|
@@ -0,0 +1,1430 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
VAULT β Agent Workspace Manager & Execution Environment
|
| 3 |
+
Docker SDK on HF Spaces.
|
| 4 |
+
|
| 5 |
+
Workspaces:
|
| 6 |
+
code/ β scripts, modules, packages
|
| 7 |
+
reports/ β generated docs, analysis outputs
|
| 8 |
+
scratch/ β temp, experiments, throwaway
|
| 9 |
+
shared/ β cross-agent artifacts
|
| 10 |
+
|
| 11 |
+
Features:
|
| 12 |
+
- Full file CRUD (read, write, delete, rename, move, copy)
|
| 13 |
+
- Version history (auto-snapshot on every write, diff view)
|
| 14 |
+
- Remote execution: bash, python, node, npm, pip, cargo, go
|
| 15 |
+
- Streaming execution output via SSE
|
| 16 |
+
- File search (content + filename)
|
| 17 |
+
- MCP tools: vault_read, vault_write, vault_list, vault_exec,
|
| 18 |
+
vault_diff, vault_versions, vault_search, vault_delete,
|
| 19 |
+
vault_mkdir, vault_move, vault_stats
|
| 20 |
+
|
| 21 |
+
MCP config:
|
| 22 |
+
{"command":"npx","args":["-y","mcp-remote","https://YOUR_SPACE.hf.space/mcp/sse"]}
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
import os, uuid, json, asyncio, time, re, shutil, subprocess, hashlib, difflib
|
| 26 |
+
from pathlib import Path
|
| 27 |
+
from datetime import datetime, timezone
|
| 28 |
+
from typing import Optional
|
| 29 |
+
|
| 30 |
+
from fastapi import FastAPI, HTTPException, Request
|
| 31 |
+
from fastapi.responses import JSONResponse, HTMLResponse, StreamingResponse
|
| 32 |
+
|
| 33 |
+
BASE = Path(__file__).parent
|
| 34 |
+
WS_ROOT = BASE / "workspace"
|
| 35 |
+
HIST_ROOT = BASE / ".vault_history"
|
| 36 |
+
META_FILE = BASE / "vault_meta.json"
|
| 37 |
+
|
| 38 |
+
# ββ Init workspace ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 39 |
+
for d in ["code","reports","scratch","shared"]:
|
| 40 |
+
(WS_ROOT / d).mkdir(parents=True, exist_ok=True)
|
| 41 |
+
HIST_ROOT.mkdir(exist_ok=True)
|
| 42 |
+
|
| 43 |
+
# Allowed execution bins (whitelist for safety on public HF)
|
| 44 |
+
ALLOWED_BINS = {
|
| 45 |
+
"bash": ["bash","-c"],
|
| 46 |
+
"sh": ["sh","-c"],
|
| 47 |
+
"python": ["python3","-c"],
|
| 48 |
+
"python3": ["python3","-c"],
|
| 49 |
+
"node": ["node","-e"],
|
| 50 |
+
"npm": ["npm"], # args passed directly
|
| 51 |
+
"pip": ["pip"],
|
| 52 |
+
"pip3": ["pip3"],
|
| 53 |
+
"cargo": ["cargo"],
|
| 54 |
+
"go": ["go"],
|
| 55 |
+
"git": ["git"],
|
| 56 |
+
"cat": ["cat"],
|
| 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 |
+
# ββ Meta / stats ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 64 |
+
def load_meta():
|
| 65 |
+
if META_FILE.exists():
|
| 66 |
+
try: return json.loads(META_FILE.read_text())
|
| 67 |
+
except: pass
|
| 68 |
+
return {"total_writes":0,"total_execs":0,"exec_log":[],"created_at":int(time.time())}
|
| 69 |
+
|
| 70 |
+
def save_meta(m):
|
| 71 |
+
META_FILE.write_text(json.dumps(m, indent=2))
|
| 72 |
+
|
| 73 |
+
META = load_meta()
|
| 74 |
+
|
| 75 |
+
# ββ Path helpers ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 76 |
+
def safe_path(rel: str) -> Path:
|
| 77 |
+
"""Resolve rel path inside WS_ROOT, raise if escaping."""
|
| 78 |
+
rel = rel.lstrip("/")
|
| 79 |
+
p = (WS_ROOT / rel).resolve()
|
| 80 |
+
if not str(p).startswith(str(WS_ROOT.resolve())):
|
| 81 |
+
raise HTTPException(400, f"Path escape attempt: {rel}")
|
| 82 |
+
return p
|
| 83 |
+
|
| 84 |
+
def rel(p: Path) -> str:
|
| 85 |
+
return str(p.relative_to(WS_ROOT))
|
| 86 |
+
|
| 87 |
+
def file_info(p: Path) -> dict:
|
| 88 |
+
stat = p.stat()
|
| 89 |
+
return {
|
| 90 |
+
"name": p.name,
|
| 91 |
+
"path": rel(p),
|
| 92 |
+
"type": "dir" if p.is_dir() else "file",
|
| 93 |
+
"size": stat.st_size,
|
| 94 |
+
"modified": int(stat.st_mtime),
|
| 95 |
+
"ext": p.suffix.lower() if p.is_file() else "",
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
# ββ Version history βββββββββββββββββββββββββββββββββββββββββββββββ
|
| 99 |
+
def hist_dir(rel_path: str) -> Path:
|
| 100 |
+
safe_key = rel_path.replace("/","__").replace("\\","__")
|
| 101 |
+
d = HIST_ROOT / safe_key
|
| 102 |
+
d.mkdir(parents=True, exist_ok=True)
|
| 103 |
+
return d
|
| 104 |
+
|
| 105 |
+
def snapshot(rel_path: str, content: str, agent: str = ""):
|
| 106 |
+
"""Save a version snapshot."""
|
| 107 |
+
hd = hist_dir(rel_path)
|
| 108 |
+
ts = int(time.time())
|
| 109 |
+
vid = f"{ts}_{uuid.uuid4().hex[:6]}"
|
| 110 |
+
sha = hashlib.sha256(content.encode()).hexdigest()[:12]
|
| 111 |
+
snap = {"id":vid,"ts":ts,"sha":sha,"agent":agent,"size":len(content.encode())}
|
| 112 |
+
(hd / f"{vid}.txt").write_text(content, encoding="utf-8", errors="replace")
|
| 113 |
+
(hd / f"{vid}.meta").write_text(json.dumps(snap))
|
| 114 |
+
# Keep last 20 versions
|
| 115 |
+
metas = sorted(hd.glob("*.meta"), key=lambda x: x.stat().st_mtime)
|
| 116 |
+
if len(metas) > 20:
|
| 117 |
+
for old_meta in metas[:-20]:
|
| 118 |
+
old_txt = old_meta.with_suffix(".txt")
|
| 119 |
+
old_meta.unlink(missing_ok=True)
|
| 120 |
+
old_txt.unlink(missing_ok=True)
|
| 121 |
+
return snap
|
| 122 |
+
|
| 123 |
+
def list_versions(rel_path: str) -> list:
|
| 124 |
+
hd = hist_dir(rel_path)
|
| 125 |
+
versions = []
|
| 126 |
+
for m in sorted(hd.glob("*.meta"), reverse=True):
|
| 127 |
+
try: versions.append(json.loads(m.read_text()))
|
| 128 |
+
except: pass
|
| 129 |
+
return versions
|
| 130 |
+
|
| 131 |
+
def get_version(rel_path: str, vid: str) -> Optional[str]:
|
| 132 |
+
hd = hist_dir(rel_path)
|
| 133 |
+
p = hd / f"{vid}.txt"
|
| 134 |
+
return p.read_text(encoding="utf-8", errors="replace") if p.exists() else None
|
| 135 |
+
|
| 136 |
+
def make_diff(old: str, new: str, fromfile: str = "old", tofile: str = "new") -> str:
|
| 137 |
+
old_lines = old.splitlines(keepends=True)
|
| 138 |
+
new_lines = new.splitlines(keepends=True)
|
| 139 |
+
return "".join(difflib.unified_diff(old_lines, new_lines,
|
| 140 |
+
fromfile=fromfile, tofile=tofile, lineterm=""))
|
| 141 |
+
|
| 142 |
+
# ββ Execution engine ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 143 |
+
def build_cmd(runtime: str, code_or_args: str, extra_args: list = []) -> list:
|
| 144 |
+
rt = runtime.lower().strip()
|
| 145 |
+
if rt not in ALLOWED_BINS:
|
| 146 |
+
raise HTTPException(400, f"Runtime '{rt}' not in allowed list: {sorted(ALLOWED_BINS)}")
|
| 147 |
+
base = ALLOWED_BINS[rt][:]
|
| 148 |
+
# Single-string runtimes (bash -c, python3 -c, node -e)
|
| 149 |
+
if base[-1] in ("-c", "-e"):
|
| 150 |
+
return base + [code_or_args] + extra_args
|
| 151 |
+
# Multi-arg tools (npm install, git status, pip install x)
|
| 152 |
+
return base + code_or_args.split() + extra_args
|
| 153 |
+
|
| 154 |
+
async def exec_command(runtime: str, code: str, cwd: str = "",
|
| 155 |
+
env_extra: dict = {},
|
| 156 |
+
timeout: int = EXEC_TIMEOUT) -> dict:
|
| 157 |
+
cmd = build_cmd(runtime, code)
|
| 158 |
+
work_dir = str(safe_path(cwd)) if cwd else str(WS_ROOT)
|
| 159 |
+
env = {**os.environ, **env_extra}
|
| 160 |
+
t0 = time.time()
|
| 161 |
+
try:
|
| 162 |
+
proc = await asyncio.create_subprocess_exec(
|
| 163 |
+
*cmd,
|
| 164 |
+
stdout=asyncio.subprocess.PIPE,
|
| 165 |
+
stderr=asyncio.subprocess.STDOUT,
|
| 166 |
+
cwd=work_dir,
|
| 167 |
+
env=env,
|
| 168 |
+
)
|
| 169 |
+
try:
|
| 170 |
+
stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=timeout)
|
| 171 |
+
except asyncio.TimeoutError:
|
| 172 |
+
proc.kill()
|
| 173 |
+
await proc.communicate()
|
| 174 |
+
return {
|
| 175 |
+
"ok": False, "exit_code": -1,
|
| 176 |
+
"output": f"[VAULT] TIMEOUT after {timeout}s",
|
| 177 |
+
"ms": int((time.time()-t0)*1000),
|
| 178 |
+
"cmd": " ".join(cmd),
|
| 179 |
+
}
|
| 180 |
+
output = stdout.decode("utf-8", errors="replace")
|
| 181 |
+
if len(output) > MAX_OUTPUT:
|
| 182 |
+
output = output[:MAX_OUTPUT] + f"\n[VAULT] Output truncated at {MAX_OUTPUT} bytes"
|
| 183 |
+
ok = proc.returncode == 0
|
| 184 |
+
result = {
|
| 185 |
+
"ok": ok, "exit_code": proc.returncode,
|
| 186 |
+
"output": output, "ms": int((time.time()-t0)*1000),
|
| 187 |
+
"cmd": " ".join(cmd), "cwd": work_dir,
|
| 188 |
+
}
|
| 189 |
+
except Exception as e:
|
| 190 |
+
result = {"ok":False,"exit_code":-1,"output":f"[VAULT] exec error: {e}",
|
| 191 |
+
"ms":int((time.time()-t0)*1000),"cmd":str(cmd)}
|
| 192 |
+
|
| 193 |
+
META["total_execs"] += 1
|
| 194 |
+
entry = {"id":uuid.uuid4().hex[:8],"ts":int(time.time()),
|
| 195 |
+
"runtime":runtime,"cmd":result.get("cmd","")[:120],
|
| 196 |
+
"ok":result["ok"],"ms":result["ms"],"exit_code":result["exit_code"]}
|
| 197 |
+
META["exec_log"] = ([entry] + META.get("exec_log",[]))[:50]
|
| 198 |
+
save_meta(META)
|
| 199 |
+
return result
|
| 200 |
+
|
| 201 |
+
# ββ File search βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 202 |
+
def search_files(query: str, scope: str = "", max_results: int = 40) -> list:
|
| 203 |
+
root = safe_path(scope) if scope else WS_ROOT
|
| 204 |
+
q = query.lower()
|
| 205 |
+
results = []
|
| 206 |
+
for p in root.rglob("*"):
|
| 207 |
+
if not p.is_file(): continue
|
| 208 |
+
if len(results) >= max_results: break
|
| 209 |
+
match_name = q in p.name.lower()
|
| 210 |
+
match_content = False
|
| 211 |
+
snippet = ""
|
| 212 |
+
if p.stat().st_size < 2*1024*1024: # 2MB limit for content search
|
| 213 |
+
try:
|
| 214 |
+
text = p.read_text(encoding="utf-8", errors="replace")
|
| 215 |
+
if q in text.lower():
|
| 216 |
+
match_content = True
|
| 217 |
+
# Find snippet
|
| 218 |
+
idx = text.lower().find(q)
|
| 219 |
+
snippet = text[max(0,idx-40):idx+80].replace("\n"," ")
|
| 220 |
+
except: pass
|
| 221 |
+
if match_name or match_content:
|
| 222 |
+
results.append({
|
| 223 |
+
**file_info(p),
|
| 224 |
+
"match_name": match_name,
|
| 225 |
+
"match_content": match_content,
|
| 226 |
+
"snippet": snippet,
|
| 227 |
+
})
|
| 228 |
+
return results
|
| 229 |
+
|
| 230 |
+
# ββ Seed workspace ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 231 |
+
def seed():
|
| 232 |
+
files = {
|
| 233 |
+
"code/hello.py": '# VAULT β example Python script\nprint("Hello from VAULT!")\n\nfor i in range(5):\n print(f" step {i+1}: processing...")\n',
|
| 234 |
+
"code/analysis.js": '// Example Node.js analysis\nconst data = [1,2,3,4,5,6,7,8,9,10];\nconst mean = data.reduce((a,b) => a+b, 0) / data.length;\nconsole.log(`Mean: ${mean}`);\nconsole.log(`Max: ${Math.max(...data)}`);\nconsole.log(`Min: ${Math.min(...data)}`);\n',
|
| 235 |
+
"code/install_deps.sh": '#!/bin/bash\n# Install project dependencies\necho "[vault] Installing Python deps..."\npip install requests numpy --quiet\necho "[vault] Done."\n',
|
| 236 |
+
"reports/README.md": '# Reports\n\nGenerated analysis outputs from agent runs.\n\n## Structure\n- `*.md` β markdown reports\n- `*.json` β structured data outputs\n- `*.csv` β tabular results\n',
|
| 237 |
+
"scratch/notes.md": '# Scratch Notes\n\nTemporary working notes for agents.\n\n## Active tasks\n- [ ] Analyze MTEB embedding benchmarks\n- [ ] Compare ki-fusion latency vs HF API\n- [x] Set up VAULT workspace\n',
|
| 238 |
+
"shared/config.json": '{\n "project": "ki-fusion-labs",\n "version": "0.1.0",\n "agents": ["researcher", "coder", "planner", "monitor"],\n "default_workspace": "scratch",\n "exec_timeout": 30\n}\n',
|
| 239 |
+
"shared/agent_contract.md": '# Agent Workspace Contract\n\nAll agents operating in VAULT must follow:\n\n1. **Read** freely from `shared/` and `reports/`\n2. **Write** to your own scoped dir under `scratch/{agent_id}/`\n3. **Code** goes to `code/` with comments\n4. **Reports** go to `reports/` with timestamp prefix\n5. **Never** delete files in `shared/` without broadcast\n',
|
| 240 |
+
}
|
| 241 |
+
for path, content in files.items():
|
| 242 |
+
p = WS_ROOT / path
|
| 243 |
+
if not p.exists():
|
| 244 |
+
p.parent.mkdir(parents=True, exist_ok=True)
|
| 245 |
+
p.write_text(content, encoding="utf-8")
|
| 246 |
+
snapshot(path, content, "vault-init")
|
| 247 |
+
|
| 248 |
+
seed()
|
| 249 |
+
|
| 250 |
+
# ββ FastAPI βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 251 |
+
app = FastAPI(title="VAULT - Workspace Manager")
|
| 252 |
+
|
| 253 |
+
def jresp(data, status=200): return JSONResponse(content=data, status_code=status)
|
| 254 |
+
|
| 255 |
+
# ββ File API ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 256 |
+
|
| 257 |
+
@app.get("/api/ls")
|
| 258 |
+
async def list_dir(path: str = ""):
|
| 259 |
+
p = safe_path(path) if path else WS_ROOT
|
| 260 |
+
if not p.exists(): raise HTTPException(404, "not found")
|
| 261 |
+
if p.is_file():
|
| 262 |
+
return jresp(file_info(p))
|
| 263 |
+
items = []
|
| 264 |
+
for child in sorted(p.iterdir(), key=lambda x: (x.is_file(), x.name)):
|
| 265 |
+
try: items.append(file_info(child))
|
| 266 |
+
except: pass
|
| 267 |
+
return jresp({"path": rel(p) if p != WS_ROOT else "", "items": items})
|
| 268 |
+
|
| 269 |
+
@app.get("/api/read")
|
| 270 |
+
async def read_file(path: str):
|
| 271 |
+
p = safe_path(path)
|
| 272 |
+
if not p.exists(): raise HTTPException(404)
|
| 273 |
+
if p.is_dir(): raise HTTPException(400, "is a directory")
|
| 274 |
+
if p.stat().st_size > 4*1024*1024:
|
| 275 |
+
raise HTTPException(413, "File too large (>4MB)")
|
| 276 |
+
try:
|
| 277 |
+
content = p.read_text(encoding="utf-8", errors="replace")
|
| 278 |
+
binary = False
|
| 279 |
+
except Exception:
|
| 280 |
+
content = p.read_bytes().hex()
|
| 281 |
+
binary = True
|
| 282 |
+
return jresp({"path": rel(p), **file_info(p), "content": content, "binary": binary})
|
| 283 |
+
|
| 284 |
+
@app.post("/api/write")
|
| 285 |
+
async def write_file(request: Request):
|
| 286 |
+
data = await request.json()
|
| 287 |
+
path = data.get("path","").strip()
|
| 288 |
+
content = data.get("content","")
|
| 289 |
+
agent = data.get("agent","unknown")
|
| 290 |
+
if not path: raise HTTPException(400, "path required")
|
| 291 |
+
p = safe_path(path)
|
| 292 |
+
p.parent.mkdir(parents=True, exist_ok=True)
|
| 293 |
+
# Snapshot existing before overwrite
|
| 294 |
+
old_content = None
|
| 295 |
+
if p.exists() and p.is_file():
|
| 296 |
+
try: old_content = p.read_text(encoding="utf-8", errors="replace")
|
| 297 |
+
except: pass
|
| 298 |
+
p.write_text(content, encoding="utf-8")
|
| 299 |
+
snap = snapshot(rel(p), content, agent)
|
| 300 |
+
META["total_writes"] += 1
|
| 301 |
+
save_meta(META)
|
| 302 |
+
diff = make_diff(old_content or "", content, "previous", "current") if old_content else ""
|
| 303 |
+
return jresp({"status":"written","path":rel(p),"snapshot":snap,
|
| 304 |
+
"diff_lines": len(diff.splitlines()), "new_file": old_content is None})
|
| 305 |
+
|
| 306 |
+
@app.post("/api/mkdir")
|
| 307 |
+
async def make_dir(request: Request):
|
| 308 |
+
data = await request.json()
|
| 309 |
+
path = data.get("path","").strip()
|
| 310 |
+
if not path: raise HTTPException(400, "path required")
|
| 311 |
+
p = safe_path(path)
|
| 312 |
+
p.mkdir(parents=True, exist_ok=True)
|
| 313 |
+
return jresp({"status":"created","path":rel(p)})
|
| 314 |
+
|
| 315 |
+
@app.delete("/api/delete")
|
| 316 |
+
async def delete_path(request: Request):
|
| 317 |
+
data = await request.json()
|
| 318 |
+
path = data.get("path","").strip()
|
| 319 |
+
if not path: raise HTTPException(400)
|
| 320 |
+
p = safe_path(path)
|
| 321 |
+
if not p.exists(): raise HTTPException(404)
|
| 322 |
+
if p.is_dir(): shutil.rmtree(p)
|
| 323 |
+
else: p.unlink()
|
| 324 |
+
return jresp({"status":"deleted","path":path})
|
| 325 |
+
|
| 326 |
+
@app.post("/api/move")
|
| 327 |
+
async def move_path(request: Request):
|
| 328 |
+
data = await request.json()
|
| 329 |
+
src = safe_path(data.get("src",""))
|
| 330 |
+
dst = safe_path(data.get("dst",""))
|
| 331 |
+
if not src.exists(): raise HTTPException(404)
|
| 332 |
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
| 333 |
+
shutil.move(str(src), str(dst))
|
| 334 |
+
return jresp({"status":"moved","from":rel(src),"to":rel(dst)})
|
| 335 |
+
|
| 336 |
+
@app.post("/api/copy")
|
| 337 |
+
async def copy_path(request: Request):
|
| 338 |
+
data = await request.json()
|
| 339 |
+
src = safe_path(data.get("src",""))
|
| 340 |
+
dst = safe_path(data.get("dst",""))
|
| 341 |
+
if not src.exists(): raise HTTPException(404)
|
| 342 |
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
| 343 |
+
if src.is_dir(): shutil.copytree(str(src), str(dst))
|
| 344 |
+
else: shutil.copy2(str(src), str(dst))
|
| 345 |
+
return jresp({"status":"copied","from":rel(src),"to":rel(dst)})
|
| 346 |
+
|
| 347 |
+
@app.get("/api/search")
|
| 348 |
+
async def search(q: str, scope: str = "", limit: int = 30):
|
| 349 |
+
if not q.strip(): raise HTTPException(400, "q required")
|
| 350 |
+
return jresp({"query":q,"results":search_files(q, scope, limit)})
|
| 351 |
+
|
| 352 |
+
# ββ Version API βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 353 |
+
|
| 354 |
+
@app.get("/api/versions")
|
| 355 |
+
async def versions(path: str):
|
| 356 |
+
return jresp({"path":path,"versions":list_versions(path)})
|
| 357 |
+
|
| 358 |
+
@app.get("/api/version")
|
| 359 |
+
async def get_ver(path: str, vid: str):
|
| 360 |
+
content = get_version(path, vid)
|
| 361 |
+
if content is None: raise HTTPException(404)
|
| 362 |
+
return jresp({"path":path,"vid":vid,"content":content})
|
| 363 |
+
|
| 364 |
+
@app.get("/api/diff")
|
| 365 |
+
async def diff_versions(path: str, from_vid: str = "", to_vid: str = ""):
|
| 366 |
+
p = safe_path(path)
|
| 367 |
+
current = p.read_text(encoding="utf-8", errors="replace") if p.exists() else ""
|
| 368 |
+
vers = list_versions(path)
|
| 369 |
+
if from_vid:
|
| 370 |
+
old = get_version(path, from_vid) or ""
|
| 371 |
+
elif vers:
|
| 372 |
+
old = get_version(path, vers[-1]["id"]) or ""
|
| 373 |
+
else:
|
| 374 |
+
old = ""
|
| 375 |
+
new = get_version(path, to_vid) if to_vid else current
|
| 376 |
+
diff = make_diff(old, new or "")
|
| 377 |
+
return jresp({"path":path,"diff":diff,"lines":len(diff.splitlines())})
|
| 378 |
+
|
| 379 |
+
# ββ Execution API βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 380 |
+
|
| 381 |
+
@app.post("/api/exec")
|
| 382 |
+
async def exec_code(request: Request):
|
| 383 |
+
data = await request.json()
|
| 384 |
+
runtime = data.get("runtime","bash")
|
| 385 |
+
code = data.get("code","")
|
| 386 |
+
cwd = data.get("cwd","")
|
| 387 |
+
env = data.get("env",{})
|
| 388 |
+
timeout = int(data.get("timeout", EXEC_TIMEOUT))
|
| 389 |
+
if not code.strip(): raise HTTPException(400, "code required")
|
| 390 |
+
result = await exec_command(runtime, code, cwd, env, min(timeout, 60))
|
| 391 |
+
return jresp(result)
|
| 392 |
+
|
| 393 |
+
@app.get("/api/exec/stream")
|
| 394 |
+
async def exec_stream(runtime: str, code: str, cwd: str = ""):
|
| 395 |
+
"""SSE streaming execution output"""
|
| 396 |
+
cmd = build_cmd(runtime, code)
|
| 397 |
+
work_dir = str(safe_path(cwd)) if cwd else str(WS_ROOT)
|
| 398 |
+
async def stream():
|
| 399 |
+
yield f"data: {json.dumps({'type':'start','cmd':' '.join(cmd)})}\n\n"
|
| 400 |
+
try:
|
| 401 |
+
proc = await asyncio.create_subprocess_exec(
|
| 402 |
+
*cmd, stdout=asyncio.subprocess.PIPE,
|
| 403 |
+
stderr=asyncio.subprocess.STDOUT, cwd=work_dir)
|
| 404 |
+
async for line in proc.stdout:
|
| 405 |
+
text = line.decode("utf-8", errors="replace").rstrip()
|
| 406 |
+
yield f"data: {json.dumps({'type':'output','line':text})}\n\n"
|
| 407 |
+
await proc.wait()
|
| 408 |
+
yield f"data: {json.dumps({'type':'done','exit_code':proc.returncode})}\n\n"
|
| 409 |
+
except Exception as e:
|
| 410 |
+
yield f"data: {json.dumps({'type':'error','message':str(e)})}\n\n"
|
| 411 |
+
return StreamingResponse(stream(), media_type="text/event-stream",
|
| 412 |
+
headers={"Cache-Control":"no-cache","X-Accel-Buffering":"no"})
|
| 413 |
+
|
| 414 |
+
@app.get("/api/exec/log")
|
| 415 |
+
async def exec_log(limit: int = 30):
|
| 416 |
+
return jresp({"log": META.get("exec_log",[])[:limit]})
|
| 417 |
+
|
| 418 |
+
@app.get("/api/runtimes")
|
| 419 |
+
async def runtimes():
|
| 420 |
+
available = {}
|
| 421 |
+
for name, cmd in ALLOWED_BINS.items():
|
| 422 |
+
try:
|
| 423 |
+
r = subprocess.run(["which", cmd[0]], capture_output=True, timeout=2)
|
| 424 |
+
available[name] = {"available": r.returncode==0, "bin": cmd[0],
|
| 425 |
+
"path": r.stdout.decode().strip()}
|
| 426 |
+
except: available[name] = {"available": False, "bin": cmd[0]}
|
| 427 |
+
return jresp(available)
|
| 428 |
+
|
| 429 |
+
@app.get("/api/stats")
|
| 430 |
+
async def stats():
|
| 431 |
+
total_size = 0
|
| 432 |
+
total_files = 0
|
| 433 |
+
by_ext: dict = {}
|
| 434 |
+
for p in WS_ROOT.rglob("*"):
|
| 435 |
+
if p.is_file():
|
| 436 |
+
total_files += 1
|
| 437 |
+
sz = p.stat().st_size
|
| 438 |
+
total_size += sz
|
| 439 |
+
ext = p.suffix.lower() or "(none)"
|
| 440 |
+
by_ext[ext] = by_ext.get(ext, 0) + sz
|
| 441 |
+
return jresp({"total_files":total_files,"total_size":total_size,
|
| 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",
|
| 448 |
+
"inputSchema":{"type":"object","required":["path"],"properties":{"path":{"type":"string"}}}},
|
| 449 |
+
{"name":"vault_write","description":"Write/create a file in the workspace",
|
| 450 |
+
"inputSchema":{"type":"object","required":["path","content"],"properties":{
|
| 451 |
+
"path":{"type":"string"},"content":{"type":"string"},"agent":{"type":"string"}}}},
|
| 452 |
+
{"name":"vault_list","description":"List files in a workspace directory",
|
| 453 |
+
"inputSchema":{"type":"object","properties":{"path":{"type":"string","default":""}}}},
|
| 454 |
+
{"name":"vault_exec","description":"Execute code in the workspace. Runtimes: bash, python, python3, node, npm, pip, git",
|
| 455 |
+
"inputSchema":{"type":"object","required":["runtime","code"],"properties":{
|
| 456 |
+
"runtime":{"type":"string","enum":["bash","sh","python","python3","node","npm","pip","git","go","cargo"]},
|
| 457 |
+
"code": {"type":"string","description":"Code to run or command args"},
|
| 458 |
+
"cwd": {"type":"string","description":"Working directory (relative to workspace root)"},
|
| 459 |
+
"timeout":{"type":"integer","default":30}}}},
|
| 460 |
+
{"name":"vault_diff","description":"Show diff between current file and a previous version",
|
| 461 |
+
"inputSchema":{"type":"object","required":["path"],"properties":{
|
| 462 |
+
"path": {"type":"string"},
|
| 463 |
+
"from_vid": {"type":"string","description":"Version ID (from vault_versions)"},
|
| 464 |
+
"to_vid": {"type":"string"}}}},
|
| 465 |
+
{"name":"vault_versions","description":"List version history of a file",
|
| 466 |
+
"inputSchema":{"type":"object","required":["path"],"properties":{"path":{"type":"string"}}}},
|
| 467 |
+
{"name":"vault_search","description":"Search files by name or content",
|
| 468 |
+
"inputSchema":{"type":"object","required":["query"],"properties":{
|
| 469 |
+
"query": {"type":"string"},"scope":{"type":"string"},"limit":{"type":"integer"}}}},
|
| 470 |
+
{"name":"vault_delete","description":"Delete a file or directory",
|
| 471 |
+
"inputSchema":{"type":"object","required":["path"],"properties":{"path":{"type":"string"}}}},
|
| 472 |
+
{"name":"vault_mkdir","description":"Create a directory in the workspace",
|
| 473 |
+
"inputSchema":{"type":"object","required":["path"],"properties":{"path":{"type":"string"}}}},
|
| 474 |
+
{"name":"vault_move","description":"Move or rename a file",
|
| 475 |
+
"inputSchema":{"type":"object","required":["src","dst"],"properties":{
|
| 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):
|
| 482 |
+
if name == "vault_read":
|
| 483 |
+
p = safe_path(args["path"])
|
| 484 |
+
if not p.exists(): return json.dumps({"error":"not found"})
|
| 485 |
+
return json.dumps({"content": p.read_text(encoding="utf-8", errors="replace"),
|
| 486 |
+
"path": args["path"], **file_info(p)})
|
| 487 |
+
if name == "vault_write":
|
| 488 |
+
p = safe_path(args["path"]); p.parent.mkdir(parents=True, exist_ok=True)
|
| 489 |
+
old = p.read_text(encoding="utf-8",errors="replace") if p.exists() else None
|
| 490 |
+
p.write_text(args["content"], encoding="utf-8")
|
| 491 |
+
snap = snapshot(rel(p), args["content"], args.get("agent","mcp"))
|
| 492 |
+
META["total_writes"] += 1; save_meta(META)
|
| 493 |
+
return json.dumps({"written": args["path"], "snapshot": snap})
|
| 494 |
+
if name == "vault_list":
|
| 495 |
+
p = safe_path(args.get("path","")) if args.get("path") else WS_ROOT
|
| 496 |
+
items = [file_info(c) for c in sorted(p.iterdir(),key=lambda x:(x.is_file(),x.name))] if p.is_dir() else []
|
| 497 |
+
return json.dumps({"path": args.get("path",""), "items": items})
|
| 498 |
+
if name == "vault_exec":
|
| 499 |
+
r = await exec_command(args["runtime"], args["code"],
|
| 500 |
+
args.get("cwd",""), {}, args.get("timeout",30))
|
| 501 |
+
return json.dumps(r)
|
| 502 |
+
if name == "vault_diff":
|
| 503 |
+
p = safe_path(args["path"])
|
| 504 |
+
curr = p.read_text(encoding="utf-8",errors="replace") if p.exists() else ""
|
| 505 |
+
vers = list_versions(args["path"])
|
| 506 |
+
old = get_version(args["path"], args.get("from_vid") or (vers[-1]["id"] if vers else "")) or ""
|
| 507 |
+
new = get_version(args["path"], args["to_vid"]) if args.get("to_vid") else curr
|
| 508 |
+
return json.dumps({"diff": make_diff(old, new or "")})
|
| 509 |
+
if name == "vault_versions":
|
| 510 |
+
return json.dumps({"versions": list_versions(args["path"])})
|
| 511 |
+
if name == "vault_search":
|
| 512 |
+
return json.dumps({"results": search_files(args["query"], args.get("scope",""), args.get("limit",20))})
|
| 513 |
+
if name == "vault_delete":
|
| 514 |
+
p = safe_path(args["path"])
|
| 515 |
+
if p.is_dir(): shutil.rmtree(p)
|
| 516 |
+
else: p.unlink(missing_ok=True)
|
| 517 |
+
return json.dumps({"deleted": args["path"]})
|
| 518 |
+
if name == "vault_mkdir":
|
| 519 |
+
safe_path(args["path"]).mkdir(parents=True, exist_ok=True)
|
| 520 |
+
return json.dumps({"created": args["path"]})
|
| 521 |
+
if name == "vault_move":
|
| 522 |
+
src = safe_path(args["src"]); dst = safe_path(args["dst"])
|
| 523 |
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
| 524 |
+
shutil.move(str(src), str(dst))
|
| 525 |
+
return json.dumps({"moved": {"from": args["src"], "to": args["dst"]}})
|
| 526 |
+
if name == "vault_stats":
|
| 527 |
+
total_size=0; total_files=0
|
| 528 |
+
for p in WS_ROOT.rglob("*"):
|
| 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")
|
| 535 |
+
async def mcp_sse():
|
| 536 |
+
async def stream():
|
| 537 |
+
init = {"jsonrpc":"2.0","method":"notifications/initialized",
|
| 538 |
+
"params":{"serverInfo":{"name":"vault","version":"1.0"},"capabilities":{"tools":{}}}}
|
| 539 |
+
yield f"data: {json.dumps(init)}\n\n"
|
| 540 |
+
await asyncio.sleep(0.1)
|
| 541 |
+
yield f"data: {json.dumps({'jsonrpc':'2.0','method':'notifications/tools/list_changed','params':{}})}\n\n"
|
| 542 |
+
while True:
|
| 543 |
+
await asyncio.sleep(25)
|
| 544 |
+
yield f"data: {json.dumps({'jsonrpc':'2.0','method':'ping'})}\n\n"
|
| 545 |
+
return StreamingResponse(stream(), media_type="text/event-stream",
|
| 546 |
+
headers={"Cache-Control":"no-cache","X-Accel-Buffering":"no"})
|
| 547 |
+
|
| 548 |
+
@app.post("/mcp")
|
| 549 |
+
async def mcp_rpc(request: Request):
|
| 550 |
+
body = await request.json()
|
| 551 |
+
method = body.get("method",""); rid = body.get("id",1)
|
| 552 |
+
if method == "initialize":
|
| 553 |
+
return jresp({"jsonrpc":"2.0","id":rid,"result":{
|
| 554 |
+
"serverInfo":{"name":"vault","version":"1.0"},"capabilities":{"tools":{}}}})
|
| 555 |
+
if method == "tools/list":
|
| 556 |
+
return jresp({"jsonrpc":"2.0","id":rid,"result":{"tools":MCP_TOOLS}})
|
| 557 |
+
if method == "tools/call":
|
| 558 |
+
p = body.get("params",{}); res = await mcp_call(p.get("name",""), p.get("arguments",{}))
|
| 559 |
+
return jresp({"jsonrpc":"2.0","id":rid,"result":{"content":[{"type":"text","text":res}]}})
|
| 560 |
+
return jresp({"jsonrpc":"2.0","id":rid,"error":{"code":-32601,"message":"not found"}})
|
| 561 |
+
|
| 562 |
+
# ββ SPA βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 563 |
+
@app.get("/", response_class=HTMLResponse)
|
| 564 |
+
async def ui():
|
| 565 |
+
return HTMLResponse(content=SPA, media_type="text/html; charset=utf-8")
|
| 566 |
+
|
| 567 |
+
SPA = r"""<!DOCTYPE html>
|
| 568 |
+
<html lang="en">
|
| 569 |
+
<head>
|
| 570 |
+
<meta charset="UTF-8">
|
| 571 |
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
| 572 |
+
<title>VAULT — Workspace Manager</title>
|
| 573 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 574 |
+
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
|
| 575 |
+
<style>
|
| 576 |
+
:root{
|
| 577 |
+
--bg:#08080f;--s1:#0f0f1a;--s2:#141422;--bd:#1a1a2e;--bd2:#20203a;
|
| 578 |
+
--acc:#ff6b00;--acc2:#ff9500;--txt:#d8d8f0;--sub:#4a4a72;--dim:#18182e;
|
| 579 |
+
--lo:#2ed573;--cr:#ff2244;--warn:#f0c040;--info:#0ea5e9;
|
| 580 |
+
--code-bg:#0a0a16;--font:'Space Mono',monospace;--mono:'JetBrains Mono',monospace;
|
| 581 |
+
/* folder colours */
|
| 582 |
+
--fc:#ffb347;--fc-code:#0ea5e9;--fc-rep:#7c3aed;--fc-scr:#2ed573;--fc-sh:#ff6b9d;
|
| 583 |
+
}
|
| 584 |
+
*{box-sizing:border-box;margin:0;padding:0;}
|
| 585 |
+
html,body{height:100%;overflow:hidden;}
|
| 586 |
+
body{font-family:var(--font);background:var(--bg);color:var(--txt);display:flex;flex-direction:column;height:100vh;}
|
| 587 |
+
body::after{content:'';position:fixed;inset:0;pointer-events:none;
|
| 588 |
+
background:repeating-linear-gradient(0deg,transparent,transparent 3px,rgba(255,107,0,.004) 3px,rgba(255,107,0,.004) 4px);}
|
| 589 |
+
|
| 590 |
+
/* HEADER */
|
| 591 |
+
#hdr{flex-shrink:0;display:flex;align-items:center;padding:.7rem 1.4rem;gap:1rem;
|
| 592 |
+
border-bottom:1px solid var(--bd);background:linear-gradient(180deg,#0d0d1a,var(--bg));z-index:10;}
|
| 593 |
+
#logo{font-size:1.2rem;font-weight:700;letter-spacing:2px;
|
| 594 |
+
background:linear-gradient(90deg,var(--acc),var(--warn));
|
| 595 |
+
-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;}
|
| 596 |
+
#logo-sub{font-size:.5rem;color:var(--sub);letter-spacing:.25em;text-transform:uppercase;margin-top:2px;}
|
| 597 |
+
#hdr-stats{display:flex;gap:.42rem;flex:1;flex-wrap:wrap;}
|
| 598 |
+
.hs{display:flex;align-items:center;gap:.3rem;background:var(--s1);border:1px solid var(--bd);
|
| 599 |
+
border-radius:4px;padding:.2rem .48rem;font-size:.52rem;color:var(--sub);}
|
| 600 |
+
.hs-n{font-size:.82rem;font-weight:700;line-height:1;color:var(--txt);}
|
| 601 |
+
.hdr-actions{display:flex;gap:.4rem;flex-shrink:0;}
|
| 602 |
+
.hdr-btn{background:var(--s1);border:1px solid var(--bd2);color:var(--sub);padding:.32rem .65rem;
|
| 603 |
+
font-family:var(--font);font-size:.6rem;border-radius:4px;cursor:pointer;transition:all .1s;}
|
| 604 |
+
.hdr-btn:hover{border-color:var(--acc);color:var(--acc);}
|
| 605 |
+
.hdr-btn.primary{background:var(--acc);color:#000;border-color:var(--acc);}
|
| 606 |
+
.hdr-btn.primary:hover{background:var(--acc2);}
|
| 607 |
+
|
| 608 |
+
/* TOOLBAR */
|
| 609 |
+
#toolbar{flex-shrink:0;display:flex;align-items:center;gap:.4rem;
|
| 610 |
+
padding:.44rem 1.4rem;border-bottom:1px solid var(--bd);background:var(--s1);flex-wrap:wrap;}
|
| 611 |
+
#breadcrumb{font-size:.6rem;color:var(--sub);display:flex;align-items:center;gap:.2rem;flex:1;min-width:0;overflow:hidden;}
|
| 612 |
+
.bc-sep{color:var(--bd2);}
|
| 613 |
+
.bc-part{cursor:pointer;color:var(--sub);transition:color .1s;white-space:nowrap;}
|
| 614 |
+
.bc-part:hover{color:var(--acc);}
|
| 615 |
+
.bc-part.cur{color:var(--txt);}
|
| 616 |
+
#search-input{background:var(--s2);border:1px solid var(--bd2);border-radius:4px;
|
| 617 |
+
padding:.3rem .55rem;font-family:var(--font);font-size:.62rem;color:var(--txt);outline:none;
|
| 618 |
+
width:160px;transition:border-color .12s;}
|
| 619 |
+
#search-input:focus{border-color:var(--acc);}
|
| 620 |
+
.tb-btn{background:var(--s2);border:1px solid var(--bd2);color:var(--sub);padding:.3rem .55rem;
|
| 621 |
+
font-family:var(--font);font-size:.58rem;border-radius:4px;cursor:pointer;transition:all .1s;white-space:nowrap;}
|
| 622 |
+
.tb-btn:hover{border-color:var(--acc);color:var(--acc);}
|
| 623 |
+
|
| 624 |
+
/* MAIN LAYOUT */
|
| 625 |
+
#main{flex:1;display:flex;min-height:0;overflow:hidden;}
|
| 626 |
+
|
| 627 |
+
/* FILE TREE PANEL */
|
| 628 |
+
#tree{width:230px;flex-shrink:0;border-right:1px solid var(--bd);display:flex;flex-direction:column;overflow:hidden;}
|
| 629 |
+
#tree-hdr{flex-shrink:0;padding:.45rem .75rem;border-bottom:1px solid var(--bd);
|
| 630 |
+
font-size:.54rem;font-weight:700;letter-spacing:.1em;color:var(--acc);
|
| 631 |
+
display:flex;align-items:center;justify-content:space-between;}
|
| 632 |
+
#tree-scroll{flex:1;overflow-y:auto;padding:.35rem 0;}
|
| 633 |
+
#tree-scroll::-webkit-scrollbar{width:3px;}
|
| 634 |
+
#tree-scroll::-webkit-scrollbar-thumb{background:var(--bd2);}
|
| 635 |
+
.ti{display:flex;align-items:center;gap:.35rem;padding:.28rem .75rem;cursor:pointer;
|
| 636 |
+
font-size:.62rem;color:var(--sub);transition:all .1s;position:relative;
|
| 637 |
+
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
|
| 638 |
+
.ti:hover{background:var(--s1);color:var(--txt);}
|
| 639 |
+
.ti.active{background:var(--dim);color:var(--txt);}
|
| 640 |
+
.ti.active::before{content:'';position:absolute;left:0;top:0;bottom:0;width:2px;background:var(--acc);}
|
| 641 |
+
.ti-icon{font-size:.75rem;flex-shrink:0;}
|
| 642 |
+
.ti-name{flex:1;overflow:hidden;text-overflow:ellipsis;}
|
| 643 |
+
.ti-depth-1{padding-left:1.3rem;}
|
| 644 |
+
.ti-depth-2{padding-left:2rem;}
|
| 645 |
+
.ti-depth-3{padding-left:2.7rem;}
|
| 646 |
+
.tree-section{font-size:.48rem;color:var(--sub);text-transform:uppercase;letter-spacing:.15em;
|
| 647 |
+
padding:.5rem .75rem .2rem;margin-top:.3rem;}
|
| 648 |
+
|
| 649 |
+
/* CONTENT AREA */
|
| 650 |
+
#content{flex:1;display:flex;flex-direction:column;overflow:hidden;}
|
| 651 |
+
|
| 652 |
+
/* TABS */
|
| 653 |
+
#tabs{flex-shrink:0;display:flex;align-items:stretch;border-bottom:1px solid var(--bd);background:var(--s1);overflow-x:auto;}
|
| 654 |
+
#tabs::-webkit-scrollbar{height:2px;}
|
| 655 |
+
.tab-item{display:flex;align-items:center;gap:.38rem;padding:.44rem .9rem;font-size:.6rem;
|
| 656 |
+
cursor:pointer;color:var(--sub);border-bottom:2px solid transparent;white-space:nowrap;
|
| 657 |
+
transition:all .1s;flex-shrink:0;}
|
| 658 |
+
.tab-item:hover{color:var(--txt);}
|
| 659 |
+
.tab-item.on{color:var(--acc);border-bottom-color:var(--acc);}
|
| 660 |
+
.tab-close{font-size:.6rem;opacity:.4;width:14px;height:14px;display:flex;align-items:center;
|
| 661 |
+
justify-content:center;border-radius:3px;}
|
| 662 |
+
.tab-close:hover{opacity:1;background:var(--bd2);}
|
| 663 |
+
#tab-terminal{color:var(--lo);}
|
| 664 |
+
#tab-terminal.on{color:var(--lo);border-bottom-color:var(--lo);}
|
| 665 |
+
|
| 666 |
+
/* EDITOR */
|
| 667 |
+
#editor-wrap{flex:1;display:flex;flex-direction:column;overflow:hidden;}
|
| 668 |
+
#editor-toolbar{flex-shrink:0;display:flex;align-items:center;gap:.4rem;
|
| 669 |
+
padding:.38rem .8rem;border-bottom:1px solid var(--bd);background:var(--s2);flex-wrap:wrap;}
|
| 670 |
+
#file-path-display{font-size:.58rem;color:var(--sub);font-family:var(--mono);flex:1;}
|
| 671 |
+
.e-btn{background:var(--s1);border:1px solid var(--bd2);color:var(--sub);padding:.26rem .55rem;
|
| 672 |
+
font-family:var(--font);font-size:.56rem;border-radius:4px;cursor:pointer;transition:all .1s;}
|
| 673 |
+
.e-btn:hover{border-color:var(--acc);color:var(--acc);}
|
| 674 |
+
.e-btn.save{background:var(--acc);color:#000;border-color:var(--acc);}
|
| 675 |
+
.e-btn.save:hover{background:var(--acc2);}
|
| 676 |
+
.e-btn.run{background:#02130a;color:var(--lo);border-color:rgba(46,213,115,.3);}
|
| 677 |
+
.e-btn.run:hover{background:#041f0f;border-color:var(--lo);}
|
| 678 |
+
#editor-area{flex:1;overflow:hidden;position:relative;}
|
| 679 |
+
#editor{width:100%;height:100%;background:var(--code-bg);color:var(--txt);border:none;outline:none;
|
| 680 |
+
font-family:var(--mono);font-size:.74rem;line-height:1.65;padding:.9rem 1rem;
|
| 681 |
+
resize:none;tab-size:2;}
|
| 682 |
+
#editor-placeholder{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;
|
| 683 |
+
justify-content:center;gap:.8rem;color:var(--sub);pointer-events:none;}
|
| 684 |
+
#editor-placeholder .big{font-size:2.5rem;opacity:.12;}
|
| 685 |
+
#editor-placeholder .msg{font-size:.6rem;opacity:.35;letter-spacing:.12em;text-transform:uppercase;}
|
| 686 |
+
|
| 687 |
+
/* FILE BROWSER VIEW */
|
| 688 |
+
#browser-view{flex:1;overflow-y:auto;padding:.6rem .9rem;display:none;}
|
| 689 |
+
#browser-view::-webkit-scrollbar{width:4px;}
|
| 690 |
+
#browser-view::-webkit-scrollbar-thumb{background:var(--bd2);}
|
| 691 |
+
.file-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:.5rem;}
|
| 692 |
+
.fg-item{background:var(--s1);border:1px solid var(--bd);border-radius:7px;padding:.65rem .75rem;
|
| 693 |
+
cursor:pointer;transition:all .12s;animation:cin .14s ease;}
|
| 694 |
+
@keyframes cin{from{opacity:0;transform:translateY(2px)}to{opacity:1;transform:none}}
|
| 695 |
+
.fg-item:hover{border-color:var(--bd2);transform:translateY(-2px);}
|
| 696 |
+
.fg-icon{font-size:1.4rem;margin-bottom:.35rem;}
|
| 697 |
+
.fg-name{font-size:.6rem;font-weight:700;color:var(--txt);word-break:break-all;margin-bottom:.2rem;}
|
| 698 |
+
.fg-meta{font-size:.5rem;color:var(--sub);}
|
| 699 |
+
.list-item{display:flex;align-items:center;gap:.5rem;padding:.35rem .55rem;
|
| 700 |
+
border:1px solid transparent;border-radius:5px;cursor:pointer;transition:all .1s;}
|
| 701 |
+
.list-item:hover{background:var(--s1);border-color:var(--bd);}
|
| 702 |
+
.li-icon{font-size:.9rem;flex-shrink:0;}
|
| 703 |
+
.li-name{font-size:.65rem;color:var(--txt);flex:1;font-family:var(--mono);}
|
| 704 |
+
.li-size{font-size:.52rem;color:var(--sub);width:60px;text-align:right;}
|
| 705 |
+
.li-date{font-size:.5rem;color:var(--dim);width:80px;text-align:right;}
|
| 706 |
+
.li-actions{display:flex;gap:.22rem;opacity:0;transition:opacity .1s;}
|
| 707 |
+
.list-item:hover .li-actions{opacity:1;}
|
| 708 |
+
.li-act{font-size:.52rem;background:var(--s2);border:1px solid var(--bd2);border-radius:3px;
|
| 709 |
+
padding:1px 5px;cursor:pointer;color:var(--sub);font-family:var(--font);}
|
| 710 |
+
.li-act:hover{color:var(--acc);border-color:var(--acc);}
|
| 711 |
+
.li-act.danger:hover{color:var(--cr);border-color:var(--cr);}
|
| 712 |
+
#view-toggle{display:flex;gap:.2rem;}
|
| 713 |
+
.vt-btn{font-size:.62rem;padding:.24rem .45rem;border-radius:3px;cursor:pointer;
|
| 714 |
+
background:var(--s2);border:1px solid var(--bd2);color:var(--sub);}
|
| 715 |
+
.vt-btn.on{border-color:var(--acc);color:var(--acc);}
|
| 716 |
+
|
| 717 |
+
/* TERMINAL */
|
| 718 |
+
#terminal-wrap{flex:1;display:flex;flex-direction:column;overflow:hidden;display:none;}
|
| 719 |
+
#terminal-hdr{flex-shrink:0;display:flex;align-items:center;gap:.5rem;
|
| 720 |
+
padding:.4rem .9rem;border-bottom:1px solid var(--bd);background:var(--s1);}
|
| 721 |
+
.rt-btn{font-size:.56rem;padding:2px 7px;border-radius:3px;cursor:pointer;border:1px solid;
|
| 722 |
+
font-family:var(--font);}
|
| 723 |
+
.rt-btn.bash{color:#f0c040;border-color:rgba(240,192,64,.3);background:rgba(240,192,64,.05);}
|
| 724 |
+
.rt-btn.python{color:#0ea5e9;border-color:rgba(14,165,233,.3);background:rgba(14,165,233,.05);}
|
| 725 |
+
.rt-btn.node{color:#2ed573;border-color:rgba(46,213,115,.3);background:rgba(46,213,115,.05);}
|
| 726 |
+
.rt-btn.npm{color:#ff6b9d;border-color:rgba(255,107,157,.3);background:rgba(255,107,157,.05);}
|
| 727 |
+
.rt-btn.git{color:#7c3aed;border-color:rgba(124,58,237,.3);background:rgba(124,58,237,.05);}
|
| 728 |
+
.rt-btn.pip{color:#ff9500;border-color:rgba(255,149,0,.3);background:rgba(255,149,0,.05);}
|
| 729 |
+
.rt-btn.on{opacity:1;}.rt-btn:not(.on){opacity:.45;}
|
| 730 |
+
#term-output{flex:1;overflow-y:auto;background:var(--code-bg);padding:.75rem 1rem;font-family:var(--mono);
|
| 731 |
+
font-size:.69rem;line-height:1.62;color:var(--lo);}
|
| 732 |
+
#term-output::-webkit-scrollbar{width:3px;}
|
| 733 |
+
#term-output::-webkit-scrollbar-thumb{background:var(--bd2);}
|
| 734 |
+
.t-line{white-space:pre-wrap;word-break:break-word;margin-bottom:.08rem;}
|
| 735 |
+
.t-line.err{color:var(--cr);}.t-line.sys{color:var(--sub);}
|
| 736 |
+
.t-line.ok{color:var(--lo);}.t-line.warn{color:var(--warn);}
|
| 737 |
+
#term-input-row{flex-shrink:0;display:flex;align-items:center;gap:.5rem;
|
| 738 |
+
padding:.5rem .9rem;border-top:1px solid var(--bd);background:var(--s1);}
|
| 739 |
+
#term-cwd{font-size:.58rem;color:var(--acc);font-family:var(--mono);flex-shrink:0;max-width:120px;
|
| 740 |
+
overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
| 741 |
+
#term-prompt{font-size:.65rem;color:var(--lo);font-family:var(--mono);flex-shrink:0;}
|
| 742 |
+
#term-input{flex:1;background:transparent;border:none;outline:none;font-family:var(--mono);
|
| 743 |
+
font-size:.7rem;color:var(--txt);}
|
| 744 |
+
#btn-run-term{background:var(--lo);color:#000;border:none;padding:.32rem .65rem;
|
| 745 |
+
font-family:var(--font);font-size:.6rem;font-weight:700;border-radius:4px;cursor:pointer;
|
| 746 |
+
transition:all .1s;}
|
| 747 |
+
#btn-run-term:hover{background:#3eff88;}
|
| 748 |
+
|
| 749 |
+
/* VERSIONS PANEL */
|
| 750 |
+
#versions-panel{position:absolute;right:0;top:0;bottom:0;width:300px;background:var(--s1);
|
| 751 |
+
border-left:1px solid var(--bd);z-index:20;display:none;flex-direction:column;overflow:hidden;
|
| 752 |
+
animation:sldin .15s ease;}
|
| 753 |
+
@keyframes sldin{from{transform:translateX(20px);opacity:0}to{transform:none;opacity:1}}
|
| 754 |
+
#versions-panel.open{display:flex;}
|
| 755 |
+
#vp-hdr{flex-shrink:0;display:flex;align-items:center;justify-content:space-between;
|
| 756 |
+
padding:.55rem .8rem;border-bottom:1px solid var(--bd);font-size:.58rem;font-weight:700;color:var(--acc);}
|
| 757 |
+
#vp-close{background:none;border:none;color:var(--sub);cursor:pointer;font-size:.8rem;
|
| 758 |
+
width:22px;height:22px;border-radius:3px;display:flex;align-items:center;justify-content:center;}
|
| 759 |
+
#vp-close:hover{background:var(--bd2);color:var(--txt);}
|
| 760 |
+
#vp-scroll{flex:1;overflow-y:auto;padding:.5rem;}
|
| 761 |
+
.ver-item{background:var(--s2);border:1px solid var(--bd);border-radius:5px;padding:.5rem .65rem;
|
| 762 |
+
margin-bottom:.35rem;cursor:pointer;transition:all .1s;}
|
| 763 |
+
.ver-item:hover{border-color:var(--bd2);}
|
| 764 |
+
.ver-ts{font-size:.62rem;font-weight:700;color:var(--txt);}
|
| 765 |
+
.ver-meta{font-size:.52rem;color:var(--sub);display:flex;gap:.5rem;margin-top:.2rem;flex-wrap:wrap;}
|
| 766 |
+
.ver-sha{font-family:var(--mono);}.ver-size{}.ver-agent{}
|
| 767 |
+
.ver-acts{display:flex;gap:.3rem;margin-top:.4rem;}
|
| 768 |
+
.ver-btn{font-size:.52rem;background:var(--s1);border:1px solid var(--bd2);border-radius:3px;
|
| 769 |
+
padding:1px 7px;cursor:pointer;color:var(--sub);font-family:var(--font);}
|
| 770 |
+
.ver-btn:hover{color:var(--acc);border-color:var(--acc);}
|
| 771 |
+
|
| 772 |
+
/* DIFF VIEW */
|
| 773 |
+
#diff-view{position:absolute;inset:0;background:var(--code-bg);z-index:30;
|
| 774 |
+
display:none;flex-direction:column;overflow:hidden;}
|
| 775 |
+
#diff-view.open{display:flex;}
|
| 776 |
+
#diff-hdr{flex-shrink:0;display:flex;align-items:center;gap:.6rem;
|
| 777 |
+
padding:.5rem .9rem;border-bottom:1px solid var(--bd);background:var(--s1);}
|
| 778 |
+
#diff-title{font-size:.65rem;font-weight:700;color:var(--acc);flex:1;}
|
| 779 |
+
#diff-close{background:var(--s2);border:1px solid var(--bd2);color:var(--sub);padding:.28rem .6rem;
|
| 780 |
+
font-family:var(--font);font-size:.58rem;border-radius:4px;cursor:pointer;}
|
| 781 |
+
#diff-close:hover{color:var(--txt);}
|
| 782 |
+
#diff-content{flex:1;overflow:auto;padding:.8rem 1rem;font-family:var(--mono);font-size:.68rem;line-height:1.6;}
|
| 783 |
+
.d-add{color:var(--lo);background:rgba(46,213,115,.06);}
|
| 784 |
+
.d-rem{color:var(--cr);background:rgba(255,34,68,.06);}
|
| 785 |
+
.d-hdr{color:var(--info);}.d-ctx{color:var(--sub);}
|
| 786 |
+
|
| 787 |
+
/* MODAL */
|
| 788 |
+
#modal{display:none;position:fixed;inset:0;background:rgba(0,0,0,.8);z-index:100;
|
| 789 |
+
backdrop-filter:blur(4px);align-items:center;justify-content:center;}
|
| 790 |
+
#modal.open{display:flex;}
|
| 791 |
+
.mdl{background:var(--s1);border:1px solid var(--bd2);border-top:2px solid var(--acc);
|
| 792 |
+
border-radius:10px;padding:1.3rem;width:420px;max-width:97vw;animation:mdin .15s ease;}
|
| 793 |
+
@keyframes mdin{from{opacity:0;transform:scale(.96)}to{opacity:1;transform:none}}
|
| 794 |
+
.mdl-title{font-size:.78rem;font-weight:700;letter-spacing:2px;color:var(--acc);margin-bottom:.9rem;}
|
| 795 |
+
.mdl-close{position:absolute;top:.7rem;right:.7rem;background:none;border:none;color:var(--sub);
|
| 796 |
+
cursor:pointer;font-size:.8rem;width:24px;height:24px;border-radius:3px;
|
| 797 |
+
display:flex;align-items:center;justify-content:center;}
|
| 798 |
+
.mdl-close:hover{background:var(--bd2);color:var(--txt);}
|
| 799 |
+
.mfl{margin-bottom:.6rem;}
|
| 800 |
+
.mfl label{display:block;font-size:.48rem;color:var(--sub);text-transform:uppercase;
|
| 801 |
+
letter-spacing:.1em;margin-bottom:.2rem;}
|
| 802 |
+
.mfl input,.mfl select{width:100%;background:var(--s2);border:1px solid var(--bd2);border-radius:4px;
|
| 803 |
+
padding:.38rem .55rem;font-family:var(--font);font-size:.68rem;color:var(--txt);outline:none;
|
| 804 |
+
transition:border-color .12s;}
|
| 805 |
+
.mfl input:focus,.mfl select:focus{border-color:var(--acc);}
|
| 806 |
+
.mdl-actions{display:flex;gap:.4rem;margin-top:.8rem;}
|
| 807 |
+
.mdl-ok{flex:1;background:var(--acc);color:#000;border:none;padding:.44rem;
|
| 808 |
+
font-family:var(--font);font-size:.65rem;font-weight:700;letter-spacing:.1em;
|
| 809 |
+
text-transform:uppercase;border-radius:4px;cursor:pointer;}
|
| 810 |
+
.mdl-ok:hover{background:var(--acc2);}
|
| 811 |
+
.mdl-cancel{background:var(--s2);color:var(--sub);border:1px solid var(--bd2);padding:.44rem .9rem;
|
| 812 |
+
font-family:var(--font);font-size:.65rem;border-radius:4px;cursor:pointer;}
|
| 813 |
+
.mdl-cancel:hover{background:var(--bd2);}
|
| 814 |
+
|
| 815 |
+
/* TOAST */
|
| 816 |
+
#toasts{position:fixed;bottom:1rem;right:1rem;z-index:200;display:flex;flex-direction:column;gap:.3rem;}
|
| 817 |
+
.tst{background:var(--s1);border:1px solid var(--bd2);border-left:3px solid var(--acc);
|
| 818 |
+
padding:.38rem .72rem;font-size:.58rem;border-radius:5px;animation:tin .14s ease;color:var(--txt);}
|
| 819 |
+
.tst.err{border-left-color:var(--cr);}.tst.ok{border-left-color:var(--lo);}
|
| 820 |
+
.tst.warn{border-left-color:var(--warn);}
|
| 821 |
+
@keyframes tin{from{opacity:0;transform:translateX(10px)}to{opacity:1;transform:none}}
|
| 822 |
+
#mcp-hint{position:fixed;bottom:1rem;left:1rem;z-index:10;background:var(--s1);
|
| 823 |
+
border:1px solid var(--bd2);border-left:3px solid var(--acc2);border-radius:5px;
|
| 824 |
+
padding:.36rem .72rem;font-size:.51rem;color:var(--sub);}
|
| 825 |
+
#mcp-hint code{color:var(--acc2);}
|
| 826 |
+
</style>
|
| 827 |
+
</head>
|
| 828 |
+
<body>
|
| 829 |
+
|
| 830 |
+
<div id="hdr">
|
| 831 |
+
<div>
|
| 832 |
+
<div id="logo">VAULT</div>
|
| 833 |
+
<div id="logo-sub">Workspace Manager & Execution Env · ki-fusion-labs.de</div>
|
| 834 |
+
</div>
|
| 835 |
+
<div id="hdr-stats">
|
| 836 |
+
<div class="hs"><span class="hs-n" id="hs-files">β</span>FILES</div>
|
| 837 |
+
<div class="hs"><span class="hs-n" id="hs-size">β</span>SIZE</div>
|
| 838 |
+
<div class="hs"><span class="hs-n" id="hs-writes">β</span>WRITES</div>
|
| 839 |
+
<div class="hs"><span class="hs-n" id="hs-execs">β</span>EXECS</div>
|
| 840 |
+
</div>
|
| 841 |
+
<div class="hdr-actions">
|
| 842 |
+
<button class="hdr-btn" id="btn-new-file">+ File</button>
|
| 843 |
+
<button class="hdr-btn" id="btn-new-dir">+ Dir</button>
|
| 844 |
+
<button class="hdr-btn primary" id="btn-open-term">⏎ Terminal</button>
|
| 845 |
+
</div>
|
| 846 |
+
</div>
|
| 847 |
+
|
| 848 |
+
<div id="toolbar">
|
| 849 |
+
<div id="breadcrumb"><span class="bc-part" data-path="">workspace</span></div>
|
| 850 |
+
<div id="view-toggle">
|
| 851 |
+
<button class="vt-btn on" id="vt-list" title="List view">☰</button>
|
| 852 |
+
<button class="vt-btn" id="vt-grid" title="Grid view">☷</button>
|
| 853 |
+
</div>
|
| 854 |
+
<input type="text" id="search-input" placeholder="Search files...">
|
| 855 |
+
<button class="tb-btn" id="btn-search">🔍</button>
|
| 856 |
+
<button class="tb-btn" id="btn-refresh">↻</button>
|
| 857 |
+
</div>
|
| 858 |
+
|
| 859 |
+
<div id="main">
|
| 860 |
+
|
| 861 |
+
<!-- FILE TREE -->
|
| 862 |
+
<div id="tree">
|
| 863 |
+
<div id="tree-hdr"><span>EXPLORER</span><span style="color:var(--sub);font-size:.48rem">WORKSPACE</span></div>
|
| 864 |
+
<div id="tree-scroll"></div>
|
| 865 |
+
</div>
|
| 866 |
+
|
| 867 |
+
<!-- CONTENT -->
|
| 868 |
+
<div id="content">
|
| 869 |
+
<div id="tabs">
|
| 870 |
+
<div class="tab-item on" id="tab-browser" >📁 Browser</div>
|
| 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 -->
|
| 877 |
+
<div id="browser-view"></div>
|
| 878 |
+
|
| 879 |
+
<!-- EDITOR -->
|
| 880 |
+
<div id="editor-wrap" style="display:none;">
|
| 881 |
+
<div id="editor-toolbar">
|
| 882 |
+
<span id="file-path-display">No file open</span>
|
| 883 |
+
<select id="editor-runtime" style="background:var(--s2);border:1px solid var(--bd2);border-radius:4px;padding:.22rem .4rem;font-family:var(--font);font-size:.58rem;color:var(--txt);outline:none;">
|
| 884 |
+
<option value="bash">bash</option>
|
| 885 |
+
<option value="python3">python3</option>
|
| 886 |
+
<option value="node">node</option>
|
| 887 |
+
</select>
|
| 888 |
+
<button class="e-btn run" id="btn-run-file">▶ Run</button>
|
| 889 |
+
<button class="e-btn" id="btn-versions">↻ History</button>
|
| 890 |
+
<button class="e-btn" id="btn-diff">⇔ Diff</button>
|
| 891 |
+
<button class="e-btn save" id="btn-save">✔ Save</button>
|
| 892 |
+
</div>
|
| 893 |
+
<div id="editor-area">
|
| 894 |
+
<textarea id="editor" spellcheck="false" autocomplete="off" autocorrect="off"></textarea>
|
| 895 |
+
<div id="editor-placeholder">
|
| 896 |
+
<div class="big">📁</div>
|
| 897 |
+
<div class="msg">Click a file to open it</div>
|
| 898 |
+
</div>
|
| 899 |
+
</div>
|
| 900 |
+
</div>
|
| 901 |
+
|
| 902 |
+
<!-- TERMINAL -->
|
| 903 |
+
<div id="terminal-wrap">
|
| 904 |
+
<div id="terminal-hdr">
|
| 905 |
+
<span style="font-size:.56rem;color:var(--sub);font-weight:700;letter-spacing:.12em">RUNTIME:</span>
|
| 906 |
+
<button class="rt-btn bash on" data-rt="bash">bash</button>
|
| 907 |
+
<button class="rt-btn python" data-rt="python3">python</button>
|
| 908 |
+
<button class="rt-btn node" data-rt="node">node</button>
|
| 909 |
+
<button class="rt-btn npm" data-rt="npm">npm</button>
|
| 910 |
+
<button class="rt-btn git" data-rt="git">git</button>
|
| 911 |
+
<button class="rt-btn pip" data-rt="pip">pip</button>
|
| 912 |
+
<span style="flex:1"></span>
|
| 913 |
+
<button class="tb-btn" id="btn-clear-term">Clear</button>
|
| 914 |
+
</div>
|
| 915 |
+
<div id="term-output"></div>
|
| 916 |
+
<div id="term-input-row">
|
| 917 |
+
<span id="term-cwd">workspace/</span>
|
| 918 |
+
<span id="term-prompt">$</span>
|
| 919 |
+
<input type="text" id="term-input" placeholder="Enter command or code...">
|
| 920 |
+
<button id="btn-run-term">Run</button>
|
| 921 |
+
</div>
|
| 922 |
+
</div>
|
| 923 |
+
|
| 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">
|
| 930 |
+
<span>VERSION HISTORY</span>
|
| 931 |
+
<button id="vp-close">✕</button>
|
| 932 |
+
</div>
|
| 933 |
+
<div id="vp-scroll"></div>
|
| 934 |
+
</div>
|
| 935 |
+
|
| 936 |
+
<!-- DIFF VIEW -->
|
| 937 |
+
<div id="diff-view">
|
| 938 |
+
<div id="diff-hdr">
|
| 939 |
+
<span id="diff-title">DIFF</span>
|
| 940 |
+
<button id="diff-close">Close diff</button>
|
| 941 |
+
</div>
|
| 942 |
+
<div id="diff-content"></div>
|
| 943 |
+
</div>
|
| 944 |
+
|
| 945 |
+
</div>
|
| 946 |
+
</div>
|
| 947 |
+
|
| 948 |
+
<div id="modal" style="position:fixed">
|
| 949 |
+
<div class="mdl" style="position:relative">
|
| 950 |
+
<button class="mdl-close" id="mdl-close">✕</button>
|
| 951 |
+
<div class="mdl-title" id="mdl-title">NEW FILE</div>
|
| 952 |
+
<div class="mfl"><label id="mdl-label">Filename</label>
|
| 953 |
+
<input type="text" id="mdl-input" placeholder="path/to/file.py"></div>
|
| 954 |
+
<div class="mdl-actions">
|
| 955 |
+
<button class="mdl-ok" id="mdl-ok">Create</button>
|
| 956 |
+
<button class="mdl-cancel" id="mdl-cancel">Cancel</button>
|
| 957 |
+
</div>
|
| 958 |
+
</div>
|
| 959 |
+
</div>
|
| 960 |
+
|
| 961 |
+
<div id="toasts"></div>
|
| 962 |
+
<div id="mcp-hint">MCP: <code>vault_read</code> <code>vault_write</code> <code>vault_exec</code> | <code>GET /mcp/sse</code></div>
|
| 963 |
+
|
| 964 |
+
<script>
|
| 965 |
+
var CWD = '';
|
| 966 |
+
var OPEN_FILE = null;
|
| 967 |
+
var RUNTIME = 'bash';
|
| 968 |
+
var TERM_HISTORY = [];
|
| 969 |
+
var TERM_HI = 0;
|
| 970 |
+
var VIEW_MODE = 'list';
|
| 971 |
+
|
| 972 |
+
function esc(s){return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');}
|
| 973 |
+
function fmtSize(b){if(b<1024)return b+'B';if(b<1048576)return (b/1024).toFixed(1)+'KB';return (b/1048576).toFixed(1)+'MB';}
|
| 974 |
+
function fmtDate(ts){return new Date(ts*1000).toLocaleString('en-GB',{day:'2-digit',month:'2-digit',hour:'2-digit',minute:'2-digit'});}
|
| 975 |
+
function post(url,data){return fetch(url,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});}
|
| 976 |
+
function del(url,data){return fetch(url,{method:'DELETE',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});}
|
| 977 |
+
|
| 978 |
+
function toast(msg,type){
|
| 979 |
+
var el=document.createElement('div');el.className='tst'+(type?' '+type:'');
|
| 980 |
+
el.textContent=msg;document.getElementById('toasts').appendChild(el);
|
| 981 |
+
setTimeout(function(){el.remove();},2600);
|
| 982 |
+
}
|
| 983 |
+
|
| 984 |
+
var EXT_ICONS = {
|
| 985 |
+
'.py':'🐍','.js':'💪','.ts':'💪','.jsx':'💪','.tsx':'💪',
|
| 986 |
+
'.sh':'🐠','.bash':'🐠','.json':'📄','.md':'📖','.txt':'📄',
|
| 987 |
+
'.html':'🌐','.css':'🎨','.rs':'⚙','.go':'🔄',
|
| 988 |
+
'.yml':'🔥','.yaml':'🔥','.toml':'🔥','.env':'🔒',
|
| 989 |
+
'.sql':'📈','.csv':'📈','.lock':'🔒','.gitignore':'🤔',
|
| 990 |
+
};
|
| 991 |
+
var DIR_ICONS = {
|
| 992 |
+
'code':'💻','reports':'📊','scratch':'⚜','shared':'🔁',
|
| 993 |
+
};
|
| 994 |
+
function fileIcon(item){
|
| 995 |
+
if(item.type==='dir') return DIR_ICONS[item.name]||'📁';
|
| 996 |
+
return EXT_ICONS[item.ext]||'📄';
|
| 997 |
+
}
|
| 998 |
+
|
| 999 |
+
// ββ Stats ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1000 |
+
function loadStats(){
|
| 1001 |
+
fetch('/api/stats').then(function(r){return r.json();}).then(function(s){
|
| 1002 |
+
document.getElementById('hs-files').textContent=s.total_files;
|
| 1003 |
+
document.getElementById('hs-size').textContent=fmtSize(s.total_size);
|
| 1004 |
+
document.getElementById('hs-writes').textContent=s.total_writes;
|
| 1005 |
+
document.getElementById('hs-execs').textContent=s.total_execs;
|
| 1006 |
+
}).catch(function(){});
|
| 1007 |
+
}
|
| 1008 |
+
|
| 1009 |
+
// ββ File Tree βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1010 |
+
function loadTree(){
|
| 1011 |
+
fetch('/api/ls').then(function(r){return r.json();}).then(function(d){
|
| 1012 |
+
var scroll=document.getElementById('tree-scroll');
|
| 1013 |
+
scroll.innerHTML='';
|
| 1014 |
+
var top=['code','reports','scratch','shared'];
|
| 1015 |
+
function addItems(items, depth){
|
| 1016 |
+
items.forEach(function(item){
|
| 1017 |
+
var el=document.createElement('div');
|
| 1018 |
+
el.className='ti ti-depth-'+depth+(OPEN_FILE===item.path?' active':'');
|
| 1019 |
+
el.innerHTML='<span class="ti-icon">'+fileIcon(item)+'</span>'
|
| 1020 |
+
+'<span class="ti-name" title="'+esc(item.name)+'">'+esc(item.name)+'</span>';
|
| 1021 |
+
el.addEventListener('click',function(e){e.stopPropagation();
|
| 1022 |
+
if(item.type==='dir'){
|
| 1023 |
+
CWD=item.path;loadBrowser(item.path);showTab('browser');buildBreadcrumb(item.path);
|
| 1024 |
+
} else { openFile(item.path); }
|
| 1025 |
+
});
|
| 1026 |
+
scroll.appendChild(el);
|
| 1027 |
+
});
|
| 1028 |
+
}
|
| 1029 |
+
// Sort: dirs first
|
| 1030 |
+
var dirs=d.items.filter(function(x){return x.type==='dir';});
|
| 1031 |
+
var files=d.items.filter(function(x){return x.type==='file';});
|
| 1032 |
+
addItems(dirs, 1);
|
| 1033 |
+
addItems(files, 1);
|
| 1034 |
+
}).catch(function(){});
|
| 1035 |
+
}
|
| 1036 |
+
|
| 1037 |
+
// ββ Browser βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1038 |
+
function loadBrowser(path){
|
| 1039 |
+
CWD = path || '';
|
| 1040 |
+
fetch('/api/ls?path='+encodeURIComponent(path||'')).then(function(r){return r.json();}).then(function(d){
|
| 1041 |
+
renderBrowser(d.items || []);
|
| 1042 |
+
buildBreadcrumb(path);
|
| 1043 |
+
}).catch(function(){toast('Error loading dir','err');});
|
| 1044 |
+
}
|
| 1045 |
+
|
| 1046 |
+
function buildBreadcrumb(path){
|
| 1047 |
+
var bc=document.getElementById('breadcrumb');
|
| 1048 |
+
var parts=['workspace'].concat((path||'').split('/').filter(Boolean));
|
| 1049 |
+
var paths=[''];
|
| 1050 |
+
(path||'').split('/').filter(Boolean).forEach(function(p,i,arr){
|
| 1051 |
+
paths.push(arr.slice(0,i+1).join('/'));
|
| 1052 |
+
});
|
| 1053 |
+
bc.innerHTML=parts.map(function(p,i){
|
| 1054 |
+
return '<span class="bc-part'+(i===parts.length-1?' cur':'')+'" data-path="'
|
| 1055 |
+
+esc(paths[i])+'">'+esc(p)+'</span>'+(i<parts.length-1?'<span class="bc-sep">/</span>':'');
|
| 1056 |
+
}).join('');
|
| 1057 |
+
bc.querySelectorAll('.bc-part').forEach(function(el){
|
| 1058 |
+
el.addEventListener('click',function(){loadBrowser(this.getAttribute('data-path'));showTab('browser');});
|
| 1059 |
+
});
|
| 1060 |
+
}
|
| 1061 |
+
|
| 1062 |
+
function renderBrowser(items){
|
| 1063 |
+
var view=document.getElementById('browser-view');
|
| 1064 |
+
view.style.display='block';
|
| 1065 |
+
if(!items.length){
|
| 1066 |
+
view.innerHTML='<div style="text-align:center;padding:2.5rem;font-size:.62rem;color:var(--sub)">Empty directory</div>';
|
| 1067 |
+
return;
|
| 1068 |
+
}
|
| 1069 |
+
if(VIEW_MODE==='grid'){
|
| 1070 |
+
var html='<div class="file-grid">';
|
| 1071 |
+
items.forEach(function(item){
|
| 1072 |
+
html+='<div class="fg-item" data-path="'+esc(item.path)+'" data-type="'+esc(item.type)+'">'
|
| 1073 |
+
+'<div class="fg-icon">'+fileIcon(item)+'</div>'
|
| 1074 |
+
+'<div class="fg-name">'+esc(item.name)+'</div>'
|
| 1075 |
+
+'<div class="fg-meta">'+(item.type==='file'?fmtSize(item.size):item.type)+'</div>'
|
| 1076 |
+
+'</div>';
|
| 1077 |
+
});
|
| 1078 |
+
html+='</div>';
|
| 1079 |
+
view.innerHTML=html;
|
| 1080 |
+
} else {
|
| 1081 |
+
var html='<div style="padding:.2rem 0">';
|
| 1082 |
+
items.forEach(function(item){
|
| 1083 |
+
html+='<div class="list-item" data-path="'+esc(item.path)+'" data-type="'+esc(item.type)+'">'
|
| 1084 |
+
+'<span class="li-icon">'+fileIcon(item)+'</span>'
|
| 1085 |
+
+'<span class="li-name">'+esc(item.name)+'</span>'
|
| 1086 |
+
+'<span class="li-size">'+(item.type==='file'?fmtSize(item.size):'')+'</span>'
|
| 1087 |
+
+'<span class="li-date">'+fmtDate(item.modified)+'</span>'
|
| 1088 |
+
+'<span class="li-actions">'
|
| 1089 |
+
+(item.type==='file'?'<span class="li-act" data-action="edit">edit</span>':'')
|
| 1090 |
+
+'<span class="li-act" data-action="rename">mv</span>'
|
| 1091 |
+
+'<span class="li-act danger" data-action="delete">rm</span>'
|
| 1092 |
+
+'</span></div>';
|
| 1093 |
+
});
|
| 1094 |
+
html+='</div>';
|
| 1095 |
+
view.innerHTML=html;
|
| 1096 |
+
}
|
| 1097 |
+
// Attach events
|
| 1098 |
+
view.querySelectorAll('[data-path]').forEach(function(el){
|
| 1099 |
+
el.addEventListener('click',function(e){
|
| 1100 |
+
var act=e.target.getAttribute('data-action');
|
| 1101 |
+
var p=this.getAttribute('data-path');
|
| 1102 |
+
var t=this.getAttribute('data-type')||el.getAttribute('data-type');
|
| 1103 |
+
if(act==='edit'||(!act&&t==='file')){openFile(p);}
|
| 1104 |
+
else if(!act&&t==='dir'){loadBrowser(p);showTab('browser');}
|
| 1105 |
+
else if(act==='rename'){renameDialog(p);}
|
| 1106 |
+
else if(act==='delete'){deleteItem(p);}
|
| 1107 |
+
});
|
| 1108 |
+
});
|
| 1109 |
+
}
|
| 1110 |
+
|
| 1111 |
+
// ββ File open / editor ββββββββββββββββββββββββββββββββββββββββββββ
|
| 1112 |
+
function openFile(path){
|
| 1113 |
+
OPEN_FILE=path;
|
| 1114 |
+
showTab('editor');
|
| 1115 |
+
document.getElementById('editor-placeholder').style.display='none';
|
| 1116 |
+
document.getElementById('file-path-display').textContent=path;
|
| 1117 |
+
// Set runtime based on extension
|
| 1118 |
+
var ext=(path.split('.').pop()||'').toLowerCase();
|
| 1119 |
+
var rtMap={py:'python3',js:'node',sh:'bash',bash:'bash',ts:'node'};
|
| 1120 |
+
document.getElementById('editor-runtime').value=rtMap[ext]||'bash';
|
| 1121 |
+
fetch('/api/read?path='+encodeURIComponent(path)).then(function(r){return r.json();}).then(function(d){
|
| 1122 |
+
document.getElementById('editor').value=d.content||'';
|
| 1123 |
+
// Update tree active
|
| 1124 |
+
document.querySelectorAll('.ti').forEach(function(el){
|
| 1125 |
+
el.classList.toggle('active',false);
|
| 1126 |
+
});
|
| 1127 |
+
loadTree();
|
| 1128 |
+
toast('Opened: '+path.split('/').pop());
|
| 1129 |
+
}).catch(function(){toast('Error opening file','err');});
|
| 1130 |
+
}
|
| 1131 |
+
|
| 1132 |
+
document.getElementById('btn-save').addEventListener('click',function(){
|
| 1133 |
+
if(!OPEN_FILE){toast('No file open','warn');return;}
|
| 1134 |
+
var content=document.getElementById('editor').value;
|
| 1135 |
+
post('/api/write',{path:OPEN_FILE,content:content,agent:'vault-ui'}).then(function(r){return r.json();}).then(function(d){
|
| 1136 |
+
toast('Saved: '+OPEN_FILE.split('/').pop(),(d.new_file?'ok':'ok'));
|
| 1137 |
+
loadStats();loadTree();
|
| 1138 |
+
}).catch(function(){toast('Save failed','err');});
|
| 1139 |
+
});
|
| 1140 |
+
|
| 1141 |
+
document.getElementById('btn-run-file').addEventListener('click',function(){
|
| 1142 |
+
if(!OPEN_FILE){toast('No file open','warn');return;}
|
| 1143 |
+
var content=document.getElementById('editor').value;
|
| 1144 |
+
var runtime=document.getElementById('editor-runtime').value;
|
| 1145 |
+
showTab('terminal');
|
| 1146 |
+
document.getElementById('term-cwd').textContent=OPEN_FILE;
|
| 1147 |
+
termPrint('[vault] Running '+OPEN_FILE+' as '+runtime+'...','sys');
|
| 1148 |
+
post('/api/exec',{runtime:runtime,code:content,cwd:CWD}).then(function(r){return r.json();}).then(function(d){
|
| 1149 |
+
(d.output||'').split('\n').forEach(function(line){
|
| 1150 |
+
termPrint(line, d.ok?'ok':'err');
|
| 1151 |
+
});
|
| 1152 |
+
termPrint('[vault] exit:'+d.exit_code+' ('+d.ms+'ms)','sys');
|
| 1153 |
+
loadStats();
|
| 1154 |
+
}).catch(function(){toast('Exec error','err');});
|
| 1155 |
+
});
|
| 1156 |
+
|
| 1157 |
+
// ββ Versions ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1158 |
+
document.getElementById('btn-versions').addEventListener('click',function(){
|
| 1159 |
+
if(!OPEN_FILE){toast('No file open','warn');return;}
|
| 1160 |
+
var panel=document.getElementById('versions-panel');
|
| 1161 |
+
panel.classList.toggle('open');
|
| 1162 |
+
if(!panel.classList.contains('open')) return;
|
| 1163 |
+
fetch('/api/versions?path='+encodeURIComponent(OPEN_FILE)).then(function(r){return r.json();}).then(function(d){
|
| 1164 |
+
var scroll=document.getElementById('vp-scroll');
|
| 1165 |
+
if(!d.versions.length){scroll.innerHTML='<div style="font-size:.6rem;color:var(--sub);padding:1rem">No versions yet</div>';return;}
|
| 1166 |
+
scroll.innerHTML=d.versions.map(function(v){
|
| 1167 |
+
return '<div class="ver-item">'
|
| 1168 |
+
+'<div class="ver-ts">'+fmtDate(v.ts)+'</div>'
|
| 1169 |
+
+'<div class="ver-meta">'
|
| 1170 |
+
+'<span class="ver-sha">🔒 '+esc(v.sha)+'</span>'
|
| 1171 |
+
+'<span class="ver-size">'+fmtSize(v.size||0)+'</span>'
|
| 1172 |
+
+(v.agent?'<span class="ver-agent">@'+esc(v.agent)+'</span>':'')
|
| 1173 |
+
+'</div>'
|
| 1174 |
+
+'<div class="ver-acts">'
|
| 1175 |
+
+'<button class="ver-btn" data-vid="'+esc(v.id)+'" data-action="restore">Restore</button>'
|
| 1176 |
+
+'<button class="ver-btn" data-vid="'+esc(v.id)+'" data-action="diff">Diff</button>'
|
| 1177 |
+
+'</div></div>';
|
| 1178 |
+
}).join('');
|
| 1179 |
+
scroll.querySelectorAll('[data-vid]').forEach(function(btn){
|
| 1180 |
+
btn.addEventListener('click',function(){
|
| 1181 |
+
var vid=this.getAttribute('data-vid');
|
| 1182 |
+
var act=this.getAttribute('data-action');
|
| 1183 |
+
if(act==='restore'){
|
| 1184 |
+
fetch('/api/version?path='+encodeURIComponent(OPEN_FILE)+'&vid='+encodeURIComponent(vid))
|
| 1185 |
+
.then(function(r){return r.json();}).then(function(d){
|
| 1186 |
+
document.getElementById('editor').value=d.content;
|
| 1187 |
+
toast('Restored version '+vid.substring(0,8),'ok');
|
| 1188 |
+
});
|
| 1189 |
+
} else {
|
| 1190 |
+
fetch('/api/diff?path='+encodeURIComponent(OPEN_FILE)+'&from_vid='+encodeURIComponent(vid))
|
| 1191 |
+
.then(function(r){return r.json();}).then(function(d){showDiff(d.diff,OPEN_FILE+' vs v'+vid.substring(0,8));});
|
| 1192 |
+
}
|
| 1193 |
+
});
|
| 1194 |
+
});
|
| 1195 |
+
});
|
| 1196 |
+
});
|
| 1197 |
+
|
| 1198 |
+
document.getElementById('vp-close').addEventListener('click',function(){
|
| 1199 |
+
document.getElementById('versions-panel').classList.remove('open');
|
| 1200 |
+
});
|
| 1201 |
+
|
| 1202 |
+
document.getElementById('btn-diff').addEventListener('click',function(){
|
| 1203 |
+
if(!OPEN_FILE){toast('No file open','warn');return;}
|
| 1204 |
+
fetch('/api/diff?path='+encodeURIComponent(OPEN_FILE)).then(function(r){return r.json();}).then(function(d){
|
| 1205 |
+
showDiff(d.diff,'Diff: '+OPEN_FILE);
|
| 1206 |
+
});
|
| 1207 |
+
});
|
| 1208 |
+
|
| 1209 |
+
function showDiff(diff,title){
|
| 1210 |
+
var view=document.getElementById('diff-view');
|
| 1211 |
+
view.classList.add('open');
|
| 1212 |
+
document.getElementById('diff-title').textContent=title||'DIFF';
|
| 1213 |
+
var content=document.getElementById('diff-content');
|
| 1214 |
+
if(!diff||!diff.trim()){
|
| 1215 |
+
content.innerHTML='<span style="color:var(--sub)">No changes from last version.</span>';
|
| 1216 |
+
return;
|
| 1217 |
+
}
|
| 1218 |
+
content.innerHTML=diff.split('\n').map(function(line){
|
| 1219 |
+
var cls='d-ctx';
|
| 1220 |
+
if(line.startsWith('+')&&!line.startsWith('+++')){cls='d-add';}
|
| 1221 |
+
else if(line.startsWith('-')&&!line.startsWith('---')){cls='d-rem';}
|
| 1222 |
+
else if(line.startsWith('@@')){cls='d-hdr';}
|
| 1223 |
+
else if(line.startsWith('+++')||line.startsWith('---')){cls='d-hdr';}
|
| 1224 |
+
return '<div class="t-line '+cls+'">'+esc(line)+'</div>';
|
| 1225 |
+
}).join('');
|
| 1226 |
+
}
|
| 1227 |
+
document.getElementById('diff-close').addEventListener('click',function(){
|
| 1228 |
+
document.getElementById('diff-view').classList.remove('open');
|
| 1229 |
+
});
|
| 1230 |
+
|
| 1231 |
+
// ββ Terminal ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1232 |
+
function termPrint(line,cls){
|
| 1233 |
+
var out=document.getElementById('term-output');
|
| 1234 |
+
var el=document.createElement('div');
|
| 1235 |
+
el.className='t-line'+(cls?' '+cls:'');
|
| 1236 |
+
el.textContent=line;
|
| 1237 |
+
out.appendChild(el);
|
| 1238 |
+
out.scrollTop=out.scrollHeight;
|
| 1239 |
+
}
|
| 1240 |
+
function termClear(){document.getElementById('term-output').innerHTML='';}
|
| 1241 |
+
|
| 1242 |
+
document.querySelectorAll('.rt-btn[data-rt]').forEach(function(btn){
|
| 1243 |
+
btn.addEventListener('click',function(){
|
| 1244 |
+
RUNTIME=this.getAttribute('data-rt');
|
| 1245 |
+
document.querySelectorAll('.rt-btn[data-rt]').forEach(function(b){b.classList.remove('on');});
|
| 1246 |
+
this.classList.add('on');
|
| 1247 |
+
document.getElementById('term-prompt').textContent=RUNTIME==='bash'?'$':RUNTIME+'>';
|
| 1248 |
+
});
|
| 1249 |
+
});
|
| 1250 |
+
|
| 1251 |
+
document.getElementById('btn-clear-term').addEventListener('click',termClear);
|
| 1252 |
+
|
| 1253 |
+
function runTerminal(){
|
| 1254 |
+
var input=document.getElementById('term-input');
|
| 1255 |
+
var code=input.value.trim();
|
| 1256 |
+
if(!code) return;
|
| 1257 |
+
TERM_HISTORY.unshift(code); TERM_HI=0;
|
| 1258 |
+
input.value='';
|
| 1259 |
+
termPrint('['+RUNTIME+'] $ '+code,'sys');
|
| 1260 |
+
// Use SSE streaming for better UX
|
| 1261 |
+
var url='/api/exec/stream?runtime='+encodeURIComponent(RUNTIME)+'&code='+encodeURIComponent(code)+'&cwd='+encodeURIComponent(CWD);
|
| 1262 |
+
var src=new EventSource(url);
|
| 1263 |
+
src.onmessage=function(e){
|
| 1264 |
+
try{
|
| 1265 |
+
var d=JSON.parse(e.data);
|
| 1266 |
+
if(d.type==='start'){termPrint('cmd: '+d.cmd,'sys');}
|
| 1267 |
+
else if(d.type==='output'){if(d.line!==undefined)termPrint(d.line);}
|
| 1268 |
+
else if(d.type==='done'){
|
| 1269 |
+
termPrint('[exit '+d.exit_code+']'+(d.exit_code===0?'ok':'err'));
|
| 1270 |
+
src.close();loadStats();
|
| 1271 |
+
}
|
| 1272 |
+
else if(d.type==='error'){termPrint('[error] '+d.message,'err');src.close();}
|
| 1273 |
+
}catch(err){}
|
| 1274 |
+
};
|
| 1275 |
+
src.onerror=function(){src.close();};
|
| 1276 |
+
}
|
| 1277 |
+
|
| 1278 |
+
document.getElementById('btn-run-term').addEventListener('click',runTerminal);
|
| 1279 |
+
document.getElementById('term-input').addEventListener('keydown',function(e){
|
| 1280 |
+
if(e.key==='Enter'){runTerminal();}
|
| 1281 |
+
else if(e.key==='ArrowUp'){if(TERM_HI<TERM_HISTORY.length){this.value=TERM_HISTORY[TERM_HI++];}}
|
| 1282 |
+
else if(e.key==='ArrowDown'){if(TERM_HI>0){this.value=TERM_HISTORY[--TERM_HI]||'';}else this.value='';}
|
| 1283 |
+
});
|
| 1284 |
+
|
| 1285 |
+
// ββ Exec Log ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1286 |
+
function loadExecLog(){
|
| 1287 |
+
fetch('/api/exec/log?limit=50').then(function(r){return r.json();}).then(function(d){
|
| 1288 |
+
var view=document.getElementById('exec-log-view');
|
| 1289 |
+
if(!d.log.length){view.innerHTML='<div style="padding:2rem;text-align:center;font-size:.6rem;color:var(--sub)">No executions yet</div>';return;}
|
| 1290 |
+
view.innerHTML=d.log.map(function(e){
|
| 1291 |
+
var col=e.ok?'var(--lo)':'var(--cr)';
|
| 1292 |
+
return '<div style="background:var(--s1);border:1px solid var(--bd);border-radius:6px;padding:.55rem .75rem;margin-bottom:.35rem;">'
|
| 1293 |
+
+'<div style="display:flex;align-items:center;gap:.4rem;margin-bottom:.25rem;flex-wrap:wrap">'
|
| 1294 |
+
+'<span style="font-size:.5rem;padding:1px 5px;border-radius:3px;background:'+col+'18;color:'+col+';border:1px solid '+col+'33">'+esc(e.runtime)+'</span>'
|
| 1295 |
+
+'<span style="font-size:.5rem;color:'+col+';">'+(e.ok?'OK':'FAIL')+'</span>'
|
| 1296 |
+
+'<span style="font-size:.5rem;color:var(--sub);">'+e.ms+'ms</span>'
|
| 1297 |
+
+'<span style="font-size:.48rem;color:var(--dim);margin-left:auto">'+fmtDate(e.ts)+'</span>'
|
| 1298 |
+
+'</div>'
|
| 1299 |
+
+'<div style="font-family:var(--mono);font-size:.62rem;color:var(--sub)">'+esc(e.cmd)+'</div>'
|
| 1300 |
+
+'</div>';
|
| 1301 |
+
}).join('');
|
| 1302 |
+
});
|
| 1303 |
+
}
|
| 1304 |
+
|
| 1305 |
+
// ββ Tabs ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1306 |
+
function showTab(t){
|
| 1307 |
+
document.getElementById('tab-browser').className='tab-item'+(t==='browser'?' on':'');
|
| 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(){
|
| 1325 |
+
VIEW_MODE='list';
|
| 1326 |
+
this.className='vt-btn on';document.getElementById('vt-grid').className='vt-btn';
|
| 1327 |
+
loadBrowser(CWD);
|
| 1328 |
+
});
|
| 1329 |
+
document.getElementById('vt-grid').addEventListener('click',function(){
|
| 1330 |
+
VIEW_MODE='grid';
|
| 1331 |
+
this.className='vt-btn on';document.getElementById('vt-list').className='vt-btn';
|
| 1332 |
+
loadBrowser(CWD);
|
| 1333 |
+
});
|
| 1334 |
+
|
| 1335 |
+
// ββ Search ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1336 |
+
document.getElementById('btn-search').addEventListener('click',function(){doSearch();});
|
| 1337 |
+
document.getElementById('search-input').addEventListener('keydown',function(e){if(e.key==='Enter')doSearch();});
|
| 1338 |
+
function doSearch(){
|
| 1339 |
+
var q=document.getElementById('search-input').value.trim();
|
| 1340 |
+
if(!q){loadBrowser(CWD);return;}
|
| 1341 |
+
fetch('/api/search?q='+encodeURIComponent(q)+'&scope='+encodeURIComponent(CWD)).then(function(r){return r.json();}).then(function(d){
|
| 1342 |
+
var view=document.getElementById('browser-view');
|
| 1343 |
+
view.style.display='block';
|
| 1344 |
+
showTab('browser');
|
| 1345 |
+
if(!d.results.length){view.innerHTML='<div style="padding:2rem;text-align:center;font-size:.6rem;color:var(--sub)">No results for "'+esc(q)+'"</div>';return;}
|
| 1346 |
+
var html='<div style="font-size:.54rem;color:var(--sub);padding:.3rem .3rem .6rem;font-weight:700;letter-spacing:.1em">'+d.results.length+' RESULTS FOR "'+esc(q)+'"</div>';
|
| 1347 |
+
html+=d.results.map(function(r){
|
| 1348 |
+
return '<div class="list-item" data-path="'+esc(r.path)+'" data-type="file">'
|
| 1349 |
+
+'<span class="li-icon">'+fileIcon(r)+'</span>'
|
| 1350 |
+
+'<span class="li-name" style="flex-direction:column;align-items:flex-start">'
|
| 1351 |
+
+'<span>'+esc(r.path)+'</span>'
|
| 1352 |
+
+(r.snippet?'<span style="font-size:.5rem;color:var(--sub);margin-top:.1rem">...'+esc(r.snippet)+'...</span>':'')
|
| 1353 |
+
+'</span>'
|
| 1354 |
+
+'<span class="li-size">'+fmtSize(r.size)+'</span>'
|
| 1355 |
+
+'</div>';
|
| 1356 |
+
}).join('');
|
| 1357 |
+
view.innerHTML=html;
|
| 1358 |
+
view.querySelectorAll('[data-path]').forEach(function(el){
|
| 1359 |
+
el.addEventListener('click',function(){openFile(this.getAttribute('data-path'));});
|
| 1360 |
+
});
|
| 1361 |
+
});
|
| 1362 |
+
}
|
| 1363 |
+
|
| 1364 |
+
// ββ New file / dir dialogs βββββββββββββββββββββββββββββββββββββββββ
|
| 1365 |
+
var MODAL_MODE = 'file';
|
| 1366 |
+
function openModal(mode,title,placeholder,prefill){
|
| 1367 |
+
MODAL_MODE=mode;
|
| 1368 |
+
document.getElementById('mdl-title').textContent=title||'';
|
| 1369 |
+
document.getElementById('mdl-label').textContent=mode==='dir'?'Directory path':'File path';
|
| 1370 |
+
document.getElementById('mdl-input').placeholder=placeholder||'';
|
| 1371 |
+
document.getElementById('mdl-input').value=prefill||(CWD?(CWD+'/'):'');
|
| 1372 |
+
document.getElementById('modal').classList.add('open');
|
| 1373 |
+
setTimeout(function(){document.getElementById('mdl-input').focus();},80);
|
| 1374 |
+
}
|
| 1375 |
+
function closeModal(){document.getElementById('modal').classList.remove('open');}
|
| 1376 |
+
|
| 1377 |
+
document.getElementById('btn-new-file').addEventListener('click',function(){openModal('file','NEW FILE','code/script.py');});
|
| 1378 |
+
document.getElementById('btn-new-dir').addEventListener('click',function(){openModal('dir','NEW DIRECTORY','scratch/experiments');});
|
| 1379 |
+
document.getElementById('mdl-close').addEventListener('click',closeModal);
|
| 1380 |
+
document.getElementById('mdl-cancel').addEventListener('click',closeModal);
|
| 1381 |
+
document.getElementById('modal').addEventListener('click',function(e){if(e.target===this)closeModal();});
|
| 1382 |
+
document.getElementById('mdl-ok').addEventListener('click',function(){
|
| 1383 |
+
var val=document.getElementById('mdl-input').value.trim();
|
| 1384 |
+
if(!val)return;
|
| 1385 |
+
if(MODAL_MODE==='dir'){
|
| 1386 |
+
post('/api/mkdir',{path:val}).then(function(){toast('Created: '+val,'ok');closeModal();loadBrowser(CWD);loadTree();});
|
| 1387 |
+
} else if(MODAL_MODE==='rename'){
|
| 1388 |
+
post('/api/move',{src:val.split('->')[0].trim(),dst:val.split('->')[1].trim()}).then(function(){toast('Moved','ok');closeModal();loadBrowser(CWD);loadTree();});
|
| 1389 |
+
} else {
|
| 1390 |
+
post('/api/write',{path:val,content:'',agent:'vault-ui'}).then(function(){
|
| 1391 |
+
closeModal();loadBrowser(CWD);loadTree();openFile(val);
|
| 1392 |
+
});
|
| 1393 |
+
}
|
| 1394 |
+
});
|
| 1395 |
+
|
| 1396 |
+
function renameDialog(path){
|
| 1397 |
+
MODAL_MODE='rename';
|
| 1398 |
+
document.getElementById('mdl-title').textContent='RENAME / MOVE';
|
| 1399 |
+
document.getElementById('mdl-label').textContent='src -> dst';
|
| 1400 |
+
document.getElementById('mdl-input').value=path+' -> '+path;
|
| 1401 |
+
document.getElementById('modal').classList.add('open');
|
| 1402 |
+
setTimeout(function(){document.getElementById('mdl-input').focus();},80);
|
| 1403 |
+
}
|
| 1404 |
+
|
| 1405 |
+
function deleteItem(path){
|
| 1406 |
+
if(!confirm('Delete '+path+'?'))return;
|
| 1407 |
+
del('/api/delete',{path:path}).then(function(){
|
| 1408 |
+
toast('Deleted: '+path,'ok');loadBrowser(CWD);loadTree();loadStats();
|
| 1409 |
+
if(OPEN_FILE===path){OPEN_FILE=null;document.getElementById('file-path-display').textContent='No file open';}
|
| 1410 |
+
}).catch(function(){toast('Delete failed','err');});
|
| 1411 |
+
}
|
| 1412 |
+
|
| 1413 |
+
// ββ Refresh βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1414 |
+
document.getElementById('btn-refresh').addEventListener('click',function(){loadBrowser(CWD);loadTree();loadStats();});
|
| 1415 |
+
|
| 1416 |
+
// ββ Keyboard shortcuts βββββββββββββββββββββββββββββββββββββββββββββ
|
| 1417 |
+
document.addEventListener('keydown',function(e){
|
| 1418 |
+
if(e.key==='Escape'){closeModal();document.getElementById('diff-view').classList.remove('open');document.getElementById('versions-panel').classList.remove('open');}
|
| 1419 |
+
var typing=document.activeElement&&['INPUT','TEXTAREA','SELECT'].includes(document.activeElement.tagName);
|
| 1420 |
+
if((e.ctrlKey||e.metaKey)&&e.key==='s'&&!typing){e.preventDefault();document.getElementById('btn-save').click();}
|
| 1421 |
+
if(e.key==='F5'&&!typing){e.preventDefault();document.getElementById('btn-refresh').click();}
|
| 1422 |
+
});
|
| 1423 |
+
|
| 1424 |
+
// ββ Init ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1425 |
+
loadStats();
|
| 1426 |
+
loadTree();
|
| 1427 |
+
loadBrowser('');
|
| 1428 |
+
</script>
|
| 1429 |
+
</body>
|
| 1430 |
+
</html>"""
|
requirements.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi>=0.111.0
|
| 2 |
+
uvicorn>=0.30.0
|
| 3 |
+
python-multipart>=0.0.9
|