SPB Git

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%
3.6 KB · 113 lines typescript
Raw Blame History
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