/**
 * pi-gateway - Hermes-style Messaging Gateway
 *
 * Architecture:
 * - Single background process
 * - Platform adapters (Discord, Telegram, etc.)
 * - Per-chat session management
 * - Background task support
 * - Security (allowlists, pairing)
 *
 * Usage:
 *   /gateway start [port]    - Start the gateway
 *   /gateway stop           - Stop the gateway
 *   /gateway status         - Show status
 *   /gateway pair <code>    - Approve pairing code
 */

import { homedir, platform } from "node:os";
import { join } from "node:path";
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
import { WebSocketServer, WebSocket } from "ws";
import { randomBytes, createHmac } from "node:crypto";

import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
import { Type } from "@sinclair/typebox";

import { initSessionStore, getOrCreateSession, listSessions, touchSession, type SessionConfig } from "./sessions/store.js";
import { initSecurityStore, isUserAllowed, approvePairingCode, generatePairingCode, listPendingPairingCodes, addToAllowlist, listAllowlistedUsers, type Platform } from "./security/auth.js";
import { initBackgroundTasks, startBackgroundTask, getPendingResultsForSession, markTaskDelivered, listTasks, type BackgroundTask } from "./background/manager.js";
import { DiscordAdapter } from "./adapters/discord.js";
import { TwitchAdapter } from "./adapters/twitch.js";
import { TelegramAdapter } from "./adapters/telegram.js";
import { SlackAdapter } from "./adapters/slack.js";
import { WhatsAppAdapter } from "./adapters/whatsapp.js";
import { BaseAdapter, type AdapterCallbacks, type PlatformMessage } from "./adapters/base.js";

const KOBOLD_DIR = join(homedir(), ".0xkobold");
const CONFIG_DIR = join(KOBOLD_DIR, "gateway");
const CONFIG_FILE = join(CONFIG_DIR, "config.json");

// Types
interface GatewayConfig {
  port: number;
  host: string;
  tokens: string[];
  corsOrigins: string[];
  enableWebSocket: boolean;
  enableHttp: boolean;
  security: {
    allowAll: boolean;
    requirePairing: boolean;
  };
  sessions: {
    resetPolicy: "daily" | "idle" | "both";
    dailyHour: number;
    idleMinutes: number;
  };
  platforms: {
    discord?: {
      enabled: boolean;
      botToken: string;
      guildId?: string;
    };
    twitch?: {
      enabled: boolean;
      clientId: string;
      clientSecret: string;
      channels?: string[];
    };
    telegram?: {
      enabled: boolean;
      token: string;
      mode?: "polling" | "webhook";
      webhookUrl?: string;
    };
    slack?: {
      enabled: boolean;
      webhookUrl?: string;
      botToken?: string;
    };
    whatsapp?: {
      enabled: boolean;
      sessionPath?: string;
      printQr?: boolean;
    };
  };
}

interface GatewayState {
  running: boolean;
  adapters: Map<string, BaseAdapter>;
  clients: Map<string, WebSocket>;
  sessions: Map<string, SessionConfig>;
}

const DEFAULT_CONFIG: GatewayConfig = {
  port: 3847,
  host: "localhost",
  tokens: [],
  corsOrigins: ["*"],
  enableWebSocket: true,
  enableHttp: true,
  security: {
    allowAll: true,
    requirePairing: false,
  },
  sessions: {
    resetPolicy: "idle",
    dailyHour: 4,
    idleMinutes: 1440,
  },
  platforms: {},
};

let config: GatewayConfig;
let state: GatewayState;
let server: ReturnType<typeof createServer> | null = null;
let wss: WebSocketServer | null = null;
let globalCtx: ExtensionContext | null = null;
let rpcProcess: any = null;
let cronInterval: ReturnType<typeof setInterval> | null = null;

// Pending RPC requests
interface PendingRequest {
  id: string;
  resolve: (msg: unknown) => void;
  reject: (err: Error) => void;
}
const pendingRequests: PendingRequest[] = [];

// Load/save config
function loadConfig(): GatewayConfig {
  try {
    if (existsSync(CONFIG_FILE)) {
      return { ...DEFAULT_CONFIG, ...JSON.parse(readFileSync(CONFIG_FILE, "utf-8")) };
    }
  } catch { /* ignore */ }
  return { ...DEFAULT_CONFIG };
}

function saveConfig(): void {
  try {
    if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
    writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
  } catch { /* ignore */ }
}

// Token auth
function verifyToken(token: string): boolean {
  if (config.tokens.length === 0) return true;
  return config.tokens.includes(token);
}

function authenticate(req: IncomingMessage): boolean {
  const auth = req.headers.authorization;
  if (!auth) return verifyToken("");
  if (auth.startsWith("Bearer ")) return verifyToken(auth.slice(7));
  return false;
}

// WebSocket helpers
function sendWs(ws: WebSocket, msg: object): void {
  if (ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify(msg));
  }
}

function broadcastClients(event: string, data: unknown): void {
  for (const ws of state.clients.values()) {
    sendWs(ws, { type: event, data });
  }
}

function broadcastPlatform(platform: string, event: string, data: unknown): void {
  for (const [clientId, ws] of state.clients) {
    // Get client's session to check platform
    const session = Array.from(state.sessions.values()).find(s => s.userId === clientId);
    if (session?.platform === platform) {
      sendWs(ws, { type: event, data });
    }
  }
}

// RPC to pi agent
function createRpcProcess(): any {
  const { spawn } = require("node:child_process");

  const proc = spawn("pi", ["--mode", "rpc", "--json"], {
    stdio: ["pipe", "pipe", "pipe"],
    env: {
      ...process.env,
      OLLAMA_HOST: process.env.OLLAMA_HOST || "localhost:11434",
    },
  });

  proc.stdout?.on("data", (data: Buffer) => {
    const lines = data.toString().split("\n").filter(Boolean);
    for (const line of lines) {
      try {
        const msg = JSON.parse(line);

        if (msg.id) {
          const idx = pendingRequests.findIndex(r => r.id === msg.id);
          if (idx !== -1) {
            const req = pendingRequests.splice(idx, 1)[0];
            req.resolve(msg);
          }
        }

        // Broadcast events
        if (msg.type === "response") {
          broadcastClients("response", msg);
        } else {
          broadcastClients("event", msg);
        }
      } catch { /* not JSON */ }
    }
  });

  proc.stderr?.on("data", (data: Buffer) => {
    console.error("[gateway] pi stderr:", data.toString());
  });

  proc.on("exit", (code: number) => {
    console.log("[gateway] pi process exited");
    rpcProcess = null;
    broadcastClients("agent_disconnected", { code });
  });

  return proc;
}

async function sendRpc(command: string, data: Record<string, unknown> = {}): Promise<unknown> {
  if (!rpcProcess) throw new Error("pi agent not running");

  const id = randomBytes(8).toString("hex");
  const payload = { id, type: command, ...data };

  return new Promise((resolve, reject) => {
    pendingRequests.push({ id, resolve, reject });

    try {
      rpcProcess.stdin.write(JSON.stringify(payload) + "\n");
    } catch (err) {
      const idx = pendingRequests.findIndex(r => r.id === id);
      if (idx !== -1) pendingRequests.splice(idx, 1);
      reject(err);
    }

    setTimeout(() => {
      const idx = pendingRequests.findIndex(r => r.id === id);
      if (idx !== -1) {
        pendingRequests.splice(idx, 1);
        reject(new Error("Request timeout"));
      }
    }, 30000);
  });
}

// Platform adapter callbacks
const adapterCallbacks: AdapterCallbacks = {
  onMessage: async (message: PlatformMessage) => {
    // Get or create session for this chat
    const session = getOrCreateSession(
      message.platform,
      message.channelId,
      message.userId,
      {
        resetPolicy: config.sessions.resetPolicy,
        dailyHour: config.sessions.dailyHour,
        idleMinutes: config.sessions.idleMinutes,
      }
    );

    // Check allowlist
    if (!isUserAllowed(message.platform as Platform, message.userId)) {
      console.log(`[gateway] User ${message.userId} not in allowlist`);
      // Could send a DM here about pairing flow
      return;
    }

    // Store session reference
    state.sessions.set(`${message.platform}:${message.channelId}`, session);

    // Send to pi agent
    if (rpcProcess) {
      await sendRpc("prompt", {
        message: message.content,
        sessionId: session.id,
      });
    }
  },
  onDisconnect: () => {
    console.log("[gateway] Platform adapter disconnected");
    updateStatus();
  },
};

// Initialize platform adapters
async function initializeAdapters(): Promise<void> {
  // Discord
  if (config.platforms.discord?.enabled && config.platforms.discord.botToken) {
    try {
      const discord = new DiscordAdapter({
        enabled: true,
        platform: "discord",
        botToken: config.platforms.discord.botToken,
        guildId: config.platforms.discord.guildId,
      });
      await discord.initialize();
      await discord.start(adapterCallbacks);
      state.adapters.set("discord", discord);
      console.log("[gateway] Discord adapter started");
    } catch (err) {
      console.error("[gateway] Failed to start Discord adapter:", err);
    }
  }

  // Twitch
  if (config.platforms.twitch?.enabled && config.platforms.twitch.clientId && config.platforms.twitch.clientSecret) {
    try {
      const twitch = new TwitchAdapter({
        enabled: true,
        platform: "twitch",
        clientId: config.platforms.twitch.clientId,
        clientSecret: config.platforms.twitch.clientSecret,
        channels: config.platforms.twitch.channels,
      });
      await twitch.initialize();
      await twitch.start(adapterCallbacks);
      state.adapters.set("twitch", twitch);
      console.log("[gateway] Twitch adapter started");
    } catch (err) {
      console.error("[gateway] Failed to start Twitch adapter:", err);
    }
  }

  // Telegram
  if (config.platforms.telegram?.enabled && config.platforms.telegram.token) {
    try {
      const telegram = new TelegramAdapter({
        enabled: true,
        platform: "telegram",
        token: config.platforms.telegram.token,
        mode: config.platforms.telegram.mode,
        webhookUrl: config.platforms.telegram.webhookUrl,
      });
      await telegram.initialize();
      await telegram.start(adapterCallbacks);
      state.adapters.set("telegram", telegram);
      console.log("[gateway] Telegram adapter started");
    } catch (err) {
      console.error("[gateway] Failed to start Telegram adapter:", err);
    }
  }

  // Slack
  if (config.platforms.slack?.enabled && (config.platforms.slack.webhookUrl || config.platforms.slack.botToken)) {
    try {
      const slack = new SlackAdapter({
        enabled: true,
        platform: "slack",
        webhookUrl: config.platforms.slack.webhookUrl,
        botToken: config.platforms.slack.botToken,
      });
      await slack.initialize();
      await slack.start(adapterCallbacks);
      state.adapters.set("slack", slack);
      console.log("[gateway] Slack adapter started");
    } catch (err) {
      console.error("[gateway] Failed to start Slack adapter:", err);
    }
  }

  // WhatsApp
  if (config.platforms.whatsapp?.enabled) {
    try {
      const whatsapp = new WhatsAppAdapter({
        enabled: true,
        platform: "whatsapp",
        sessionPath: config.platforms.whatsapp.sessionPath,
        printQr: config.platforms.whatsapp.printQr,
      });
      await whatsapp.initialize();
      await whatsapp.start(adapterCallbacks);
      state.adapters.set("whatsapp", whatsapp);
      console.log("[gateway] WhatsApp adapter started");
    } catch (err) {
      console.error("[gateway] Failed to start WhatsApp adapter:", err);
    }
  }
}

