Luigit
repositories / dotfiles

dotfiles

bugabingas dorkfiles

owned by admin

claude/cc-statusline-hook.js

Raw
#!/usr/bin/env node
// Claude Code hook → feeds the statusline's generic badge seam.
// Registered as a PreToolUse hook matching the "Workflow" tool. On a workflow
// launch it appends a self-expiring badge line to ~/.claude/.cc-statusline-extra
// which statusline.js renders (and ages out via CONFIG.extraTtlMs).
//
// File format: one "<epochMillis>|<text>" per line. We prune expired lines on
// every write so the file never grows unbounded.
//
// Contract: read hook JSON from stdin, exit 0, emit nothing to stdout (no
// context injection). Never throw in a way that blocks the tool.

"use strict";

const fs = require("fs");
const os = require("os");
const path = require("path");

const TTL_MS = 120000; // must match statusline CONFIG.extraTtlMs
const MAX_LINES = 10;

try {
  const raw = fs.readFileSync(0, "utf8");
  const h = raw && raw.trim() ? JSON.parse(raw) : {};

  const claudeDir0 = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude");

  // Turn boundary: drop a per-session marker the statusline consumes to
  // rebaseline its "turn cost" (Δ since this prompt).
  if (h.hook_event_name === "UserPromptSubmit") {
    try {
      const sid = String(h.session_id || "default").replace(/[^A-Za-z0-9._-]/g, "");
      fs.writeFileSync(path.join(claudeDir0, `.cc-turn-${sid}`), "");
    } catch {}
  }

  // Only act on a Workflow tool launch.
  if (h.hook_event_name === "PreToolUse" && h.tool_name === "Workflow") {
    const ti = h.tool_input || {};
    // Predefined workflow → ti.name. Inline script → meta.name inside ti.script.
    let name = typeof ti.name === "string" ? ti.name : "";
    if (!name && typeof ti.script === "string") {
      const m = ti.script.match(/name\s*:\s*['"]([^'"]+)['"]/);
      if (m) name = m[1];
    }
    name = (name || "workflow").replace(/[\x00-\x1F|]/g, "").slice(0, 40);

    const claudeDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude");
    const f = path.join(claudeDir, ".cc-statusline-extra");
    const now = Date.now();

    // Keep only non-expired existing lines, then append ours.
    let kept = [];
    try {
      kept = fs
        .readFileSync(f, "utf8")
        .split(/\r?\n/)
        .filter((l) => {
          const i = l.indexOf("|");
          if (i <= 0) return false;
          const ts = Number(l.slice(0, i));
          return Number.isFinite(ts) && now - ts <= TTL_MS;
        });
    } catch {}

    kept.push(`${now}|⚙ wf:${name}`);
    if (kept.length > MAX_LINES) kept = kept.slice(-MAX_LINES);

    const tmp = `${f}.${process.pid}.tmp`;
    fs.writeFileSync(tmp, kept.join("\n") + "\n");
    fs.renameSync(tmp, f);
  }
} catch {
  // Swallow everything — a statusline-feeder hook must never block a tool.
}

process.exit(0);