@clivly/sdk 0.7.1 → 0.7.2
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 +122 -122
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,36 +1,66 @@
|
|
|
1
1
|
# @clivly/sdk
|
|
2
2
|
|
|
3
|
-
Connect
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
your app to start a sync from the dashboard.
|
|
3
|
+
Connect an app to Clivly Cloud. The SDK sends heartbeats, reports schema
|
|
4
|
+
discovery data, syncs source rows to Clivly over outbound HTTPS, and can expose
|
|
5
|
+
a signed trigger route so Clivly can start a sync from the dashboard.
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
[`docs/guides/connecting-to-clivly-cloud.md`](../../docs/guides/connecting-to-clivly-cloud.md).
|
|
10
|
-
This README stays focused on the SDK surface itself.
|
|
7
|
+
## Which package should I install?
|
|
11
8
|
|
|
12
|
-
|
|
9
|
+
For most new integrations, install the consolidated public package:
|
|
13
10
|
|
|
14
11
|
```bash
|
|
15
12
|
npm install clivly
|
|
16
13
|
```
|
|
17
14
|
|
|
18
|
-
|
|
19
|
-
you are intentionally validating a prerelease build.
|
|
20
|
-
|
|
21
|
-
Add `drizzle-orm` when you use `clivly/drizzle`, and
|
|
22
|
-
`vite` when you use the Vite/TanStack Start plugin.
|
|
23
|
-
|
|
24
|
-
## Usage
|
|
15
|
+
and import from:
|
|
25
16
|
|
|
26
17
|
```ts
|
|
27
18
|
import { createClivlySDK, defineClivlyConfig } from "clivly/sdk";
|
|
28
19
|
import { fromDrizzle } from "clivly/drizzle";
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`@clivly/sdk` remains a public package if you prefer a direct dependency on the
|
|
23
|
+
SDK package itself:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install @clivly/sdk
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
with imports from:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { createClivlySDK, defineClivlyConfig } from "@clivly/sdk";
|
|
33
|
+
import { fromDrizzle } from "@clivly/sdk/drizzle";
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Stable releases publish on npm's `latest` tag. Use `@next` only when you are
|
|
37
|
+
intentionally validating a prerelease build.
|
|
38
|
+
|
|
39
|
+
Add `drizzle-orm` when you use the Drizzle helpers, and `vite` when you use the
|
|
40
|
+
Vite/TanStack Start plugin.
|
|
41
|
+
|
|
42
|
+
## Surface
|
|
43
|
+
|
|
44
|
+
Public exports from this package:
|
|
45
|
+
|
|
46
|
+
- `@clivly/sdk`:
|
|
47
|
+
`createClivlySDK`, `defineClivlyConfig`, `createChatSessionHandler`,
|
|
48
|
+
`ClivlyError`, `ClivlyNetworkError`, `ClivlyRateLimitError`,
|
|
49
|
+
`ClivlyAuthError`
|
|
50
|
+
- `@clivly/sdk/drizzle`:
|
|
51
|
+
Drizzle source helpers such as `fromDrizzle`
|
|
52
|
+
- `@clivly/sdk/vite`:
|
|
53
|
+
the Vite/TanStack Start lifecycle plugin
|
|
54
|
+
- `@clivly/sdk/chat-session`:
|
|
55
|
+
chat-session handler helpers when you want that subpath explicitly
|
|
56
|
+
|
|
57
|
+
## Basic usage
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import { createClivlySDK, defineClivlyConfig } from "@clivly/sdk";
|
|
61
|
+
import { fromDrizzle } from "@clivly/sdk/drizzle";
|
|
29
62
|
import { db, participants, accounts } from "./db";
|
|
30
63
|
|
|
31
|
-
// Declarative entity config — the non-deprecated source of truth. It maps your
|
|
32
|
-
// host tables/columns onto CRM concepts, and `defineClivlyConfig` validates it
|
|
33
|
-
// at load (warning on any non-canonical field key that would mirror as empty).
|
|
34
64
|
const entities = defineClivlyConfig({
|
|
35
65
|
entities: {
|
|
36
66
|
participants: {
|
|
@@ -47,62 +77,67 @@ const entities = defineClivlyConfig({
|
|
|
47
77
|
});
|
|
48
78
|
|
|
49
79
|
const clivly = createClivlySDK({
|
|
50
|
-
apiKey: process.env.CLIVLY_SECRET_KEY!,
|
|
80
|
+
apiKey: process.env.CLIVLY_SECRET_KEY!,
|
|
51
81
|
entities,
|
|
52
82
|
source: {
|
|
53
|
-
contacts: fromDrizzle(db, participants, {
|
|
54
|
-
|
|
83
|
+
contacts: fromDrizzle(db, participants, {
|
|
84
|
+
entity: "participants",
|
|
85
|
+
cursorField: "updatedAt",
|
|
86
|
+
}),
|
|
87
|
+
companies: fromDrizzle(db, accounts, {
|
|
88
|
+
entity: "accounts",
|
|
89
|
+
cursorField: "updatedAt",
|
|
90
|
+
}),
|
|
55
91
|
},
|
|
56
92
|
sync: { onBoot: true, intervalMs: 30_000, batchSize: 500 },
|
|
57
93
|
});
|
|
58
94
|
|
|
59
|
-
const status = await clivly.connect();
|
|
60
|
-
if (!status.ok)
|
|
61
|
-
|
|
95
|
+
const status = await clivly.connect();
|
|
96
|
+
if (!status.ok) {
|
|
97
|
+
throw new Error(`Clivly: ${status.reason}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
await clivly.start();
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`fromDrizzle` builds the keyset-paginated `fetchPage` function the SDK needs.
|
|
104
|
+
`cursorField` and `idField` are Drizzle property keys such as `updatedAt`, not
|
|
105
|
+
database column names such as `updated_at`. Non-Drizzle apps can provide their
|
|
106
|
+
own `fetchPage` implementation.
|
|
62
107
|
|
|
63
|
-
|
|
64
|
-
|
|
108
|
+
## Remote sync trigger
|
|
109
|
+
|
|
110
|
+
If you configure `syncTrigger`, Clivly can ask your app to run a sync from the
|
|
111
|
+
dashboard:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
const clivly = createClivlySDK({
|
|
65
115
|
apiKey: process.env.CLIVLY_SECRET_KEY!,
|
|
66
116
|
entities,
|
|
67
|
-
source: { /*
|
|
117
|
+
source: { /* ... */ },
|
|
68
118
|
syncTrigger: {
|
|
69
119
|
path: "/api/clivly/tick",
|
|
70
120
|
url: process.env.CLIVLY_SYNC_TRIGGER_URL,
|
|
71
|
-
secret: process.env.CLIVLY_SYNC_TRIGGER_SECRET,
|
|
121
|
+
secret: process.env.CLIVLY_SYNC_TRIGGER_SECRET,
|
|
72
122
|
},
|
|
73
123
|
});
|
|
74
124
|
|
|
75
125
|
export async function POST(request: Request) {
|
|
76
|
-
return
|
|
126
|
+
return clivly.createClivlyHandler()(request);
|
|
77
127
|
}
|
|
78
128
|
```
|
|
79
129
|
|
|
80
|
-
`
|
|
81
|
-
|
|
82
|
-
`
|
|
83
|
-
|
|
84
|
-
applies the field map you confirmed during onboarding, so re-mapping never needs
|
|
85
|
-
a redeploy. Non-Drizzle hosts can still pass a hand-written `fetchPage` closure
|
|
86
|
-
instead.
|
|
87
|
-
|
|
88
|
-
With `syncTrigger.url` + `syncTrigger.secret` configured, the SDK reports the
|
|
89
|
-
trigger URL on each heartbeat and rejects unsigned or badly-signed trigger
|
|
90
|
-
requests. Without `syncTrigger`, cloud mode is push-only: Clivly can observe
|
|
91
|
-
the app but cannot start a sync from the dashboard.
|
|
92
|
-
|
|
93
|
-
If you rotate a remote-sync secret from Clivly's dashboard after the encryption
|
|
94
|
-
rollout, Clivly stores it encrypted at rest. The server-side deployment must
|
|
95
|
-
have `CLIVLY_TRIGGER_SECRET_WRAPPING_KEY` configured, while your app continues
|
|
96
|
-
to use `CLIVLY_SYNC_TRIGGER_SECRET`. Existing plaintext rows remain valid during
|
|
97
|
-
rollout, and older rows can be backfilled with
|
|
98
|
-
`node ./scripts/backfill-sync-trigger-secrets.mjs --write`.
|
|
130
|
+
With `syncTrigger.url` and `syncTrigger.secret` configured, the SDK reports the
|
|
131
|
+
trigger URL on each heartbeat and rejects unsigned or badly-signed requests.
|
|
132
|
+
Without `syncTrigger`, cloud mode is push-only: Clivly can observe the app but
|
|
133
|
+
cannot start a sync from the dashboard.
|
|
99
134
|
|
|
100
135
|
## Vite / TanStack Start
|
|
101
136
|
|
|
102
|
-
For long-lived Vite dev servers, wire the SDK lifecycle through the
|
|
137
|
+
For long-lived Vite dev servers, wire the SDK lifecycle through the plugin:
|
|
103
138
|
|
|
104
139
|
```ts
|
|
105
|
-
import { clivlyVite } from "clivly/vite";
|
|
140
|
+
import { clivlyVite } from "@clivly/sdk/vite";
|
|
106
141
|
import clivly from "./clivly.config";
|
|
107
142
|
import { defineConfig } from "vite";
|
|
108
143
|
|
|
@@ -111,115 +146,80 @@ export default defineConfig({
|
|
|
111
146
|
});
|
|
112
147
|
```
|
|
113
148
|
|
|
114
|
-
The plugin calls `connect()` for a
|
|
115
|
-
|
|
116
|
-
`createClivlyHandler()` for
|
|
117
|
-
|
|
118
|
-
> **Legacy:** the flat `contactEntity` / `contactFields` form is still accepted but
|
|
119
|
-
> **deprecated** — prefer the `entities` config above, which is the same model the
|
|
120
|
-
> mapping table and sync engine share.
|
|
149
|
+
The plugin calls `connect()` for a startup status line, then `start()` for the
|
|
150
|
+
heartbeat and sync loop. It is dev-server only; use `runScheduled()` or
|
|
151
|
+
`createClivlyHandler()` for request-scoped runtimes.
|
|
121
152
|
|
|
122
153
|
## Verify your setup
|
|
123
154
|
|
|
124
|
-
|
|
155
|
+
If your instance lives in `clivly.config.ts`, the CLI can validate the setup:
|
|
125
156
|
|
|
126
157
|
```bash
|
|
127
|
-
npx clivly ping
|
|
128
|
-
npx clivly doctor
|
|
158
|
+
npx clivly ping
|
|
159
|
+
npx clivly doctor
|
|
129
160
|
```
|
|
130
161
|
|
|
131
|
-
In
|
|
132
|
-
|
|
133
|
-
```bash
|
|
134
|
-
npx clivly doctor --config ./apps/web/clivly.config.ts
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
Or in code: `await clivly.doctor()` returns `{ ok, checks }` — the same six read-only checks.
|
|
162
|
+
In code, `await clivly.doctor()` returns the same structured read-only checks.
|
|
138
163
|
|
|
139
164
|
## Serverless / edge
|
|
140
165
|
|
|
141
|
-
On request-scoped runtimes
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
scheduled trigger with `runScheduled()`, or mount `createClivlyHandler()` at a route:
|
|
166
|
+
On request-scoped runtimes such as Cloudflare Workers, Vercel, or Lambda, do
|
|
167
|
+
not call `start()`. Use `runScheduled()` or mount `createClivlyHandler()`
|
|
168
|
+
instead:
|
|
145
169
|
|
|
146
170
|
```ts
|
|
147
|
-
// Cloudflare Workers
|
|
148
171
|
export default {
|
|
149
|
-
scheduled: () => clivly.runScheduled(),
|
|
150
|
-
fetch: clivly.createClivlyHandler(),
|
|
172
|
+
scheduled: () => clivly.runScheduled(),
|
|
173
|
+
fetch: clivly.createClivlyHandler(),
|
|
151
174
|
};
|
|
152
175
|
```
|
|
153
176
|
|
|
154
|
-
`runScheduled()` throws
|
|
155
|
-
`createClivlyHandler()` returns `200`
|
|
156
|
-
`500` otherwise. If the run carries a Clivly-issued run id, the SDK also reports
|
|
157
|
-
the final outcome back to Clivly so the dashboard reflects `running` vs
|
|
158
|
-
`succeeded` vs `failed` truthfully. `start()` emits a warning if it detects a
|
|
159
|
-
serverless runtime. Verify a connection from the terminal any time with
|
|
160
|
-
`npx clivly ping`.
|
|
177
|
+
`runScheduled()` throws on a failed push so cron jobs surface failures.
|
|
178
|
+
`createClivlyHandler()` returns `200` on success and `500` on a failed run.
|
|
161
179
|
|
|
162
180
|
### Persisting the delta cursor
|
|
163
181
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
serverless cold start**, so a fresh isolate would re-page the whole entity. Pass a
|
|
167
|
-
`cursorStore` backed by your own KV/DB to persist it:
|
|
182
|
+
By default the delta cursor lives in memory. For serverless runtimes, pass a
|
|
183
|
+
persistent `cursorStore`:
|
|
168
184
|
|
|
169
185
|
```ts
|
|
170
186
|
const clivly = createClivlySDK({
|
|
171
187
|
apiKey: process.env.CLIVLY_SECRET_KEY!,
|
|
172
188
|
entities,
|
|
173
|
-
source: { /*
|
|
189
|
+
source: { /* ... */ },
|
|
174
190
|
cursorStore: {
|
|
175
|
-
// `SyncCursor` is { id: string; time: Date } — serialize `time` (e.g. ISO string).
|
|
176
191
|
get: async (entity) => {
|
|
177
192
|
const raw = await kv.get(`clivly:cursor:${entity}`);
|
|
178
193
|
return raw ? { id: raw.id, time: new Date(raw.time) } : null;
|
|
179
194
|
},
|
|
180
195
|
set: (entity, cursor) =>
|
|
181
|
-
kv.put(
|
|
196
|
+
kv.put(
|
|
197
|
+
`clivly:cursor:${entity}`,
|
|
198
|
+
cursor ? { id: cursor.id, time: cursor.time.toISOString() } : null
|
|
199
|
+
),
|
|
182
200
|
},
|
|
183
201
|
});
|
|
184
202
|
```
|
|
185
203
|
|
|
186
|
-
Delta sync is
|
|
187
|
-
|
|
188
|
-
`sync({ mode: "full" })` when you need the server to reconcile deletions (archive-on-leave).
|
|
189
|
-
|
|
190
|
-
## Many-to-many company membership
|
|
191
|
-
|
|
192
|
-
If a mapping links contacts to companies through a join table
|
|
193
|
-
(`via: { through: … }`), that entity's source must return **one row per
|
|
194
|
-
membership**, each row carrying the join table's foreign-key column.
|
|
195
|
-
|
|
196
|
-
```ts
|
|
197
|
-
// Ada belongs to two organizations: two rows, differing only in org_id.
|
|
198
|
-
{ id: "u1", full_name: "Ada", email: "ada@acme.test", org_id: "acme" }
|
|
199
|
-
{ id: "u1", full_name: "Ada", email: "ada@acme.test", org_id: "globex" }
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
This mirrors what the embedded model produces with `LEFT JOIN <join table>`, and
|
|
203
|
-
it is what lets Clivly keep one CRM row per membership instead of collapsing the
|
|
204
|
-
person onto whichever company happened to come first.
|
|
205
|
-
|
|
206
|
-
A person with no membership may omit the column entirely or send it as `null` —
|
|
207
|
-
both are accepted. But if **no** pushed row carries the column, the push is
|
|
208
|
-
rejected with `400 invalid_config`: silently keeping one membership would be
|
|
209
|
-
worse than failing, because the row counts look correct either way.
|
|
204
|
+
Delta sync is at-least-once. Without a persisted cursor after a cold start,
|
|
205
|
+
rows may be re-sent; they are not silently skipped.
|
|
210
206
|
|
|
211
207
|
## Known limitations
|
|
212
208
|
|
|
213
|
-
- `start()` is for long-lived runtimes only.
|
|
214
|
-
|
|
215
|
-
- `
|
|
216
|
-
|
|
217
|
-
- The flat `contactEntity` / `contactFields` config still works but is deprecated
|
|
218
|
-
in favor of `defineClivlyConfig({ entities: ... })`.
|
|
209
|
+
- `start()` is for long-lived runtimes only.
|
|
210
|
+
- `fromDrizzle` is the first-class generated source path today.
|
|
211
|
+
- The legacy flat `contactEntity` / `contactFields` config is still accepted but
|
|
212
|
+
deprecated in favor of `defineClivlyConfig({ entities: ... })`.
|
|
219
213
|
|
|
220
214
|
## More docs
|
|
221
215
|
|
|
222
|
-
-
|
|
223
|
-
[
|
|
224
|
-
- CLI
|
|
225
|
-
|
|
216
|
+
- Cloud onboarding guide:
|
|
217
|
+
[Connecting to Clivly Cloud](https://github.com/amani-joseph/clivly.com/blob/master/docs/guides/connecting-to-clivly-cloud.md)
|
|
218
|
+
- CLI reference:
|
|
219
|
+
[packages/cli/README.md](https://github.com/amani-joseph/clivly.com/blob/master/packages/cli/README.md)
|
|
220
|
+
- Release notes:
|
|
221
|
+
[CHANGELOG.md](https://github.com/amani-joseph/clivly.com/blob/master/CHANGELOG.md)
|
|
222
|
+
|
|
223
|
+
## License
|
|
224
|
+
|
|
225
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clivly/sdk",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Clivly SDK — connect your app to Clivly Cloud (heartbeat, discovery, auth bridge).",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"access": "public"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@clivly/core": "0.7.
|
|
58
|
+
"@clivly/core": "0.7.2"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
61
|
"drizzle-orm": ">=0.30 || >=1.0.0-rc.0",
|