// Cron job for background tasks and session cleanup
function startCron(): void {
  cronInterval = setInterval(async () => {
    // Check for pending background results
    for (const session of state.sessions.values()) {
      const pending = getPendingResultsForSession(session.id);
      for (const task of pending) {
        // Deliver result to user via their platform
        const adapter = state.adapters.get(session.platform);
        if (adapter) {
          const resultText = task.status === "completed"
            ? `✅ Background task completed:\n\`\`\`\n${JSON.stringify(task.result, null, 2)}\n\`\`\``
            : `❌ Background task failed:\n\`\`\`\n${task.error}\n\`\`\``;

          await adapter.sendMessage(session.channelId, resultText);
          markTaskDelivered(task.id);
        }
      }
    }

    // Touch active sessions
    for (const session of state.sessions.values()) {
      touchSession(session.id);
    }
  }, 60000); // Every 60 seconds (Hermes-style)
}

function stopCron(): void {
  if (cronInterval) {
    clearInterval(cronInterval);
    cronInterval = null;
  }
}

// HTTP handlers
async function handleHttpRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
  res.setHeader("Access-Control-Allow-Origin", config.corsOrigins.join(",") || "*");
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
  res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");

  if (req.method === "OPTIONS") {
    res.writeHead(204);
    res.end();
    return;
  }

  if (!authenticate(req)) {
    res.writeHead(401);
    res.end(JSON.stringify({ error: "Unauthorized" }));
    return;
  }

  const url = new URL(req.url || "/", `http://${req.headers.host}`);

  // API endpoints
  if (url.pathname === "/api/status" && req.method === "GET") {
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify({
      running: state.running,
      adapters: Array.from(state.adapters.keys()),
      clients: state.clients.size,
      sessions: state.sessions.size,
      agent: rpcProcess !== null,
    }));
    return;
  }

  if (url.pathname === "/api/sessions" && req.method === "GET") {
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify(listSessions()));
    return;
  }

  if (url.pathname === "/api/background" && req.method === "GET") {
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify(listTasks()));
    return;
  }

  if (url.pathname === "/api/allowlist" && req.method === "GET") {
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify(listAllowlistedUsers()));
    return;
  }

  if (url.pathname === "/api/pairing" && req.method === "GET") {
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify(listPendingPairingCodes()));
    return;
  }

  res.writeHead(404);
  res.end(JSON.stringify({ error: "Not found" }));
}

// WebSocket handler
function handleWebSocket(ws: WebSocket, req: IncomingMessage): void {
  if (!authenticate(req)) {
    ws.close(1008, "Unauthorized");
    return;
  }

  const clientId = randomBytes(8).toString("hex");
  state.clients.set(clientId, ws);

  console.log(`[gateway] WebSocket client connected: ${clientId}`);

  sendWs(ws, { type: "connected", data: { clientId } });

  ws.on("message", async (data) => {
    try {
      const msg = JSON.parse(data.toString());

      switch (msg.type) {
        case "prompt": {
          const result = await sendRpc("prompt", { message: msg.data?.message || "" });
          sendWs(ws, { type: "response", id: msg.id, data: result });
          break;
        }
        case "background": {
          const task = startBackgroundTask(msg.data?.sessionId || "default", msg.data?.command || "");
          sendWs(ws, { type: "background_started", data: task });
          break;
        }
        case "ping": {
          sendWs(ws, { type: "pong", data: { time: Date.now() } });
          break;
        }
      }
    } catch (err) {
      sendWs(ws, { type: "error", data: { error: String(err) } });
    }
  });

  ws.on("close", () => {
    state.clients.delete(clientId);
    console.log(`[gateway] WebSocket client disconnected: ${clientId}`);
  });
}

// Status update
function updateStatus(): void {
  if (!globalCtx) return;

  const adapterCount = state.adapters.size;
  const clientCount = state.clients.size;

  const statusText = state.running
    ? adapterCount > 0
      ? `🟢 Gateway (${adapterCount} platform${adapterCount !== 1 ? "s" : ""})`
      : `🟡 Gateway (waiting)`
    : "🔴 Gateway";

  globalCtx.ui.setStatus("gateway", statusText);
}

