""" Audit service — SQLite-backed command log. Every executed command is recorded with enough info to reconstruct the inverse operation (for undo). Uses aiosqlite so it doesn't block the async event loop. """ from __future__ import annotations import json import aiosqlite from config import AUDIT_DB_PATH _CREATE_TABLE = """ CREATE TABLE IF NOT EXISTS audit_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, command TEXT NOT NULL, intent TEXT NOT NULL, diff TEXT, created_at TEXT DEFAULT (datetime('now')) ); """ _db: aiosqlite.Connection | None = None async def _get_db() -> aiosqlite.Connection: global _db if _db is None: _db = await aiosqlite.connect(AUDIT_DB_PATH) await _db.execute(_CREATE_TABLE) await _db.commit() return _db async def log_command( session_id: str, command: str, intent: dict | None, diff: dict | None, ) -> None: """Insert an audit row.""" db = await _get_db() await db.execute( "INSERT INTO audit_log (session_id, command, intent, diff) VALUES (?, ?, ?, ?)", ( session_id, command, json.dumps(intent or {}, ensure_ascii=False), json.dumps(diff or {}, ensure_ascii=False), ), ) await db.commit() async def get_history(session_id: str, limit: int = 50) -> list[dict]: """Fetch recent commands for a session (for undo UI).""" db = await _get_db() cursor = await db.execute( "SELECT id, command, intent, diff, created_at FROM audit_log " "WHERE session_id = ? ORDER BY id DESC LIMIT ?", (session_id, limit), ) rows = await cursor.fetchall() return [ { "id": r[0], "command": r[1], "intent": json.loads(r[2]), "diff": json.loads(r[3]), "created_at": r[4], } for r in rows ] async def close() -> None: global _db if _db is not None: await _db.close() _db = None