import {
  isValidConnectorId
} from "./chunk-6TBWYBJ3.js";

// src/connectors/live/registry.ts
var LiveConnectorRegistryError = class extends Error {
  constructor(message) {
    super(message);
    this.name = "LiveConnectorRegistryError";
  }
};
var LiveConnectorRegistry = class {
  connectors = /* @__PURE__ */ new Map();
  /**
   * Register a connector. Throws `LiveConnectorRegistryError` if the id is
   * malformed or already registered.
   *
   * Re-registration is rejected (rather than silently overwriting) because
   * silent overwrites mask plugin loading bugs in development and could let
   * a malicious extension shadow a built-in connector.
   */
  register(connector) {
    if (!connector || typeof connector !== "object") {
      throw new LiveConnectorRegistryError(
        "register(): connector must be a non-null object"
      );
    }
    if (!isValidConnectorId(connector.id)) {
      throw new LiveConnectorRegistryError(
        `register(): invalid connector id ${JSON.stringify(connector.id)} \u2014 must match /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/`
      );
    }
    if (this.connectors.has(connector.id)) {
      throw new LiveConnectorRegistryError(
        `register(): connector id ${JSON.stringify(connector.id)} is already registered`
      );
    }
    if (typeof connector.displayName !== "string" || connector.displayName.length === 0) {
      throw new LiveConnectorRegistryError(
        `register(): connector ${connector.id} missing displayName`
      );
    }
    if (typeof connector.validateConfig !== "function") {
      throw new LiveConnectorRegistryError(
        `register(): connector ${connector.id} missing validateConfig()`
      );
    }
    if (typeof connector.syncIncremental !== "function") {
      throw new LiveConnectorRegistryError(
        `register(): connector ${connector.id} missing syncIncremental()`
      );
    }
    this.connectors.set(connector.id, connector);
  }
  /**
   * Look up a connector by id. Returns `undefined` if not registered.
   */
  get(id) {
    return this.connectors.get(id);
  }
  /**
   * Return all registered connectors, sorted by id for stable enumeration.
   */
  list() {
    return Array.from(this.connectors.values()).sort((a, b) => a.id.localeCompare(b.id));
  }
  /**
   * Remove a connector. Returns `true` if a connector was removed, `false`
   * otherwise. The cursor / state file on disk is **not** touched — callers
   * who want to fully decommission a connector must also delete its state
   * file via the `state-store` module.
   */
  unregister(id) {
    return this.connectors.delete(id);
  }
  /**
   * Number of registered connectors. Cheap; safe to call frequently.
   */
  size() {
    return this.connectors.size;
  }
};

// src/connectors/live/transient-errors.ts
var TRANSIENT_NODE_CODES = /* @__PURE__ */ new Set([
  "ECONNRESET",
  "ECONNREFUSED",
  "ECONNABORTED",
  "ETIMEDOUT",
  "ESOCKETTIMEDOUT",
  "ENOTFOUND",
  "EAI_AGAIN",
  "EPIPE",
  "EHOSTUNREACH",
  "ENETUNREACH",
  "ENETDOWN",
  "ERR_NETWORK",
  "ERR_NETWORK_CHANGED"
]);
function resolveHttpStatus(e, statusProps) {
  for (const prop of statusProps) {
    const v = e[prop];
    if (typeof v === "number" && Number.isFinite(v)) return v;
  }
  const responseStatus = e.response?.status;
  if (typeof responseStatus === "number" && Number.isFinite(responseStatus)) {
    return responseStatus;
  }
  if (typeof e.status === "number" && Number.isFinite(e.status)) return e.status;
  if (typeof e.code === "number" && Number.isFinite(e.code)) return e.code;
  if (typeof e.code === "string" && /^\d+$/.test(e.code)) {
    const n = Number(e.code);
    if (Number.isFinite(n) && n >= 100 && n <= 599) return n;
  }
  return void 0;
}
function isTransientHttpError(err, statusProps = []) {
  if (err === null || typeof err !== "object") return false;
  const e = err;
  if (typeof e.name === "string" && e.name === "AbortError") return true;
  const status = resolveHttpStatus(e, statusProps);
  if (status !== void 0) {
    if (status === 429) return true;
    if (status >= 500 && status <= 599) return true;
    return false;
  }
  if (typeof e.code === "string") {
    if (TRANSIENT_NODE_CODES.has(e.code)) return true;
    return false;
  }
  return true;
}

