@clivly/sdk 0.2.0 → 0.3.0-next.1

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.cjs CHANGED
@@ -1,14 +1,97 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_drizzle_meta = require("./drizzle-meta-C0T6F3vL.cjs");
2
+ const require_drizzle_meta = require("./drizzle-meta-iAu8w1hj.cjs");
3
+ //#region src/namespace-probe.ts
4
+ /**
5
+ * Verify the tables the integration depends on are genuinely reachable:
6
+ * connect, list tables, confirm each required table exists, then prove it's
7
+ * selectable with a sample `SELECT ... LIMIT 1`. Column-shape validation is
8
+ * not this probe's job — `doctor()`'s "Cursor/id columns exist" check
9
+ * validates columns using the correct Drizzle metadata. Never throws —
10
+ * connection/permission problems become `error.kind: "unreachable"`, missing
11
+ * tables become `"schema"`.
12
+ */
13
+ async function probeNamespace(introspector, requiredTables) {
14
+ let present;
15
+ try {
16
+ present = new Set(await introspector.listTables());
17
+ } catch (error) {
18
+ return {
19
+ reachable: false,
20
+ missingTables: requiredTables,
21
+ error: {
22
+ kind: "unreachable",
23
+ message: errorMessage(error)
24
+ }
25
+ };
26
+ }
27
+ const missingTables = requiredTables.filter((t) => !present.has(t));
28
+ if (missingTables.length > 0) return {
29
+ reachable: false,
30
+ missingTables,
31
+ error: {
32
+ kind: "schema",
33
+ message: `missing table(s): ${missingTables.join(", ")}`
34
+ }
35
+ };
36
+ for (const table of requiredTables) try {
37
+ await introspector.sampleSelect(table);
38
+ } catch (error) {
39
+ return {
40
+ reachable: false,
41
+ missingTables: [],
42
+ error: {
43
+ kind: "unreachable",
44
+ message: errorMessage(error)
45
+ }
46
+ };
47
+ }
48
+ return {
49
+ reachable: true,
50
+ missingTables: []
51
+ };
52
+ }
53
+ /** The distinct source tables the integration reads, derived from the entity config. */
54
+ function requiredTablesFromConfig(config) {
55
+ const tables = /* @__PURE__ */ new Set();
56
+ const sources = [
57
+ config.source?.contacts,
58
+ config.source?.companies,
59
+ config.source?.deals,
60
+ ...config.source?.custom ?? []
61
+ ].filter((source) => Boolean(source));
62
+ const tableNameByEntity = new Map(sources.map((source) => [source.entity, source[require_drizzle_meta.DRIZZLE_META]?.tableName]));
63
+ for (const entity of Object.values(config.entities?.entities ?? {})) tables.add(tableNameByEntity.get(entity.source) ?? entity.source);
64
+ return [...tables];
65
+ }
66
+ function errorMessage(error) {
67
+ return error instanceof Error ? error.message : String(error);
68
+ }
69
+ //#endregion
3
70
  //#region src/doctor.ts
4
71
  function configuredSources(config) {
5
72
  const sources = [];
6
73
  if (config.source?.contacts) sources.push(config.source.contacts);
7
74
  if (config.source?.companies) sources.push(config.source.companies);
75
+ if (config.source?.deals) sources.push(config.source.deals);
76
+ sources.push(...config.source?.custom ?? []);
8
77
  return sources;
9
78
  }
79
+ async function namespaceCheck(config) {
80
+ if (!config.introspector) return {
81
+ name: "Namespace reachable",
82
+ ok: false,
83
+ skipped: true,
84
+ detail: "No introspector configured — set `introspector` in clivly.config to probe the namespace."
85
+ };
86
+ const probeResult = await probeNamespace(config.introspector, requiredTablesFromConfig(config));
87
+ return {
88
+ name: "Namespace reachable",
89
+ ok: probeResult.reachable,
90
+ detail: probeResult.reachable ? "All required source tables are reachable." : probeResult.error?.message ?? "namespace unreachable"
91
+ };
92
+ }
10
93
  /**
11
- * Run the six read-only setup checks and return a structured report. Every check
94
+ * Run the read-only setup checks and return a structured report. Every check
12
95
  * captures its own error into `detail` rather than throwing, so one failure never
13
96
  * hides the rest. `ok` is the AND of all non-skipped checks.
14
97
  */