export default function (pi: ExtensionAPI) {
  config = loadConfig();
  state = {
    running: false,
    adapters: new Map(),
    clients: new Map(),
    sessions: new Map(),
  };

  // Initialize stores
  initSessionStore();
  initSecurityStore();
  initBackgroundTasks();

  // Register commands
  pi.registerCommand("gateway", {
    description: "Manage Hermes-style messaging gateway",
    getArgumentCompletions: (prefix: string) => {
      const cmds = ["start", "stop", "status", "restart", "pair", "allow", "sessions", "tasks", "config"];
      return cmds.filter(c => c.startsWith(prefix)).map(c => ({ value: c, label: c }));
    },
    handler: async (args, ctx) => {
      const parts = args.split(/\s+/).filter(Boolean);
      const subcmd = parts[0]?.toLowerCase();

      switch (subcmd) {
        case "start": {
          if (state.running) {
            ctx.ui.notify("Gateway already running", "info");
            return;
          }

          const port = parseInt(parts[1]) || config.port;

          // Start HTTP server
          server = createServer(handleHttpRequest);

          if (config.enableWebSocket) {
            wss = new WebSocketServer({ server });
            wss.on("connection", handleWebSocket);
          }

          server.listen(port, config.host, () => {
            console.log(`[gateway] HTTP server started on ${config.host}:${port}`);
          });

          // Start pi agent
          rpcProcess = createRpcProcess();

          // Initialize platform adapters
          await initializeAdapters();

          // Start cron
          startCron();

          state.running = true;
          updateStatus();

          ctx.ui.notify(
            `✅ Gateway started on http://${config.host}:${port}\n\n` +
            `Platforms: ${state.adapters.size > 0 ? Array.from(state.adapters.keys()).join(", ") : "none"}\n` +
            `Sessions: Idle reset every ${config.sessions.idleMinutes} min`,
            "info"
          );
          return;
        }

        case "stop": {
          if (!state.running) {
            ctx.ui.notify("Gateway not running", "info");
            return;
          }

          // Stop adapters
          for (const adapter of state.adapters.values()) {
            await adapter.stop();
          }
          state.adapters.clear();

          // Stop cron
          stopCron();

          // Close WebSocket clients
          for (const ws of state.clients.values()) {
            ws.close(1000, "Server shutting down");
          }
          state.clients.clear();

          // Stop HTTP server
          server?.close();
          server = null;
          wss = null;

          // Kill pi process
          if (rpcProcess) {
            rpcProcess.kill();
            rpcProcess = null;
          }

          state.running = false;
          updateStatus();

          ctx.ui.notify("Gateway stopped", "info");
          return;
        }

        case "status": {
          const lines: string[] = [];
          lines.push(`Status: ${state.running ? "🟢 Running" : "🔴 Stopped"}`);
          lines.push(`Port: ${config.port}`);
          lines.push(`Adapters: ${state.adapters.size}`);
          lines.push(`Clients: ${state.clients.size}`);
          lines.push(`Sessions: ${state.sessions.size}`);
          lines.push(`Agent: ${rpcProcess ? "✅ Connected" : "❌ Disconnected"}`);
          lines.push("");
          lines.push(`Session Reset: ${config.sessions.resetPolicy}`);
          lines.push(`  - Daily at ${config.sessions.dailyHour}:00`);
          lines.push(`  - Idle after ${config.sessions.idleMinutes} min`);
          lines.push("");
          lines.push(`Security: ${config.security.allowAll ? "Allow all" : "Allowlist only"}`);

          ctx.ui.setWidget("gateway-status", lines, { placement: "belowEditor" });
          setTimeout(() => ctx.ui.setWidget("gateway-status", undefined), 15000);
          return;
        }

        case "pair": {
          const code = parts[1]?.toUpperCase();
          if (!code) {
            const pending = listPendingPairingCodes();
            ctx.ui.notify(
              "Pending pairing codes:\n" +
              (pending.length > 0
                ? pending.map(p => `${p.code} - ${p.platform} (${Math.round(p.expiresIn / 60000)}min)`).join("\n")
                : "None"),
              "info"
            );
            return;
          }

          if (approvePairingCode(code)) {
            ctx.ui.notify("Pairing code approved", "info");
          } else {
            ctx.ui.notify(`❌ Invalid or expired pairing code`, "error");
          }
          return;
        }

        case "allow": {
          const platform = parts[1] as Platform;
          const userId = parts[2];
          if (!platform || !userId) {
            const list = listAllowlistedUsers();
            ctx.ui.notify(
              "Allowlisted users:\n" +
              (list.length > 0
                ? list.map(u => `${u.platform}:${u.userId}`).join("\n")
                : "None"),
              "info"
            );
            return;
          }

          addToAllowlist(platform, userId);
          ctx.ui.notify(`Added ${userId} to allowlist`, "info");
          return;
        }

        case "sessions": {
          const sessions = listSessions();
          ctx.ui.notify(
            "Active sessions:\n" +
            sessions.slice(0, 10).map(s =>
              `${s.platform}:${s.channelId} (${s.id.slice(0, 8)}...)`
            ).join("\n"),
            "info"
          );
          return;
        }

        case "tasks": {
          const tasks = listTasks();
          ctx.ui.notify(
            "Background tasks:\n" +
            tasks.slice(0, 10).map(t =>
              `${t.id.slice(0, 12)}... - ${t.status} (${t.progress}%)`
            ).join("\n"),
            "info"
          );
          return;
        }

        case "config": {
          ctx.ui.notify(
            `Gateway Config:\n\n` +
            `Port: ${config.port}\n` +
            `Sessions: ${config.sessions.resetPolicy}\n` +
            `Security: ${config.security.allowAll ? "Allow all" : "Allowlist"}\n` +
            `Discord: ${config.platforms.discord?.enabled ? "Enabled" : "Disabled"}`,
            "info"
          );
          return;
        }

        default: {
          ctx.ui.notify(
            "pi Gateway Commands:\n\n" +
            "  /gateway start [port]  - Start gateway\n" +
            "  /gateway stop         - Stop gateway\n" +
            "  /gateway restart      - Restart gateway\n" +
            "  /gateway status       - Show status\n" +
            "  /gateway pair <code>  - Approve pairing\n" +
            "  /gateway allow <p> <u>- Add user to allowlist\n" +
            "  /gateway sessions     - List sessions\n" +
            "  /gateway tasks        - List background tasks\n" +
            "  /gateway config       - Show config\n\n" +
            "Hermes-style features:\n" +
            "  - Per-chat sessions with reset policies\n" +
            "  - Platform adapters (Discord, etc.)\n" +
            "  - Background task support\n" +
            "  - Allowlist security",
            "info"
          );
        }
      }
    },
  });

  // Register tools
  pi.registerTool({
    name: "gateway_status",
    label: "Gateway Status",
    description: "Check Hermes-style gateway status",
    parameters: Type.Object({}),
    async execute(_toolCallId, _params, _signal, _onUpdate, _ctx) {
      return {
        content: [{
          type: "text",
          text: `Gateway: ${state.running ? "Running" : "Stopped"}\n` +
                `Adapters: ${state.adapters.size}\n` +
                `Clients: ${state.clients.size}\n` +
                `Sessions: ${state.sessions.size}\n` +
                `Agent: ${rpcProcess ? "Connected" : "Disconnected"}`
        }],
        details: { running: state.running, adapters: state.adapters.size, clients: state.clients.size, sessions: state.sessions.size },
      };
    },
  });

  pi.registerTool({
    name: "gateway_sessions",
    label: "Gateway Sessions",
    description: "List active gateway sessions",
    parameters: Type.Object({}),
    async execute(_toolCallId, _params, _signal, _onUpdate, _ctx) {
      const sessions = listSessions();
      return {
        content: [{
          type: "text",
          text: `Active sessions: ${sessions.length}\n` +
                JSON.stringify(sessions.map(s => ({
                  id: s.id.slice(0, 12),
                  platform: s.platform,
                  channel: s.channelId,
                  lastActivity: new Date(s.lastActivity).toISOString(),
                })), null, 2)
        }],
        details: { count: sessions.length },
      };
    },
  });

  pi.registerTool({
    name: "gateway_background_tasks",
    label: "Background Tasks",
    description: "List and manage background tasks",
    parameters: Type.Object({
      status: Type.Optional(Type.String()),
    }),
    async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
      const tasks = listTasks(params.status as any);
      return {
        content: [{
          type: "text",
          text: `Background tasks: ${tasks.length}\n` +
                JSON.stringify(tasks.map(t => ({
                  id: t.id.slice(0, 12),
                  status: t.status,
                  progress: t.progress,
                  command: t.command.slice(0, 50),
                })), null, 2)
        }],
        details: { count: tasks.length },
      };
    },
  });

  pi.registerTool({
    name: "gateway_pairing",
    label: "Gateway Pairing",
    description: "Generate or approve pairing codes",
    parameters: Type.Object({
      action: Type.Union([Type.Literal("generate"), Type.Literal("list"), Type.Literal("approve")]),
      platform: Type.Optional(Type.String()),
      userId: Type.Optional(Type.String()),
      code: Type.Optional(Type.String()),
    }),
    async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
      const { action, platform, userId, code } = params;
      switch (action) {
        case "generate": {
          if (!platform || !userId) {
            return { content: [{ type: "text", text: "platform and userId required" }], details: { error: true } };
          }
          const pairingCode = generatePairingCode(platform as Platform, userId);
          return {
            content: [{
              type: "text",
              text: `Pairing code: ${pairingCode}\n\nShare this code with the user to approve access.`
            }],
            details: { code: pairingCode },
          };
        }
        case "approve": {
          if (!code) {
            return { content: [{ type: "text", text: "code required" }], details: { error: true } };
          }
          const success = approvePairingCode(code);
          return {
            content: [{ type: "text", text: success ? "✅ Code approved" : "❌ Invalid/expired" }],
            details: { success },
          };
        }
        case "list": {
          const pending = listPendingPairingCodes();
          return {
            content: [{
              type: "text",
              text: `Pending codes: ${pending.length}\n` +
                    JSON.stringify(pending, null, 2)
            }],
            details: { count: pending.length },
          };
        }
      }
    },
  });

  // Notify on session start
  pi.on("session_start", async (_event, ctx) => {
    globalCtx = ctx;
    updateStatus();
  });

  console.log("[pi-gateway] Hermes-style gateway extension loaded");
}


/**
 * WhatsApp Adapter - Hermes-style WhatsApp platform adapter
 *
 * Features:
 * - WhatsApp Web protocol via Baileys
 * - QR code authentication
 * - Contact and group management
 * - Media messaging
 */

import { BaseAdapter, type PlatformMessage, type PlatformConfig } from "./base.js";

export interface WhatsAppConfig extends PlatformConfig {
  platform: "whatsapp";
  sessionPath?: string;
  printQr?: boolean;  // Print QR to console
  maxMessageLength?: number;
}

interface WhatsAppContact {
  id: string;
  name?: string;
  isGroup: boolean;
}

export class WhatsAppAdapter extends BaseAdapter {
  readonly platform = "whatsapp" as const;
  config: WhatsAppConfig;

  private sock: any = null;
  private connected = false;
  private qrCode: string | null = null;

  constructor(config: WhatsAppConfig) {
    super();
    this.config = {
      enabled: true,
      platform: "whatsapp",
      sessionPath: "./whatsapp-session",
      printQr: true,
      maxMessageLength: 4096,
      ...config,
    };
  }

  async initialize(): Promise<void> {
    try {
      const baileys = await import("@whiskeysockets/baileys");

      const { state, saveCreds } = await baileys.useMultiFileAuthState(this.config.sessionPath || "./whatsapp-session");

      this.sock = baileys.makeWASocket({
        auth: state,
        printQRInTerminal: this.config.printQr,
        defaultQueryTimeoutMs: 60 * 1000,
      });

      // Handle QR code
      this.sock.ev.on("qr", (qr: string) => {
        this.qrCode = qr;
        console.log("[WhatsApp] QR Code received - scan with WhatsApp app");
        console.log(qr);
      });

      // Handle connection update
      this.sock.ev.on("connection.update", ({ qr, connection }: any) => {
        if (qr) {
          this.qrCode = qr;
        }
        if (connection === "open") {
          this.connected = true;
          this.qrCode = null;
          console.log("[WhatsApp] Connected!");
        }
        if (connection === "close") {
          this.connected = false;
          console.log("[WhatsApp] Disconnected");
        }
      });

      // Handle credentials update
      this.sock.ev.on("creds.update", saveCreds);

      // Handle messages
      this.sock.ev.on("messages.upsert", ({ messages }: any) => {
        this.handleMessages(messages);
      });

      console.log("[WhatsApp] Initializing...");
    } catch (err) {
      console.error("[WhatsApp] Failed to initialize:", err);
      throw err;
    }
  }

  private handleMessages(messages: any[]): void {
    for (const msg of messages) {
      // Skip messages sent by us
      if (msg.key.fromMe) continue;

      const jid = msg.key.remoteJid;
      const isGroup = jid?.endsWith("@g.us");

      // Get message content
      const content =
        msg.message?.conversation ||
        msg.message?.extendedTextMessage?.text ||
        msg.message?.imageMessage?.caption ||
        "";

      if (!content) continue;

      const message: PlatformMessage = {
        id: msg.key.id || this.generateMessageId(),
        platform: "whatsapp",
        channelId: jid,
        userId: msg.key.participant || jid,
        content,
        timestamp: msg.messageTimestamp ? msg.messageTimestamp * 1000 : Date.now(),
        metadata: {
          isGroup,
          messageType: msg.message ? Object.keys(msg.message)[0] : "unknown",
          pushName: msg.pushName,
        },
      };

      this.emitMessage(message);
    }
  }

  async start(callbacks): Promise<void> {
    await super.start(callbacks);

    // Wait for connection
    let attempts = 0;
    while (!this.connected && attempts < 30) {
      await this.sleep(1000);
      attempts++;
    }

    if (!this.connected) {
      console.warn("[WhatsApp] Not yet connected - waiting for QR scan");
    }
  }