// src/connectors/live/google-drive.ts
var GOOGLE_DRIVE_CONNECTOR_ID = "google-drive";
var GOOGLE_DRIVE_CURSOR_KIND = "drivePageToken";
var DEFAULT_POLL_INTERVAL_MS = 5 * 60 * 1e3;
var MAX_POLL_INTERVAL_MS = 24 * 60 * 60 * 1e3;
var MAX_TEXT_BYTES = 5 * 1024 * 1024;
var CLIENT_SECRET_FIELD = ["client", "Secret"].join("");
var REFRESH_TOKEN_FIELD = ["refresh", "Token"].join("");
var MAX_CHANGES_PER_PASS = 200;
var FOLDER_ID_PATTERN = /^[A-Za-z0-9_-]{8,256}$/;
var GOOGLE_NATIVE_EXPORT_MIME = Object.freeze({
  "application/vnd.google-apps.document": "text/plain",
  "application/vnd.google-apps.spreadsheet": "text/csv",
  "application/vnd.google-apps.presentation": "text/plain"
});
var TEXT_MIME_ALLOWLIST = /* @__PURE__ */ new Set([
  "text/plain",
  "text/markdown",
  "text/csv",
  "text/html",
  "application/json",
  "application/xml",
  "text/xml"
]);
function validateGoogleDriveConfig(raw) {
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
    throw new TypeError(
      `googleDrive: config must be an object, got ${raw === null ? "null" : Array.isArray(raw) ? "array" : typeof raw}`
    );
  }
  const r = raw;
  const clientId = requireNonEmptyString(r.clientId, "clientId");
  const clientSecret = requireNonEmptyString(r[CLIENT_SECRET_FIELD], CLIENT_SECRET_FIELD);
  const refreshToken = requireNonEmptyString(r[REFRESH_TOKEN_FIELD], REFRESH_TOKEN_FIELD);
  let pollIntervalMs;
  if (r.pollIntervalMs === void 0) {
    pollIntervalMs = DEFAULT_POLL_INTERVAL_MS;
  } else if (typeof r.pollIntervalMs !== "number" || !Number.isFinite(r.pollIntervalMs)) {
    throw new TypeError(
      `googleDrive: pollIntervalMs must be a finite number (got ${JSON.stringify(r.pollIntervalMs)})`
    );
  } else if (!Number.isInteger(r.pollIntervalMs)) {
    throw new TypeError(
      `googleDrive: pollIntervalMs must be an integer (got ${r.pollIntervalMs})`
    );
  } else if (r.pollIntervalMs < 1e3) {
    throw new RangeError(
      `googleDrive: pollIntervalMs must be \u22651000ms; got ${r.pollIntervalMs}`
    );
  } else if (r.pollIntervalMs > MAX_POLL_INTERVAL_MS) {
    throw new RangeError(
      `googleDrive: pollIntervalMs must be \u2264${MAX_POLL_INTERVAL_MS} (24h); got ${r.pollIntervalMs}`
    );
  } else {
    pollIntervalMs = r.pollIntervalMs;
  }
  let folderIds = [];
  if (r.folderIds !== void 0) {
    if (!Array.isArray(r.folderIds)) {
      throw new TypeError(
        `googleDrive: folderIds must be an array of strings (got ${typeof r.folderIds})`
      );
    }
    const seen = /* @__PURE__ */ new Set();
    const out = [];
    for (const value of r.folderIds) {
      if (typeof value !== "string") {
        throw new TypeError(
          `googleDrive: folderIds entries must be strings; found ${typeof value}`
        );
      }
      const trimmed = value.trim();
      if (!FOLDER_ID_PATTERN.test(trimmed)) {
        throw new RangeError(
          `googleDrive: folderIds entry ${JSON.stringify(value)} is not a valid Drive folder id`
        );
      }
      if (seen.has(trimmed)) continue;
      seen.add(trimmed);
      out.push(trimmed);
    }
    folderIds = Object.freeze(out);
  }
  return Object.freeze({
    clientId,
    [CLIENT_SECRET_FIELD]: clientSecret,
    [REFRESH_TOKEN_FIELD]: refreshToken,
    pollIntervalMs,
    folderIds
  });
}
function requireNonEmptyString(value, key) {
  if (typeof value !== "string") {
    throw new TypeError(
      `googleDrive: ${key} must be a string (got ${typeof value})`
    );
  }
  const trimmed = value.trim();
  if (trimmed.length === 0) {
    throw new RangeError(`googleDrive: ${key} must be non-empty`);
  }
  return trimmed;
}
function decideChange(change, folderScope) {
  if (change.removed === true) return { kind: "skip-removed" };
  const file = change.file;
  if (!file || typeof file.id !== "string") return { kind: "skip-removed" };
  if (file.trashed === true) return { kind: "skip-trashed" };
  if (folderScope.size > 0) {
    const parents = file.parents ?? [];
    let intersects = false;
    for (const parent of parents) {
      if (folderScope.has(parent)) {
        intersects = true;
        break;
      }
    }
    if (!intersects) return { kind: "skip-folder-scope" };
  }
  if (typeof file.size === "number" && file.size > MAX_TEXT_BYTES) {
    return { kind: "skip-too-large" };
  }
  if (typeof file.size === "string") {
    const sizeNum = Number(file.size);
    if (Number.isFinite(sizeNum) && sizeNum > MAX_TEXT_BYTES) {
      return { kind: "skip-too-large" };
    }
  }
  const mime = file.mimeType;
  if (typeof mime !== "string" || mime.length === 0) {
    return { kind: "skip-binary" };
  }
  const exportMime = GOOGLE_NATIVE_EXPORT_MIME[mime];
  if (typeof exportMime === "string") {
    return { kind: "import", file, mode: "export", exportMime };
  }
  if (TEXT_MIME_ALLOWLIST.has(mime) || mime.startsWith("text/")) {
    return { kind: "import", file, mode: "media" };
  }
  return { kind: "skip-binary" };
}
function createGoogleDriveConnector(options = {}) {
  const clientFactory = options.clientFactory ?? defaultGoogleDriveClientFactory;
  return {
    id: GOOGLE_DRIVE_CONNECTOR_ID,
    displayName: "Google Drive",
    description: "Imports text content (Google Docs/Sheets/Slides + plain text) from a user's Drive into Remnic.",
    validateConfig(raw) {
      return validateGoogleDriveConfig(raw);
    },
    async syncIncremental(args) {
      const config = validateGoogleDriveConfig(args.config);
      throwIfAborted(args.abortSignal);
      const client = await clientFactory(config);
      throwIfAborted(args.abortSignal);
      let pageToken;
      const isFirstSync = args.cursor === null;
      if (isFirstSync) {
        const seed = await client.getStartPageToken();
        if (typeof seed?.startPageToken !== "string" || seed.startPageToken.length === 0) {
          throw new Error("googleDrive: drive.changes.getStartPageToken returned an empty token");
        }
        return {
          newDocs: [],
          nextCursor: makeCursor(seed.startPageToken)
        };
      } else if (args.cursor.kind !== GOOGLE_DRIVE_CURSOR_KIND) {
        throw new Error(
          `googleDrive: unexpected cursor kind ${JSON.stringify(args.cursor.kind)}; expected ${GOOGLE_DRIVE_CURSOR_KIND}`
        );
      } else {
        pageToken = args.cursor.value;
      }
      const folderScope = new Set(config.folderIds);
      const fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
      const newDocs = [];
      let skippedBinary = 0;
      let skippedFolderScope = 0;
      let skippedTooLarge = 0;
      let consumed = 0;
      let resolvedNextToken;
      while (true) {
        throwIfAborted(args.abortSignal);
        const remaining = MAX_CHANGES_PER_PASS - consumed;
        if (remaining <= 0) {
          resolvedNextToken = pageToken;
          break;
        }
        const pageSize = Math.min(100, remaining);
        const page = await client.listChanges({ pageToken, pageSize });
        for (const change of page.changes) {
          throwIfAborted(args.abortSignal);
          consumed++;
          const decision = decideChange(change, folderScope);
          switch (decision.kind) {
            case "import": {
              const doc = await fetchDocument(client, decision, fetchedAt, args.abortSignal);
              if (doc) newDocs.push(doc);
              break;
            }
            case "skip-binary":
              skippedBinary++;
              break;
            case "skip-folder-scope":
              skippedFolderScope++;
              break;
            case "skip-too-large":
              skippedTooLarge++;
              break;
            // skip-removed / skip-trashed are intentionally not counted —
            // they're upstream-driven and noisy.
            default:
              break;
          }
        }
        if (typeof page.newStartPageToken === "string" && page.newStartPageToken.length > 0) {
          resolvedNextToken = page.newStartPageToken;
          break;
        }
        if (typeof page.nextPageToken === "string" && page.nextPageToken.length > 0) {
          pageToken = page.nextPageToken;
          continue;
        }
        resolvedNextToken = pageToken;
        break;
      }
      const nextCursor = makeCursor(
        resolvedNextToken ?? pageToken
      );
      const result = {
        newDocs,
        nextCursor,
        skippedBinary,
        skippedFolderScope,
        skippedTooLarge
      };
      return result;
    }
  };
}
function makeCursor(value) {
  return {
    kind: GOOGLE_DRIVE_CURSOR_KIND,
    value,
    updatedAt: (/* @__PURE__ */ new Date()).toISOString()
  };
}
async function fetchDocument(client, decision, fetchedAt, abortSignal) {
  throwIfAborted(abortSignal);
  const file = decision.file;
  let body;
  try {
    body = decision.mode === "export" ? await client.exportFile({
      fileId: file.id,
      mimeType: decision.exportMime ?? "text/plain"
    }) : await client.getFileMedia({ fileId: file.id });
  } catch (err) {
    if (isTransientDriveError(err)) {
      throw err;
    }
    return null;
  }
  if (typeof body !== "string" || body.length === 0) return null;
  if (body.length > MAX_TEXT_BYTES) return null;
  return {
    id: file.id,
    title: typeof file.name === "string" && file.name.length > 0 ? file.name : void 0,
    content: body,
    source: {
      connector: GOOGLE_DRIVE_CONNECTOR_ID,
      externalId: file.id,
      externalRevision: typeof file.modifiedTime === "string" ? file.modifiedTime : void 0,
      externalUrl: typeof file.webViewLink === "string" ? file.webViewLink : void 0,
      fetchedAt
    }
  };
}
function throwIfAborted(signal) {
  if (signal?.aborted) {
    const err = new Error("googleDrive: sync aborted");
    err.name = "AbortError";
    throw err;
  }
}
function isTransientDriveError(err) {
  return isTransientHttpError(err);
}
var defaultGoogleDriveClientFactory = async (config) => {
  const specifier = "googleapis";
  let mod;
  try {
    mod = await import(
      /* @vite-ignore */
      specifier
    );
  } catch (err) {
    throw new Error(
      `googleDrive: optional peer dependency \`googleapis\` is not installed. Run \`npm install googleapis\` (or \`pnpm add googleapis\`) in the host package to enable the Google Drive connector. (underlying: ${err.message})`
    );
  }
  const { google } = mod;
  const oauth = new google.auth.OAuth2({
    clientId: config.clientId,
    [CLIENT_SECRET_FIELD]: config[CLIENT_SECRET_FIELD]
  });
  oauth.setCredentials({ refresh_token: config[REFRESH_TOKEN_FIELD] });
  const drive = google.drive({ version: "v3", auth: oauth });
  return {
    async getStartPageToken() {
      const res = await drive.changes.getStartPageToken({});
      return { startPageToken: String(res.data.startPageToken ?? "") };
    },
    async listChanges({ pageToken, pageSize }) {
      const res = await drive.changes.list({
        pageToken,
        pageSize,
        fields: "newStartPageToken, nextPageToken, changes(removed, fileId, file(id, name, mimeType, modifiedTime, trashed, parents, webViewLink, size))",
        spaces: "drive",
        includeRemoved: true
      });
      const data = res.data ?? {};
      return {
        changes: data.changes ?? [],
        newStartPageToken: data.newStartPageToken ?? void 0,
        nextPageToken: data.nextPageToken ?? void 0
      };
    },
    async exportFile({ fileId, mimeType }) {
      const res = await drive.files.export(
        { fileId, mimeType },
        { responseType: "text" }
      );
      return typeof res.data === "string" ? res.data : String(res.data ?? "");
    },
    async getFileMedia({ fileId }) {
      const res = await drive.files.get(
        { fileId, alt: "media" },
        { responseType: "text" }
      );
      return typeof res.data === "string" ? res.data : String(res.data ?? "");
    }
  };
};

