// packages/core/src/conduit/conduit-client.ts
var ConduitClient = class {
  transport;
  credential;
  state = "disconnected";
  /** Create a ConduitClient backed by the given transport and credential. */
  constructor(transport, credential) {
    this.transport = transport;
    this.credential = credential;
  }
  /** The agent ID from the bound credential. */
  get agentId() {
    return this.credential.agentId;
  }
  /** Current connection state (disconnected → connecting → connected | error). */
  getState() {
    return this.state;
  }
  /** Connect the underlying transport using the bound credential. */
  async connect() {
    this.state = "connecting";
    try {
      await this.transport.connect({
        agentId: this.credential.agentId,
        apiKey: this.credential.apiKey,
        apiBaseUrl: this.credential.apiBaseUrl,
        ...this.credential.transportConfig
      });
      this.state = "connected";
    } catch (err) {
      this.state = "error";
      throw err;
    }
  }
  /** Send a message to another agent, optionally within a thread. */
  async send(to, content, options) {
    const result = await this.transport.push(to, content, {
      conversationId: options?.threadId
    });
    return {
      messageId: result.messageId,
      deliveredAt: (/* @__PURE__ */ new Date()).toISOString()
    };
  }
  /** One-shot poll for new messages. Delegates to the transport's poll method. */
  async poll(options) {
    return this.transport.poll(options);
  }
  /** Subscribe to incoming messages. Uses real-time transport when available, else polls. */
  onMessage(handler) {
    if (this.transport.subscribe) {
      return this.transport.subscribe(handler);
    }
    const interval = setInterval(async () => {
      const messages = await this.transport.poll();
      for (const msg of messages) handler(msg);
      if (messages.length > 0) {
        await this.transport.ack(messages.map((m) => m.id));
      }
    }, this.credential.transportConfig.pollIntervalMs ?? 5e3);
    return () => clearInterval(interval);
  }
  /** Send an empty heartbeat to maintain presence on the relay. */
  async heartbeat() {
    await this.transport.push(this.credential.agentId, "", {});
  }
  /** Check whether a remote agent is currently online via the cloud API. */
  async isOnline(agentId) {
    try {
      const response = await fetch(`${this.credential.apiBaseUrl}/agents/${agentId}`, {
        headers: {
          Authorization: `Bearer ${this.credential.apiKey}`,
          "X-Agent-Id": this.credential.agentId
        }
      });
      if (!response.ok) return false;
      const data = await response.json();
      return data.data?.agent?.status === "online";
    } catch {
      return false;
    }
  }
  /** Disconnect the transport and reset state to disconnected. */
  async disconnect() {
    await this.transport.disconnect();
    this.state = "disconnected";
  }
};

