/** * Zendy — unified pi extension entry point. * * Provides: * - Tools (LLM-callable): zendy_ticket_get, zendy_ticket_search, * zendy_helm_get, zendy_kg_search, zendy_source_status * - Slash commands: /zendy-config, /zendy-status * - Session lifecycle: workspace dir + cleanup + orphan sweep * - Custom header on session start * * No external CLI dependencies (zcli, zendesk-kg) required. * All data access is through direct REST APIs. */ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { registerAllTools } from "./tools.js"; import { registerAllCommands } from "./commands.js"; import { mkdirSync, rmSync, readdirSync } from "node:fs"; import { join } from "node:path"; // ── Constants ────────────────────────────────────────────────────────── const BASE_DIR = "/tmp"; const SESSION_PREFIX = "zendy-session-"; const WIPE_PREFIXES = ["dify-", SESSION_PREFIX]; // ── Session workspace helpers ────────────────────────────────────────── function parseSessionDir(name: string): { pid: number } | null { const m = /^zendy-session-(\d+)-(\d+)$/.exec(name); return m ? { pid: parseInt(m[1]!, 10) } : null; } function isProcessAlive(pid: number): boolean { try { process.kill(pid, 0); return true; } catch (e) { return (e as NodeJS.ErrnoException).code === "EPERM"; } } function safeRmrf(path: string): boolean { try { rmSync(path, { recursive: true, force: true }); return true; } catch { return false; } } function sweepOrphans(base: string, exclude: string): string[] { let entries; try { entries = readdirSync(base, { withFileTypes: true }); } catch { return []; } const removed: string[] = []; for (const e of entries) { if (!e.isDirectory()) continue; const info = parseSessionDir(e.name); if (!info) continue; const full = join(base, e.name); if (full === exclude) continue; if (isProcessAlive(info.pid)) continue; if (safeRmrf(full)) removed.push(full); } return removed; } // ── ASCII header ─────────────────────────────────────────────────────── const HEADER = ` ███████╗███████╗███╗ ██╗██████╗ ██╗ ██╗ ╚══███╔╝██╔════╝████╗ ██║██╔══██╗╚██╗ ██╔╝ ███╔╝ █████╗ ██╔██╗ ██║██║ ██║ ╚████╔╝ ███╔╝ ██╔══╝ ██║╚██╗██║██║ ██║ ╚██╔╝ ███████╗███████╗██║ ╚████║██████╔╝ ██║ ╚══════╝╚══════╝╚═╝ ╚═══╝╚═════╝ ╚═╝ Dify Enterprise Support Harness `; // ── Extension entry ──────────────────────────────────────────────────── export default function (pi: ExtensionAPI) { // ── Tools & Commands ──────────────────────────────────────────────── registerAllTools(pi); registerAllCommands(pi); // ── Session lifecycle ─────────────────────────────────────────────── const ts = Date.now(); const sessionDir = join(BASE_DIR, `${SESSION_PREFIX}${process.pid}-${ts}`); let cleanedUp = false; function ensureDir(): void { try { mkdirSync(sessionDir, { recursive: true, mode: 0o700 }); } catch { // non-fatal } } function cleanupSession(): void { if (cleanedUp) return; cleanedUp = true; safeRmrf(sessionDir); // Do not rely on the human remembering manual cleanup: wipe standalone // source clones too when pi exits. The current session dir was already // removed above; this covers clones created directly under /tmp/dify-*. try { const entries = readdirSync(BASE_DIR, { withFileTypes: true }); for (const e of entries) { if (!e.isDirectory()) continue; if (!WIPE_PREFIXES.some((p) => e.name.startsWith(p))) continue; safeRmrf(join(BASE_DIR, e.name)); } } catch { // non-fatal } } process.on("exit", cleanupSession); process.on("SIGINT", () => { cleanupSession(); process.exit(130); }); process.on("SIGTERM", () => { cleanupSession(); process.exit(143); }); process.on("uncaughtException", (err) => { cleanupSession(); console.error(err); process.exit(1); }); pi.on("session_start", async (_event, ctx) => { ensureDir(); process.env["ZENDY_SRC_DIR"] = sessionDir; // Custom header if (ctx.hasUI) { ctx.ui.notify(HEADER, "info"); } // Sweep orphan session dirs const removed = sweepOrphans(BASE_DIR, sessionDir); if (ctx.hasUI && removed.length > 0) { ctx.ui.notify(`[zendy] swept ${removed.length} orphan session dir(s)`, "info"); } }); pi.on("session_shutdown", async () => { cleanupSession(); }); }