"""Structured logging for skill operations. Each skill invocation creates a SkillLogger that captures commands, their output, and contextual messages. On save(), the log is written to logs/--.md in markdown format. """ from datetime import datetime from pathlib import Path from lib.config import paths class SkillLogger: """Captures structured output from a skill invocation.""" def __init__(self, skill_name: str, recipe_name: str | None = None): self.skill_name = skill_name self.recipe_name = recipe_name self.started = datetime.now() self._entries: list[dict] = [] def step(self, description: str) -> None: """Log a major step in the skill execution.""" self._entries.append({"type": "step", "text": description}) print(f"\n=== {description} ===", flush=True) def command(self, cmd: str, output: str, returncode: int) -> None: """Log a command and its result.""" self._entries.append({ "type": "command", "cmd": cmd, "output": output, "returncode": returncode, }) def info(self, message: str) -> None: """Log an informational message.""" self._entries.append({"type": "info", "text": message}) print(f" {message}", flush=True) def warn(self, message: str) -> None: """Log a warning.""" self._entries.append({"type": "warn", "text": message}) print(f" WARNING: {message}", flush=True) def error(self, message: str) -> None: """Log an error.""" self._entries.append({"type": "error", "text": message}) print(f" ERROR: {message}", flush=True) def save(self) -> Path: """Write the log to logs/--.md.""" date_str = self.started.strftime("%Y-%m-%d") if self.recipe_name: filename = f"{self.skill_name}-{self.recipe_name}-{date_str}.md" else: filename = f"{self.skill_name}-{date_str}.md" log_dir = paths.LOGS_DIR log_dir.mkdir(parents=True, exist_ok=True) log_path = log_dir / filename lines = [ f"# {self.skill_name}", "", ] if self.recipe_name: lines.append(f"Recipe: {self.recipe_name}") lines.append("") lines.append( f"Started: {self.started.strftime('%Y-%m-%d %H:%M:%S')}" ) lines.append( f"Finished: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" ) lines.append("") for entry in self._entries: if entry["type"] == "step": lines.append(f"## {entry['text']}") lines.append("") elif entry["type"] == "command": lines.append(f"```") lines.append(f"$ {entry['cmd']}") if entry["output"]: lines.append(entry["output"]) lines.append(f"```") if entry["returncode"] != 0: lines.append(f"Exit code: {entry['returncode']}") lines.append("") elif entry["type"] == "info": lines.append(f"{entry['text']}") lines.append("") elif entry["type"] == "warn": lines.append(f"**WARNING:** {entry['text']}") lines.append("") elif entry["type"] == "error": lines.append(f"**ERROR:** {entry['text']}") lines.append("") with open(log_path, "w") as f: f.write("\n".join(lines)) print(f"\n Log saved: {log_path}", flush=True) return log_path