// Session-grain usage emission (novox/hq ADR 0054). On a schedule, read every transcript under // `~/.claude/projects/*/.jsonl`, sum its tokens, and emit one session-grain usage event // per session. The consumer IS the (node,module) session's fixed binding, so no per-message account // attribution is done — just the totals (port map "don't-map" #3). // // Runs as `mesh-tools run` (no broker), so events are emitted best-effort via the sibling mesh-tools // `emit` primitive; the totals are also written to a file so the reading is observable without one. import { readdirSync, statSync, writeFileSync, renameSync, mkdirSync } from "node:fs"; import { join, dirname } from "node:path"; import { readSessionFile, type SessionUsage } from "../transcript.js"; function projectsDir(): string { return process.env.MESH_CLAUDE_PROJECTS_DIR ?? `${process.env.HOME ?? "/root"}/.claude/projects`; } /** Every `.jsonl` under the projects tree, with the project directory it sits in. */ function transcripts(root: string): { path: string; sessionId: string }[] { const found: { path: string; sessionId: string }[] = []; let projects: string[]; try { projects = readdirSync(root); } catch { return found; // no projects yet is not a failure — there is simply nothing to report. } for (const proj of projects) { const dir = join(root, proj); let entries: string[]; try { if (!statSync(dir).isDirectory()) continue; entries = readdirSync(dir); } catch { continue; } for (const file of entries) { if (!file.endsWith(".jsonl")) continue; found.push({ path: join(dir, file), sessionId: file.replace(/\.jsonl$/, "") }); } } return found; } async function main(): Promise { const module = process.env.MESH_MODULE ?? "anthropic-consumer"; const node = process.env.MESH_NODE ?? "unknown"; const readings: SessionUsage[] = []; for (const t of transcripts(projectsDir())) { try { readings.push(await readSessionFile(t.path, t.sessionId)); } catch (err) { console.error(`[anthropic-consumer] could not read ${t.path}: ${err}`); } } for (const r of readings) { await emitUsage({ grain: "session", node, module, ...r }); } if (process.env.MESH_ANTHROPIC_USAGE_OUT) { atomicWrite(process.env.MESH_ANTHROPIC_USAGE_OUT, JSON.stringify(readings, null, 2)); } console.error(`[anthropic-consumer] reported ${readings.length} session(s)`); } function atomicWrite(path: string, content: string): void { mkdirSync(dirname(path), { recursive: true }); const tmp = `${path}.tmp`; writeFileSync(tmp, content, { mode: 0o600 }); renameSync(tmp, path); } /** Emit best-effort via the sibling mesh-tools `emit`, which wires a broker a run step has none. */ async function emitUsage(body: Record): Promise { const main = process.env.MESH_TOOLS_MAIN ?? "/app/dist/main.js"; const { spawn } = await import("node:child_process"); await new Promise((resolve) => { const child = spawn( process.execPath, [main, "emit", "module.anthropic-consumer.usage.session", JSON.stringify(body)], { stdio: "inherit" }, ); child.on("exit", () => resolve()); child.on("error", (err) => { console.error(`[anthropic-consumer] could not emit usage: ${err}`); resolve(); }); }); } await main();