@@ -77,6 +160,7 @@ async function runDoctor(args) {
77
160
  ok: result.ok,
78
161
  detail: result.ok ? `Reached Clivly — org "${result.org?.name ?? "(unnamed)"}".` : `Could not connect (${result.reason}).`
79
162
  });
163
+ checks.push(await namespaceCheck(config));
80
164
  return {
81
165
  ok: checks.every((check) => check.skipped || check.ok),
82
166
  checks
@@ -108,7 +192,7 @@ function nextCursor(rows, cursorField, idField, current) {
108
192
  return max;
109
193
  }
110
194
  async function syncSource(args) {
111
- const { source, path, since, batchSize, push } = args;
195
+ const { source, path, since, batchSize, push, objectType, maxRows } = args;
112
196
  const idField = source.idField ?? DEFAULT_ID_FIELD;
113
197
  let cursor = since;
114
198
  let pushed = 0;
@@ -118,18 +202,20 @@ async function syncSource(args) {
118
202
  limit: batchSize
119
203
  });
120
204
  if (rows.length === 0) break;
205
+ const capped = maxRows === void 0 ? rows : rows.slice(0, maxRows);
121
206
  if (!await push(path, {
122
207
  source: source.entity,
123
- rows,
124
- mode: "delta"
208
+ rows: capped,
209
+ mode: "delta",
210
+ ...objectType ? { objectType } : {}
125
211
  })) return {
126
212
  pushed,
127
213
  cursor,
128
214
  ok: false
129
215
  };
130
- pushed += rows.length;
131
- cursor = nextCursor(rows, source.cursorField, idField, cursor);
132
- if (rows.length < batchSize) break;
216
+ pushed += capped.length;
217
+ cursor = nextCursor(capped, source.cursorField, idField, cursor);
218
+ if (maxRows !== void 0 || rows.length < batchSize) break;
133
219
  }
134
220
  return {
135
221
  pushed,
@@ -138,7 +224,7 @@ async function syncSource(args) {
138
224
  };
139
225
  }
