Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save kdeenanauth/f73f0787759549842e4e7fe09664886e to your computer and use it in GitHub Desktop.

Select an option

Save kdeenanauth/f73f0787759549842e4e7fe09664886e to your computer and use it in GitHub Desktop.
even-terminal updates
# 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