Files
claude c68b2303a0 Add explore skill: interactive pseudocode browser
Serves a localhost page in the pseudo-skill register with the
reference markers as clickable buttons: clicks land in
<workdir>/events.jsonl, a persistent Monitor tail wakes the session,
the session rewrites page.html, and the browser live-reloads over SSE.
Actions per marker: expand / show real code / explain / ask, plus a
page-level ask box; anchors become forge deep-links when the project
has a reachable remote; incidental findings are surfaced on the page
as anchored notes.

The port is configured in one place (the PORT constant atop
scripts/server.py; --port overrides per run). Mechanics proven live
against the aura repo before extraction into this skill.
2026-07-20 12:11:57 +02:00

140 lines
4.6 KiB
Python

#!/usr/bin/env python3
"""explore — localhost server behind the /explore skill.
Serves <workdir>/page.html, records marker-click events into
<workdir>/events.jsonl (one JSON object per line), and pushes an SSE
"reload" event whenever page.html changes on disk. The Claude session
arms a Monitor (`tail -F -n 0 <workdir>/events.jsonl`) as the
back-channel; rewriting page.html is the reply channel.
Usage: server.py <workdir> [--port N]
Stdlib only. Binds to localhost.
"""
import argparse
import json
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
# The single place the default port is configured (--port overrides per run).
PORT = 8000
# SSE tuning: how often to poll the page mtime, and how many idle polls
# between keep-alive comments (0.5s * 30 = one ping every 15s).
POLL_S = 0.5
PINGS_EVERY = 30
# Set in main() from the workdir argument.
PAGE = None
EVENTS = None
class Handler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def do_GET(self):
if self.path in ("/", "/index.html"):
self._serve_page()
elif self.path == "/stream":
self._serve_stream()
else:
self.send_error(404)
def do_POST(self):
if self.path == "/event":
self._record_event()
else:
self.send_error(404)
def _serve_page(self):
# Read fresh from disk every time — Claude rewrites this file.
try:
body = PAGE.read_bytes()
except OSError:
self.send_error(503, "page.html missing")
return
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Cache-Control", "no-store")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def _serve_stream(self):
# Long-lived SSE connection: emit "reload" whenever page.html
# changes on disk, keep-alive comments in between.
self.send_response(200)
self.send_header("Content-Type", "text/event-stream")
self.send_header("Cache-Control", "no-store")
self.end_headers()
try:
last = PAGE.stat().st_mtime_ns
except OSError:
last = 0
idle = 0
try:
while True:
time.sleep(POLL_S)
try:
cur = PAGE.stat().st_mtime_ns
except OSError:
continue # page briefly missing mid-rewrite
if cur != last:
last = cur
self.wfile.write(b"event: reload\ndata: 1\n\n")
self.wfile.flush()
idle = 0
else:
idle += 1
if idle >= PINGS_EVERY:
self.wfile.write(b": ping\n\n")
self.wfile.flush()
idle = 0
except (BrokenPipeError, ConnectionResetError):
return # client went away — normal
def _record_event(self):
try:
length = int(self.headers.get("Content-Length", 0))
except ValueError:
self.send_error(411)
return
raw = self.rfile.read(length)
try:
event = json.loads(raw)
if not isinstance(event, dict):
raise ValueError("expected a JSON object")
except (ValueError, UnicodeDecodeError):
self.send_error(400, "invalid JSON")
return
event["ts"] = time.strftime("%Y-%m-%dT%H:%M:%S")
# Single-line appends in "a" mode are atomic enough here.
with EVENTS.open("a", encoding="utf-8") as f:
f.write(json.dumps(event, ensure_ascii=False) + "\n")
self.send_response(204)
self.end_headers()
def log_message(self, *args):
pass # keep stdout clean — it is the background-task log
def main():
global PAGE, EVENTS
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("workdir", type=Path, help="directory holding page.html and events.jsonl")
ap.add_argument("--port", type=int, default=PORT, help=f"listen port (default {PORT})")
a = ap.parse_args()
workdir = a.workdir.resolve()
workdir.mkdir(parents=True, exist_ok=True)
PAGE = workdir / "page.html"
EVENTS = workdir / "events.jsonl"
EVENTS.touch()
srv = ThreadingHTTPServer(("127.0.0.1", a.port), Handler)
print(f"explore serving {workdir} on http://127.0.0.1:{a.port}", flush=True)
srv.serve_forever()
if __name__ == "__main__":
main()