Created
June 15, 2026 06:03
-
-
Save kdeenanauth/f73f0787759549842e4e7fe09664886e to your computer and use it in GitHub Desktop.
even-terminal updates
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment