@clivly/sdk 0.9.1 → 0.9.3

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/README.md CHANGED
@@ -1,9 +1,10 @@
1
1
  # @clivly/sdk
2
2
 
3
- Connect your app to Clivly Cloud. The SDK sends a heartbeat, reports your schema
4
- for entity mapping, and mirrors your contacts and companies into Clivly over
5
- outbound HTTPS. If you configure `syncTrigger`, Clivly can also call back into
6
- your app to start a sync from the dashboard.
3
+ Connect your app to Clivly Cloud. The SDK reports your schema for entity mapping
4
+ and mirrors your contacts and companies over outbound HTTPS. Long-lived Node
5
+ hosts can opt into WebSocket presence, which replaces the periodic HTTP
6
+ liveness heartbeat while the socket is ready and falls back automatically if it
7
+ is not.
7
8
 
8
9
  Clivly never connects to your database — the SDK runs inside your app and is the
9
10
  only thing that touches your data.
@@ -69,6 +70,9 @@ export default createClivlySDK({
69
70
  cursorField: "updatedAt",
70
71
  }),
71
72
  },
73
+ presence: {
74
+ transport: "websocket", // long-lived Node hosts only; HTTP fallback is automatic
75
+ },
72
76
  });
73
77
  ```
74
78
 
@@ -96,13 +100,44 @@ The single most consequential decision in the setup.
96
100
 
97
101
  | Host runtime | Use | Why |
98
102
  | --- | --- | --- |
99
- | Long-lived Node server, Vite dev | `clivlyVite(sdk)` or `await sdk.start()` | The `setInterval` keep-alive loop persists |
103
+ | Long-lived Node server, Vite dev | `clivlyVite(sdk)` or `await sdk.start()` | The keep-alive loop persists; optionally use WebSocket presence |
100
104
  | Cloudflare Workers, Vercel, Lambda | `sdk.createClivlyHandler()` at a route, and/or `sdk.runScheduled()` from cron | `setInterval` does **not** survive between invocations |
101
105
  | One-off health check | `await sdk.connect()` | Structured `{ ok, status, reason, retryable, org? }` |
102
106
 
103
107
  `start()` emits a console warning when it detects a serverless runtime, because
104
108
  there it stops ticking after the first response.
105
109
 
110
+ ### WebSocket presence
111
+
112
+ HTTP is the default presence transport. To replace the 60-second HTTP liveness
113
+ cadence on a long-lived Node host, opt in:
114
+
115
+ ```ts
116
+ createClivlySDK({
117
+ // ...the rest of your configuration
118
+ presence: {
119
+ transport: "websocket",
120
+ deploymentId: process.env.CLIVLY_DEPLOYMENT_ID, // optional
121
+ },
122
+ });
123
+ ```
124
+
125
+ `start()` performs its initial HTTP check-in, then opens an authenticated
126
+ outbound socket. When the server confirms it is ready, the socket becomes the
127
+ liveness signal and the periodic HTTP heartbeat pauses. A closed or unavailable
128
+ socket reconnects with backoff while the SDK resumes HTTP heartbeat
129
+ automatically. This does not alter `sync.intervalMs`; it only changes presence.
130
+
131
+ Keep the default HTTP transport for serverless and edge runtimes. They cannot
132
+ retain a WebSocket across invocations, so use `runScheduled()` or
133
+ `createClivlyHandler()` there.
134
+
135
+ For rollout reliability, enabled WebSocket presence emits bounded lifecycle
136
+ metrics (event, transport, outcome, state, duration and attempt). It never
137
+ includes credentials, organization or deployment identifiers, host URLs, row
138
+ data, error messages, or stack traces. See
139
+ [SECURITY.md](../../SECURITY.md#telemetry).
140
+
106
141
  ### Entities vs. sources — the distinction that bites
107
142
 
108
143
  ```ts