  private sleep(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  async stop(): Promise<void> {
    if (this.sock) {
      await this.sock.logout();
      this.sock = null;
    }
    this.connected = false;
    await super.stop();
  }

  async sendMessage(channelId: string, content: string): Promise<string> {
    if (!this.sock || !this.connected) {
      throw new Error("WhatsApp not connected");
    }

    // Truncate if too long
    const text = content.length > (this.config.maxMessageLength || 4096)
      ? content.slice(0, this.config.maxMessageLength - 3) + "..."
      : content;

    try {
      const result = await this.sock.sendMessage(channelId, { text });
      return result?.key?.id || this.generateMessageId();
    } catch (err) {
      console.error("[WhatsApp] Send error:", err);
      throw err;
    }
  }

  async sendImage(channelId: string, imageUrl: string, caption?: string): Promise<string> {
    if (!this.sock || !this.connected) {
      throw new Error("WhatsApp not connected");
    }

    try {
      const result = await this.sock.sendMessage(channelId, {
        image: { url: imageUrl },
        caption,
      });
      return result?.key?.id || this.generateMessageId();
    } catch (err) {
      console.error("[WhatsApp] Send image error:", err);
      throw err;
    }
  }

  async sendReaction(channelId: string, messageId: string, emoji: string): Promise<void> {
    if (!this.sock || !this.connected) {
      throw new Error("WhatsApp not connected");
    }

    try {
      await this.sock.sendMessage(channelId, {
        react: { text: emoji, key: { remoteJid: channelId, id: messageId } },
      });
    } catch (err) {
      console.error("[WhatsApp] Reaction error:", err);
    }
  }

  async reply(channelId: string, content: string, messageId: string): Promise<string> {
    if (!this.sock || !this.connected) {
      throw new Error("WhatsApp not connected");
    }

    try {
      const result = await this.sock.sendMessage(channelId, {
        text: content,
        contextInfo: {
          stanzaId: messageId,
          remoteJid: channelId,
        },
      });
      return result?.key?.id || this.generateMessageId();
    } catch (err) {
      console.error("[WhatsApp] Reply error:", err);
      throw err;
    }
  }

  async editMessage(channelId: string, messageId: string, content: string): Promise<void> {
    if (!this.sock || !this.connected) {
      throw new Error("WhatsApp not connected");
    }

    try {
      await this.sock.relayMessage(channelId, {
        protocolMessage: {
          type: 6, // MESSAGE_EDIT
          key: { remoteJid: channelId, id: messageId },
          editedMessage: { conversation: [{ text: content }] },
        },
      }, {});
    } catch (err) {
      console.error("[WhatsApp] Edit error:", err);
    }
  }

  async deleteMessage(channelId: string, messageId: string): Promise<void> {
    if (!this.sock || !this.connected) {
      throw new Error("WhatsApp not connected");
    }

    try {
      await this.sock.sendMessage(channelId, {
        delete: { remoteJid: channelId, id: messageId },
      });
    } catch (err) {
      console.error("[WhatsApp] Delete error:", err);
    }
  }

  async setTyping(channelId: string, isTyping: boolean): Promise<void> {
    if (!this.sock || !this.connected) return;

    try {
      await this.sock.sendPresenceUpdate(isTyping ? "composing" : "available", channelId);
    } catch (err) {
      // Ignore presence errors
    }
  }

  async getStatus(): Promise<{ connected: boolean; latency?: number }> {
    return { connected: this.connected };
  }

  async getContacts(): Promise<WhatsAppContact[]> {
    if (!this.sock?.store?.contacts) {
      return [];
    }

    return Object.entries(this.sock.store.contacts).map(([id, contact]: [string, any]) => ({
      id,
      name: contact?.name || contact?.notify || id.split("@")[0],
      isGroup: id.endsWith("@g.us"),
    }));
  }

  async getContact(jid: string): Promise<WhatsAppContact | null> {
    const contacts = await this.getContacts();
    return contacts.find(c => c.id === jid) || null;
  }

  getQrCode(): string | null {
    return this.qrCode;
  }
}


/**
 * Background Task Manager - Hermes-style background session handling
 *
 * Features:
 * - Isolated sessions for background tasks
 * - Result delivery to parent session
 * - Progress notifications
 * - Timeout handling
 */

import { Database } from "bun:sqlite";
import { join } from "path";
import { homedir } from "os";
import { existsSync, mkdirSync } from "fs";
import { spawn, type ChildProcess } from "node:child_process";
import { createBackgroundSession, type SessionConfig } from "../sessions/store.js";
import { randomBytes } from "node:crypto";

export type BackgroundStatus = "running" | "completed" | "failed" | "timeout" | "delivered";

export interface BackgroundTask {
  id: string;
  sessionId: string;
  parentSessionId: string;
  command: string;
  status: BackgroundStatus;
  progress: number;
  progressMessage?: string;
  result?: unknown;
  error?: string;
  createdAt: number;
  startedAt?: number;
  completedAt?: number;
  deliveredAt?: number;
}

interface TaskRow {
  id: string;
  session_id: string;
  parent_session_id: string;
  command: string;
  status: string;
  progress: number;
  progress_message: string | null;
  result: string | null;
  error: string | null;
  created_at: number;
  started_at: number | null;
  completed_at: number | null;
  delivered_at: number | null;
}

const KOBOLD_DIR = join(homedir(), ".0xkobold");
const TASKS_DB = join(KOBOLD_DIR, "gateway-background-tasks.db");

let db: Database | null = null;
const runningProcesses: Map<string, ChildProcess> = new Map();
const progressCallbacks: Map<string, (task: BackgroundTask) => void> = new Map();

/**
 * Initialize background tasks database
 */
export function initBackgroundTasks(): Database {
  if (db) return db;

  if (!existsSync(KOBOLD_DIR)) {
    mkdirSync(KOBOLD_DIR, { recursive: true });
  }

  db = new Database(TASKS_DB);
  db.run("PRAGMA journal_mode = WAL;");

  // Tasks table
  db.run(`
    CREATE TABLE IF NOT EXISTS background_tasks (
      id TEXT PRIMARY KEY,
      session_id TEXT NOT NULL,
      parent_session_id TEXT NOT NULL,
      command TEXT NOT NULL,
      status TEXT NOT NULL DEFAULT 'running',
      progress INTEGER NOT NULL DEFAULT 0,
      progress_message TEXT,
      result TEXT,
      error TEXT,
      created_at INTEGER NOT NULL,
      started_at INTEGER,
      completed_at INTEGER,
      delivered_at INTEGER
    )
  `);

  // Indexes
  db.run(`CREATE INDEX IF NOT EXISTS idx_tasks_parent ON background_tasks(parent_session_id)`);
  db.run(`CREATE INDEX IF NOT EXISTS idx_tasks_status ON background_tasks(status)`);

  console.log("[BackgroundTasks] Database initialized");
  return db;
}

/**
 * Start a background task
 */
export function startBackgroundTask(
  parentSessionId: string,
  command: string,
  onProgress?: (task: BackgroundTask) => void
): BackgroundTask {
  const database = initBackgroundTasks();

  const id = `bg-${Date.now()}-${randomBytes(4).toString("hex")}`;
  const now = Date.now();

  // Create background session
  const session = createBackgroundSession("background", id, "system", parentSessionId);

  // Create task record
  const task: BackgroundTask = {
    id,
    sessionId: session.id,
    parentSessionId,
    command,
    status: "running",
    progress: 0,
    createdAt: now,
  };

  database.run(`
    INSERT INTO background_tasks
    (id, session_id, parent_session_id, command, status, progress, created_at)
    VALUES (?, ?, ?, ?, ?, ?, ?)
  `, [id, session.id, parentSessionId, command, "running", 0, now]);

  // Register progress callback
  if (onProgress) {
    progressCallbacks.set(id, onProgress);
  }

  // Start the actual process
  const proc = spawn("pi", ["--json", "--no-stream", command], {
    stdio: ["pipe", "pipe", "pipe"],
    env: { ...process.env },
  });

  runningProcesses.set(id, proc);

  let stdout = "";
  let stderr = "";

  proc.stdout?.on("data", (data: Buffer) => {
    stdout += data.toString();

    // Parse progress updates
    try {
      const lines = stdout.split("\n").filter(Boolean);
      for (const line of lines) {
        const parsed = JSON.parse(line);
        if (parsed.progress !== undefined) {
          updateTaskProgress(id, parsed.progress, parsed.message);
        }
      }
    } catch {
      // Not JSON yet
    }
  });

  proc.stderr?.on("data", (data: Buffer) => {
    stderr += data.toString();
  });

  proc.on("close", (code) => {
    runningProcesses.delete(id);

    if (code === 0) {
      try {
        const result = stdout.trim() ? JSON.parse(stdout) : { success: true };
        completeTask(id, result);
      } catch {
        completeTask(id, { success: true, output: stdout });
      }
    } else {
      failTask(id, stderr || `Process exited with code ${code}`);
    }
  });

  proc.on("error", (err) => {
    runningProcesses.delete(id);
    failTask(id, err.message);
  });

  console.log(`[BackgroundTasks] Started task ${id.slice(0, 12)}...`);
  return task;
}

/**
 * Update task progress
 */
export function updateTaskProgress(taskId: string, progress: number, message?: string): void {
  const database = initBackgroundTasks();

  database.run(`
    UPDATE background_tasks SET progress = ?, progress_message = ?
    WHERE id = ?
  `, [progress, message ?? null, taskId]);

  // Notify via callback
  const callback = progressCallbacks.get(taskId);
  if (callback) {
    const task = getTask(taskId);
    if (task) callback(task);
  }
}

/**
 * Complete a task successfully
 */
export function completeTask(taskId: string, result: unknown): void {
  const database = initBackgroundTasks();
  const now = Date.now();

  database.run(`
    UPDATE background_tasks
    SET status = 'completed', progress = 100, result = ?, completed_at = ?
    WHERE id = ?
  `, [JSON.stringify(result), now, taskId]);

  console.log(`[BackgroundTasks] Task ${taskId.slice(0, 12)}... completed`);
}

/**
 * Fail a task
 */
export function failTask(taskId: string, error: string): void {
  const database = initBackgroundTasks();
  const now = Date.now();

  database.run(`
    UPDATE background_tasks
    SET status = 'failed', error = ?, completed_at = ?
    WHERE id = ?
  `, [error, now, taskId]);

  console.log(`[BackgroundTasks] Task ${taskId.slice(0, 12)}... failed: ${error}`);
}

/**
 * Mark task as delivered to user
 */
export function markTaskDelivered(taskId: string): void {
  const database = initBackgroundTasks();
  database.run(`
    UPDATE background_tasks SET status = 'delivered', delivered_at = ?
    WHERE id = ?
  `, [Date.now(), taskId]);
}

/**
 * Get task by ID
 */
export function getTask(taskId: string): BackgroundTask | null {
  const database = initBackgroundTasks();
  const row = database.query("SELECT * FROM background_tasks WHERE id = ?").get(taskId) as TaskRow | undefined;
  return row ? rowToTask(row) : null;
}

/**
 * Get pending results for parent session
 */
export function getPendingResultsForSession(parentSessionId: string): BackgroundTask[] {
  const database = initBackgroundTasks();

  const rows = database.query(`
    SELECT * FROM background_tasks
    WHERE parent_session_id = ? AND status IN ('completed', 'failed')
    AND delivered_at IS NULL
    ORDER BY created_at ASC
  `).all(parentSessionId) as TaskRow[];

  return rows.map(rowToTask);
}

/**
 * List all tasks
 */
export function listTasks(status?: BackgroundStatus): BackgroundTask[] {
  const database = initBackgroundTasks();

  const query = status
    ? "SELECT * FROM background_tasks WHERE status = ? ORDER BY created_at DESC"
    : "SELECT * FROM background_tasks ORDER BY created_at DESC";

  const rows = status
    ? database.query(query).all(status) as TaskRow[]
    : database.query(query).all() as TaskRow[];

  return rows.map(rowToTask);
}

/**
 * Cancel a running task
 */
export function cancelTask(taskId: string): boolean {
  const proc = runningProcesses.get(taskId);
  if (proc) {
    proc.kill("SIGTERM");
    runningProcesses.delete(taskId);

    const database = initBackgroundTasks();
    database.run(`
      UPDATE background_tasks SET status = 'failed', error = ?, completed_at = ?
      WHERE id = ?
    `, ["Cancelled by user", Date.now(), taskId]);

    return true;
  }
  return false;
}

/**
 * Clean up old tasks
 */
export function cleanupOldTasks(maxAgeMs: number = 7 * 24 * 60 * 60 * 1000): number {
  const database = initBackgroundTasks();
  const cutoff = Date.now() - maxAgeMs;
  const result = database.run("DELETE FROM background_tasks WHERE created_at < ?", [cutoff]);
  return result.changes;
}

// Helper to convert DB row to BackgroundTask
function rowToTask(row: TaskRow): BackgroundTask {
  return {
    id: row.id,
    sessionId: row.session_id,
    parentSessionId: row.parent_session_id,
    command: row.command,
    status: row.status as BackgroundStatus,
    progress: row.progress,
    progressMessage: row.progress_message ?? undefined,
    result: row.result ? JSON.parse(row.result) : undefined,
    error: row.error ?? undefined,
    createdAt: row.created_at,
    startedAt: row.started_at ?? undefined,
    completedAt: row.completed_at ?? undefined,
    deliveredAt: row.delivered_at ?? undefined,
  };
}


import * as fs from "fs";
import * as path from "path";
import * as qrcode from "qrcode-terminal";
import type { ChallengeAuth } from "../auth/challenge-auth.js";
import type { ExternalMessage } from "../types.js";
import type { ITransportProvider } from "./interface.js";

// Dynamic import for ESM modules
type WASocket = any;

async function loadBaileys() {
  const baileys = await import("@whiskeysockets/baileys");
  return baileys;
}

/**
 * WhatsApp transport provider using @whiskeysockets/baileys
 */
export class WhatsAppProvider implements ITransportProvider {
  readonly type = "whatsapp";
  private socket: WASocket | null = null;
  private _isConnected = false;
  private authPath: string;
  private messageHandler?: (message: ExternalMessage) => void;
  private errorHandler?: (error: Error) => void;
  private lastProcessedMessageId = "";
  private debug: boolean;
  private isManualConnect = false;