// packages/core/src/conduit/http-transport.ts
var HttpTransport = class {
  name = "http";
  state = null;
  /** Connect to the SignalDock API, probing primary/fallback health when both are configured. */
  async connect(config) {
    const primaryUrl = config.apiBaseUrl;
    const fallbackUrl = config.apiBaseUrlFallback ?? null;
    let activeUrl = primaryUrl;
    if (fallbackUrl) {
      const [primaryResult, fallbackResult] = await Promise.allSettled([
        fetch(`${primaryUrl}/health`, { method: "GET", signal: AbortSignal.timeout(5e3) }),
        fetch(`${fallbackUrl}/health`, { method: "GET", signal: AbortSignal.timeout(5e3) })
      ]);
      const primaryOk = primaryResult.status === "fulfilled" && primaryResult.value.ok;
      const fallbackOk = fallbackResult.status === "fulfilled" && fallbackResult.value.ok;
      if (!primaryOk && fallbackOk) {
        activeUrl = fallbackUrl;
      }
    }
    this.state = {
      agentId: config.agentId,
      apiKey: config.apiKey,
      primaryUrl,
      fallbackUrl,
      activeUrl,
      connected: true
    };
  }
  /** Disconnect and clear connection state. */
  async disconnect() {
    this.state = null;
  }
  /** Send a message to an agent (direct or within a conversation thread). */
  async push(to, content, options) {
    this.ensureConnected();
    const body = { content };
    let path;
    if (options?.conversationId) {
      path = `/conversations/${options.conversationId}/messages`;
      if (options.replyTo) {
        body["replyTo"] = options.replyTo;
      }
    } else {
      path = "/messages";
      body["toAgentId"] = to;
    }
    const response = await this.fetchWithFallback(path, {
      method: "POST",
      headers: this.headers(),
      body: JSON.stringify(body)
    });
    if (!response.ok) {
      const text = await response.text().catch(() => "");
      throw new Error(`HttpTransport push failed: ${response.status} ${text}`);
    }
    const data = await response.json();
    const messageId = data.data?.message?.id ?? data.data?.id ?? "unknown";
    return { messageId };
  }
  /** Poll for new messages for this agent. Returns empty array on HTTP error. */
  async poll(options) {
    this.ensureConnected();
    const params = new URLSearchParams();
    if (options?.limit) params.set("limit", String(options.limit));
    if (options?.since) params.set("since", options.since);
    const response = await this.fetchWithFallback(`/messages/peek?${params}`, {
      method: "GET",
      headers: this.headers()
    });
    if (!response.ok) return [];
    const data = await response.json();
    return (data.data?.messages ?? []).map((m) => ({
      id: m.id,
      from: m.fromAgentId ?? "unknown",
      content: m.content ?? "",
      threadId: m.conversationId,
      timestamp: m.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
    }));
  }
  /** Acknowledge messages by ID so they are not returned by future polls. */
  async ack(messageIds) {
    this.ensureConnected();
    await this.fetchWithFallback("/messages/ack", {
      method: "POST",
      headers: this.headers(),
      body: JSON.stringify({ messageIds })
    });
  }
  /**
   * Fetch with automatic failover. Tries activeUrl first.
   * If it fails and a fallback exists, retries on the other URL
   * and swaps activeUrl for subsequent calls.
   */
  async fetchWithFallback(path, init) {
    const timeout = AbortSignal.timeout(1e4);
    const signal = init.signal ? AbortSignal.any([init.signal, timeout]) : timeout;
    const url = `${this.state.activeUrl}${path}`;
    try {
      return await fetch(url, { ...init, signal });
    } catch (primaryErr) {
      const otherUrl = this.state.activeUrl === this.state.primaryUrl ? this.state.fallbackUrl : this.state.primaryUrl;
      if (!otherUrl) throw primaryErr;
      try {
        const fallbackSignal = init.signal ? AbortSignal.any([init.signal, AbortSignal.timeout(1e4)]) : AbortSignal.timeout(1e4);
        const fallbackResponse = await fetch(`${otherUrl}${path}`, {
          ...init,
          signal: fallbackSignal
        });
        this.state.activeUrl = otherUrl;
        return fallbackResponse;
      } catch {
        throw primaryErr;
      }
    }
  }
  headers() {
    return {
      "Content-Type": "application/json",
      Authorization: `Bearer ${this.state.apiKey}`,
      "X-Agent-Id": this.state.agentId
    };
  }
  ensureConnected() {
    if (!this.state?.connected) {
      throw new Error("HttpTransport not connected. Call connect() first.");
    }
  }
};

// packages/core/src/conduit/local-transport.ts
import { randomUUID } from "node:crypto";
import { existsSync } from "node:fs";
import { createRequire as createRequire2 } from "node:module";

// packages/core/src/store/conduit-sqlite.ts
import { createRequire } from "node:module";
import { dirname, join } from "node:path";
var _require = createRequire(import.meta.url);
var { DatabaseSync } = _require("node:sqlite");
var CONDUIT_DB_FILENAME = "conduit.db";
function getConduitDbPath(projectRoot) {
  return join(projectRoot, ".cleo", CONDUIT_DB_FILENAME);
}

