Add a live feed to view Timmy's internal state and agent activity

Introduces a new CLI script `timmy-watch` to establish a WebSocket connection and display real-time updates of Timmy's status, agent states, and recent events.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 90c7a60b-2c61-4699-b5c6-6a1ac7469a4d
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 56ce6fae-6759-4857-bf0c-606a96a71bdb
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
alexpaynex
2026-03-19 23:07:26 +00:00
parent e58055d0b6
commit b837094ed4
2 changed files with 266 additions and 0 deletions

View File

@@ -5,6 +5,7 @@
"type": "module",
"scripts": {
"hello": "tsx ./src/hello.ts",
"timmy-watch": "tsx ./src/timmy-watch.ts",
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"devDependencies": {

265
scripts/src/timmy-watch.ts Normal file
View File

@@ -0,0 +1,265 @@
/**
* 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=<n> API server port (default 3000)
* --port <n> Override PORT
* --host <h> 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<string, string> = {
alpha: "α orchestrator",
beta: "β eval ",
gamma: "γ work ",
delta: "δ lightning ",
timmy: "✦ timmy ",
};
const agentStates: Record<string, string> = {
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<string, unknown>;
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<string, string> | 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();