// Force strict mode and setup for ESM
"use strict";
import {
  ClientSideConnection,
  PROTOCOL_VERSION,
  ndJsonStream
} from "./chunk-UFC57OYT.js";
import {
  init_esbuild_shims
} from "./chunk-A4BMJM77.js";
import {
  __name
} from "./chunk-J2S4EL5Y.js";

// packages/channels/base/dist/paths.js
init_esbuild_shims();
import * as path from "node:path";
import * as os from "node:os";
function resolvePath(dir) {
  let resolved = dir;
  if (resolved === "~" || resolved.startsWith("~/") || resolved.startsWith("~\\")) {
    const relativeSegments = resolved === "~" ? [] : resolved.slice(2).split(/[/\\]+/).filter(Boolean);
    resolved = path.join(os.homedir(), ...relativeSegments);
  }
  if (!path.isAbsolute(resolved)) {
    resolved = path.resolve(resolved);
  }
  return resolved;
}
__name(resolvePath, "resolvePath");
function getGlobalQwenDir() {
  const envDir = process.env["QWEN_HOME"];
  if (envDir) {
    return resolvePath(envDir);
  }
  const homeDir = os.homedir();
  return homeDir ? path.join(homeDir, ".qwen") : path.join(os.tmpdir(), ".qwen");
}
__name(getGlobalQwenDir, "getGlobalQwenDir");

