spb/khaelor Public
KHAELOR — a terminal-native autonomous engineering agent powered by Anthropic.
TypeScript 82.9%
HTML 14.9%
CSS 1.1%
JavaScript 0.7%
1/**2 * KHAELOR3 * File: src/cli/logger.ts4 * Description: Tiny redacting file logger for ~/.khaelor/logs — developer logs never pollute the TUI, secrets never reach disk.5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import { appendFileSync, mkdirSync, writeFileSync } from "node:fs";11import { homedir } from "node:os";12import { dirname, join } from "node:path";1314/** Default developer log directory (ARCHITECTURE.md §5.1, CLAUDE.md §19). */15export function defaultLogDir(): string {16 return join(homedir(), ".khaelor", "logs");17}1819/** Secret-shaped substrings that must never reach a log file. */20const REDACTIONS: readonly RegExp[] = [21 /sk-ant-[A-Za-z0-9_-]+/g, // Anthropic API keys22 /(bearer\s+)[A-Za-z0-9._-]{8,}/gi, // bearer tokens (before the header rules eat "Bearer")23 /(authorization\s*[:=]\s*)\S+/gi, // Authorization headers24 /(x-api-key\s*[:=]\s*)\S+/gi, // raw header form25 /(ANTHROPIC_API_KEY\s*[:=]\s*)\S+/g, // env echoes26];2728/** Redact API keys and authorization material from arbitrary text. */29export function redactForLog(text: string): string {30 let out = text;31 for (const pattern of REDACTIONS) {32 out = out.replace(pattern, (_match, prefix: unknown) =>33 typeof prefix === "string" ? `${prefix}[redacted]` : "[redacted]",34 );35 }36 return out;37}3839export type LogLevel = "debug" | "info" | "warn" | "error";4041/**42 * Append-only file logger. No dependencies, synchronous appends (log volume43 * is low — lifecycle events and error paths only). When `enabled` is false44 * only `warn`/`error` lines are written; `--debug` enables everything.45 */46export class FileLogger {47 readonly filePath: string;48 #debug: boolean;49 #ready = false;50 #broken = false;5152 constructor(filePath: string, options: { debug?: boolean } = {}) {53 this.filePath = filePath;54 this.#debug = options.debug ?? false;55 }5657 get debugEnabled(): boolean {58 return this.#debug;59 }6061 log(level: LogLevel, message: string, details?: Record<string, unknown>): void {62 if (this.#broken) return;63 if (!this.#debug && (level === "debug" || level === "info")) return;64 const line =65 JSON.stringify({66 ts: new Date().toISOString(),67 level,68 message: redactForLog(message),69 ...(details !== undefined ? { details: JSON.parse(redactForLog(JSON.stringify(details))) as unknown } : {}),70 }) + "\n";71 try {72 this.#ensureDir();73 appendFileSync(this.filePath, line, "utf8");74 } catch {75 // A broken log file must never break the app (CLAUDE.md §19).76 this.#broken = true;77 }78 }7980 debug(message: string, details?: Record<string, unknown>): void {81 this.log("debug", message, details);82 }8384 error(message: string, details?: Record<string, unknown>): void {85 this.log("error", message, details);86 }8788 #ensureDir(): void {89 if (this.#ready) return;90 mkdirSync(dirname(this.filePath), { recursive: true });91 this.#ready = true;92 }93}9495/**96 * Write a crash report file (never the screen — the terminal guard restores97 * the shell; details live under ~/.khaelor/logs/). Returns the file path,98 * or null when even that failed.99 */100export function writeCrashFile(logDir: string, error: unknown): string | null {101 const stamp = new Date().toISOString().replace(/[:.]/g, "-");102 const file = join(logDir, `crash-${stamp}.log`);103 const body =104 error instanceof Error ? (error.stack ?? `${error.name}: ${error.message}`) : String(error);105 try {106 mkdirSync(logDir, { recursive: true });107 writeFileSync(file, redactForLog(body) + "\n", "utf8");108 return file;109 } catch {110 return null;111 }112}113