@clocknext/sdk 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -193,6 +193,22 @@ Signals are billable usage events. Each kind posts to its own route.
193
193
  `signals.track(signal)` is the underlying call the four convenience methods wrap;
194
194
  use it when the kind is dynamic.
195
195
 
196
+ `tokens` takes `input` and `output` (required), plus two optional prompt-cache
197
+ counts that each default to 0:
198
+
199
+ - `cache` — tokens **read** from the prompt cache, priced at the model's cache-read rate.
200
+ - `cacheWrite` — tokens **written** into the prompt cache, priced at the model's
201
+ separate cache-write rate.
202
+
203
+ ```ts
204
+ await cnk.signals.credit({
205
+ customerId,
206
+ model: "claude-sonnet-4-5",
207
+ agentKey: "api_credit",
208
+ tokens: { input: 1200, output: 340, cache: 800, cacheWrite: 2000 },
209
+ });
210
+ ```
211
+
196
212
  Every signal method can take `externalId` (your own id for the customer — see
197
213
  [Your own customer ids](#your-own-customer-ids)) instead of `customerId`.
198
214
 
@@ -528,10 +544,12 @@ signal log tells you why an individual signal did not.
528
544
 
529
545
  ```ts
530
546
  const balances = await cnk.customers.balances(customerId);
531
- balances.wallet?.balanceUsd;
547
+ balances.wallet.balance; // USD; always present
548
+ balances.wallet.reservedBalance; // USD; undefined unless Reserved Wallet is on
532
549
  balances.credits; // granted, used, remaining
533
550
  balances.outcomes; // granted, remaining, steps
534
551
  balances.units; // granted, remaining
552
+ balances.composites; // prepaid composite pools: refId, granted, used, remaining
535
553
 
536
554
  // Convenience lookups — these fetch the full snapshot, so prefer `balances()`
537
555
  // when you need more than one row from the same customer.
@@ -584,19 +602,20 @@ the customer's ordinary wallet (Settings → Organisation → "Enable Reserved
584
602
  balance in wallet"). When a wallet-funded Arrear charge comes in, it draws from
585
603
  the Wallet first, then from the Reserved Wallet down to 0, and anything still
586
604
  left pushes the Wallet negative. The Reserved Wallet itself never goes
587
- negative, and a normal (`MAIN`) wallet adjustment never touches it.
605
+ negative, and a normal (`main`) wallet adjustment never touches it.
588
606
 
589
607
  ```ts
590
608
  // Top up (or, with a negative delta, reduce) the Reserved Wallet.
591
- const { remaining } = await cnk.customers.adjustWallet(customerId, { delta: 500, pool: "RESERVED" });
609
+ const { remaining } = await cnk.customers.adjustWallet(customerId, { delta: 500, pool: "reserved" });
592
610
  // `remaining` is the Reserved balance. Reducing below 0 is refused with an
593
- // AllowanceError (down to exactly 0 is fine); any RESERVED call when the
611
+ // AllowanceError (down to exactly 0 is fine); any "reserved" call when the
594
612
  // workspace hasn't enabled the feature is a PlanError.
595
613
 
596
- const { reservedWallet } = await cnk.customers.balances(customerId); // { balanceUsd } | null
614
+ const { wallet } = await cnk.customers.balances(customerId);
615
+ wallet.reservedBalance; // number | undefined
597
616
  ```
598
617
 
599
- `reservedWallet` is `null` when the feature is off.
618
+ When the feature is off, `wallet.reservedBalance` is left out of `balances()`.
600
619
 
601
620
  To change how much a customer is granted **every billing cycle** (not just
602
621
  their balance right now), use `updateRecurringGrant`. Any increase or decrease
@@ -685,8 +704,75 @@ Customer objects carry `externalId`. Everything else — `signals.usageLog()`,
685
704
  the invoice, payment and purchase lists, `portal.createToken()` and members —
686
705
  still takes ClockNext's id.
687
706
 
688
- Models, credits, outcomes, units, composites and plans are created and edited in
689
- the ClockNext dashboard, not through the SDK.
707
+ ## Catalogue: credits, outcomes, units, composites and plans
708
+
709
+ Each of `cnk.credits`, `cnk.outcomes`, `cnk.units`, `cnk.composites` and
710
+ `cnk.plans` has the same six calls: `list()`, `get(id)`, `create(input)`,
711
+ `update(id, input)`, `archive(id)` and `unarchive(id)`.
712
+
713
+ - **Nothing is deleted.** There is no `delete()`: retire an item with
714
+ `archive()`, bring it back with `unarchive()`. Usage history never loses
715
+ what it points at.
716
+ - **`update()` is partial** — only the fields you send change. A list you do
717
+ send (`modelBundle`, `steps`, `tiers`, `entitlements`) is the complete new
718
+ list.
719
+ - **Prices are never typed in.** A credit or outcome step sends a
720
+ `modelBundle` — one line per model — and ClockNext works the price out from
721
+ the models' live prices.
722
+ - **A plan's currency** is your workspace's primary currency; it can't be set.
723
+
724
+ ```ts
725
+ const credit = await cnk.credits.create({
726
+ name: "Deep Research",
727
+ agentKey: "deep_research",
728
+ marginPercent: 20,
729
+ modelBundle: [
730
+ { modelId: "claude-sonnet-4-5", averageTokensPerLLMCall: 100000, input: 60, output: 30, cache: 5, cacheWrite: 5 },
731
+ ],
732
+ });
733
+
734
+ const outcome = await cnk.outcomes.create({
735
+ name: "Contract review",
736
+ agentKey: "contract_review",
737
+ marginPercent: 100,
738
+ steps: [
739
+ { name: "Extract clauses", agentKey: "contract_review.extract",
740
+ modelBundle: [{ modelId: "claude-sonnet-4-5", averageTokensPerLLMCall: 50000, input: 70, output: 30, cache: 0 }] },
741
+ ],
742
+ });
743
+
744
+ const unit = await cnk.units.create({
745
+ name: "API calls",
746
+ agentKey: "api_calls",
747
+ pricingType: "SLAB",
748
+ tiers: [{ upTo: 1000, price: 0.01 }, { upTo: null, price: 0.005 }], // last tier: "and above"
749
+ });
750
+
751
+ const composite = await cnk.composites.create({
752
+ name: "Voice AI call",
753
+ refId: "voice_ai",
754
+ price: 2.5,
755
+ entitlements: { creditIds: [credit.id] },
756
+ });
757
+
758
+ const plan = await cnk.plans.create({
759
+ name: "Pro",
760
+ billingCycle: "MONTHLY",
761
+ entitlements: [
762
+ { type: "CREDIT", billingMode: "ADVANCE", creditId: credit.id, quantity: 1000, rollover: true },
763
+ { type: "OUTCOME", billingMode: "ARREAR", outcomeId: outcome.id },
764
+ { type: "PRICING_METRIC", billingMode: "ADVANCE", compositeId: composite.id, quantity: 50 },
765
+ { type: "WALLET", billingMode: "ADVANCE", amount: 25 },
766
+ ],
767
+ });
768
+
769
+ await cnk.credits.update(credit.id, { marginPercent: 50 }); // only the margin changes
770
+ await cnk.units.archive(unit.id); // retire it
771
+ await cnk.units.unarchive(unit.id); // and bring it back
772
+ ```
773
+
774
+ Models are enabled in the ClockNext dashboard. A bundle line names its model by
775
+ `modelId` — the same id you send as `usage.model` on a signal.
690
776
 
691
777
  ## Customer portal
692
778
 
package/dist/index.cjs CHANGED
@@ -65,6 +65,7 @@ var FIELD_PATHS = [
65
65
  [/\busage\.inputTokens\b/g, "tokens.input"],
66
66
  [/\busage\.outputTokens\b/g, "tokens.output"],
67
67
  [/\busage\.cacheTokens\b/g, "tokens.cache"],
68
+ [/\busage\.cacheWriteTokens\b/g, "tokens.cacheWrite"],
68
69
  [/\busage\.model\b/g, "model"],
69
70
  // A missing/invalid `usage` block as a whole: the caller's equivalent is
70
71
  // `tokens` (plus `model`, which the SDK hoists to the top level).
@@ -332,7 +333,7 @@ function sleep(ms) {
332
333
  }
333
334
 
334
335
  // src/http.ts
335
- var SDK_VERSION = "0.15.0" ;
336
+ var SDK_VERSION = "0.17.0" ;
336
337
  var Transport = class {
337
338
  constructor(cfg) {
338
339
  this.cfg = cfg;
@@ -436,6 +437,143 @@ var Transport = class {
436
437
  }
437
438
  };
438
439
 
440
+ // src/filters.ts
441
+ function activeQuery(active) {
442
+ return active === true ? "true" : void 0;
443
+ }
444
+ function filterByActive(rows, active) {
445
+ return active === false ? rows.filter((row) => !row.isActive) : rows;
446
+ }
447
+
448
+ // src/resources/composites.ts
449
+ var Composites = class {
450
+ constructor(transport) {
451
+ this.transport = transport;
452
+ }
453
+ transport;
454
+ /** List them. `{ active: true }` returns only active ones, `{ active: false }`
455
+ * only archived ones; omit it for both. */
456
+ async list(params = {}) {
457
+ const res = await this.transport.request({
458
+ method: "GET",
459
+ path: "/api/v1/composites",
460
+ query: { active: activeQuery(params.active) }
461
+ });
462
+ return filterByActive(res.composites, params.active);
463
+ }
464
+ /** One composite's configuration. */
465
+ async get(id) {
466
+ const res = await this.transport.request({
467
+ method: "GET",
468
+ path: `/api/v1/composites/${encodeURIComponent(id)}`
469
+ });
470
+ return res.composite;
471
+ }
472
+ /** Create one. It starts active. Not retried on failure — a retry could
473
+ * create a second row. */
474
+ async create(input) {
475
+ const res = await this.transport.request({
476
+ method: "POST",
477
+ path: "/api/v1/composites",
478
+ body: input
479
+ });
480
+ return res.composite;
481
+ }
482
+ /** PARTIAL update: only the fields you send change. `entitlements`, when
483
+ * sent, is the complete new set. Renaming `refId` cuts live traffic over at
484
+ * once — ship the new tag to your callers first. */
485
+ async update(id, input) {
486
+ const res = await this.transport.request({
487
+ method: "PATCH",
488
+ path: `/api/v1/composites/${encodeURIComponent(id)}`,
489
+ body: input
490
+ });
491
+ return res.composite;
492
+ }
493
+ /** Retire it (`isActive` → false). Nothing is deleted. Safe to retry. */
494
+ async archive(id) {
495
+ const res = await this.transport.request({
496
+ method: "POST",
497
+ path: `/api/v1/composites/${encodeURIComponent(id)}/archive`,
498
+ retry: true
499
+ });
500
+ return res.composite;
501
+ }
502
+ /** Bring an archived one back (`isActive` → true). Safe to retry. */
503
+ async unarchive(id) {
504
+ const res = await this.transport.request({
505
+ method: "POST",
506
+ path: `/api/v1/composites/${encodeURIComponent(id)}/unarchive`,
507
+ retry: true
508
+ });
509
+ return res.composite;
510
+ }
511
+ };
512
+
513
+ // src/resources/credits.ts
514
+ var Credits = class {
515
+ constructor(transport) {
516
+ this.transport = transport;
517
+ }
518
+ transport;
519
+ /** List them. `{ active: true }` returns only active ones, `{ active: false }`
520
+ * only archived ones; omit it for both. */
521
+ async list(params = {}) {
522
+ const res = await this.transport.request({
523
+ method: "GET",
524
+ path: "/api/v1/credits",
525
+ query: { active: activeQuery(params.active) }
526
+ });
527
+ return filterByActive(res.credits, params.active);
528
+ }
529
+ /** One credit's configuration. */
530
+ async get(id) {
531
+ const res = await this.transport.request({
532
+ method: "GET",
533
+ path: `/api/v1/credits/${encodeURIComponent(id)}`
534
+ });
535
+ return res.credit;
536
+ }
537
+ /** Create one. It starts active. Not retried on failure — a retry could
538
+ * create a second row. */
539
+ async create(input) {
540
+ const res = await this.transport.request({
541
+ method: "POST",
542
+ path: "/api/v1/credits",
543
+ body: input
544
+ });
545
+ return res.credit;
546
+ }
547
+ /** PARTIAL update: only the fields you send change. Send `modelBundle` to
548
+ * re-price; leave it out to keep the stored base price. */
549
+ async update(id, input) {
550
+ const res = await this.transport.request({
551
+ method: "PATCH",
552
+ path: `/api/v1/credits/${encodeURIComponent(id)}`,
553
+ body: input
554
+ });
555
+ return res.credit;
556
+ }
557
+ /** Retire it (`isActive` → false). Nothing is deleted. Safe to retry. */
558
+ async archive(id) {
559
+ const res = await this.transport.request({
560
+ method: "POST",
561
+ path: `/api/v1/credits/${encodeURIComponent(id)}/archive`,
562
+ retry: true
563
+ });
564
+ return res.credit;
565
+ }
566
+ /** Bring an archived one back (`isActive` → true). Safe to retry. */
567
+ async unarchive(id) {
568
+ const res = await this.transport.request({
569
+ method: "POST",
570
+ path: `/api/v1/credits/${encodeURIComponent(id)}/unarchive`,
571
+ retry: true
572
+ });
573
+ return res.credit;
574
+ }
575
+ };
576
+
439
577
  // src/idempotency.ts
440
578
  function newKey() {
441
579
  const c = globalThis.crypto;
@@ -664,7 +802,7 @@ var Customers = class {
664
802
  * Top up (positive `delta`) or draw down (negative `delta`) a customer's
665
803
  * wallet, in USD — allowed whatever the customer's plan. `remaining` is the
666
804
  * resulting USD balance. No `agentKey` — a customer has exactly one wallet.
667
- * Pass `pool: "RESERVED"` to move the Reserved Wallet instead (then
805
+ * Pass `pool: "reserved"` to move the Reserved Wallet instead (then
668
806
  * `remaining` is its balance). `POST …/:id/wallet/adjust`.
669
807
  */
670
808
  adjustWallet(id, input) {
@@ -795,6 +933,71 @@ var Invoices = class {
795
933
  }
796
934
  };
797
935
 
936
+ // src/resources/outcomes.ts
937
+ var Outcomes = class {
938
+ constructor(transport) {
939
+ this.transport = transport;
940
+ }
941
+ transport;
942
+ /** List them. `{ active: true }` returns only active ones, `{ active: false }`
943
+ * only archived ones; omit it for both. */
944
+ async list(params = {}) {
945
+ const res = await this.transport.request({
946
+ method: "GET",
947
+ path: "/api/v1/outcomes",
948
+ query: { active: activeQuery(params.active) }
949
+ });
950
+ return filterByActive(res.outcomes, params.active);
951
+ }
952
+ /** One outcome's configuration, with every step in full. */
953
+ async get(id) {
954
+ const res = await this.transport.request({
955
+ method: "GET",
956
+ path: `/api/v1/outcomes/${encodeURIComponent(id)}`
957
+ });
958
+ return res.outcome;
959
+ }
960
+ /** Create one. It starts active. Not retried on failure — a retry could
961
+ * create a second row. */
962
+ async create(input) {
963
+ const res = await this.transport.request({
964
+ method: "POST",
965
+ path: "/api/v1/outcomes",
966
+ body: input
967
+ });
968
+ return res.outcome;
969
+ }
970
+ /** PARTIAL update: only the fields you send change. Leave `steps` out to
971
+ * leave the steps alone; send it and it is the complete list — pass each
972
+ * existing step's `id` (or its same `agentKey`) to keep it. */
973
+ async update(id, input) {
974
+ const res = await this.transport.request({
975
+ method: "PATCH",
976
+ path: `/api/v1/outcomes/${encodeURIComponent(id)}`,
977
+ body: input
978
+ });
979
+ return res.outcome;
980
+ }
981
+ /** Retire it (`isActive` → false). Nothing is deleted. Safe to retry. */
982
+ async archive(id) {
983
+ const res = await this.transport.request({
984
+ method: "POST",
985
+ path: `/api/v1/outcomes/${encodeURIComponent(id)}/archive`,
986
+ retry: true
987
+ });
988
+ return res.outcome;
989
+ }
990
+ /** Bring an archived one back (`isActive` → true). Safe to retry. */
991
+ async unarchive(id) {
992
+ const res = await this.transport.request({
993
+ method: "POST",
994
+ path: `/api/v1/outcomes/${encodeURIComponent(id)}/unarchive`,
995
+ retry: true
996
+ });
997
+ return res.outcome;
998
+ }
999
+ };
1000
+
798
1001
  // src/resources/payments.ts
799
1002
  var Payments = class {
800
1003
  constructor(transport) {
@@ -819,14 +1022,6 @@ var Payments = class {
819
1022
  }
820
1023
  };
821
1024
 
822
- // src/filters.ts
823
- function activeQuery(active) {
824
- return active === true ? "true" : void 0;
825
- }
826
- function filterByActive(rows, active) {
827
- return active === false ? rows.filter((row) => !row.isActive) : rows;
828
- }
829
-
830
1025
  // src/resources/plans.ts
831
1026
  var Plans = class {
832
1027
  constructor(transport) {
@@ -844,6 +1039,54 @@ var Plans = class {
844
1039
  });
845
1040
  return filterByActive(res.plans, params.active);
846
1041
  }
1042
+ /** One plan's configuration. */
1043
+ async get(id) {
1044
+ const res = await this.transport.request({
1045
+ method: "GET",
1046
+ path: `/api/v1/plans/${encodeURIComponent(id)}`
1047
+ });
1048
+ return res.plan;
1049
+ }
1050
+ /** Create a plan. It starts active. Not retried on failure — a retry could
1051
+ * create a second plan. */
1052
+ async create(input) {
1053
+ const res = await this.transport.request({
1054
+ method: "POST",
1055
+ path: "/api/v1/plans",
1056
+ body: input
1057
+ });
1058
+ return res.plan;
1059
+ }
1060
+ /** PARTIAL update: only the fields you send change. `entitlements`, when
1061
+ * sent, is the complete new list — the list `get()` returned can be sent
1062
+ * back as it is. Changes apply to new purchases only. */
1063
+ async update(id, input) {
1064
+ const res = await this.transport.request({
1065
+ method: "PATCH",
1066
+ path: `/api/v1/plans/${encodeURIComponent(id)}`,
1067
+ body: input
1068
+ });
1069
+ return res.plan;
1070
+ }
1071
+ /** Retire a plan (`isActive` → false): it can't be bought any more; customers
1072
+ * already on it are unaffected. Safe to retry. */
1073
+ async archive(id) {
1074
+ const res = await this.transport.request({
1075
+ method: "POST",
1076
+ path: `/api/v1/plans/${encodeURIComponent(id)}/archive`,
1077
+ retry: true
1078
+ });
1079
+ return res.plan;
1080
+ }
1081
+ /** Make an archived plan purchasable again (`isActive` → true). Safe to retry. */
1082
+ async unarchive(id) {
1083
+ const res = await this.transport.request({
1084
+ method: "POST",
1085
+ path: `/api/v1/plans/${encodeURIComponent(id)}/unarchive`,
1086
+ retry: true
1087
+ });
1088
+ return res.plan;
1089
+ }
847
1090
  };
848
1091
 
849
1092
  // src/resources/portal.ts
@@ -949,12 +1192,14 @@ function signalToWire(signal) {
949
1192
  const body = {
950
1193
  ...customerRefToWire(signal),
951
1194
  // `usage` is required on all three model-priced kinds. input/output are
952
- // always sent; `cacheTokens` only when provided (the server defaults 0).
1195
+ // always sent; `cacheTokens` / `cacheWriteTokens` only when provided (the
1196
+ // server defaults each to 0).
953
1197
  usage: {
954
1198
  model: signal.model,
955
1199
  inputTokens: t.input,
956
1200
  outputTokens: t.output,
957
- ...t.cache != null ? { cacheTokens: t.cache } : {}
1201
+ ...t.cache != null ? { cacheTokens: t.cache } : {},
1202
+ ...t.cacheWrite != null ? { cacheWriteTokens: t.cacheWrite } : {}
958
1203
  }
959
1204
  };
960
1205
  if (signal.member) body.member = signal.member;
@@ -1204,14 +1449,86 @@ var Signals = class {
1204
1449
  }
1205
1450
  };
1206
1451
 
1452
+ // src/resources/units.ts
1453
+ var Units = class {
1454
+ constructor(transport) {
1455
+ this.transport = transport;
1456
+ }
1457
+ transport;
1458
+ /** List them. `{ active: true }` returns only active ones, `{ active: false }`
1459
+ * only archived ones; omit it for both. */
1460
+ async list(params = {}) {
1461
+ const res = await this.transport.request({
1462
+ method: "GET",
1463
+ path: "/api/v1/units",
1464
+ query: { active: activeQuery(params.active) }
1465
+ });
1466
+ return filterByActive(res.units, params.active);
1467
+ }
1468
+ /** One unit's configuration. */
1469
+ async get(id) {
1470
+ const res = await this.transport.request({
1471
+ method: "GET",
1472
+ path: `/api/v1/units/${encodeURIComponent(id)}`
1473
+ });
1474
+ return res.unit;
1475
+ }
1476
+ /** Create one. It starts active. Not retried on failure — a retry could
1477
+ * create a second row. */
1478
+ async create(input) {
1479
+ const res = await this.transport.request({
1480
+ method: "POST",
1481
+ path: "/api/v1/units",
1482
+ body: input
1483
+ });
1484
+ return res.unit;
1485
+ }
1486
+ /** PARTIAL update: only the fields you send change. `tiers`, when sent, is
1487
+ * the complete new tier list. */
1488
+ async update(id, input) {
1489
+ const res = await this.transport.request({
1490
+ method: "PATCH",
1491
+ path: `/api/v1/units/${encodeURIComponent(id)}`,
1492
+ body: input
1493
+ });
1494
+ return res.unit;
1495
+ }
1496
+ /** Retire it (`isActive` → false). Nothing is deleted. Safe to retry. */
1497
+ async archive(id) {
1498
+ const res = await this.transport.request({
1499
+ method: "POST",
1500
+ path: `/api/v1/units/${encodeURIComponent(id)}/archive`,
1501
+ retry: true
1502
+ });
1503
+ return res.unit;
1504
+ }
1505
+ /** Bring an archived one back (`isActive` → true). Safe to retry. */
1506
+ async unarchive(id) {
1507
+ const res = await this.transport.request({
1508
+ method: "POST",
1509
+ path: `/api/v1/units/${encodeURIComponent(id)}/unarchive`,
1510
+ retry: true
1511
+ });
1512
+ return res.unit;
1513
+ }
1514
+ };
1515
+
1207
1516
  // src/client.ts
1208
1517
  var ClockNext = class {
1209
1518
  /** Record usage signals (credit / wallet / outcome / unit) and read the usage log. */
1210
1519
  signals;
1211
1520
  /** Manage customers, their members, and balances. */
1212
1521
  customers;
1213
- /** List billing plans. */
1522
+ /** Billing plans: list, get, create, update, archive, unarchive. */
1214
1523
  plans;
1524
+ /** Credits: list, get, create, update, archive, unarchive (no delete). */
1525
+ credits;
1526
+ /** Outcomes: list, get, create, update, archive, unarchive (no delete). */
1527
+ outcomes;
1528
+ /** Units: list, get, create, update, archive, unarchive (no delete). */
1529
+ units;
1530
+ /** Composites: list, get, create, update, archive, unarchive (no delete). */
1531
+ composites;
1215
1532
  /** Subscribe customers to plans (purchases). */
1216
1533
  purchases;
1217
1534
  /** Read invoices and mint hosted pay links. */
@@ -1234,6 +1551,10 @@ var ClockNext = class {
1234
1551
  this.signals = new Signals(transport, this.flusher, this.cfg);
1235
1552
  this.customers = new Customers(transport);
1236
1553
  this.plans = new Plans(transport);
1554
+ this.credits = new Credits(transport);
1555
+ this.outcomes = new Outcomes(transport);
1556
+ this.units = new Units(transport);
1557
+ this.composites = new Composites(transport);
1237
1558
  this.purchases = new Purchases(transport);
1238
1559
  this.invoices = new Invoices(transport);
1239
1560
  this.payments = new Payments(transport);