// packages/channels/base/dist/SessionRouter.js
init_esbuild_shims();
import { existsSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
var SessionRouter = class {
  static {
    __name(this, "SessionRouter");
  }
  toSession = /* @__PURE__ */ new Map();
  // routing key → session ID
  toTarget = /* @__PURE__ */ new Map();
  // session ID → target
  toCwd = /* @__PURE__ */ new Map();
  // session ID → cwd
  bridge;
  defaultCwd;
  defaultScope;
  channelScopes = /* @__PURE__ */ new Map();
  persistPath;
  constructor(bridge, defaultCwd, scope = "user", persistPath) {
    this.bridge = bridge;
    this.defaultCwd = defaultCwd;
    this.defaultScope = scope;
    this.persistPath = persistPath;
  }
  /** Replace the bridge instance (used after crash recovery restart). */
  setBridge(bridge) {
    this.bridge = bridge;
  }
  /** Set scope override for a specific channel. */
  setChannelScope(channelName, scope) {
    this.channelScopes.set(channelName, scope);
  }
  routingKey(channelName, senderId, chatId, threadId) {
    const scope = this.channelScopes.get(channelName) || this.defaultScope;
    switch (scope) {
      case "thread":
        return `${channelName}:${threadId || chatId}`;
      case "single":
        return `${channelName}:__single__`;
      case "user":
      default:
        return `${channelName}:${senderId}:${chatId}`;
    }
  }
  /**
   * Sender-scoped key prefix for by-sender (no chatId) scans. The trailing
   * ':' delimiter ensures a sender id matches as a whole segment — without it
   * sender "bob" would also match another sender "bobby"
   * (key `${channelName}:bobby:${chatId}`).
   */
  senderPrefix(channelName, senderId) {
    return `${channelName}:${senderId}:`;
  }
  async resolve(channelName, senderId, chatId, threadId, cwd) {
    const key = this.routingKey(channelName, senderId, chatId, threadId);
    const existing = this.toSession.get(key);
    if (existing) {
      return existing;
    }
    const sessionCwd = cwd || this.defaultCwd;
    const sessionId = await this.bridge.newSession(sessionCwd);
    this.toSession.set(key, sessionId);
    this.toTarget.set(sessionId, { channelName, senderId, chatId, threadId });
    this.toCwd.set(sessionId, sessionCwd);
    this.persist();
    return sessionId;
  }
  getTarget(sessionId) {
    return this.toTarget.get(sessionId);
  }
  getSession(channelName, senderId, chatId, threadId) {
    return this.toSession.get(this.routingKey(channelName, senderId, chatId, threadId));
  }
  hasSession(channelName, senderId, chatId) {
    if (chatId) {
      return this.toSession.has(this.routingKey(channelName, senderId, chatId));
    }
    const prefix = this.senderPrefix(channelName, senderId);
    for (const k of this.toSession.keys()) {
      if (k.startsWith(prefix))
        return true;
    }
    return false;
  }
  /**
   * Remove session(s) for the given sender. Returns the removed session IDs.
   */
  removeSession(channelName, senderId, chatId) {
    const removedIds = [];
    if (chatId) {
      const key = this.routingKey(channelName, senderId, chatId);
      const sessionId = this.deleteByKey(key);
      if (sessionId)
        removedIds.push(sessionId);
    } else {
      const prefix = this.senderPrefix(channelName, senderId);
      for (const k of [...this.toSession.keys()]) {
        if (k.startsWith(prefix)) {
          const sessionId = this.deleteByKey(k);
          if (sessionId)
            removedIds.push(sessionId);
        }
      }
    }
    if (removedIds.length > 0)
      this.persist();
    return removedIds;
  }
  deleteByKey(key) {
    const sessionId = this.toSession.get(key);
    if (!sessionId)
      return null;
    this.toSession.delete(key);
    this.toTarget.delete(sessionId);
    this.toCwd.delete(sessionId);
    return sessionId;
  }
  /** Get all session entries for crash recovery. */
  getAll() {
    const entries = [];
    for (const [key, sessionId] of this.toSession) {
      const target = this.toTarget.get(sessionId);
      if (target) {
        entries.push({ key, sessionId, target });
      }
    }
    return entries;
  }
  /**
   * Restore session mappings from a previous bridge.
   * Called after bridge restart — attempts loadSession for each saved mapping.
   * Failed loads are silently dropped (new session on next message).
   */
  async restoreSessions() {
    if (!this.persistPath || !existsSync(this.persistPath)) {
      return { restored: 0, failed: 0 };
    }
    let entries;
    try {
      entries = JSON.parse(readFileSync(this.persistPath, "utf-8"));
    } catch {
      return { restored: 0, failed: 0 };
    }
    let restored = 0;
    let failed = 0;
    for (const [key, entry] of Object.entries(entries)) {
      try {
        const sessionId = await this.bridge.loadSession(entry.sessionId, entry.cwd);
        this.toSession.set(key, sessionId);
        this.toTarget.set(sessionId, entry.target);
        this.toCwd.set(sessionId, entry.cwd);
        restored++;
      } catch {
        failed++;
      }
    }
    if (failed > 0) {
      this.persist();
    }
    return { restored, failed };
  }
  /** Clear in-memory state and delete persist file. Used on clean shutdown. */
  clearAll() {
    this.toSession.clear();
    this.toTarget.clear();
    this.toCwd.clear();
    if (this.persistPath && existsSync(this.persistPath)) {
      try {
        unlinkSync(this.persistPath);
      } catch {
      }
    }
  }
  persist() {
    if (!this.persistPath)
      return;
    const data = {};
    for (const [key, sessionId] of this.toSession) {
      const target = this.toTarget.get(sessionId);
      if (target) {
        data[key] = {
          sessionId,
          target,
          cwd: this.toCwd.get(sessionId) || this.defaultCwd
        };
      }
    }
    try {
      writeFileSync(this.persistPath, JSON.stringify(data, null, 2), "utf-8");
    } catch {
    }
  }
};

// packages/channels/base/dist/ChannelBase.js
init_esbuild_shims();

// packages/channels/base/dist/BlockStreamer.js
init_esbuild_shims();
var BlockStreamer = class {
  static {
    __name(this, "BlockStreamer");
  }
  buffer = "";
  idleTimer = null;
  sending = Promise.resolve();
  opts;
  /** Number of blocks emitted so far. */
  blockCount = 0;
  constructor(opts) {
    this.opts = opts;
  }
  /** Feed a new text chunk from the agent stream. */
  push(chunk) {
    this.buffer += chunk;
    this.clearIdleTimer();
    this.checkEmit();
    if (this.buffer.length > 0 && this.opts.idleMs > 0) {
      this.idleTimer = setTimeout(() => this.onIdle(), this.opts.idleMs);
    }
  }
  /** Flush all remaining buffered text. Awaits all pending sends. */
  async flush() {
    this.clearIdleTimer();
    if (this.buffer.length > 0) {
      this.emitBlock(this.buffer);
      this.buffer = "";
    }
    await this.sending;
  }
  /** Drop buffered text and cancel future idle emission. */
  stop() {
    this.clearIdleTimer();
    this.buffer = "";
  }
  // ---------------------------------------------------------------------------
  // Internal
  // ---------------------------------------------------------------------------
  checkEmit() {
    while (this.buffer.length >= this.opts.maxChars) {
      const bp = this.findBreakPoint(this.buffer, this.opts.maxChars);
      this.emitBlock(this.buffer.slice(0, bp));
      this.buffer = this.buffer.slice(bp);
    }
    if (this.buffer.length >= this.opts.minChars) {
      const bp = this.findBlockBoundary(this.buffer);
      if (bp > 0) {
        this.emitBlock(this.buffer.slice(0, bp));
        this.buffer = this.buffer.slice(bp);
      }
    }
  }
  onIdle() {
    this.idleTimer = null;
    if (this.buffer.length >= this.opts.minChars) {
      this.emitBlock(this.buffer);
      this.buffer = "";
    }
  }
  emitBlock(text) {
    const trimmed = text.trim();
    if (!trimmed)
      return;
    this.blockCount++;
    this.sending = this.sending.then(() => this.opts.send(trimmed)).catch(() => {
    });
  }
  /**
   * Find the last paragraph boundary (\n\n) in the buffer.
   * Returns the position after the boundary, or -1 if no suitable boundary
   * exists at or after minChars.
   */
  findBlockBoundary(text) {
    const last = text.lastIndexOf("\n\n");
    if (last < 0 || last < this.opts.minChars)
      return -1;
    return last + 2;
  }
  /**
   * Find the best break point at or before maxPos.
   * Prefers paragraph break > newline > space > maxPos.
   */
  findBreakPoint(text, maxPos) {
    const sub = text.slice(0, maxPos);
    const para = sub.lastIndexOf("\n\n");
    if (para > 0)
      return para + 2;
    const nl = sub.lastIndexOf("\n");
    if (nl > 0)
      return nl + 1;
    const sp = sub.lastIndexOf(" ");
    if (sp > 0)
      return sp + 1;
    return maxPos;
  }
  clearIdleTimer() {
    if (this.idleTimer !== null) {
      clearTimeout(this.idleTimer);
      this.idleTimer = null;
    }
  }
};

// packages/channels/base/dist/GroupGate.js
init_esbuild_shims();
var GroupGate = class {
  static {
    __name(this, "GroupGate");
  }
  policy;
  groups;
  constructor(policy = "disabled", groups = {}) {
    this.policy = policy;
    this.groups = groups;
  }
  /**
   * Full group check: policy + allowlist + mention gating.
   * Evaluation order:
   *   1. groupPolicy (disabled → drop)
   *   2. group allowlist (allowlist mode, no match → drop)
   *   3. mention gating (requireMention + not mentioned → drop silently)
   *
   * Mention gating runs before sender gate so that unmentioned messages
   * in groups don't trigger pairing flows.
   */
  check(envelope) {
    if (!envelope.isGroup) {
      return { allowed: true };
    }
    if (this.policy === "disabled") {
      return { allowed: false, reason: "disabled" };
    }
    if (this.policy === "allowlist") {
      if (!this.groups[envelope.chatId]) {
        return { allowed: false, reason: "not_allowlisted" };
      }
    }
    const groupConfig = this.groups[envelope.chatId] || this.groups["*"] || {};
    const requireMention = groupConfig.requireMention ?? true;
    if (requireMention && !envelope.isMentioned && !envelope.isReplyToBot) {
      return { allowed: false, reason: "mention_required" };
    }
    return { allowed: true };
  }
};

// packages/channels/base/dist/SenderGate.js
init_esbuild_shims();
var SenderGate = class {
  static {
    __name(this, "SenderGate");
  }
  policy;
  allowedUsers;
  pairingStore;
  constructor(policy, allowedUsers = [], pairingStore) {
    this.policy = policy;
    this.allowedUsers = new Set(allowedUsers);
    this.pairingStore = pairingStore || null;
  }
  check(senderId, senderName) {
    switch (this.policy) {
      case "open":
        return { allowed: true };
      case "allowlist":
        return { allowed: this.allowedUsers.has(senderId) };
      case "pairing": {
        if (this.allowedUsers.has(senderId)) {
          return { allowed: true };
        }
        if (this.pairingStore?.isApproved(senderId)) {
          return { allowed: true };
        }
        const code = this.pairingStore?.createRequest(senderId, senderName || senderId);
        return { allowed: false, pairingCode: code ?? null };
      }
      default:
        throw new Error(`Unknown sender policy: ${this.policy}`);
    }
  }
};

// packages/channels/base/dist/PairingStore.js
init_esbuild_shims();
import * as crypto from "node:crypto";
import * as fs from "node:fs";
import * as path2 from "node:path";
var SAFE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
var CODE_LENGTH = 8;
var EXPIRY_MS = 60 * 60 * 1e3;
var MAX_PENDING = 3;
var PairingStore = class {
  static {
    __name(this, "PairingStore");
  }
  dir;
  pendingPath;
  allowlistPath;
  constructor(channelName) {
    this.dir = path2.join(getGlobalQwenDir(), "channels");
    this.pendingPath = path2.join(this.dir, `${channelName}-pairing.json`);
    this.allowlistPath = path2.join(this.dir, `${channelName}-allowlist.json`);
  }
  isApproved(senderId) {
    const list = this.readAllowlist();
    return list.includes(senderId);
  }
  /**
   * Create a pairing request for an unknown sender.
   * Returns the code if created, or null if the pending cap is reached.
   * If the sender already has a non-expired pending request, returns that code.
   */
  createRequest(senderId, senderName) {
    const pending = this.readPending();
    const now = Date.now();
    const active = pending.filter((r) => now - r.createdAt < EXPIRY_MS);
    const existing = active.find((r) => r.senderId === senderId);
    if (existing) {
      return existing.code;
    }
    if (active.length >= MAX_PENDING) {
      return null;
    }
    const code = generateCode();
    active.push({ senderId, senderName, code, createdAt: now });
    this.writePending(active);
    return code;
  }
  /**
   * Approve a pairing request by code.
   * Returns the sender ID if found, or null if not found / expired.
   */
  approve(code) {
    const pending = this.readPending();
    const now = Date.now();
    const idx = pending.findIndex((r) => r.code === code.toUpperCase() && now - r.createdAt < EXPIRY_MS);
    if (idx === -1)
      return null;
    const request = pending[idx];
    pending.splice(idx, 1);
    this.writePending(pending);
    const list = this.readAllowlist();
    if (!list.includes(request.senderId)) {
      list.push(request.senderId);
      this.writeAllowlist(list);
    }
    return request;
  }
  listPending() {
    const pending = this.readPending();
    const now = Date.now();
    return pending.filter((r) => now - r.createdAt < EXPIRY_MS);
  }
  getAllowlist() {
    return this.readAllowlist();
  }
  ensureDir() {
    if (!fs.existsSync(this.dir)) {
      fs.mkdirSync(this.dir, { recursive: true });
    }
  }
  readPending() {
    try {
      const data = fs.readFileSync(this.pendingPath, "utf-8");
      return JSON.parse(data);
    } catch {
      return [];
    }
  }
  writePending(requests) {
    this.ensureDir();
    fs.writeFileSync(this.pendingPath, JSON.stringify(requests, null, 2));
  }
  readAllowlist() {
    try {
      const data = fs.readFileSync(this.allowlistPath, "utf-8");
      return JSON.parse(data);
    } catch {
      return [];
    }
  }
  writeAllowlist(list) {
    this.ensureDir();
    fs.writeFileSync(this.allowlistPath, JSON.stringify(list, null, 2));
  }
};
function generateCode() {
  let code = "";
  for (let i = 0; i < CODE_LENGTH; i++) {
    code += SAFE_ALPHABET[crypto.randomInt(SAFE_ALPHABET.length)];
  }
  return code;
}
__name(generateCode, "generateCode");

// packages/channels/base/dist/ChannelBase.js
var ChannelBase = class {
  static {
    __name(this, "ChannelBase");
  }
  config;
  bridge;
  groupGate;
  gate;
  router;
  name;
  /** Resolved proxy URL, available to subclasses for adapter-specific clients. */
  proxy;
  instructedSessions = /* @__PURE__ */ new Set();
  commands = /* @__PURE__ */ new Map();
  /** Per-session promise chain to serialize prompt + send (followup mode). */
  sessionQueues = /* @__PURE__ */ new Map();
  /** Per-session active prompt tracking for dispatch modes. */
  activePrompts = /* @__PURE__ */ new Map();
  /** Per-session message buffer for collect mode. */
  collectBuffers = /* @__PURE__ */ new Map();
  constructor(name, config, bridge, options) {
    this.name = name;
    this.config = config;
    this.bridge = bridge;
    this.proxy = options?.proxy;
    this.groupGate = new GroupGate(config.groupPolicy, config.groups);
    const pairingStore = config.senderPolicy === "pairing" ? new PairingStore(name) : void 0;
    this.gate = new SenderGate(config.senderPolicy, config.allowedUsers, pairingStore);
    this.router = options?.router || new SessionRouter(bridge, config.cwd, config.sessionScope);
    this.registerSharedCommands();
    if (!options?.router) {
      bridge.on("toolCall", (event) => {
        const target = this.router.getTarget(event.sessionId);
        if (target) {
          this.onToolCall(target.chatId, event);
        }
      });
    }
  }
  /** Replace the bridge instance (used after crash recovery restart). */
  setBridge(bridge) {
    this.bridge = bridge;
  }
  onToolCall(_chatId, _event) {
  }
  /**
   * Called when a prompt actually begins processing (inside the session queue).
   * Override to show a platform-specific working indicator (e.g., typing, reaction).
   * Not called for buffered messages (collect mode) or gated/blocked messages.
   */
  onPromptStart(_chatId, _sessionId, _messageId) {
  }
  /**
   * Called when a prompt finishes (response sent or cancelled).
   * Override to hide the working indicator.
   */
  onPromptEnd(_chatId, _sessionId, _messageId) {
  }
  /**
   * Called for each text chunk as the agent streams its response.
   * Override to implement progressive display (e.g., updating an AI card in-place).
   * Default: no-op (chunks are collected internally and delivered via onResponseComplete).
   */
  onResponseChunk(_chatId, _chunk, _sessionId) {
  }
  /**
   * Called when the agent's full response is ready.
   * Override to customize delivery (e.g., finalize an AI card).
   * Default: calls sendMessage() with the full response text.
   */
  async onResponseComplete(chatId, fullText, _sessionId) {
    await this.sendMessage(chatId, fullText);
  }
  /**
   * Register a slash command handler. Subclasses can call this to add
   * platform-specific commands (e.g., /start for Telegram).
   * Overrides shared commands if the same name is registered.
   */
  registerCommand(name, handler) {
    this.commands.set(name.toLowerCase(), handler);
  }
  registerCancelCommand(name = "cancel") {
    this.registerCommand(name, async (envelope) => {
      const activeSessionId = this.findActiveSessionId(envelope);
      if (!activeSessionId) {
        await this.sendMessage(envelope.chatId, "No request is currently running.");
        return true;
      }
      const active = this.activePrompts.get(activeSessionId);
      if (!active) {
        await this.sendMessage(envelope.chatId, "No request is currently running.");
        return true;
      }
      const cancelRequested = active.cancelRequested ?? this.bridge.cancelSession(activeSessionId).then(() => true, (err) => {
        process.stderr.write(`[${this.name}] cancelSession failed for session=${activeSessionId}: ${err instanceof Error ? err.message : err}
`);
        active.cancelRequested = void 0;
        return false;
      });
      active.cancelRequested = cancelRequested;
      const cancelSucceeded = await cancelRequested;
      if (!cancelSucceeded) {
        await this.sendMessage(envelope.chatId, "Failed to cancel current request.");
        return true;
      }
      active.cancelled = true;
      active.stopStreaming?.();
      this.collectBuffers.delete(activeSessionId);
      await this.sendMessage(envelope.chatId, "Cancelled current request.");
      return true;
    });
  }
  /** Register shared slash commands. Called from constructor. */
  registerSharedCommands() {
    const clearHandler = /* @__PURE__ */ __name(async (envelope) => {
      const removedIds = this.router.removeSession(this.name, envelope.senderId, envelope.chatId);
      if (removedIds.length > 0) {
        for (const id of removedIds) {
          this.instructedSessions.delete(id);
        }
        await this.sendMessage(envelope.chatId, "Session cleared. Your next message will start a fresh conversation.");
      } else {
        await this.sendMessage(envelope.chatId, "No active session to clear.");
      }
      return true;
    }, "clearHandler");
    this.registerCommand("clear", clearHandler);
    this.registerCommand("reset", clearHandler);
    this.registerCommand("new", clearHandler);
    this.registerCommand("help", async (envelope) => {
      const lines = [
        "Commands:",
        "/help \u2014 Show this help",
        "/clear \u2014 Clear your session (aliases: /reset, /new)",
        "/status \u2014 Show session info"
      ];
      const sharedCmds = /* @__PURE__ */ new Set(["help", "clear", "reset", "new", "status"]);
      const platformCmds = [...this.commands.keys()].filter((c) => !sharedCmds.has(c));
      if (platformCmds.length > 0) {
        for (const cmd of platformCmds) {
          lines.push(`/${cmd}`);
        }
      }
      const agentCommands = this.bridge.availableCommands;
      if (agentCommands.length > 0) {
        lines.push("", "Agent commands (forwarded to Qwen Code):");
        for (const cmd of agentCommands) {
          lines.push(`/${cmd.name} \u2014 ${cmd.description}`);
        }
      }
      lines.push("", "Send any text to chat with the agent.");
      await this.sendMessage(envelope.chatId, lines.join("\n"));
      return true;
    });
    this.registerCommand("status", async (envelope) => {
      const hasSession = this.router.hasSession(this.name, envelope.senderId, envelope.chatId);
      const policy = this.config.senderPolicy;
      const lines = [
        `Session: ${hasSession ? "active" : "none"}`,
        `Access: ${policy}`,
        `Channel: ${this.name}`
      ];
      await this.sendMessage(envelope.chatId, lines.join("\n"));
      return true;
    });
  }
  /** Check if a message text matches a registered local command. */
  isLocalCommand(text) {
    const parsed = this.parseCommand(text);
    return parsed !== null && this.commands.has(parsed.command);
  }
  findActiveSessionId(envelope) {
    const sessionId = this.router.getSession(this.name, envelope.senderId, envelope.chatId, envelope.threadId);
    return sessionId && this.activePrompts.has(sessionId) ? sessionId : void 0;
  }
  /**
   * Parse a slash command from message text.
   * Returns { command, args } or null if not a slash command.
   */
  parseCommand(text) {
    if (!text.startsWith("/"))
      return null;
    const match = text.match(/^\/([a-zA-Z0-9_]+)(?:@\S+)?\s*(.*)/s);
    if (!match)
      return null;
    return { command: match[1].toLowerCase(), args: match[2].trim() };
  }
  async handleInbound(envelope) {
    const groupResult = this.groupGate.check(envelope);
    if (!groupResult.allowed) {
      return;
    }
    const result = this.gate.check(envelope.senderId, envelope.senderName);
    if (!result.allowed) {
      if (result.pairingCode !== void 0) {
        await this.onPairingRequired(envelope.chatId, result.pairingCode);
      }
      return;
    }
    const parsed = this.parseCommand(envelope.text);
    if (parsed) {
      const handler = this.commands.get(parsed.command);
      if (handler) {
        const handled = await handler(envelope, parsed.args);
        if (handled)
          return;
      }
    }
    const sessionId = await this.router.resolve(this.name, envelope.senderId, envelope.chatId, envelope.threadId, this.config.cwd);
    if (envelope.text.startsWith("!")) {
      const cmd = envelope.text.slice(1).trim();
      const bridgeShellCommand = this.bridge["shellCommand"];
      if (cmd && typeof bridgeShellCommand === "function") {
        try {
          const result2 = await bridgeShellCommand(sessionId, cmd);
          const longestRun = Math.max(0, ...Array.from((result2.output || "").matchAll(/`+/g), (m) => m[0].length));
          const fence = "`".repeat(Math.max(3, longestRun + 1));
          const output = result2.output ? `${fence}
${result2.output}
${fence}` : "(no output)";
          const exitLine = result2.exitCode !== null && result2.exitCode !== 0 ? `
Exit code: ${result2.exitCode}` : "";
          await this.sendMessage(envelope.chatId, `$ ${cmd}
${output}${exitLine}`);
        } catch (error) {
          await this.sendMessage(envelope.chatId, `Shell command failed: ${error instanceof Error ? error.message : String(error)}`);
        }
        return;
      }
    }
    let promptText = envelope.text;
    if (envelope.referencedText) {
      promptText = `[Replying to: "${envelope.referencedText}"]

${promptText}`;
    }
    let imageBase64 = envelope.imageBase64;
    let imageMimeType = envelope.imageMimeType;
    if (envelope.attachments?.length) {
      const filePaths = [];
      for (const att of envelope.attachments) {
        if (att.type === "image" && att.data && !imageBase64) {
          imageBase64 = att.data;
          imageMimeType = att.mimeType;
        } else if (att.filePath) {
          const label = att.type === "file" ? "file" : att.type;
          const name = att.fileName ? ` "${att.fileName}"` : "";
          filePaths.push(`User sent a ${label}${name}. It has been saved to: ${att.filePath}`);
        }
      }
      if (filePaths.length > 0) {
        promptText = promptText + "\n\n" + filePaths.join("\n");
      }
    }
    if (this.config.instructions && !this.instructedSessions.has(sessionId)) {
      promptText = `${this.config.instructions}

${promptText}`;
      this.instructedSessions.add(sessionId);
    }
    const groupCfg = envelope.isGroup ? this.config.groups[envelope.chatId] || this.config.groups["*"] : void 0;
    const mode = groupCfg?.dispatchMode || this.config.dispatchMode || "steer";
    const active = this.activePrompts.get(sessionId);
    if (active) {
      switch (mode) {
        case "collect": {
          let buffer = this.collectBuffers.get(sessionId);
          if (!buffer) {
            buffer = [];
            this.collectBuffers.set(sessionId, buffer);
          }
          buffer.push({ text: promptText, envelope });
          return;
        }
        case "steer": {
          active.cancelled = true;
          await this.bridge.cancelSession(sessionId).catch(() => {
          });
          await active.done;
          promptText = `[The user sent a new message while you were working. Their previous request has been cancelled.]

${promptText}`;
          break;
        }
        case "followup": {
          break;
        }
        default: {
          const _exhaustive = mode;
          throw new Error(`Unknown dispatch mode: ${_exhaustive}`);
        }
      }
    }
    const prev = this.sessionQueues.get(sessionId) ?? Promise.resolve();
    const useBlockStreaming = this.config.blockStreaming === "on";
    const current = prev.then(async () => {
      let doneResolve = /* @__PURE__ */ __name(() => {
      }, "doneResolve");
      const done = new Promise((r) => {
        doneResolve = r;
      });
      const promptState = {
        cancelled: false,
        done,
        resolve: doneResolve
      };
      this.activePrompts.set(sessionId, promptState);
      this.onPromptStart(envelope.chatId, sessionId, envelope.messageId);
      const streamer = useBlockStreaming ? new BlockStreamer({
        minChars: this.config.blockStreamingChunk?.minChars ?? 400,
        maxChars: this.config.blockStreamingChunk?.maxChars ?? 1e3,
        idleMs: this.config.blockStreamingCoalesce?.idleMs ?? 1500,
        send: /* @__PURE__ */ __name((text) => this.sendMessage(envelope.chatId, text), "send")
      }) : null;
      promptState.stopStreaming = () => streamer?.stop();
      const onChunk = /* @__PURE__ */ __name((sid, chunk) => {
        if (sid === sessionId && !promptState.cancelled) {
          this.onResponseChunk(envelope.chatId, chunk, sessionId);
          streamer?.push(chunk);
        }
      }, "onChunk");
      this.bridge.on("textChunk", onChunk);
      try {
        const response = await this.bridge.prompt(sessionId, promptText, {
          imageBase64,
          imageMimeType
        });
        if (promptState.cancelRequested && !promptState.cancelled) {
          promptState.cancelled = await promptState.cancelRequested;
        }
        if (!promptState.cancelled && response) {
          if (streamer) {
            await streamer.flush();
          } else {
            await this.onResponseComplete(envelope.chatId, response, sessionId);
          }
        }
      } finally {
        this.bridge.off("textChunk", onChunk);
        streamer?.stop();
        this.onPromptEnd(envelope.chatId, sessionId, envelope.messageId);
        this.activePrompts.delete(sessionId);
        promptState.resolve();
        const buffer = this.collectBuffers.get(sessionId);
        if (buffer && buffer.length > 0) {
          this.collectBuffers.delete(sessionId);
          const coalesced = buffer.map((b) => b.text).join("\n\n");
          const lastEnvelope = buffer[buffer.length - 1].envelope;
          const syntheticEnvelope = {
            ...lastEnvelope,
            text: coalesced,
            // Clear attachments/references — already resolved in original text
            referencedText: void 0,
            attachments: void 0,
            imageBase64: void 0,
            imageMimeType: void 0
          };
          this.handleInbound(syntheticEnvelope).catch(() => {
          });
        }
      }
    });
    this.sessionQueues.set(sessionId, current.catch(() => {
    }));
    await current;
  }
  async onPairingRequired(chatId, code) {
    if (code) {
      await this.sendMessage(chatId, `Your pairing code is: ${code}

Ask the bot operator to approve you with:
  qwen channel pairing approve ${this.name} ${code}`);
    } else {
      await this.sendMessage(chatId, "Too many pending pairing requests. Please try again later.");
    }
  }
};

// packages/channels/base/dist/index.js
init_esbuild_shims();

// packages/channels/base/dist/AcpBridge.js
init_esbuild_shims();
import { spawn } from "node:child_process";
import { Readable, Writable } from "node:stream";
import { EventEmitter } from "node:events";
var AcpBridge = class extends EventEmitter {
  static {
    __name(this, "AcpBridge");
  }
  child = null;
  connection = null;
  options;
  _availableCommands = [];
  constructor(options) {
    super();
    this.options = options;
  }
  get availableCommands() {
    return this._availableCommands;
  }
  async start() {
    const { cliEntryPath, cwd } = this.options;
    const args = [
      ...process.execArgv.filter((a) => !/^--inspect(-brk)?($|=)/.test(a)),
      cliEntryPath,
      "--acp"
    ];
    if (this.options.model) {
      args.push("--model", this.options.model);
    }
    this.child = spawn(process.execPath, args, {
      cwd,
      stdio: ["pipe", "pipe", "pipe"],
      env: { ...process.env },
      shell: false
    });
    this.child.stderr?.on("data", (data) => {
      const msg = data.toString().trim();
      if (msg) {
        process.stderr.write(`[AcpBridge] ${msg}
`);
      }
    });
    this.child.on("exit", (code, signal) => {
      process.stderr.write(`[AcpBridge] Process exited (code=${code}, signal=${signal})
`);
      this.connection = null;
      this.child = null;
      this.emit("disconnected", code, signal);
    });
    await new Promise((resolve2) => setTimeout(resolve2, 1e3));
    if (!this.child || this.child.killed) {
      throw new Error("ACP process failed to start");
    }
    const stdout = Readable.toWeb(this.child.stdout);
    const stdin = Writable.toWeb(this.child.stdin);
    const stream = ndJsonStream(stdin, stdout);
    this.connection = new ClientSideConnection(() => ({
      sessionUpdate: /* @__PURE__ */ __name((params) => {
        this.handleSessionUpdate(params);
        return Promise.resolve();
      }, "sessionUpdate"),
      requestPermission: /* @__PURE__ */ __name(async (params) => {
        const options = Array.isArray(params.options) ? params.options : [];
        const optionId = options.find((o) => o.optionId === "proceed_once")?.optionId || options[0]?.optionId || "proceed_once";
        return { outcome: { outcome: "selected", optionId } };
      }, "requestPermission"),
      extNotification: /* @__PURE__ */ __name(async () => {
      }, "extNotification")
    }), stream);
    await this.connection.initialize({
      protocolVersion: PROTOCOL_VERSION,
      clientCapabilities: {}
    });
  }
  async newSession(cwd) {
    const conn = this.ensureConnection();
    const response = await conn.newSession({ cwd, mcpServers: [] });
    return response.sessionId;
  }
  async loadSession(sessionId, cwd) {
    const conn = this.ensureConnection();
    const response = await conn.loadSession({
      sessionId,
      cwd,
      mcpServers: []
    });
    return response.sessionId;
  }
  async prompt(sessionId, text, options) {
    const conn = this.ensureConnection();
    const chunks = [];
    const onChunk = /* @__PURE__ */ __name((sid, chunk) => {
      if (sid === sessionId)
        chunks.push(chunk);
    }, "onChunk");
    this.on("textChunk", onChunk);
    const prompt = [];
    if (options?.imageBase64 && options.imageMimeType) {
      prompt.push({
        type: "image",
        data: options.imageBase64,
        mimeType: options.imageMimeType
      });
    }
    prompt.push({ type: "text", text });
    try {
      await conn.prompt({
        sessionId,
        prompt
      });
    } finally {
      this.off("textChunk", onChunk);
    }
    return chunks.join("");
  }
  async cancelSession(sessionId) {
    const conn = this.ensureConnection();
    await conn.cancel({ sessionId });
  }
  stop() {
    if (this.child) {
      this.child.kill();
      this.child = null;
    }
    this.connection = null;
  }
  get isConnected() {
    return this.child !== null && !this.child.killed && this.child.exitCode === null;
  }
  handleSessionUpdate(params) {
    const { sessionId } = params;
    const update = params["update"];
    if (!update)
      return;
    const type = update["sessionUpdate"];
    switch (type) {
      case "agent_message_chunk": {
        const content = update["content"];
        if (content?.type === "text" && content.text) {
          this.emit("textChunk", sessionId, content.text);
        }
        break;
      }
      case "tool_call": {
        const event = {
          sessionId,
          toolCallId: update["toolCallId"],
          kind: update["kind"] || "",
          title: update["title"] || "",
          status: update["status"] || "pending",
          rawInput: update["rawInput"]
        };
        this.emit("toolCall", event);
        break;
      }
      case "available_commands_update": {
        if (Array.isArray(update["availableCommands"])) {
          this._availableCommands = update["availableCommands"];
        }
        break;
      }
      default:
        break;
    }
    this.emit("sessionUpdate", params);
  }
  ensureConnection() {
    if (!this.connection || !this.isConnected) {
      throw new Error("Not connected to ACP agent");
    }
    return this.connection;
  }
};

// packages/channels/base/dist/DaemonChannelBridge.js
init_esbuild_shims();

export {
  resolvePath,
  getGlobalQwenDir,
  AcpBridge,
  PairingStore,
  SessionRouter,
  ChannelBase
};
