/** * Pi DevOps β€” Optimized extension for CharityRight/QuikCue infrastructure * * Features: * πŸ—οΈ Infra context β€” injects full topology into every AI prompt * πŸ›‘οΈ Safety guard β€” confirms destructive ops before execution * πŸ“Š Smart footer β€” model, context, cost, tool tally, server indicator * 🎯 Focus widget β€” keeps current task visible at all times * ⚑ /health β€” live container + service status check * πŸ“‹ /logs [svc] β€” tail logs for a service * πŸ—ΊοΈ /infra β€” print full infra topology * 🎯 /focus [task] β€” set/change the current task * πŸ›‘ /stop β€” abort current AI action * πŸ”§ /ssh [cmd] β€” run a raw SSH command on the server * * Usage: pi -e extensions/pi-devops.ts */ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent"; import { isToolCallEventType } from "@mariozechner/pi-coding-agent"; import type { AssistantMessage } from "@mariozechner/pi-ai"; import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui"; import { basename } from "node:path"; import { execSync } from "node:child_process"; import { applyExtensionDefaults } from "./themeMap.ts"; // ═══════════════════════════════════════════════════════════════════════════ // Infra constants // ═══════════════════════════════════════════════════════════════════════════ const SSH = `ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=accept-new -p 22 root@159.195.60.33`; const INCUS_CR = `${SSH} "incus exec cr-server-new --"`; const INFRA_CONTEXT = ` You are Pi, a DevOps assistant managing live production infrastructure. ## Primary Server - SSH: ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=accept-new -p 22 root@159.195.60.33 "" - HAProxy routes: charityright domains β†’ cr-server-new, quikcue domains β†’ qc-server-new ## Incus Containers (on primary server) - cr-server-new (10.213.16.224) β€” CharityRight. Docker runs INSIDE here. - qc-server-new (10.213.16.234) β€” QuikCue - qc-server β€” STOPPED legacy ## Docker (inside cr-server-new only!) - Run: ssh ... "incus exec cr-server-new -- docker " - Postgres container: dokploy-migrated-cr-postgres-data - DB: donation_warehouse - Query: ssh ... "incus exec cr-server-new -- docker exec dokploy-migrated-cr-postgres-data psql -U postgres -d donation_warehouse -c \\"SELECT ...\\"" ## Services - /opt/ayn-antivirus β€” AYN antivirus scanner - /opt/enthuse-db-sync-v2 β€” Enthuse donation sync - /opt/launchgood-sync β€” LaunchGood sync - /root/legacy-donation-system-laravel β€” CharityRight Laravel app - /root/redis-v2 β€” Redis ## Critical Rules - NEVER run docker commands directly on the primary host - Always use incus exec cr-server-new -- for Docker ops - Warn before any restart/stop of production services - Always test with SELECT before UPDATE/DELETE `.trim(); // ═══════════════════════════════════════════════════════════════════════════ // Dangerous patterns // ═══════════════════════════════════════════════════════════════════════════ const DANGEROUS: { pattern: RegExp; reason: string }[] = [ { pattern: /rm\s+-[rRf]{1,3}\s+[\/~]/, reason: "Deleting from root or home!" }, { pattern: /rm\s+-rf/, reason: "Recursive force delete" }, { pattern: /DROP\s+(TABLE|DATABASE|SCHEMA)/i, reason: "SQL DROP detected" }, { pattern: /TRUNCATE\s+TABLE/i, reason: "SQL TRUNCATE detected" }, { pattern: /DELETE\s+FROM\s+\w+\s*(WHERE\s+1=1\s*)?(;|$)/i, reason: "Potentially unsafe DELETE" }, { pattern: /UPDATE\s+\w+\s+SET\s+.*\s*;?\s*$/i, reason: "UPDATE without WHERE β€” may affect all rows" }, { pattern: /docker\s+system\s+prune/, reason: "Docker prune β€” removes all unused data" }, { pattern: /incus\s+(delete|stop)\s+\S+/, reason: "Incus container operation" }, { pattern: /systemctl\s+(stop|disable|restart)\s+(haproxy|docker|nginx)/, reason: "Touching critical production service" }, { pattern: /git\s+push\s+.*--force/, reason: "Force push β€” may overwrite remote history" }, { pattern: /git\s+reset\s+--hard/, reason: "Hard reset β€” may lose uncommitted changes" }, { pattern: /mkfs\./, reason: "Filesystem format command!" }, { pattern: /dd\s+if=/, reason: "Raw disk copy via dd" }, { pattern: /curl.*\|\s*(ba)?sh/, reason: "Piping remote script to shell" }, { pattern: /wget.*\|\s*(ba)?sh/, reason: "Piping remote script to shell" }, { pattern: /chmod\s+-R\s+777/, reason: "Recursive world-writable permissions" }, ]; // ═══════════════════════════════════════════════════════════════════════════ // Helpers // ═══════════════════════════════════════════════════════════════════════════ function runSSH(cmd: string, timeout = 15000): string { try { return execSync(`${SSH} "${cmd.replace(/"/g, '\\"')}"`, { timeout, encoding: "utf8" }).trim(); } catch (e: any) { return e?.stdout?.trim() || e?.message || "SSH error"; } } // ═══════════════════════════════════════════════════════════════════════════ // Extension // ═══════════════════════════════════════════════════════════════════════════ export default function (pi: ExtensionAPI) { let focus: string | undefined; let activeCtx: ExtensionContext | undefined; const toolCounts: Record = {}; // ── Tool tracking ───────────────────────────────────────────────────── pi.on("tool_execution_end", async (event) => { toolCounts[event.toolName] = (toolCounts[event.toolName] || 0) + 1; }); // ── Session start ───────────────────────────────────────────────────── pi.on("session_start", async (_event, ctx) => { applyExtensionDefaults(import.meta.url, ctx); activeCtx = ctx; // Ask for focus task (non-blocking) void (async () => { const answer = await ctx.ui.input( "🎯 What are you working on today?", "e.g. Debug launchgood sync, check cr-server disk, investigate slow queries" ); if (answer?.trim()) { focus = answer.trim(); setFocusWidget(ctx); ctx.ui.notify(`🎯 Focus set: ${focus}`, "success"); } else { ctx.ui.notify("πŸ’‘ No focus set. Use /focus to set one anytime.", "info"); } })(); // ── Footer ──────────────────────────────────────────────────────── ctx.ui.setFooter((tui, theme, footerData) => { const unsub = footerData.onBranchChange(() => tui.requestRender()); return { dispose: unsub, invalidate() {}, render(width: number): string[] { // Accumulate tokens + cost let tokIn = 0, tokOut = 0, cost = 0; for (const entry of ctx.sessionManager.getBranch()) { if (entry.type === "message" && entry.message.role === "assistant") { const m = entry.message as AssistantMessage; tokIn += m.usage.input; tokOut += m.usage.output; cost += m.usage.cost.total; } } const fmt = (n: number) => n < 1000 ? `${n}` : `${(n / 1000).toFixed(1)}k`; const model = ctx.model?.id || "no-model"; const branch = footerData.getGitBranch(); const dir = basename(ctx.cwd); // Context bar const usage = ctx.getContextUsage(); const pct = usage?.percent ?? 0; const filled = Math.round(pct / 10); const bar = (pct < 70 ? theme.fg("success", "β–ˆ".repeat(filled)) : pct < 90 ? theme.fg("warning", "β–ˆ".repeat(filled)) : theme.fg("error", "β–ˆ".repeat(filled))) + theme.fg("dim", "β–‘".repeat(10 - filled)); // Line 1: model | context | tokens | cost const l1Left = theme.fg("accent", " Pi") + theme.fg("dim", ` ${model} `) + bar + theme.fg("dim", ` ${Math.round(pct)}%`); const l1Right = theme.fg("success", fmt(tokIn)) + theme.fg("dim", "↓ ") + theme.fg("accent", fmt(tokOut)) + theme.fg("dim", "↑ ") + theme.fg("warning", `$${cost.toFixed(4)}`) + theme.fg("dim", " "); const pad1 = " ".repeat(Math.max(1, width - visibleWidth(l1Left) - visibleWidth(l1Right))); const line1 = truncateToWidth(l1Left + pad1 + l1Right, width, ""); // Line 2: cwd | branch | tools const l2Left = theme.fg("dim", ` ${dir}`) + (branch ? theme.fg("dim", " ") + theme.fg("warning", "(") + theme.fg("success", branch) + theme.fg("warning", ")") : "") + theme.fg("dim", " β€Ί 159.195.60.33"); const entries = Object.entries(toolCounts); const l2Right = entries.length === 0 ? theme.fg("dim", "no tools yet ") : entries.map(([n, c]) => theme.fg("accent", n) + theme.fg("dim", ":") + theme.fg("success", `${c}`)).join(theme.fg("dim", " ")) + " "; const pad2 = " ".repeat(Math.max(1, width - visibleWidth(l2Left) - visibleWidth(l2Right))); const line2 = truncateToWidth(l2Left + pad2 + l2Right, width, ""); return [line1, line2]; }, }; }); }); pi.on("session_switch", async (_event, ctx) => { activeCtx = ctx; }); // ── Safety guard ────────────────────────────────────────────────────── pi.on("tool_call", async (event, ctx) => { if (isToolCallEventType("bash", event)) { const cmd = event.input.command; for (const { pattern, reason } of DANGEROUS) { if (pattern.test(cmd)) { const confirmed = await ctx.ui.confirm( "⚠️ Destructive Operation", `${reason}\n\nCommand:\n${cmd}\n\nRun this on LIVE production?`, { timeout: 30000 } ); if (!confirmed) { ctx.ui.notify(`πŸ›‘οΈ Blocked: ${reason}`, "warning"); ctx.abort(); return { block: true, reason: `πŸ›‘ BLOCKED: ${reason}\n\nUser declined. Ask what to do instead.`, }; } break; } } } return { block: false }; }); // ── System prompt injection ─────────────────────────────────────────── pi.on("before_agent_start", async (event) => { let extra = `\n\n${INFRA_CONTEXT}`; if (focus) { extra += `\n\n\n${focus}\nStay focused on this task. If asked something unrelated, note it but bring back to the focus.\n`; } return { systemPrompt: event.systemPrompt + extra }; }); // ── Focus widget ────────────────────────────────────────────────────── function setFocusWidget(ctx: ExtensionContext) { ctx.ui.setWidget("focus", () => ({ render(width: number): string[] { const line = truncateToWidth(` 🎯 ${focus}`, width - 2, "…"); return [ " ".repeat(width), line + " ".repeat(Math.max(0, width - visibleWidth(line))), " ".repeat(width), ]; }, invalidate() {}, })); } // ═══════════════════════════════════════════════════════════════════════ // Commands // ═══════════════════════════════════════════════════════════════════════ pi.registerCommand("stop", { description: "Abort current AI action", handler: async (args, ctx) => { ctx.abort(); ctx.ui.notify(args?.trim() ? `πŸ›‘ Stopped: ${args.trim()}` : "πŸ›‘ Stopped.", "warning"); }, }); pi.registerCommand("focus", { description: "Set or change current task focus", handler: async (args, ctx) => { if (args?.trim()) { focus = args.trim(); setFocusWidget(ctx); ctx.ui.notify(`🎯 Focus set: ${focus}`, "success"); } else { const answer = await ctx.ui.input("🎯 What are you working on?", focus || "e.g. Debug sync, check logs..."); if (answer?.trim()) { focus = answer.trim(); setFocusWidget(ctx); ctx.ui.notify(`🎯 Focus set: ${focus}`, "success"); } } }, }); pi.registerCommand("infra", { description: "Show infra topology", handler: async (_args, ctx) => { ctx.ui.notify( "πŸ—ΊοΈ Infra Topology\n\n" + "Server: root@159.195.60.33\n\n" + "Containers:\n" + " cr-server-new (10.213.16.224) β€” CharityRight + Docker\n" + " qc-server-new (10.213.16.234) β€” QuikCue\n" + " qc-server β€” STOPPED\n\n" + "HAProxy:\n" + " charityright.* β†’ cr-server-new:443\n" + " quikcue.* β†’ qc-server-new:443\n" + " antivirus.quikcue.com β†’ localhost:8877\n\n" + "Services (on cr-server-new):\n" + " /opt/ayn-antivirus\n" + " /opt/enthuse-db-sync-v2\n" + " /opt/launchgood-sync\n" + " /root/legacy-donation-system-laravel\n" + " /root/redis-v2", "info" ); }, }); pi.registerCommand("health", { description: "Live check of containers and key services", handler: async (_args, ctx) => { ctx.ui.notify("⏳ Checking infra health...", "info"); try { const containers = runSSH("incus list --format=csv -c ns", 10000); const haproxy = runSSH("systemctl is-active haproxy", 5000); const docker = runSSH("incus exec cr-server-new -- docker ps --format '{{.Names}}\\t{{.Status}}' 2>/dev/null | head -10", 10000); const disk = runSSH("df -h / | tail -1 | awk '{print $5\" used of \"$2}'", 5000); const mem = runSSH("free -m | awk 'NR==2{printf \"%sMB / %sMB (%.0f%%)\", $3,$2,$3*100/$2}'", 5000); ctx.ui.notify( "βœ… Infrastructure Health\n\n" + `HAProxy: ${haproxy === "active" ? "βœ… active" : "❌ " + haproxy}\n\n` + `Containers:\n${containers.split("\n").map(l => " " + l).join("\n")}\n\n` + `Docker (cr-server-new):\n${docker.split("\n").map(l => " " + l).join("\n")}\n\n` + `Disk: ${disk}\n` + `RAM: ${mem}`, "info" ); } catch (e: any) { ctx.ui.notify(`❌ Health check failed: ${e.message}`, "error"); } }, }); pi.registerCommand("logs", { description: "Tail logs for a service (usage: /logs launchgood)", handler: async (args, ctx) => { const services: Record = { launchgood: "incus exec cr-server-new -- journalctl -u launchgood-sync -n 50 --no-pager 2>/dev/null || incus exec cr-server-new -- tail -50 /opt/launchgood-sync/logs/app.log 2>/dev/null", enthuse: "incus exec cr-server-new -- journalctl -u enthuse-sync -n 50 --no-pager 2>/dev/null || incus exec cr-server-new -- tail -50 /opt/enthuse-db-sync-v2/logs/app.log 2>/dev/null", antivirus: "journalctl -u ayn-antivirus -n 50 --no-pager 2>/dev/null", haproxy: "journalctl -u haproxy -n 50 --no-pager", laravel: "incus exec cr-server-new -- tail -50 /root/legacy-donation-system-laravel/storage/logs/laravel.log 2>/dev/null", }; const svc = args?.trim().toLowerCase(); if (!svc || !services[svc]) { const opts = Object.keys(services); const choice = await ctx.ui.select("Which service logs?", opts); if (!choice) return; const cmd = services[choice]; ctx.ui.notify("⏳ Fetching logs...", "info"); const out = runSSH(cmd, 15000); ctx.ui.notify(`πŸ“‹ ${choice} logs:\n\n${out.slice(-3000)}`, "info"); } else { ctx.ui.notify("⏳ Fetching logs...", "info"); const out = runSSH(services[svc], 15000); ctx.ui.notify(`πŸ“‹ ${svc} logs:\n\n${out.slice(-3000)}`, "info"); } }, }); pi.registerCommand("ssh", { description: "Run a raw SSH command on the server", handler: async (args, ctx) => { if (!args?.trim()) { ctx.ui.notify("Usage: /ssh \nExample: /ssh incus list", "warning"); return; } ctx.ui.notify(`⏳ Running: ${args.trim()}`, "info"); const out = runSSH(args.trim(), 20000); ctx.ui.notify(`$ ${args.trim()}\n\n${out}`, "info"); }, }); }