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:
@@ -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
265
scripts/src/timmy-watch.ts
Normal 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();
|
||||
Reference in New Issue
Block a user