@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clocknext/sdk",
3
- "version": "0.15.0",
3
+ "version": "0.17.0",
4
4
  "description": "Official ClockNext SDK — record usage signals and manage plans, purchases, invoices, customers, and billing from your backend.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/client.ts CHANGED
@@ -1,13 +1,17 @@
1
1
  import { resolveConfig } from "./config.js";
2
2
  import { Flusher } from "./flusher.js";
3
3
  import { Transport } from "./http.js";
4
+ import { Composites } from "./resources/composites.js";
5
+ import { Credits } from "./resources/credits.js";
4
6
  import { Customers } from "./resources/customers.js";
5
7
  import { Invoices } from "./resources/invoices.js";
8
+ import { Outcomes } from "./resources/outcomes.js";
6
9
  import { Payments } from "./resources/payments.js";
7
10
  import { Plans } from "./resources/plans.js";
8
11
  import { Portal } from "./resources/portal.js";
9
12
  import { Purchases } from "./resources/purchases.js";
10
13
  import { Signals } from "./resources/signals.js";
14
+ import { Units } from "./resources/units.js";
11
15
  import type { ClockNextConfig, ResolvedConfig } from "./types.js";
12
16
 
13
17
  /**
@@ -32,8 +36,16 @@ export class ClockNext {
32
36
  readonly signals: Signals;
33
37
  /** Manage customers, their members, and balances. */
34
38
  readonly customers: Customers;
35
- /** List billing plans. */
39
+ /** Billing plans: list, get, create, update, archive, unarchive. */
36
40
  readonly plans: Plans;
41
+ /** Credits: list, get, create, update, archive, unarchive (no delete). */
42
+ readonly credits: Credits;
43
+ /** Outcomes: list, get, create, update, archive, unarchive (no delete). */
44
+ readonly outcomes: Outcomes;
45
+ /** Units: list, get, create, update, archive, unarchive (no delete). */
46
+ readonly units: Units;
47
+ /** Composites: list, get, create, update, archive, unarchive (no delete). */
48
+ readonly composites: Composites;
37
49
  /** Subscribe customers to plans (purchases). */
38
50
  readonly purchases: Purchases;
39
51
  /** Read invoices and mint hosted pay links. */
