@clivly/sdk 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,3 +1,87 @@
1
+ import { t as DRIZZLE_META } from "./drizzle-meta-CAJ43cLc.mjs";
2
+ //#region src/doctor.ts
3
+ function configuredSources(config) {
4
+ const sources = [];
5
+ if (config.source?.contacts) sources.push(config.source.contacts);
6
+ if (config.source?.companies) sources.push(config.source.companies);
7
+ return sources;
8
+ }
9
+ /**
10
+ * Run the six read-only setup checks and return a structured report. Every check
11
+ * captures its own error into `detail` rather than throwing, so one failure never
12
+ * hides the rest. `ok` is the AND of all non-skipped checks.
13
+ */
14
+ async function runDoctor(args) {
15
+ const { config, connect } = args;
16
+ const checks = [];
17
+ const sources = configuredSources(config);
18
+ const hasKey = Boolean(config.apiKey);
19
+ checks.push({
20
+ name: "API key present",
21
+ ok: hasKey,
22
+ detail: hasKey ? "CLIVLY_API_KEY is set." : "apiKey is empty — set CLIVLY_API_KEY."
23
+ });
24
+ const entityCount = config.entities ? Object.keys(config.entities.entities).length : 0;
25
+ checks.push({
26
+ name: "Entities configured",
27
+ ok: entityCount > 0,
28
+ detail: entityCount > 0 ? `${entityCount} entit${entityCount === 1 ? "y" : "ies"} configured.` : "No `entities` config — pass defineClivlyConfig(...) as `entities`."
29
+ });
30
+ checks.push({
31
+ name: "Sources configured",
32
+ ok: sources.length > 0,
33
+ detail: sources.length > 0 ? `${sources.length} source(s) to mirror.` : "No `source.contacts` — nothing to sync."
34
+ });
35
+ for (const source of sources) {
36
+ const meta = source[DRIZZLE_META];
37
+ if (!meta) {
38
+ checks.push({
39
+ name: `Cursor/id columns exist (${source.entity})`,
40
+ ok: false,
41
+ skipped: true,
42
+ detail: `"${source.entity}" is a hand-written source — column check skipped.`
43
+ });
44
+ continue;
45
+ }
46
+ const idField = source.idField ?? "id";
47
+ const missing = [meta.cursorField, idField].filter((field) => !meta.columnKeys.includes(field));
48
+ checks.push({
49
+ name: `Cursor/id columns exist (${source.entity})`,
50
+ ok: missing.length === 0,
51
+ detail: missing.length === 0 ? `"${source.entity}": ${meta.cursorField} + ${idField} present.` : `"${source.entity}" missing column(s): ${missing.join(", ")}.`
52
+ });
53
+ }
54
+ for (const source of sources) try {
55
+ const rows = await source.fetchPage({
56
+ since: null,
57
+ limit: 1
58
+ });
59
+ const firstRow = rows[0];
60
+ const missingCursor = firstRow !== void 0 && !(source.cursorField in firstRow);
61
+ checks.push({
62
+ name: `Sample fetch (${source.entity})`,
63
+ ok: !missingCursor,
64
+ detail: missingCursor ? `"${source.entity}" rows lack the cursor key "${source.cursorField}".` : `"${source.entity}" returned ${rows.length} row(s).`
65
+ });
66
+ } catch (error) {
67
+ checks.push({
68
+ name: `Sample fetch (${source.entity})`,
69
+ ok: false,
70
+ detail: `"${source.entity}" fetchPage threw: ${error.message}`
71
+ });
72
+ }
73
+ const result = await connect();
74
+ checks.push({
75
+ name: "Heartbeat",
76
+ ok: result.ok,
77
+ detail: result.ok ? `Reached Clivly — org "${result.org?.name ?? "(unnamed)"}".` : `Could not connect (${result.reason}).`
78
+ });
79
+ return {
80
+ ok: checks.every((check) => check.skipped || check.ok),
81
+ checks
82
+ };
83
+ }
84
+ //#endregion
1
85
  //#region src/sync.ts
2
86
  const DEFAULT_ID_FIELD = "id";
3
87
  function isAfter(candidate, current) {
@@ -86,6 +170,42 @@ async function fullSyncSource(args) {
86
170
  };
87
171
  }
88
172
  //#endregion