140
226
  async function fullSyncSource(args) {
141
- const { source, path, batchSize, push } = args;
227
+ const { source, path, batchSize, push, objectType } = args;
142
228
  const idField = source.idField ?? DEFAULT_ID_FIELD;
143
229
  const all = [];
144
230
  let cursor = null;
@@ -162,7 +248,8 @@ async function fullSyncSource(args) {
162
248
  const ok = await push(path, {
163
249
  source: source.entity,
164
250
  rows: all,
165
- mode: "full"
251
+ mode: "full",
252
+ ...objectType ? { objectType } : {}
166
253
  });
167
254
  return {
168
255
  pushed: ok ? all.length : 0,
@@ -211,12 +298,15 @@ var ClivlyNetworkError = class extends ClivlyError {};
211
298
  const DEFAULT_API_URL = "https://api.clivly.com";
212
299
  const DEFAULT_HEARTBEAT_MS = 6e4;
213
300
  const DEFAULT_BATCH_SIZE = 500;
301
+ const SAMPLE_BATCH_SIZE = 1;
214
302
  const DEFAULT_MAX_ATTEMPTS = 3;
215
303
  const DEFAULT_BASE_DELAY_MS = 500;
216
304
  const RETRYABLE_STATUS_FLOOR = 500;
217
305
  const RATE_LIMITED_STATUS = 429;
218
306
  const SYNC_CONTACTS_PATH = "/clivly/sdk/sync/contacts";
219
307
  const SYNC_COMPANIES_PATH = "/clivly/sdk/sync/companies";
308
+ const SYNC_DEALS_PATH = "/clivly/sdk/sync/deals";
309
+ const SYNC_CUSTOM_PATH = "/clivly/sdk/sync/custom";
220
310
  const TRAILING_SLASH = /\/$/;
221
311
  const BEARER_PREFIX = /^Bearer\s+/i;
222
312
  function entitiesToTables(entities, byName) {
@@ -231,6 +321,17 @@ function resolveContactEntity(config) {
231
321
  if (config.contactEntity) return config.contactEntity;
232
322
  for (const entity of Object.values(config.entities?.entities ?? {})) if (entity.concept === "contact") return entity.source;
233
323
  }
324
+ function collectSchemaMeta(config) {
325
+ const byTable = /* @__PURE__ */ new Map();
326
+ for (const table of config.schema ?? []) if (table.columnsMeta) byTable.set(table.name, table.columnsMeta);
327
+ const sources = [];
328
+ if (config.source) sources.push(config.source.contacts, config.source.companies, config.source.deals, ...config.source.custom ?? []);
329
+ for (const source of sources) {
330
+ const meta = source?.[require_drizzle_meta.DRIZZLE_META];
331
+ if (meta?.tableName && meta.columnsMeta) byTable.set(meta.tableName, meta.columnsMeta);
332
+ }
333
+ return byTable;
334
+ }
234
335
  function buildSchema(config) {
235
336
  const byName = /* @__PURE__ */ new Map();
236
337
  for (const table of config.schema ?? []) {
@@ -240,10 +341,16 @@ function buildSchema(config) {
240
341
  }
241
342
  entitiesToTables(config.entities, byName);
242
343
  if (config.contactEntity && !byName.has(config.contactEntity)) byName.set(config.contactEntity, new Set(config.contactFields ?? []));
243
- return [...byName].map(([name, columns]) => ({
244
- name,
245
- columns: [...columns]
246
- }));
344
+ const metaByTable = collectSchemaMeta(config);
345
+ return [...byName].map(([name, columns]) => {
346
+ const columnsMeta = metaByTable.get(name);
347
+ if (columnsMeta) for (const col of columnsMeta) columns.add(col.name);
348
+ return {
349
+ name,
350
+ columns: [...columns],
351
+ ...columnsMeta ? { columnsMeta } : {}
352
+ };
353
+ });
247
354
  }
248
355
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
249
356
  function createMemoryCursorStore() {
@@ -274,6 +381,36 @@ function timingSafeEqual(a, b) {
274
381
  for (let i = 0; i < a.length; i++) mismatch += a.charCodeAt(i) === b.charCodeAt(i) ? 0 : 1;
275
382
  return mismatch === 0;
276
383
  }
384
+ const RUN_ID_HEADER = "x-clivly-run-id";
385
+ const SIGNATURE_HEADER = "x-clivly-signature";
386
+ const SIGNATURE_TOLERANCE_SECONDS = 300;
387
+ const SIGNATURE_HEADER_RE = /^t=(\d+),v1=([0-9a-f]+)$/;
388
+ const SECONDS_PER_MS = 1e3;
389
+ function toHex(buffer) {
390
+ return [...new Uint8Array(buffer)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
391
+ }
392
+ /**
393
+ * Verify that a sync-trigger request really came from Clivly.
394
+ *
395
+ * Clivly signs `${timestamp}.${rawBody}` with the shared secret, so this proves
396
+ * two things at once: the caller holds the secret, and the body wasn't altered in
397
+ * flight. The timestamp is inside the signed material, so an attacker can't
398
+ * rewrite it to extend the replay window.
399
+ *
400
+ * Uses WebCrypto (not node:crypto) so it works unchanged on edge and worker
401
+ * runtimes, where a host app is most likely to mount this.
402
+ */
403
+ async function verifyTriggerSignature(header, rawBody, secret) {
404
+ const match = header ? SIGNATURE_HEADER_RE.exec(header) : null;
405
+ if (!match) return false;
406
+ const [, timestamp, provided] = match;
407
+ if (Math.abs(Math.floor(Date.now() / SECONDS_PER_MS) - Number(timestamp)) > SIGNATURE_TOLERANCE_SECONDS) return false;
408
+ const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), {
409
+ name: "HMAC",
410
+ hash: "SHA-256"
411
+ }, false, ["sign"]);
412
+ return timingSafeEqual(provided, toHex(await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(`${timestamp}.${rawBody}`))));
413
+ }
277
414
  function bearerToken(req) {
278
415
  const header = req.headers.get("authorization");
279
416
  if (!(header && BEARER_PREFIX.test(header))) return null;
@@ -326,6 +463,73 @@ function createFetcher(retry) {
326
463
  }
327
464
  };
328
465
  }
466
+ function resolveSampleSource(config, entityKey) {
467
+ const entity = config.entities?.entities?.[entityKey];
468
+ const src = config.source;
469
+ if (!(entity && src)) return null;
470
+ if (entity.concept === "contact" && src.contacts) return {
471
+ source: src.contacts,
472
+ path: SYNC_CONTACTS_PATH
473
+ };
474
+ if (entity.concept === "company" && src.companies) return {
475
+ source: src.companies,
476
+ path: SYNC_COMPANIES_PATH
477
+ };
478
+ if (entity.concept === "deal" && src.deals) return {
479
+ source: src.deals,
480
+ path: SYNC_DEALS_PATH
481
+ };
482
+ if (entity.concept === "custom") {
483
+ const custom = (src.custom ?? []).find((c) => c.entity === entity.source || c.entity === entityKey);
484
+ return custom ? {
485
+ source: custom,
486
+ path: SYNC_CUSTOM_PATH,
487
+ objectType: custom.objectType
488
+ } : null;
489
+ }
490
+ return null;
491
+ }
492
+ const PREVIEW_LIMIT = 5;
493
+ function resolvePreviewSource(config, sourceTable) {
494
+ const src = config.source;
495
+ if (!src) return null;
496
+ return [
497
+ src.contacts,
498
+ src.companies,
499
+ src.deals,
500
+ ...src.custom ?? []
501
+ ].filter((s) => Boolean(s)).find((s) => s.entity === sourceTable) ?? null;
502
+ }
503
+ function toPreviewCell(value) {
504
+ if (value === null || value === void 0) return null;
505
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
506
+ if (value instanceof Date) return value.toISOString();
507
+ return String(value);
508
+ }
509
+ function slimPreviewRow(row, needed) {
510
+ const out = {};
511
+ for (const key of needed) out[key] = toPreviewCell(row[key]);
512
+ return out;
513
+ }
514
+ async function handlePreviewRequest(config, preview) {
515
+ const source = resolvePreviewSource(config, preview.sourceTable);
516
+ if (!source) return jsonResponse({ error: `No configured source for table "${preview.sourceTable}"` }, 400);
517
+ const idField = source.idField ?? "id";
518
+ const filterColumns = preview.filter ? Object.keys(preview.filter).filter((k) => k !== "$raw") : [];
519
+ const needed = new Set([
520
+ idField,
521
+ ...Object.values(preview.fieldMap),
522
+ ...filterColumns
523
+ ]);
524
+ const rows = (await source.fetchPage({
525
+ since: null,
526
+ limit: PREVIEW_LIMIT
527
+ })).slice(0, PREVIEW_LIMIT).map((row) => slimPreviewRow(row, needed));
528
+ return jsonResponse({
529
+ rows,
530
+ sampledCount: rows.length
531
+ }, 200);
532
+ }
329
533
  /**
330
534
  * Create the Clivly SDK. The implemented surface — heartbeat/discovery reporting
331
535
  * and the auth-verify handler — connects a developer's app to a running Clivly
@@ -343,42 +547,77 @@ function createClivlySDK(config) {
343
547
  const batchSize = config.sync?.batchSize ?? DEFAULT_BATCH_SIZE;
344
548
  const cursorStore = config.cursorStore ?? createMemoryCursorStore();
345
549
  const fetcher = createFetcher(config.retry);
346
- const push = async (path, body) => {
347
- return (await fetcher(`${apiUrl}${path}`, {
550
+ let warnedUnsignedTrigger = false;
551
+ let lastPushFailure = null;
552
+ const createPush = (runId) => async (path, body) => {
553
+ const response = await fetcher(`${apiUrl}${path}`, {
348
554
  method: "POST",
349
555
  headers: {
350
556
  "content-type": "application/json",
351
- authorization: `Bearer ${config.apiKey}`
557
+ authorization: `Bearer ${config.apiKey}`,
558
+ ...runId ? { [RUN_ID_HEADER]: runId } : {}
352
559
  },
353
560
  body: JSON.stringify(body)
354
- }))?.ok ?? false;
561
+ });
562
+ if (response?.ok) return true;
563
+ lastPushFailure ??= { status: response?.status ?? null };
564
+ return false;
355
565
  };
356
- const syncOne = async (source, path, mode) => {
566
+ const describePushFailure = (entities) => {
567
+ const base = `Clivly sync: push failed for ${entities.join(", ")}`;
568
+ const status = lastPushFailure?.status ?? null;
569
+ let cause;
570
+ if (status === 401 || status === 403) cause = "the API key was rejected — check CLIVLY_API_KEY is set to a valid key for this org";
571
+ else if (status === null) cause = `couldn't reach Clivly at ${apiUrl} — check network access and CLIVLY_API_URL`;
572
+ else cause = `Clivly returned HTTP ${status}`;
573
+ return `${base}: ${cause}. Run \`clivly doctor\` to diagnose.`;
574
+ };
575
+ const syncOne = async (source, path, mode, objectType, runId, batchSizeOverride, maxRowsOverride, persistCursor = true) => {
357
576
  const since = mode === "full" ? null : await cursorStore.get(source.entity) ?? null;
577
+ const push = createPush(runId);
578
+ const effectiveBatchSize = batchSizeOverride ?? batchSize;
358
579
  const result = mode === "full" ? await fullSyncSource({
359
580
  source,
360
581
  path,
361
- batchSize,
362
- push
582
+ batchSize: effectiveBatchSize,
583
+ push,
584
+ objectType
363
585
  }) : await syncSource({
364
586
  source,
365
587
  path,
366
588
  since,
367
- batchSize,
368
- push
589
+ batchSize: effectiveBatchSize,
590
+ push,
591
+ objectType,
592
+ maxRows: maxRowsOverride
369
593
  });
370
- await cursorStore.set(source.entity, result.cursor);
594
+ if (persistCursor) await cursorStore.set(source.entity, result.cursor);
371
595
  return result.ok;
372
596
  };
597
+ const syncCustomSources = async (customSources, mode, runId) => {
598
+ const failed = [];
599
+ for (const customSource of customSources ?? []) if (!await syncOne(customSource, SYNC_CUSTOM_PATH, mode, customSource.objectType, runId)) failed.push(customSource.entity);
600
+ return failed;
601
+ };
373
602
  const runSync = async (options) => {
374
603
  if (!config.source) return;
604
+ lastPushFailure = null;
375
605
  const mode = options?.mode ?? "delta";
606
+ const runId = options?.runId;
376
607
  const failed = [];
377
608
  if (config.source.companies) {
378
- if (!await syncOne(config.source.companies, SYNC_COMPANIES_PATH, mode)) failed.push(config.source.companies.entity);
609
+ if (!await syncOne(config.source.companies, SYNC_COMPANIES_PATH, mode, void 0, runId)) failed.push(config.source.companies.entity);
379
610
  }
380
- if (!await syncOne(config.source.contacts, SYNC_CONTACTS_PATH, mode)) failed.push(config.source.contacts.entity);
381
- if (failed.length > 0) throw new Error(`Clivly sync: push failed for ${failed.join(", ")}`);
611
+ if (!await syncOne(config.source.contacts, SYNC_CONTACTS_PATH, mode, void 0, runId)) failed.push(config.source.contacts.entity);
612
+ if (config.source.deals && !await syncOne(config.source.deals, SYNC_DEALS_PATH, mode, void 0, runId)) failed.push(config.source.deals.entity);
613
+ failed.push(...await syncCustomSources(config.source.custom, mode, runId));
614
+ if (failed.length > 0) throw new Error(describePushFailure(failed));
615
+ };
616
+ const runSampleSync = async (entityKey, runId) => {
617
+ const resolved = resolveSampleSource(config, entityKey);
618
+ if (!resolved) throw new Error(`Clivly sync: sample entityKey "${entityKey}" did not resolve to a configured source`);
619
+ lastPushFailure = null;
620
+ if (!await syncOne(resolved.source, resolved.path, "delta", resolved.objectType, runId, SAMPLE_BATCH_SIZE, SAMPLE_BATCH_SIZE, false)) throw new Error(describePushFailure([resolved.source.entity]));
382
621
  };
383
622
  const sendHeartbeat = () => fetcher(`${apiUrl}/clivly/sdk/heartbeat`, {
384
623
  method: "POST",
@@ -392,7 +631,8 @@ function createClivlySDK(config) {
392
631
  sdkVersion: config.sdkVersion,
393
632
  contactEntity: resolveContactEntity(config),
394
633
  namespaceReady: config.namespaceReady ?? false,
395
- schema: buildSchema(config)
634
+ schema: buildSchema(config),
635
+ ...config.syncTrigger?.url ? { syncTrigger: { url: config.syncTrigger.url } } : {}
396
636
  })
397
637
  });
398
638
  const heartbeat = async () => {
@@ -428,13 +668,48 @@ function createClivlySDK(config) {
428
668
  await heartbeat();
429
669
  if (config.source) await runSync(options);
430
670
  };
431
- const createClivlyHandler = () => async (_req) => {
671
+ const triggerRequestAllowed = async (req, rawBody) => {
672
+ const secret = config.syncTrigger?.secret;
673
+ if (secret) return await verifyTriggerSignature(req.headers.get(SIGNATURE_HEADER), rawBody, secret);
674
+ if (!warnedUnsignedTrigger) {
675
+ warnedUnsignedTrigger = true;
676
+ console.warn("Clivly: the sync trigger route is mounted without `syncTrigger.secret`, so anyone who can reach the URL can start a sync. Set a secret (Integrations → Remote sync in Clivly) to require signed requests.");
677
+ }
678
+ return true;
679
+ };
680
+ const parseTriggerBody = (rawBody) => {
681
+ if (!rawBody) return {};
432
682
  try {
433
- await runScheduled();
434
- return jsonResponse({ ok: true }, 200);
683
+ return JSON.parse(rawBody);
684
+ } catch {
685
+ return {};
686
+ }
687
+ };
688
+ const createClivlyHandler = () => async (req) => {
689
+ const rawBody = await req.text().catch(() => "");
690
+ if (!await triggerRequestAllowed(req, rawBody)) return jsonResponse({
691
+ ok: false,
692
+ error: "unauthorized"
693
+ }, UNAUTHORIZED_STATUS);
694
+ const payload = parseTriggerBody(rawBody);
695
+ const runId = payload.runId ?? null;
696
+ const startedAt = Date.now();
697
+ try {
698
+ if (payload.preview) return await handlePreviewRequest(config, payload.preview);
699
+ if (payload.sample) await runSampleSync(payload.sample.entityKey, payload.runId);
700
+ else await runScheduled({
701
+ mode: payload.mode,
702
+ runId: payload.runId
703
+ });
704
+ return jsonResponse({
705
+ ok: true,
706
+ runId,
707
+ durationMs: Date.now() - startedAt
708
+ }, 200);
435
709
  } catch (error) {
436
710
  return jsonResponse({
437
711
  ok: false,
712
+ runId,
438
713
  error: error instanceof Error ? error.message : String(error)
439
714
  }, 500);
440
715
  }
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as ClivlyUser, c as DoctorCheck, i as ClivlySDKConfig, l as DoctorReport, n as ClivlyConnectResult, o as CursorStore, r as ClivlySDK, s as DiscoveredTable, t as ClivlyConnectReason } from "./types-B0aSxJ6V.cjs";
1
+ import { a as ClivlySDKConfig, c as DiscoveredTable, i as ClivlySDK, l as DoctorCheck, n as ClivlyConnectReason, o as ClivlyUser, r as ClivlyConnectResult, s as CursorStore, u as DoctorReport } from "./namespace-probe-DIBgCQca.cjs";
2
2
 
3
3
  //#region src/errors.d.ts
4
4
  declare class ClivlyError extends Error {
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as ClivlyUser, c as DoctorCheck, i as ClivlySDKConfig, l as DoctorReport, n as ClivlyConnectResult, o as CursorStore, r as ClivlySDK, s as DiscoveredTable, t as ClivlyConnectReason } from "./types-DWt-ytBQ.mjs";
1
+ import { a as ClivlySDKConfig, c as DiscoveredTable, i as ClivlySDK, l as DoctorCheck, n as ClivlyConnectReason, o as ClivlyUser, r as ClivlyConnectResult, s as CursorStore, u as DoctorReport } from "./namespace-probe-DFpvfl7T.mjs";
2
2
 
3
3
  //#region src/errors.d.ts
4
4
  declare class ClivlyError extends Error {