// packages/core/src/conduit/local-transport.ts
var _require2 = createRequire2(import.meta.url);
var { DatabaseSync: DatabaseSyncClass } = _require2("node:sqlite");
var LocalTransport = class {
  name = "local";
  state = null;
  /**
   * Connect to conduit.db for in-process messaging.
   *
   * Opens the database, sets WAL mode pragmas, and verifies
   * the messages table exists. Throws if conduit.db is missing
   * or uninitialized (run `cleo init` first).
   *
   * @task T356
   * @epic T310
   */
  async connect(config) {
    const dbPath = getConduitDbPath(process.cwd());
    if (!existsSync(dbPath)) {
      throw new Error(`LocalTransport: conduit.db not found at ${dbPath}. Run: cleo init`);
    }
    const db = new DatabaseSyncClass(dbPath);
    db.exec("PRAGMA journal_mode = WAL");
    db.exec("PRAGMA busy_timeout = 5000");
    db.exec("PRAGMA foreign_keys = ON");
    const hasMessages = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='messages'").get();
    if (!hasMessages) {
      db.close();
      throw new Error(
        "LocalTransport: conduit.db exists but messages table missing \u2014 run cleo init or allow auto-migration (T358)"
      );
    }
    this.state = {
      agentId: config.agentId,
      db,
      dbPath,
      subscribers: /* @__PURE__ */ new Set(),
      pollTimer: null
    };
  }
  /** Close the database connection and stop any subscriber polling. */
  async disconnect() {
    if (!this.state) return;
    if (this.state.pollTimer) {
      clearInterval(this.state.pollTimer);
    }
    this.state.subscribers.clear();
    this.state.db.close();
    this.state = null;
  }
  /**
   * Store a message in conduit.db.
   *
   * Inserts into the messages table with status 'pending'.
   * For conversation messages, also links via conversation_participants
   * if not already present.
   */
  async push(to, content, options) {
    this.ensureConnected();
    const { db, agentId } = this.state;
    const messageId = randomUUID();
    const nowUnix = Math.floor(Date.now() / 1e3);
    if (options?.conversationId) {
      db.prepare(
        `INSERT INTO messages (id, conversation_id, from_agent_id, to_agent_id, content, content_type, status, created_at)
         VALUES (?, ?, ?, ?, ?, 'text', 'pending', ?)`
      ).run(messageId, options.conversationId, agentId, to, content, nowUnix);
    } else {
      const convId = this.ensureDmConversation(agentId, to);
      db.prepare(
        `INSERT INTO messages (id, conversation_id, from_agent_id, to_agent_id, content, content_type, status, created_at)
         VALUES (?, ?, ?, ?, ?, 'text', 'pending', ?)`
      ).run(messageId, convId, agentId, to, content, nowUnix);
    }
    this.notifySubscribers({
      id: messageId,
      from: agentId,
      content,
      threadId: options?.conversationId,
      timestamp: new Date(nowUnix * 1e3).toISOString()
    });
    return { messageId };
  }
  /**
   * Poll for messages addressed to this agent.
   *
   * Returns messages with status 'pending' where to_agent_id matches
   * the connected agent. Messages are returned oldest-first.
   */
  async poll(options) {
    this.ensureConnected();
    const { db, agentId } = this.state;
    const limit = options?.limit ?? 50;
    let query;
    let params;
    if (options?.since) {
      query = `SELECT id, from_agent_id, content, conversation_id, created_at
               FROM messages
               WHERE to_agent_id = ? AND status = 'pending' AND created_at > ?
               ORDER BY created_at ASC
               LIMIT ?`;
      params = [agentId, options.since, limit];
    } else {
      query = `SELECT id, from_agent_id, content, conversation_id, created_at
               FROM messages
               WHERE to_agent_id = ? AND status = 'pending'
               ORDER BY created_at ASC
               LIMIT ?`;
      params = [agentId, limit];
    }
    const rows = db.prepare(query).all(...params);
    return rows.map((r) => ({
      id: r.id,
      from: r.from_agent_id,
      content: r.content,
      threadId: r.conversation_id ?? void 0,
      timestamp: new Date(r.created_at * 1e3).toISOString()
    }));
  }
  /**
   * Acknowledge messages by marking them as 'delivered'.
   *
   * Updates the status and delivered_at timestamp for each message ID.
   */
  async ack(messageIds) {
    this.ensureConnected();
    if (messageIds.length === 0) return;
    const { db } = this.state;
    const nowUnix = Math.floor(Date.now() / 1e3);
    const placeholders = messageIds.map(() => "?").join(", ");
    db.prepare(
      `UPDATE messages SET status = 'delivered', delivered_at = ? WHERE id IN (${placeholders})`
    ).run(nowUnix, ...messageIds);
  }
  /**
   * Subscribe to real-time local messages.
   *
   * Since this is in-process, subscribers are notified synchronously
   * when push() is called. Additionally, a polling interval checks
   * for messages inserted by other processes (e.g., Rust CLI).
   *
   * @returns Unsubscribe function.
   */
  subscribe(handler) {
    this.ensureConnected();
    this.state.subscribers.add(handler);
    if (!this.state.pollTimer && this.state.subscribers.size === 1) {
      this.state.pollTimer = setInterval(() => {
        void this.pollAndNotify();
      }, 1e3);
    }
    return () => {
      this.state?.subscribers.delete(handler);
      if (this.state?.subscribers.size === 0 && this.state.pollTimer) {
        clearInterval(this.state.pollTimer);
        this.state.pollTimer = null;
      }
    };
  }
  /**
   * Check whether conduit.db is available for local transport.
   *
   * Used by factory.ts to decide whether to use LocalTransport.
   *
   * @task T356
   * @epic T310
   * @param cwd - Optional working directory override (defaults to process.cwd()).
   * @returns `true` if conduit.db exists at the expected path.
   */
  static isAvailable(cwd) {
    const dbPath = getConduitDbPath(cwd ?? process.cwd());
    return existsSync(dbPath);
  }
  /** Poll for new messages and notify subscribers (cross-process sync). */
  async pollAndNotify() {
    if (!this.state || this.state.subscribers.size === 0) return;
    const messages = await this.poll({ limit: 20 });
    for (const msg of messages) {
      this.notifySubscribers(msg);
    }
    if (messages.length > 0) {
      await this.ack(messages.map((m) => m.id));
    }
  }
  /** Notify all active subscribers of a new message. */
  notifySubscribers(message) {
    if (!this.state) return;
    for (const handler of this.state.subscribers) {
      try {
        handler(message);
      } catch {
      }
    }
  }
  /**
   * Ensure a DM conversation exists between two agents.
   *
   * Conversations store participants as a comma-separated TEXT field.
   * We search for existing private conversations containing both agents.
   *
   * @returns The conversation ID.
   */
  ensureDmConversation(fromAgentId, toAgentId) {
    const { db } = this.state;
    const sortedParticipants = [fromAgentId, toAgentId].sort().join(",");
    const existing = db.prepare(
      `SELECT id FROM conversations
         WHERE visibility = 'private' AND participants = ?
         LIMIT 1`
    ).get(sortedParticipants);
    if (existing) return existing.id;
    const convId = randomUUID();
    const nowUnix = Math.floor(Date.now() / 1e3);
    db.prepare(
      `INSERT INTO conversations (id, participants, visibility, message_count, created_at, updated_at)
       VALUES (?, ?, 'private', 0, ?, ?)`
    ).run(convId, sortedParticipants, nowUnix, nowUnix);
    return convId;
  }
  /** Throw if not connected. */
  ensureConnected() {
    if (!this.state) {
      throw new Error("LocalTransport not connected. Call connect() first.");
    }
  }
};

