import * as path from "node:path"; import { getWolfDir, ensureWolfDir, readJSON, writeJSON, estimateTokens, readStdin, normalizePath, getProjectDir, hookMain, getSessionFilePath } from "./shared.js"; import { lookupEntry } from "./anatomy-store.js"; /** * The PostToolUse payload carries the tool result in `tool_response`, whose * shape depends on the tool and harness version: a plain string, an array of * content blocks, or a structured object ({content} or {file:{content}}). * Older builds of this hook read a `tool_output` field that never existed in * Claude Code's payload, so read-token tracking was always zero (issue: the * ledger under-reported every session). */ function extractToolResponseText(resp) { if (typeof resp === "string") return resp; if (Array.isArray(resp)) { return resp .map((block) => (block && typeof block === "object" && typeof block.text === "string" ? block.text : "")) .join(""); } if (resp && typeof resp === "object") { const obj = resp; if (typeof obj.content === "string") return obj.content; if (obj.file && typeof obj.file.content === "string") return obj.file.content; } return ""; } async function main() { ensureWolfDir(); const wolfDir = getWolfDir(); const raw = await readStdin(); let input; try { input = JSON.parse(raw); } catch { return; } const sessionFile = getSessionFilePath(input); const filePath = input.tool_input?.file_path ?? input.tool_input?.path ?? ""; const content = extractToolResponseText(input.tool_response) || input.tool_output?.content || ""; if (!filePath) { return; } // Ranged reads: pre-read already recorded the contact with ranged:true. // Registering them as full reads here is what used to make a later // legitimate full read look like a duplicate (~20x warning inflation). if (input.tool_input?.offset !== undefined || input.tool_input?.limit !== undefined) { return; } const normalizedFile = normalizePath(filePath); // Skip tracking for .wolf/ internal files — consistent with pre-read const projectDir = normalizePath(getProjectDir()); const relToProject = normalizedFile.startsWith(projectDir) ? normalizedFile.slice(projectDir.length).replace(/^\//, "") : ""; if (relToProject.startsWith(".wolf/") || relToProject.startsWith(".wolf\\")) { // 2.4: measure OpenWolf's own context cost instead of hiding it. Tagged // separately from project reads so anatomy hit-rates stay meaningful. try { if (content) { const session = readJSON(sessionFile, { files_read: {} }); const tok = estimateTokens(content, "prose"); session.wolf_internal_tokens = (session.wolf_internal_tokens ?? 0) + tok; const perFile = (session.wolf_internal_reads ?? {}); perFile[relToProject] = (perFile[relToProject] ?? 0) + tok; session.wolf_internal_reads = perFile; writeJSON(sessionFile, session); } } catch { } return; } const ext = path.extname(filePath).toLowerCase(); const codeExts = new Set([".ts", ".js", ".tsx", ".jsx", ".py", ".rs", ".go", ".java", ".c", ".cpp", ".css", ".json", ".yaml", ".yml"]); const proseExts = new Set([".md", ".txt", ".rst"]); const type = codeExts.has(ext) ? "code" : proseExts.has(ext) ? "prose" : "mixed"; let tokens = content ? estimateTokens(content, type) : 0; // Fallback: if tool_output had no content, use the anatomy token estimate if (tokens === 0) { const entry = lookupEntry(wolfDir, projectDir, normalizedFile); if (entry) tokens = entry.tokens; } const session = readJSON(sessionFile, { files_read: {} }); const existing = session.files_read[normalizedFile]; if (existing && existing.ranged !== true) { existing.tokens = tokens; } else { // Fresh full read (or an upgrade of a ranged-only contact to a full read). session.files_read[normalizedFile] = { count: 1, tokens, first_read: existing?.first_read ?? new Date().toISOString(), }; } writeJSON(sessionFile, session); } hookMain("post-read", main); //# sourceMappingURL=post-read.js.map