@@ -397,7 +432,9 @@ is honoured. Other `4xx` responses return immediately.
397
432
  | `schema` | `DiscoveredTable[]` | — | Use `discoverFromDrizzle(schema)`. It reports keys, types, nullability and FK edges alongside column names; [derived objects](#derived-objects) cannot be authored without them, so prefer it over a hand-written array |
398
433
  | `source` | `{ contacts, companies?, deals?, custom? }` | — | Keyed by CRM concept. `contacts` is required when `source` is present |
399
434
  | `cursorStore` | `CursorStore` | in-memory | Persist on serverless |
400
- | `heartbeatIntervalMs` | `number` | `60000` | |
435
+ | `heartbeatIntervalMs` | `number` | `60000` | HTTP presence cadence and default sync cadence; paused when opt-in WebSocket presence is ready |
436
+ | `presence.transport` | `"http" \\| "websocket"` | `"http"` | WebSocket is for long-lived Node hosts; HTTP resumes during reconnect/fallback |
437
+ | `presence.deploymentId` | `string` | platform-derived | Optional release identity for a WebSocket deployment |
401
438
  | `sync.onBoot` | `boolean` | `true` | Only affects `start()` |
402
439
  | `sync.intervalMs` | `number` | `heartbeatIntervalMs` | |
403
440
  | `sync.batchSize` | `number` | `500` | Rows per page |
@@ -416,14 +453,28 @@ is honoured. Other `4xx` responses return immediately.
416
453
  | Method | Returns | Notes |
417
454
  | --- | --- | --- |
418
455
  | `connect()` | `Promise<ClivlyConnectResult>` | One heartbeat, structured. Never throws |
456
+ | `ping()` | `Promise<ClivlyConnectResult>` | Zero-write reachability probe. Same shape as `connect()`, but records no heartbeat — and it alone returns `workspace` |
419
457
  | `heartbeat()` | `Promise<boolean>` | One heartbeat, boolean |
420
- | `start()` | `Promise<void>` | Heartbeat + boot sync + timers. Long-lived hosts only |
458
+ | `start()` | `Promise<void>` | Heartbeat + boot sync + timers. Long-lived hosts only; opens opt-in WebSocket presence |
421
459
  | `stop()` | `void` | Clears the timers |
422
460
  | `sync(options?)` | `Promise<void>` | `{ mode?: "delta" \| "full", runId?: string }`. Throws on push failure |
423
461
  | `runScheduled(options?)` | `Promise<void>` | One heartbeat + one sync, no timers. Throws on push failure |
424
462
  | `createClivlyHandler()` | `(Request) => Promise<Response>` | Mount at a route |
425
463
  | `createAuthVerifyHandler(resolveUser)` | `(Request) => Promise<Response>` | `503` unless `verifyToken` is set |
426
464
  | `doctor()` | `Promise<DoctorReport>` | `{ ok, checks }` |
465
+ | `ensureSchema()` | `Promise<EnsureSchemaResult \| null>` | Declares your custom object types **and** the mappings that make them receive rows. `null` = could not deliver; **rejects** if Clivly refuses the config |
466
+ | `ensureCustomObjects()` | `Promise<{ slug, id, created }[] \| null>` | Object types only, no mappings. Superseded by `ensureSchema()` |
467
+
468
+ `clivly push-schema` calls `ensureSchema()` for you, so you rarely need it
469
+ directly — reach for it when you want config-as-code to run from your own
470
+ deploy script or a CI step.
471
+
472
+ The `null`-vs-throw split is deliberate. `null` means the request never landed
473
+ (network, or a Clivly older than the route), which is worth a retry. A rejection
474
+ means Clivly understood the config and declined it — most often because an
475
+ entity would overwrite a mapping someone edited in the dashboard — which is
476
+ worth reading, not retrying. A config-declared mapping is owned by your config;
477
+ a dashboard-edited one is never silently overwritten.
427
478
 
428
479
  ### `ClivlyConnectResult.reason`
429
480
 
@@ -431,6 +482,17 @@ is honoured. Other `4xx` responses return immediately.
431
482
  `server_error` (5xx) · `client_error` (other 4xx) · `network_error` (no
432
483
  response).
433
484
 
485
+ ### `ClivlyConnectResult.workspace`
486
+
487
+ `ping()` also returns `workspace` — the setup ladder for the org behind your key
488
+ (`IntegrationState` from `@clivly/core`), computed by the server so the CLI and
489
+ the dashboard cannot disagree about how far setup has got. This is what
490
+ `clivly status` prints under **Workspace**.
491
+
492
+ It is `undefined` or `null` when the server predates the field or could not read
493
+ it. Treat that as *unknown*, never as *nothing is set up* — an old server, a
494
+ rejected key and a dead network all mean the question went unanswered.
495
+
434
496
  ### Subpath exports
435
497
 
436
498
  | From `clivly` | From `@clivly/sdk` | Contents |
@@ -444,21 +506,27 @@ response).
444
506
 
445
507
  A custom object can be derived from a base table plus explicit joins, instead of
446
508
  mapping exactly one table — an `Enrollment` built from `enrollments` joined to
