-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathclaude_code_view.py
More file actions
249 lines (205 loc) · 9.03 KB
/
Copy pathclaude_code_view.py
File metadata and controls
249 lines (205 loc) · 9.03 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
#!/usr/bin/env python3
"""Claude Code View — live dashboard for visualizing Claude Code projects and local models.
Starts a local HTTP server that serves a real-time dashboard. The browser polls
for fresh data, so changes to your Claude Code config are reflected automatically.
Usage:
python3 claude_code_view.py # reads config.json
python3 claude_code_view.py --folders ~/a ~/b # override folders
python3 claude_code_view.py --static -o out.html # one-shot HTML file
"""
import argparse
import json
import os
import sys
import webbrowser
from functools import partial
from http.server import HTTPServer, BaseHTTPRequestHandler
from pathlib import Path
from scanners import scan_all
# ---------------------------------------------------------------------------
# Config
# ---------------------------------------------------------------------------
DEFAULT_PORT = 7878
DEFAULT_REFRESH = 30
CONFIG_FILE = "config.json"
EXAMPLE_CONFIG_FILE = "config.example.json"
DASHBOARD_FILE = "dashboard.html"
def _script_dir() -> Path:
"""Directory where this script lives."""
return Path(__file__).resolve().parent
def load_config(config_path: Path | None = None) -> dict:
"""Load config.json from the script directory."""
path = config_path or (_script_dir() / CONFIG_FILE)
if not path.is_file():
return {}
try:
return json.loads(path.read_text(errors="replace"))
except (json.JSONDecodeError, OSError):
return {}
def generate_default_config(folder: str | None = None) -> dict:
"""Generate a sensible default config."""
if folder:
folders = [{"path": folder, "name": Path(folder).expanduser().name, "depth": 1}]
else:
folders = [{"path": str(Path.cwd()), "name": "Projects", "depth": 1}]
return {
"folders": folders,
"port": DEFAULT_PORT,
"refresh_seconds": DEFAULT_REFRESH,
}
def save_config(config: dict, path: Path | None = None) -> None:
"""Write config to disk."""
target = path or (_script_dir() / CONFIG_FILE)
target.write_text(json.dumps(config, indent=2) + "\n")
def build_config(args) -> dict:
"""Build final config from config.json + CLI overrides."""
config = load_config()
# CLI folders override config file
if args.folders:
names = args.names or []
folders = []
for i, f in enumerate(args.folders):
name = names[i] if i < len(names) else Path(f).expanduser().name
folders.append({"path": f, "name": name, "depth": args.depth})
config["folders"] = folders
# If still no folders, generate default and save
if not config.get("folders"):
config = generate_default_config()
save_config(config)
print(f"Created {CONFIG_FILE} — edit it to add your project folders.")
# CLI overrides for port/refresh
if args.port is not None:
config["port"] = args.port
if args.refresh is not None:
config["refresh_seconds"] = args.refresh
config.setdefault("port", DEFAULT_PORT)
config.setdefault("refresh_seconds", DEFAULT_REFRESH)
return config
# ---------------------------------------------------------------------------
# HTTP server
# ---------------------------------------------------------------------------
class DashboardHandler(BaseHTTPRequestHandler):
"""Serves the dashboard HTML and live JSON data."""
def __init__(self, *args, config: dict, **kwargs):
self.config = config
super().__init__(*args, **kwargs)
def do_GET(self) -> None:
if self.path == "/" or self.path == "/index.html":
self._serve_html()
elif self.path == "/data":
self._serve_data()
elif self.path == "/config":
self._serve_config()
else:
self.send_error(404)
def _serve_html(self) -> None:
html_path = _script_dir() / DASHBOARD_FILE
if not html_path.is_file():
self.send_error(500, "dashboard.html not found")
return
content = html_path.read_bytes()
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(content)))
self.send_header("Cache-Control", "no-cache")
self.end_headers()
self.wfile.write(content)
def _serve_data(self) -> None:
data = scan_all(self.config)
data["refresh_seconds"] = self.config.get("refresh_seconds", DEFAULT_REFRESH)
payload = json.dumps(data, indent=None, default=str).encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(payload)))
self.send_header("Cache-Control", "no-cache")
self.end_headers()
self.wfile.write(payload)
def _serve_config(self) -> None:
"""Return sanitized config (tab names only, no paths)."""
safe = {
"tabs": [{"name": f["name"]} for f in self.config.get("folders", [])],
"refresh_seconds": self.config.get("refresh_seconds", DEFAULT_REFRESH),
}
payload = json.dumps(safe).encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(payload)))
self.end_headers()
self.wfile.write(payload)
def log_message(self, format, *args) -> None:
"""Suppress default request logging (too noisy with polling)."""
pass
def run_server(config: dict) -> None:
"""Start the HTTP server and open the dashboard in a browser."""
port = config.get("port", DEFAULT_PORT)
handler = partial(DashboardHandler, config=config)
server = HTTPServer(("127.0.0.1", port), handler)
url = f"http://localhost:{port}"
print(f"Claude Code View running at {url}")
print(f" Scanning {len(config.get('folders', []))} folder(s), refresh every {config.get('refresh_seconds', DEFAULT_REFRESH)}s")
print(f" Press Ctrl+C to stop\n")
webbrowser.open(url)
try:
server.serve_forever()
except KeyboardInterrupt:
print("\nStopped.")
server.server_close()
# ---------------------------------------------------------------------------
# Static HTML generation (backward compat)
# ---------------------------------------------------------------------------
def generate_static_html(config: dict) -> str:
"""Generate a self-contained HTML file with data embedded."""
html_path = _script_dir() / DASHBOARD_FILE
if not html_path.is_file():
print(f"Error: {DASHBOARD_FILE} not found", file=sys.stderr)
sys.exit(1)
template = html_path.read_text(errors="replace")
data = scan_all(config)
json_data = json.dumps(data, indent=None, default=str)
# Replace the fetch-based data loading with embedded data
static_script = f"const DATA = {json_data};\nconst STATIC_MODE = true;"
html = template.replace("/*__DATA__*/", static_script)
return html
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def main() -> None:
parser = argparse.ArgumentParser(
description="Claude Code View — live dashboard for your Claude Code projects."
)
parser.add_argument("--folders", nargs="+", metavar="PATH",
help="Project folders to scan (overrides config.json)")
parser.add_argument("--names", nargs="+", metavar="NAME",
help="Tab names for each folder (must match --folders count)")
parser.add_argument("--depth", type=int, default=1,
help="Scan depth within each folder (default: 1)")
parser.add_argument("--port", type=int, default=None,
help=f"HTTP server port (default: {DEFAULT_PORT})")
parser.add_argument("--refresh", type=int, default=None,
help=f"Data refresh interval in seconds (default: {DEFAULT_REFRESH})")
parser.add_argument("--static", action="store_true",
help="Generate a static HTML file instead of running a server")
parser.add_argument("--output", "-o", default="claude-code-view.html",
help="Output file for --static mode")
parser.add_argument("--no-open", action="store_true",
help="Don't auto-open browser")
args = parser.parse_args()
config = build_config(args)
if args.static:
# One-shot static HTML generation
print(f"Scanning {len(config['folders'])} folder(s)...")
html = generate_static_html(config)
output_path = Path(args.output).resolve()
output_path.write_text(html)
print(f"Generated: {output_path}")
if not args.no_open:
webbrowser.open(f"file://{output_path}")
else:
# Live server mode
if args.no_open:
# Monkey-patch to skip browser open
import webbrowser as wb
wb.open = lambda *a, **k: None
run_server(config)
if __name__ == "__main__":
main()