clivly 0.10.8 → 0.10.9
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 +31 -1
- package/index.cjs +206 -13
- package/index.d.cts +2 -0
- package/index.d.mts +2 -0
- package/index.mjs +206 -13
- package/package.json +1 -1
- package/{sdk-BAHcnhm7.cjs → sdk-BIYu0N3V.cjs} +69 -12
- package/{sdk-C0DU5KDS.mjs → sdk-DofHordq.mjs} +69 -12
- package/sdk.cjs +1 -1
- package/sdk.mjs +1 -1
package/README.md
CHANGED
|
@@ -530,7 +530,11 @@ subpaths:
|
|
|
530
530
|
|
|
531
531
|
```ts
|
|
532
532
|
import { createClivlySDK, defineClivlyConfig } from "clivly/sdk";
|
|
533
|
-
import {
|
|
533
|
+
import {
|
|
534
|
+
fromDrizzle,
|
|
535
|
+
fromDrizzleProjection,
|
|
536
|
+
discoverFromDrizzle,
|
|
537
|
+
} from "clivly/drizzle";
|
|
534
538
|
import { clivlyVite } from "clivly/vite";
|
|
535
539
|
import { createClerkAuthAdapter } from "clivly/auth/clerk";
|
|
536
540
|
import { createWorkOSAuthAdapter } from "clivly/auth/workos";
|
|
@@ -542,6 +546,32 @@ import { createBetterAuthAdapter } from "clivly/auth/better-auth";
|
|
|
542
546
|
> importing it from app code drags the whole CLI into your bundle and throws at
|
|
543
547
|
> import time on Workers and other edge runtimes.
|
|
544
548
|
|
|
549
|
+
### Projection entities need `fromDrizzleProjection`
|
|
550
|
+
|
|
551
|
+
An entity with a `projection` spans several tables. `fromDrizzle` emits a
|
|
552
|
+
single-table select, so pairing it with a projection pushes bare base-table rows
|
|
553
|
+
and every joined field mirrors empty. Use the projection builder instead:
|
|
554
|
+
|
|
555
|
+
```ts
|
|
556
|
+
source: {
|
|
557
|
+
custom: [
|
|
558
|
+
fromDrizzleProjection(db, clientAccountsProjection, {
|
|
559
|
+
entity: "client_accounts",
|
|
560
|
+
objectType: "client-accounts",
|
|
561
|
+
cursorField: "base.updated_at",
|
|
562
|
+
}),
|
|
563
|
+
],
|
|
564
|
+
},
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
The builder also labels the push with the projection's `baseTable`, which is
|
|
568
|
+
what the mapping is filed under. A hand-built source (`fromDrizzle` with an
|
|
569
|
+
overridden `fetchPage`) is still supported — the SDK derives the same label from
|
|
570
|
+
your config — but the builder performs the joins for you.
|
|
571
|
+
|
|
572
|
+
> Authoring a derived object in the dashboard? Its **Connect** panel generates
|
|
573
|
+
> this snippet for you, projection literal included.
|
|
574
|
+
|
|
545
575
|
Provider and framework integrations use **optional peer dependencies**. Install
|
|
546
576
|
only what your app actually uses:
|
|
547
577
|
|
package/index.cjs
CHANGED
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
3
3
|
const require_entity_config = require("./entity-config-C2MfIDL2.cjs");
|
|
4
4
|
require("./mapping-score-CaB3F4Uu.cjs");
|
|
5
5
|
const require_internal_entity_heuristics = require("./internal-entity-heuristics.cjs");
|
|
6
|
-
const require_sdk = require("./sdk-
|
|
6
|
+
const require_sdk = require("./sdk-BIYu0N3V.cjs");
|
|
7
7
|
let node_child_process = require("node:child_process");
|
|
8
8
|
let node_fs = require("node:fs");
|
|
9
9
|
let node_module = require("node:module");
|
|
@@ -293,6 +293,124 @@ function addCommand(pm, packages) {
|
|
|
293
293
|
};
|
|
294
294
|
}
|
|
295
295
|
//#endregion
|
|
296
|
+
//#region src/detect-workspace.ts
|
|
297
|
+
const SELECTABLE_FRAMEWORKS = [
|
|
298
|
+
"nextjs",
|
|
299
|
+
"tanstack-start",
|
|
300
|
+
"sveltekit",
|
|
301
|
+
"remix",
|
|
302
|
+
"nuxt"
|
|
303
|
+
];
|
|
304
|
+
/**
|
|
305
|
+
* Validate a `--framework` value.
|
|
306
|
+
*
|
|
307
|
+
* Rejecting a typo matters more than accepting one leniently: an unrecognised
|
|
308
|
+
* value silently falling back to detection would scaffold route files in the
|
|
309
|
+
* wrong shape, and a route in the wrong shape is not a build error — the
|
|
310
|
+
* generator simply skips it and the endpoint 404s at runtime.
|
|
311
|
+
*/
|
|
312
|
+
function parseFrameworkFlag(value) {
|
|
313
|
+
if (value === void 0) return;
|
|
314
|
+
if (SELECTABLE_FRAMEWORKS.includes(value)) return value;
|
|
315
|
+
throw new Error(`"${value}" is not a supported framework. Accepted: ${SELECTABLE_FRAMEWORKS.join(", ")}.`);
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* A caution for when the framework came from a sibling package, or null.
|
|
319
|
+
*
|
|
320
|
+
* Borrowing the ORM or auth is unambiguous — one database and one auth instance
|
|
321
|
+
* serve the whole workspace. Borrowing the *framework* is not: route files are
|
|
322
|
+
* generated in that framework's shape and written into the directory `init` ran
|
|
323
|
+
* in, so if the framework lives next door the routes probably belong next door
|
|
324
|
+
* too. Writing TanStack `src/routes/` files into a Hono app is not a build
|
|
325
|
+
* error — the route generator simply never sees them and the endpoints 404.
|
|
326
|
+
*/
|
|
327
|
+
function borrowedFrameworkWarning(framework, source) {
|
|
328
|
+
if (!source) return null;
|
|
329
|
+
return ` ⚠️ ${FRAMEWORK_LABELS[framework]} was detected in "${source}", not here.\n Route files are generated in that framework's shape. If this app
|
|
330
|
+
does not serve them, run init inside "${source}" instead, or pass\n --framework to say what this app actually is.
|
|
331
|
+
`;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Extra guidance for a detection failure inside a workspace, or "" outside one.
|
|
335
|
+
*
|
|
336
|
+
* Detection has already scanned every sibling by the time either failure fires,
|
|
337
|
+
* so the marker really is absent everywhere — which is worth saying, because
|
|
338
|
+
* the bare message sends a developer looking for a dependency that was never
|
|
339
|
+
* missing. Returning "" for the single-package case keeps the caller
|
|
340
|
+
* branch-free.
|
|
341
|
+
*/
|
|
342
|
+
function workspaceHint(kind, packageCount) {
|
|
343
|
+
if (packageCount === 0) return "";
|
|
344
|
+
const scanned = `\nScanned ${packageCount} workspace package(s) and found no ${kind === "orm" ? "ORM" : "framework"} marker`;
|
|
345
|
+
return kind === "orm" ? `${scanned}.\nIf your schema is Drizzle, point at it and init will take your word:\n\n clivly init --schema <path-or-package>\n` : `${scanned}\nin any of them. If this app is served by one of the frameworks above,\nname it outright:\n\n clivly init --framework tanstack-start\n`;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Resolve one signal, preferring the app's own dependencies.
|
|
349
|
+
*
|
|
350
|
+
* The app's package.json is authoritative: a sibling on a different framework
|
|
351
|
+
* must never redefine what this app is. Only when the app itself is silent do
|
|
352
|
+
* we look outward, and then the first package by name wins.
|
|
353
|
+
*
|
|
354
|
+
* Sorting here rather than trusting the caller matters: the real package list
|
|
355
|
+
* comes from glob expansion, whose order is not guaranteed, so an unsorted scan
|
|
356
|
+
* would let the same repo detect differently between runs and report a
|
|
357
|
+
* different package as the source.
|
|
358
|
+
*/
|
|
359
|
+
function resolveSignal(appDeps, packages, detect, unknown) {
|
|
360
|
+
const own = detect(appDeps);
|
|
361
|
+
if (own !== unknown) return {
|
|
362
|
+
source: null,
|
|
363
|
+
value: own
|
|
364
|
+
};
|
|
365
|
+
const ordered = [...packages].sort((a, b) => a.name.localeCompare(b.name));
|
|
366
|
+
for (const pkg of ordered) {
|
|
367
|
+
const found = detect(pkg.deps);
|
|
368
|
+
if (found !== unknown) return {
|
|
369
|
+
source: pkg.name,
|
|
370
|
+
value: found
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
return {
|
|
374
|
+
source: null,
|
|
375
|
+
value: unknown
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Detect framework, ORM and auth across a whole workspace.
|
|
380
|
+
*
|
|
381
|
+
* A monorepo routinely splits these across packages — framework in the frontend
|
|
382
|
+
* app, ORM in a `db` package, auth in an `auth` package — so reading only the
|
|
383
|
+
* directory `init` was run from finds at most one of the three and fails
|
|
384
|
+
* whichever way the developer turns. Widening the search to sibling packages is
|
|
385
|
+
* what makes those layouts installable.
|
|
386
|
+
*
|
|
387
|
+
* Detection is deliberately a union rather than a per-package view: `init`
|
|
388
|
+
* generates one config for one app, and that app reaches its database and auth
|
|
389
|
+
* through workspace dependencies whether or not they are named in its own
|
|
390
|
+
* manifest.
|
|
391
|
+
*
|
|
392
|
+
* `packages` may include the app itself; that is harmless, since the app's own
|
|
393
|
+
* deps are consulted first anyway. Pass `[]` outside a workspace, which reduces
|
|
394
|
+
* this to a plain single-package read.
|
|
395
|
+
*/
|
|
396
|
+
function detectAcrossWorkspace(appDeps, packages) {
|
|
397
|
+
const framework = resolveSignal(appDeps, packages, detectFramework, "unknown");
|
|
398
|
+
const orm = resolveSignal(appDeps, packages, detectOrm, "unknown");
|
|
399
|
+
const auth = resolveSignal(appDeps, packages, detectAuth, "unknown");
|
|
400
|
+
const sources = {
|
|
401
|
+
auth: auth.source,
|
|
402
|
+
framework: framework.source,
|
|
403
|
+
orm: orm.source
|
|
404
|
+
};
|
|
405
|
+
return {
|
|
406
|
+
auth: auth.value,
|
|
407
|
+
borrowed: Object.values(sources).some((source) => source !== null),
|
|
408
|
+
framework: framework.value,
|
|
409
|
+
orm: orm.value,
|
|
410
|
+
sources
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
//#endregion
|
|
296
414
|
//#region src/tick-route.ts
|
|
297
415
|
/**
|
|
298
416
|
* The tick route's public URL path, and the template `clivly login` writes for
|
|
@@ -1258,10 +1376,12 @@ Usage:
|
|
|
1258
1376
|
clivly init [--force] [--dry-run] [--diff] [--yes] [--no-login] [--no-browser]
|
|
1259
1377
|
[--schema <path|package>] [--db-import <specifier>]
|
|
1260
1378
|
[--auth-import <specifier>] [--contact-table <table>]
|
|
1261
|
-
[--company-table <table>]
|
|
1379
|
+
[--company-table <table>] [--framework <name>]
|
|
1262
1380
|
|
|
1263
1381
|
Scaffolds clivly.config.ts, the Clivly route files, and .env placeholders.
|
|
1264
1382
|
Detects your framework, ORM, auth adapter and app layout from the project.
|
|
1383
|
+
In a workspace the whole package graph is scanned, so a monorepo that keeps
|
|
1384
|
+
its framework, ORM and auth in separate packages is detected from any app.
|
|
1265
1385
|
|
|
1266
1386
|
Options:
|
|
1267
1387
|
--dry-run Show what would be written; write nothing
|
|
@@ -1269,7 +1389,8 @@ Options:
|
|
|
1269
1389
|
--force Overwrite files Clivly owns
|
|
1270
1390
|
--yes Skip prompts
|
|
1271
1391
|
--no-login Skip browser pairing
|
|
1272
|
-
--
|
|
1392
|
+
--framework <n> nextjs | tanstack-start | sveltekit | remix | nuxt
|
|
1393
|
+
--schema <p> Path or package holding your ORM schema (implies Drizzle)
|
|
1273
1394
|
--db-import <s> Module specifier exporting your database instance
|
|
1274
1395
|
--auth-import <s> Module specifier exporting your auth instance
|
|
1275
1396
|
--contact-table <t> Table to map as contacts
|
|
@@ -1701,13 +1822,52 @@ function toScoredTable(table) {
|
|
|
1701
1822
|
function columnNames(table) {
|
|
1702
1823
|
return table.columns.map((column) => column.dbName);
|
|
1703
1824
|
}
|
|
1704
|
-
|
|
1825
|
+
/**
|
|
1826
|
+
* Company candidates the vocabulary cannot name, promoted on foreign-key
|
|
1827
|
+
* evidence.
|
|
1828
|
+
*
|
|
1829
|
+
* The company vocabulary is seven generic business words (companies,
|
|
1830
|
+
* organizations, teams, accounts…). A domain word for the same idea —
|
|
1831
|
+
* `campuses`, `branches`, `sites`, `practices`, `chapters` — matches none of
|
|
1832
|
+
* them, so such a table was never a candidate at all and no amount of ranking
|
|
1833
|
+
* could reach it. On `vue-academy` that left Better Auth's `organization` as
|
|
1834
|
+
* the only company candidate, and init duly scaffolded the auth table while
|
|
1835
|
+
* `campuses`, which every student row references, was invisible.
|
|
1836
|
+
*
|
|
1837
|
+
* Guessing more words does not fix this; the next unlisted word fails
|
|
1838
|
+
* identically. The schema already states the answer: when the CONTACT table
|
|
1839
|
+
* holds a real foreign key to some table, that target is what a contact
|
|
1840
|
+
* belongs to. `references` is reflected metadata, so this is a fact rather
|
|
1841
|
+
* than a name guess.
|
|
1842
|
+
*
|
|
1843
|
+
* Two guards keep it honest. The target must pass `hasDefiningColumn`, the
|
|
1844
|
+
* same "can this table BE a company" test every other candidate passes — a
|
|
1845
|
+
* join table with no `name` is not promoted. And framework tables are skipped,
|
|
1846
|
+
* because `contacts.organization_id → organization` is exactly the auth
|
|
1847
|
+
* plumbing this is meant to see past.
|
|
1848
|
+
*/
|
|
1849
|
+
function fkPromotedCompanies(tables, contact) {
|
|
1850
|
+
if (!contact) return [];
|
|
1851
|
+
const targets = /* @__PURE__ */ new Set();
|
|
1852
|
+
for (const column of contact.columns) {
|
|
1853
|
+
const table = column.references?.table;
|
|
1854
|
+
if (table && !require_internal_entity_heuristics.isFrameworkTable(table)) targets.add(table.toLowerCase());
|
|
1855
|
+
}
|
|
1856
|
+
return tables.filter((t) => targets.has(t.tableName.toLowerCase()) && t.tableName !== contact.tableName && require_internal_entity_heuristics.hasDefiningColumn("company", columnNames(t)));
|
|
1857
|
+
}
|
|
1858
|
+
function rankCandidates$1(tables, concept, contact = null) {
|
|
1859
|
+
const promoted = concept === "company" ? fkPromotedCompanies(tables, contact) : [];
|
|
1860
|
+
const promotedNames = new Set(promoted.map((t) => t.tableName));
|
|
1705
1861
|
return tables.map((t) => ({
|
|
1706
1862
|
t,
|
|
1707
|
-
m: require_internal_entity_heuristics.matchesConcept(t.tableName, concept)
|
|
1863
|
+
m: require_internal_entity_heuristics.matchesConcept(t.tableName, concept) ?? (promotedNames.has(t.tableName) ? { exact: false } : null)
|
|
1708
1864
|
})).filter((c) => Boolean(c.m)).sort((a, b) => {
|
|
1709
1865
|
const usable = Number(require_internal_entity_heuristics.hasDefiningColumn(concept, columnNames(b.t))) - Number(require_internal_entity_heuristics.hasDefiningColumn(concept, columnNames(a.t)));
|
|
1710
1866
|
if (usable !== 0) return usable;
|
|
1867
|
+
const aPromoted = promotedNames.has(a.t.tableName);
|
|
1868
|
+
if (aPromoted !== promotedNames.has(b.t.tableName)) {
|
|
1869
|
+
if (require_internal_entity_heuristics.isFrameworkTable((aPromoted ? b.t : a.t).tableName)) return aPromoted ? -1 : 1;
|
|
1870
|
+
}
|
|
1711
1871
|
const exact = Number(b.m.exact) - Number(a.m.exact);
|
|
1712
1872
|
if (exact !== 0) return exact;
|
|
1713
1873
|
return Number(require_internal_entity_heuristics.isFrameworkTable(a.t.tableName)) - Number(require_internal_entity_heuristics.isFrameworkTable(b.t.tableName));
|
|
@@ -1715,7 +1875,7 @@ function rankCandidates$1(tables, concept) {
|
|
|
1715
1875
|
}
|
|
1716
1876
|
function selectEntities(tables) {
|
|
1717
1877
|
const contactCands = rankCandidates$1(tables, "contact");
|
|
1718
|
-
const companyCands = rankCandidates$1(tables, "company");
|
|
1878
|
+
const companyCands = rankCandidates$1(tables, "company", contactCands[0] ?? null);
|
|
1719
1879
|
const contactNames = new Set(contactCands.map((t) => t.tableName));
|
|
1720
1880
|
const companyNames = new Set(companyCands.map((t) => t.tableName));
|
|
1721
1881
|
const contactExclusive = contactCands.filter((t) => !companyNames.has(t.tableName));
|
|
@@ -3962,6 +4122,18 @@ function resolveInitDeps(options) {
|
|
|
3962
4122
|
authWiring: renderAuthWiring("unknown")
|
|
3963
4123
|
};
|
|
3964
4124
|
}
|
|
4125
|
+
/**
|
|
4126
|
+
* The three detection lines, annotating any signal borrowed from a sibling.
|
|
4127
|
+
*
|
|
4128
|
+
* Naming the source package is what makes a workspace detection auditable:
|
|
4129
|
+
* without it the banner claims an ORM the developer cannot find in the
|
|
4130
|
+
* directory they are standing in.
|
|
4131
|
+
*/
|
|
4132
|
+
function renderDetectionLines(detection) {
|
|
4133
|
+
const from = (source) => source ? ` (from ${source})` : "";
|
|
4134
|
+
const frameworkNote = detection.frameworkForced ? " (--framework)" : from(detection.sources.framework);
|
|
4135
|
+
return ` Framework: ${FRAMEWORK_LABELS[detection.framework]}${frameworkNote}\n ORM: ${detection.orm}${from(detection.sources.orm)}\n Auth: ${detection.auth}${from(detection.sources.auth)}${detection.authWired ? " (wired)" : ""}\n`;
|
|
4136
|
+
}
|
|
3965
4137
|
async function acquireSchema(options, deps) {
|
|
3966
4138
|
const { out } = deps;
|
|
3967
4139
|
const appDeps = deps.readDepsFn(options.cwd);
|
|
@@ -3969,26 +4141,39 @@ async function acquireSchema(options, deps) {
|
|
|
3969
4141
|
out(`No package.json found in ${options.cwd}. Run \`clivly init\` from inside an existing app — cd into your project first.\n`);
|
|
3970
4142
|
return { code: 1 };
|
|
3971
4143
|
}
|
|
3972
|
-
const
|
|
3973
|
-
|
|
3974
|
-
|
|
4144
|
+
const workspacePackages = (deps.graphFor(options.cwd)?.packages ?? []).map((pkg) => ({
|
|
4145
|
+
deps: deps.readDepsFn(pkg.dir),
|
|
4146
|
+
name: pkg.name
|
|
4147
|
+
}));
|
|
4148
|
+
const detected = detectAcrossWorkspace(appDeps, workspacePackages);
|
|
4149
|
+
const framework = options.frameworkFlag ?? detected.framework;
|
|
4150
|
+
const { auth, orm, sources } = detected;
|
|
3975
4151
|
deps.framework = framework;
|
|
3976
4152
|
const layout = deps.resolveAppLayout(options.cwd, deps.fileExists);
|
|
3977
4153
|
deps.layout = layout;
|
|
3978
4154
|
out("\nClivly init\n");
|
|
3979
4155
|
deps.authWiring = renderAuthWiring(auth, auth === "better-auth" ? authInstanceFromFlag(options.authImportFlag) ?? await deps.probeAuth(options.cwd) : null);
|
|
3980
|
-
out(
|
|
3981
|
-
|
|
3982
|
-
|
|
4156
|
+
out(renderDetectionLines({
|
|
4157
|
+
auth,
|
|
4158
|
+
authWired: deps.authWiring.complete,
|
|
4159
|
+
framework,
|
|
4160
|
+
frameworkForced: options.frameworkFlag !== void 0,
|
|
4161
|
+
orm,
|
|
4162
|
+
sources
|
|
4163
|
+
}));
|
|
3983
4164
|
if (framework === "nextjs") out(` Layout: ${layout}\n`);
|
|
4165
|
+
const frameworkCaution = options.frameworkFlag ? null : borrowedFrameworkWarning(framework, sources.framework);
|
|
4166
|
+
if (frameworkCaution) out(`\n${frameworkCaution}`);
|
|
3984
4167
|
if (auth === "better-auth" && !deps.authWiring.complete) out("\n ⚠️ Could not locate your better-auth instance.\n The verify route will import a placeholder path that does not\n resolve. Point it at the real module with:\n\n clivly init --force --auth-import <specifier>\n\n Accepts an alias ($lib/server/auth, @/lib/auth), a package name,\n or a project-relative path (src/lib/server/auth).\n");
|
|
3985
4168
|
out("\n");
|
|
3986
|
-
if (orm !== "drizzle") {
|
|
4169
|
+
if (orm !== "drizzle" && !options.schemaFlag) {
|
|
3987
4170
|
out(`clivly init currently generates config for Drizzle only. Detected ORM: ${orm}\n`);
|
|
4171
|
+
out(workspaceHint("orm", workspacePackages.length));
|
|
3988
4172
|
return { code: 1 };
|
|
3989
4173
|
}
|
|
3990
4174
|
if (framework === "unknown") {
|
|
3991
4175
|
out("clivly init doesn't recognise your framework yet (looked for Next.js, TanStack Start, SvelteKit, Remix, Nuxt). Route file generation needs one of these.\n");
|
|
4176
|
+
out(workspaceHint("framework", workspacePackages.length));
|
|
3992
4177
|
if ("vue" in appDeps) out("\nUsing Vue? Clivly supports Vue through Nuxt. A Vite SPA has no server\nto mount the sync route on, so the route files need Nuxt's server\ndirectory (or any of the frameworks above).\n");
|
|
3993
4178
|
return { code: 1 };
|
|
3994
4179
|
}
|
|
@@ -4416,6 +4601,13 @@ function defaultLoginDeps() {
|
|
|
4416
4601
|
};
|
|
4417
4602
|
}
|
|
4418
4603
|
function runInitFromArgv(argv, cwd) {
|
|
4604
|
+
let frameworkFlag;
|
|
4605
|
+
try {
|
|
4606
|
+
frameworkFlag = parseFrameworkFlag(flagValue(argv, "--framework"));
|
|
4607
|
+
} catch (error) {
|
|
4608
|
+
process.stdout.write(`${error.message}\n`);
|
|
4609
|
+
return Promise.resolve(1);
|
|
4610
|
+
}
|
|
4419
4611
|
return runInit({
|
|
4420
4612
|
cwd,
|
|
4421
4613
|
force: argv.includes("--force"),
|
|
@@ -4425,6 +4617,7 @@ function runInitFromArgv(argv, cwd) {
|
|
|
4425
4617
|
noLogin: argv.includes("--no-login"),
|
|
4426
4618
|
openBrowser: !argv.includes("--no-browser"),
|
|
4427
4619
|
schemaFlag: flagValue(argv, "--schema"),
|
|
4620
|
+
frameworkFlag,
|
|
4428
4621
|
dbImportFlag: flagValue(argv, "--db-import"),
|
|
4429
4622
|
authImportFlag: flagValue(argv, "--auth-import"),
|
|
4430
4623
|
contactTableFlag: flagValue(argv, "--contact-table"),
|
package/index.d.cts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
//#region src/detect.d.ts
|
|
2
|
+
type Framework = "nextjs" | "tanstack-start" | "sveltekit" | "remix" | "nuxt" | "unknown";
|
|
2
3
|
type PackageManager = "npm" | "pnpm" | "yarn" | "bun";
|
|
3
4
|
//#endregion
|
|
4
5
|
//#region src/app-layout.d.ts
|
|
@@ -84,6 +85,7 @@ interface RunInitOptions {
|
|
|
84
85
|
dryRun?: boolean;
|
|
85
86
|
fileExists?: (path: string) => boolean;
|
|
86
87
|
force: boolean;
|
|
88
|
+
frameworkFlag?: Framework;
|
|
87
89
|
installPackages?: (options: {
|
|
88
90
|
cwd: string;
|
|
89
91
|
packages: string[];
|
package/index.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
//#region src/detect.d.ts
|
|
2
|
+
type Framework = "nextjs" | "tanstack-start" | "sveltekit" | "remix" | "nuxt" | "unknown";
|
|
2
3
|
type PackageManager = "npm" | "pnpm" | "yarn" | "bun";
|
|
3
4
|
//#endregion
|
|
4
5
|
//#region src/app-layout.d.ts
|
|
@@ -84,6 +85,7 @@ interface RunInitOptions {
|
|
|
84
85
|
dryRun?: boolean;
|
|
85
86
|
fileExists?: (path: string) => boolean;
|
|
86
87
|
force: boolean;
|
|
88
|
+
frameworkFlag?: Framework;
|
|
87
89
|
installPackages?: (options: {
|
|
88
90
|
cwd: string;
|
|
89
91
|
packages: string[];
|
package/index.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { t as CANONICAL_FIELDS } from "./entity-config-bC3DX9f1.mjs";
|
|
3
3
|
import "./mapping-score-C68uAqGB.mjs";
|
|
4
4
|
import { hasDefiningColumn, isFrameworkTable, matchesConcept, requiredFieldsFor, scoreFieldMap, scoreRelationships, suggestCursorField, toFieldMap } from "./internal-entity-heuristics.mjs";
|
|
5
|
-
import { s as isPlaceholderUrl, t as createClivlySDK } from "./sdk-
|
|
5
|
+
import { s as isPlaceholderUrl, t as createClivlySDK } from "./sdk-DofHordq.mjs";
|
|
6
6
|
import { createRequire } from "node:module";
|
|
7
7
|
import { spawn } from "node:child_process";
|
|
8
8
|
import { accessSync, constants, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, watch, writeFileSync } from "node:fs";
|
|
@@ -292,6 +292,124 @@ function addCommand(pm, packages) {
|
|
|
292
292
|
};
|
|
293
293
|
}
|
|
294
294
|
//#endregion
|
|
295
|
+
//#region src/detect-workspace.ts
|
|
296
|
+
const SELECTABLE_FRAMEWORKS = [
|
|
297
|
+
"nextjs",
|
|
298
|
+
"tanstack-start",
|
|
299
|
+
"sveltekit",
|
|
300
|
+
"remix",
|
|
301
|
+
"nuxt"
|
|
302
|
+
];
|
|
303
|
+
/**
|
|
304
|
+
* Validate a `--framework` value.
|
|
305
|
+
*
|
|
306
|
+
* Rejecting a typo matters more than accepting one leniently: an unrecognised
|
|
307
|
+
* value silently falling back to detection would scaffold route files in the
|
|
308
|
+
* wrong shape, and a route in the wrong shape is not a build error — the
|
|
309
|
+
* generator simply skips it and the endpoint 404s at runtime.
|
|
310
|
+
*/
|
|
311
|
+
function parseFrameworkFlag(value) {
|
|
312
|
+
if (value === void 0) return;
|
|
313
|
+
if (SELECTABLE_FRAMEWORKS.includes(value)) return value;
|
|
314
|
+
throw new Error(`"${value}" is not a supported framework. Accepted: ${SELECTABLE_FRAMEWORKS.join(", ")}.`);
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* A caution for when the framework came from a sibling package, or null.
|
|
318
|
+
*
|
|
319
|
+
* Borrowing the ORM or auth is unambiguous — one database and one auth instance
|
|
320
|
+
* serve the whole workspace. Borrowing the *framework* is not: route files are
|
|
321
|
+
* generated in that framework's shape and written into the directory `init` ran
|
|
322
|
+
* in, so if the framework lives next door the routes probably belong next door
|
|
323
|
+
* too. Writing TanStack `src/routes/` files into a Hono app is not a build
|
|
324
|
+
* error — the route generator simply never sees them and the endpoints 404.
|
|
325
|
+
*/
|
|
326
|
+
function borrowedFrameworkWarning(framework, source) {
|
|
327
|
+
if (!source) return null;
|
|
328
|
+
return ` ⚠️ ${FRAMEWORK_LABELS[framework]} was detected in "${source}", not here.\n Route files are generated in that framework's shape. If this app
|
|
329
|
+
does not serve them, run init inside "${source}" instead, or pass\n --framework to say what this app actually is.
|
|
330
|
+
`;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Extra guidance for a detection failure inside a workspace, or "" outside one.
|
|
334
|
+
*
|
|
335
|
+
* Detection has already scanned every sibling by the time either failure fires,
|
|
336
|
+
* so the marker really is absent everywhere — which is worth saying, because
|
|
337
|
+
* the bare message sends a developer looking for a dependency that was never
|
|
338
|
+
* missing. Returning "" for the single-package case keeps the caller
|
|
339
|
+
* branch-free.
|
|
340
|
+
*/
|
|
341
|
+
function workspaceHint(kind, packageCount) {
|
|
342
|
+
if (packageCount === 0) return "";
|
|
343
|
+
const scanned = `\nScanned ${packageCount} workspace package(s) and found no ${kind === "orm" ? "ORM" : "framework"} marker`;
|
|
344
|
+
return kind === "orm" ? `${scanned}.\nIf your schema is Drizzle, point at it and init will take your word:\n\n clivly init --schema <path-or-package>\n` : `${scanned}\nin any of them. If this app is served by one of the frameworks above,\nname it outright:\n\n clivly init --framework tanstack-start\n`;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Resolve one signal, preferring the app's own dependencies.
|
|
348
|
+
*
|
|
349
|
+
* The app's package.json is authoritative: a sibling on a different framework
|
|
350
|
+
* must never redefine what this app is. Only when the app itself is silent do
|
|
351
|
+
* we look outward, and then the first package by name wins.
|
|
352
|
+
*
|
|
353
|
+
* Sorting here rather than trusting the caller matters: the real package list
|
|
354
|
+
* comes from glob expansion, whose order is not guaranteed, so an unsorted scan
|
|
355
|
+
* would let the same repo detect differently between runs and report a
|
|
356
|
+
* different package as the source.
|
|
357
|
+
*/
|
|
358
|
+
function resolveSignal(appDeps, packages, detect, unknown) {
|
|
359
|
+
const own = detect(appDeps);
|
|
360
|
+
if (own !== unknown) return {
|
|
361
|
+
source: null,
|
|
362
|
+
value: own
|
|
363
|
+
};
|
|
364
|
+
const ordered = [...packages].sort((a, b) => a.name.localeCompare(b.name));
|
|
365
|
+
for (const pkg of ordered) {
|
|
366
|
+
const found = detect(pkg.deps);
|
|
367
|
+
if (found !== unknown) return {
|
|
368
|
+
source: pkg.name,
|
|
369
|
+
value: found
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
return {
|
|
373
|
+
source: null,
|
|
374
|
+
value: unknown
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Detect framework, ORM and auth across a whole workspace.
|
|
379
|
+
*
|
|
380
|
+
* A monorepo routinely splits these across packages — framework in the frontend
|
|
381
|
+
* app, ORM in a `db` package, auth in an `auth` package — so reading only the
|
|
382
|
+
* directory `init` was run from finds at most one of the three and fails
|
|
383
|
+
* whichever way the developer turns. Widening the search to sibling packages is
|
|
384
|
+
* what makes those layouts installable.
|
|
385
|
+
*
|
|
386
|
+
* Detection is deliberately a union rather than a per-package view: `init`
|
|
387
|
+
* generates one config for one app, and that app reaches its database and auth
|
|
388
|
+
* through workspace dependencies whether or not they are named in its own
|
|
389
|
+
* manifest.
|
|
390
|
+
*
|
|
391
|
+
* `packages` may include the app itself; that is harmless, since the app's own
|
|
392
|
+
* deps are consulted first anyway. Pass `[]` outside a workspace, which reduces
|
|
393
|
+
* this to a plain single-package read.
|
|
394
|
+
*/
|
|
395
|
+
function detectAcrossWorkspace(appDeps, packages) {
|
|
396
|
+
const framework = resolveSignal(appDeps, packages, detectFramework, "unknown");
|
|
397
|
+
const orm = resolveSignal(appDeps, packages, detectOrm, "unknown");
|
|
398
|
+
const auth = resolveSignal(appDeps, packages, detectAuth, "unknown");
|
|
399
|
+
const sources = {
|
|
400
|
+
auth: auth.source,
|
|
401
|
+
framework: framework.source,
|
|
402
|
+
orm: orm.source
|
|
403
|
+
};
|
|
404
|
+
return {
|
|
405
|
+
auth: auth.value,
|
|
406
|
+
borrowed: Object.values(sources).some((source) => source !== null),
|
|
407
|
+
framework: framework.value,
|
|
408
|
+
orm: orm.value,
|
|
409
|
+
sources
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
//#endregion
|
|
295
413
|
//#region src/tick-route.ts
|
|
296
414
|
/**
|
|
297
415
|
* The tick route's public URL path, and the template `clivly login` writes for
|
|
@@ -1257,10 +1375,12 @@ Usage:
|
|
|
1257
1375
|
clivly init [--force] [--dry-run] [--diff] [--yes] [--no-login] [--no-browser]
|
|
1258
1376
|
[--schema <path|package>] [--db-import <specifier>]
|
|
1259
1377
|
[--auth-import <specifier>] [--contact-table <table>]
|
|
1260
|
-
[--company-table <table>]
|
|
1378
|
+
[--company-table <table>] [--framework <name>]
|
|
1261
1379
|
|
|
1262
1380
|
Scaffolds clivly.config.ts, the Clivly route files, and .env placeholders.
|
|
1263
1381
|
Detects your framework, ORM, auth adapter and app layout from the project.
|
|
1382
|
+
In a workspace the whole package graph is scanned, so a monorepo that keeps
|
|
1383
|
+
its framework, ORM and auth in separate packages is detected from any app.
|
|
1264
1384
|
|
|
1265
1385
|
Options:
|
|
1266
1386
|
--dry-run Show what would be written; write nothing
|
|
@@ -1268,7 +1388,8 @@ Options:
|
|
|
1268
1388
|
--force Overwrite files Clivly owns
|
|
1269
1389
|
--yes Skip prompts
|
|
1270
1390
|
--no-login Skip browser pairing
|
|
1271
|
-
--
|
|
1391
|
+
--framework <n> nextjs | tanstack-start | sveltekit | remix | nuxt
|
|
1392
|
+
--schema <p> Path or package holding your ORM schema (implies Drizzle)
|
|
1272
1393
|
--db-import <s> Module specifier exporting your database instance
|
|
1273
1394
|
--auth-import <s> Module specifier exporting your auth instance
|
|
1274
1395
|
--contact-table <t> Table to map as contacts
|
|
@@ -1700,13 +1821,52 @@ function toScoredTable(table) {
|
|
|
1700
1821
|
function columnNames(table) {
|
|
1701
1822
|
return table.columns.map((column) => column.dbName);
|
|
1702
1823
|
}
|
|
1703
|
-
|
|
1824
|
+
/**
|
|
1825
|
+
* Company candidates the vocabulary cannot name, promoted on foreign-key
|
|
1826
|
+
* evidence.
|
|
1827
|
+
*
|
|
1828
|
+
* The company vocabulary is seven generic business words (companies,
|
|
1829
|
+
* organizations, teams, accounts…). A domain word for the same idea —
|
|
1830
|
+
* `campuses`, `branches`, `sites`, `practices`, `chapters` — matches none of
|
|
1831
|
+
* them, so such a table was never a candidate at all and no amount of ranking
|
|
1832
|
+
* could reach it. On `vue-academy` that left Better Auth's `organization` as
|
|
1833
|
+
* the only company candidate, and init duly scaffolded the auth table while
|
|
1834
|
+
* `campuses`, which every student row references, was invisible.
|
|
1835
|
+
*
|
|
1836
|
+
* Guessing more words does not fix this; the next unlisted word fails
|
|
1837
|
+
* identically. The schema already states the answer: when the CONTACT table
|
|
1838
|
+
* holds a real foreign key to some table, that target is what a contact
|
|
1839
|
+
* belongs to. `references` is reflected metadata, so this is a fact rather
|
|
1840
|
+
* than a name guess.
|
|
1841
|
+
*
|
|
1842
|
+
* Two guards keep it honest. The target must pass `hasDefiningColumn`, the
|
|
1843
|
+
* same "can this table BE a company" test every other candidate passes — a
|
|
1844
|
+
* join table with no `name` is not promoted. And framework tables are skipped,
|
|
1845
|
+
* because `contacts.organization_id → organization` is exactly the auth
|
|
1846
|
+
* plumbing this is meant to see past.
|
|
1847
|
+
*/
|
|
1848
|
+
function fkPromotedCompanies(tables, contact) {
|
|
1849
|
+
if (!contact) return [];
|
|
1850
|
+
const targets = /* @__PURE__ */ new Set();
|
|
1851
|
+
for (const column of contact.columns) {
|
|
1852
|
+
const table = column.references?.table;
|
|
1853
|
+
if (table && !isFrameworkTable(table)) targets.add(table.toLowerCase());
|
|
1854
|
+
}
|
|
1855
|
+
return tables.filter((t) => targets.has(t.tableName.toLowerCase()) && t.tableName !== contact.tableName && hasDefiningColumn("company", columnNames(t)));
|
|
1856
|
+
}
|
|
1857
|
+
function rankCandidates$1(tables, concept, contact = null) {
|
|
1858
|
+
const promoted = concept === "company" ? fkPromotedCompanies(tables, contact) : [];
|
|
1859
|
+
const promotedNames = new Set(promoted.map((t) => t.tableName));
|
|
1704
1860
|
return tables.map((t) => ({
|
|
1705
1861
|
t,
|
|
1706
|
-
m: matchesConcept(t.tableName, concept)
|
|
1862
|
+
m: matchesConcept(t.tableName, concept) ?? (promotedNames.has(t.tableName) ? { exact: false } : null)
|
|
1707
1863
|
})).filter((c) => Boolean(c.m)).sort((a, b) => {
|
|
1708
1864
|
const usable = Number(hasDefiningColumn(concept, columnNames(b.t))) - Number(hasDefiningColumn(concept, columnNames(a.t)));
|
|
1709
1865
|
if (usable !== 0) return usable;
|
|
1866
|
+
const aPromoted = promotedNames.has(a.t.tableName);
|
|
1867
|
+
if (aPromoted !== promotedNames.has(b.t.tableName)) {
|
|
1868
|
+
if (isFrameworkTable((aPromoted ? b.t : a.t).tableName)) return aPromoted ? -1 : 1;
|
|
1869
|
+
}
|
|
1710
1870
|
const exact = Number(b.m.exact) - Number(a.m.exact);
|
|
1711
1871
|
if (exact !== 0) return exact;
|
|
1712
1872
|
return Number(isFrameworkTable(a.t.tableName)) - Number(isFrameworkTable(b.t.tableName));
|
|
@@ -1714,7 +1874,7 @@ function rankCandidates$1(tables, concept) {
|
|
|
1714
1874
|
}
|
|
1715
1875
|
function selectEntities(tables) {
|
|
1716
1876
|
const contactCands = rankCandidates$1(tables, "contact");
|
|
1717
|
-
const companyCands = rankCandidates$1(tables, "company");
|
|
1877
|
+
const companyCands = rankCandidates$1(tables, "company", contactCands[0] ?? null);
|
|
1718
1878
|
const contactNames = new Set(contactCands.map((t) => t.tableName));
|
|
1719
1879
|
const companyNames = new Set(companyCands.map((t) => t.tableName));
|
|
1720
1880
|
const contactExclusive = contactCands.filter((t) => !companyNames.has(t.tableName));
|
|
@@ -3961,6 +4121,18 @@ function resolveInitDeps(options) {
|
|
|
3961
4121
|
authWiring: renderAuthWiring("unknown")
|
|
3962
4122
|
};
|
|
3963
4123
|
}
|
|
4124
|
+
/**
|
|
4125
|
+
* The three detection lines, annotating any signal borrowed from a sibling.
|
|
4126
|
+
*
|
|
4127
|
+
* Naming the source package is what makes a workspace detection auditable:
|
|
4128
|
+
* without it the banner claims an ORM the developer cannot find in the
|
|
4129
|
+
* directory they are standing in.
|
|
4130
|
+
*/
|
|
4131
|
+
function renderDetectionLines(detection) {
|
|
4132
|
+
const from = (source) => source ? ` (from ${source})` : "";
|
|
4133
|
+
const frameworkNote = detection.frameworkForced ? " (--framework)" : from(detection.sources.framework);
|
|
4134
|
+
return ` Framework: ${FRAMEWORK_LABELS[detection.framework]}${frameworkNote}\n ORM: ${detection.orm}${from(detection.sources.orm)}\n Auth: ${detection.auth}${from(detection.sources.auth)}${detection.authWired ? " (wired)" : ""}\n`;
|
|
4135
|
+
}
|
|
3964
4136
|
async function acquireSchema(options, deps) {
|
|
3965
4137
|
const { out } = deps;
|
|
3966
4138
|
const appDeps = deps.readDepsFn(options.cwd);
|
|
@@ -3968,26 +4140,39 @@ async function acquireSchema(options, deps) {
|
|
|
3968
4140
|
out(`No package.json found in ${options.cwd}. Run \`clivly init\` from inside an existing app — cd into your project first.\n`);
|
|
3969
4141
|
return { code: 1 };
|
|
3970
4142
|
}
|
|
3971
|
-
const
|
|
3972
|
-
|
|
3973
|
-
|
|
4143
|
+
const workspacePackages = (deps.graphFor(options.cwd)?.packages ?? []).map((pkg) => ({
|
|
4144
|
+
deps: deps.readDepsFn(pkg.dir),
|
|
4145
|
+
name: pkg.name
|
|
4146
|
+
}));
|
|
4147
|
+
const detected = detectAcrossWorkspace(appDeps, workspacePackages);
|
|
4148
|
+
const framework = options.frameworkFlag ?? detected.framework;
|
|
4149
|
+
const { auth, orm, sources } = detected;
|
|
3974
4150
|
deps.framework = framework;
|
|
3975
4151
|
const layout = deps.resolveAppLayout(options.cwd, deps.fileExists);
|
|
3976
4152
|
deps.layout = layout;
|
|
3977
4153
|
out("\nClivly init\n");
|
|
3978
4154
|
deps.authWiring = renderAuthWiring(auth, auth === "better-auth" ? authInstanceFromFlag(options.authImportFlag) ?? await deps.probeAuth(options.cwd) : null);
|
|
3979
|
-
out(
|
|
3980
|
-
|
|
3981
|
-
|
|
4155
|
+
out(renderDetectionLines({
|
|
4156
|
+
auth,
|
|
4157
|
+
authWired: deps.authWiring.complete,
|
|
4158
|
+
framework,
|
|
4159
|
+
frameworkForced: options.frameworkFlag !== void 0,
|
|
4160
|
+
orm,
|
|
4161
|
+
sources
|
|
4162
|
+
}));
|
|
3982
4163
|
if (framework === "nextjs") out(` Layout: ${layout}\n`);
|
|
4164
|
+
const frameworkCaution = options.frameworkFlag ? null : borrowedFrameworkWarning(framework, sources.framework);
|
|
4165
|
+
if (frameworkCaution) out(`\n${frameworkCaution}`);
|
|
3983
4166
|
if (auth === "better-auth" && !deps.authWiring.complete) out("\n ⚠️ Could not locate your better-auth instance.\n The verify route will import a placeholder path that does not\n resolve. Point it at the real module with:\n\n clivly init --force --auth-import <specifier>\n\n Accepts an alias ($lib/server/auth, @/lib/auth), a package name,\n or a project-relative path (src/lib/server/auth).\n");
|
|
3984
4167
|
out("\n");
|
|
3985
|
-
if (orm !== "drizzle") {
|
|
4168
|
+
if (orm !== "drizzle" && !options.schemaFlag) {
|
|
3986
4169
|
out(`clivly init currently generates config for Drizzle only. Detected ORM: ${orm}\n`);
|
|
4170
|
+
out(workspaceHint("orm", workspacePackages.length));
|
|
3987
4171
|
return { code: 1 };
|
|
3988
4172
|
}
|
|
3989
4173
|
if (framework === "unknown") {
|
|
3990
4174
|
out("clivly init doesn't recognise your framework yet (looked for Next.js, TanStack Start, SvelteKit, Remix, Nuxt). Route file generation needs one of these.\n");
|
|
4175
|
+
out(workspaceHint("framework", workspacePackages.length));
|
|
3991
4176
|
if ("vue" in appDeps) out("\nUsing Vue? Clivly supports Vue through Nuxt. A Vite SPA has no server\nto mount the sync route on, so the route files need Nuxt's server\ndirectory (or any of the frameworks above).\n");
|
|
3992
4177
|
return { code: 1 };
|
|
3993
4178
|
}
|
|
@@ -4415,6 +4600,13 @@ function defaultLoginDeps() {
|
|
|
4415
4600
|
};
|
|
4416
4601
|
}
|
|
4417
4602
|
function runInitFromArgv(argv, cwd) {
|
|
4603
|
+
let frameworkFlag;
|
|
4604
|
+
try {
|
|
4605
|
+
frameworkFlag = parseFrameworkFlag(flagValue(argv, "--framework"));
|
|
4606
|
+
} catch (error) {
|
|
4607
|
+
process.stdout.write(`${error.message}\n`);
|
|
4608
|
+
return Promise.resolve(1);
|
|
4609
|
+
}
|
|
4418
4610
|
return runInit({
|
|
4419
4611
|
cwd,
|
|
4420
4612
|
force: argv.includes("--force"),
|
|
@@ -4424,6 +4616,7 @@ function runInitFromArgv(argv, cwd) {
|
|
|
4424
4616
|
noLogin: argv.includes("--no-login"),
|
|
4425
4617
|
openBrowser: !argv.includes("--no-browser"),
|
|
4426
4618
|
schemaFlag: flagValue(argv, "--schema"),
|
|
4619
|
+
frameworkFlag,
|
|
4427
4620
|
dbImportFlag: flagValue(argv, "--db-import"),
|
|
4428
4621
|
authImportFlag: flagValue(argv, "--auth-import"),
|
|
4429
4622
|
contactTableFlag: flagValue(argv, "--contact-table"),
|
package/package.json
CHANGED
|
@@ -392,11 +392,15 @@ function errorMessage(error) {
|
|
|
392
392
|
/**
|
|
393
393
|
* The entities to ask about, in the exact terms the sync pushes them.
|
|
394
394
|
*
|
|
395
|
-
* `source` is the
|
|
395
|
+
* `source` is whatever the push will actually send — the value that arrives at
|
|
396
396
|
* `ingestPush` as `input.source` and is compared against `mapping.sourceTable`.
|
|
397
|
+
* Usually that is `SyncSource.entity`, but a projection pushes under its base
|
|
398
|
+
* table, so this asks `customPushSource` rather than reading `entity` directly.
|
|
399
|
+
*
|
|
397
400
|
* It is NOT the entity config's own `source` field, which names a host table;
|
|
398
|
-
* the two are routinely conflated, and using the wrong one
|
|
399
|
-
*
|
|
401
|
+
* the two are routinely conflated, and using the wrong one reports a correctly
|
|
402
|
+
* mapped workspace as unmapped — which is exactly what happened to every
|
|
403
|
+
* projection until this used the push label.
|
|
400
404
|
*/
|
|
401
405
|
function readinessEntitiesFromConfig(config) {
|
|
402
406
|
const entities = [];
|
|
@@ -405,7 +409,7 @@ function readinessEntitiesFromConfig(config) {
|
|
|
405
409
|
if (!resolved) continue;
|
|
406
410
|
const objectTypeSlug = resolved.source.objectType;
|
|
407
411
|
entities.push({
|
|
408
|
-
source: resolved.source
|
|
412
|
+
source: customPushSource(config, resolved.source),
|
|
409
413
|
concept: resolved.concept,
|
|
410
414
|
...resolved.concept === "custom" && objectTypeSlug ? { objectTypeSlug } : {}
|
|
411
415
|
});
|
|
@@ -1468,8 +1472,18 @@ function jsonResponse$1(body, status) {
|
|
|
1468
1472
|
function normalizeApiUrl(apiUrl) {
|
|
1469
1473
|
return apiUrl.replace(TRAILING_SLASH$1, "");
|
|
1470
1474
|
}
|
|
1471
|
-
|
|
1472
|
-
|
|
1475
|
+
/**
|
|
1476
|
+
* The origin *claimed* by the request, for reporting upstream.
|
|
1477
|
+
*
|
|
1478
|
+
* Falls back to the request URL so Clivly still receives an origin for a
|
|
1479
|
+
* non-browser caller. This value is reported, never trusted: only a real
|
|
1480
|
+
* `Origin` header is evidence of where a request came from, so the access
|
|
1481
|
+
* decision uses {@link trustedOrigin} instead. Keeping the two apart is the
|
|
1482
|
+
* point — conflating them let a caller that simply omitted the header be
|
|
1483
|
+
* judged against the server's own origin, which is usually in the allowlist.
|
|
1484
|
+
*/
|
|
1485
|
+
function reportedOrigin(req) {
|
|
1486
|
+
const headerOrigin = trustedOrigin(req);
|
|
1473
1487
|
if (headerOrigin) return headerOrigin;
|
|
1474
1488
|
try {
|
|
1475
1489
|
return new URL(req.url).origin;
|
|
@@ -1477,6 +1491,15 @@ function resolveOrigin(req) {
|
|
|
1477
1491
|
return null;
|
|
1478
1492
|
}
|
|
1479
1493
|
}
|
|
1494
|
+
/**
|
|
1495
|
+
* The only origin worth an access decision: one the browser set itself.
|
|
1496
|
+
*
|
|
1497
|
+
* A missing header means "not a browser request we can vouch for", which is
|
|
1498
|
+
* untrusted — never the server's own origin.
|
|
1499
|
+
*/
|
|
1500
|
+
function trustedOrigin(req) {
|
|
1501
|
+
return req.headers.get("origin");
|
|
1502
|
+
}
|
|
1480
1503
|
function isChatSessionRequestBody(value) {
|
|
1481
1504
|
if (typeof value !== "object" || value === null) return false;
|
|
1482
1505
|
const body = value;
|
|
@@ -1487,6 +1510,17 @@ function isChatSessionRequestBody(value) {
|
|
|
1487
1510
|
if (typeof visitor.email !== "string" || visitor.email.trim().length === 0) return false;
|
|
1488
1511
|
return body.visitorToken === void 0 || body.visitorToken === null || typeof body.visitorToken === "string";
|
|
1489
1512
|
}
|
|
1513
|
+
/**
|
|
1514
|
+
* Judge a request against the configured allowlist.
|
|
1515
|
+
*
|
|
1516
|
+
* `origin` must be a *trusted* origin (see {@link trustedOrigin}) — passing
|
|
1517
|
+
* the reported one would reinstate the bypass this guards against.
|
|
1518
|
+
*
|
|
1519
|
+
* An unset allowlist still admits every origin, because requiring it would
|
|
1520
|
+
* break every existing deployment on upgrade. The unset case is instead
|
|
1521
|
+
* warned about once at construction, so the permissive default is loud
|
|
1522
|
+
* rather than silent.
|
|
1523
|
+
*/
|
|
1490
1524
|
function originAllowed(origin, allowedOrigins) {
|
|
1491
1525
|
if (!allowedOrigins || allowedOrigins.length === 0) return true;
|
|
1492
1526
|
if (!origin) return false;
|
|
@@ -1494,6 +1528,7 @@ function originAllowed(origin, allowedOrigins) {
|
|
|
1494
1528
|
}
|
|
1495
1529
|
function createChatSessionHandler({ allowedOrigins, apiKey, apiUrl = DEFAULT_API_URL$1, fetchImpl = fetch, resolveUser }) {
|
|
1496
1530
|
const sessionUrl = `${normalizeApiUrl(apiUrl)}/clivly/widget/session`;
|
|
1531
|
+
if (!allowedOrigins || allowedOrigins.length === 0) console.warn("[clivly] createChatSessionHandler has no allowedOrigins; every origin can open a chat session against this workspace. Pass allowedOrigins to restrict it.");
|
|
1497
1532
|
return async (request) => {
|
|
1498
1533
|
if (request.method !== "POST") return new Response(JSON.stringify({ error: "method_not_allowed" }), {
|
|
1499
1534
|
status: 405,
|
|
@@ -1502,8 +1537,8 @@ function createChatSessionHandler({ allowedOrigins, apiKey, apiUrl = DEFAULT_API
|
|
|
1502
1537
|
allow: "POST"
|
|
1503
1538
|
}
|
|
1504
1539
|
});
|
|
1505
|
-
|
|
1506
|
-
|
|
1540
|
+
if (!originAllowed(trustedOrigin(request), allowedOrigins)) return jsonResponse$1({ error: "origin_not_allowed" }, 403);
|
|
1541
|
+
const origin = reportedOrigin(request);
|
|
1507
1542
|
let body;
|
|
1508
1543
|
try {
|
|
1509
1544
|
body = await request.json();
|
|
@@ -1701,6 +1736,18 @@ var TriggerRequestError = class extends Error {
|
|
|
1701
1736
|
function triggerErrorStatus(error) {
|
|
1702
1737
|
return error instanceof TriggerRequestError ? error.status : 500;
|
|
1703
1738
|
}
|
|
1739
|
+
const SENTENCE_END_RE = /[.!?]$/;
|
|
1740
|
+
async function readPushFailureMessage(response) {
|
|
1741
|
+
if (!response || typeof response.json !== "function") return null;
|
|
1742
|
+
try {
|
|
1743
|
+
const body = await response.json();
|
|
1744
|
+
if (typeof body !== "object" || body === null) return null;
|
|
1745
|
+
const { message } = body;
|
|
1746
|
+
return typeof message === "string" && message.trim().length > 0 ? message : null;
|
|
1747
|
+
} catch {
|
|
1748
|
+
return null;
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1704
1751
|
const SIGNATURE_TOLERANCE_SECONDS = 300;
|
|
1705
1752
|
const SIGNATURE_HEADER_RE = /^t=(\d+),v1=([0-9a-f]+)$/;
|
|
1706
1753
|
const SECONDS_PER_MS = 1e3;
|
|
@@ -1923,17 +1970,27 @@ function createClivlySDK(config) {
|
|
|
1923
1970
|
}, response);
|
|
1924
1971
|
return true;
|
|
1925
1972
|
}
|
|
1926
|
-
lastPushFailure ??= {
|
|
1973
|
+
lastPushFailure ??= {
|
|
1974
|
+
status: response?.status ?? null,
|
|
1975
|
+
message: await readPushFailureMessage(response)
|
|
1976
|
+
};
|
|
1927
1977
|
return false;
|
|
1928
1978
|
};
|
|
1929
1979
|
const describePushFailure = (entities) => {
|
|
1930
1980
|
const base = `Clivly sync: push failed for ${entities.join(", ")}`;
|
|
1931
1981
|
const status = lastPushFailure?.status ?? null;
|
|
1982
|
+
const serverMessage = lastPushFailure?.message ?? null;
|
|
1932
1983
|
let cause;
|
|
1933
1984
|
if (status === 401 || status === 403) cause = "the API key was rejected — check CLIVLY_API_KEY is set to a valid key for this org";
|
|
1934
1985
|
else if (status === null) cause = `couldn't reach Clivly at ${apiUrl} — check network access and CLIVLY_API_URL`;
|
|
1986
|
+
else if (serverMessage) cause = `Clivly returned HTTP ${status}: ${serverMessage}`;
|
|
1935
1987
|
else cause = `Clivly returned HTTP ${status}`;
|
|
1936
|
-
|
|
1988
|
+
const separator = SENTENCE_END_RE.test(cause) ? "" : ".";
|
|
1989
|
+
return `${base}: ${cause}${separator} Run \`clivly doctor\` to diagnose.`;
|
|
1990
|
+
};
|
|
1991
|
+
const pushFailureError = (message) => {
|
|
1992
|
+
const status = lastPushFailure?.status ?? null;
|
|
1993
|
+
return status !== null && status >= CLIENT_ERROR_FLOOR && status < RETRYABLE_STATUS_FLOOR ? new TriggerRequestError(status, message) : new Error(message);
|
|
1937
1994
|
};
|
|
1938
1995
|
const syncOne = async (source, path, mode, objectType, runId, batchSizeOverride, maxRowsOverride, persistCursor = true) => {
|
|
1939
1996
|
const since = mode === "full" ? null : await cursorStore.get(source.entity) ?? null;
|
|
@@ -2023,7 +2080,7 @@ function createClivlySDK(config) {
|
|
|
2023
2080
|
failed: true,
|
|
2024
2081
|
error: message
|
|
2025
2082
|
};
|
|
2026
|
-
throw
|
|
2083
|
+
throw pushFailureError(message);
|
|
2027
2084
|
}
|
|
2028
2085
|
lastPush = { failed: false };
|
|
2029
2086
|
};
|
|
@@ -2031,7 +2088,7 @@ function createClivlySDK(config) {
|
|
|
2031
2088
|
const resolved = resolveSampleSource(config, entityKey);
|
|
2032
2089
|
if (!resolved) throw new TriggerRequestError(400, `Clivly sync: no source is configured for entity "${entityKey}". The dashboard maps this table, but clivly.config declares no matching entity — add it to \`entities\`, or remove the mapping in Integrations → Mapping.`);
|
|
2033
2090
|
lastPushFailure = null;
|
|
2034
|
-
if (!await syncOne(resolved.source, resolved.path, "delta", resolved.objectType, runId, SAMPLE_BATCH_SIZE, SAMPLE_BATCH_SIZE, false)) throw
|
|
2091
|
+
if (!await syncOne(resolved.source, resolved.path, "delta", resolved.objectType, runId, SAMPLE_BATCH_SIZE, SAMPLE_BATCH_SIZE, false)) throw pushFailureError(describePushFailure([resolved.source.entity]));
|
|
2035
2092
|
};
|
|
2036
2093
|
const describeSyncTrigger = () => ({
|
|
2037
2094
|
configured: config.syncTrigger?.url,
|
|
@@ -392,11 +392,15 @@ function errorMessage(error) {
|
|
|
392
392
|
/**
|
|
393
393
|
* The entities to ask about, in the exact terms the sync pushes them.
|
|
394
394
|
*
|
|
395
|
-
* `source` is the
|
|
395
|
+
* `source` is whatever the push will actually send — the value that arrives at
|
|
396
396
|
* `ingestPush` as `input.source` and is compared against `mapping.sourceTable`.
|
|
397
|
+
* Usually that is `SyncSource.entity`, but a projection pushes under its base
|
|
398
|
+
* table, so this asks `customPushSource` rather than reading `entity` directly.
|
|
399
|
+
*
|
|
397
400
|
* It is NOT the entity config's own `source` field, which names a host table;
|
|
398
|
-
* the two are routinely conflated, and using the wrong one
|
|
399
|
-
*
|
|
401
|
+
* the two are routinely conflated, and using the wrong one reports a correctly
|
|
402
|
+
* mapped workspace as unmapped — which is exactly what happened to every
|
|
403
|
+
* projection until this used the push label.
|
|
400
404
|
*/
|
|
401
405
|
function readinessEntitiesFromConfig(config) {
|
|
402
406
|
const entities = [];
|
|
@@ -405,7 +409,7 @@ function readinessEntitiesFromConfig(config) {
|
|
|
405
409
|
if (!resolved) continue;
|
|
406
410
|
const objectTypeSlug = resolved.source.objectType;
|
|
407
411
|
entities.push({
|
|
408
|
-
source: resolved.source
|
|
412
|
+
source: customPushSource(config, resolved.source),
|
|
409
413
|
concept: resolved.concept,
|
|
410
414
|
...resolved.concept === "custom" && objectTypeSlug ? { objectTypeSlug } : {}
|
|
411
415
|
});
|
|
@@ -1468,8 +1472,18 @@ function jsonResponse$1(body, status) {
|
|
|
1468
1472
|
function normalizeApiUrl(apiUrl) {
|
|
1469
1473
|
return apiUrl.replace(TRAILING_SLASH$1, "");
|
|
1470
1474
|
}
|
|
1471
|
-
|
|
1472
|
-
|
|
1475
|
+
/**
|
|
1476
|
+
* The origin *claimed* by the request, for reporting upstream.
|
|
1477
|
+
*
|
|
1478
|
+
* Falls back to the request URL so Clivly still receives an origin for a
|
|
1479
|
+
* non-browser caller. This value is reported, never trusted: only a real
|
|
1480
|
+
* `Origin` header is evidence of where a request came from, so the access
|
|
1481
|
+
* decision uses {@link trustedOrigin} instead. Keeping the two apart is the
|
|
1482
|
+
* point — conflating them let a caller that simply omitted the header be
|
|
1483
|
+
* judged against the server's own origin, which is usually in the allowlist.
|
|
1484
|
+
*/
|
|
1485
|
+
function reportedOrigin(req) {
|
|
1486
|
+
const headerOrigin = trustedOrigin(req);
|
|
1473
1487
|
if (headerOrigin) return headerOrigin;
|
|
1474
1488
|
try {
|
|
1475
1489
|
return new URL(req.url).origin;
|
|
@@ -1477,6 +1491,15 @@ function resolveOrigin(req) {
|
|
|
1477
1491
|
return null;
|
|
1478
1492
|
}
|
|
1479
1493
|
}
|
|
1494
|
+
/**
|
|
1495
|
+
* The only origin worth an access decision: one the browser set itself.
|
|
1496
|
+
*
|
|
1497
|
+
* A missing header means "not a browser request we can vouch for", which is
|
|
1498
|
+
* untrusted — never the server's own origin.
|
|
1499
|
+
*/
|
|
1500
|
+
function trustedOrigin(req) {
|
|
1501
|
+
return req.headers.get("origin");
|
|
1502
|
+
}
|
|
1480
1503
|
function isChatSessionRequestBody(value) {
|
|
1481
1504
|
if (typeof value !== "object" || value === null) return false;
|
|
1482
1505
|
const body = value;
|
|
@@ -1487,6 +1510,17 @@ function isChatSessionRequestBody(value) {
|
|
|
1487
1510
|
if (typeof visitor.email !== "string" || visitor.email.trim().length === 0) return false;
|
|
1488
1511
|
return body.visitorToken === void 0 || body.visitorToken === null || typeof body.visitorToken === "string";
|
|
1489
1512
|
}
|
|
1513
|
+
/**
|
|
1514
|
+
* Judge a request against the configured allowlist.
|
|
1515
|
+
*
|
|
1516
|
+
* `origin` must be a *trusted* origin (see {@link trustedOrigin}) — passing
|
|
1517
|
+
* the reported one would reinstate the bypass this guards against.
|
|
1518
|
+
*
|
|
1519
|
+
* An unset allowlist still admits every origin, because requiring it would
|
|
1520
|
+
* break every existing deployment on upgrade. The unset case is instead
|
|
1521
|
+
* warned about once at construction, so the permissive default is loud
|
|
1522
|
+
* rather than silent.
|
|
1523
|
+
*/
|
|
1490
1524
|
function originAllowed(origin, allowedOrigins) {
|
|
1491
1525
|
if (!allowedOrigins || allowedOrigins.length === 0) return true;
|
|
1492
1526
|
if (!origin) return false;
|
|
@@ -1494,6 +1528,7 @@ function originAllowed(origin, allowedOrigins) {
|
|
|
1494
1528
|
}
|
|
1495
1529
|
function createChatSessionHandler({ allowedOrigins, apiKey, apiUrl = DEFAULT_API_URL$1, fetchImpl = fetch, resolveUser }) {
|
|
1496
1530
|
const sessionUrl = `${normalizeApiUrl(apiUrl)}/clivly/widget/session`;
|
|
1531
|
+
if (!allowedOrigins || allowedOrigins.length === 0) console.warn("[clivly] createChatSessionHandler has no allowedOrigins; every origin can open a chat session against this workspace. Pass allowedOrigins to restrict it.");
|
|
1497
1532
|
return async (request) => {
|
|
1498
1533
|
if (request.method !== "POST") return new Response(JSON.stringify({ error: "method_not_allowed" }), {
|
|
1499
1534
|
status: 405,
|
|
@@ -1502,8 +1537,8 @@ function createChatSessionHandler({ allowedOrigins, apiKey, apiUrl = DEFAULT_API
|
|
|
1502
1537
|
allow: "POST"
|
|
1503
1538
|
}
|
|
1504
1539
|
});
|
|
1505
|
-
|
|
1506
|
-
|
|
1540
|
+
if (!originAllowed(trustedOrigin(request), allowedOrigins)) return jsonResponse$1({ error: "origin_not_allowed" }, 403);
|
|
1541
|
+
const origin = reportedOrigin(request);
|
|
1507
1542
|
let body;
|
|
1508
1543
|
try {
|
|
1509
1544
|
body = await request.json();
|
|
@@ -1701,6 +1736,18 @@ var TriggerRequestError = class extends Error {
|
|
|
1701
1736
|
function triggerErrorStatus(error) {
|
|
1702
1737
|
return error instanceof TriggerRequestError ? error.status : 500;
|
|
1703
1738
|
}
|
|
1739
|
+
const SENTENCE_END_RE = /[.!?]$/;
|
|
1740
|
+
async function readPushFailureMessage(response) {
|
|
1741
|
+
if (!response || typeof response.json !== "function") return null;
|
|
1742
|
+
try {
|
|
1743
|
+
const body = await response.json();
|
|
1744
|
+
if (typeof body !== "object" || body === null) return null;
|
|
1745
|
+
const { message } = body;
|
|
1746
|
+
return typeof message === "string" && message.trim().length > 0 ? message : null;
|
|
1747
|
+
} catch {
|
|
1748
|
+
return null;
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1704
1751
|
const SIGNATURE_TOLERANCE_SECONDS = 300;
|
|
1705
1752
|
const SIGNATURE_HEADER_RE = /^t=(\d+),v1=([0-9a-f]+)$/;
|
|
1706
1753
|
const SECONDS_PER_MS = 1e3;
|
|
@@ -1923,17 +1970,27 @@ function createClivlySDK(config) {
|
|
|
1923
1970
|
}, response);
|
|
1924
1971
|
return true;
|
|
1925
1972
|
}
|
|
1926
|
-
lastPushFailure ??= {
|
|
1973
|
+
lastPushFailure ??= {
|
|
1974
|
+
status: response?.status ?? null,
|
|
1975
|
+
message: await readPushFailureMessage(response)
|
|
1976
|
+
};
|
|
1927
1977
|
return false;
|
|
1928
1978
|
};
|
|
1929
1979
|
const describePushFailure = (entities) => {
|
|
1930
1980
|
const base = `Clivly sync: push failed for ${entities.join(", ")}`;
|
|
1931
1981
|
const status = lastPushFailure?.status ?? null;
|
|
1982
|
+
const serverMessage = lastPushFailure?.message ?? null;
|
|
1932
1983
|
let cause;
|
|
1933
1984
|
if (status === 401 || status === 403) cause = "the API key was rejected — check CLIVLY_API_KEY is set to a valid key for this org";
|
|
1934
1985
|
else if (status === null) cause = `couldn't reach Clivly at ${apiUrl} — check network access and CLIVLY_API_URL`;
|
|
1986
|
+
else if (serverMessage) cause = `Clivly returned HTTP ${status}: ${serverMessage}`;
|
|
1935
1987
|
else cause = `Clivly returned HTTP ${status}`;
|
|
1936
|
-
|
|
1988
|
+
const separator = SENTENCE_END_RE.test(cause) ? "" : ".";
|
|
1989
|
+
return `${base}: ${cause}${separator} Run \`clivly doctor\` to diagnose.`;
|
|
1990
|
+
};
|
|
1991
|
+
const pushFailureError = (message) => {
|
|
1992
|
+
const status = lastPushFailure?.status ?? null;
|
|
1993
|
+
return status !== null && status >= CLIENT_ERROR_FLOOR && status < RETRYABLE_STATUS_FLOOR ? new TriggerRequestError(status, message) : new Error(message);
|
|
1937
1994
|
};
|
|
1938
1995
|
const syncOne = async (source, path, mode, objectType, runId, batchSizeOverride, maxRowsOverride, persistCursor = true) => {
|
|
1939
1996
|
const since = mode === "full" ? null : await cursorStore.get(source.entity) ?? null;
|
|
@@ -2023,7 +2080,7 @@ function createClivlySDK(config) {
|
|
|
2023
2080
|
failed: true,
|
|
2024
2081
|
error: message
|
|
2025
2082
|
};
|
|
2026
|
-
throw
|
|
2083
|
+
throw pushFailureError(message);
|
|
2027
2084
|
}
|
|
2028
2085
|
lastPush = { failed: false };
|
|
2029
2086
|
};
|
|
@@ -2031,7 +2088,7 @@ function createClivlySDK(config) {
|
|
|
2031
2088
|
const resolved = resolveSampleSource(config, entityKey);
|
|
2032
2089
|
if (!resolved) throw new TriggerRequestError(400, `Clivly sync: no source is configured for entity "${entityKey}". The dashboard maps this table, but clivly.config declares no matching entity — add it to \`entities\`, or remove the mapping in Integrations → Mapping.`);
|
|
2033
2090
|
lastPushFailure = null;
|
|
2034
|
-
if (!await syncOne(resolved.source, resolved.path, "delta", resolved.objectType, runId, SAMPLE_BATCH_SIZE, SAMPLE_BATCH_SIZE, false)) throw
|
|
2091
|
+
if (!await syncOne(resolved.source, resolved.path, "delta", resolved.objectType, runId, SAMPLE_BATCH_SIZE, SAMPLE_BATCH_SIZE, false)) throw pushFailureError(describePushFailure([resolved.source.entity]));
|
|
2035
2092
|
};
|
|
2036
2093
|
const describeSyncTrigger = () => ({
|
|
2037
2094
|
configured: config.syncTrigger?.url,
|
package/sdk.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_entity_config = require("./entity-config-C2MfIDL2.cjs");
|
|
3
|
-
const require_sdk = require("./sdk-
|
|
3
|
+
const require_sdk = require("./sdk-BIYu0N3V.cjs");
|
|
4
4
|
exports.ClivlyAuthError = require_sdk.ClivlyAuthError;
|
|
5
5
|
exports.ClivlyError = require_sdk.ClivlyError;
|
|
6
6
|
exports.ClivlyNetworkError = require_sdk.ClivlyNetworkError;
|
package/sdk.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { c as defineClivlyConfig, g as validateEntitiesConfig, s as customObjectDeclarations } from "./entity-config-bC3DX9f1.mjs";
|
|
2
|
-
import { a as ClivlyRateLimitError, i as ClivlyNetworkError, n as ClivlyAuthError, o as createChatSessionHandler, r as ClivlyError, s as isPlaceholderUrl, t as createClivlySDK } from "./sdk-
|
|
2
|
+
import { a as ClivlyRateLimitError, i as ClivlyNetworkError, n as ClivlyAuthError, o as createChatSessionHandler, r as ClivlyError, s as isPlaceholderUrl, t as createClivlySDK } from "./sdk-DofHordq.mjs";
|
|
3
3
|
export { ClivlyAuthError, ClivlyError, ClivlyNetworkError, ClivlyRateLimitError, createChatSessionHandler, createClivlySDK, customObjectDeclarations, defineClivlyConfig, isPlaceholderUrl, validateEntitiesConfig };
|