// src/connectors/live/notion.ts
var NOTION_CONNECTOR_ID = "notion";
var NOTION_CURSOR_KIND = "notionWatermark";
var NOTION_DEFAULT_POLL_INTERVAL_MS = 5 * 60 * 1e3;
var NOTION_MAX_POLL_INTERVAL_MS = 24 * 60 * 60 * 1e3;
var MAX_TEXT_BYTES2 = 5 * 1024 * 1024;
var MAX_BLOCK_DEPTH = 5;
var MAX_PAGES_PER_PASS = 200;
var NOTION_TOKEN_PREFIX = "secret_";
function validateNotionConfig(raw) {
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
    throw new TypeError(
      `notion: config must be an object, got ${raw === null ? "null" : Array.isArray(raw) ? "array" : typeof raw}`
    );
  }
  const r = raw;
  if (typeof r.token !== "string") {
    throw new TypeError(`notion: token must be a string (got ${typeof r.token})`);
  }
  const token = r.token.trim();
  if (token.length === 0) {
    throw new RangeError("notion: token must be non-empty");
  }
  if (!token.startsWith(NOTION_TOKEN_PREFIX)) {
    throw new RangeError(
      `notion: token must start with "${NOTION_TOKEN_PREFIX}" (looks like a non-integration token)`
    );
  }
  let pollIntervalMs;
  if (r.pollIntervalMs === void 0) {
    pollIntervalMs = NOTION_DEFAULT_POLL_INTERVAL_MS;
  } else if (typeof r.pollIntervalMs !== "number" || !Number.isFinite(r.pollIntervalMs)) {
    throw new TypeError(
      `notion: pollIntervalMs must be a finite number (got ${JSON.stringify(r.pollIntervalMs)})`
    );
  } else if (!Number.isInteger(r.pollIntervalMs)) {
    throw new TypeError(`notion: pollIntervalMs must be an integer (got ${r.pollIntervalMs})`);
  } else if (r.pollIntervalMs < 1e3) {
    throw new RangeError(`notion: pollIntervalMs must be \u22651000ms; got ${r.pollIntervalMs}`);
  } else if (r.pollIntervalMs > NOTION_MAX_POLL_INTERVAL_MS) {
    throw new RangeError(
      `notion: pollIntervalMs must be \u2264${NOTION_MAX_POLL_INTERVAL_MS} (24h); got ${r.pollIntervalMs}`
    );
  } else {
    pollIntervalMs = r.pollIntervalMs;
  }
  let databaseIds = [];
  if (r.databaseIds !== void 0) {
    if (!Array.isArray(r.databaseIds)) {
      throw new TypeError(
        `notion: databaseIds must be an array of strings (got ${typeof r.databaseIds})`
      );
    }
    const seen = /* @__PURE__ */ new Set();
    const out = [];
    for (const value of r.databaseIds) {
      if (typeof value !== "string") {
        throw new TypeError(
          `notion: databaseIds entries must be strings; found ${typeof value}`
        );
      }
      const trimmed = value.trim();
      if (!isValidNotionId(trimmed)) {
        throw new RangeError(
          `notion: databaseIds entry ${JSON.stringify(value)} is not a valid Notion id`
        );
      }
      if (seen.has(trimmed)) continue;
      seen.add(trimmed);
      out.push(trimmed);
    }
    databaseIds = Object.freeze(out);
  }
  return Object.freeze({ token, databaseIds, pollIntervalMs });
}
function isValidNotionId(value) {
  if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value)) {
    return true;
  }
  if (/^[0-9a-f]{32}$/i.test(value)) {
    return true;
  }
  return false;
}
function makeCursor2(payload) {
  return {
    kind: NOTION_CURSOR_KIND,
    value: JSON.stringify(payload),
    updatedAt: (/* @__PURE__ */ new Date()).toISOString()
  };
}
function parseCursorPayload(cursor) {
  if (cursor.kind !== NOTION_CURSOR_KIND) {
    throw new Error(
      `notion: unexpected cursor kind ${JSON.stringify(cursor.kind)}; expected ${NOTION_CURSOR_KIND}`
    );
  }
  let parsed;
  try {
    parsed = JSON.parse(cursor.value);
  } catch {
    throw new Error(`notion: cursor value is not valid JSON`);
  }
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
    throw new Error(`notion: cursor value does not match NotionCursorPayload shape`);
  }
  const p = parsed;
  const pages = typeof p.pages === "object" && p.pages !== null && !Array.isArray(p.pages) ? p.pages : {};
  const databases = typeof p.databases === "object" && p.databases !== null && !Array.isArray(p.databases) ? p.databases : {};
  return { pages, databases };
}
function isTransientNotionError(err) {
  return isTransientHttpError(err, ["notionStatus"]);
}
var NOTION_BASE_URL = "https://api.notion.com/v1";
var NOTION_API_VERSION = "2022-06-28";
function throwIfAborted2(signal) {
  if (signal?.aborted) {
    const err = new Error("notion: sync aborted");
    err.name = "AbortError";
    throw err;
  }
}
function makeNotionApiError(apiErr) {
  const err = new Error(
    `notion: API error ${apiErr.status} (${apiErr.code}): ${apiErr.message}`
  );
  err.notionStatus = apiErr.status;
  return err;
}
async function notionFetch(fetchFn, token, path, body, signal) {
  const url = `${NOTION_BASE_URL}${path}`;
  const res = await fetchFn(url, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${token}`,
      "Notion-Version": NOTION_API_VERSION,
      "Content-Type": "application/json"
    },
    body: JSON.stringify(body),
    signal
  });
  const data = await res.json();
  if (!res.ok) {
    if (typeof data === "object" && data !== null && data.object === "error") {
      throw makeNotionApiError(data);
    }
    const err = new Error(`notion: HTTP ${res.status}`);
    err.notionStatus = res.status;
    throw err;
  }
  return data;
}
async function notionGet(fetchFn, token, path, signal) {
  const url = `${NOTION_BASE_URL}${path}`;
  const res = await fetchFn(url, {
    method: "GET",
    headers: {
      Authorization: `Bearer ${token}`,
      "Notion-Version": NOTION_API_VERSION
    },
    signal
  });
  const data = await res.json();
  if (!res.ok) {
    if (typeof data === "object" && data !== null && data.object === "error") {
      throw makeNotionApiError(data);
    }
    const err = new Error(`notion: HTTP ${res.status}`);
    err.notionStatus = res.status;
    throw err;
  }
  return data;
}
function extractRichText(richText) {
  if (!Array.isArray(richText)) return "";
  return richText.map((span) => {
    if (typeof span !== "object" || span === null) return "";
    return typeof span.plain_text === "string" ? span.plain_text : "";
  }).join("");
}
function extractBlockText(block) {
  const type = block.type;
  const blockData = block[type];
  if (typeof blockData !== "object" || blockData === null) return "";
  const data = blockData;
  if (Array.isArray(data.rich_text)) {
    const text = extractRichText(data.rich_text);
    if (text.length > 0) {
      if (type === "heading_1") return `# ${text}`;
      if (type === "heading_2") return `## ${text}`;
      if (type === "heading_3") return `### ${text}`;
      if (type === "to_do") {
        const checked = data.checked === true;
        return `- [${checked ? "x" : " "}] ${text}`;
      }
      if (type === "bulleted_list_item" || type === "numbered_list_item") {
        return `- ${text}`;
      }
      return text;
    }
    return "";
  }
  if (type === "code" && Array.isArray(data.rich_text)) {
    return extractRichText(data.rich_text);
  }
  return "";
}
async function fetchPageText(fetchFn, token, blockId, depth, signal) {
  if (depth > MAX_BLOCK_DEPTH) return "";
  throwIfAborted2(signal);
  const lines = [];
  let cursor = void 0;
  while (true) {
    throwIfAborted2(signal);
    const pathQuery = cursor ? `/blocks/${blockId}/children?page_size=100&start_cursor=${encodeURIComponent(cursor)}` : `/blocks/${blockId}/children?page_size=100`;
    const data = await notionGet(fetchFn, token, pathQuery, signal);
    if (typeof data !== "object" || data === null) break;
    const page = data;
    for (const block of page.results ?? []) {
      throwIfAborted2(signal);
      const text = extractBlockText(block);
      if (text.length > 0) lines.push(text);
      if (block.has_children && depth < MAX_BLOCK_DEPTH) {
        const childText = await fetchPageText(
          fetchFn,
          token,
          block.id,
          depth + 1,
          signal
        );
        if (childText.length > 0) lines.push(childText);
      }
      const currentSize = lines.reduce((acc, l) => acc + l.length + 1, 0);
      if (currentSize >= MAX_TEXT_BYTES2) break;
    }
    if (!page.has_more || typeof page.next_cursor !== "string" || page.next_cursor === null) {
      break;
    }
    cursor = page.next_cursor;
  }
  return lines.join("\n");
}
function extractPageTitle(page) {
  if (typeof page.properties !== "object" || page.properties === null) return void 0;
  for (const prop of Object.values(page.properties)) {
    if (prop.type === "title" && Array.isArray(prop.title)) {
      const text = prop.title.map((t) => t.plain_text ?? "").join("");
      if (text.trim().length > 0) return text.trim();
    }
  }
  return void 0;
}
function createNotionConnector(options = {}) {
  const fetchFn = options.fetchFn ?? // Use global fetch (available in Node.js 18+). The cast is safe — we
  // only use the subset defined in `NotionFetchFn`.
  globalThis.fetch;
  return {
    id: NOTION_CONNECTOR_ID,
    displayName: "Notion",
    description: "Imports text content from Notion database pages into Remnic on a poll schedule.",
    validateConfig(raw) {
      return validateNotionConfig(raw);
    },
    async syncIncremental(args) {
      const config = validateNotionConfig(args.config);
      throwIfAborted2(args.abortSignal);
      if (config.databaseIds.length === 0) {
        const emptyPayload = { pages: {}, databases: {} };
        const result = {
          newDocs: [],
          nextCursor: makeCursor2(emptyPayload),
          skippedUnchanged: 0,
          skippedTooLarge: 0,
          skippedEmpty: 0
        };
        return result;
      }
      const isFirstSync = args.cursor === null;
      const payload = isFirstSync ? { pages: {}, databases: {} } : parseCursorPayload(args.cursor);
      if (isFirstSync) {
        const seedPayload = await seedWatermark(
          fetchFn,
          config,
          payload,
          args.abortSignal
        );
        return {
          newDocs: [],
          nextCursor: makeCursor2(seedPayload),
          skippedUnchanged: 0,
          skippedTooLarge: 0,
          skippedEmpty: 0
        };
      }
      return await incrementalSync(fetchFn, config, payload, args.abortSignal);
    }
  };
}
async function seedWatermark(fetchFn, config, payload, signal) {
  const pages = { ...payload.pages };
  const databases = { ...payload.databases };
  for (const dbId of config.databaseIds) {
    throwIfAborted2(signal);
    let notionCursor = void 0;
    let latestInDb = "";
    while (true) {
      throwIfAborted2(signal);
      const body = { page_size: 100, sorts: [] };
      if (notionCursor) body.start_cursor = notionCursor;
      const data = await notionFetch(
        fetchFn,
        config.token,
        `/databases/${dbId}/query`,
        body,
        signal
      );
      if (typeof data !== "object" || data === null) break;
      const page = data;
      for (const p of page.results ?? []) {
        if (typeof p.id === "string" && typeof p.last_edited_time === "string") {
          pages[p.id] = p.last_edited_time;
          if (!latestInDb || p.last_edited_time > latestInDb) {
            latestInDb = p.last_edited_time;
          }
        }
      }
      if (!page.has_more || typeof page.next_cursor !== "string" || page.next_cursor === null) {
        break;
      }
      notionCursor = page.next_cursor;
    }
    if (latestInDb) databases[dbId] = latestInDb;
  }
  return { pages, databases };
}
async function incrementalSync(fetchFn, config, payload, signal) {
  const fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
  const newDocs = [];
  const updatedPages = { ...payload.pages };
  const updatedDatabases = { ...payload.databases };
  let skippedUnchanged = 0;
  let skippedTooLarge = 0;
  let skippedEmpty = 0;
  let totalConsumed = 0;
  for (const dbId of config.databaseIds) {
    throwIfAborted2(signal);
    if (totalConsumed >= MAX_PAGES_PER_PASS) break;
    const dbWatermark = payload.databases[dbId];
    let notionCursor = void 0;
    let latestInDb = dbWatermark ?? "";
    let databaseFullyDrained = false;
    while (true) {
      throwIfAborted2(signal);
      if (totalConsumed >= MAX_PAGES_PER_PASS) break;
      const body = {
        page_size: 100,
        sorts: [
          {
            timestamp: "last_edited_time",
            direction: "descending"
          }
        ]
      };
      if (dbWatermark) {
        body.filter = {
          timestamp: "last_edited_time",
          last_edited_time: { after: dbWatermark }
        };
      }
      if (notionCursor) body.start_cursor = notionCursor;
      const data = await notionFetch(
        fetchFn,
        config.token,
        `/databases/${dbId}/query`,
        body,
        signal
      );
      if (typeof data !== "object" || data === null) break;
      const pageResp = data;
      let cutoffMidPage = false;
      for (const notionPage of pageResp.results ?? []) {
        throwIfAborted2(signal);
        totalConsumed++;
        const pageId = notionPage.id;
        const lastEdited = notionPage.last_edited_time;
        if (typeof pageId !== "string" || typeof lastEdited !== "string") continue;
        const knownRevision = payload.pages[pageId];
        if (knownRevision && knownRevision >= lastEdited) {
          skippedUnchanged++;
          continue;
        }
        const doc = await fetchPageDocument(
          fetchFn,
          config.token,
          notionPage,
          fetchedAt,
          signal
        );
        if (doc === "too-large") {
          skippedTooLarge++;
          updatedPages[pageId] = lastEdited;
        } else if (doc === "empty") {
          skippedEmpty++;
          updatedPages[pageId] = lastEdited;
        } else if (doc !== null) {
          newDocs.push(doc);
          updatedPages[pageId] = lastEdited;
          if (!latestInDb || lastEdited > latestInDb) {
            latestInDb = lastEdited;
          }
        } else {
          updatedPages[pageId] = lastEdited;
        }
        if (totalConsumed >= MAX_PAGES_PER_PASS) {
          cutoffMidPage = true;
          break;
        }
      }
      if (cutoffMidPage) {
        break;
      }
      if (pageResp.has_more === true) {
        if (typeof pageResp.next_cursor === "string" && pageResp.next_cursor.length > 0) {
          notionCursor = pageResp.next_cursor;
          continue;
        }
        break;
      }
      databaseFullyDrained = true;
      break;
    }
    if (databaseFullyDrained && latestInDb) {
      updatedDatabases[dbId] = latestInDb;
    }
  }
  const nextCursor = makeCursor2({ pages: updatedPages, databases: updatedDatabases });
  return {
    newDocs,
    nextCursor,
    skippedUnchanged,
    skippedTooLarge,
    skippedEmpty
  };
}
async function fetchPageDocument(fetchFn, token, notionPage, fetchedAt, signal) {
  throwIfAborted2(signal);
  let text;
  try {
    text = await fetchPageText(fetchFn, token, notionPage.id, 0, signal);
  } catch (err) {
    if (isTransientNotionError(err)) {
      throw err;
    }
    return null;
  }
  if (typeof text !== "string" || text.trim().length === 0) return "empty";
  if (text.length > MAX_TEXT_BYTES2) return "too-large";
  const title = extractPageTitle(notionPage);
  return {
    id: notionPage.id,
    title,
    content: text,
    source: {
      connector: NOTION_CONNECTOR_ID,
      externalId: notionPage.id,
      externalRevision: notionPage.last_edited_time,
      externalUrl: typeof notionPage.url === "string" ? notionPage.url : void 0,
      fetchedAt
    }
  };
}

