deepline 0.1.104 → 0.1.105

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/dist/cli/index.js CHANGED
@@ -239,10 +239,12 @@ var SDK_RELEASE = {
239
239
  // scrubbing, and word-boundary watch truncation.
240
240
  // 0.1.103 ships the refined SDK CLI command surface.
241
241
  // 0.1.104 ships postgres_fast suspension/billing parity and runtime worker hardening.
242
- version: "0.1.104",
242
+ // 0.1.105 ships the billing catalog surface: billing plans, subscribe,
243
+ // subscription status/cancel, invoices, and the client.billing namespace.
244
+ version: "0.1.105",
243
245
  apiContract: "2026-06-dataset-column-cell-stale-hard-cutover",
244
246
  supportPolicy: {
245
- latest: "0.1.104",
247
+ latest: "0.1.105",
246
248
  minimumSupported: "0.1.53",
247
249
  deprecatedBelow: "0.1.53"
248
250
  }
@@ -373,7 +375,7 @@ var HttpClient = class {
373
375
  signal: controller.signal
374
376
  });
375
377
  clearTimeout(timeoutId);
376
- if (response.status === 401 || response.status === 403) {
378
+ if (response.status === 401 || response.status === 403 && !options?.forbiddenAsApiError) {
377
379
  throw new AuthError();
378
380
  }
379
381
  if (response.status === 429) {
@@ -1465,6 +1467,9 @@ var INCLUDE_TOOL_METADATA_HEADER = "x-deepline-include-tool-metadata";
1465
1467
  var EXECUTE_RESPONSE_CONTRACT_HEADER = "x-deepline-execute-response-contract";
1466
1468
  var V2_EXECUTE_RESPONSE_CONTRACT = "v2-tool-response";
1467
1469
  var COMPILE_MANIFEST_RETRY_DELAYS_MS = [250, 1e3];
1470
+ var REGISTER_PLAY_ARTIFACTS_COMPILE_CONCURRENCY = 3;
1471
+ var REGISTER_PLAY_ARTIFACTS_MAX_BATCH_COUNT = 3;
1472
+ var REGISTER_PLAY_ARTIFACTS_MAX_BATCH_BYTES = 25e5;
1468
1473
  function sleep2(ms) {
1469
1474
  return new Promise((resolve16) => setTimeout(resolve16, ms));
1470
1475
  }
@@ -1477,6 +1482,45 @@ function isTransientCompileManifestError(error) {
1477
1482
  message
1478
1483
  );
1479
1484
  }
1485
+ async function mapWithConcurrency(items, concurrency, mapper) {
1486
+ const results = new Array(items.length);
1487
+ let nextIndex = 0;
1488
+ const workerCount = Math.min(Math.max(1, concurrency), items.length);
1489
+ await Promise.all(
1490
+ Array.from({ length: workerCount }, async () => {
1491
+ for (; ; ) {
1492
+ const index = nextIndex;
1493
+ nextIndex += 1;
1494
+ if (index >= items.length) {
1495
+ return;
1496
+ }
1497
+ results[index] = await mapper(items[index], index);
1498
+ }
1499
+ })
1500
+ );
1501
+ return results;
1502
+ }
1503
+ function jsonUtf8Bytes(value) {
1504
+ return new TextEncoder().encode(JSON.stringify(value)).length;
1505
+ }
1506
+ function chunkRegisterPlayArtifacts(artifacts) {
1507
+ const chunks = [];
1508
+ let current = [];
1509
+ for (const artifact of artifacts) {
1510
+ const candidate = [...current, artifact];
1511
+ const candidateTooLarge = candidate.length > REGISTER_PLAY_ARTIFACTS_MAX_BATCH_COUNT || jsonUtf8Bytes({ artifacts: candidate }) > REGISTER_PLAY_ARTIFACTS_MAX_BATCH_BYTES;
1512
+ if (current.length > 0 && candidateTooLarge) {
1513
+ chunks.push(current);
1514
+ current = [artifact];
1515
+ } else {
1516
+ current = candidate;
1517
+ }
1518
+ }
1519
+ if (current.length > 0) {
1520
+ chunks.push(current);
1521
+ }
1522
+ return chunks;
1523
+ }
1480
1524
  var RUN_LOGS_PAGE_LIMIT = 1e3;
1481
1525
  function isRecord2(value) {
1482
1526
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
@@ -1644,6 +1688,8 @@ var DeeplineClient = class {
1644
1688
  config;
1645
1689
  /** Canonical run lifecycle namespace backed by `/api/v2/runs`. */
1646
1690
  runs;
1691
+ /** Billing namespace: subscription status/cancel and invoice history. */
1692
+ billing;
1647
1693
  /**
1648
1694
  * Create a low-level SDK client.
1649
1695
  *
@@ -1664,6 +1710,16 @@ var DeeplineClient = class {
1664
1710
  exportDatasetRows: (input2) => this.getPlaySheetRows(input2),
1665
1711
  stop: (runId, options2) => this.stopRun(runId, options2)
1666
1712
  };
1713
+ this.billing = {
1714
+ plans: () => this.getBillingPlans(),
1715
+ subscription: {
1716
+ status: () => this.getBillingSubscriptionStatus(),
1717
+ cancel: (options2) => this.cancelBillingSubscription(options2)
1718
+ },
1719
+ invoices: {
1720
+ list: (options2) => this.listBillingInvoices(options2)
1721
+ }
1722
+ };
1667
1723
  }
1668
1724
  /** The resolved base URL this client is targeting (e.g. `"http://localhost:3000"`). */
1669
1725
  get baseUrl() {
@@ -2032,8 +2088,13 @@ var DeeplineClient = class {
2032
2088
  * first when a compiler manifest is not already supplied.
2033
2089
  */
2034
2090
  async registerPlayArtifacts(artifacts) {
2035
- const compiledArtifacts = await Promise.all(
2036
- artifacts.map(async (artifact) => ({
2091
+ if (artifacts.length === 0) {
2092
+ return this.http.post("/api/v2/plays/artifacts", { artifacts });
2093
+ }
2094
+ const compiledArtifacts = await mapWithConcurrency(
2095
+ artifacts,
2096
+ REGISTER_PLAY_ARTIFACTS_COMPILE_CONCURRENCY,
2097
+ async (artifact) => ({
2037
2098
  ...artifact,
2038
2099
  compilerManifest: artifact.compilerManifest ?? await this.compilePlayManifest({
2039
2100
  name: artifact.name,
@@ -2041,11 +2102,20 @@ var DeeplineClient = class {
2041
2102
  sourceFiles: artifact.sourceFiles,
2042
2103
  artifact: artifact.artifact
2043
2104
  })
2044
- }))
2105
+ })
2045
2106
  );
2046
- return this.http.post("/api/v2/plays/artifacts", {
2047
- artifacts: compiledArtifacts
2048
- });
2107
+ const responses = [];
2108
+ for (const chunk of chunkRegisterPlayArtifacts(compiledArtifacts)) {
2109
+ responses.push(
2110
+ await this.http.post("/api/v2/plays/artifacts", {
2111
+ artifacts: chunk
2112
+ })
2113
+ );
2114
+ }
2115
+ return {
2116
+ success: responses.every((response) => response.success),
2117
+ artifacts: responses.flatMap((response) => response.artifacts)
2118
+ };
2049
2119
  }
2050
2120
  /**
2051
2121
  * Compile a bundled play artifact into the server-side compiler manifest.
@@ -3018,6 +3088,61 @@ var DeeplineClient = class {
3018
3088
  // ——————————————————————————————————————————————————————————
3019
3089
  // Health
3020
3090
  // ——————————————————————————————————————————————————————————
3091
+ /**
3092
+ * Published plans plus the caller's active plan: prices, monthly grant
3093
+ * credits, rollover policy, and which plans are open for subscription.
3094
+ * Prefer `client.billing.plans()`.
3095
+ *
3096
+ * @returns Snake_case catalog from `GET /api/v2/billing/catalog/current`
3097
+ */
3098
+ async getBillingPlans() {
3099
+ return this.http.get("/api/v2/billing/catalog/current");
3100
+ }
3101
+ /**
3102
+ * Subscription state for the active workspace: active plan, whether a
3103
+ * Stripe subscription backs it, renewal/cancellation facts, and remaining
3104
+ * Deepline credit pools. Prefer `client.billing.subscription.status()`.
3105
+ *
3106
+ * @returns Snake_case subscription status from `GET /api/v2/billing/subscription/status`
3107
+ */
3108
+ async getBillingSubscriptionStatus() {
3109
+ return this.http.get(
3110
+ "/api/v2/billing/subscription/status"
3111
+ );
3112
+ }
3113
+ /**
3114
+ * Schedule subscription cancellation at period end, or reverse a pending
3115
+ * cancellation with `{ undo: true }`. The customer keeps the cycle they
3116
+ * paid for and every remaining credit — cancellation never claws back
3117
+ * credits. Prefer `client.billing.subscription.cancel(...)`.
3118
+ *
3119
+ * @throws {@link DeeplineError} with `statusCode: 409` when the workspace
3120
+ * has no active subscription, and `statusCode: 502` when Stripe rejects
3121
+ * the update (the server message is preserved).
3122
+ */
3123
+ async cancelBillingSubscription(options) {
3124
+ return this.http.post(
3125
+ "/api/v2/billing/subscription/cancel",
3126
+ { action: options?.undo ? "undo_cancel" : "cancel" }
3127
+ );
3128
+ }
3129
+ /**
3130
+ * Customer-facing billing history: subscription invoices plus one-time
3131
+ * credit purchase receipts, newest first, with Stripe-hosted links.
3132
+ * Prefer `client.billing.invoices.list(...)`.
3133
+ *
3134
+ * @param options.limit - Maximum entries to return (server clamps to 1–100, default 24).
3135
+ */
3136
+ async listBillingInvoices(options) {
3137
+ const params = new URLSearchParams();
3138
+ if (options?.limit !== void 0) {
3139
+ params.set("limit", String(options.limit));
3140
+ }
3141
+ const suffix = Array.from(params).length > 0 ? `?${params.toString()}` : "";
3142
+ return this.http.get(
3143
+ `/api/v2/billing/invoices${suffix}`
3144
+ );
3145
+ }
3021
3146
  /**
3022
3147
  * Check API connectivity and server health.
3023
3148
  *
@@ -4142,6 +4267,56 @@ var import_commander = require("commander");
4142
4267
  var import_promises2 = require("fs/promises");
4143
4268
  var import_node_path5 = require("path");
4144
4269
  var import_sync3 = require("csv-stringify/sync");
4270
+ var SUBSCRIPTION_STATUS_NEXT_COMMAND = "deepline billing subscription status --json";
4271
+ var SUBSCRIPTION_CANCEL_PATH = "/api/v2/billing/subscription/cancel";
4272
+ function billingFailureFromError(error, options) {
4273
+ if (error instanceof AuthError) {
4274
+ return {
4275
+ exitCode: 3,
4276
+ code: "AUTH_ERROR",
4277
+ message: error.message,
4278
+ next: "deepline auth status --json"
4279
+ };
4280
+ }
4281
+ if (!(error instanceof DeeplineError) || typeof error.statusCode !== "number") {
4282
+ return null;
4283
+ }
4284
+ if (error.statusCode === 404 || error.statusCode === 409) {
4285
+ return {
4286
+ exitCode: 4,
4287
+ code: options.notFoundCode,
4288
+ message: error.message,
4289
+ next: SUBSCRIPTION_STATUS_NEXT_COMMAND
4290
+ };
4291
+ }
4292
+ if (error.statusCode >= 500) {
4293
+ return {
4294
+ exitCode: 5,
4295
+ code: "BILLING_SERVER_ERROR",
4296
+ message: error.message,
4297
+ next: SUBSCRIPTION_STATUS_NEXT_COMMAND
4298
+ };
4299
+ }
4300
+ return null;
4301
+ }
4302
+ function reportBillingFailure(failure, options) {
4303
+ const textLines = [failure.message];
4304
+ if (failure.next) {
4305
+ textLines.push(`Next: ${failure.next}`);
4306
+ }
4307
+ printCommandEnvelope(
4308
+ {
4309
+ ok: false,
4310
+ exitCode: failure.exitCode,
4311
+ code: failure.code,
4312
+ message: failure.message,
4313
+ ...failure.next ? { next: failure.next } : {}
4314
+ },
4315
+ { json: options.json, text: `${textLines.join("\n")}
4316
+ ` }
4317
+ );
4318
+ process.exitCode = failure.exitCode;
4319
+ }
4145
4320
  function humanize(value) {
4146
4321
  return String(value || "").split("_").filter(Boolean).map((token) => token[0]?.toUpperCase() + token.slice(1)).join(" ") || "Unknown";
4147
4322
  }
@@ -4417,6 +4592,273 @@ async function handleLedgerExportAll(options) {
4417
4592
  { json: options.json }
4418
4593
  );
4419
4594
  }
4595
+ function planPriceText(priceUsd, priceInterval) {
4596
+ if (priceUsd === null || priceUsd === void 0) {
4597
+ return "no list price";
4598
+ }
4599
+ return `$${priceUsd}${priceInterval ? `/${priceInterval}` : ""}`;
4600
+ }
4601
+ function planRolloverText(rollover) {
4602
+ const mode = rollover?.mode ?? "none";
4603
+ if (mode === "none") return "no rollover";
4604
+ if (mode === "bounded") {
4605
+ return `rollover up to ${rollover?.max_credits ?? "(unknown)"} credits`;
4606
+ }
4607
+ return `rollover ${mode}`;
4608
+ }
4609
+ async function handlePlans(options) {
4610
+ const { http } = getAuthedHttpClient();
4611
+ const payload = await http.get(
4612
+ "/api/v2/billing/catalog/current"
4613
+ );
4614
+ const activePlan = payload.active_plan ?? {};
4615
+ const plans = Array.isArray(payload.plans) ? payload.plans : [];
4616
+ const metrics = Array.isArray(payload.metrics) ? payload.metrics : [];
4617
+ const lines = [
4618
+ `Catalog: ${payload.catalog_id ?? "(unknown)"} (version ${payload.catalog_version ?? "(unknown)"})`,
4619
+ `Active plan: ${activePlan.public_name ?? "(unknown)"} (${activePlan.plan_version_id ?? "(unknown)"})`,
4620
+ `Assigned by: ${activePlan.assigned_by ?? "default"} | subscription: ${activePlan.has_subscription ? "yes" : "no"}`,
4621
+ ...plans.length === 0 ? ["Plans: none published"] : [
4622
+ "Plans:",
4623
+ ...plans.map(
4624
+ (plan) => `${plan.public_name ?? "(unknown)"} (${plan.plan_version_id ?? "(unknown)"}) | ${planPriceText(plan.price_usd, plan.price_interval)} | ${plan.monthly_grant_credits ?? 0} credits/cycle | ${planRolloverText(plan.rollover)} | ${plan.acquirable ? "acquirable" : "not acquirable"}`
4625
+ )
4626
+ ],
4627
+ ...metrics.length === 0 ? ["Metrics: none"] : [
4628
+ "Metrics:",
4629
+ ...metrics.map(
4630
+ (metric) => `${metric.id ?? "(unknown)"} | ${metric.name ?? "(unknown)"}`
4631
+ )
4632
+ ]
4633
+ ];
4634
+ printCommandEnvelope(
4635
+ {
4636
+ ...payload,
4637
+ render: { sections: [{ title: "billing plans", lines }] }
4638
+ },
4639
+ { json: options.json }
4640
+ );
4641
+ }
4642
+ async function handleSubscribe(planVersionId, options) {
4643
+ const { http } = getAuthedHttpClient();
4644
+ const payload = await http.request(
4645
+ "/api/v2/billing/subscription/checkout",
4646
+ {
4647
+ method: "POST",
4648
+ body: { plan_version_id: planVersionId },
4649
+ forbiddenAsApiError: true
4650
+ }
4651
+ );
4652
+ const url = String(payload.checkout_url || "");
4653
+ if (!options.json && options.open !== false && url) openInBrowser(url);
4654
+ printCommandEnvelope(
4655
+ {
4656
+ ...payload,
4657
+ render: {
4658
+ sections: [
4659
+ {
4660
+ title: "billing subscribe",
4661
+ lines: [url || "Subscription checkout session created."]
4662
+ }
4663
+ ]
4664
+ }
4665
+ },
4666
+ { json: options.json }
4667
+ );
4668
+ }
4669
+ async function handleSubscriptionStatus(options) {
4670
+ const client2 = new DeeplineClient();
4671
+ const payload = await client2.billing.subscription.status();
4672
+ const pools = payload.credit_pools ?? [];
4673
+ const lines = [
4674
+ `Plan: ${payload.plan_name ?? "(unknown)"} (${payload.plan_version_id ?? "(unknown)"})`,
4675
+ `Subscribed: ${payload.subscribed ? "yes" : "no"}`,
4676
+ ...payload.cancel_at_period_end ? [
4677
+ `Cancellation scheduled: ends ${payload.current_period_end ?? "(unknown)"} at period end (credits are kept)`
4678
+ ] : [],
4679
+ `Price: ${planPriceText(payload.price_usd, payload.price_interval)}`,
4680
+ `Monthly grant: ${payload.monthly_grant_credits ?? 0} credits`,
4681
+ `Pooled credits remaining: ${payload.pooled_credits_remaining ?? 0}`,
4682
+ ...pools.length === 0 ? ["Credit pools: none"] : [
4683
+ "Credit pools:",
4684
+ ...pools.map(
4685
+ (pool) => `${pool.pool ?? "(unknown)"} | ${pool.credits_remaining ?? 0}/${pool.credits_granted ?? 0} credits remaining | ${pool.source ?? "(unknown)"}`
4686
+ )
4687
+ ],
4688
+ `Assigned by: ${payload.assigned_by ?? "default"}`
4689
+ ];
4690
+ printCommandEnvelope(
4691
+ {
4692
+ ...payload,
4693
+ render: { sections: [{ title: "billing subscription", lines }] }
4694
+ },
4695
+ { json: options.json }
4696
+ );
4697
+ }
4698
+ async function handleSubscriptionCancel(options) {
4699
+ const client2 = new DeeplineClient();
4700
+ const action = options.undo ? "undo_cancel" : "cancel";
4701
+ if (options.dryRun) {
4702
+ const status = await client2.billing.subscription.status();
4703
+ if (!status.subscribed) {
4704
+ reportBillingFailure(
4705
+ {
4706
+ exitCode: 4,
4707
+ code: "NO_ACTIVE_SUBSCRIPTION",
4708
+ message: "This workspace has no active subscription to cancel.",
4709
+ next: SUBSCRIPTION_STATUS_NEXT_COMMAND
4710
+ },
4711
+ options
4712
+ );
4713
+ return;
4714
+ }
4715
+ const consequence = action === "cancel" ? `Would schedule cancellation at period end${status.current_period_end ? ` (${status.current_period_end})` : ""}. The subscription stays active until then and remaining credits are kept.` : "Would reverse the pending cancellation; the subscription would renew normally.";
4716
+ printCommandEnvelope(
4717
+ {
4718
+ ok: true,
4719
+ dry_run: true,
4720
+ action,
4721
+ plan_version_id: status.plan_version_id,
4722
+ plan_name: status.plan_name,
4723
+ cancel_at_period_end: status.cancel_at_period_end,
4724
+ current_period_end: status.current_period_end,
4725
+ consequence,
4726
+ planned_request: {
4727
+ method: "POST",
4728
+ path: SUBSCRIPTION_CANCEL_PATH,
4729
+ body: { action }
4730
+ },
4731
+ render: {
4732
+ sections: [
4733
+ {
4734
+ title: "billing subscription cancel (dry run)",
4735
+ lines: [
4736
+ `Plan: ${status.plan_name} (${status.plan_version_id})`,
4737
+ `Planned request: POST ${SUBSCRIPTION_CANCEL_PATH} {"action":"${action}"}`,
4738
+ consequence,
4739
+ "No request was sent. Re-run without --dry-run to apply."
4740
+ ]
4741
+ }
4742
+ ]
4743
+ }
4744
+ },
4745
+ { json: options.json }
4746
+ );
4747
+ return;
4748
+ }
4749
+ let result;
4750
+ try {
4751
+ result = await client2.billing.subscription.cancel({
4752
+ undo: Boolean(options.undo)
4753
+ });
4754
+ } catch (error) {
4755
+ const failure = billingFailureFromError(error, {
4756
+ notFoundCode: "NO_ACTIVE_SUBSCRIPTION"
4757
+ });
4758
+ if (!failure) throw error;
4759
+ reportBillingFailure(failure, options);
4760
+ return;
4761
+ }
4762
+ const lines = options.undo ? [
4763
+ "Pending cancellation reversed; the subscription will renew normally.",
4764
+ `Subscription: ${result.subscription_id}`
4765
+ ] : [
4766
+ `Cancellation scheduled for subscription ${result.subscription_id}.`,
4767
+ `The subscription stays active until ${result.current_period_end ?? "the end of the current billing period"}, then cancels at period end.`,
4768
+ "Remaining credits are yours to keep \u2014 cancellation never removes credits.",
4769
+ "Undo with: deepline billing subscription cancel --undo"
4770
+ ];
4771
+ printCommandEnvelope(
4772
+ {
4773
+ ok: true,
4774
+ ...result,
4775
+ render: {
4776
+ sections: [{ title: "billing subscription cancel", lines }]
4777
+ }
4778
+ },
4779
+ { json: options.json }
4780
+ );
4781
+ }
4782
+ function invoiceAmountText(amountCents, currency) {
4783
+ const value = (Number(amountCents ?? 0) / 100).toFixed(2);
4784
+ return String(currency ?? "usd").toLowerCase() === "usd" ? `$${value}` : `${value} ${String(currency).toUpperCase()}`;
4785
+ }
4786
+ function invoiceDateText(createdAt) {
4787
+ return String(createdAt ?? "").slice(0, 10);
4788
+ }
4789
+ function invoiceLine(entry, compact) {
4790
+ const amount = invoiceAmountText(entry.amount_cents, entry.currency);
4791
+ const link = entry.url ?? "(no link)";
4792
+ if (compact) {
4793
+ return [
4794
+ entry.id,
4795
+ invoiceDateText(entry.created_at),
4796
+ amount,
4797
+ entry.status,
4798
+ link
4799
+ ].join(" | ");
4800
+ }
4801
+ return [
4802
+ invoiceDateText(entry.created_at),
4803
+ entry.description,
4804
+ amount,
4805
+ entry.status,
4806
+ link
4807
+ ].join(" | ");
4808
+ }
4809
+ async function handleInvoices(options) {
4810
+ let limit;
4811
+ if (options.limit !== void 0) {
4812
+ limit = Number.parseInt(options.limit, 10);
4813
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
4814
+ reportBillingFailure(
4815
+ {
4816
+ exitCode: 2,
4817
+ code: "INVALID_LIMIT",
4818
+ message: "--limit must be an integer between 1 and 100."
4819
+ },
4820
+ options
4821
+ );
4822
+ return;
4823
+ }
4824
+ }
4825
+ const client2 = new DeeplineClient();
4826
+ let payload;
4827
+ try {
4828
+ payload = await client2.billing.invoices.list(
4829
+ limit === void 0 ? void 0 : { limit }
4830
+ );
4831
+ } catch (error) {
4832
+ const failure = billingFailureFromError(error, {
4833
+ notFoundCode: "NOT_FOUND"
4834
+ });
4835
+ if (!failure) throw error;
4836
+ reportBillingFailure(failure, options);
4837
+ return;
4838
+ }
4839
+ const entries = Array.isArray(payload.entries) ? payload.entries : [];
4840
+ const compact = Boolean(options.compact);
4841
+ const header = compact ? "id | date | amount | status | link" : "date | description | amount | status | link";
4842
+ const lines = entries.length === 0 ? ["No invoices or receipts yet."] : [header, ...entries.map((entry) => invoiceLine(entry, compact))];
4843
+ const envelope = compact ? {
4844
+ org_id: payload.org_id,
4845
+ count: entries.length,
4846
+ entries: entries.map((entry) => ({
4847
+ id: entry.id,
4848
+ date: invoiceDateText(entry.created_at),
4849
+ amount: invoiceAmountText(entry.amount_cents, entry.currency),
4850
+ status: entry.status,
4851
+ url: entry.url
4852
+ }))
4853
+ } : { ...payload, count: entries.length };
4854
+ printCommandEnvelope(
4855
+ {
4856
+ ...envelope,
4857
+ render: { sections: [{ title: "billing invoices", lines }] }
4858
+ },
4859
+ { json: options.json }
4860
+ );
4861
+ }
4420
4862
  async function handleCheckout(options) {
4421
4863
  const { http } = getAuthedHttpClient();
4422
4864
  const payload = await http.post(
@@ -4465,19 +4907,33 @@ async function handleRedeemCode(code, options) {
4465
4907
  );
4466
4908
  }
4467
4909
  function registerBillingCommands(program) {
4468
- const billing = program.command("billing").description("Inspect balance, usage, limits, and checkout flows.").addHelpText(
4910
+ const billing = program.command("billing").description("See your plan and credits, buy more, and manage billing.").addHelpText(
4469
4911
  "after",
4470
4912
  `
4471
4913
  Concepts:
4472
4914
  Billing commands show Deepline credits, not raw provider spend.
4473
- set-limit/off mutate the monthly workspace cap. checkout/redeem-code can open
4474
- a browser unless --no-open is set.
4915
+ checkout/subscribe/redeem-code can open a browser unless --no-open is set.
4475
4916
 
4476
4917
  Examples:
4918
+ # See where you stand
4477
4919
  deepline billing balance --json
4478
4920
  deepline billing usage --limit 20 --json
4479
- deepline billing set-limit 500 --json
4921
+ deepline billing plans --json
4922
+ deepline billing subscription status --json
4923
+
4924
+ # Buy credits or a plan
4480
4925
  deepline billing checkout --credits 1000 --no-open --json
4926
+ deepline billing subscribe runtime-395-2026-07-v1 --no-open --json
4927
+ deepline billing redeem-code --code ABC123 --no-open --json
4928
+
4929
+ # Manage
4930
+ deepline billing subscription cancel --dry-run --json
4931
+ deepline billing set-limit 500 --json
4932
+
4933
+ # Records
4934
+ deepline billing invoices --limit 12 --json
4935
+ deepline billing history --time 1m --json
4936
+ deepline billing ledger export all --json
4481
4937
  `
4482
4938
  );
4483
4939
  billing.command("balance").description("Show current billing balance.").addHelpText(
@@ -4603,6 +5059,110 @@ Examples:
4603
5059
  deepline billing checkout --credits 1000 --discount-code LAUNCH --no-open
4604
5060
  `
4605
5061
  ).option("--tier <tierId>", "Named pricing tier").option("--credits <credits>", "Custom credit amount").option("--discount-code <code>", "Apply a discount code").option("--no-open", "Print the checkout URL without opening a browser").option("--json", "Emit JSON output. Also automatic when stdout is piped").action(handleCheckout);
5062
+ billing.command("plans").description("Show published plans and the plan you are on.").addHelpText(
5063
+ "after",
5064
+ `
5065
+ Notes:
5066
+ Read-only. Answers "what plans exist and what am I on": each plan's price,
5067
+ monthly grant credits, rollover policy, and whether it is open for
5068
+ subscription, plus the catalog's usage metrics. All amounts are Deepline
5069
+ credits and Deepline-facing USD, never raw provider spend. Subscribe to a
5070
+ listed plan with: deepline billing subscribe <plan_version_id>
5071
+
5072
+ Examples:
5073
+ deepline billing plans
5074
+ deepline billing plans --json
5075
+ `
5076
+ ).option("--json", "Emit JSON output. Also automatic when stdout is piped").action(handlePlans);
5077
+ billing.command("subscribe").description(
5078
+ "Start a subscription checkout for a plan and optionally open it in your browser."
5079
+ ).addHelpText(
5080
+ "after",
5081
+ `
5082
+ Notes:
5083
+ Creates a Stripe subscription checkout session for the given plan version.
5084
+ Opens the checkout URL in a browser unless --no-open is set. Fails loudly
5085
+ with the server error when subscription checkout is not enabled or the plan
5086
+ is not open for subscription.
5087
+
5088
+ Examples:
5089
+ deepline billing subscribe runtime-395-2026-07-v1
5090
+ deepline billing subscribe runtime-395-2026-07-v1 --no-open --json
5091
+ `
5092
+ ).argument("<plan_version_id>", "Plan version id from `billing plans`").option("--no-open", "Print the checkout URL without opening a browser").option("--json", "Emit JSON output. Also automatic when stdout is piped").action(handleSubscribe);
5093
+ billing.command("subscription").description("Inspect and manage subscription state for the workspace.").addHelpText(
5094
+ "after",
5095
+ `
5096
+ Examples:
5097
+ deepline billing subscription status
5098
+ deepline billing subscription status --json
5099
+ deepline billing subscription cancel --dry-run
5100
+ deepline billing subscription cancel --json
5101
+ deepline billing subscription cancel --undo --json
5102
+ `
5103
+ ).addCommand(
5104
+ new import_commander.Command("status").description("Show active plan, subscription state, and credit pools.").addHelpText(
5105
+ "after",
5106
+ `
5107
+ Notes:
5108
+ Read-only. Shows the active plan, whether a Stripe subscription backs it,
5109
+ scheduled cancellation state, and remaining Deepline credits per grant pool.
5110
+
5111
+ Examples:
5112
+ deepline billing subscription status
5113
+ deepline billing subscription status --json
5114
+ `
5115
+ ).option(
5116
+ "--json",
5117
+ "Emit JSON output. Also automatic when stdout is piped"
5118
+ ).action(handleSubscriptionStatus)
5119
+ ).addCommand(
5120
+ new import_commander.Command("cancel").description("Cancel the subscription at period end. Credits are kept.").addHelpText(
5121
+ "after",
5122
+ `
5123
+ Notes:
5124
+ Mutates subscription state. Cancellation is always AT PERIOD END: the
5125
+ subscription stays active until the end of the paid cycle and remaining
5126
+ Deepline credits are kept \u2014 cancellation never removes credits. --undo
5127
+ reverses a pending cancellation before the period ends. --dry-run reads
5128
+ subscription status and prints the planned mutation without calling the
5129
+ cancel endpoint.
5130
+
5131
+ Exit codes:
5132
+ 0 cancellation scheduled (or reversed with --undo)
5133
+ 3 auth error
5134
+ 4 no active subscription to cancel
5135
+ 5 Stripe/server failure (the server message is preserved)
5136
+
5137
+ Examples:
5138
+ deepline billing subscription cancel --dry-run
5139
+ deepline billing subscription cancel
5140
+ deepline billing subscription cancel --undo
5141
+ deepline billing subscription cancel --json
5142
+ `
5143
+ ).option("--undo", "Reverse a pending cancellation before period end").option(
5144
+ "--dry-run",
5145
+ "Print the planned cancellation without calling the cancel endpoint"
5146
+ ).option(
5147
+ "--json",
5148
+ "Emit JSON output. Also automatic when stdout is piped"
5149
+ ).action(handleSubscriptionCancel)
5150
+ );
5151
+ billing.command("invoices").description("List subscription invoices and credit purchase receipts.").addHelpText(
5152
+ "after",
5153
+ `
5154
+ Notes:
5155
+ Read-only. Shows customer-facing billing history from Stripe, newest first:
5156
+ subscription invoices plus one-time Deepline credit purchase receipts, with
5157
+ Stripe-hosted links. Amounts are what you paid \u2014 never provider spend.
5158
+ --compact keeps id/date/amount/status/url only.
5159
+
5160
+ Examples:
5161
+ deepline billing invoices
5162
+ deepline billing invoices --limit 12 --json
5163
+ deepline billing invoices --compact --json
5164
+ `
5165
+ ).option("--limit <n>", "Maximum entries to return (1-100, default 24)").option("--compact", "Keep only id, date, amount, status, and url").option("--json", "Emit JSON output. Also automatic when stdout is piped").action(handleInvoices);
4606
5166
  billing.command("redeem-code").description("Redeem a billing code.").addHelpText(
4607
5167
  "after",
4608
5168
  `