cd /news/developer-tools/even-terminal-updates Β· home β€Ί topics β€Ί developer-tools β€Ί article
[ARTICLE Β· art-28123] src=gist.github.com β†— pub= topic=developer-tools verified=true sentiment=↑ positive

even-terminal updates

Even Realities released a patch for even-terminal that adds passive mirroring of external Claude Code sessions. The update allows the terminal to tail session JSONL files and broadcast entries over SSE, enabling users to view Claude Code sessions initiated outside the Even Terminal app directly in their glasses.

read11 min publishedJun 15, 2026

| # 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. |

── more in #developer-tools 4 stories Β· sorted by recency
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/even-terminal-update…] indexed:0 read:11min 2026-06-15 Β· β€”