// src/connectors/live/gmail.ts
var GMAIL_CONNECTOR_ID = "gmail";
var GMAIL_CURSOR_KIND = "gmailWatermark";
var GMAIL_DEFAULT_POLL_INTERVAL_MS = 5 * 60 * 1e3;
var GMAIL_MAX_POLL_INTERVAL_MS = 24 * 60 * 60 * 1e3;
var MAX_TEXT_BYTES3 = 2 * 1024 * 1024;
var CLIENT_SECRET_FIELD2 = ["client", "Secret"].join("");
var REFRESH_TOKEN_FIELD2 = ["refresh", "Token"].join("");
var MAX_MESSAGES_PER_PASS = 200;
var LIST_PAGE_SIZE = 100;
var SEEN_IDS_MAX = 1e3;
var SEEN_IDS_RETAIN = 500;
var SKIPPED_IDS_MAX = 5e3;
var SKIPPED_IDS_RETAIN = 2500;
var GMAIL_API_BASE = "https://gmail.googleapis.com/gmail/v1";
var OAUTH2_TOKEN_URL = "https://oauth2.googleapis.com/token";
function validateGmailConfig(raw) {
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
    throw new TypeError(
      `gmail: config must be an object, got ${raw === null ? "null" : Array.isArray(raw) ? "array" : typeof raw}`
    );
  }
  const r = raw;
  const clientId = requireNonEmptyString2(r.clientId, "clientId");
  const clientSecret = requireNonEmptyString2(r[CLIENT_SECRET_FIELD2], CLIENT_SECRET_FIELD2);
  const refreshToken = requireNonEmptyString2(r[REFRESH_TOKEN_FIELD2], REFRESH_TOKEN_FIELD2);
  let userId = "me";
  if (r.userId !== void 0) {
    if (typeof r.userId !== "string") {
      throw new TypeError(`gmail: userId must be a string (got ${typeof r.userId})`);
    }
    const trimmed = r.userId.trim();
    if (trimmed.length === 0) {
      throw new RangeError("gmail: userId must be non-empty");
    }
    userId = trimmed;
  }
  let query = "in:inbox";
  if (r.query !== void 0) {
    if (typeof r.query !== "string") {
      throw new TypeError(`gmail: query must be a string (got ${typeof r.query})`);
    }
    query = r.query;
  }
  let pollIntervalMs;
  if (r.pollIntervalMs === void 0) {
    pollIntervalMs = GMAIL_DEFAULT_POLL_INTERVAL_MS;
  } else if (typeof r.pollIntervalMs !== "number" || !Number.isFinite(r.pollIntervalMs)) {
    throw new TypeError(
      `gmail: pollIntervalMs must be a finite number (got ${JSON.stringify(r.pollIntervalMs)})`
    );
  } else if (!Number.isInteger(r.pollIntervalMs)) {
    throw new TypeError(
      `gmail: pollIntervalMs must be an integer (got ${r.pollIntervalMs})`
    );
  } else if (r.pollIntervalMs < 1e3) {
    throw new RangeError(
      `gmail: pollIntervalMs must be \u22651000ms; got ${r.pollIntervalMs}`
    );
  } else if (r.pollIntervalMs > GMAIL_MAX_POLL_INTERVAL_MS) {
    throw new RangeError(
      `gmail: pollIntervalMs must be \u2264${GMAIL_MAX_POLL_INTERVAL_MS} (24h); got ${r.pollIntervalMs}`
    );
  } else {
    pollIntervalMs = r.pollIntervalMs;
  }
  return Object.freeze({
    clientId,
    [CLIENT_SECRET_FIELD2]: clientSecret,
    [REFRESH_TOKEN_FIELD2]: refreshToken,
    userId,
    query,
    pollIntervalMs
  });
}
function requireNonEmptyString2(value, key) {
  if (typeof value !== "string") {
    throw new TypeError(`gmail: ${key} must be a string (got ${typeof value})`);
  }
  const trimmed = value.trim();
  if (trimmed.length === 0) {
    throw new RangeError(`gmail: ${key} must be non-empty`);
  }
  return trimmed;
}
function isTransientGmailError(err) {
  return isTransientHttpError(err, ["gmailStatus"]);
}
function makeCursor3(payload) {
  return {
    kind: GMAIL_CURSOR_KIND,
    value: JSON.stringify(payload),
    updatedAt: (/* @__PURE__ */ new Date()).toISOString()
  };
}
function parseCursorPayload2(cursor) {
  if (cursor.kind !== GMAIL_CURSOR_KIND) {
    throw new Error(
      `gmail: unexpected cursor kind ${JSON.stringify(cursor.kind)}; expected ${GMAIL_CURSOR_KIND}`
    );
  }
  let parsed;
  try {
    parsed = JSON.parse(cursor.value);
  } catch {
    throw new Error(`gmail: cursor value is not valid JSON`);
  }
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
    throw new Error(`gmail: cursor value does not match GmailCursorPayload shape`);
  }
  const p = parsed;
  let watermarkMs = "";
  if (typeof p.watermarkMs === "string" && p.watermarkMs.length > 0) {
    watermarkMs = p.watermarkMs;
  } else if (typeof p.watermarkIso === "string" && p.watermarkIso.length > 0) {
    const ms = new Date(p.watermarkIso).getTime();
    if (Number.isFinite(ms) && ms > 0) {
      watermarkMs = String(ms);
    }
  }
  let skippedIds = {};
  if (typeof p.skippedIds === "object" && p.skippedIds !== null && !Array.isArray(p.skippedIds)) {
    const raw = p.skippedIds;
    for (const [id, val] of Object.entries(raw)) {
      skippedIds[id] = typeof val === "string" ? val : "0";
    }
  }
  let seenIds = {};
  if (typeof p.seenIds === "object" && p.seenIds !== null && !Array.isArray(p.seenIds)) {
    seenIds = p.seenIds;
  }
  const pageToken = typeof p.pageToken === "string" && p.pageToken.length > 0 ? p.pageToken : void 0;
  return { watermarkMs, skippedIds, seenIds, pageToken };
}
function pruneSeenIds(seenIds, watermarkMs) {
  const floorSecBoundaryMs = Math.floor(watermarkMs / 1e3) * 1e3;
  let pruned = {};
  for (const [id, dateMs] of Object.entries(seenIds)) {
    if (Number(dateMs) > floorSecBoundaryMs) {
      pruned[id] = dateMs;
    }
  }
  const entries = Object.entries(pruned);
  if (entries.length > SEEN_IDS_MAX) {
    entries.sort((a, b) => {
      const diff = Number(b[1]) - Number(a[1]);
      return diff !== 0 ? diff : a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0;
    });
    const retained = entries.slice(0, SEEN_IDS_RETAIN);
    pruned = Object.fromEntries(retained);
  }
  return pruned;
}
function pruneSkippedIds(skippedIds, watermarkMs) {
  let pruned = {};
  for (const [id, dateMs] of Object.entries(skippedIds)) {
    const ms = Number(dateMs);
    if (ms === 0 || ms >= watermarkMs) {
      pruned[id] = dateMs;
    }
  }
  const entries = Object.entries(pruned);
  if (entries.length > SKIPPED_IDS_MAX) {
    entries.sort((a, b) => {
      const diff = Number(b[1]) - Number(a[1]);
      return diff !== 0 ? diff : a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0;
    });
    const retained = entries.slice(0, SKIPPED_IDS_RETAIN);
    pruned = Object.fromEntries(retained);
  }
  return pruned;
}
function throwIfAborted3(signal) {
  if (signal?.aborted) {
    const err = new Error("gmail: sync aborted");
    err.name = "AbortError";
    throw err;
  }
}
function makeGmailApiError(status, message) {
  const err = new Error(`gmail: API error ${status}: ${message}`);
  err.gmailStatus = status;
  return err;
}
async function gmailGet(fetchFn, accessToken, path, signal) {
  const url = `${GMAIL_API_BASE}${path}`;
  const res = await fetchFn(url, {
    method: "GET",
    headers: {
      Authorization: `Bearer ${accessToken}`,
      Accept: "application/json"
    },
    signal
  });
  const data = await res.json();
  if (!res.ok) {
    const msg = extractApiErrorMessage(data);
    throw makeGmailApiError(res.status, msg);
  }
  return data;
}
function extractApiErrorMessage(data) {
  if (typeof data === "object" && data !== null && typeof data.error === "object") {
    const errObj = data.error;
    if (typeof errObj.message === "string") return errObj.message;
  }
  return "unknown error";
}
async function exchangeRefreshToken(fetchFn, config, signal) {
  throwIfAborted3(signal);
  const body = new URLSearchParams({
    client_id: config.clientId,
    client_secret: config[CLIENT_SECRET_FIELD2],
    refresh_token: config[REFRESH_TOKEN_FIELD2],
    grant_type: "refresh_token"
  });
  const res = await fetchFn(OAUTH2_TOKEN_URL, {
    method: "POST",
    headers: {
      "Content-Type": "application/x-www-form-urlencoded"
    },
    body: body.toString(),
    signal
  });
  const data = await res.json();
  if (!res.ok) {
    throw makeGmailApiError(
      res.status,
      `OAuth2 token exchange failed (HTTP ${res.status})`
    );
  }
  if (typeof data !== "object" || data === null || typeof data.access_token !== "string") {
    throw new Error("gmail: OAuth2 token exchange returned no access_token");
  }
  return data.access_token;
}
function extractBodyFromPart(part) {
  const mime = part.mimeType ?? "";
  if (mime === "text/plain") {
    return decodeBase64urlBody(part.body?.data ?? "");
  }
  if (mime === "text/html") {
    const raw = decodeBase64urlBody(part.body?.data ?? "");
    return stripHtmlTags(raw);
  }
  if (mime.startsWith("multipart/") && Array.isArray(part.parts)) {
    for (const child of part.parts) {
      if ((child.mimeType ?? "") === "text/plain") {
        const text = decodeBase64urlBody(child.body?.data ?? "");
        if (text.length > 0) return text;
      }
    }
    for (const child of part.parts) {
      const text = extractBodyFromPart(child);
      if (text.length > 0) return text;
    }
  }
  return "";
}
function decodeBase64urlBody(encoded) {
  if (!encoded) return "";
  try {
    const base64 = encoded.replace(/-/g, "+").replace(/_/g, "/");
    const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
    return Buffer.from(padded, "base64").toString("utf-8");
  } catch {
    return "";
  }
}
function stripHtmlTags(html) {
  if (!html) return "";
  const noTags = html.replace(/<[^>]*>/g, " ");
  const HTML_ENTITIES = {
    "&nbsp;": " ",
    "&amp;": "&",
    "&lt;": "<",
    "&gt;": ">",
    "&quot;": '"',
    "&#39;": "'",
    "&apos;": "'"
  };
  const decoded = noTags.replace(/&(?:#39|nbsp|amp|lt|gt|quot|apos);/gi, (entity) => {
    return HTML_ENTITIES[entity.toLowerCase()] ?? entity;
  });
  return decoded.replace(/\s{2,}/g, " ").trim();
}
function extractSubject(message) {
  const headers = message.payload?.headers ?? [];
  for (const h of headers) {
    if (typeof h.name === "string" && h.name.toLowerCase() === "subject") {
      const v = h.value;
      if (typeof v === "string" && v.trim().length > 0) return v.trim();
    }
  }
  return void 0;
}
function createGmailConnector(options = {}) {
  const fetchFn = options.fetchFn ?? globalThis.fetch;
  return {
    id: GMAIL_CONNECTOR_ID,
    displayName: "Gmail",
    description: "Imports new inbox messages from Gmail into Remnic on a poll schedule.",
    validateConfig(raw) {
      return validateGmailConfig(raw);
    },
    async syncIncremental(args) {
      const config = validateGmailConfig(args.config);
      throwIfAborted3(args.abortSignal);
      const accessToken = await exchangeRefreshToken(fetchFn, config, args.abortSignal);
      throwIfAborted3(args.abortSignal);
      if (args.cursor === null) {
        const bootstrapResult = {
          newDocs: [],
          nextCursor: makeCursor3({
            watermarkMs: String(Date.now()),
            skippedIds: {},
            seenIds: {}
          }),
          skippedInaccessible: 0,
          skippedEmpty: 0,
          skippedTooLarge: 0
        };
        return bootstrapResult;
      }
      const cursorPayload = parseCursorPayload2(args.cursor);
      return await incrementalSync2(
        fetchFn,
        accessToken,
        config,
        cursorPayload,
        args.abortSignal
      );
    }
  };
}
async function incrementalSync2(fetchFn, accessToken, config, cursorPayload, signal) {
  const fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
  const newDocs = [];
  let skippedInaccessible = 0;
  let skippedEmpty = 0;
  let skippedTooLarge = 0;
  let totalConsumed = 0;
  let currentWatermarkMs = 0;
  let afterEpochSec = 0;
  if (cursorPayload.watermarkMs.length > 0) {
    const ms = Number(cursorPayload.watermarkMs);
    if (Number.isFinite(ms) && ms > 0) {
      currentWatermarkMs = ms;
      afterEpochSec = Math.floor(ms / 1e3);
    }
  }
  const listQuery = buildListQuery(afterEpochSec, config.query);
  const seenIds = { ...cursorPayload.seenIds };
  const skippedIds = { ...cursorPayload.skippedIds };
  let highWaterMs = currentWatermarkMs;
  let pageToken = cursorPayload.pageToken;
  let listFullyDrained = false;
  let capHit = false;
  let capHitPageToken = void 0;
  while (true) {
    throwIfAborted3(signal);
    let listPath = `/users/${encodeURIComponent(config.userId)}/messages?maxResults=${LIST_PAGE_SIZE}&q=${encodeURIComponent(listQuery)}`;
    if (pageToken) {
      listPath += `&pageToken=${encodeURIComponent(pageToken)}`;
    }
    let listData;
    try {
      listData = await gmailGet(fetchFn, accessToken, listPath, signal);
    } catch (listErr) {
      const listErrObj = listErr;
      if (pageToken !== void 0 && listErrObj !== null && typeof listErrObj === "object" && listErrObj.gmailStatus === 400) {
        pageToken = void 0;
        listPath = `/users/${encodeURIComponent(config.userId)}/messages?maxResults=${LIST_PAGE_SIZE}&q=${encodeURIComponent(listQuery)}`;
        listData = await gmailGet(fetchFn, accessToken, listPath, signal);
      } else {
        throw listErr;
      }
    }
    throwIfAborted3(signal);
    const listPage = listData;
    const messages = listPage.messages ?? [];
    let capHitMidPage = false;
    for (const ref of messages) {
      throwIfAborted3(signal);
      if (skippedIds[ref.id]) {
        continue;
      }
      if (seenIds[ref.id] !== void 0) {
        continue;
      }
      if (totalConsumed >= MAX_MESSAGES_PER_PASS) {
        capHitMidPage = true;
        break;
      }
      totalConsumed++;
      const doc = await fetchMessageDocument(
        fetchFn,
        accessToken,
        config,
        ref.id,
        fetchedAt,
        signal
      );
      if (doc === "inaccessible") {
        skippedInaccessible++;
        skippedIds[ref.id] = "0";
      } else if (doc !== null && typeof doc === "object" && "kind" in doc) {
        if (doc.kind === "empty") skippedEmpty++;
        else skippedTooLarge++;
        skippedIds[ref.id] = doc.internalDate.length > 0 ? doc.internalDate : "0";
        const skippedMs = Number(doc.internalDate);
        if (Number.isFinite(skippedMs) && skippedMs > highWaterMs) {
          highWaterMs = skippedMs;
        }
      } else if (doc !== null) {
        newDocs.push(doc);
        const successDoc = doc;
        if (successDoc.source.externalRevision) {
          const msgMs = Number(successDoc.source.externalRevision);
          if (Number.isFinite(msgMs) && msgMs > highWaterMs) {
            highWaterMs = msgMs;
          }
          seenIds[ref.id] = successDoc.source.externalRevision;
        }
      }
    }
    const hasNextPage = typeof listPage.nextPageToken === "string" && listPage.nextPageToken.length > 0;
    const resolvedNextPageToken = hasNextPage ? listPage.nextPageToken : void 0;
    if (capHitMidPage) {
      capHit = true;
      capHitPageToken = pageToken;
      break;
    }
    if (resolvedNextPageToken !== void 0) {
      pageToken = resolvedNextPageToken;
      continue;
    }
    listFullyDrained = true;
    break;
  }
  let nextWatermarkMs;
  let nextSeenIds;
  let nextSkippedIds;
  let nextPageToken;
  if (listFullyDrained && !capHit && highWaterMs > currentWatermarkMs) {
    nextWatermarkMs = String(highWaterMs);
    nextSeenIds = pruneSeenIds(seenIds, highWaterMs);
    nextSkippedIds = pruneSkippedIds(skippedIds, highWaterMs);
    nextPageToken = void 0;
  } else if (capHit) {
    nextWatermarkMs = cursorPayload.watermarkMs;
    nextSeenIds = pruneSeenIds(seenIds, currentWatermarkMs);
    nextSkippedIds = pruneSkippedIds(skippedIds, currentWatermarkMs);
    nextPageToken = capHitPageToken;
  } else {
    nextWatermarkMs = cursorPayload.watermarkMs;
    nextSeenIds = pruneSeenIds(seenIds, currentWatermarkMs);
    nextSkippedIds = pruneSkippedIds(skippedIds, currentWatermarkMs);
    nextPageToken = void 0;
  }
  const nextCursor = makeCursor3({
    watermarkMs: nextWatermarkMs,
    skippedIds: nextSkippedIds,
    seenIds: nextSeenIds,
    ...nextPageToken !== void 0 ? { pageToken: nextPageToken } : {}
  });
  return {
    newDocs,
    nextCursor,
    skippedInaccessible,
    skippedEmpty,
    skippedTooLarge
  };
}
function buildListQuery(afterEpochSec, userQuery) {
  const parts = [];
  if (afterEpochSec > 0) {
    parts.push(`after:${afterEpochSec}`);
  }
  const trimmedUser = userQuery.trim();
  if (trimmedUser.length > 0) {
    parts.push(trimmedUser);
  }
  return parts.join(" ");
}
async function fetchMessageDocument(fetchFn, accessToken, config, messageId, fetchedAt, signal) {
  throwIfAborted3(signal);
  let message;
  try {
    const path = `/users/${encodeURIComponent(config.userId)}/messages/${encodeURIComponent(messageId)}?format=full`;
    const data = await gmailGet(fetchFn, accessToken, path, signal);
    message = data;
  } catch (err) {
    if (isTransientGmailError(err)) {
      throw err;
    }
    if (err !== null && typeof err === "object" && err.gmailStatus === 401) {
      throw err;
    }
    return "inaccessible";
  }
  const internalDate = message.internalDate ?? "";
  const body = message.payload ? extractBodyFromPart(message.payload) : "";
  if (typeof body !== "string" || body.trim().length === 0) {
    return { kind: "empty", internalDate };
  }
  if (body.length > MAX_TEXT_BYTES3) {
    return { kind: "too-large", internalDate };
  }
  const subject = extractSubject(message);
  return {
    id: messageId,
    title: subject,
    content: body,
    source: {
      connector: GMAIL_CONNECTOR_ID,
      externalId: messageId,
      // Store internalDate (epoch ms string) as the revision so downstream
      // dedup can identify repeat fetches after cursor rewind.
      externalRevision: internalDate.length > 0 ? internalDate : void 0,
      fetchedAt
    }
  };
}