  constructor(config: { authPath?: string; debug?: boolean },
    private auth: ChallengeAuth
  ) {
    // Default auth path to ~/.pi/msg-bridge-whatsapp-auth
    this.authPath = config.authPath || path.join(
      process.env.HOME || process.env.USERPROFILE || ".",
      ".pi",
      "msg-bridge-whatsapp-auth"
    );
    this.debug = config.debug || false;
  }

  get isConnected(): boolean {
    return this._isConnected;
  }

  async connect(manual: boolean = false): Promise<void> {
    if (this._isConnected) return;
    this.isManualConnect = manual;

    // Ensure auth directory exists
    if (!fs.existsSync(this.authPath)) {
      fs.mkdirSync(this.authPath, { recursive: true, mode: 0o700 });
    } else {
      // Ensure existing directory has secure permissions
      try {
        fs.chmodSync(this.authPath, 0o700);
      } catch (err) {
        console.warn("Failed to set auth directory permissions:", err);
      }
    }

    const baileys = await loadBaileys();
    const { state, saveCreds } = await baileys.useMultiFileAuthState(this.authPath);

    // Suppress Baileys logger unless debug mode
    const silentLogger: any = {
      level: 'silent' as const,
      child: () => silentLogger,
      trace: () => {},
      debug: () => {},
      info: () => {},
      warn: () => {},
      error: () => {},
      fatal: () => {},
    };

    this.socket = baileys.default({
      auth: state,
      printQRInTerminal: false, // We'll handle QR display ourselves
      logger: this.debug ? undefined : silentLogger,
    });

    // Handle connection updates - set up IMMEDIATELY after socket creation
    this.socket.ev.on("connection.update", async (update: any) => {
      const { connection, lastDisconnect, qr } = update;

      if (this.debug) {
        console.log("[WhatsApp] connection.update event received:", JSON.stringify(update, null, 2));
      }

      if (qr && this.isManualConnect) {
        console.log("\n📱 Scan this QR code with WhatsApp to authenticate:\n");
        qrcode.generate(qr, { small: true });
        console.log("\n");
      }

      if (connection === "close") {
        const statusCode = (lastDisconnect?.error as any)?.output?.statusCode;
        const shouldReconnect = statusCode !== baileys.DisconnectReason.loggedOut;

        if (this.debug) {
          console.log(`[WhatsApp] Connection closed. Status: ${statusCode}, Reconnect: ${shouldReconnect}`);
        }

        this._isConnected = false;

        if (this.errorHandler && statusCode !== baileys.DisconnectReason.loggedOut) {
          this.errorHandler(new Error(`connection closed (status ${statusCode})`));
        }

        // If unauthorized (401), clear invalid auth
        if (statusCode === 401) {
          try {
            if (fs.existsSync(this.authPath)) {
              fs.rmSync(this.authPath, { recursive: true, force: true });
              if (this.isManualConnect) {
                console.log("🔄 WhatsApp session expired. Cleared credentials. Reconnecting with QR code...");
                // Reconnect after clearing auth to get fresh QR (manual configure only)
                setTimeout(() => this.connect(true), 1000);
              } else {
                console.log("🔄 WhatsApp session expired. Cleared credentials. Run /msg-bridge configure whatsapp to reconnect.");
              }
            }
          } catch (err) {
            console.error("Failed to clear auth:", err);
          }
        } else if (shouldReconnect) {
          // Reconnect after 3 seconds
          setTimeout(() => this.connect(), 3000);
        } else if (this.debug) {
          console.warn("⚠ WhatsApp logged out. Run /msg-bridge configure whatsapp to reconnect.");
        }
      } else if (connection === "open") {
        this._isConnected = true;
        console.log("✅ WhatsApp connected!");
      }
    });

    // Save credentials when updated
    this.socket.ev.on("creds.update", saveCreds);

    // Handle incoming messages
    this.socket.ev.on("messages.upsert", async ({ messages, type }: any) => {
      if (type !== "notify") {
        return;
      }

      for (const msg of messages) {
        await this.handleMessage(msg);
      }
    });
  }

  async disconnect(): Promise<void> {
    if (!this._isConnected || !this.socket) return;

    this.socket.end(undefined);
    this.socket = null;
    this._isConnected = false;
    console.log("[WhatsApp] Disconnected");
  }

  async sendMessage(chatId: string, text: string): Promise<void> {
    if (!this.socket) {
      throw new Error("WhatsApp not connected");
    }

    await this.socket.sendMessage(chatId, { text });
  }

  async sendTyping(chatId: string): Promise<void> {
    if (!this.socket) return;

    try {
      await this.socket.sendPresenceUpdate("composing", chatId);
      // Auto-stop composing after 3 seconds
      setTimeout(async () => {
        try {
          await this.socket?.sendPresenceUpdate("paused", chatId);
        } catch {
          // Ignore
        }
      }, 3000);
    } catch (error) {
      console.warn("[WhatsApp] Failed to send typing:", error);
    }
  }

  onMessage(handler: (message: ExternalMessage) => void): void {
    this.messageHandler = handler;
  }

  onError(handler: (error: Error) => void): void {
    this.errorHandler = handler;
  }

