Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

aa: show admin state in HUD, load session history on resume

- Bridge: GET /api/history — reads the session's Claude JSONL
transcript (gated by same bearer+ADMIN_SUB check as every other
endpoint) and returns raw user/assistant events. sessionId is
UUID-validated before path composition to block traversal.

- aa.mjs: HUD label reflects auth state visually —
lime admin, fresh or resumed session
red ✗ not admin (403)
yellow ? no token / 401
yellow ! bridge unreachable / other non-2xx
Server still enforces on every call; this is a UX cue only.
Submit is guarded by (isAdmin && token) and state is cleared on
403/401 so a stale flag can't authorize anything.

- aa.mjs: on probe success with a stored sessionId, fetch history
and replay user/assistant events via a shared renderClaudeEvent
helper. Live reload is already inherited from chat.boot's version
poll (dumduel/arena pattern).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+141 -7
+47
help/bridge/aa-bridge.mjs
··· 10 10 // POST /api/reset — clear stored session for this user (bearer required) 11 11 // GET /health — liveness probe 12 12 // GET /api/session — return current stored session id for this user 13 + // GET /api/history — return prior user/assistant events for this user's session 13 14 14 15 import http from "http"; 15 16 import { spawn } from "child_process"; ··· 124 125 }); 125 126 req.on("error", reject); 126 127 }); 128 + } 129 + 130 + // ───────── session transcript reader ───────── 131 + // Claude stores per-project session transcripts as JSONL here: 132 + // ~/.claude/projects/<slash-replaced-cwd>/<sessionId>.jsonl 133 + // We filter to user + assistant events (skipping queue-operation, attachment, 134 + // last-prompt etc.) and return the raw rows — aa.mjs does the rendering. 135 + async function readSessionTranscript(sessionId) { 136 + if (!/^[a-f0-9-]{36}$/i.test(sessionId)) throw new Error("invalid session id"); 137 + const projectDir = WORK_DIR.replace(/\//g, "-"); 138 + const path = join(homedir(), ".claude", "projects", projectDir, `${sessionId}.jsonl`); 139 + if (!existsSync(path)) return []; 140 + const content = await readFile(path, "utf8"); 141 + const events = []; 142 + for (const line of content.split("\n")) { 143 + if (!line.trim()) continue; 144 + try { 145 + const o = JSON.parse(line); 146 + if (o.type === "user" || o.type === "assistant") events.push(o); 147 + } catch {} 148 + } 149 + return events; 127 150 } 128 151 129 152 // ───────── claude spawn ───────── ··· 279 302 const sid = await getSessionId(sub); 280 303 res.writeHead(200, { "Content-Type": "application/json", ...corsHeaders(origin) }); 281 304 res.end(JSON.stringify({ sessionId: sid })); 305 + return; 306 + } 307 + 308 + if (req.url === "/api/history" && req.method === "GET") { 309 + const sub = await validateBearer(req.headers.authorization); 310 + if (!sub || (!DEV_BYPASS && sub !== ADMIN_SUB)) { 311 + res.writeHead(sub ? 403 : 401, corsHeaders(origin)); 312 + res.end(); 313 + return; 314 + } 315 + const sid = await getSessionId(sub); 316 + if (!sid) { 317 + res.writeHead(200, { "Content-Type": "application/json", ...corsHeaders(origin) }); 318 + res.end(JSON.stringify({ sessionId: null, events: [] })); 319 + return; 320 + } 321 + try { 322 + const events = await readSessionTranscript(sid); 323 + res.writeHead(200, { "Content-Type": "application/json", ...corsHeaders(origin) }); 324 + res.end(JSON.stringify({ sessionId: sid, events })); 325 + } catch (err) { 326 + res.writeHead(500, { "Content-Type": "application/json", ...corsHeaders(origin) }); 327 + res.end(JSON.stringify({ error: err.message })); 328 + } 282 329 return; 283 330 } 284 331
+94 -7
system/public/aesthetic.computer/disks/aa.mjs
··· 16 16 let pending = false; 17 17 let abortCtrl = null; 18 18 let userHandleRef = null; 19 + let hudRef = null; 19 20 20 21 let msgCounter = 0; 21 22 const nextId = () => `aa-${Date.now()}-${msgCounter++}`; 22 23 24 + // HUD state — visible indicator of admin / bridge status. 25 + // Server still enforces auth on every call; this is a UX cue only. 26 + function setStatus(state) { 27 + if (!hudRef) return; 28 + const map = { 29 + loading: ["aa", undefined], // default color 30 + ok: ["aa", "lime"], // admin, fresh session 31 + resumed: ["aa", "lime"], // admin, resumed session 32 + forbidden: ["aa ✗", "red"], // 403 not admin 33 + unauth: ["aa ?", "yellow"], // 401 / no token 34 + down: ["aa !", "yellow"], // bridge unreachable or odd status 35 + }; 36 + const [label, color] = map[state] || map.loading; 37 + hudRef.label(label, color); 38 + } 39 + 23 40 // Cool slate theme — distinct from chat's defaults so AA reads as a separate space. 24 41 const THEME = { 25 42 background: [12, 14, 22], ··· 53 70 // chat.mjs gates paint on `connecting`, so flip it false up front. 54 71 client.system.connecting = false; 55 72 56 - hud.label("aa"); 73 + hudRef = hud; 74 + setStatus("loading"); 57 75 userHandleRef = handle; 58 76 59 77 if (!user?.sub) { 78 + setStatus("unauth"); 60 79 pushSystem("log in first to talk to aa."); 61 80 } else { 62 81 try { 63 82 token = await authorize(); 64 83 if (!token) { 84 + setStatus("unauth"); 65 85 pushSystem("no auth0 token — refresh and retry."); 66 86 } else { 67 87 const probe = await fetch(`${ENDPOINT}/api/session`, { ··· 70 90 if (probe.status === 200) { 71 91 isAdmin = true; 72 92 const data = await probe.json(); 73 - pushSystem( 74 - data.sessionId 75 - ? `resuming session ${data.sessionId.slice(0, 8)}…` 76 - : "fresh session.", 77 - ); 93 + if (data.sessionId) { 94 + setStatus("resumed"); 95 + pushSystem(`resuming ${data.sessionId.slice(0, 8)}…`); 96 + loadHistory(); 97 + } else { 98 + setStatus("ok"); 99 + pushSystem("fresh session."); 100 + } 78 101 } else if (probe.status === 403) { 102 + setStatus("forbidden"); 103 + isAdmin = false; 104 + token = null; 79 105 pushSystem("not admin — this piece is @jeffrey-only."); 106 + } else if (probe.status === 401) { 107 + setStatus("unauth"); 108 + isAdmin = false; 109 + token = null; 110 + pushSystem("unauthorized — session expired, refresh."); 80 111 } else { 112 + setStatus("down"); 81 113 pushSystem(`bridge said ${probe.status}.`); 82 114 } 83 115 } 84 116 } catch (err) { 117 + setStatus("down"); 85 118 pushSystem(`bridge unreachable: ${err.message}`); 86 119 } 87 120 } 88 121 89 122 await chat.boot(api, client.system, { 90 123 submitHandler: async (text) => { 91 - if (!isAdmin) { pushSystem("not authorized."); return; } 124 + if (!isAdmin || !token) { pushSystem("not authorized."); return; } 92 125 if (text === "/reset") return reset(); 93 126 if (text === "/clear") { 94 127 client.system.messages.length = 0; ··· 162 195 } catch (err) { 163 196 pushSystem(`reset failed: ${err.message}`); 164 197 } 198 + } 199 + 200 + async function loadHistory() { 201 + try { 202 + const res = await fetch(`${ENDPOINT}/api/history`, { 203 + headers: { Authorization: `Bearer ${token}` }, 204 + }); 205 + if (!res.ok) return; 206 + const data = await res.json(); 207 + const events = Array.isArray(data?.events) ? data.events : []; 208 + let count = 0; 209 + for (const ev of events) count += renderClaudeEvent(ev, { fromHistory: true }); 210 + if (count) pushSystem(`loaded ${count} prior ${count === 1 ? "message" : "messages"}.`); 211 + } catch (err) { 212 + pushSystem(`history fetch failed: ${err.message}`); 213 + } 214 + } 215 + 216 + // Render one claude transcript event (from live stream or saved history) 217 + // as zero or more chat messages. Returns the number of messages produced. 218 + function renderClaudeEvent(ev, { fromHistory = false } = {}) { 219 + if (!ev || typeof ev !== "object") return 0; 220 + let produced = 0; 221 + if (ev.type === "assistant" && ev.message?.content) { 222 + for (const b of ev.message.content) { 223 + if (b.type === "text" && b.text) { 224 + pushMessage("@aa", b.text, { sound: !fromHistory }); 225 + produced++; 226 + } else if (b.type === "tool_use") { 227 + pushMessage("log", `→ ${b.name} ${summarizeInput(b.input)}`); 228 + produced++; 229 + } 230 + } 231 + } else if (ev.type === "user" && ev.message?.content) { 232 + const c = ev.message.content; 233 + // History format: user content is usually a plain string (the prompt). 234 + if (typeof c === "string") { 235 + const me = userHandleRef?.() || "@jeffrey"; 236 + pushMessage(me, c); 237 + produced++; 238 + } else if (Array.isArray(c)) { 239 + for (const b of c) { 240 + if (b.type === "tool_result") { 241 + const t = stringifyResult(b.content); 242 + if (t) { pushMessage("log", truncate(t, 600)); produced++; } 243 + } else if (b.type === "text" && b.text) { 244 + const me = userHandleRef?.() || "@jeffrey"; 245 + pushMessage(me, b.text); 246 + produced++; 247 + } 248 + } 249 + } 250 + } 251 + return produced; 165 252 } 166 253 167 254 async function sendToBridge(text) {