173
+ //#region src/errors.ts
174
+ var ClivlyError = class ClivlyError extends Error {
175
+ /** HTTP status, or null when the request never completed (network error). */
176
+ status;
177
+ reason;
178
+ /** Whether a retry could plausibly succeed (network, 429, 5xx). */
179
+ retryable;
180
+ constructor(result, message) {
181
+ super(message ?? `Clivly connection failed: ${result.reason}`);
182
+ this.name = new.target.name;
183
+ this.status = result.status;
184
+ this.reason = result.reason;
185
+ this.retryable = result.retryable;
186
+ }
187
+ /**
188
+ * Build the error subclass matching a failed result's reason, or `null` when
189
+ * the result is `ok`.
190
+ */
191
+ static from(result) {
192
+ if (result.ok) return null;
193
+ switch (result.reason) {
194
+ case "invalid_api_key":
195
+ case "forbidden": return new ClivlyAuthError(result);
196
+ case "rate_limited": return new ClivlyRateLimitError(result);
197
+ case "network_error": return new ClivlyNetworkError(result);
198
+ default: return new ClivlyError(result);
199
+ }
200
+ }
201
+ };
202
+ /** The API key was rejected (401) or lacks access (403). Not retryable. */
203
+ var ClivlyAuthError = class extends ClivlyError {};
204
+ /** Rate limited (429). Retryable after backoff / `Retry-After`. */
205
+ var ClivlyRateLimitError = class extends ClivlyError {};
206
+ /** The request never reached the server (network/DNS/TLS). Retryable. */
207
+ var ClivlyNetworkError = class extends ClivlyError {};
208
+ //#endregion
89
209
  //#region src/index.ts
90
210
  const DEFAULT_API_URL = "https://api.clivly.com";
91
211
  const DEFAULT_HEARTBEAT_MS = 6e4;
@@ -125,6 +245,22 @@ function buildSchema(config) {
125
245
  }));
126
246
  }