@@ -60,6 +72,10 @@ export class ClockNext {
60
72
  this.signals = new Signals(transport, this.flusher, this.cfg);
61
73
  this.customers = new Customers(transport);
62
74
  this.plans = new Plans(transport);
75
+ this.credits = new Credits(transport);
76
+ this.outcomes = new Outcomes(transport);
77
+ this.units = new Units(transport);
78
+ this.composites = new Composites(transport);
63
79
  this.purchases = new Purchases(transport);
64
80
  this.invoices = new Invoices(transport);
65
81
  this.payments = new Payments(transport);
package/src/errors.ts CHANGED
@@ -107,7 +107,7 @@ export class NetworkError extends ClockNextError {
107
107
  *
108
108
  * The server validates the WIRE body and names the offending path in its 400
109
109
  * (`usage.inputTokens: Expected number…`). But the SDK groups those fields
110
- * differently — `tokens: { input, output, cache }` — so the path it reports
110
+ * differently — `tokens: { input, output, cache, cacheWrite }` — so the path it reports
111
111
  * names something that does not exist anywhere in this SDK's API. A caller
112
112
  * (human or LLM) then searches their own code for `usage.inputTokens`, finds
113
113
  * nothing, and has no next move.
@@ -120,6 +120,7 @@ const FIELD_PATHS: ReadonlyArray<readonly [RegExp, string]> = [
120
120
  [/\busage\.inputTokens\b/g, "tokens.input"],
121
121
  [/\busage\.outputTokens\b/g, "tokens.output"],
122
122
  [/\busage\.cacheTokens\b/g, "tokens.cache"],
123
+ [/\busage\.cacheWriteTokens\b/g, "tokens.cacheWrite"],
123
124
  [/\busage\.model\b/g, "model"],
124
125
  // A missing/invalid `usage` block as a whole: the caller's equivalent is
125
126
  // `tokens` (plus `model`, which the SDK hoists to the top level).
package/src/index.ts CHANGED
@@ -72,6 +72,7 @@ export type {
72
72
  CreditBalance,
73
73
  OutcomeBalance,
74
74
  UnitBalance,
75
+ CompositeBalance,
75
76
  CustomerPlan,
76
77
  CustomerPlanDetails,
77
78
  // members
@@ -92,6 +93,31 @@ export type {
92
93
  RecurringGrantResult,
93
94
  // plans
94
95
  Plan,
96
+ PlanEntitlement,
97
+ PlanEntitlementInput,
98
+ CreatePlanInput,
99
+ UpdatePlanInput,
100
+ // catalogue: credits, outcomes, units, composites
101
+ ModelBundleLine,
102
+ ModelBundleLineInput,
103
+ Credit,
104
+ CreateCreditInput,
105
+ UpdateCreditInput,
106
+ Outcome,
107
+ OutcomeStep,
108
+ OutcomeWithSteps,
109
+ OutcomeStepInput,
110
+ CreateOutcomeInput,
111
+ UpdateOutcomeInput,
112
+ UnitPricingType,
113
+ UnitTier,
114
+ Unit,
115
+ CreateUnitInput,
116
+ UpdateUnitInput,
117
+ CompositeItem,
118
+ Composite,
119
+ CreateCompositeInput,
120
+ UpdateCompositeInput,
95
121
  PlanComponent,
96
122
  BillingCycle,
97
123
  PlanType,
@@ -0,0 +1,82 @@
1
+ import { activeQuery, filterByActive } from "../filters.js";
2
+ import type { Transport } from "../http.js";
3
+ import type { Composite, CreateCompositeInput, UpdateCompositeInput } from "../types.js";
4
+
5
+ /**
6
+ * The `composites` resource — several credits / outcomes / units grouped under
7
+ * one tag (`refId`) and billed as ONE thing per completed occurrence.
8
+ * `/api/v1/composites`.
9
+ *
10
+ * Creating a composite charges nobody: it only bills once a plan sells it as
11
+ * a `PRICING_METRIC` entitlement (`plans.create()` with its `compositeId`).
12
+ *
13
+ * There is no `delete()` — the API never deletes catalogue items. Retire a
14
+ * composite with `archive()` (new signals stop resolving to it; its history is
15
+ * kept); `unarchive()` brings it back.
16
+ */
17
+ export class Composites {
18
+ constructor(private readonly transport: Transport) {}
19
+
20
+ /** List them. `{ active: true }` returns only active ones, `{ active: false }`
21
+ * only archived ones; omit it for both. */
22
+ async list(params: { active?: boolean } = {}): Promise<Composite[]> {
23
+ const res = await this.transport.request<{ composites: Composite[] }>({
24
+ method: "GET",
25
+ path: "/api/v1/composites",
26
+ query: { active: activeQuery(params.active) },
27
+ });
28
+ return filterByActive(res.composites, params.active);
29
+ }
30
+
31
+ /** One composite's configuration. */
32
+ async get(id: string): Promise<Composite> {
33
+ const res = await this.transport.request<{ composite: Composite }>({
34
+ method: "GET",
35
+ path: `/api/v1/composites/${encodeURIComponent(id)}`,
36
+ });
37
+ return res.composite;
38
+ }
39
+
40
+ /** Create one. It starts active. Not retried on failure — a retry could
41
+ * create a second row. */
42
+ async create(input: CreateCompositeInput): Promise<Composite> {
43
+ const res = await this.transport.request<{ created: true; composite: Composite }>({
44
+ method: "POST",
45
+ path: "/api/v1/composites",
46
+ body: input,
47
+ });
48
+ return res.composite;
49
+ }
50
+
51
+ /** PARTIAL update: only the fields you send change. `entitlements`, when
52
+ * sent, is the complete new set. Renaming `refId` cuts live traffic over at
53
+ * once — ship the new tag to your callers first. */
54
+ async update(id: string, input: UpdateCompositeInput): Promise<Composite> {
55
+ const res = await this.transport.request<{ updated: true; composite: Composite }>({
56
+ method: "PATCH",
57
+ path: `/api/v1/composites/${encodeURIComponent(id)}`,
58
+ body: input,
59
+ });
60
+ return res.composite;
61
+ }
62
+
63
+ /** Retire it (`isActive` → false). Nothing is deleted. Safe to retry. */
64
+ async archive(id: string): Promise<Composite> {
65
+ const res = await this.transport.request<{ archived: true; composite: Composite }>({
66
+ method: "POST",
67
+ path: `/api/v1/composites/${encodeURIComponent(id)}/archive`,
68
+ retry: true,
69
+ });
70
+ return res.composite;
71
+ }
72
+
73
+ /** Bring an archived one back (`isActive` → true). Safe to retry. */
74
+ async unarchive(id: string): Promise<Composite> {
75
+ const res = await this.transport.request<{ unarchived: true; composite: Composite }>({
76
+ method: "POST",
77
+ path: `/api/v1/composites/${encodeURIComponent(id)}/unarchive`,
78
+ retry: true,
79
+ });
80
+ return res.composite;
81
+ }
82
+ }
@@ -0,0 +1,81 @@
1
+ import { activeQuery, filterByActive } from "../filters.js";
2
+ import type { Transport } from "../http.js";
3
+ import type { CreateCreditInput, Credit, UpdateCreditInput } from "../types.js";
4
+
5
+ /**
6
+ * The `credits` resource — the token-metered credits your product draws down
7
+ * by `agentKey`. `/api/v1/credits`.
8
+ *
9
+ * Pricing is never typed in: `create()` takes a `modelBundle` (one line per
10
+ * model, each with `averageTokensPerLLMCall` and its input / output / cache /
11
+ * cacheWrite split) and the server works out `basePrice` and
12
+ * `pricePerCredit` from the models' live prices.
13
+ *
14
+ * There is no `delete()` — the API never deletes catalogue items. Retire a
15
+ * credit with `archive()`; `unarchive()` brings it back unchanged.
16
+ */
17
+ export class Credits {
18
+ constructor(private readonly transport: Transport) {}
19
+
20
+ /** List them. `{ active: true }` returns only active ones, `{ active: false }`
21
+ * only archived ones; omit it for both. */
22
+ async list(params: { active?: boolean } = {}): Promise<Credit[]> {
23
+ const res = await this.transport.request<{ credits: Credit[] }>({
24
+ method: "GET",
25
+ path: "/api/v1/credits",
26
+ query: { active: activeQuery(params.active) },
27
+ });
28
+ return filterByActive(res.credits, params.active);
29
+ }
30
+
31
+ /** One credit's configuration. */
32
+ async get(id: string): Promise<Credit> {
33
+ const res = await this.transport.request<{ credit: Credit }>({
34
+ method: "GET",
35
+ path: `/api/v1/credits/${encodeURIComponent(id)}`,
36
+ });
37
+ return res.credit;
38
+ }
39
+
40
+ /** Create one. It starts active. Not retried on failure — a retry could
41
+ * create a second row. */
42
+ async create(input: CreateCreditInput): Promise<Credit> {
43
+ const res = await this.transport.request<{ created: true; credit: Credit }>({
44
+ method: "POST",
45
+ path: "/api/v1/credits",
46
+ body: input,
47
+ });
48
+ return res.credit;
49
+ }
50
+
51
+ /** PARTIAL update: only the fields you send change. Send `modelBundle` to
52
+ * re-price; leave it out to keep the stored base price. */
53
+ async update(id: string, input: UpdateCreditInput): Promise<Credit> {
54
+ const res = await this.transport.request<{ updated: true; credit: Credit }>({
55
+ method: "PATCH",
56
+ path: `/api/v1/credits/${encodeURIComponent(id)}`,
57
+ body: input,
58
+ });
59
+ return res.credit;
60
+ }
61
+
62
+ /** Retire it (`isActive` → false). Nothing is deleted. Safe to retry. */
63
+ async archive(id: string): Promise<Credit> {
64
+ const res = await this.transport.request<{ archived: true; credit: Credit }>({
65
+ method: "POST",
66
+ path: `/api/v1/credits/${encodeURIComponent(id)}/archive`,
67
+ retry: true,
68
+ });
69
+ return res.credit;
70
+ }
71
+
72
+ /** Bring an archived one back (`isActive` → true). Safe to retry. */
73
+ async unarchive(id: string): Promise<Credit> {
74
+ const res = await this.transport.request<{ unarchived: true; credit: Credit }>({
75
+ method: "POST",
76
+ path: `/api/v1/credits/${encodeURIComponent(id)}/unarchive`,
77
+ retry: true,
78
+ });
79
+ return res.credit;
80
+ }
81
+ }
@@ -310,7 +310,7 @@ export class Customers {
310
310
  * Top up (positive `delta`) or draw down (negative `delta`) a customer's
311
311
  * wallet, in USD — allowed whatever the customer's plan. `remaining` is the
312
312
  * resulting USD balance. No `agentKey` — a customer has exactly one wallet.
313
- * Pass `pool: "RESERVED"` to move the Reserved Wallet instead (then
313
+ * Pass `pool: "reserved"` to move the Reserved Wallet instead (then
314
314
  * `remaining` is its balance). `POST …/:id/wallet/adjust`.
315
315
  */
316
316
  adjustWallet(id: string, input: WalletAdjustInput): Promise<AdjustResult> {
@@ -0,0 +1,81 @@
1
+ import { activeQuery, filterByActive } from "../filters.js";
2
+ import type { Transport } from "../http.js";
3
+ import type { CreateOutcomeInput, Outcome, OutcomeWithSteps, UpdateOutcomeInput } from "../types.js";
4
+
5
+ /**
6
+ * The `outcomes` resource — multi-step deliverables billed per completed
7
+ * outcome. `/api/v1/outcomes`.
8
+ *
9
+ * `list()` and every write return the outcome without its steps; `get()`
10
+ * returns it with every step in full (ids included). Each new step is priced
11
+ * from its own `modelBundle`, on the server.
12
+ *
13
+ * There is no `delete()` — the API never deletes catalogue items. Retire an
14
+ * outcome with `archive()`; `unarchive()` brings it back unchanged.
15
+ */
16
+ export class Outcomes {
17
+ constructor(private readonly transport: Transport) {}
18
+
19
+ /** List them. `{ active: true }` returns only active ones, `{ active: false }`
20
+ * only archived ones; omit it for both. */
21
+ async list(params: { active?: boolean } = {}): Promise<Outcome[]> {
22
+ const res = await this.transport.request<{ outcomes: Outcome[] }>({
23
+ method: "GET",
24
+ path: "/api/v1/outcomes",
25
+ query: { active: activeQuery(params.active) },
26
+ });
27
+ return filterByActive(res.outcomes, params.active);
28
+ }
29
+
30
+ /** One outcome's configuration, with every step in full. */
31
+ async get(id: string): Promise<OutcomeWithSteps> {
32
+ const res = await this.transport.request<{ outcome: OutcomeWithSteps }>({
33
+ method: "GET",
34
+ path: `/api/v1/outcomes/${encodeURIComponent(id)}`,
35
+ });
36
+ return res.outcome;
37
+ }
38
+
39
+ /** Create one. It starts active. Not retried on failure — a retry could
40
+ * create a second row. */
41
+ async create(input: CreateOutcomeInput): Promise<Outcome> {
42
+ const res = await this.transport.request<{ created: true; outcome: Outcome }>({
43
+ method: "POST",
44
+ path: "/api/v1/outcomes",
45
+ body: input,
46
+ });
47
+ return res.outcome;
48
+ }
49
+
50
+ /** PARTIAL update: only the fields you send change. Leave `steps` out to
51
+ * leave the steps alone; send it and it is the complete list — pass each
52
+ * existing step's `id` (or its same `agentKey`) to keep it. */
53
+ async update(id: string, input: UpdateOutcomeInput): Promise<Outcome> {
54
+ const res = await this.transport.request<{ updated: true; outcome: Outcome }>({
55
+ method: "PATCH",
56
+ path: `/api/v1/outcomes/${encodeURIComponent(id)}`,
57
+ body: input,
58
+ });
59
+ return res.outcome;
60
+ }
61
+
62
+ /** Retire it (`isActive` → false). Nothing is deleted. Safe to retry. */
63
+ async archive(id: string): Promise<Outcome> {
64
+ const res = await this.transport.request<{ archived: true; outcome: Outcome }>({
65
+ method: "POST",
66
+ path: `/api/v1/outcomes/${encodeURIComponent(id)}/archive`,
67
+ retry: true,
68
+ });
69
+ return res.outcome;
70
+ }
71
+
72
+ /** Bring an archived one back (`isActive` → true). Safe to retry. */
73
+ async unarchive(id: string): Promise<Outcome> {
74
+ const res = await this.transport.request<{ unarchived: true; outcome: Outcome }>({
75
+ method: "POST",
76
+ path: `/api/v1/outcomes/${encodeURIComponent(id)}/unarchive`,
77
+ retry: true,
78
+ });
79
+ return res.outcome;
80
+ }
81
+ }
@@ -1,11 +1,16 @@
1
1
  import { activeQuery, filterByActive } from "../filters.js";
2
2
  import type { Transport } from "../http.js";
3
- import type { Plan } from "../types.js";
3
+ import type { CreatePlanInput, Plan, UpdatePlanInput } from "../types.js";
4
4
 
5
5
  /**
6
- * The `plans` resource — read the billing plans customers can subscribe to.
7
- * Each plan is a set of entitlement components (wallet / credit / outcome /
8
- * unit / flat) billed on a cycle. `/api/v1/plans`.
6
+ * The `plans` resource — the billing plans customers subscribe to. Each plan
7
+ * is a set of `entitlements` (wallet / credit / outcome / unit / composite /
8
+ * flat lines) billed on a cycle. `/api/v1/plans`.
9
+ *
10
+ * A plan's currency is the workspace's primary currency; it can't be chosen.
11
+ * There is no `delete()` — the API never deletes plans, so customers already
12
+ * on one keep what they bought. Retire a plan with `archive()`; `unarchive()`
13
+ * makes it purchasable again.
9
14
  */
10
15
  export class Plans {
11
16
  constructor(private readonly transport: Transport) {}
@@ -21,4 +26,57 @@ export class Plans {
21
26
  });
22
27
  return filterByActive(res.plans, params.active);
23
28
  }
29
+
30
+ /** One plan's configuration. */
31
+ async get(id: string): Promise<Plan> {
32
+ const res = await this.transport.request<{ plan: Plan }>({
33
+ method: "GET",
34
+ path: `/api/v1/plans/${encodeURIComponent(id)}`,
35
+ });
36
+ return res.plan;
37
+ }
38
+
39
+ /** Create a plan. It starts active. Not retried on failure — a retry could
40
+ * create a second plan. */
41
+ async create(input: CreatePlanInput): Promise<Plan> {
42
+ const res = await this.transport.request<{ created: true; plan: Plan }>({
43
+ method: "POST",
44
+ path: "/api/v1/plans",
45
+ body: input,
46
+ });
47
+ return res.plan;
48
+ }
49
+
50
+ /** PARTIAL update: only the fields you send change. `entitlements`, when
51
+ * sent, is the complete new list — the list `get()` returned can be sent
52
+ * back as it is. Changes apply to new purchases only. */
53
+ async update(id: string, input: UpdatePlanInput): Promise<Plan> {
54
+ const res = await this.transport.request<{ updated: true; plan: Plan }>({
55
+ method: "PATCH",
56
+ path: `/api/v1/plans/${encodeURIComponent(id)}`,
57
+ body: input,
58
+ });
59
+ return res.plan;
60
+ }
61
+
62
+ /** Retire a plan (`isActive` → false): it can't be bought any more; customers
63
+ * already on it are unaffected. Safe to retry. */
64
+ async archive(id: string): Promise<Plan> {
65
+ const res = await this.transport.request<{ archived: true; plan: Plan }>({
66
+ method: "POST",
67
+ path: `/api/v1/plans/${encodeURIComponent(id)}/archive`,
68
+ retry: true,
69
+ });
70
+ return res.plan;
71
+ }
72
+
73
+ /** Make an archived plan purchasable again (`isActive` → true). Safe to retry. */
74
+ async unarchive(id: string): Promise<Plan> {
75
+ const res = await this.transport.request<{ unarchived: true; plan: Plan }>({
76
+ method: "POST",
77
+ path: `/api/v1/plans/${encodeURIComponent(id)}/unarchive`,
78
+ retry: true,
79
+ });
80
+ return res.plan;
81
+ }
24
82
  }
@@ -69,12 +69,14 @@ export function signalToWire(signal: Signal): Record<string, unknown> {
69
69
  const body: Record<string, unknown> = {
70
70
  ...customerRefToWire(signal),
71
71
  // `usage` is required on all three model-priced kinds. input/output are
72
- // always sent; `cacheTokens` only when provided (the server defaults 0).
72
+ // always sent; `cacheTokens` / `cacheWriteTokens` only when provided (the
73
+ // server defaults each to 0).
73
74
  usage: {
74
75
  model: signal.model,
75
76
  inputTokens: t.input,
76
77
  outputTokens: t.output,
77
78
  ...(t.cache != null ? { cacheTokens: t.cache } : {}),
79
+ ...(t.cacheWrite != null ? { cacheWriteTokens: t.cacheWrite } : {}),
78
80
  },
79
81
  };
80
82
  if (signal.member) body.member = signal.member;
@@ -0,0 +1,76 @@
1
+ import { activeQuery, filterByActive } from "../filters.js";
2
+ import type { Transport } from "../http.js";
3
+ import type { CreateUnitInput, Unit, UpdateUnitInput } from "../types.js";
4
+
5
+ /**
6
+ * The `units` resource — fixed-price, non-LLM events (uploads, seats, API
7
+ * calls), priced FLAT, SLAB or VOLUME. `/api/v1/units`.
8
+ *
9
+ * There is no `delete()` — the API never deletes catalogue items. Retire a
10
+ * unit with `archive()`; `unarchive()` brings it back unchanged.
11
+ */
12
+ export class Units {
13
+ constructor(private readonly transport: Transport) {}
14
+
15
+ /** List them. `{ active: true }` returns only active ones, `{ active: false }`
16
+ * only archived ones; omit it for both. */
17
+ async list(params: { active?: boolean } = {}): Promise<Unit[]> {
18
+ const res = await this.transport.request<{ units: Unit[] }>({
19
+ method: "GET",
20
+ path: "/api/v1/units",
21
+ query: { active: activeQuery(params.active) },
22
+ });
23
+ return filterByActive(res.units, params.active);
24
+ }
25
+
26
+ /** One unit's configuration. */
27
+ async get(id: string): Promise<Unit> {
28
+ const res = await this.transport.request<{ unit: Unit }>({
29
+ method: "GET",
30
+ path: `/api/v1/units/${encodeURIComponent(id)}`,
31
+ });
32
+ return res.unit;
33
+ }
34
+
35
+ /** Create one. It starts active. Not retried on failure — a retry could
36
+ * create a second row. */
37
+ async create(input: CreateUnitInput): Promise<Unit> {
38
+ const res = await this.transport.request<{ created: true; unit: Unit }>({
39
+ method: "POST",
40
+ path: "/api/v1/units",
41
+ body: input,
42
+ });
43
+ return res.unit;
44
+ }
45
+
46
+ /** PARTIAL update: only the fields you send change. `tiers`, when sent, is
47
+ * the complete new tier list. */
48
+ async update(id: string, input: UpdateUnitInput): Promise<Unit> {
49
+ const res = await this.transport.request<{ updated: true; unit: Unit }>({
50
+ method: "PATCH",
51
+ path: `/api/v1/units/${encodeURIComponent(id)}`,
52
+ body: input,
53
+ });
54
+ return res.unit;
55
+ }
56
+
57
+ /** Retire it (`isActive` → false). Nothing is deleted. Safe to retry. */
58
+ async archive(id: string): Promise<Unit> {
59
+ const res = await this.transport.request<{ archived: true; unit: Unit }>({
60
+ method: "POST",
61
+ path: `/api/v1/units/${encodeURIComponent(id)}/archive`,
62
+ retry: true,
63
+ });
64
+ return res.unit;
65
+ }
66
+
67
+ /** Bring an archived one back (`isActive` → true). Safe to retry. */
68
+ async unarchive(id: string): Promise<Unit> {
69
+ const res = await this.transport.request<{ unarchived: true; unit: Unit }>({
70
+ method: "POST",
71
+ path: `/api/v1/units/${encodeURIComponent(id)}/unarchive`,
72
+ retry: true,
73
+ });
74
+ return res.unit;
75
+ }
76
+ }