  private async handleMessage(msg: any): Promise<void> {
    // Debug: log message structure
    if (this.debug) {
      console.log("[WhatsApp] Message received:", JSON.stringify(msg, null, 2));
    }

    // Skip non-text messages
    const text =
      msg.message?.conversation || msg.message?.extendedTextMessage?.text;
    if (!text) {
      return;
    }

    const messageId = msg.key.id || String(Date.now());

    // Filter out old/duplicate messages
    if (messageId === this.lastProcessedMessageId) {
      return;
    }
    this.lastProcessedMessageId = messageId;

    const chatId = msg.key.remoteJid || "";
    // Try multiple fields for username
    const pushName = msg.pushName || msg.verifiedBizName || msg.key.fromMe ? "You" : "Unknown";

    // Skip status broadcasts
    if (chatId === "status@broadcast") {
      return;
    }

    // Detect if DM or group
    // Group JIDs end with @g.us, individual JIDs end with @s.whatsapp.net
    const isGroupChat = chatId.endsWith("@g.us");
    const userId = chatId.split("@")[0]; // Phone number

    // For WhatsApp, mentions work differently - we'll consider it as not mentioned for now
    const wasMentioned = false;

    // Check authorization
    const sendMessageToUser = async (cId: string, text: string) => {
      if (this.socket) {
        await this.socket.sendMessage(cId, { text });
      }
    };

    const isAuthorized = await this.auth.checkAuthorization(
      userId,
      chatId,
      pushName,
      isGroupChat,
      wasMentioned,
      sendMessageToUser,
      this.type
    );

    // Handle admin commands and challenge codes in DM
    if (!isGroupChat && (text.startsWith("/") || text.match(/^\d{6}$/))) {
      const handled = await this.auth.handleAdminCommand(
        text,
        chatId,
        userId,
        async (text) => await this.sendMessage(chatId, text),
        this.type
      );
      if (handled) {
        return;
      }
    }

    if (!isAuthorized) {
      return; // Auth handler already sent challenge/error messages
    }

    // Forward to message handler
    if (this.messageHandler) {
      const externalMessage: ExternalMessage = {
        chatId,
        transport: this.type,
        content: text.trim(),
        username: pushName,
        userId,
        timestamp: new Date(Number(msg.messageTimestamp) * 1000),
        messageId,
        isGroupChat,
        wasMentioned,
      };

      this.messageHandler(externalMessage);
    }
  }
}


**Investigating package issues**

I need to check if the gateway is using `require` in ESM TypeScript, which could be a compile bug. There's also an undeclared issue with bun:sqlite at runtime. I see a version mismatch: the npm version is 0.6.0, but the package states 0.4.0. That feels suspicious. The user asked about auditing extensions, which might be enough, but I also think I need to investigate runtime issues further. Checking the npm package contents could help clarify everything!

bash

set -e
work=/tmp/pi-gateway-npm-inspect
rm -rf "$work"
mkdir -p "$work"
cd "$work"
npm pack @0xkobold/pi-gateway >/dev/null
pkg=$(ls *.tgz)
tar -xzf "$pkg"
echo '--- packed package.json ---'
cat package/package.json
printf '\n--- packed files ---\n'
find package -maxdepth 3 -type f | sed 's#^package/##' | sort | head -200


npm notice
npm notice 📦  @0xkobold/pi-gateway@0.6.0
npm notice Tarball Contents
npm notice 6.3kB README.md
npm notice 2.5kB dist/adapters/base.d.ts
npm notice 731B dist/adapters/base.js
npm notice 786B dist/adapters/base.js.map
npm notice 1.6kB dist/adapters/discord.d.ts
npm notice 7.6kB dist/adapters/discord.js
npm notice 6.7kB dist/adapters/discord.js.map
npm notice 721B dist/adapters/index.d.ts
npm notice 358B dist/adapters/index.js
npm notice 404B dist/adapters/index.js.map
npm notice 1.6kB dist/adapters/slack.d.ts
npm notice 7.6kB dist/adapters/slack.js
npm notice 7.1kB dist/adapters/slack.js.map
npm notice 1.8kB dist/adapters/telegram.d.ts
npm notice 8.9kB dist/adapters/telegram.js
npm notice 8.0kB dist/adapters/telegram.js.map
npm notice 2.3kB dist/adapters/twitch.d.ts
npm notice 9.0kB dist/adapters/twitch.js
npm notice 7.5kB dist/adapters/twitch.js.map
npm notice 1.1kB dist/adapters/websocket.d.ts
npm notice 4.5kB dist/adapters/websocket.js
npm notice 4.2kB dist/adapters/websocket.js.map
npm notice 1.7kB dist/adapters/whatsapp.d.ts
npm notice 8.4kB dist/adapters/whatsapp.js
npm notice 8.0kB dist/adapters/whatsapp.js.map
npm notice 3.2kB dist/api.d.ts
npm notice 19.4kB dist/api.js
npm notice 17.9kB dist/api.js.map
npm notice 188B dist/background/index.d.ts
npm notice 177B dist/background/index.js
npm notice 214B dist/background/index.js.map
npm notice 1.5kB dist/background/manager.d.ts
npm notice 7.4kB dist/background/manager.js
npm notice 6.9kB dist/background/manager.js.map
npm notice 506B dist/db.d.ts
npm notice 2.5kB dist/db.js
npm notice 2.8kB dist/db.js.map
npm notice 956B dist/index.d.ts
npm notice 14.2kB dist/index.js
npm notice 11.1kB dist/index.js.map
npm notice 1.5kB dist/security/auth.d.ts
npm notice 6.0kB dist/security/auth.js
npm notice 5.2kB dist/security/auth.js.map
npm notice 215B dist/security/index.d.ts
npm notice 233B dist/security/index.js
npm notice 246B dist/security/index.js.map
npm notice 160B dist/sessions/index.d.ts
npm notice 155B dist/sessions/index.js
npm notice 208B dist/sessions/index.js.map
npm notice 1.3kB dist/sessions/store.d.ts
npm notice 7.0kB dist/sessions/store.js
npm notice 5.8kB dist/sessions/store.js.map
npm notice 1.4kB package.json
npm notice 2.8kB src/adapters/base.ts
npm notice 7.7kB src/adapters/discord.ts
npm notice 721B src/adapters/index.ts
npm notice 8.0kB src/adapters/slack.ts
npm notice 9.4kB src/adapters/telegram.ts
npm notice 10.0kB src/adapters/twitch.ts
npm notice 4.4kB src/adapters/websocket.ts
npm notice 8.0kB src/adapters/whatsapp.ts
npm notice 20.5kB src/api.ts
npm notice 188B src/background/index.ts
npm notice 8.4kB src/background/manager.ts
npm notice 2.6kB src/db.ts
npm notice 12.6kB src/index.ts
npm notice 7.1kB src/security/auth.ts
npm notice 215B src/security/index.ts
npm notice 160B src/sessions/index.ts
npm notice 7.8kB src/sessions/store.ts
npm notice Tarball Details
npm notice name: @0xkobold/pi-gateway
npm notice version: 0.6.0
npm notice filename: 0xkobold-pi-gateway-0.6.0.tgz
npm notice package size: 67.9 kB
npm notice unpacked size: 338.5 kB
npm notice shasum: fb391dcf2a85cca117edba7ea061527e4e6f5588
npm notice integrity: sha512-NcSuR+46Ro3HZ[...]NQf96oTM1Ldeg==
npm notice total files: 70
npm notice
--- packed package.json ---
{
  "name": "@0xkobold/pi-gateway",
  "version": "0.6.0",
  "description": "Hermes-style messaging gateway for pi - multi-platform agent with sessions, security, and background tasks",
  "author": "0xKobold",
  "license": "MIT",
  "keywords": ["pi-package", "pi-extension", "gateway", "messaging", "hermes", "multi-platform"],
  "type": "module",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/index.js",
      "types": "./dist/index.d.ts"
    },
    "./api": {
      "import": "./dist/api.js",
      "types": "./dist/api.d.ts"
    }
  },
  "files": [
    "dist/",
    "src/",
    "README.md"
  ],
  "scripts": {
    "build": "tsc",
    "dev": "tsc --watch",
    "test": "npx tsx tests/index.ts",
    "demo": "npx tsx tests/demo-server.ts",
    "client": "npx tsx tests/client-demo.ts",
    "prepublishOnly": "rm -rf dist && tsc"
  },
  "dependencies": {
    "sql.js": "^1.14.1",
    "ws": "^8.18.0"
  },
  "devDependencies": {
    "@types/sql.js": "^1.4.11",
    "@types/node": "^20.19.37",
    "@types/ws": "^8.18.1",
    "bun-types": "^1.3.11",
    "tsx": "^4.19.0",
    "typescript": "^6.0.2"
  },
  "peerDependencies": {
    "@mariozechner/pi-coding-agent": ">=0.65.0",
    "@sinclair/typebox": ">=0.32.0"
  },
  "pi": {
    "extensions": ["./dist/index.js"]
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/0xKobold/pi-gateway.git"
  }
}
--- packed files ---
dist/adapters/base.d.ts
dist/adapters/base.js
dist/adapters/base.js.map
dist/adapters/discord.d.ts
dist/adapters/discord.js
dist/adapters/discord.js.map
dist/adapters/index.d.ts
dist/adapters/index.js
dist/adapters/index.js.map
dist/adapters/slack.d.ts
dist/adapters/slack.js
dist/adapters/slack.js.map
dist/adapters/telegram.d.ts
dist/adapters/telegram.js
dist/adapters/telegram.js.map
dist/adapters/twitch.d.ts
dist/adapters/twitch.js
dist/adapters/twitch.js.map
dist/adapters/websocket.d.ts
dist/adapters/websocket.js
dist/adapters/websocket.js.map
dist/adapters/whatsapp.d.ts
dist/adapters/whatsapp.js
dist/adapters/whatsapp.js.map
dist/api.d.ts
dist/api.js
dist/api.js.map
dist/background/index.d.ts
dist/background/index.js
dist/background/index.js.map
dist/background/manager.d.ts
dist/background/manager.js
dist/background/manager.js.map
dist/db.d.ts
dist/db.js
dist/db.js.map
dist/index.d.ts
dist/index.js
dist/index.js.map
dist/security/auth.d.ts
dist/security/auth.js
dist/security/auth.js.map
dist/security/index.d.ts
dist/security/index.js
dist/security/index.js.map
dist/sessions/index.d.ts
dist/sessions/index.js
dist/sessions/index.js.map
dist/sessions/store.d.ts
dist/sessions/store.js
dist/sessions/store.js.map
package.json
README.md
src/adapters/base.ts
src/adapters/discord.ts
src/adapters/index.ts
src/adapters/slack.ts
src/adapters/telegram.ts
src/adapters/twitch.ts
src/adapters/websocket.ts
src/adapters/whatsapp.ts
src/api.ts
src/background/index.ts
src/background/manager.ts
src/db.ts
src/index.ts
src/security/auth.ts
src/security/index.ts
src/sessions/index.ts
src/sessions/store.ts


**Inspecting the source**

I need to check the distribution since it might be using sql.js instead of bun. It’s a bit odd that there's no README in the repository. I wonder if using a tarball could be better than dealing with the repo directly. I could also note that the repository looks outdated compared to the published package, and that presents a risk. I should inspect the dist/db to see if it really is using sql.js and not bun.


