Chris4K commited on
Commit
2b40cbb
Β·
verified Β·
1 Parent(s): 7b3a4bf

Upload 4 files

Browse files
Files changed (4) hide show
  1. Dockerfile +22 -0
  2. README.md +121 -8
  3. main.py +1430 -0
  4. 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: gray
5
- colorTo: gray
6
- sdk: gradio
7
- sdk_version: 6.9.0
8
- app_file: app.py
9
  pinned: false
10
- short_description: agent-vault
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 &mdash; 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 &amp; Execution Env &middot; 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">&#9166; 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">&#9776;</button>
852
+ <button class="vt-btn" id="vt-grid" title="Grid view">&#9783;</button>
853
+ </div>
854
+ <input type="text" id="search-input" placeholder="Search files...">
855
+ <button class="tb-btn" id="btn-search">&#128269;</button>
856
+ <button class="tb-btn" id="btn-refresh">&#8635;</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" >&#128193; Browser</div>
871
+ <div class="tab-item" id="tab-editor" >&#9998; Editor</div>
872
+ <div class="tab-item" id="tab-terminal">&#62; Terminal</div>
873
+ <div class="tab-item" id="tab-exec-log">&#9881; 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">&#9654; Run</button>
889
+ <button class="e-btn" id="btn-versions">&#8635; History</button>
890
+ <button class="e-btn" id="btn-diff">&#8660; Diff</button>
891
+ <button class="e-btn save" id="btn-save">&#x2714; 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">&#128193;</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">&#x2715;</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">&#x2715;</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> &nbsp;|&nbsp; <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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
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':'&#128013;','.js':'&#128170;','.ts':'&#128170;','.jsx':'&#128170;','.tsx':'&#128170;',
986
+ '.sh':'&#128032;','.bash':'&#128032;','.json':'&#128196;','.md':'&#128214;','.txt':'&#128196;',
987
+ '.html':'&#127760;','.css':'&#127912;','.rs':'&#9881;','.go':'&#128260;',
988
+ '.yml':'&#128293;','.yaml':'&#128293;','.toml':'&#128293;','.env':'&#128274;',
989
+ '.sql':'&#128200;','.csv':'&#128200;','.lock':'&#128274;','.gitignore':'&#129300;',
990
+ };
991
+ var DIR_ICONS = {
992
+ 'code':'&#128187;','reports':'&#128202;','scratch':'&#9884;','shared':'&#128257;',
993
+ };
994
+ function fileIcon(item){
995
+ if(item.type==='dir') return DIR_ICONS[item.name]||'&#128193;';
996
+ return EXT_ICONS[item.ext]||'&#128196;';
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">&#128274; '+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