127
247
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
248
+ function createMemoryCursorStore() {
249
+ const cursors = /* @__PURE__ */ new Map();
250
+ return {
251
+ get: (entity) => cursors.get(entity) ?? null,
252
+ set: (entity, cursor) => {
253
+ cursors.set(entity, cursor);
254
+ }
255
+ };
256
+ }
257
+ function isLikelyServerless() {
258
+ const runtime = globalThis;
259
+ if (runtime.navigator?.userAgent === "Cloudflare-Workers") return true;
260
+ if (runtime.EdgeRuntime !== void 0) return true;
261
+ const env = runtime.process?.env;
262
+ return Boolean(env?.AWS_LAMBDA_FUNCTION_NAME || env?.VERCEL);
263
+ }
128
264
  function jsonResponse(body, status) {
129
265
  return new Response(JSON.stringify(body), {
130
266
  status,
@@ -145,6 +281,25 @@ function bearerToken(req) {
145
281
  function isRetryableStatus(status) {
146
282
  return status === RATE_LIMITED_STATUS || status >= RETRYABLE_STATUS_FLOOR;
147
283
  }
284
+ const UNAUTHORIZED_STATUS = 401;
285
+ const FORBIDDEN_STATUS = 403;
286
+ const CLIENT_ERROR_FLOOR = 400;
287
+ const SERVICE_UNAVAILABLE_STATUS = 503;
288
+ function connectReason(status) {
289
+ if (status === UNAUTHORIZED_STATUS) return "invalid_api_key";
290
+ if (status === FORBIDDEN_STATUS) return "forbidden";
291
+ if (status === RATE_LIMITED_STATUS) return "rate_limited";
292
+ if (status >= RETRYABLE_STATUS_FLOOR) return "server_error";
293
+ if (status >= CLIENT_ERROR_FLOOR) return "client_error";
294
+ return "server_error";
295
+ }
296
+ async function resolveOrg(response) {
297
+ try {
298
+ return (await response.json())?.org;
299
+ } catch {
300
+ return;
301
+ }
302
+ }
148
303
  function retryAfterMs(res) {
149
304
  const header = res.headers.get("retry-after");
150
305
  if (!header) return null;
@@ -185,7 +340,7 @@ function createClivlySDK(config) {
185
340
  sync: null
186
341
  };
187
342
  const batchSize = config.sync?.batchSize ?? DEFAULT_BATCH_SIZE;
188
- const cursors = /* @__PURE__ */ new Map();
343
+ const cursorStore = config.cursorStore ?? createMemoryCursorStore();
189
344
  const fetcher = createFetcher(config.retry);
190
345
  const push = async (path, body) => {
191
346
  return (await fetcher(`${apiUrl}${path}`, {
@@ -198,6 +353,7 @@ function createClivlySDK(config) {
198
353
  }))?.ok ?? false;
199
354
  };
200
355
  const syncOne = async (source, path, mode) => {
356
+ const since = mode === "full" ? null : await cursorStore.get(source.entity) ?? null;
201
357
  const result = mode === "full" ? await fullSyncSource({
202
358
  source,
203
359
  path,
@@ -206,11 +362,11 @@ function createClivlySDK(config) {
206
362
  }) : await syncSource({
207
363
  source,
208
364
  path,
209
- since: cursors.get(source.entity) ?? null,
365
+ since,
210
366
  batchSize,
211
367
  push
212
368
  });
213
- cursors.set(source.entity, result.cursor);
369
+ await cursorStore.set(source.entity, result.cursor);
214
370
  return result.ok;
215
371
  };
216
372
  const runSync = async (options) => {
@@ -223,24 +379,67 @@ function createClivlySDK(config) {
223
379
  if (!await syncOne(config.source.contacts, SYNC_CONTACTS_PATH, mode)) failed.push(config.source.contacts.entity);
224
380
  if (failed.length > 0) throw new Error(`Clivly sync: push failed for ${failed.join(", ")}`);
225
381
  };
382
+ const sendHeartbeat = () => fetcher(`${apiUrl}/clivly/sdk/heartbeat`, {
383
+ method: "POST",
384
+ headers: {
385
+ "content-type": "application/json",
386
+ authorization: `Bearer ${config.apiKey}`
387
+ },
388
+ body: JSON.stringify({
389
+ framework: config.framework,
390
+ orm: config.orm,
391
+ sdkVersion: config.sdkVersion,
392
+ contactEntity: resolveContactEntity(config),
393
+ namespaceReady: config.namespaceReady ?? false,
394
+ schema: buildSchema(config)
395
+ })
396
+ });
226
397
  const heartbeat = async () => {
227
- return (await fetcher(`${apiUrl}/clivly/sdk/heartbeat`, {
228
- method: "POST",
229
- headers: {
230
- "content-type": "application/json",
231
- authorization: `Bearer ${config.apiKey}`
232
- },
233
- body: JSON.stringify({
234
- framework: config.framework,
235
- orm: config.orm,
236
- sdkVersion: config.sdkVersion,
237
- contactEntity: resolveContactEntity(config),
238
- namespaceReady: config.namespaceReady ?? false,
239
- schema: buildSchema(config)
240
- })
241
- }))?.ok ?? false;
398
+ return (await sendHeartbeat())?.ok ?? false;
399
+ };
400
+ const connect = async () => {
401
+ const response = await sendHeartbeat();
402
+ if (response === null) return {
403
+ ok: false,
404
+ status: null,
405
+ reason: "network_error",
406
+ retryable: true
407
+ };
408
+ if (response.ok) return {
409
+ ok: true,
410
+ status: response.status,
411
+ reason: "ok",
412
+ retryable: false,
413
+ org: await resolveOrg(response)
414
+ };
415
+ return {
416
+ ok: false,
417
+ status: response.status,
418
+ reason: connectReason(response.status),
419
+ retryable: isRetryableStatus(response.status)
420
+ };
421
+ };
422
+ const doctor = () => runDoctor({
423
+ config,
424
+ connect
425
+ });
426
+ const runScheduled = async (options) => {
427
+ await heartbeat();
428
+ if (config.source) await runSync(options);
429
+ };
430
+ const createClivlyHandler = () => async (_req) => {
431
+ try {
432
+ await runScheduled();
433
+ return jsonResponse({ ok: true }, 200);
434
+ } catch (error) {
435
+ return jsonResponse({
436
+ ok: false,
437
+ error: error instanceof Error ? error.message : String(error)
438
+ }, 500);
439
+ }
242
440
  };
243
441
  const start = async () => {
442
+ if (isLikelyServerless()) console.warn("clivly.start() uses a setInterval keep-alive loop, which does not persist on serverless/edge runtimes — it will stop ticking after the first response. Call clivly.runScheduled() from a scheduled/cron trigger, or mount clivly.createClivlyHandler() at a route instead.");
244
443
  await heartbeat();
245
444
  if (config.sync?.onBoot !== false) await runSync().catch(() => {});
246
445
  if (!timers.heartbeat) {
@@ -267,7 +466,7 @@ function createClivlySDK(config) {
267
466
  }
268
467
  };
269
468
  const createAuthVerifyHandler = (resolveUser) => async (req) => {
270
- if (!config.verifyToken) return jsonResponse({ error: "verify token not configured" }, 500);
469
+ if (!config.verifyToken) return jsonResponse({ error: "Clivly verifyToken is not configured on the host app" }, SERVICE_UNAVAILABLE_STATUS);
271
470
  const token = bearerToken(req);
272
471
  if (!(token && timingSafeEqual(token, config.verifyToken))) return jsonResponse({ error: "unauthorized" }, 401);
273
472
  const user = await resolveUser(req);
@@ -278,9 +477,13 @@ function createClivlySDK(config) {
278
477
  start,
279
478
  stop,
280
479
  heartbeat,
480
+ connect,
481
+ doctor,
482
+ runScheduled,
483
+ createClivlyHandler,
281
484
  sync: runSync,
282
485
  createAuthVerifyHandler
283
486
  };
284
487
  }
285
488
  //#endregion
286
- export { createClivlySDK };
489
+ export { ClivlyAuthError, ClivlyError, ClivlyNetworkError, ClivlyRateLimitError, createClivlySDK };
@@ -0,0 +1,224 @@
1
+ import { ClivlyEntitiesConfig } from "@clivly/core";
2
+
3
+ //#region src/drizzle-meta.d.ts
4
+ declare const DRIZZLE_META: unique symbol;
5
+ interface DrizzleSourceMeta {
6
+ /** Drizzle property keys of every column on the source table. */
7
+ columnKeys: string[];
8
+ cursorField: string;
9
+ idField: string;
10
+ }
11
+ //#endregion
12
+ //#region src/types.d.ts
13
+ interface DiscoveredTable {
14
+ columns: string[];
15
+ name: string;
16
+ }
17
+ interface ClivlySDKConfig {
18
+ /** Org-scoped API token (sk_live_…) from Settings → API keys. */
19
+ apiKey: string;
20
+ /** Clivly API base URL. Defaults to https://api.clivly.com. */
21
+ apiUrl?: string;
22
+ /**
23
+ * The host table Clivly treats as the contact entity.
24
+ * @deprecated Prefer `entities` — the same mapping-derived shape
25
+ * `crm_entity_mappings` (and `@clivly/core`'s `mappingsToEntitiesConfig`)
26
+ * produce, so the SDK, the mappings table, and the sync engine share one
27
+ * model. A bare `contactEntity` is treated as a single contact entity.
28
+ */
29
+ contactEntity?: string;
30
+ /** Which columns of the contact entity Clivly may read. */
31
+ contactFields?: string[];
32
+ /**
33
+ * Where per-entity delta cursors are persisted between syncs. Defaults to an
34
+ * in-memory store (fine for a long-lived process). On serverless/edge, pass a
35
+ * store backed by your own KV/DB so a cold start resumes delta sync instead of
36
+ * re-paging the whole entity. See `CursorStore`.
37
+ */
38
+ cursorStore?: CursorStore;
39
+ /**
40
+ * Declarative entity config — the single source of truth shared with
41
+ * `crm_entity_mappings`. When set, discovery and heartbeat are derived from
42
+ * it (each entity's `source` table + mapped columns), superseding the flat
43
+ * `contactEntity`.
44
+ */
45
+ entities?: ClivlyEntitiesConfig;
46
+ /**
47
+ * 🚧 Not implemented in 0.1.x. Reserved options for tunnel mode and CRM
48
+ * write-back — they have NO effect yet and land with the Cloudflare
49
+ * infrastructure. Grouped under `experimental` so the stable config surface
50
+ * above is unambiguous; nothing here is part of the supported API.
51
+ */
52
+ experimental?: {
53
+ /** Cloudflare Tunnel token — reserved for tunnel mode. Not wired up yet. */tunnelToken?: string; /** CRM write-back target — reserved. Not wired up yet. */
54
+ writeBack?: {
55
+ enabled?: boolean; /** Only crm_* tables are ever accepted; this narrows further. */
56
+ tables?: string[];
57
+ };
58
+ };
59
+ /** Reported to the portal so it can show the detected stack. */
60
+ framework?: string;
61
+ /** Heartbeat cadence in ms. Defaults to 60s. */
62
+ heartbeatIntervalMs?: number;
63
+ /** Whether the crm_* namespace exists in the host DB (self-reported). */
64
+ namespaceReady?: boolean;
65
+ orm?: string;
66
+ /**
67
+ * Network retry policy for outbound calls (heartbeat + sync push). Transient
68
+ * failures (network errors, HTTP 429, HTTP 5xx) are retried with exponential
69
+ * backoff + jitter; a `Retry-After` header on a 429 is honoured. Non-transient
70
+ * responses (e.g. 4xx other than 429) are returned immediately, not retried.
71
+ */
72
+ retry?: {
73
+ /** Total attempts including the first. Defaults to 3. Min 1. */maxAttempts?: number; /** Base backoff in ms (doubled each attempt). Defaults to 500. */
74
+ baseDelayMs?: number;
75
+ };
76
+ /** Tables/columns to report for entity discovery + mapping. */
77
+ schema?: DiscoveredTable[];
78
+ sdkVersion?: string;
79
+ /** Host entities to mirror into Clivly Cloud. */
80
+ source?: {
81
+ contacts: SyncSource;
82
+ companies?: SyncSource;
83
+ };
84
+ sync?: {
85
+ onBoot?: boolean;
86
+ /**
87
+ * Sync cadence in ms for the background keep-alive loop. Defaults to
88
+ * `heartbeatIntervalMs` (60s) so sync and heartbeat share a tick unless you
89
+ * decouple them — e.g. a slow full-mirror alongside frequent heartbeats.
90
+ */
91
+ intervalMs?: number;
92
+ batchSize?: number;
93
+ };
94
+ /**
95
+ * Shared secret Clivly Cloud presents (as `Authorization: Bearer <token>`) when
96
+ * calling the `/clivly/auth/verify` endpoint. Required for the verify handler to
97
+ * authenticate the caller; without it the handler fails closed (HTTP 503).
98
+ */
99
+ verifyToken?: string;
100
+ }
101
+ interface ClivlyUser {
102
+ email: string;
103
+ id: string;
104
+ name?: string;
105
+ }
106
+ interface DoctorCheck {
107
+ /** Human-readable outcome — what passed, or why it failed. */
108
+ detail: string;
109
+ /** Short label, e.g. "API key present". */
110
+ name: string;
111
+ /** True when the check passed. Ignored when `skipped` is true. */
112
+ ok: boolean;
113
+ /** True when the check could not run (e.g. a hand-written source). */
114
+ skipped?: boolean;
115
+ }
116
+ interface DoctorReport {
117
+ checks: DoctorCheck[];
118
+ /** AND of every non-skipped check's `ok`. */
119
+ ok: boolean;
120
+ }
121
+ type ClivlyConnectReason = "ok" | "invalid_api_key" | "forbidden" | "rate_limited" | "server_error" | "client_error" | "network_error";
122
+ interface ClivlyConnectResult {
123
+ ok: boolean;
124
+ /** Org resolved from a successful heartbeat, when the server returns it. */
125
+ org?: {
126
+ id?: string;
127
+ name?: string;
128
+ };
129
+ reason: ClivlyConnectReason;
130
+ /** Whether a retry could plausibly succeed (network, 429, 5xx). */
131
+ retryable: boolean;
132
+ /** HTTP status, or null when the request never completed (network error). */
133
+ status: number | null;
134
+ }
135
+ interface SyncCursor {
136
+ /** The row's id, breaking ties when timestamps collide. */
137
+ id: string;
138
+ /** The row's cursor-field value (typically `updated_at`). */
139
+ time: Date;
140
+ }
141
+ interface SyncSource {
142
+ cursorField: string;
143
+ entity: string;
144
+ fetchPage(args: {
145
+ since: SyncCursor | null;
146
+ limit: number;
147
+ }): Promise<Record<string, unknown>[]>;
148
+ /** Stable, unique tie-breaker column for keyset paging. Defaults to "id". */
149
+ idField?: string;
150
+ /** Set by `fromDrizzle`; lets `doctor` introspect the backing table. */
151
+ [DRIZZLE_META]?: DrizzleSourceMeta;
152
+ }
153
+ type SyncMode = "delta" | "full";
154
+ interface CursorStore {
155
+ get(entity: string): Promise<SyncCursor | null> | SyncCursor | null;
156
+ set(entity: string, cursor: SyncCursor | null): Promise<void> | void;
157
+ }
158
+ interface ClivlySDK {
159
+ /**
160
+ * Send a single heartbeat and report the outcome richly: `ok`, the HTTP
161
+ * `status` (null on network failure), a coarse `reason`, whether it's
162
+ * `retryable`, and the resolved `org` when the server returns it. Never throws
163
+ * — inspect the result, or turn a failure into an exception with
164
+ * `ClivlyError.from(result)`. Prefer this over `heartbeat()` for onboarding,
165
+ * health checks, and one-shot serverless calls.
166
+ */
167
+ connect(): Promise<ClivlyConnectResult>;
168
+ /**
169
+ * Build the `/clivly/auth/verify` handler the developer mounts in their app.
170
+ * Clivly's cloud calls it to validate a CRM user's identity against the host
171
+ * app's session. Pass a resolver that reads the host session from the request.
172
+ *
173
+ * The handler authenticates the caller against `config.verifyToken` (presented
174
+ * as `Authorization: Bearer <token>`) before invoking the resolver, so the
175
+ * endpoint can't be used by third parties to probe host sessions. It fails
176
+ * closed with HTTP 503 if `verifyToken` is unset (a missing config value, not a
177
+ * server fault — so it won't trip the host's 5xx error alerting).
178
+ */
179
+ createAuthVerifyHandler(resolveUser: (req: Request) => Promise<ClivlyUser | null> | ClivlyUser | null): (req: Request) => Promise<Response>;
180
+ /**
181
+ * A fetch handler to mount at a route and drive from an external scheduler
182
+ * (Cloudflare Workers cron, an uptime ping, etc.). Each request runs one
183
+ * `runScheduled()` and returns JSON: 200 when the sync succeeded, 500 otherwise.
184
+ * The ergonomic serverless entry point alongside `runScheduled()`.
185
+ */
186
+ createClivlyHandler(): (req: Request) => Promise<Response>;
187
+ /** Validate this setup end-to-end (read-only) before the first sync. */
188
+ doctor(): Promise<DoctorReport>;
189
+ /**
190
+ * Send a single heartbeat now. Returns true on success. Kept for the keep-alive
191
+ * loop and back-compat; use `connect()` when you need the failure reason.
192
+ */
193
+ heartbeat(): Promise<boolean>;
194
+ /**
195
+ * One-shot heartbeat + a single sync, with no keep-alive timers. The correct
196
+ * entry point on serverless/edge (Workers, Vercel, Lambda), where `start()`'s
197
+ * `setInterval` loop can't persist between invocations — call this from a
198
+ * scheduled/cron trigger. Throws if a sync push fails (so the run is marked
199
+ * failed); heartbeat is best-effort.
200
+ */
201
+ runScheduled(options?: {
202
+ mode?: SyncMode;
203
+ }): Promise<void>;
204
+ /**
205
+ * Send the initial heartbeat and start the keep-alive loop. For long-lived
206
+ * hosts only — on serverless/edge the interval can't persist, so prefer
207
+ * `runScheduled()` / `createClivlyHandler()`. Emits a warning if a serverless
208
+ * runtime is detected.
209
+ */
210
+ start(): Promise<void>;
211
+ /** Stop the keep-alive loop. */
212
+ stop(): void;
213
+ /**
214
+ * Sync all configured sources now. `delta` (default) pages changed rows since
215
+ * the last cursor; `full` pushes each complete entity so the server can
216
+ * reconcile leavers (archive-on-leave). Throws if a push fails, so callers see
217
+ * the failure — the background keep-alive loop swallows and retries instead.
218
+ */
219
+ sync(options?: {
220
+ mode?: SyncMode;
221
+ }): Promise<void>;
222
+ }
223
+ //#endregion
224
+ export { ClivlyUser as a, DoctorCheck as c, ClivlySDKConfig as i, DoctorReport as l, ClivlyConnectResult as n, CursorStore as o, ClivlySDK as r, DiscoveredTable as s, ClivlyConnectReason as t, SyncSource as u };