import initSqlJs from "sql.js";
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
import { dirname } from "path";
let SQL = null;
export class Database {
    db;
    dbPath;
    saveTimer = null;
    dirty = false;
    constructor(db, dbPath) {
        this.db = db;
        this.dbPath = dbPath;
    }
    static async open(dbPath) {
        if (!SQL)
            SQL = await initSqlJs();
        const dir = dirname(dbPath);
        if (!existsSync(dir))
            mkdirSync(dir, { recursive: true });
        let db;
        if (existsSync(dbPath)) {
            const buffer = readFileSync(dbPath);
            db = new SQL.Database(buffer);
        }
        else {
            db = new SQL.Database();
        }
        db.exec("PRAGMA journal_mode = WAL");
        return new Database(db, dbPath);
    }
    scheduleSave() {
        if (!this.dirty)
            return;
        if (this.saveTimer)
            clearTimeout(this.saveTimer);
        this.saveTimer = setTimeout(() => this.flush(), 2000);
    }
    flush() {
        if (!this.dirty)
            return;
        try {
            const data = this.db.export();
            writeFileSync(this.dbPath, Buffer.from(data));
            this.dirty = false;
        }
        catch (err) {
            console.error("[db] flush error:", err);
        }
    }
    run(sql, params = []) {
        this.db.run(sql, params);
        this.dirty = true;
        this.scheduleSave();
        return { changes: this.db.getRowsModified() };
    }
    query(sql) {
        return {
            get: (...params) => {
                const stmt = this.db.prepare(sql);
                stmt.bind(params);
                if (stmt.step()) {
                    const row = stmt.getAsObject();
                    stmt.free();
                    return row;
                }
                stmt.free();
                return undefined;
            },
            all: (...params) => {
                const results = [];
                const stmt = this.db.prepare(sql);
                stmt.bind(params);
                while (stmt.step()) {
                    results.push(stmt.getAsObject());
                }
                stmt.free();
                return results;
            },
        };
    }
    close() {
        if (this.saveTimer)
            clearTimeout(this.saveTimer);
        this.flush();
        this.db.close();
    }
}
//# sourceMappingURL=db.js.map


import { Database } from "../db.js";
import { join } from "path";
import { homedir } from "os";
import { spawn } from "node:child_process";
import { createBackgroundSession } from "../sessions/store.js";
import { randomBytes } from "node:crypto";
const KOBOLD_DIR = join(homedir(), ".0xkobold");
const TASKS_DB = join(KOBOLD_DIR, "gateway-background-tasks.db");
let db = null;
const runningProcesses = new Map();
const progressCallbacks = new Map();
export async function initBackgroundTasks() {
    if (db)
        return db;
    db = await Database.open(TASKS_DB);
    db.run(`
    CREATE TABLE IF NOT EXISTS background_tasks (
      id TEXT PRIMARY KEY,
      session_id TEXT NOT NULL,
      parent_session_id TEXT NOT NULL,
      command TEXT NOT NULL,
      status TEXT NOT NULL DEFAULT 'running',
      progress INTEGER NOT NULL DEFAULT 0,
      progress_message TEXT,
      result TEXT,
      error TEXT,
      created_at INTEGER NOT NULL,
      started_at INTEGER,
      completed_at INTEGER,
      delivered_at INTEGER
    )
  `);
    db.run(`CREATE INDEX IF NOT EXISTS idx_tasks_parent ON background_tasks(parent_session_id)`);
    db.run(`CREATE INDEX IF NOT EXISTS idx_tasks_status ON background_tasks(status)`);
    console.log("[BackgroundTasks] Database initialized");
    return db;
}
export async function startBackgroundTask(parentSessionId, command, onProgress) {
    const database = await initBackgroundTasks();
    const id = `bg-${Date.now()}-${randomBytes(4).toString("hex")}`;
    const now = Date.now();
    const session = await createBackgroundSession("background", id, "system", parentSessionId);
    const task = {
        id,
        sessionId: session.id,
        parentSessionId,
        command,
        status: "running",
        progress: 0,
        createdAt: now,
    };
    database.run(`
    INSERT INTO background_tasks
    (id, session_id, parent_session_id, command, status, progress, created_at)
    VALUES (?, ?, ?, ?, ?, ?, ?)
  `, [id, session.id, parentSessionId, command, "running", 0, now]);
    if (onProgress) {
        progressCallbacks.set(id, onProgress);
    }
    const proc = spawn("pi", ["--json", "--no-stream", command], {
        stdio: ["pipe", "pipe", "pipe"],
        env: { ...process.env },
    });
    runningProcesses.set(id, proc);
    let stdout = "";
    let stderr = "";
    proc.stdout?.on("data", (data) => {
        stdout += data.toString();
        try {
            const lines = stdout.split("\n").filter(Boolean);
            for (const line of lines) {
                const parsed = JSON.parse(line);
                if (parsed.progress !== undefined) {
                    updateTaskProgress(id, parsed.progress, parsed.message);
                }
            }
        }
        catch {
            // Not JSON yet
        }
    });
    proc.stderr?.on("data", (data) => {
        stderr += data.toString();
    });
    proc.on("close", (code) => {
        runningProcesses.delete(id);
        if (code === 0) {
            try {
                const result = stdout.trim() ? JSON.parse(stdout) : { success: true };
                completeTask(id, result);
            }
            catch {
                completeTask(id, { success: true, output: stdout });
            }
        }
        else {
            failTask(id, stderr || `Process exited with code ${code}`);
        }
    });
    proc.on("error", (err) => {
        runningProcesses.delete(id);
        failTask(id, err.message);
    });
    console.log(`[BackgroundTasks] Started task ${id.slice(0, 12)}...`);
    return task;
}
export async function updateTaskProgress(taskId, progress, message) {
    const database = await initBackgroundTasks();
    database.run(`
    UPDATE background_tasks SET progress = ?, progress_message = ?
    WHERE id = ?
  `, [progress, message ?? null, taskId]);
    const callback = progressCallbacks.get(taskId);
    if (callback) {
        const task = await getTask(taskId);
        if (task)
            callback(task);
    }
}
export async function completeTask(taskId, result) {
    const database = await initBackgroundTasks();
    const now = Date.now();
    database.run(`
    UPDATE background_tasks
    SET status = 'completed', progress = 100, result = ?, completed_at = ?
    WHERE id = ?
  `, [JSON.stringify(result), now, taskId]);
    console.log(`[BackgroundTasks] Task ${taskId.slice(0, 12)}... completed`);
}
export async function failTask(taskId, error) {
    const database = await initBackgroundTasks();
    const now = Date.now();
    database.run(`
    UPDATE background_tasks
    SET status = 'failed', error = ?, completed_at = ?
    WHERE id = ?
  `, [error, now, taskId]);
    console.log(`[BackgroundTasks] Task ${taskId.slice(0, 12)}... failed: ${error}`);
}
export async function markTaskDelivered(taskId) {
    const database = await initBackgroundTasks();
    database.run(`
    UPDATE background_tasks SET status = 'delivered', delivered_at = ?
    WHERE id = ?
  `, [Date.now(), taskId]);
}
export async function getTask(taskId) {
    const database = await initBackgroundTasks();
    const row = database.query("SELECT * FROM background_tasks WHERE id = ?").get(taskId);
    return row ? rowToTask(row) : null;
}
export async function getPendingResultsForSession(parentSessionId) {
    const database = await initBackgroundTasks();
    const rows = database.query(`
    SELECT * FROM background_tasks
    WHERE parent_session_id = ? AND status IN ('completed', 'failed')
    AND delivered_at IS NULL
    ORDER BY created_at ASC
  `).all(parentSessionId);
    return rows.map(rowToTask);
}
export async function listTasks(status) {
    const database = await initBackgroundTasks();
    const query = status
        ? "SELECT * FROM background_tasks WHERE status = ? ORDER BY created_at DESC"
        : "SELECT * FROM background_tasks ORDER BY created_at DESC";
    const rows = status
        ? database.query(query).all(status)
        : database.query(query).all();
    return rows.map(rowToTask);
}
export async function cancelTask(taskId) {
    const proc = runningProcesses.get(taskId);
    if (proc) {
        proc.kill("SIGTERM");
        runningProcesses.delete(taskId);
        const database = await initBackgroundTasks();
        database.run(`
      UPDATE background_tasks SET status = 'failed', error = ?, completed_at = ?
      WHERE id = ?
    `, ["Cancelled by user", Date.now(), taskId]);
        return true;
    }
    return false;
}
export async function cleanupOldTasks(maxAgeMs = 7 * 24 * 60 * 60 * 1000) {
    const database = await initBackgroundTasks();
    const cutoff = Date.now() - maxAgeMs;
    const result = database.run("DELETE FROM background_tasks WHERE created_at < ?", [cutoff]);
    return result.changes;
}
function rowToTask(row) {
    return {
        id: row.id,
        sessionId: row.session_id,
        parentSessionId: row.parent_session_id,
        command: row.command,
        status: row.status,
        progress: row.progress,
        progressMessage: row.progress_message ?? undefined,
        result: row.result ? JSON.parse(row.result) : undefined,
        error: row.error ?? undefined,
        createdAt: row.created_at,
        startedAt: row.started_at ?? undefined,
        completedAt: row.completed_at ?? undefined,
        deliveredAt: row.delivered_at ?? undefined,
    };
}
//# sourceMappingURL=manager.js.map


# Pi-Gateway

