/** * timmy-watch — Live terminal feed of Timmy's inner world. * * Connects to the /api/ws WebSocket and streams a human-readable, * ANSI-coloured log to stdout. Works in any tmux pane or Emacs buffer * (shell-mode, comint, vterm, eat, etc.). * * Usage: * pnpm --filter @workspace/scripts timmy-watch * node --import tsx/esm scripts/src/timmy-watch.ts * * Env / flags: * PORT= API server port (default 3000) * --port Override PORT * --host Override host (default 127.0.0.1) * --no-color Disable ANSI colour */ // ── ANSI helpers ────────────────────────────────────────────────────────────── const NO_COLOR = process.argv.includes("--no-color") || process.env["NO_COLOR"] !== undefined || process.env["TERM"] === "dumb"; function esc(code: string, text: string): string { return NO_COLOR ? text : `\x1b[${code}m${text}\x1b[0m`; } const c = { dim: (s: string) => esc("2", s), bold: (s: string) => esc("1", s), cyan: (s: string) => esc("36", s), yellow: (s: string) => esc("33", s), green: (s: string) => esc("32", s), red: (s: string) => esc("31", s), magenta: (s: string) => esc("35", s), blue: (s: string) => esc("34", s), white: (s: string) => esc("97", s), bgRed: (s: string) => esc("41;97", s), bCyan: (s: string) => esc("1;36", s), bGreen: (s: string) => esc("1;32", s), bYellow: (s: string) => esc("1;33", s), }; // ── CLI args ────────────────────────────────────────────────────────────────── function argAfter(flag: string): string | undefined { const idx = process.argv.indexOf(flag); return idx !== -1 ? process.argv[idx + 1] : undefined; } const host = argAfter("--host") ?? "127.0.0.1"; const port = Number(argAfter("--port") ?? process.env["PORT"] ?? 3000); const wsUrl = `ws://${host}:${port}/api/ws`; // ── Timestamp ───────────────────────────────────────────────────────────────── function ts(): string { return c.dim(new Date().toLocaleTimeString("en-US", { hour12: false })); } // ── Agent display ───────────────────────────────────────────────────────────── const AGENT_LABELS: Record = { alpha: "α orchestrator", beta: "β eval ", gamma: "γ work ", delta: "δ lightning ", timmy: "✦ timmy ", }; const agentStates: Record = { alpha: "idle", beta: "idle", gamma: "idle", delta: "idle", }; function agentLabel(id: string): string { return AGENT_LABELS[id] ?? id; } function stateChip(state: string): string { switch (state) { case "working": return c.bGreen(`[${state}]`); case "thinking": return c.bCyan(`[${state}]`); case "active": return c.bYellow(`[${state}]`); case "idle": return c.dim(`[${state} ]`); default: return c.dim(`[${state}]`); } } function agentRow(): string { return Object.entries(agentStates) .map(([id, st]) => `${c.bold(id)} ${stateChip(st)}`) .join(" "); } // ── Event pretty-printer ────────────────────────────────────────────────────── type WsMsg = Record; function printLine(label: string, detail: string): void { process.stdout.write(`${ts()} ${label} ${detail}\n`); } function handleMessage(msg: WsMsg): void { const type = msg["type"] as string; switch (type) { case "ping": return; // handled externally; suppress output case "world_state": { const timmy = msg["timmyState"] as { mood: string; activity: string } | undefined; const agents = msg["agentStates"] as Record | undefined; const events = msg["recentEvents"] as Array<{ type: string; summary: string }> | undefined; if (agents) Object.assign(agentStates, agents); printLine( c.bCyan("◉ connected "), `${wsUrl}`, ); printLine( c.cyan(" world "), timmy ? `Timmy is ${c.bold(timmy.mood)} / ${c.bold(timmy.activity)}` : "state unknown", ); printLine(c.cyan(" agents "), agentRow()); if (events && events.length > 0) { printLine(c.dim(" history "), c.dim(`last ${events.length} events:`)); for (const ev of events) { printLine(c.dim(" "), c.dim(`${ev.type} ${ev.summary}`)); } } printLine(c.dim("─────────────"), c.dim("live feed starting ↓")); break; } case "agent_state": { const id = String(msg["agentId"] ?? "?"); const state = String(msg["state"] ?? "?"); const prev = agentStates[id]; if (prev === state) return; // skip no-op transitions agentStates[id] = state; printLine( c.yellow(" agent "), `${c.bold(agentLabel(id))} ${c.dim(prev ?? "?")} → ${stateChip(state)}`, ); break; } case "job_started": { const jobId = String(msg["jobId"] ?? "?").slice(0, 8); const agentId = String(msg["agentId"] ?? "?"); printLine( c.bYellow("▶ job start "), `job ${c.bold(jobId)} via ${c.bold(agentLabel(agentId))}`, ); break; } case "job_completed": { const jobId = String(msg["jobId"] ?? "?").slice(0, 8); const agentId = String(msg["agentId"] ?? "?"); printLine( c.bGreen("✔ job done "), `job ${c.bold(jobId)} via ${c.bold(agentLabel(agentId))}`, ); break; } case "chat": { const agentId = String(msg["agentId"] ?? "?"); const text = String(msg["text"] ?? ""); const isTimmy = agentId === "timmy"; const isDelta = agentId === "delta"; const isVisitor = agentId === "visitor"; const label = isTimmy ? c.magenta("✦ timmy ") : isDelta ? c.green("⚡ delta ") : isVisitor ? c.blue("👤 visitor ") : c.blue(`💬 ${agentId.padEnd(8)} `); const body = isTimmy ? c.magenta(text) : isDelta ? c.green(text) : text; printLine(label, body); break; } case "visitor_count": { const count = msg["count"] as number; printLine(c.dim(" visitors "), c.dim(`${count} connected`)); break; } case "agent_count": { const count = msg["count"] as number; printLine(c.dim(" agents "), c.dim(`${count} connected`)); break; } default: { const json = JSON.stringify(msg); printLine(c.dim(" unknown "), c.dim(json.slice(0, 120))); } } } // ── WebSocket connection with auto-reconnect ────────────────────────────────── let retryDelay = 1000; const MAX_DELAY = 30_000; function connect(): void { printLine(c.dim(" connecting"), c.dim(wsUrl)); const ws = new WebSocket(wsUrl); ws.addEventListener("open", () => { retryDelay = 1000; ws.send(JSON.stringify({ type: "subscribe" })); ws.send(JSON.stringify({ type: "visitor_enter", visitorId: "timmy-watch-cli" })); }); ws.addEventListener("message", (ev) => { try { const msg = JSON.parse(String(ev.data)) as WsMsg; if (msg["type"] === "ping") { ws.send(JSON.stringify({ type: "pong" })); return; } handleMessage(msg); } catch { printLine(c.red(" parse err "), String(ev.data).slice(0, 80)); } }); ws.addEventListener("close", (ev) => { printLine( c.dim(" closed "), c.dim(`code=${ev.code} retrying in ${retryDelay / 1000}s…`), ); setTimeout(() => { retryDelay = Math.min(retryDelay * 2, MAX_DELAY); connect(); }, retryDelay); }); ws.addEventListener("error", () => { printLine(c.red(" ws error "), c.red(`cannot reach ${wsUrl}`)); }); } // ── Banner ──────────────────────────────────────────────────────────────────── const banner = [ "", c.bold(c.magenta(" TIMMY WATCH")), c.dim(` ${wsUrl} · Ctrl-C to quit`), "", ].join("\n"); process.stdout.write(banner + "\n"); connect();