# even-terminal updates

> Source: <https://gist.github.com/kdeenanauth/f73f0787759549842e4e7fe09664886e>
> Published: 2026-06-15 06:03:25+00:00

| # Patch: live-mirror external Claude Code sessions in even-terminal | |
| `@evenrealities/even-terminal` only streams sessions it drives itself via the | |
| Agent SDK. This patch adds passive mirroring: when you open a session in the | |
| app/glasses, it tails that session's `~/.claude/projects/**/<id>.jsonl` (which | |
| the `claude` process in your own terminal appends to live) and broadcasts each | |
| new entry over SSE in the same message shapes the UI already renders. | |
| Applies to dist/ of a global install (`$(npm root -g)/@evenrealities/even-terminal/dist`). | |
| Restart even-terminal after applying. Note: dist edits are wiped on reinstall. | |
| --- | |
| ## 1. NEW FILE — `dist/claude/tail.js` | |
| ``` js | |
| // Live-mirror tailer for *external* Claude Code sessions. | |
| import { existsSync, readdirSync, statSync, openSync, readSync, closeSync, watch } from "node:fs"; | |
| import { join } from "node:path"; | |
| import { homedir } from "node:os"; | |
| import { summarizeClaudeToolCall } from "./summarize.js"; | |
| import { claudeSessionStatus } from "./provider.js"; | |
| import { pushMessage, broadcast, sessionHasClients } from "../routes/events.js"; | |
| // session ids Even Terminal is itself driving — tailer stays quiet for these | |
| export const drivenSessions = new Set(); | |
| const tails = new Map(); | |
| function findSessionFile(sessionId) { | |
| const claudeDir = join(homedir(), ".claude", "projects"); | |
| if (!existsSync(claudeDir)) return null; | |
| for (const dir of readdirSync(claudeDir)) { | |
| const p = join(claudeDir, dir, `${sessionId}.jsonl`); | |
| if (existsSync(p)) return p; | |
| } | |
| return null; | |
| } | |
| function emit(sessionId, msg) { | |
| const id = pushMessage(sessionId, msg); | |
| broadcast(sessionId, msg, id); | |
| } | |
| /** Convert one Claude Code jsonl entry into SSE display messages. */ | |
| function entryToMessages(entry, t) { | |
| const out = []; | |
| if (!entry || typeof entry !== "object") return out; | |
| if (entry.type === "assistant") { | |
| const content = entry.message?.content; | |
| if (!Array.isArray(content)) return out; | |
| for (const block of content) { | |
| if (block.type === "text" && block.text?.trim()) { | |
| out.push({ type: "status", state: "text_start", sessionId: t.sessionId }); | |
| out.push({ type: "text_delta", text: block.text }); | |
| out.push({ type: "status", state: "text_end", sessionId: t.sessionId }); | |
| } else if (block.type === "thinking" && typeof block.thinking === "string" && block.thinking.trim()) { | |
| out.push({ type: "status", state: "think_start", sessionId: t.sessionId }); | |
| out.push({ type: "text_delta", text: block.thinking }); | |
| out.push({ type: "status", state: "think_end", sessionId: t.sessionId }); | |
| } else if (block.type === "tool_use") { | |
| t.pending.set(block.id, { name: block.name, input: block.input }); | |
| out.push({ type: "tool_start", name: block.name, toolId: block.id }); | |
| } | |
| } | |
| } else if (entry.type === "user") { | |
| const content = entry.message?.content; | |
| if (typeof content === "string") { | |
| if (content.trim() && !content.includes("Request interrupted by user")) | |
| out.push({ type: "user_prompt", text: content }); | |
| } else if (Array.isArray(content)) { | |
| for (const block of content) { | |
| if (block.type === "tool_result") { | |
| const toolId = block.tool_use_id; | |
| const pending = t.pending.get(toolId); | |
| let output; | |
| if (typeof block.content === "string") output = block.content; | |
| else if (Array.isArray(block.content)) | |
| output = block.content.filter((b) => b.type === "text").map((b) => b.text).join("\n"); | |
| out.push({ | |
| type: "tool_end", | |
| name: pending?.name ?? "", | |
| toolId, | |
| summary: pending ? summarizeClaudeToolCall(pending.name, pending.input) : "", | |
| detail: { input: pending?.input, output }, | |
| }); | |
| t.pending.delete(toolId); | |
| } else if (block.type === "text" && block.text?.trim()) { | |
| out.push({ type: "user_prompt", text: block.text }); | |
| } | |
| } | |
| } | |
| } else if (entry.type === "result") { | |
| out.push({ | |
| type: "result", | |
| success: entry.subtype === "success", | |
| text: entry.result ?? "", | |
| sessionId: entry.session_id ?? t.sessionId, | |
| costUsd: entry.total_cost_usd ?? 0, | |
| provider: "claude", | |
| turns: entry.num_turns ?? 0, | |
| durationMs: entry.duration_ms ?? 0, | |
| inputTokens: 0, | |
| outputTokens: 0, | |
| }); | |
| } | |
| return out; | |
| } | |
| function processFile(sessionId) { | |
| const t = tails.get(sessionId); | |
| if (!t) return; | |
| let size; | |
| try { size = statSync(t.file).size; } catch { return; } | |
| if (size < t.offset) { t.offset = 0; t.remainder = ""; } // truncated/rotated | |
| if (size === t.offset) return; | |
| let chunk; | |
| try { | |
| const fd = openSync(t.file, "r"); | |
| try { | |
| const buf = Buffer.alloc(size - t.offset); | |
| readSync(fd, buf, 0, buf.length, t.offset); | |
| chunk = buf.toString("utf8"); | |
| } finally { closeSync(fd); } | |
| } catch { return; } | |
| t.offset = size; | |
| const data = t.remainder + chunk; | |
| const lines = data.split("\n"); | |
| t.remainder = lines.pop() ?? ""; // keep trailing partial line | |
| if (drivenSessions.has(sessionId)) return; // ET drives it → SDK already emits | |
| for (const line of lines) { | |
| const trimmed = line.trim(); | |
| if (!trimmed) continue; | |
| let entry; | |
| try { entry = JSON.parse(trimmed); } catch { continue; } | |
| if (entry.uuid) { | |
| if (t.seen.has(entry.uuid)) continue; | |
| t.seen.add(entry.uuid); | |
| } | |
| for (const m of entryToMessages(entry, t)) emit(sessionId, m); | |
| } | |
| const status = claudeSessionStatus(sessionId); | |
| if (status !== t.lastStatus) { | |
| t.lastStatus = status; | |
| emit(sessionId, { type: "status", state: status, sessionId }); | |
| } | |
| } | |
| export function startTail(sessionId) { | |
| if (!sessionId || tails.has(sessionId)) return; | |
| const file = findSessionFile(sessionId); | |
| if (!file) return; | |
| let size = 0; | |
| try { size = statSync(file).size; } catch {} | |
| const t = { | |
| sessionId, file, | |
| offset: size, // start at EOF: stream only NEW activity | |
| remainder: "", pending: new Map(), seen: new Set(), | |
| lastStatus: null, watcher: null, timer: null, | |
| }; | |
| tails.set(sessionId, t); | |
| const status = claudeSessionStatus(sessionId); | |
| t.lastStatus = status; | |
| emit(sessionId, { type: "status", state: status, sessionId }); | |
| try { t.watcher = watch(file, { persistent: false }, () => processFile(sessionId)); } catch {} | |
| t.timer = setInterval(() => processFile(sessionId), 1000); // poll fallback | |
| console.log(`[tail] Mirroring external session=${sessionId} from offset=${size}`); | |
| } | |
| export function stopTail(sessionId) { | |
| const t = tails.get(sessionId); | |
| if (!t) return; | |
| if (t.watcher) { try { t.watcher.close(); } catch {} } | |
| if (t.timer) clearInterval(t.timer); | |
| tails.delete(sessionId); | |
| console.log(`[tail] Stopped mirroring session=${sessionId}`); | |
| } | |
| export function stopTailIfIdle(sessionId) { | |
| if (!sessionHasClients(sessionId)) stopTail(sessionId); | |
| } | |
| ``` | |
| --- | |
| ## 2. `dist/routes/events.js` | |
| ``` diff | |
| import { Router } from "express"; | |
| import { writeToLogFile } from "../logger.js"; | |
| +import { startTail, stopTailIfIdle } from "../claude/tail.js"; | |
| @@ in router.get("/events", ...) @@ | |
| s.clients.add(res); | |
| console.log(`[sse] Client connected session=${sessionId} (session clients: ${s.clients.size}, total: ${clientCount()})`); | |
| + // Mirror an externally-running Claude session's live jsonl output over SSE. | |
| + startTail(sessionId); | |
| @@ req.on("close", ...) @@ | |
| clearInterval(heartbeat); | |
| s.clients.delete(res); | |
| + stopTailIfIdle(sessionId); | |
| console.log(`[sse] Client disconnected (total: ${clientCount()})`); | |
| ``` | |
| --- | |
| ## 3. `dist/claude/session.js` (dedup: stay quiet for ET-driven sessions) | |
| ``` diff | |
| import { query } from "@anthropic-ai/claude-agent-sdk"; | |
| import { existsSync } from "node:fs"; | |
| +import { drivenSessions } from "./tail.js"; | |
| @@ async run(prompt) @@ | |
| this._busy = true; | |
| this.busyEmitted = false; | |
| this.currentBlockType = null; | |
| + this.onIdReady((sid) => drivenSessions.add(sid)); | |
| this.turnStartMs = Date.now(); | |
| @@ query finally block @@ | |
| this._busy = false; | |
| + if (this.sessionId) | |
| + drivenSessions.delete(this.sessionId); | |
| this.queryHandle = null; | |
| ``` | |
| --- | |
| ## 4. Re-apply script — `~/.even-terminal/apply-tail-patch.mjs` | |
| Run after upgrading/reinstalling even-terminal (npm wipes `dist/` edits), then | |
| restart even-terminal: | |
| ``` | |
| node ~/.even-terminal/apply-tail-patch.mjs | |
| ``` | |
| Idempotent — safe to run repeatedly. | |
| ``` js | |
| #!/usr/bin/env node | |
| // Re-applies the "live-mirror external Claude sessions" patch to a globally | |
| // installed @evenrealities/even-terminal. Safe to run repeatedly. | |
| import { execSync } from "node:child_process"; | |
| import { readFileSync, writeFileSync, existsSync } from "node:fs"; | |
| import { join } from "node:path"; | |
| const root = execSync("npm root -g").toString().trim(); | |
| const pkg = join(root, "@evenrealities", "even-terminal"); | |
| if (!existsSync(pkg)) { | |
| console.error(`even-terminal not found under ${root}`); | |
| process.exit(1); | |
| } | |
| const dist = join(pkg, "dist"); | |
| // Full source of dist/claude/tail.js (see section 1 of this gist). | |
| // Paste the tail.js body between the backticks below. | |
| const TAIL_JS = `<<< PASTE dist/claude/tail.js SOURCE HERE >>>`; | |
| function patchFile(path, edits, marker) { | |
| let src = readFileSync(path, "utf8"); | |
| if (src.includes(marker)) { | |
| console.log(` already patched: ${path}`); | |
| return; | |
| } | |
| for (const [find, replace] of edits) { | |
| if (!src.includes(find)) | |
| throw new Error(`anchor not found in ${path}:\n${find}`); | |
| src = src.replace(find, replace); | |
| } | |
| writeFileSync(path, src); | |
| console.log(` patched: ${path}`); | |
| } | |
| // 1. tail.js | |
| writeFileSync(join(dist, "claude", "tail.js"), TAIL_JS); | |
| console.log(` wrote: ${join(dist, "claude", "tail.js")}`); | |
| // 2. events.js | |
| patchFile(join(dist, "routes", "events.js"), [ | |
| [ | |
| `import { Router } from "express";\nimport { writeToLogFile } from "../logger.js";`, | |
| `import { Router } from "express";\nimport { writeToLogFile } from "../logger.js";\nimport { startTail, stopTailIfIdle } from "../claude/tail.js";`, | |
| ], | |
| [ | |
| ` s.clients.add(res);\n console.log(\`[sse] Client connected session=\${sessionId} (session clients: \${s.clients.size}, total: \${clientCount()})\`);`, | |
| ` s.clients.add(res);\n console.log(\`[sse] Client connected session=\${sessionId} (session clients: \${s.clients.size}, total: \${clientCount()})\`);\n // Mirror an externally-running Claude session's live jsonl output over SSE.\n startTail(sessionId);`, | |
| ], | |
| [ | |
| ` clearInterval(heartbeat);\n s.clients.delete(res);\n console.log(\`[sse] Client disconnected (total: \${clientCount()})\`);`, | |
| ` clearInterval(heartbeat);\n s.clients.delete(res);\n stopTailIfIdle(sessionId);\n console.log(\`[sse] Client disconnected (total: \${clientCount()})\`);`, | |
| ], | |
| ], `../claude/tail.js`); | |
| // 3. session.js | |
| patchFile(join(dist, "claude", "session.js"), [ | |
| [ | |
| `import { query } from "@anthropic-ai/claude-agent-sdk";\nimport { existsSync } from "node:fs";`, | |
| `import { query } from "@anthropic-ai/claude-agent-sdk";\nimport { existsSync } from "node:fs";\nimport { drivenSessions } from "./tail.js";`, | |
| ], | |
| [ | |
| ` this._busy = true;\n this.busyEmitted = false;\n this.currentBlockType = null;\n this.turnStartMs = Date.now();`, | |
| ` this._busy = true;\n this.busyEmitted = false;\n this.currentBlockType = null;\n this.onIdReady((sid) => drivenSessions.add(sid));\n this.turnStartMs = Date.now();`, | |
| ], | |
| [ | |
| ` this._busy = false;\n this.queryHandle = null;`, | |
| ` this._busy = false;\n if (this.sessionId)\n drivenSessions.delete(this.sessionId);\n this.queryHandle = null;`, | |
| ], | |
| ], `drivenSessions`); | |
| console.log("Done. Restart even-terminal for changes to take effect."); | |
| ``` | |
| > **Note:** assign `TAIL_JS` to the section-1 `tail.js` source (e.g. a template | |
| > literal, or `readFileSync` it from a saved copy). In the working version on | |
| > disk it's embedded inline. |