447
- `students` and `courses`. You build one in the Clivly dashboard, not in code.
448
-
449
- The SDK's only job is to report enough schema for it: `discoverFromDrizzle`
450
- sends primary keys, unique constraints, nullability, column types and
451
- foreign-key edges. Validation is fail-closed, so if your config hand-writes
452
- `schema` as `{ name, columns }` pairs, the dashboard will refuse to author a
453
- projection against those tables with *"has no key/column metadata — upgrade
454
- `@clivly/sdk` and re-run discovery"*.
455
-
456
- The fix is to pass `discoverFromDrizzle(schema)` and redeploy, so a fresh
457
- heartbeat carries the richer payload. Re-run it after schema changes too:
458
- projections validate against the last reported snapshot.
459
-
460
- > Requires `clivly@0.9.0` / `@clivly/sdk@0.9.0` or later. Earlier versions
461
- > reported names only, so no host app could author a derived object.
509
+ `students` and `courses`. You declare one on an entity in `clivly.config.ts`,
510
+ alongside the object it materializes into; `validateEntitiesConfig` checks it in
511
+ CI.
512
+
513
+ Two things have to line up:
514
+
515
+ - **Discovery must be rich enough.** `discoverFromDrizzle` sends primary keys,
516
+ unique constraints, nullability, column types and foreign-key edges.
517
+ Validation is fail-closed, so if your config hand-writes `schema` as
518
+ `{ name, columns }` pairs, every table is rejected with *"has no key/column
519
+ metadata — upgrade `@clivly/sdk` and re-run discovery"*. Pass
520
+ `discoverFromDrizzle(schema)` and redeploy so a fresh heartbeat carries the
521
+ richer payload — and re-run it after schema changes, since projections
522
+ validate against the last reported snapshot.
523
+ - **The server must have the capability enabled.** Authoring sits behind
524
+ `CLIVLY_PROJECTION_MAPPINGS=true` while the surface settles. Reading and
525
+ syncing an existing derived object is never gated.
526
+
527
+ > Requires `clivly@0.9.0` / `@clivly/sdk@0.9.0` or later for the discovery
528
+ > metadata. Earlier versions reported names only, so no host app could author a
529
+ > derived object.
462
530
 
463
531
  Full guide: [`docs/guides/derived-objects.md`](../../docs/guides/derived-objects.md).
464
532
 
@@ -466,6 +534,8 @@ Full guide: [`docs/guides/derived-objects.md`](../../docs/guides/derived-objects
466
534
 
467
535
  - `start()` is for long-lived runtimes only. On serverless/edge, use
468
536
  `runScheduled()` or `createClivlyHandler()`.
537
+ - WebSocket presence is opt-in and long-lived Node-only. It does not replace
538
+ HTTP presence on serverless or edge hosts.
469
539
  - `fromDrizzle` is the only generated source path. Non-Drizzle hosts write
470
540
  `fetchPage` by hand.
471
541
  - The flat `contactEntity` / `contactFields` config still works but is
@@ -1,4 +1,4 @@
1
- import { c as CreateChatSessionHandlerOptions } from "./types-QPbki8ur.cjs";
1
+ import { c as CreateChatSessionHandlerOptions } from "./types-Cgwxd4h_.cjs";
2
2
  //#region src/chat-session.d.ts
3
3
  declare function createChatSessionHandler({ allowedOrigins, apiKey, apiUrl, fetchImpl, resolveUser }: CreateChatSessionHandlerOptions): (request: Request) => Promise<Response>;
4
4
  //#endregion
@@ -1,4 +1,4 @@
1
- import { c as CreateChatSessionHandlerOptions } from "./types-QPbki8ur.mjs";
1
+ import { c as CreateChatSessionHandlerOptions } from "./types-Cgwxd4h_.mjs";
2
2
  //#region src/chat-session.d.ts
3
3
  declare function createChatSessionHandler({ allowedOrigins, apiKey, apiUrl, fetchImpl, resolveUser }: CreateChatSessionHandlerOptions): (request: Request) => Promise<Response>;
4
4
  //#endregion
@@ -1,4 +1,4 @@
1
- import { m as NamespaceIntrospector, p as SyncSource } from "./types-QPbki8ur.cjs";
1
+ import { _ as NamespaceIntrospector, g as SyncSource } from "./types-Cgwxd4h_.cjs";
2
2
  import { Table } from "drizzle-orm";
3
3
  import { buildUniqueConstraints, discoverFromDrizzle } from "@clivly/core/drizzle";
4
4
  //#region src/drizzle.d.ts
@@ -1,4 +1,4 @@
1
- import { m as NamespaceIntrospector, p as SyncSource } from "./types-QPbki8ur.mjs";
1
+ import { _ as NamespaceIntrospector, g as SyncSource } from "./types-Cgwxd4h_.mjs";
2
2
  import { buildUniqueConstraints, discoverFromDrizzle } from "@clivly/core/drizzle";
3
3
  import { Table } from "drizzle-orm";
4
4
  //#region src/drizzle.d.ts