// lib/stats/stats-append.mjs // Append one command-stats record to /-stats.jsonl. // // Why this exists: the commands' stats phases said "append to // ${CLAUDE_PLUGIN_DATA}/-stats.jsonl; if it is not set, skip silently". // The Bash tool's env carries no CLAUDE_PLUGIN_DATA, so the record was skipped // whenever the model did not guess the directory itself — the same silent skip // that left brief-approved at 0 records. This writer resolves the directory // the way scripts/yardstick.mjs reads it, and a failed write is REPORTED // (exit 1 + reason), never swallowed. Stats still never block the workflow: // the command prose reports the failure and carries on. // // CLI (the record is read from stdin, so no shell quoting of JSON): // node lib/stats/stats-append.mjs <<'JSON' // { ...record... } // JSON // → prints {"written":true,"path":...} exit 0 | {"written":false,"reason":...} exit 1 import { appendFileSync, mkdirSync, readFileSync } from 'node:fs'; import { homedir } from 'node:os'; import { join } from 'node:path'; export const STATS_KINDS = Object.freeze([ 'trekbrief', 'trekresearch', 'trekplan', 'trekexecute', 'trekreview', 'trekcontinue', ]); /** * The plugin data directory: CLAUDE_PLUGIN_DATA when the harness provides it, * otherwise the directory the hooks write to and scripts/yardstick.mjs reads. */ export function resolveStatsDir(env = process.env) { if (env.CLAUDE_PLUGIN_DATA) return env.CLAUDE_PLUGIN_DATA; const home = env.HOME && env.HOME.length > 0 ? env.HOME : homedir(); return join(home, '.claude', 'plugins', 'data', 'voyage-ktg-plugin-marketplace'); } /** * @returns {{ written: boolean, path: string|null, reason?: string }} */ export function appendStatsRecord(kind, record, env = process.env) { if (!STATS_KINDS.includes(kind)) { return { written: false, path: null, reason: `unknown kind "${kind}" (one of ${STATS_KINDS.join(', ')})` }; } if (!record || typeof record !== 'object' || Array.isArray(record)) { return { written: false, path: null, reason: 'record must be a JSON object' }; } const dir = resolveStatsDir(env); const path = join(dir, `${kind}-stats.jsonl`); try { mkdirSync(dir, { recursive: true }); appendFileSync(path, JSON.stringify(record) + '\n'); } catch (e) { return { written: false, path, reason: `${e.code || 'write-failed'}: ${e.message}` }; } return { written: true, path }; } if (import.meta.url === `file://${process.argv[1]}`) { const kind = process.argv[2]; let record = null; let reason = null; try { record = JSON.parse(readFileSync(0, 'utf8')); } catch (e) { reason = `stdin is not JSON: ${e.message}`; } const r = reason ? { written: false, path: null, reason } : appendStatsRecord(kind, record); process.stdout.write(JSON.stringify(r) + '\n'); process.exit(r.written ? 0 : 1); }