// packages/core/src/conduit/sse-transport.ts
var MAX_RECONNECT_ATTEMPTS = 3;
var MAX_RECONNECT_DELAY_MS = 3e4;
var SseTransport = class {
  name = "sse";
  state = null;
  /**
   * Connect to the SSE endpoint for real-time message delivery.
   *
   * If SSE connection fails, falls back to HTTP polling mode.
   * Auth is conveyed via query parameter (SSE doesn't support custom headers).
   */
  async connect(config) {
    if (this.state?.connected) {
      throw new Error("SseTransport already connected. Disconnect first.");
    }
    const sseEndpoint = config.sseEndpoint;
    if (!sseEndpoint && !config.apiBaseUrl) {
      throw new Error("SseTransport requires sseEndpoint or apiBaseUrl in config.");
    }
    const endpoint = sseEndpoint ?? `${config.apiBaseUrl}/messages/stream`;
    this.state = {
      agentId: config.agentId,
      apiKey: config.apiKey,
      apiBaseUrl: config.apiBaseUrl,
      sseEndpoint: endpoint,
      eventSource: null,
      mode: "sse",
      messageBuffer: [],
      subscribers: /* @__PURE__ */ new Set(),
      reconnectAttempts: 0,
      reconnectTimer: null,
      connected: false
    };
    try {
      await this.connectSse();
    } catch {
      this.state.mode = "http-fallback";
      this.state.connected = true;
    }
  }
  /** Disconnect the transport, closing SSE and clearing all state. */
  async disconnect() {
    if (!this.state) return;
    if (this.state.eventSource) {
      this.state.eventSource.close();
    }
    if (this.state.reconnectTimer) {
      clearTimeout(this.state.reconnectTimer);
    }
    this.state.messageBuffer = [];
    this.state.connected = false;
    this.state = null;
  }
  /**
   * Send a message via HTTP POST.
   *
   * SSE is receive-only — all sends go through HTTP regardless of SSE state.
   */
  async push(to, content, options) {
    this.ensureConnected();
    const body = { content, toAgentId: to };
    let path = "/messages";
    if (options?.conversationId) {
      path = `/conversations/${options.conversationId}/messages`;
      if (options.replyTo) {
        body["replyTo"] = options.replyTo;
      }
    }
    const response = await this.httpFetch(path, {
      method: "POST",
      body: JSON.stringify(body)
    });
    if (!response.ok) {
      const text = await response.text().catch(() => "");
      throw new Error(`SseTransport push failed: ${response.status} ${text}`);
    }
    const data = await response.json();
    return { messageId: data.data?.message?.id ?? data.data?.id ?? "unknown" };
  }
  /**
   * Poll for messages.
   *
   * In SSE mode: drains the internal message buffer (no HTTP request).
   * In HTTP fallback mode: fetches via GET /messages/peek.
   */
  async poll(options) {
    this.ensureConnected();
    if (this.state.mode === "sse" && this.state.eventSource) {
      let messages = this.state.messageBuffer.splice(0);
      if (options?.since) {
        messages = messages.filter((m) => m.timestamp > options.since);
      }
      if (options?.limit && messages.length > options.limit) {
        const excess = messages.splice(options.limit);
        this.state.messageBuffer.unshift(...excess);
      }
      return messages;
    }
    return this.httpPoll(options);
  }
  /** Acknowledge messages via HTTP POST. */
  async ack(messageIds) {
    this.ensureConnected();
    if (messageIds.length === 0) return;
    await this.httpFetch("/messages/ack", {
      method: "POST",
      body: JSON.stringify({ messageIds })
    });
  }
  /**
   * Subscribe to real-time messages.
   *
   * In SSE mode, messages are pushed to the handler as they arrive.
   * In HTTP fallback mode, polls on an interval.
   */
  subscribe(handler) {
    this.ensureConnected();
    this.state.subscribers.add(handler);
    const interval = this.state.mode === "http-fallback" ? setInterval(async () => {
      if (this.state?.mode === "http-fallback") {
        const messages = await this.httpPoll({ limit: 20 });
        for (const msg of messages) handler(msg);
        if (messages.length > 0) {
          await this.ack(messages.map((m) => m.id));
        }
      }
    }, 5e3) : null;
    return () => {
      if (interval) clearInterval(interval);
      if (this.state) {
        this.state.subscribers.delete(handler);
      }
    };
  }
  // --------------------------------------------------------------------------
  // SSE connection management
  // --------------------------------------------------------------------------
  /** Establish SSE connection. Resolves when open, rejects on error. */
  connectSse() {
    return new Promise((resolve, reject) => {
      if (!this.state) {
        reject(new Error("No state"));
        return;
      }
      const url = `${this.state.sseEndpoint}?token=${encodeURIComponent(this.state.apiKey)}&agent_id=${encodeURIComponent(this.state.agentId)}`;
      const es = new EventSource(url);
      this.state.eventSource = es;
      const timeout = setTimeout(() => {
        es.close();
        reject(new Error("SSE connection timeout"));
      }, 1e4);
      es.addEventListener("open", () => {
        clearTimeout(timeout);
        this.state.connected = true;
        this.state.reconnectAttempts = 0;
        resolve();
      });
      es.addEventListener("message", (event) => {
        this.handleSseMessage(event);
      });
      es.addEventListener("error", () => {
        clearTimeout(timeout);
        if (!this.state.connected) {
          es.close();
          reject(new Error("SSE connection failed"));
        } else {
          this.handleSseDisconnect();
        }
      });
    });
  }
  /** Handle an incoming SSE message event. */
  handleSseMessage(event) {
    if (!this.state) return;
    try {
      const data = JSON.parse(event.data);
      if (data.type === "heartbeat" || data.type === "ping") return;
      const from = data.from_agent_id ?? data.from ?? "unknown";
      if (from === this.state.agentId) return;
      const message = {
        id: data.id ?? `sse-${Date.now()}`,
        from,
        content: data.content ?? "",
        threadId: data.conversation_id ?? data.threadId,
        timestamp: data.created_at ?? data.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
      };
      this.state.messageBuffer.push(message);
      for (const h of this.state.subscribers) {
        try {
          h(message);
        } catch {
        }
      }
    } catch {
    }
  }
  /** Handle SSE connection drop — attempt reconnect with backoff. */
  handleSseDisconnect() {
    if (!this.state) return;
    this.state.eventSource?.close();
    this.state.eventSource = null;
    this.state.reconnectAttempts++;
    if (this.state.reconnectAttempts > MAX_RECONNECT_ATTEMPTS) {
      this.state.mode = "http-fallback";
      return;
    }
    const delay = Math.min(1e3 * 2 ** (this.state.reconnectAttempts - 1), MAX_RECONNECT_DELAY_MS);
    this.state.reconnectTimer = setTimeout(() => {
      void this.connectSse().catch(() => {
        this.handleSseDisconnect();
      });
    }, delay);
  }
  // --------------------------------------------------------------------------
  // HTTP helpers
  // --------------------------------------------------------------------------
  /** HTTP poll for messages (used in fallback mode). */
  async httpPoll(options) {
    const params = new URLSearchParams();
    params.set("mentioned", this.state.agentId);
    if (options?.limit) params.set("limit", String(options.limit));
    if (options?.since) params.set("since", options.since);
    const response = await this.httpFetch(`/messages/peek?${params}`, { method: "GET" });
    if (!response.ok) return [];
    const data = await response.json();
    return (data.data?.messages ?? []).map((m) => ({
      id: m.id,
      from: m.fromAgentId ?? "unknown",
      content: m.content ?? "",
      threadId: m.conversationId,
      timestamp: m.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
    }));
  }
  /** Make an authenticated HTTP request to the API. */
  async httpFetch(path, init) {
    const url = `${this.state.apiBaseUrl}${path}`;
    return fetch(url, {
      ...init,
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${this.state.apiKey}`,
        "X-Agent-Id": this.state.agentId,
        ...init.headers
      },
      signal: init.signal ?? AbortSignal.timeout(1e4)
    });
  }
  /** Throw if not connected. */
  ensureConnected() {
    if (!this.state?.connected) {
      throw new Error("SseTransport not connected. Call connect() first.");
    }
  }
};

// packages/core/src/conduit/factory.ts
function resolveTransport(credential) {
  if (LocalTransport.isAvailable()) {
    return new LocalTransport();
  }
  const isCloudBacked = credential.apiBaseUrl && credential.apiBaseUrl !== "local" && credential.apiBaseUrl.startsWith("http");
  if (isCloudBacked && credential.transportConfig.sseEndpoint) {
    return new SseTransport();
  }
  return new HttpTransport();
}
async function createConduit(registry, agentId) {
  const credential = agentId ? await registry.get(agentId) : await registry.getActive();
  if (!credential) {
    throw new Error(
      "No agent credential found. Run: cleo agent register --id <id> --api-key <key>"
    );
  }
  const transport = resolveTransport(credential);
  const conduit = new ConduitClient(transport, credential);
  await conduit.connect();
  return conduit;
}
export {
  ConduitClient,
  HttpTransport,
  LocalTransport,
  SseTransport,
  createConduit,
  resolveTransport
};
//# sourceMappingURL=index.js.map
