/** * KHAELOR * File: src/tools/format.ts * Description: Shared formatting helpers for tool outputs — line numbering, paths, durations, header checks. * * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai */ import * as path from "node:path"; /** Right-aligned line number + `→` separator (TOOL_PROTOCOL §2.3). */ export function numberedLine(lineNumber: number, text: string): string { return `${String(lineNumber).padStart(6)}→${text}`; } /** Resolve a possibly-relative tool path against the workspace cwd. */ export function resolveToolPath(cwd: string, p: string): string { return path.resolve(cwd, p); } /** Workspace-relative display path (falls back to absolute outside the project). */ export function displayPath(cwd: string, abs: string): string { const rel = path.relative(cwd, abs); if (rel.length === 0) return "."; return rel.startsWith("..") ? abs : rel.split(path.sep).join("/"); } /** `3.2s`-style duration. */ export function formatDuration(ms: number): string { return `${(ms / 1000).toFixed(1)}s`; } /** `04:32`-style elapsed time (hh:mm:ss above one hour). */ export function formatElapsed(ms: number): string { const totalSeconds = Math.floor(ms / 1000); const seconds = totalSeconds % 60; const minutes = Math.floor(totalSeconds / 60) % 60; const hours = Math.floor(totalSeconds / 3600); const mm = String(minutes).padStart(2, "0"); const ss = String(seconds).padStart(2, "0"); return hours > 0 ? `${String(hours).padStart(2, "0")}:${mm}:${ss}` : `${mm}:${ss}`; } /** Count logical lines: a trailing newline does not open a new line. */ export function countContentLines(content: string): number { if (content.length === 0) return 0; const lines = content.split("\n"); if (lines[lines.length - 1] === "") lines.pop(); return lines.length; } /** Extensions that require the KHAELOR author header (CLAUDE.md §2, Rule #0). */ const HEADER_REQUIRED_EXTENSIONS = new Set([ ".ts", ".tsx", ".js", ".mjs", ".cjs", ".sh", ".rs", ".md", ]); /** Paths exempt from the header requirement (vendored/generated). */ const HEADER_EXEMPT_SEGMENTS = ["node_modules", "references", "dist", "build"]; /** Does this new file need the KHAELOR header (TOOL_PROTOCOL §3.2)? */ export function requiresKhaelorHeader(cwd: string, abs: string): boolean { if (!HEADER_REQUIRED_EXTENSIONS.has(path.extname(abs))) return false; const rel = path.relative(cwd, abs); if (rel.startsWith("..") || path.isAbsolute(rel)) return false; // outside the project const segments = rel.split(path.sep); return !segments.some((segment) => HEADER_EXEMPT_SEGMENTS.includes(segment)); } /** Mirrors scripts/check-headers.sh: KHAELOR marker + author + contact within the first 12 lines. */ export function hasKhaelorHeader(content: string): boolean { const head = content.split("\n").slice(0, 12).join("\n"); return ( head.includes("KHAELOR") && head.includes("Simon-Pierre Boucher") && head.includes("contact@spboucher.ai") ); } /** The header reminder appended to headerless new source files (TOOL_PROTOCOL §3.2, verbatim). */ export const HEADER_REMINDER_NOTE = [ "NOTE: This new source file is missing the mandatory KHAELOR author header", "(Author: Simon-Pierre Boucher · Contact: contact@spboucher.ai · File · Description).", "Add it now — the header lint check fails the build without it.", ].join("\n");