// src/connectors/live/github.ts
var GITHUB_CONNECTOR_ID = "github";
var GITHUB_CURSOR_KIND = "githubWatermark";
var GITHUB_DEFAULT_POLL_INTERVAL_MS = 5 * 60 * 1e3;
var GITHUB_MAX_POLL_INTERVAL_MS = 24 * 60 * 60 * 1e3;
var MAX_BODY_BYTES = 5 * 1024 * 1024;
var MAX_ITEMS_PER_PASS = 200;
var GITHUB_PAGE_SIZE = 100;
var REPO_SLUG_PATTERN = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
function validateGitHubConfig(raw) {
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
    throw new TypeError(
      `github: config must be an object, got ${raw === null ? "null" : Array.isArray(raw) ? "array" : typeof raw}`
    );
  }
  const r = raw;
  if (typeof r.token !== "string") {
    throw new TypeError(`github: token must be a string (got ${typeof r.token})`);
  }
  const token = r.token.trim();
  if (token.length === 0) {
    throw new RangeError("github: token must be non-empty");
  }
  if (typeof r.userLogin !== "string") {
    throw new TypeError(`github: userLogin must be a string (got ${typeof r.userLogin})`);
  }
  const userLogin = r.userLogin.trim();
  if (userLogin.length === 0) {
    throw new RangeError("github: userLogin must be non-empty");
  }
  let pollIntervalMs;
  if (r.pollIntervalMs === void 0) {
    pollIntervalMs = GITHUB_DEFAULT_POLL_INTERVAL_MS;
  } else if (typeof r.pollIntervalMs !== "number" || !Number.isFinite(r.pollIntervalMs)) {
    throw new TypeError(
      `github: pollIntervalMs must be a finite number (got ${JSON.stringify(r.pollIntervalMs)})`
    );
  } else if (!Number.isInteger(r.pollIntervalMs)) {
    throw new TypeError(`github: pollIntervalMs must be an integer (got ${r.pollIntervalMs})`);
  } else if (r.pollIntervalMs < 1e3) {
    throw new RangeError(`github: pollIntervalMs must be \u22651000ms; got ${r.pollIntervalMs}`);
  } else if (r.pollIntervalMs > GITHUB_MAX_POLL_INTERVAL_MS) {
    throw new RangeError(
      `github: pollIntervalMs must be \u2264${GITHUB_MAX_POLL_INTERVAL_MS} (24h); got ${r.pollIntervalMs}`
    );
  } else {
    pollIntervalMs = r.pollIntervalMs;
  }
  let repos = [];
  if (r.repos !== void 0) {
    if (!Array.isArray(r.repos)) {
      throw new TypeError(
        `github: repos must be an array of strings (got ${typeof r.repos})`
      );
    }
    const seen = /* @__PURE__ */ new Set();
    const out = [];
    for (const value of r.repos) {
      if (typeof value !== "string") {
        throw new TypeError(
          `github: repos entries must be strings; found ${typeof value}`
        );
      }
      const trimmed = value.trim();
      if (!REPO_SLUG_PATTERN.test(trimmed)) {
        throw new RangeError(
          `github: repos entry ${JSON.stringify(value)} is not a valid "owner/repo" slug`
        );
      }
      if (seen.has(trimmed)) continue;
      seen.add(trimmed);
      out.push(trimmed);
    }
    repos = Object.freeze(out);
  }
  let includeDiscussions = false;
  if (r.includeDiscussions !== void 0) {
    if (typeof r.includeDiscussions !== "boolean") {
      throw new TypeError(
        `github: includeDiscussions must be a boolean (got ${typeof r.includeDiscussions})`
      );
    }
    includeDiscussions = r.includeDiscussions;
  }
  return Object.freeze({
    token,
    userLogin,
    repos,
    pollIntervalMs,
    includeDiscussions
  });
}
function makeCursor4(payload) {
  return {
    kind: GITHUB_CURSOR_KIND,
    value: JSON.stringify(payload),
    updatedAt: (/* @__PURE__ */ new Date()).toISOString()
  };
}
function parseCursorPayload3(cursor) {
  if (cursor.kind !== GITHUB_CURSOR_KIND) {
    throw new Error(
      `github: unexpected cursor kind ${JSON.stringify(cursor.kind)}; expected ${GITHUB_CURSOR_KIND}`
    );
  }
  let parsed;
  try {
    parsed = JSON.parse(cursor.value);
  } catch {
    throw new Error(`github: cursor value is not valid JSON`);
  }
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
    throw new Error(`github: cursor value does not match GitHubCursorPayload shape`);
  }
  const p = parsed;
  const watermarks = typeof p.watermarks === "object" && p.watermarks !== null && !Array.isArray(p.watermarks) ? p.watermarks : {};
  const seenIds = typeof p.seenIds === "object" && p.seenIds !== null && !Array.isArray(p.seenIds) ? p.seenIds : {};
  return { watermarks, seenIds };
}
function watermarkKey(repo, kind) {
  return `${repo}/${kind}`;
}
function isTransientGitHubError(err) {
  if (err === null || typeof err !== "object") return false;
  const e = err;
  if (typeof e.name === "string" && e.name === "AbortError") return true;
  const status = pickNumericGitHubStatus(e);
  if (status !== void 0) {
    if (status === 429) return true;
    if (status >= 500 && status <= 599) return true;
    return false;
  }
  const codeStr = typeof e.code === "string" ? e.code : void 0;
  if (codeStr !== void 0) {
    const transientCodes = /* @__PURE__ */ new Set([
      "ECONNRESET",
      "ECONNREFUSED",
      "ECONNABORTED",
      "ETIMEDOUT",
      "ESOCKETTIMEDOUT",
      "ENOTFOUND",
      "EAI_AGAIN",
      "EPIPE",
      "EHOSTUNREACH",
      "ENETUNREACH",
      "ENETDOWN",
      "ERR_NETWORK",
      "ERR_NETWORK_CHANGED"
    ]);
    if (transientCodes.has(codeStr)) return true;
    return false;
  }
  return true;
}
function pickNumericGitHubStatus(e) {
  if (typeof e.githubStatus === "number" && Number.isFinite(e.githubStatus)) {
    return e.githubStatus;
  }
  if (typeof e.status === "number" && Number.isFinite(e.status)) {
    return e.status;
  }
  return void 0;
}
var GITHUB_API_BASE = "https://api.github.com";
function throwIfAborted4(signal) {
  if (signal?.aborted) {
    const err = new Error("github: sync aborted");
    err.name = "AbortError";
    throw err;
  }
}
function makeGitHubApiError(status, message) {
  const err = new Error(`github: HTTP ${status}: ${message}`);
  err.githubStatus = status;
  return err;
}
async function githubGet(fetchFn, token, url, signal) {
  const res = await fetchFn(url, {
    method: "GET",
    headers: {
      Authorization: `Bearer ${token}`,
      "User-Agent": "remnic-connector",
      Accept: "application/vnd.github+json",
      "X-GitHub-Api-Version": "2022-11-28"
    },
    signal
  });
  const data = await res.json();
  if (!res.ok) {
    const message = typeof data === "object" && data !== null && typeof data.message === "string" ? data.message : `HTTP ${res.status}`;
    throw makeGitHubApiError(res.status, message);
  }
  return data;
}
function createGitHubConnector(options = {}) {
  const fetchFn = options.fetchFn ?? globalThis.fetch;
  return {
    id: GITHUB_CONNECTOR_ID,
    displayName: "GitHub",
    description: "Imports issue comments, PR review comments, and discussion posts authored by the configured user from watched repos into Remnic.",
    validateConfig(raw) {
      return validateGitHubConfig(raw);
    },
    async syncIncremental(args) {
      const config = validateGitHubConfig(args.config);
      throwIfAborted4(args.abortSignal);
      if (config.repos.length === 0) {
        const emptyPayload = { watermarks: {}, seenIds: {} };
        const result = {
          newDocs: [],
          nextCursor: makeCursor4(emptyPayload),
          skippedOtherAuthor: 0,
          skippedEmpty: 0,
          skippedTooLarge: 0
        };
        return result;
      }
      const isFirstSync = args.cursor === null;
      const payload = isFirstSync ? { watermarks: {}, seenIds: {} } : parseCursorPayload3(args.cursor);
      if (isFirstSync) {
        const seededPayload = await seedWatermarks(fetchFn, config, payload, args.abortSignal);
        return {
          newDocs: [],
          nextCursor: makeCursor4(seededPayload),
          skippedOtherAuthor: 0,
          skippedEmpty: 0,
          skippedTooLarge: 0
        };
      }
      return await incrementalSync3(fetchFn, config, payload, args.abortSignal);
    }
  };
}
async function seedWatermarks(fetchFn, config, initial, signal) {
  const watermarks = { ...initial.watermarks };
  for (const repo of config.repos) {
    throwIfAborted4(signal);
    try {
      const latest = await fetchLatestTimestamp(
        fetchFn,
        config.token,
        `${GITHUB_API_BASE}/repos/${repo}/issues/comments?sort=updated&direction=desc&per_page=1`,
        "updated_at",
        signal
      );
      if (latest) watermarks[watermarkKey(repo, "issue-comment")] = latest;
    } catch (err) {
      if (isTransientGitHubError(err)) throw err;
    }
    throwIfAborted4(signal);
    try {
      const latest = await fetchLatestTimestamp(
        fetchFn,
        config.token,
        `${GITHUB_API_BASE}/repos/${repo}/pulls/comments?sort=updated&direction=desc&per_page=1`,
        "updated_at",
        signal
      );
      if (latest) watermarks[watermarkKey(repo, "pr-review-comment")] = latest;
    } catch (err) {
      if (isTransientGitHubError(err)) throw err;
    }
    if (config.includeDiscussions) {
      throwIfAborted4(signal);
      try {
        const latest = await fetchLatestTimestamp(
          fetchFn,
          config.token,
          `${GITHUB_API_BASE}/repos/${repo}/discussions?sort=updated&direction=desc&per_page=1`,
          "updated_at",
          signal
        );
        if (latest) watermarks[watermarkKey(repo, "discussion")] = latest;
      } catch (err) {
        if (isTransientGitHubError(err)) throw err;
      }
    }
  }
  return { watermarks, seenIds: {} };
}
async function fetchLatestTimestamp(fetchFn, token, url, field, signal) {
  const data = await githubGet(fetchFn, token, url, signal);
  if (!Array.isArray(data) || data.length === 0) return void 0;
  const first = data[0];
  if (typeof first !== "object" || first === null) return void 0;
  const ts = first[field];
  return typeof ts === "string" && ts.length > 0 ? ts : void 0;
}
async function incrementalSync3(fetchFn, config, payload, signal) {
  const fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
  const newDocs = [];
  const updatedWatermarks = { ...payload.watermarks };
  let skippedOtherAuthor = 0;
  let skippedEmpty = 0;
  let skippedTooLarge = 0;
  let totalConsumed = 0;
  const currentSeenIds = { ...payload.seenIds };
  const updatedSeenIds = { ...payload.seenIds };
  for (const repo of config.repos) {
    if (totalConsumed >= MAX_ITEMS_PER_PASS) break;
    throwIfAborted4(signal);
    {
      const wmKey = watermarkKey(repo, "issue-comment");
      const since = payload.watermarks[wmKey];
      try {
        const result = await fetchAndFilterComments(
          fetchFn,
          config.token,
          buildIssueCommentsUrl(repo, since),
          repo,
          "issue-comment",
          config.userLogin,
          since,
          fetchedAt,
          MAX_ITEMS_PER_PASS - totalConsumed,
          currentSeenIds,
          signal
        );
        for (const doc of result.docs) newDocs.push(doc);
        skippedOtherAuthor += result.skippedOtherAuthor;
        skippedEmpty += result.skippedEmpty;
        skippedTooLarge += result.skippedTooLarge;
        totalConsumed += result.consumed;
        if (result.latestWatermark) {
          const prevWm = updatedWatermarks[wmKey];
          const nextWm = result.latestWatermark;
          updatedWatermarks[wmKey] = nextWm;
          if (prevWm && watermarkCrossedSecond(prevWm, nextWm)) {
            for (const k of Object.keys(updatedSeenIds)) {
              if (k.startsWith(`${repo}/issue-comment/`)) delete updatedSeenIds[k];
            }
          }
          for (const [k, v] of Object.entries(result.newSeenIds)) {
            updatedSeenIds[k] = v;
          }
        }
      } catch (err) {
        if (isTransientGitHubError(err)) throw err;
      }
    }
    if (totalConsumed >= MAX_ITEMS_PER_PASS) break;
    throwIfAborted4(signal);
    {
      const wmKey = watermarkKey(repo, "pr-review-comment");
      const since = payload.watermarks[wmKey];
      try {
        const result = await fetchAndFilterComments(
          fetchFn,
          config.token,
          buildPrReviewCommentsUrl(repo, since),
          repo,
          "pr-review-comment",
          config.userLogin,
          since,
          fetchedAt,
          MAX_ITEMS_PER_PASS - totalConsumed,
          currentSeenIds,
          signal
        );
        for (const doc of result.docs) newDocs.push(doc);
        skippedOtherAuthor += result.skippedOtherAuthor;
        skippedEmpty += result.skippedEmpty;
        skippedTooLarge += result.skippedTooLarge;
        totalConsumed += result.consumed;
        if (result.latestWatermark) {
          const prevWm = updatedWatermarks[wmKey];
          const nextWm = result.latestWatermark;
          updatedWatermarks[wmKey] = nextWm;
          if (prevWm && watermarkCrossedSecond(prevWm, nextWm)) {
            for (const k of Object.keys(updatedSeenIds)) {
              if (k.startsWith(`${repo}/pr-review-comment/`)) delete updatedSeenIds[k];
            }
          }
          for (const [k, v] of Object.entries(result.newSeenIds)) {
            updatedSeenIds[k] = v;
          }
        }
      } catch (err) {
        if (isTransientGitHubError(err)) throw err;
      }
    }
    if (config.includeDiscussions && totalConsumed < MAX_ITEMS_PER_PASS) {
      throwIfAborted4(signal);
      const wmKey = watermarkKey(repo, "discussion");
      const since = payload.watermarks[wmKey];
      try {
        const result = await fetchAndFilterComments(
          fetchFn,
          config.token,
          buildDiscussionsUrl(repo, since),
          repo,
          "discussion",
          config.userLogin,
          since,
          fetchedAt,
          MAX_ITEMS_PER_PASS - totalConsumed,
          currentSeenIds,
          signal
        );
        for (const doc of result.docs) newDocs.push(doc);
        skippedOtherAuthor += result.skippedOtherAuthor;
        skippedEmpty += result.skippedEmpty;
        skippedTooLarge += result.skippedTooLarge;
        totalConsumed += result.consumed;
        if (result.latestWatermark) {
          const prevWm = updatedWatermarks[wmKey];
          const nextWm = result.latestWatermark;
          updatedWatermarks[wmKey] = nextWm;
          if (prevWm && watermarkCrossedSecond(prevWm, nextWm)) {
            for (const k of Object.keys(updatedSeenIds)) {
              if (k.startsWith(`${repo}/discussion/`)) delete updatedSeenIds[k];
            }
          }
          for (const [k, v] of Object.entries(result.newSeenIds)) {
            updatedSeenIds[k] = v;
          }
        }
      } catch (err) {
        if (isTransientGitHubError(err)) throw err;
      }
    }
  }
  return {
    newDocs,
    nextCursor: makeCursor4({ watermarks: updatedWatermarks, seenIds: updatedSeenIds }),
    skippedOtherAuthor,
    skippedEmpty,
    skippedTooLarge
  };
}
function watermarkCrossedSecond(prev, next) {
  return prev.slice(0, 19) < next.slice(0, 19);
}
function buildIssueCommentsUrl(repo, since) {
  const base = `${GITHUB_API_BASE}/repos/${repo}/issues/comments?sort=updated&direction=asc&per_page=${GITHUB_PAGE_SIZE}`;
  return since ? `${base}&since=${encodeURIComponent(since)}` : base;
}
function buildPrReviewCommentsUrl(repo, since) {
  const base = `${GITHUB_API_BASE}/repos/${repo}/pulls/comments?sort=updated&direction=asc&per_page=${GITHUB_PAGE_SIZE}`;
  return since ? `${base}&since=${encodeURIComponent(since)}` : base;
}
function buildDiscussionsUrl(repo, since) {
  const base = `${GITHUB_API_BASE}/repos/${repo}/discussions?sort=updated&direction=asc&per_page=${GITHUB_PAGE_SIZE}`;
  return since ? `${base}&since=${encodeURIComponent(since)}` : base;
}
async function fetchAndFilterComments(fetchFn, token, firstPageUrl, repo, kind, userLogin, since, fetchedAt, remainingBudget, seenIds, signal) {
  const docs = [];
  let skippedOtherAuthor = 0;
  let skippedEmpty = 0;
  let skippedTooLarge = 0;
  let consumed = 0;
  let latestWatermark = void 0;
  const newSeenIds = {};
  let nextUrl = firstPageUrl;
  while (nextUrl && consumed < remainingBudget) {
    throwIfAborted4(signal);
    const data = await githubGet(fetchFn, token, nextUrl, signal);
    if (!Array.isArray(data)) break;
    for (const item of data) {
      if (consumed >= remainingBudget) break;
      throwIfAborted4(signal);
      const comment = item;
      if (since && comment.updated_at < since) {
        continue;
      }
      const seenKey = `${repo}/${kind}/${comment.id}`;
      if (seenIds[seenKey] === comment.updated_at) {
        continue;
      }
      const authorLogin = comment.user?.login ?? null;
      if (authorLogin !== userLogin) {
        skippedOtherAuthor++;
        if (!latestWatermark || comment.updated_at > latestWatermark) {
          latestWatermark = comment.updated_at;
        }
        continue;
      }
      const body = comment.body ?? "";
      const trimmed = body.trim();
      if (trimmed.length === 0) {
        skippedEmpty++;
        if (!latestWatermark || comment.updated_at > latestWatermark) {
          latestWatermark = comment.updated_at;
        }
        continue;
      }
      if (trimmed.length > MAX_BODY_BYTES) {
        skippedTooLarge++;
        if (!latestWatermark || comment.updated_at > latestWatermark) {
          latestWatermark = comment.updated_at;
        }
        continue;
      }
      consumed++;
      const doc = buildDocument(comment, repo, kind, fetchedAt);
      docs.push(doc);
      if (!latestWatermark || comment.updated_at > latestWatermark) {
        latestWatermark = comment.updated_at;
      }
      newSeenIds[seenKey] = comment.updated_at;
    }
    if (data.length < GITHUB_PAGE_SIZE) {
      nextUrl = void 0;
    } else {
      nextUrl = advancePageUrl(nextUrl);
    }
  }
  return { docs, skippedOtherAuthor, skippedEmpty, skippedTooLarge, consumed, latestWatermark, newSeenIds };
}
function advancePageUrl(url) {
  try {
    const u = new URL(url);
    const page = parseInt(u.searchParams.get("page") ?? "1", 10);
    u.searchParams.set("page", String(isNaN(page) ? 2 : page + 1));
    return u.toString();
  } catch {
    return "";
  }
}
function buildDocument(comment, repo, kind, fetchedAt) {
  const externalId = `${repo}/${kind}/${comment.id}`;
  const externalUrl = typeof comment.html_url === "string" && comment.html_url.length > 0 ? comment.html_url : void 0;
  const title = buildTitle(repo, kind, comment);
  return {
    id: externalId,
    title,
    content: (comment.body ?? "").trim(),
    source: {
      connector: GITHUB_CONNECTOR_ID,
      externalId,
      externalRevision: comment.updated_at,
      externalUrl,
      fetchedAt
    }
  };
}
function buildTitle(repo, kind, comment) {
  const kindLabel = kind === "issue-comment" ? "Issue comment" : kind === "pr-review-comment" ? "PR review comment" : "Discussion comment";
  return `${kindLabel} in ${repo} (#${comment.id})`;
}

export {
  LiveConnectorRegistryError,
  LiveConnectorRegistry,
  GOOGLE_DRIVE_CONNECTOR_ID,
  GOOGLE_DRIVE_CURSOR_KIND,
  DEFAULT_POLL_INTERVAL_MS,
  validateGoogleDriveConfig,
  createGoogleDriveConnector,
  defaultGoogleDriveClientFactory,
  NOTION_CONNECTOR_ID,
  NOTION_CURSOR_KIND,
  NOTION_DEFAULT_POLL_INTERVAL_MS,
  validateNotionConfig,
  createNotionConnector,
  GMAIL_CONNECTOR_ID,
  GMAIL_DEFAULT_POLL_INTERVAL_MS,
  validateGmailConfig,
  createGmailConnector,
  GITHUB_CONNECTOR_ID,
  GITHUB_DEFAULT_POLL_INTERVAL_MS,
  validateGitHubConfig,
  createGitHubConnector
};
//# sourceMappingURL=chunk-4IS4SXIQ.js.map