Hermes-style messaging gateway for [pi-coding-agent](https://github.com/badlogic/pi-mono) — multi-platform session management, security, and background tasks.

## Installation

```bash
pi install npm:@0xkobold/pi-gateway
```

Or as part of the meta-extension:

```bash
pi install npm:@0xkobold/pi-kobold
```

## Features

- **Per-Chat Sessions** — Unique session ID per platform/channel with reset policies
- **Reset Policies** — Daily (hour-based) and idle (minutes-based) session resets
- **Session Persistence** — Sessions survive restarts via SQLite (sql.js)
- **Background Tasks** — Isolated sessions for long-running commands
- **Security** — Per-platform user allowlists and DM pairing flow
- **Rate Limiting** — Configurable per-identifier rate limiting
- **Multi-Platform Adapters** — Discord, Telegram, Slack, WhatsApp, Twitch, WebSocket

## Architecture

```
┌──────────────────────────────────────────────────────┐
│                    Pi-Gateway                         │
├──────────────┬──────────────┬────────────────────────┤
│  Sessions    │  Security    │  Background Tasks      │
│  ──────────  │  ──────────  │  ──────────────────   │
│  • Per-chat  │  • Allowlist │  • Isolated sessions   │
│  • Reset     │  • Pairing   │  • Progress tracking   │
│  • Persist   │  • Rate limit│  • Result delivery     │
├──────────────┴──────────────┴────────────────────────┤
│              Platform Adapters                        │
│  Discord · Telegram · Slack · WhatsApp · Twitch      │
├──────────────────────────────────────────────────────┤
│              SQLite via sql.js                        │
└──────────────────────────────────────────────────────┘
```

## Commands

| Command | Description |
|---------|-------------|
| `/gateway start [port]` | Start the gateway |
| `/gateway stop` | Stop the gateway |
| `/gateway status` | Show status |
| `/gateway restart` | Restart the gateway |
| `/gateway pair <code>` | Approve a pairing code |
| `/gateway allow <platform> <userId>` | Add user to allowlist |
| `/gateway sessions` | List active sessions |
| `/gateway tasks` | List background tasks |
| `/gateway config` | Show configuration |

## Tools

| Tool | Description |
|------|-------------|
| `gateway_status` | Check gateway status |
| `gateway_sessions` | List active sessions |
| `gateway_background_tasks` | List and manage background tasks |
| `gateway_pairing` | Generate, list, or approve pairing codes |

## Programmatic API

Pi-gateway now exports a programmatic API so it can be started from outside a pi session:

```typescript
import { startGateway, stopGateway, isGatewayRunning, getStatus } from '@0xkobold/pi-gateway/api';

// Check if already running
const running = await isGatewayRunning(3847);

// Start the gateway
const status = await startGateway({ port: 3847, host: '0.0.0.0' });

// Get status
const current = getStatus();

// Stop
await stopGateway();
```

| Function | Description |
|----------|-------------|
| `startGateway(opts?)` | Start HTTP/WS server, adapters, and cron |
| `stopGateway()` | Stop all adapters, close server, kill RPC process |
| `isGatewayRunning(port?)` | Check if a gateway is already running on a port |
| `getStatus()` | Get current status (running, adapters, clients, sessions) |
| `getConfig()` | Get current gateway config |
| `isRunning()` | Check if gateway is running (sync) |
| `getAdapter(platform)` | Get a platform adapter by name |
| `getAdapters()` | Get all active adapters |
| `sendMessage(platform, channelId, content)` | Send a message through a platform adapter |
| `broadcast(event, data)` | Broadcast to all connected WebSocket clients |

### Start Options

```typescript
interface StartGatewayOptions {
  port?: number;       // Override config port
  host?: string;      // Override config host
  noAgent?: boolean;  // Don't spawn pi RPC process (when pi is already running)
}
```

## Session Reset Policies

- **idle** — Reset after N minutes of inactivity (default: 1440 = 24h)
- **daily** — Reset at a specific hour each day (default: 4:00)
- **both** — Reset on either condition

## Security

### Allowlist Mode

By default, all users are allowed (`allowAll: true`). To restrict access:

1. Set `allowAll: false` in config
2. Use pairing codes or `/gateway allow` to grant access

### Pairing Flow

1. User sends a message on a platform
2. Agent generates a pairing code via `gateway_pairing` (generate)
3. Admin approves code via `/gateway pair <code>`
4. User is added to the allowlist

## Configuration

Stored at `~/.0xkobold/gateway/config.json`:

```json
{
  "port": 3847,
  "host": "localhost",
  "tokens": [],
  "corsOrigins": ["*"],
  "enableWebSocket": true,
  "enableHttp": true,
  "security": {
    "allowAll": true,
    "requirePairing": false
  },
  "sessions": {
    "resetPolicy": "idle",
    "dailyHour": 4,
    "idleMinutes": 1440
  },
  "platforms": {
    "discord": {
      "enabled": false,
      "botToken": ""
    }
  }
}
```

## Local Storage

Data is stored in SQLite via [sql.js](https://github.com/nicolo-ribaudo/nicolo-nicolo/tree/main/nicolo) (WebAssembly) for cross-runtime compatibility (Node.js and Bun):

- `~/.0xkobold/gateway-sessions.db` — Session data
- `~/.0xkobold/gateway-security.db` — Allowlist, pairing codes, rate limits
- `~/.0xkobold/gateway-background-tasks.db` — Background task records

## Local Development

```bash
git clone https://github.com/0xKobold/pi-gateway
cd pi-gateway
npm install
npm run build
pi install ./
```

## License

MIT


**Investigating package installability**

I’m looking into whether the package can be installed. The user asked for a find and audit. It’s worth noting that the gateway's published tarball differs from the GitHub repo, suggesting a source mismatch. I might inspect the messenger bridge's transport code for any issues.

Should I run an npm audit on the gateway package using the unpacked package? It seems tricky without installation. Maybe I could check dependencies or use a temporary package.json to simplify things. I'll try npm install in the extracted tarball.

set -e
cd /tmp/pi-gateway-npm-inspect/package
npm install --ignore-scripts
npm audit --json --omit=dev > audit.json || true
node -e 'const a=require("./audit.json"); console.log(JSON.stringify(a.metadata?.vulnerabilities||{},null,2)); console.log(Object.keys(a.vulnerabilities||{}))'
node -e 'import("./dist/index.js").then(()=>console.log("import ok")).catch(e=>{console.error("import failed",e.message); process.exit(1)})'


npm warn deprecated node-domexception@1.0.0: Use your platform's native DOMException instead

added 268 packages, and audited 269 packages in 10s

36 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
{
  "info": 0,
  "low": 0,
  "moderate": 0,
  "high": 0,
  "critical": 0,
  "total": 0
}
[]
import ok


Updated #2 (pending → completed)

Audit whatsapp-pi repo and install if acceptable

Clone the GitHub repo, scan for suspicious or risky behavior, summarize findings, and install locally if no major issues are found.

Find and audit Pi extensions for WhatsApp integration

Search npm/GitHub for Pi extensions/packages related to WhatsApp, shortlist realistic candidates, audit their repos/packages for weird behavior and dependency risk, and summarize tradeoffs.


Found 3 real candidates.

## 1) `whatsapp-pi`
- Fit: WhatsApp-only Pi extension.
- Repo: `RaphaCastelloes/whatsapp-pi`
- Audit:
  - No install scripts.
  - No obvious exfil / webhook / curl / ssh junk.
  - Expected behavior only: WhatsApp bridge via Baileys, stores auth/config locally, forwards inbound text/media into Pi.
  - Audio transcription shells out to local Whisper binary.
- Risk:
  - Runtime deps bad: `7` prod vulns, incl `3 critical`, `1 high`.
  - Main issue chain: `@whiskeysockets/baileys` → `protobufjs` RCE advisory, plus parser/DoS issues.
  - Tests: `83/85` pass. 2 weak path-test failures only.
- Verdict:
  - Repo itself looks sane.
  - Best fit if you want WhatsApp only.
  - Dependency risk is the main problem.

## 2) `pi-messenger-bridge`
- Fit: Multi-messenger bridge. WhatsApp + Telegram + Slack + Discord.
- Repo: `tintinweb/pi-messenger-bridge`
- Audit:
  - No install scripts.
  - No obvious malicious behavior.
  - Better auth model than `whatsapp-pi`: challenge-based trust, lockfile guard, config perms `600/700`.
  - WhatsApp transport behavior looks normal.
- Risk:
  - Much larger attack surface.
  - Runtime deps worse: `18` prod vulns, incl `5 critical`, `3 high`.
  - Pulls in `discord.js`, `node-telegram-bot-api`, `@slack/bolt`, `baileys`.
  - Includes deprecated `request` chain.
- Quality:
  - Build passes.
  - Tests pass: `31/31`.
- Verdict:
  - Cleaner security model at app level.
  - Heavier and riskier dependency tree.
  - Good only if you want multi-platform, not just WhatsApp.

## 3) `@0xkobold/pi-gateway`
- Fit: Gateway framework. WhatsApp is one adapter among many.
- Repo: `0xKobold/pi-gateway`
- Audit:
  - No obvious malware.
  - Published npm package imports cleanly.
  - Runtime deps in published package audit clean: `0` vulns.
- Big caution:
  - GitHub repo and npm package are out of sync.
  - Repo `package.json` says `0.4.0`; npm publishes `0.6.0`.
  - Repo source uses `bun:sqlite` and looked half-shifted.
  - Published tarball instead uses `sql.js` and includes extra built files not reflected in repo.
- Verdict:
  - Lowest dependency vuln count.
  - Highest trust ambiguity because source-of-truth is messy.
  - More framework than plug-and-play WhatsApp extension.

## Ranking

If goal = **just WhatsApp on Pi**:
1. `whatsapp-pi` — best fit, but vulnerable deps
2. `pi-messenger-bridge` — overkill, more vuln surface
3. `@0xkobold/pi-gateway` — not really the same product

If goal = **safest dependency profile**:
1. `@0xkobold/pi-gateway`
2. `whatsapp-pi`
3. `pi-messenger-bridge`

If goal = **most trustworthy packaging/repo alignment**:
1. `pi-messenger-bridge`
2. `whatsapp-pi`
3. `@0xkobold/pi-gateway`

My call:
- `whatsapp-pi`: best practical fit, medium trust, bad deps.
- `pi-messenger-bridge`: more mature controls, worse dependency blast radius.
- `pi-gateway`: interesting, but repo/package mismatch is a red flag for maintainability.

Next:
- `install whatsapp-pi anyway`
- `install pi-messenger-bridge`
- `deep audit one candidate`


and for telegram?


**Searching for Telegram extensions**

I need to provide a direct answer regarding Telegram and see if there are any specific extensions like the pi-messenger-bridge or pi-gateway. Since the user asked specifically about Telegram, it might be good to state that there’s no dedicated Telegram-only extension found so far. I should reference current findings and consider doing a quick npm search for any Telegram-specific pi extensions. Let's explore that together!

printf '== npm search: telegram pi-package ==\n'
npm search --long --searchlimit=40 telegram pi-package | sed -n '1,220p'
printf '\n== npm search: keywords:pi-package telegram ==\n'
npm search --long --searchlimit=40 keywords:pi-package telegram | sed -n '1,220p'