deepline 0.2.9 → 0.2.11

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.
@@ -530,6 +530,8 @@ export interface ContextOptions {
530
530
  vercelProtectionBypassToken?: string | null;
531
531
  /** Optional per-run integration execution mode for provider calls. */
532
532
  integrationMode?: 'live' | 'eval_stub' | 'fixture';
533
+ /** Preview/dev test seam that applies provider pacing to fixture responses. */
534
+ enforceFixtureProviderPacing?: boolean;
533
535
  orgId?: string;
534
536
  userEmail?: string;
535
537
  playName?: string;
@@ -2,6 +2,7 @@ import type {
2
2
  EncryptedPostgresUrl,
3
3
  PostgresUrlEncryptionRequest,
4
4
  } from './db-session-crypto';
5
+ import { WORKFLOW_EXECUTOR_TOKEN_TTL_SECONDS } from './runtime-constants';
5
6
 
6
7
  // Workflow DB sessions must not expire before the workflow run/retry state they
7
8
  // pair with. Run/retry state lives 60 min (WORKFLOW_RUN_STATE_TTL_MS in the
@@ -11,6 +12,13 @@ import type {
11
12
  export const DB_SESSION_DEFAULT_TTL_SECONDS = 60 * 60;
12
13
  export const DB_SESSION_MAX_TTL_SECONDS = 60 * 60;
13
14
 
15
+ // A sandbox cannot mint new database authority after launch. Its preloaded
16
+ // sessions therefore have to remain valid for the same bounded activity as
17
+ // the executor token that unwraps them. Keep the shorter default/max above for
18
+ // renewable control-plane sessions; they do not govern launch-time preloads.
19
+ export const PRELOADED_RUNTIME_DB_SESSION_TTL_SECONDS =
20
+ WORKFLOW_EXECUTOR_TOKEN_TTL_SECONDS;
21
+
14
22
  export const DB_SESSION_OPERATIONS = [
15
23
  'rows.read',
16
24
  'rows.append',
@@ -122,6 +122,8 @@ export interface PlayRunnerContextConfig {
122
122
  runtimeTestFaultHeader?: string | null;
123
123
  vercelProtectionBypassToken?: string | null;
124
124
  integrationMode?: 'live' | 'eval_stub' | 'fixture';
125
+ /** Preview/dev test seam that applies provider pacing to fixture responses. */
126
+ enforceFixtureProviderPacing?: boolean;
125
127
  /** Immutable tool-error payload schema copied from the run contract. */
126
128
  toolErrorSchemaVersion?: ToolExecutionErrorSchemaVersion;
127
129
  orgId?: string;
@@ -4396,10 +4396,26 @@ export async function claimRuntimeWorkReceipts(
4396
4396
  leaseTtlMs?: number | null;
4397
4397
  },
4398
4398
  ): Promise<WorkReceiptClaim[]> {
4399
- const keys = [
4400
- ...new Set(input.keys.map((key) => key.trim()).filter(Boolean)),
4401
- ];
4402
- if (keys.length === 0) return [];
4399
+ if (
4400
+ input.leaseIds !== undefined &&
4401
+ input.leaseIds.length !== input.keys.length
4402
+ ) {
4403
+ throw new Error(
4404
+ `Runtime receipt bulk claim requires one lease ID per key. Received ${input.leaseIds.length} lease IDs for ${input.keys.length} keys.`,
4405
+ );
4406
+ }
4407
+ const positionalEntries = input.keys
4408
+ .map((key, originalIndex) => ({ key: key.trim(), originalIndex }))
4409
+ .filter((entry) => Boolean(entry.key));
4410
+ const positionalKeys = positionalEntries.map((entry) => entry.key);
4411
+ if (positionalKeys.length === 0) return [];
4412
+ const firstPositionByKey = new Map<string, number>();
4413
+ positionalEntries.forEach(({ key, originalIndex }) => {
4414
+ if (!firstPositionByKey.has(key)) {
4415
+ firstPositionByKey.set(key, originalIndex);
4416
+ }
4417
+ });
4418
+ const keys = [...firstPositionByKey.keys()];
4403
4419
  const session = await getRuntimeWorkReceiptSessionForKeys(context, {
4404
4420
  playName: input.playName,
4405
4421
  keys,
@@ -4409,16 +4425,9 @@ export async function claimRuntimeWorkReceipts(
4409
4425
  session,
4410
4426
  async (client) => {
4411
4427
  const keyHexes = keys.map(workReceiptKeyHex);
4412
- if (
4413
- input.leaseIds !== undefined &&
4414
- input.leaseIds.length !== keys.length
4415
- ) {
4416
- throw new Error(
4417
- `Runtime receipt bulk claim requires one lease ID per key. Received ${input.leaseIds.length} lease IDs for ${keys.length} keys.`,
4418
- );
4419
- }
4420
- const leaseIds = keys.map((_, index) => {
4421
- const providedLeaseId = input.leaseIds?.[index]?.trim();
4428
+ const leaseIds = keys.map((key) => {
4429
+ const position = firstPositionByKey.get(key)!;
4430
+ const providedLeaseId = input.leaseIds?.[position]?.trim();
4422
4431
  return (
4423
4432
  providedLeaseId ||
4424
4433
  (input.leaseAware === true ? newRuntimeWorkReceiptLeaseId() : null)
@@ -4604,19 +4613,40 @@ export async function claimRuntimeWorkReceipts(
4604
4613
  input.reclaimRunning === true,
4605
4614
  ],
4606
4615
  );
4607
- return rows.map((row) => {
4616
+ const claimsByKey = new Map<string, WorkReceiptClaim>();
4617
+ for (const row of rows) {
4608
4618
  const receipt = mapRuntimeWorkReceiptRow(row);
4609
4619
  if (row.claimed === true) {
4610
- return { disposition: 'claimed', receipt } satisfies WorkReceiptClaim;
4620
+ claimsByKey.set(receipt.key, {
4621
+ disposition: 'claimed',
4622
+ receipt,
4623
+ });
4624
+ continue;
4611
4625
  }
4612
4626
  if (input.forceRefresh !== true && isReusableWorkReceipt(receipt)) {
4613
- return { disposition: 'reused', receipt } satisfies WorkReceiptClaim;
4627
+ claimsByKey.set(receipt.key, {
4628
+ disposition: 'reused',
4629
+ receipt,
4630
+ });
4631
+ continue;
4614
4632
  }
4615
- return runtimeWorkReceiptClaimDispositionForBlockedReceipt({
4616
- receipt,
4617
- claimantRunId: input.runId,
4618
- claimantRunAttempt: runAttempt,
4619
- });
4633
+ claimsByKey.set(
4634
+ receipt.key,
4635
+ runtimeWorkReceiptClaimDispositionForBlockedReceipt({
4636
+ receipt,
4637
+ claimantRunId: input.runId,
4638
+ claimantRunAttempt: runAttempt,
4639
+ }),
4640
+ );
4641
+ }
4642
+ return positionalKeys.map((key) => {
4643
+ const claim = claimsByKey.get(key);
4644
+ if (!claim) {
4645
+ throw new Error(
4646
+ `Runtime receipt ${key} bulk claim did not return a positional result.`,
4647
+ );
4648
+ }
4649
+ return claim;
4620
4650
  });
4621
4651
  },
4622
4652
  );
@@ -69,6 +69,21 @@ export class RuntimeReceiptWriterClosedError extends Error {
69
69
  }
70
70
  }
71
71
 
72
+ export class RuntimeReceiptWriterRetryDeadlineError extends Error {
73
+ constructor(
74
+ readonly attempts: number,
75
+ readonly elapsedMs: number,
76
+ readonly maxRetryElapsedMs: number,
77
+ options?: { cause?: unknown },
78
+ ) {
79
+ super(
80
+ `Runtime receipt persistence did not recover within ${maxRetryElapsedMs}ms after ${attempts} attempts.`,
81
+ options,
82
+ );
83
+ this.name = 'RuntimeReceiptWriterRetryDeadlineError';
84
+ }
85
+ }
86
+
72
87
  /**
73
88
  * Private batching implementation used by a Play Durability Store Adapter.
74
89
  *
@@ -90,6 +105,7 @@ export class RuntimeReceiptWriter<Input, Output> {
90
105
  readonly #maxBatchBytes: number;
91
106
  readonly #maxBufferedBytes: number;
92
107
  readonly #maxFlushMs: number;
108
+ readonly #maxRetryElapsedMs: number;
93
109
  readonly #onRetryEvent:
94
110
  | ((event: RuntimeReceiptWriterRetryEvent<Input>) => void)
95
111
  | null;
@@ -121,6 +137,7 @@ export class RuntimeReceiptWriter<Input, Output> {
121
137
  maxBatchBytes?: number;
122
138
  maxBufferedBytes?: number;
123
139
  maxFlushMs?: number;
140
+ maxRetryElapsedMs?: number;
124
141
  onRetryEvent?: (event: RuntimeReceiptWriterRetryEvent<Input>) => void;
125
142
  }) {
126
143
  this.#batchKey = options.batchKey;
@@ -138,6 +155,10 @@ export class RuntimeReceiptWriter<Input, Output> {
138
155
  this.#maxBufferedBytes,
139
156
  );
140
157
  this.#maxFlushMs = Math.max(0, Math.floor(options.maxFlushMs ?? 5));
158
+ this.#maxRetryElapsedMs = normalizePositiveInteger(
159
+ options.maxRetryElapsedMs,
160
+ 2 * 60_000,
161
+ );
141
162
  this.#onRetryEvent = options.onRetryEvent ?? null;
142
163
  }
143
164
 
@@ -357,7 +378,24 @@ export class RuntimeReceiptWriter<Input, Output> {
357
378
  } catch (error) {
358
379
  const retry = this.#classifyRetryableError(error);
359
380
  if (!retry) throw error;
381
+ const elapsedMs = Date.now() - startedAt;
382
+ if (elapsedMs >= this.#maxRetryElapsedMs) {
383
+ throw new RuntimeReceiptWriterRetryDeadlineError(
384
+ attempt,
385
+ elapsedMs,
386
+ this.#maxRetryElapsedMs,
387
+ { cause: error },
388
+ );
389
+ }
360
390
  const retryAfterMs = jitteredRetryDelayMs(retry.retryAfterMs);
391
+ if (elapsedMs + retryAfterMs > this.#maxRetryElapsedMs) {
392
+ throw new RuntimeReceiptWriterRetryDeadlineError(
393
+ attempt,
394
+ elapsedMs,
395
+ this.#maxRetryElapsedMs,
396
+ { cause: error },
397
+ );
398
+ }
361
399
  this.#emitRetryEvent({
362
400
  phase: 'retry',
363
401
  attempt,
@@ -26,7 +26,7 @@ type ParsedRuntimeTestFaults =
26
26
 
27
27
  type RuntimeTestSeamContext = {
28
28
  internalTokenHeader?: string | null;
29
- syntheticRunHeader?: string | null;
29
+ verifiedSyntheticExecutor?: boolean;
30
30
  };
31
31
 
32
32
  type ParsedRuntimeTestFaultCounts =
@@ -44,6 +44,8 @@ type ValidatedRuntimeTestFaultHeader =
44
44
  export type RuntimeTestPolicyOverrides = {
45
45
  /** Opt-in bounded runner map latency profile for local/preview diagnosis. */
46
46
  mapLatencyProfile?: boolean;
47
+ /** Exercise provider pacing during fixture runs without dispatching provider traffic. */
48
+ enforceFixtureProviderPacing?: boolean;
47
49
  receiptLeaseTtlMs?: number;
48
50
  sheetAttemptLeaseMs?: number;
49
51
  heartbeatIntervalMs?: number;
@@ -117,10 +119,7 @@ function runtimeTestSeamsAuthorized(
117
119
  return (
118
120
  testRuntimeSeamsEnabled() ||
119
121
  internalTokenAllowsRuntimeSeams(context) ||
120
- Boolean(
121
- context?.syntheticRunHeader?.trim() &&
122
- context.syntheticRunHeader.trim() !== '0',
123
- )
122
+ context?.verifiedSyntheticExecutor === true
124
123
  );
125
124
  }
126
125
 
@@ -183,7 +182,8 @@ function parseRuntimeTestPolicyOverrides(
183
182
  (key) =>
184
183
  !RUNTIME_TEST_POLICY_MS_FIELDS.has(key) &&
185
184
  key !== 'workBudgetYieldLimits' &&
186
- key !== 'mapLatencyProfile',
185
+ key !== 'mapLatencyProfile' &&
186
+ key !== 'enforceFixtureProviderPacing',
187
187
  );
188
188
  if (unknownKeys.length > 0) {
189
189
  return {
@@ -200,6 +200,16 @@ function parseRuntimeTestPolicyOverrides(
200
200
  }
201
201
  overrides.mapLatencyProfile = record.mapLatencyProfile;
202
202
  }
203
+ if ('enforceFixtureProviderPacing' in record) {
204
+ if (typeof record.enforceFixtureProviderPacing !== 'boolean') {
205
+ return {
206
+ error:
207
+ 'testPolicyOverrides.enforceFixtureProviderPacing must be a boolean.',
208
+ };
209
+ }
210
+ overrides.enforceFixtureProviderPacing =
211
+ record.enforceFixtureProviderPacing;
212
+ }
203
213
  for (const field of RUNTIME_TEST_POLICY_MS_FIELDS) {
204
214
  if (!(field in record)) continue;
205
215
  const parsed = readPositiveIntegerField({
@@ -279,6 +289,7 @@ export function readRuntimeTestFaultRegistry(input: {
279
289
  headerValue: string | null;
280
290
  internalTokenHeader?: string | null;
281
291
  syntheticRunHeader?: string | null;
292
+ verifiedSyntheticExecutor?: boolean;
282
293
  }): ParsedRuntimeTestFaults {
283
294
  const headerValue = input.headerValue?.trim();
284
295
  if (!headerValue) {
@@ -320,6 +331,7 @@ export function validateRuntimeTestFaultHeader(input: {
320
331
  headerValue: string | null;
321
332
  internalTokenHeader?: string | null;
322
333
  syntheticRunHeader?: string | null;
334
+ verifiedSyntheticExecutor?: boolean;
323
335
  }): ValidatedRuntimeTestFaultHeader {
324
336
  const parsed = parseRuntimeTestFaultCounts(input);
325
337
  if (parsed.ok === false) {
@@ -332,6 +344,7 @@ export function parseRuntimeTestFaultCounts(input: {
332
344
  headerValue: string | null;
333
345
  internalTokenHeader?: string | null;
334
346
  syntheticRunHeader?: string | null;
347
+ verifiedSyntheticExecutor?: boolean;
335
348
  }): ParsedRuntimeTestFaultCounts {
336
349
  const headerValue = input.headerValue?.trim();
337
350
  if (!headerValue) {
package/dist/cli/index.js CHANGED
@@ -1040,7 +1040,7 @@ var SDK_RELEASE = {
1040
1040
  // 0.2.0 makes Dataset Handles uniformly async-only after 0.1.320 briefly
1041
1041
  // exposed storage-dependent synchronous access. This deliberate minor
1042
1042
  // release keeps lazy paging semantics independent of row residency.
1043
- version: "0.2.9",
1043
+ version: "0.2.11",
1044
1044
  contracts: {
1045
1045
  api: {
1046
1046
  name: "sdk-http-api",
@@ -2177,6 +2177,13 @@ function projectPlayRunActivity(input2) {
2177
2177
  (dataset) => dataset.phase === "registered" || dataset.complete !== true && dataset.phase !== "failed"
2178
2178
  ).sort((left, right) => (right.updatedAt ?? 0) - (left.updatedAt ?? 0))[0];
2179
2179
  if (pendingDataset) {
2180
+ const activeDatasetStep = input2.nodeStates?.find(
2181
+ (candidate) => candidate.nodeId === input2.activeNodeId && candidate.status === "running" && candidate.artifactTableNamespace === pendingDataset.tableNamespace
2182
+ );
2183
+ const datasetStep = activeDatasetStep ?? input2.nodeStates?.find(
2184
+ (candidate) => candidate.artifactTableNamespace === pendingDataset.tableNamespace
2185
+ );
2186
+ const datasetProgress = datasetStep?.progress ?? null;
2180
2187
  return projection(
2181
2188
  {
2182
2189
  schemaVersion: 1,
@@ -2189,12 +2196,12 @@ function projectPlayRunActivity(input2) {
2189
2196
  state: {
2190
2197
  kind: "active",
2191
2198
  progress: {
2192
- completed: pendingDataset.persistedRows,
2193
- total: typeof pendingDataset.succeededRows === "number" || typeof pendingDataset.failedRows === "number" ? (pendingDataset.succeededRows ?? 0) + (pendingDataset.failedRows ?? 0) : void 0,
2194
- failed: pendingDataset.failedRows
2199
+ completed: datasetProgress?.completed ?? pendingDataset.persistedRows,
2200
+ total: datasetProgress?.total ?? (typeof pendingDataset.succeededRows === "number" || typeof pendingDataset.failedRows === "number" ? (pendingDataset.succeededRows ?? 0) + (pendingDataset.failedRows ?? 0) : void 0),
2201
+ failed: datasetProgress?.failed ?? pendingDataset.failedRows
2195
2202
  }
2196
2203
  },
2197
- observedAt: pendingDataset.updatedAt ?? observedAt
2204
+ observedAt: datasetStep?.updatedAt ?? pendingDataset.updatedAt ?? observedAt
2198
2205
  },
2199
2206
  now
2200
2207
  );
@@ -3499,6 +3506,17 @@ function resolveToolExecuteTimeoutMs(toolId, input2) {
3499
3506
  }
3500
3507
  var RUNS_FAILED_LOG_LIMIT = 20;
3501
3508
  var RUN_LOGS_PAGE_LIMIT = 1e3;
3509
+ function requireTargetBillingIdempotencyKey(value) {
3510
+ const normalized = value.trim();
3511
+ if (normalized.length === 0 || normalized.length > 200 || normalized !== value) {
3512
+ throw new DeeplineError(
3513
+ "Billing idempotencyKey must contain 1\u2013200 characters with no leading or trailing whitespace.",
3514
+ void 0,
3515
+ "INVALID_BILLING_IDEMPOTENCY_KEY"
3516
+ );
3517
+ }
3518
+ return normalized;
3519
+ }
3502
3520
  function isRecord6(value) {
3503
3521
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
3504
3522
  }
@@ -3769,7 +3787,12 @@ var DeeplineClient = class {
3769
3787
  },
3770
3788
  invoices: {
3771
3789
  list: (options2) => this.listBillingInvoices(options2)
3772
- }
3790
+ },
3791
+ targetPlans: () => this.getTargetBillingPlans(),
3792
+ targetStatus: () => this.getTargetBillingStatus(),
3793
+ purchaseCredits: (options2) => this.purchaseTargetBillingCredits(options2),
3794
+ transitionPlan: (options2) => this.transitionTargetBillingPlan(options2),
3795
+ portalSession: () => this.createTargetBillingPortalSession()
3773
3796
  };
3774
3797
  this.monitors = {
3775
3798
  status: () => this.getMonitorsAccess(),
@@ -5683,6 +5706,52 @@ var DeeplineClient = class {
5683
5706
  `/api/v2/billing/invoices${suffix}`
5684
5707
  );
5685
5708
  }
5709
+ /** List the reviewed target plans and whether new acquisition is enabled. */
5710
+ async getTargetBillingPlans() {
5711
+ return this.http.get("/api/v2/billing/plans");
5712
+ }
5713
+ /** Read the workspace's normalized target plan, payment, and balance state. */
5714
+ async getTargetBillingStatus() {
5715
+ return this.http.get("/api/v2/billing/status");
5716
+ }
5717
+ /**
5718
+ * Purchase target-billing credits through the durable commercial operation
5719
+ * flow. The caller supplies an idempotency key for safe retries.
5720
+ */
5721
+ async purchaseTargetBillingCredits(options) {
5722
+ const idempotencyKey = requireTargetBillingIdempotencyKey(
5723
+ options.idempotencyKey
5724
+ );
5725
+ return this.http.post(
5726
+ "/api/v2/billing/credit-purchases",
5727
+ { credits: options.credits },
5728
+ { "Idempotency-Key": idempotencyKey },
5729
+ { maxRetries: 0, exactUrlOnly: true }
5730
+ );
5731
+ }
5732
+ /**
5733
+ * Start, change, cancel, or restore a target plan through one idempotent
5734
+ * commercial operation.
5735
+ */
5736
+ async transitionTargetBillingPlan(options) {
5737
+ const idempotencyKey = requireTargetBillingIdempotencyKey(
5738
+ options.idempotencyKey
5739
+ );
5740
+ return this.http.post(
5741
+ "/api/v2/billing/plan-transitions",
5742
+ {
5743
+ action: options.action,
5744
+ ...options.targetPlanSku ? { target_plan_sku: options.targetPlanSku } : {}
5745
+ },
5746
+ { "Idempotency-Key": idempotencyKey },
5747
+ { maxRetries: 0, exactUrlOnly: true }
5748
+ );
5749
+ }
5750
+ /** Create a Stripe-hosted portal session for payment recovery and invoices. */
5751
+ async createTargetBillingPortalSession() {
5752
+ const response = await this.http.post("/api/v2/billing/portal-sessions", {});
5753
+ return response.data;
5754
+ }
5686
5755
  // ——————————————————————————————————————————————————————————
5687
5756
  // Monitors
5688
5757
  // ——————————————————————————————————————————————————————————
@@ -7493,6 +7562,9 @@ function topUpIdempotencyKey(raw) {
7493
7562
  }
7494
7563
  return `cli_topup:${Date.now()}:${(0, import_node_crypto2.randomUUID)()}`;
7495
7564
  }
7565
+ function targetBillingIdempotencyKey(raw) {
7566
+ return typeof raw === "string" ? raw : `cli_target_billing:${Date.now()}:${(0, import_node_crypto2.randomUUID)()}`;
7567
+ }
7496
7568
  function checkoutCommandForCredits(credits) {
7497
7569
  return `deepline billing checkout --credits ${credits} --no-open --json`;
7498
7570
  }
@@ -8298,6 +8370,92 @@ async function handleTopUp(creditsRaw, options) {
8298
8370
  json: options.json
8299
8371
  });
8300
8372
  }
8373
+ async function handleTargetStatus(options) {
8374
+ const client2 = new DeeplineClient();
8375
+ const payload = await client2.billing.targetStatus();
8376
+ printCommandEnvelope(
8377
+ {
8378
+ ok: true,
8379
+ ...payload,
8380
+ render: {
8381
+ sections: [
8382
+ {
8383
+ title: "billing status",
8384
+ lines: [
8385
+ `Plan: ${payload.plan.name} (${payload.plan.sku})`,
8386
+ `State: ${payload.state}`,
8387
+ `Payment: ${payload.payment_state}`,
8388
+ `Automatic recharge: ${payload.recharge_state}`
8389
+ ]
8390
+ }
8391
+ ]
8392
+ }
8393
+ },
8394
+ { json: options.json }
8395
+ );
8396
+ }
8397
+ async function handleBuyCredits(creditsRaw, options) {
8398
+ const credits = parseTopUpCredits(creditsRaw);
8399
+ if (credits === null) {
8400
+ reportBillingFailure(
8401
+ {
8402
+ exitCode: 2,
8403
+ code: "INVALID_CREDITS",
8404
+ message: "<credits> must be a positive integer."
8405
+ },
8406
+ options
8407
+ );
8408
+ return;
8409
+ }
8410
+ const idempotencyKey = targetBillingIdempotencyKey(options.idempotencyKey);
8411
+ const payload = await new DeeplineClient().billing.purchaseCredits({
8412
+ credits,
8413
+ idempotencyKey
8414
+ });
8415
+ printCommandEnvelope(
8416
+ { ok: true, idempotency_key: idempotencyKey, ...payload },
8417
+ { json: options.json }
8418
+ );
8419
+ }
8420
+ async function handleTargetPlan(planSku, options) {
8421
+ if (planSku !== "payg-v1" && planSku !== "builder-v1" && planSku !== "team-v1") {
8422
+ reportBillingFailure(
8423
+ {
8424
+ exitCode: 2,
8425
+ code: "INVALID_PLAN",
8426
+ message: "Plan must be payg-v1, builder-v1, or team-v1."
8427
+ },
8428
+ options
8429
+ );
8430
+ return;
8431
+ }
8432
+ const idempotencyKey = targetBillingIdempotencyKey(options.idempotencyKey);
8433
+ const payload = await new DeeplineClient().billing.transitionPlan({
8434
+ action: "start_or_change",
8435
+ targetPlanSku: planSku,
8436
+ idempotencyKey
8437
+ });
8438
+ printCommandEnvelope(
8439
+ { ok: true, idempotency_key: idempotencyKey, ...payload },
8440
+ { json: options.json }
8441
+ );
8442
+ }
8443
+ async function handleTargetPlanCancellation(options) {
8444
+ const idempotencyKey = targetBillingIdempotencyKey(options.idempotencyKey);
8445
+ const payload = await new DeeplineClient().billing.transitionPlan({
8446
+ action: options.undo ? "undo_cancel" : "cancel",
8447
+ idempotencyKey
8448
+ });
8449
+ printCommandEnvelope(
8450
+ { ok: true, idempotency_key: idempotencyKey, ...payload },
8451
+ { json: options.json }
8452
+ );
8453
+ }
8454
+ async function handleTargetPortal(options) {
8455
+ const payload = await new DeeplineClient().billing.portalSession();
8456
+ if (!options.json && !options.noOpen) openInBrowser(payload.url);
8457
+ printCommandEnvelope({ ok: true, ...payload }, { json: options.json });
8458
+ }
8301
8459
  async function handleRedeemCode(code, options) {
8302
8460
  const { http } = getAuthedHttpClient();
8303
8461
  const payload = await http.post(
@@ -8493,6 +8651,11 @@ Examples:
8493
8651
  "--idempotency-key <key>",
8494
8652
  "Stable retry key for the same intended top-up"
8495
8653
  ).option("--dry-run", "Print the planned top-up without charging").option("--compact", "Keep only high-signal fields in JSON output").option("--json", "Emit JSON output. Also automatic when stdout is piped").action(handleTopUp);
8654
+ billing.command("buy").description("Buy credits through the target billing contract.").argument("<credits>", "Positive integer Deepline credit amount").option("--idempotency-key <key>", "Stable retry key").option("--json", "Emit JSON output").action(handleBuyCredits);
8655
+ billing.command("status").description("Show normalized target billing state.").option("--json", "Emit JSON output").action(handleTargetStatus);
8656
+ billing.command("change-plan").description("Start or change the target billing plan.").argument("<plan_sku>", "payg-v1, builder-v1, or team-v1").option("--idempotency-key <key>", "Stable retry key").option("--json", "Emit JSON output").action(handleTargetPlan);
8657
+ billing.command("cancel-plan").description("Cancel a target subscription at period end, or undo it.").option("--undo", "Undo a pending period-end cancellation").option("--idempotency-key <key>", "Stable retry key").option("--json", "Emit JSON output").action(handleTargetPlanCancellation);
8658
+ billing.command("portal").description("Open the Stripe-hosted billing recovery portal.").option("--no-open", "Print the URL without opening a browser").option("--json", "Emit JSON output").action(handleTargetPortal);
8496
8659
  billing.command("plans").description("Show published plans and the plan you are on.").addHelpText(
8497
8660
  "after",
8498
8661
  `