@clocknext/sdk 0.13.0 → 0.15.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.
@@ -1,12 +1,14 @@
1
1
  /**
2
2
  * Basic usage: record a few signals from a long-lived Node process.
3
- * Run with: CLOCKNEXT_API_KEY=cnk_… npx tsx examples/node-basic.ts
4
3
  *
5
4
  * Note the discovery step first. `customerId`, `model` and `agentKey` are not
6
5
  * strings you can invent — they name rows that must already exist in your
7
6
  * workspace, and a signal naming one that does not is rejected. So this example
8
- * reads them out of the API rather than hard-coding placeholders that are
9
- * guaranteed to 422 on someone else's workspace.
7
+ * reads the customer and its agent keys out of the API rather than hard-coding
8
+ * placeholders that are guaranteed to 422 on someone else's workspace. The
9
+ * model id comes from Settings → Models in the dashboard:
10
+ *
11
+ * CLOCKNEXT_API_KEY=cnk_… CLOCKNEXT_MODEL=gpt-4o npx tsx examples/node-basic.ts
10
12
  */
11
13
  import { ClockNext } from "@clocknext/sdk";
12
14
 
@@ -18,34 +20,28 @@ const cnk = new ClockNext({
18
20
  });
19
21
 
20
22
  async function main() {
21
- // 0. Ground the integration. Check WHICH workspace this key points at before
22
- // sending anything — a live key bills for real.
23
- const { organization } = await cnk.workspace.me();
24
- console.log(`workspace: ${organization.name} (${organization.environment})`);
25
-
26
- // 1. Discover real ids instead of guessing them.
27
- const [models, credits, customers] = await Promise.all([
28
- cnk.workspace.models({ active: true }),
29
- cnk.credits.list({ active: true }),
30
- cnk.customers.list({ limit: 1 }),
31
- ]);
32
-
33
- const model = models[0];
34
- const credit = credits[0];
35
- const customer = customers.customers[0];
36
- if (!model || !credit || !customer) {
23
+ // 1. Discover real ids instead of guessing them. A customer's balances list
24
+ // every credit and outcome step its plan sells, each with its agentKey.
25
+ const model = process.env.CLOCKNEXT_MODEL;
26
+ const customer = (await cnk.customers.list({ limit: 1 })).customers[0];
27
+ if (!model || !customer) {
37
28
  throw new Error(
38
- "This workspace needs at least one enabled model, one credit type, and one customer before it can meter anything.",
29
+ "Set CLOCKNEXT_MODEL to a model enabled under Settings → Models, and create at least one customer, before metering anything.",
39
30
  );
40
31
  }
41
- console.log(`metering ${credit.agentKey} on ${model.modelId}`);
32
+ const balances = await cnk.customers.balances(customer.id);
33
+ const credit = balances.credits[0];
34
+ if (!credit) {
35
+ throw new Error("This customer's plan needs at least one credit before it can meter credit usage.");
36
+ }
37
+ console.log(`metering ${credit.agentKey} on ${model}`);
42
38
 
43
39
  // 2. Credit usage — metered against a credit by its stable `agentKey`, priced
44
40
  // by tokens. Optionally group it into a composite occurrence, billed once
45
41
  // on close.
46
42
  await cnk.signals.credit({
47
43
  customerId: customer.id,
48
- model: model.modelId,
44
+ model,
49
45
  agentKey: credit.agentKey,
50
46
  tokens: { input: 1200, output: 340, cache: 0 },
51
47
  custom: { feature: "summarize", tier: "pro" },
@@ -54,23 +50,19 @@ async function main() {
54
50
  // 3. Wallet usage (debits the customer's USD wallet at model cost).
55
51
  await cnk.signals.wallet({
56
52
  customerId: customer.id,
57
- model: model.modelId,
53
+ model,
58
54
  tokens: { input: 800, output: 5000 },
59
55
  });
60
56
 
61
57
  // 4. Outcome run — every step shares a `runId`; a separate, dedicated call
62
58
  // (`completeOutcome`) closes and bills the run — never a flag on a step.
63
- // `agentKey` is the OutcomeStep's key, which you read off
64
- // `cnk.outcomes.list()` the same way.
65
- // `outcomes.list()` returns catalogue rows WITHOUT their steps; the step
66
- // agent keys — which is what a signal reports against — are on the detail.
67
- const outcomeRow = (await cnk.outcomes.list({ active: true }))[0];
68
- const outcome = outcomeRow ? await cnk.outcomes.get(outcomeRow.id) : undefined;
59
+ // `agentKey` is the OutcomeStep's key, read off the same balances.
60
+ const outcome = balances.outcomes[0];
69
61
  if (outcome?.steps[0]) {
70
62
  const runId = `demo_run_${Date.now()}`;
71
63
  await cnk.signals.outcome({
72
64
  customerId: customer.id,
73
- model: model.modelId,
65
+ model,
74
66
  agentKey: outcome.steps[0].agentKey,
75
67
  runId,
76
68
  tokens: { input: 600, output: 150 },
@@ -83,8 +75,8 @@ async function main() {
83
75
  // asynchronous, so this — not the track() return value — is what proves a
84
76
  // signal actually landed.
85
77
  await cnk.flush();
86
- const usage = await cnk.signals.list({ customerId: customer.id, limit: 10 });
87
- console.log("recent logs:", usage.logs.length, "totals:", usage.totals);
78
+ const page = await cnk.signals.usageLog({ customerId: customer.id, limit: 10 });
79
+ console.log("recent logs:", page.usageLogs.length);
88
80
 
89
81
  await cnk.close();
90
82
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clocknext/sdk",
3
- "version": "0.13.0",
3
+ "version": "0.15.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,19 +1,13 @@
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";
6
4
  import { Customers } from "./resources/customers.js";
7
5
  import { Invoices } from "./resources/invoices.js";
8
- import { Outcomes } from "./resources/outcomes.js";
9
6
  import { Payments } from "./resources/payments.js";
10
7
  import { Plans } from "./resources/plans.js";
11
8
  import { Portal } from "./resources/portal.js";
12
9
  import { Purchases } from "./resources/purchases.js";
13
10
  import { Signals } from "./resources/signals.js";
14
- import { Units } from "./resources/units.js";
15
- import { Webhooks } from "./resources/webhooks.js";
16
- import { Workspace } from "./resources/workspace.js";
17
11
  import type { ClockNextConfig, ResolvedConfig } from "./types.js";
18
12
 
19
13
  /**
@@ -34,11 +28,11 @@ import type { ClockNextConfig, ResolvedConfig } from "./types.js";
34
28
  * ```
35
29
  */
36
30
  export class ClockNext {
37
- /** Record + read usage signals (credit / wallet / outcome / unit). */
31
+ /** Record usage signals (credit / wallet / outcome / unit) and read the usage log. */
38
32
  readonly signals: Signals;
39
- /** Manage customers, their members, wallet, and balances. */
33
+ /** Manage customers, their members, and balances. */
40
34
  readonly customers: Customers;
41
- /** Define and manage billing plans. */
35
+ /** List billing plans. */
42
36
  readonly plans: Plans;
43
37
  /** Subscribe customers to plans (purchases). */
44
38
  readonly purchases: Purchases;
@@ -46,20 +40,8 @@ export class ClockNext {
46
40
  readonly invoices: Invoices;
47
41
  /** Read payments (paid invoices). */
48
42
  readonly payments: Payments;
49
- /** Manage the credit-type catalog. */
50
- readonly credits: Credits;
51
- /** Manage the composite catalog — bundles billed as one thing. */
52
- readonly composites: Composites;
53
- /** Manage the outcome-type catalog. */
54
- readonly outcomes: Outcomes;
55
- /** Manage the unit-type catalog. */
56
- readonly units: Units;
57
- /** Configure threshold webhooks. */
58
- readonly webhooks: Webhooks;
59
43
  /** Mint customer-portal embed tokens. */
60
44
  readonly portal: Portal;
61
- /** Identify the workspace (sandbox/live) + list its models. `/me`, `/models`. */
62
- readonly workspace: Workspace;
63
45
 
64
46
  private readonly cfg: ResolvedConfig;
65
47
  private readonly flusher: Flusher;
@@ -81,13 +63,7 @@ export class ClockNext {
81
63
  this.purchases = new Purchases(transport);
82
64
  this.invoices = new Invoices(transport);
83
65
  this.payments = new Payments(transport);
84
- this.credits = new Credits(transport);
85
- this.composites = new Composites(transport);
86
- this.outcomes = new Outcomes(transport);
87
- this.units = new Units(transport);
88
- this.webhooks = new Webhooks(transport);
89
66
  this.portal = new Portal(transport);
90
- this.workspace = new Workspace(transport);
91
67
  }
92
68
 
93
69
  /** Number of signals currently buffered (async mode). */
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The customer half of a request body: `customerId` and/or `externalId`,
3
+ * each only when set. Both are forwarded as given — the server applies the
4
+ * precedence (`customerId` wins), so the SDK never second-guesses it.
5
+ */
6
+ export function customerRefToWire(ref: {
7
+ customerId?: string;
8
+ externalId?: string;
9
+ }): { customerId?: string; externalId?: string } {
10
+ return {
11
+ ...(ref.customerId != null ? { customerId: ref.customerId } : {}),
12
+ ...(ref.externalId != null ? { externalId: ref.externalId } : {}),
13
+ };
14
+ }
package/src/errors.ts CHANGED
@@ -154,7 +154,11 @@ export function translateFieldPaths(message: string): string {
154
154
  * toward it still classifies, never as the sole test.
155
155
  */
156
156
  const ALLOWANCE_MESSAGE =
157
- /\binsufficient\b|\bno allowance left\b|\bno slots left\b|\boutcomes remaining\b|\btop up the (?:allowance|balance)\b/i;
157
+ /\binsufficient\b|\bno allowance left\b|\bno slots left\b|\boutcomes remaining\b|\btop up the (?:allowance|balance)\b|\bbelow zero\b/i;
158
+
159
+ /** The adjust API's 422 for an idempotency key reused with a DIFFERENT
160
+ * request. A conflict, not a plan problem — and never worth retrying. */
161
+ const IDEMPOTENCY_KEY_REUSED = /\bidempotency key was already used\b/i;
158
162
 
159
163
  export function isAllowanceFailure(message: string, code?: string): boolean {
160
164
  if (code) return code === "ALLOWANCE_EXHAUSTED";
@@ -205,6 +209,9 @@ export function errorFromResponse(
205
209
  });
206
210
  }
207
211
  if (status === 422) {
212
+ if (IDEMPOTENCY_KEY_REUSED.test(raw)) {
213
+ return new ConflictError(raw, { ...opts, retryable: false });
214
+ }
208
215
  return isAllowanceFailure(raw, code)
209
216
  ? new AllowanceError(raw, opts)
210
217
  : new PlanError(raw, opts);
package/src/filters.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The `active` list filter, shared by every catalog resource.
2
+ * The `active` list filter, used by `plans.list()`.
3
3
  *
4
4
  * The API offers exactly ONE filter: `?active=true` narrows a list to active
5
5
  * rows. There is no inactive-only parameter — the server tests
package/src/index.ts CHANGED
@@ -47,7 +47,6 @@ export type {
47
47
  OutcomeCompletionInput,
48
48
  OutcomeCompletionResult,
49
49
  TrackResult,
50
- UsageList,
51
50
  SerializedUsageLog,
52
51
  UsageBalance,
53
52
  SerializedRuleApplication,
@@ -56,13 +55,15 @@ export type {
56
55
  UnitTrackResult,
57
56
  CompositeCompletionInput,
58
57
  CompositeCompletionResult,
59
- UnitUsageRow,
60
- UnitUsageList,
61
58
  SerializedUnitUsage,
62
- // workspace
63
- WorkspaceIdentity,
64
- Model,
59
+ UsageLogPeriod,
60
+ UsageLogListParams,
61
+ UsageLogAppliedRule,
62
+ UsageLogOutcomeAttribution,
63
+ UsageLogEntry,
64
+ UsageLogPage,
65
65
  // customers
66
+ CustomerRef,
66
67
  ApiCustomer,
67
68
  CustomerProfileInput,
68
69
  CreateCustomerInput,
@@ -79,30 +80,19 @@ export type {
79
80
  RemovedMember,
80
81
  AddMemberInput,
81
82
  UpdateMemberInput,
82
- // wallet & balance adjustments
83
- WalletTransaction,
84
- WalletTransactionType,
85
- WalletTransactionSource,
86
- WalletEntitlementKind,
87
- WalletList,
88
- WalletEntryInput,
83
+ // balance adjustments
84
+ WalletAdjustInput,
85
+ AdjustResult,
86
+ WalletPool,
89
87
  CreditAdjustInput,
90
88
  OutcomeAdjustInput,
91
89
  UnitAdjustInput,
92
90
  CompositeAdjustInput,
91
+ RecurringGrantInput,
92
+ RecurringGrantResult,
93
93
  // plans
94
94
  Plan,
95
- PlanDetail,
96
95
  PlanComponent,
97
- PlanComponentInput,
98
- WalletComponentInput,
99
- FlatComponentInput,
100
- CreditComponentInput,
101
- OutcomeComponentInput,
102
- UnitComponentInput,
103
- CompositeComponentInput,
104
- CreatePlanInput,
105
- UpdatePlanInput,
106
96
  BillingCycle,
107
97
  PlanType,
108
98
  PlanKind,
@@ -126,42 +116,6 @@ export type {
126
116
  // payments
127
117
  PaymentRow,
128
118
  PaymentDetail,
129
- // cost analytics (credits / outcomes)
130
- CostPoint,
131
- CostStats,
132
- CreditAnalytics,
133
- OutcomeAnalytics,
134
- // credits catalog
135
- Credit,
136
- CreditDetail,
137
- CreateCreditInput,
138
- UpdateCreditInput,
139
- ModelBundleEntry,
140
- // outcomes catalog
141
- Outcome,
142
- OutcomeDetail,
143
- OutcomeStepInput,
144
- CreateOutcomeInput,
145
- UpdateOutcomeInput,
146
- // composites catalog
147
- Composite,
148
- CompositeCatalogueRef,
149
- CreateCompositeInput,
150
- // units catalog
151
- Unit,
152
- UnitDetail,
153
- UnitCustomerRow,
154
- UnitTier,
155
- UnitTierInput,
156
- UnitPricingType,
157
- CreateUnitInput,
158
- FlatUnitInput,
159
- TieredUnitInput,
160
- UpdateUnitInput,
161
- // webhooks
162
- Webhook,
163
- UpsertWebhookInput,
164
- ThresholdKind,
165
119
  // portal
166
120
  PortalTokenInput,
167
121
  PortalToken,