@oxygen-agent/cli 1.1010.721 → 1.1010.905

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.
Files changed (100) hide show
  1. package/README.md +1 -1
  2. package/dist/auto-update.d.ts +129 -0
  3. package/dist/auto-update.js +392 -0
  4. package/dist/command-manifest.js +14 -0
  5. package/dist/credentials.d.ts +2 -0
  6. package/dist/credentials.js +6 -3
  7. package/dist/functions-commands.js +1 -1
  8. package/dist/http-client.js +28 -4
  9. package/dist/index.js +583 -145
  10. package/dist/run-wait.d.ts +3 -1
  11. package/dist/run-wait.js +19 -5
  12. package/dist/streamed-file-import.d.ts +58 -0
  13. package/dist/streamed-file-import.js +115 -0
  14. package/dist/update.d.ts +29 -0
  15. package/dist/update.js +62 -16
  16. package/dist/workflow-plan-limit-notices.d.ts +8 -0
  17. package/dist/workflow-plan-limit-notices.js +28 -0
  18. package/node_modules/@oxygen/cli-ugc/dist/commands.js +3 -3
  19. package/node_modules/@oxygen/shared/dist/billing-anchors.d.ts +17 -0
  20. package/node_modules/@oxygen/shared/dist/billing-anchors.js +27 -0
  21. package/node_modules/@oxygen/shared/dist/billing.d.ts +191 -35
  22. package/node_modules/@oxygen/shared/dist/billing.js +333 -42
  23. package/node_modules/@oxygen/shared/dist/capability-discovery.js +55 -5
  24. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +2 -2
  25. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +2 -2
  26. package/node_modules/@oxygen/shared/dist/cost-estimate-view.d.ts +50 -0
  27. package/node_modules/@oxygen/shared/dist/cost-estimate-view.js +90 -0
  28. package/node_modules/@oxygen/shared/dist/cost-estimate.d.ts +167 -0
  29. package/node_modules/@oxygen/shared/dist/cost-estimate.js +361 -0
  30. package/node_modules/@oxygen/shared/dist/credit-gate.d.ts +26 -0
  31. package/node_modules/@oxygen/shared/dist/credit-gate.js +65 -0
  32. package/node_modules/@oxygen/shared/dist/email-deliverability-policy.d.ts +51 -0
  33. package/node_modules/@oxygen/shared/dist/email-deliverability-policy.js +101 -0
  34. package/node_modules/@oxygen/shared/dist/email-hard-bounce.d.ts +3 -1
  35. package/node_modules/@oxygen/shared/dist/email-hard-bounce.js +3 -3
  36. package/node_modules/@oxygen/shared/dist/error-redaction.d.ts +1 -1
  37. package/node_modules/@oxygen/shared/dist/error-redaction.js +1 -1
  38. package/node_modules/@oxygen/shared/dist/feature-gates.d.ts +4 -0
  39. package/node_modules/@oxygen/shared/dist/feature-gates.js +5 -0
  40. package/node_modules/@oxygen/shared/dist/file-import.d.ts +13 -1
  41. package/node_modules/@oxygen/shared/dist/file-import.js +33 -6
  42. package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +73 -3
  43. package/node_modules/@oxygen/shared/dist/hosted-ai.js +246 -24
  44. package/node_modules/@oxygen/shared/dist/import-limits.d.ts +25 -1
  45. package/node_modules/@oxygen/shared/dist/import-limits.js +35 -2
  46. package/node_modules/@oxygen/shared/dist/index.d.ts +2 -22
  47. package/node_modules/@oxygen/shared/dist/index.js +2 -42
  48. package/node_modules/@oxygen/shared/dist/object-storage.d.ts +9 -0
  49. package/node_modules/@oxygen/shared/dist/object-storage.js +17 -0
  50. package/node_modules/@oxygen/shared/dist/operational-telemetry.d.ts +41 -0
  51. package/node_modules/@oxygen/shared/dist/operational-telemetry.js +55 -0
  52. package/node_modules/@oxygen/shared/dist/plan-band.d.ts +117 -1
  53. package/node_modules/@oxygen/shared/dist/plan-band.js +175 -10
  54. package/node_modules/@oxygen/shared/dist/plan-capabilities.d.ts +77 -7
  55. package/node_modules/@oxygen/shared/dist/plan-capabilities.js +87 -7
  56. package/node_modules/@oxygen/shared/dist/plan-limits-view.d.ts +219 -0
  57. package/node_modules/@oxygen/shared/dist/plan-limits-view.js +330 -0
  58. package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +204 -6
  59. package/node_modules/@oxygen/shared/dist/plan-limits.js +197 -15
  60. package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +80 -36
  61. package/node_modules/@oxygen/shared/dist/pricing-sheet.js +80 -31
  62. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +38 -20
  63. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +47 -34
  64. package/node_modules/@oxygen/shared/dist/provider-http-error.d.ts +10 -0
  65. package/node_modules/@oxygen/shared/dist/provider-http-error.js +27 -0
  66. package/node_modules/@oxygen/shared/dist/repricing.d.ts +127 -0
  67. package/node_modules/@oxygen/shared/dist/repricing.js +407 -6
  68. package/node_modules/@oxygen/shared/dist/semver.d.ts +21 -0
  69. package/node_modules/@oxygen/shared/dist/semver.js +41 -0
  70. package/node_modules/@oxygen/shared/dist/sending-limits.d.ts +5 -7
  71. package/node_modules/@oxygen/shared/dist/sending-limits.js +10 -16
  72. package/node_modules/@oxygen/shared/dist/sending-seats.d.ts +18 -15
  73. package/node_modules/@oxygen/shared/dist/sending-seats.js +22 -17
  74. package/node_modules/@oxygen/shared/dist/spend-safety.d.ts +57 -8
  75. package/node_modules/@oxygen/shared/dist/spend-safety.js +64 -11
  76. package/node_modules/@oxygen/shared/dist/stripe-price-catalog.d.ts +15 -7
  77. package/node_modules/@oxygen/shared/dist/stripe-price-catalog.js +18 -5
  78. package/node_modules/@oxygen/shared/dist/table-capacity.d.ts +34 -5
  79. package/node_modules/@oxygen/shared/dist/table-capacity.js +25 -8
  80. package/node_modules/@oxygen/shared/dist/telemetry-export-observer.d.ts +6 -0
  81. package/node_modules/@oxygen/shared/dist/telemetry-export-observer.js +13 -5
  82. package/node_modules/@oxygen/shared/dist/telemetry-resource.d.ts +40 -0
  83. package/node_modules/@oxygen/shared/dist/telemetry-resource.js +35 -0
  84. package/node_modules/@oxygen/shared/dist/telemetry.js +5 -0
  85. package/node_modules/@oxygen/shared/dist/ugc.d.ts +15 -0
  86. package/node_modules/@oxygen/shared/dist/ugc.js +29 -0
  87. package/node_modules/@oxygen/shared/dist/version.d.ts +1 -3
  88. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  89. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  90. package/node_modules/@oxygen/shared/dist/version.js +14 -27
  91. package/node_modules/@oxygen/shared/dist/workspace-file-storage.d.ts +5 -0
  92. package/node_modules/@oxygen/shared/dist/workspace-file-storage.js +5 -0
  93. package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.d.ts +3 -3
  94. package/node_modules/@oxygen/workflows/dist/graph/types.d.ts +15 -1
  95. package/node_modules/@oxygen/workflows/dist/graph/types.js +15 -1
  96. package/node_modules/@oxygen/workflows/dist/index.d.ts +45 -0
  97. package/node_modules/@oxygen/workflows/dist/index.js +152 -2
  98. package/node_modules/@oxygen/workflows/dist/usage-estimate.d.ts +10 -1
  99. package/node_modules/@oxygen/workflows/dist/usage-estimate.js +33 -29
  100. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
- import { AUTOMATION_ACTION_CREDITS as SNAPSHOT_AUTOMATION_ACTION_CREDITS, CREDITS_PER_USD as SNAPSHOT_CREDITS_PER_USD, PRICING_PLAN_SNAPSHOT, SENDING_MAILBOX_CREDITS_PER_MONTH as SNAPSHOT_SENDING_MAILBOX_CREDITS_PER_MONTH, TOPUP_USD_CENTS_PER_100_CREDITS as SNAPSHOT_TOPUP_USD_CENTS_PER_100_CREDITS, } from "./pricing-snapshot.generated.js";
2
- import { currentSubscriptionGrantSlice } from "./billing-anchors.js";
1
+ import { CREDITS_PER_USD as SNAPSHOT_CREDITS_PER_USD, PRICING_PLAN_SNAPSHOT, MANAGED_INBOX_CREDITS_PER_MONTH as SNAPSHOT_MANAGED_MAILBOX_CREDITS_PER_MONTH, SENDING_MAILBOX_CREDITS_PER_MONTH as SNAPSHOT_SENDING_MAILBOX_CREDITS_PER_MONTH, } from "./pricing-snapshot.generated.js";
2
+ import { currentSubscriptionGrantSlice, dueSubscriptionGrantSlices, } from "./billing-anchors.js";
3
3
  import { freeTierEntitlementEnabled } from "./plan-capabilities.js";
4
+ import { AUTOMATION_STEP_CREDITS_REPRICING, AUTOMATION_STEPS_COUNT_TOWARD_RUN_CAP_REPRICING, isRepricingInForce, BYOK_PLATFORM_FEE_REPRICING, OXYGEN_49_BYOK_REPRICING, repricedValue, TOPUP_RATE_REPRICING, } from "./repricing.js";
4
5
  import { asRecordOrNull } from "./type-guards.js";
5
6
  export const WEEKLY_USAGE_WINDOW_DAYS = 7;
6
7
  export const BILLING_CURRENCIES = ["usd"];
@@ -35,10 +36,10 @@ export const OXYGEN_PLAN_KEYS = [
35
36
  // person's booking page into every public pricing card.
36
37
  export const CONTACT_SALES_URL = "mailto:priority@oxygen-agent.com";
37
38
  // One Oxygen credit is worth $0.01 of managed work: $1 = 100 credits.
38
- // Subscription plans convert at face value — every subscription dollar grants
39
- // exactly 100 credits — while on-demand top-up packs price the same credit
40
- // at $1.25 per 100, making subscription credits exactly 20% cheaper than
41
- // credits bought on demand.
39
+ // Subscription plans convert at least at face value — every subscription dollar
40
+ // grants 100 credits, and the $499, $999 and $1,999 plans add a volume bonus on
41
+ // top (see planVolumeBonusCredits) — while on-demand top-ups price the same
42
+ // credit higher, so subscription credits are always the cheaper way to buy.
42
43
  //
43
44
  // Re-denominated from $1 = 1,000 credits on 2026-09-19. That was a PURE unit
44
45
  // change: every credit-denominated price divided by 10 in the same release, so
@@ -49,11 +50,23 @@ export const CONTACT_SALES_URL = "mailto:priority@oxygen-agent.com";
49
50
  // disagreed every markup-derived price would be silently wrong by the ratio.
50
51
  // Change them in packages/control-db/src/pricing/seed.ts.
51
52
  export const CREDITS_PER_USD = SNAPSHOT_CREDITS_PER_USD;
52
- export const TOPUP_USD_CENTS_PER_100_CREDITS = SNAPSHOT_TOPUP_USD_CENTS_PER_100_CREDITS;
53
- // On-demand purchases start at the original fixed packs' $10 floor and run up
54
- // to a $1,250 ceiling, in any 100-credit increment. The ceiling is a
55
- // single-checkout guard rail, not a spend cap: a workspace that needs more
56
- // tops up again (purchased credits never expire).
53
+ /**
54
+ * US cents per 100 on-demand top-up credits right now. The rate rises from 125
55
+ * ($1.25) to the seed's 130 ($1.30) at the 2026-09 repricing's effective instant
56
+ * (decision 1.5, slice S10), so it is read per call through the switch and never
57
+ * from the snapshot: plan credits are then at least 23% cheaper (1 - 1.00/1.30),
58
+ * where they were 20% cheaper at $1.25. A page that shows it resolves it on the
59
+ * server and passes the number down, because a client bundle cannot read the
60
+ * switch.
61
+ */
62
+ export function creditTopupUsdCentsPer100(options = {}) {
63
+ return repricedValue(TOPUP_RATE_REPRICING, options);
64
+ }
65
+ // On-demand purchases run from 800 to 100,000 credits in any 100-credit
66
+ // increment ($10.00 to $1,250 at $1.25 per 100; $10.40 to $1,300 at $1.30). The
67
+ // ceiling is a single-checkout guard rail, not a spend cap: a workspace that
68
+ // needs more tops up again (purchased credits never expire). The range and step
69
+ // are unchanged by the 2026-09 repricing (proposal P-7).
57
70
  export const CREDIT_TOPUP_MIN_CREDITS = 800;
58
71
  export const CREDIT_TOPUP_MAX_CREDITS = 100_000;
59
72
  export const CREDIT_TOPUP_STEP_CREDITS = 100;
@@ -84,8 +97,9 @@ export function creditTopupAmountForShortfall(credits) {
84
97
  const stepped = Math.ceil(credits / CREDIT_TOPUP_STEP_CREDITS) * CREDIT_TOPUP_STEP_CREDITS;
85
98
  return Math.min(CREDIT_TOPUP_MAX_CREDITS, Math.max(CREDIT_TOPUP_MIN_CREDITS, stepped));
86
99
  }
87
- export function creditTopupUsdCents(credits) {
88
- return creditTopupUsdCentsAtRate(credits, TOPUP_USD_CENTS_PER_100_CREDITS);
100
+ /** Price of a custom top-up at the rate in force now (`creditTopupUsdCentsPer100`). */
101
+ export function creditTopupUsdCents(credits, options = {}) {
102
+ return creditTopupUsdCentsAtRate(credits, creditTopupUsdCentsPer100(options));
89
103
  }
90
104
  /**
91
105
  * Price of a custom top-up at an explicit rate. A checkout session records the
@@ -104,29 +118,66 @@ export function creditTopupUsdCentsAtRate(credits, usdCentsPer100) {
104
118
  /** Checkout-session metadata key carrying the rate a top-up was quoted at. */
105
119
  export const CREDIT_TOPUP_RATE_METADATA_KEY = "usd_cents_per_100";
106
120
  /**
107
- * Every top-up rate (US cents per 100 credits) a checkout has ever been minted
108
- * at. Append when the rate changes; never remove one, because an unpaid session
109
- * quoted at it can still be paid. The first entry is also the rate of sessions
110
- * created before the rate was written into their metadata.
121
+ * Every top-up rate (US cents per 100 credits) a checkout has been or can be
122
+ * minted at. Append when the rate changes; never remove one, because an unpaid
123
+ * session quoted at it can still be paid. The first entry is also the rate of
124
+ * sessions created before the rate was written into their metadata.
125
+ *
126
+ * 130 is the 2026-09 rate (decision 1.5). It is listed before the switch's
127
+ * instant as well, so a session minted after the instant is still credited if
128
+ * an operator rolls the switch back before the customer pays. Accepting a rate
129
+ * higher than the one in force can never credit a purchase cheaply.
111
130
  */
112
- export const CREDIT_TOPUP_SOLD_USD_CENTS_PER_100 = [125];
131
+ export const CREDIT_TOPUP_SOLD_USD_CENTS_PER_100 = [125, 130];
113
132
  export const CREDIT_TOPUP_LEGACY_USD_CENTS_PER_100 = CREDIT_TOPUP_SOLD_USD_CENTS_PER_100[0];
114
133
  /**
115
134
  * Whether fulfillment may price a session at `usdCentsPer100`: a rate OXYGEN has
116
135
  * sold at, or the rate in force now. Anything else is metadata no OXYGEN server
117
136
  * wrote, and it is refused rather than credited at a cheaper rate.
118
137
  */
119
- export function isAcceptedCreditTopupRate(usdCentsPer100, currentUsdCentsPer100 = TOPUP_USD_CENTS_PER_100_CREDITS) {
138
+ export function isAcceptedCreditTopupRate(usdCentsPer100, currentUsdCentsPer100 = creditTopupUsdCentsPer100()) {
120
139
  if (!Number.isInteger(usdCentsPer100) || usdCentsPer100 <= 0)
121
140
  return false;
122
141
  return usdCentsPer100 === currentUsdCentsPer100
123
142
  || CREDIT_TOPUP_SOLD_USD_CENTS_PER_100.includes(usdCentsPer100);
124
143
  }
125
- // Workflow/automation actions (steps, tool calls, row writes, retries) draw
126
- // from the same credit pool at a flat capacity-guard rate — the old separate
127
- // monthly action allowance + USD overage meter economy is retired (2026-07-14).
128
- // Seed key: charge.automation.action.
129
- export const AUTOMATION_ACTION_CREDITS = SNAPSHOT_AUTOMATION_ACTION_CREDITS;
144
+ /**
145
+ * Credits one workflow step costs right now. Every step a live run executes
146
+ * (a manifest step, a billable graph node, a recipe checkpoint, the durable
147
+ * recipe runtime) draws this from the one credit pool; rows written inside a run
148
+ * are free (repricing 2026-09, decisions 6.1 and 6.2). Seed key:
149
+ * charge.automation.action.
150
+ *
151
+ * The step price rises 0.001 → 0.05 at the repricing's effective instant (S07),
152
+ * so it is read per call through the switch and never from the snapshot. A page
153
+ * that shows it resolves it on the server and passes the number down.
154
+ */
155
+ export function automationStepCredits(options = {}) {
156
+ return repricedValue(AUTOMATION_STEP_CREDITS_REPRICING, options);
157
+ }
158
+ /**
159
+ * Credits one provider call made with the customer's own API key (BYOK) costs
160
+ * right now: the platform fee of decision 6.3. The vendor bills the call to the
161
+ * customer's own account; this is OXYGEN's fee for running it. Seed key:
162
+ * charge.byok.platform_fee.
163
+ *
164
+ * 0 until the repricing's effective instant (S07), then 0.05, so it is read per
165
+ * call through the switch and never from the snapshot. Every BYOK provider call
166
+ * is admitted and captured at this price by `chargeByokCall`
167
+ * (`@oxygen/integrations`).
168
+ */
169
+ export function byokPlatformFeeCredits(options = {}) {
170
+ return repricedValue(BYOK_PLATFORM_FEE_REPRICING, options);
171
+ }
172
+ /**
173
+ * Whether the step credits a live run executes count toward its approved
174
+ * `max_credits` and a trigger's per-delivery ceiling (proposal P-45). Dark until
175
+ * the step price rises with it: counting 0.001 steps would only tighten ceilings
176
+ * customers already approved.
177
+ */
178
+ export function automationStepsCountTowardRunCap(options = {}) {
179
+ return repricedValue(AUTOMATION_STEPS_COUNT_TOWARD_RUN_CAP_REPRICING, options);
180
+ }
130
181
  // ---------------------------------------------------------------------------
131
182
  // FIXED (committed) credit commitments — recurring per-resource monthly charges
132
183
  // ---------------------------------------------------------------------------
@@ -171,11 +222,6 @@ export const CREDIT_COMMITMENT_KINDS = [
171
222
  * there is one definition of the price.
172
223
  */
173
224
  export const SENDING_MAILBOX_MONTHLY_CREDITS = SNAPSHOT_SENDING_MAILBOX_CREDITS_PER_MONTH;
174
- /**
175
- * Oxygen-sold managed mailbox per month, in credits ($3.00). Flat, superseding
176
- * the dynamic vendor-COGS x 1.25 quote for the recurring mailbox line.
177
- */
178
- export const MANAGED_MAILBOX_MONTHLY_CREDITS = 300;
179
225
  /**
180
226
  * Grace window after a commitment goes past_due before the account disconnects.
181
227
  * Repricing 2026-09 decision 2.8: sending from the account pauses at once, and
@@ -183,6 +229,25 @@ export const MANAGED_MAILBOX_MONTHLY_CREDITS = 300;
183
229
  * days with no auto-cancel). The pause and the disconnect sweep are slice S23.
184
230
  */
185
231
  export const COMMITMENT_PAST_DUE_GRACE_DAYS = 7;
232
+ /**
233
+ * P-15 (PROPOSED default, awaiting ratification): what "disconnect" means for a
234
+ * rented phone number. Releasing a number at the carrier is irreversible, so it is
235
+ * suspended at COMMITMENT_PAST_DUE_GRACE_DAYS (it already cannot dial while past
236
+ * due) and released only after this many days without funds.
237
+ */
238
+ export const PHONE_NUMBER_PAST_DUE_RELEASE_DAYS = 30;
239
+ const GRACE_DAY_MS = 24 * 60 * 60 * 1000;
240
+ export function commitmentGraceSchedule(kind, pastDueSince) {
241
+ const graceEnds = new Date(pastDueSince.getTime() + COMMITMENT_PAST_DUE_GRACE_DAYS * GRACE_DAY_MS);
242
+ if (kind === "phone_number") {
243
+ return {
244
+ teardown: "release",
245
+ teardownAt: new Date(pastDueSince.getTime() + PHONE_NUMBER_PAST_DUE_RELEASE_DAYS * GRACE_DAY_MS),
246
+ suspendedAt: graceEnds,
247
+ };
248
+ }
249
+ return { teardown: "disconnect", teardownAt: graceEnds, suspendedAt: null };
250
+ }
186
251
  /**
187
252
  * Reconnect window in which an ENDED commitment resumes instead of starting a new
188
253
  * one. The single most important anti-double-charge rule: a Unipile re-auth, a
@@ -219,7 +284,10 @@ export function ratifiedCommitmentCredits(kind) {
219
284
  case "sending_mailbox":
220
285
  return SENDING_MAILBOX_MONTHLY_CREDITS;
221
286
  case "managed_mailbox":
222
- return MANAGED_MAILBOX_MONTHLY_CREDITS;
287
+ // The pricing seed's `commitment.managed_mailbox`, the one definition (S32).
288
+ // Before the 2026-09 switch the price in force is lower: resolve it with
289
+ // repricedChargeCredits from ./repricing.js.
290
+ return SNAPSHOT_MANAGED_MAILBOX_CREDITS_PER_MONTH;
223
291
  // Ratified in @oxygen/control-db (MAILBOX_WARMUP_MONTHLY_CREDITS /
224
292
  // DELIVERABILITY_UNIT_MONTHLY_CREDITS) since 2026-07-29, alongside the seat
225
293
  // prices below. @oxygen/shared cannot import control-db, so these still
@@ -238,16 +306,65 @@ export function ratifiedCommitmentCredits(kind) {
238
306
  return null;
239
307
  }
240
308
  }
241
- export const CREDIT_TOPUP_PACKS = [
309
+ /**
310
+ * The dollar packs sold at $1.25 per 100 credits, on offer until the 2026-09
311
+ * repricing's effective instant. The id is the pack's price in dollars.
312
+ */
313
+ export const CREDIT_TOPUP_LEGACY_PACKS = [
242
314
  { id: "10", usdCents: 1_000, credits: 800 },
243
315
  { id: "25", usdCents: 2_500, credits: 2_000 },
244
316
  { id: "100", usdCents: 10_000, credits: 8_000 },
245
317
  { id: "250", usdCents: 25_000, credits: 20_000 },
246
318
  ];
319
+ /**
320
+ * The credit-first packs on offer from the 2026-09 repricing's instant, at $1.30
321
+ * per 100 credits (decision 1.5; the pack set is PROPOSED P-7). Dollar packs at
322
+ * $1.30 would buy fractional credits ($10 → 769.2), so these fix the credits and
323
+ * the id names them. The ids are new on purpose: fulfillment credits a pack
324
+ * session `pack.credits` without re-checking its amount, so reusing '10' or '25'
325
+ * with a new price would credit an old unpaid session from the new table.
326
+ */
327
+ export const CREDIT_TOPUP_PACKS_2026_09 = [
328
+ { id: "1000", usdCents: 1_300, credits: 1_000 },
329
+ { id: "2000", usdCents: 2_600, credits: 2_000 },
330
+ { id: "8000", usdCents: 10_400, credits: 8_000 },
331
+ { id: "20000", usdCents: 26_000, credits: 20_000 },
332
+ ];
333
+ /** Every pack id a checkout session may carry, whichever table sold it. */
334
+ export const CREDIT_TOPUP_KNOWN_PACKS = [
335
+ ...CREDIT_TOPUP_LEGACY_PACKS,
336
+ ...CREDIT_TOPUP_PACKS_2026_09,
337
+ ];
338
+ /** The packs on offer now: the dollar packs until the switch, the credit-first packs from it. */
339
+ export function creditTopupPacks(options = {}) {
340
+ return isRepricingInForce(options) ? CREDIT_TOPUP_PACKS_2026_09 : CREDIT_TOPUP_LEGACY_PACKS;
341
+ }
342
+ /**
343
+ * Any pack a session may name, with the credits and price it was SOLD at. Used by
344
+ * fulfillment, which must credit an old session its original credits whichever
345
+ * packs are on offer when Stripe confirms the payment.
346
+ */
247
347
  export function resolveCreditTopupPack(id) {
248
348
  if (!id)
249
349
  return null;
250
- return CREDIT_TOPUP_PACKS.find((pack) => pack.id === id) ?? null;
350
+ return CREDIT_TOPUP_KNOWN_PACKS.find((pack) => pack.id === id) ?? null;
351
+ }
352
+ /**
353
+ * How the top-up route sells a pack id right now. A pack on offer sells at its
354
+ * own price. Any other known id (a dollar pack after the switch from an older
355
+ * CLI, or a credit-first pack before it) still works and buys the pack's credits
356
+ * as a custom amount at the rate in force, so an old client sees the current
357
+ * quote and never an old price. Null for an unknown id.
358
+ */
359
+ export function quoteCreditTopupPack(id, options = {}) {
360
+ const pack = resolveCreditTopupPack(id);
361
+ if (!pack)
362
+ return null;
363
+ const offered = creditTopupPacks(options).some((candidate) => candidate.id === pack.id);
364
+ if (offered)
365
+ return { pack, offered, credits: pack.credits, usdCents: pack.usdCents };
366
+ const usdCents = creditTopupUsdCents(pack.credits, options);
367
+ return usdCents == null ? null : { pack, offered, credits: pack.credits, usdCents };
251
368
  }
252
369
  // Card-required free trial: every new signup chooses a self-serve Stripe plan,
253
370
  // gets TRIAL_CREDIT_GRANT credits (a value-demonstration budget sized to the
@@ -333,6 +450,86 @@ function planPricing(planKey) {
333
450
  byokEnabled: row.byokEnabled,
334
451
  };
335
452
  }
453
+ /**
454
+ * The volume bonus of a plan (repricing 2026-09, decision 1.3): the monthly
455
+ * credits it grants above face value ($1 = 100 credits). +5% on $499 (52,395),
456
+ * +10% on $999 (109,890), +15% on $1,999 (229,885); $49-$199 stay flat.
457
+ *
458
+ * DERIVED from the seeded monthlyCredits rather than declared, so the schedule
459
+ * lives in one place (packages/control-db/src/pricing/seed.ts). Only the
460
+ * purchasable Oxygen rungs carry a bonus: a legacy plan whose credits exceed its
461
+ * price (launch_50 bought 20,000 credits for $50 under the old unit) is not a
462
+ * volume bonus, and a contact-sales plan has no face value.
463
+ */
464
+ export function planVolumeBonusCredits(planKey) {
465
+ if (!planKey || !OXYGEN_PLAN_KEYS.includes(planKey))
466
+ return 0;
467
+ const { monthlyPriceCents, monthlyCredits } = planPricing(planKey);
468
+ if (monthlyPriceCents === null || monthlyCredits === null)
469
+ return 0;
470
+ const faceValueCredits = Math.round((monthlyPriceCents / 100) * CREDITS_PER_USD);
471
+ return Math.max(0, monthlyCredits - faceValueCredits);
472
+ }
473
+ /**
474
+ * The volume bonus inside one subscription grant of `monthlyCredits` on
475
+ * `planKey`: the plan's bonus when the grant is the plan's seeded allowance, and
476
+ * 0 when a metadata override replaced it (a custom allowance carries no
477
+ * schedule to defer). Grant writers pass this to grantSubscriptionCredits.
478
+ */
479
+ export function subscriptionGrantVolumeBonusCredits(planKey, monthlyCredits) {
480
+ if (!planKey || typeof monthlyCredits !== "number")
481
+ return 0;
482
+ const bonus = planVolumeBonusCredits(planKey);
483
+ return bonus > 0 && planPricing(planKey).monthlyCredits === monthlyCredits ? bonus : 0;
484
+ }
485
+ /** The volume bonus as a whole percentage of face value: 5, 10, 15, or 0. */
486
+ export function planVolumeBonusPercent(planKey) {
487
+ const bonus = planVolumeBonusCredits(planKey);
488
+ if (bonus === 0 || !planKey)
489
+ return 0;
490
+ const faceValueCredits = Math.round(((planPricing(planKey).monthlyPriceCents ?? 0) / 100) * CREDITS_PER_USD);
491
+ return faceValueCredits > 0 ? Math.round((bonus / faceValueCredits) * 100) : 0;
492
+ }
493
+ /**
494
+ * Agency partner bonus (repricing 2026-09, decision 9.1): a partner earns
495
+ * +15% of its plan's face value in bonus credits every month, on every Oxygen
496
+ * size from $99, on top of the volume bonus. The two percentages ADD (spec
497
+ * PROPOSED default P-1, option A of the agency-partner benchmark): the $1,999
498
+ * plan grants 229,885 + 29,985 = 259,870, never 229,885 x 1.15.
499
+ *
500
+ * The bonus is its own ledger line (category `partner_bonus`), never part of
501
+ * the plan's monthlyCredits, so it stays out of every limit or guard derived
502
+ * from the plan allowance (P-3, P-54, P-61).
503
+ */
504
+ export const PARTNER_BONUS_PERCENT = 15;
505
+ /** The smallest plan that earns the partner bonus: $99 a month. */
506
+ export const PARTNER_BONUS_MINIMUM_MONTHLY_PRICE_CENTS = 9_900;
507
+ /**
508
+ * The monthly partner bonus of a plan: 15% of face value ($1 = 100 credits),
509
+ * rounded half up to a whole credit (section 2's rounding rule). 0 for Free,
510
+ * $49, the grandfathered Starter/Pro/Team and older legacy plans, and the
511
+ * contact-sales plans (P-3: Oxygen sizes only).
512
+ */
513
+ export function planPartnerBonusCredits(planKey) {
514
+ if (!planKey || !OXYGEN_PLAN_KEYS.includes(planKey))
515
+ return 0;
516
+ const { monthlyPriceCents } = planPricing(planKey);
517
+ if (monthlyPriceCents === null || monthlyPriceCents < PARTNER_BONUS_MINIMUM_MONTHLY_PRICE_CENTS)
518
+ return 0;
519
+ const faceValueCredits = Math.round((monthlyPriceCents / 100) * CREDITS_PER_USD);
520
+ return Math.round((faceValueCredits * PARTNER_BONUS_PERCENT) / 100);
521
+ }
522
+ /**
523
+ * The partner bonus that rides with one subscription grant of `monthlyCredits`
524
+ * on `planKey`: the plan's bonus when the grant is the plan's seeded allowance,
525
+ * 0 when a metadata override replaced it (a custom contract is priced by hand).
526
+ */
527
+ export function subscriptionGrantPartnerBonusCredits(planKey, monthlyCredits) {
528
+ if (!planKey || typeof monthlyCredits !== "number")
529
+ return 0;
530
+ const bonus = planPartnerBonusCredits(planKey);
531
+ return bonus > 0 && planPricing(planKey).monthlyCredits === monthlyCredits ? bonus : 0;
532
+ }
336
533
  /**
337
534
  * The monthly-credit feature bullet, DERIVED rather than written down.
338
535
  *
@@ -345,8 +542,12 @@ function monthlyCreditsFeature(planKey) {
345
542
  const credits = planPricing(planKey).monthlyCredits;
346
543
  if (credits === null)
347
544
  return "Custom monthly credits";
348
- return `${credits.toLocaleString("en-US")} credits every month`;
545
+ const bonusPercent = planVolumeBonusPercent(planKey);
546
+ return bonusPercent > 0
547
+ ? `${credits.toLocaleString("en-US")} credits every month (${bonusPercent}% volume bonus)`
548
+ : `${credits.toLocaleString("en-US")} credits every month`;
349
549
  }
550
+ const BRING_YOUR_OWN_KEYS_FEATURE = "Bring your own keys";
350
551
  export const BASE_PRICING_PLANS = {
351
552
  // The ENTRY tier since 2026-08-16, superseding the 2026-07-22 retirement that
352
553
  // left this as the unentitled fallback for churned / never-subscribed orgs.
@@ -370,10 +571,14 @@ export const BASE_PRICING_PLANS = {
370
571
  monthlyAutomationActions: null,
371
572
  automationOverageCentsPerMillion: null,
372
573
  automationOverageEnabledDefault: false,
373
- description: "Your CRM, tables, and workflows — free forever. Credits pay for enrichment and AI; connecting a sender, publishing, managed email infrastructure, and your own provider keys need a plan.",
574
+ // Static text cannot read the credit gate, and once it is in force a free
575
+ // workspace may connect senders, publish and buy managed email on credits
576
+ // (repricing 2026-09, S22). So it names only what needs a plan either way;
577
+ // the gate-aware walls speak for the rest.
578
+ description: "Your CRM and tables — free forever. Credits pay for enrichment, AI and workflow steps; your own provider keys need a paid plan.",
374
579
  ctaLabel: "Upgrade",
375
580
  features: [
376
- "CRM, Tables, Workflows, and Knowledge — unmetered",
581
+ "CRM, Tables, and Knowledge — unmetered; workflows pay per step",
377
582
  "Sequences: author, preview, and enroll",
378
583
  "Agents and Workspace Copilot",
379
584
  "Credits for enrichment and AI, topped up any time",
@@ -398,7 +603,7 @@ export const BASE_PRICING_PLANS = {
398
603
  monthlyCreditsFeature("oxygen_49"),
399
604
  "All integrations",
400
605
  "Workflows",
401
- "Bring your own keys",
606
+ BRING_YOUR_OWN_KEYS_FEATURE,
402
607
  "Standard support",
403
608
  ],
404
609
  },
@@ -561,14 +766,15 @@ export function resolveSubscriptionEntitlement(input) {
561
766
  : { entitled: true, reason: "trialing", graceEndsAt: null };
562
767
  }
563
768
  if (input.status === "past_due") {
564
- if (!input.currentPeriodStart) {
769
+ const graceStartsAt = pastDueGraceStart(input, now);
770
+ if (!graceStartsAt) {
565
771
  return {
566
772
  entitled: false,
567
773
  reason: "missing_past_due_period_start",
568
774
  graceEndsAt: null,
569
775
  };
570
776
  }
571
- const graceEndsAt = new Date(input.currentPeriodStart.getTime() + PAST_DUE_GRACE_DAYS * 24 * 60 * 60 * 1000);
777
+ const graceEndsAt = new Date(graceStartsAt.getTime() + PAST_DUE_GRACE_DAYS * 24 * 60 * 60 * 1000);
572
778
  return graceEndsAt.getTime() > now.getTime()
573
779
  ? { entitled: true, reason: "past_due_grace", graceEndsAt }
574
780
  : { entitled: false, reason: "past_due_grace_expired", graceEndsAt };
@@ -582,6 +788,35 @@ export function resolveSubscriptionEntitlement(input) {
582
788
  }
583
789
  return { entitled: false, reason: "missing_subscription", graceEndsAt: null };
584
790
  }
791
+ /**
792
+ * Where a past-due subscription's grace starts (OXP-43.4).
793
+ *
794
+ * The grace used to count from `currentPeriodStart`. For a monthly plan that is
795
+ * the renewal whose payment failed, but a yearly plan's period start can be
796
+ * months back, so a yearly subscriber who went past due mid-year lost access at
797
+ * once. The grace now counts from `pastDueSince`, the instant the unpaid streak
798
+ * was first seen. A stamp older than the current period start is left over from
799
+ * an earlier streak, so the period start wins over it, which keeps a monthly
800
+ * plan's grace at least as long as it always was. A row without a stamp
801
+ * (written before the column existed) counts from the monthly credit slice
802
+ * `now` falls in: its period start for a monthly plan, exactly as before.
803
+ */
804
+ function pastDueGraceStart(input, now) {
805
+ const periodStart = validDateOrNull(input.currentPeriodStart);
806
+ const pastDueSince = validDateOrNull(input.pastDueSince);
807
+ if (pastDueSince) {
808
+ return periodStart && periodStart.getTime() > pastDueSince.getTime()
809
+ ? periodStart
810
+ : pastDueSince;
811
+ }
812
+ const periodEnd = validDateOrNull(input.currentPeriodEnd);
813
+ return periodStart && periodEnd
814
+ ? dueSubscriptionGrantSlices(periodStart, periodEnd, now).at(-1)?.start ?? periodStart
815
+ : periodStart;
816
+ }
817
+ function validDateOrNull(value) {
818
+ return value instanceof Date && !Number.isNaN(value.getTime()) ? value : null;
819
+ }
585
820
  /**
586
821
  * The six purchasable rungs, keyed by plan key.
587
822
  *
@@ -609,10 +844,31 @@ function oxygenRung(planKey) {
609
844
  ctaLabel: dollars === null ? "Get started" : `Choose $${dollars.toLocaleString("en-US")}`,
610
845
  features: [
611
846
  monthlyCreditsFeature(planKey),
612
- ...BASE_PRICING_PLANS.oxygen.features.slice(1),
847
+ // A rung without BYOK in the seed ($49 since the 2026-09 repricing) must not
848
+ // promise it. The card follows the seed, not the S07 hold below: it
849
+ // under-promises to a $49 customer for the notice period rather than
850
+ // advertising an entitlement that ends at the effective date.
851
+ ...BASE_PRICING_PLANS.oxygen.features
852
+ .slice(1)
853
+ .filter((feature) => pricing.byokEnabled || feature !== BRING_YOUR_OWN_KEYS_FEATURE),
613
854
  ],
614
855
  };
615
856
  }
857
+ /**
858
+ * The plan in force for `planKey` right now: the catalog row with every value the
859
+ * 2026-09 repricing holds back until its effective date put back.
860
+ *
861
+ * Only the $49 plan's BYOK entitlement is held here (S22, decision 2.7): the seed
862
+ * and snapshot already say `false`, and until the switch's instant the plan keeps
863
+ * the `true` it was sold with. The representative `oxygen` row carries the $49
864
+ * numbers, so it is held the same way.
865
+ */
866
+ function withRepricingHolds(planKey, plan, options = {}) {
867
+ if (planKey !== "oxygen_49" && planKey !== "oxygen")
868
+ return plan;
869
+ const byokEnabled = repricedValue(OXYGEN_49_BYOK_REPRICING, options);
870
+ return byokEnabled === plan.byokEnabled ? plan : { ...plan, byokEnabled };
871
+ }
616
872
  const LEGACY_PRICING_PLAN_OVERRIDES = {
617
873
  launch_50: {
618
874
  ...BASE_PRICING_PLANS.starter,
@@ -660,7 +916,7 @@ const LEGACY_PRICING_PLAN_OVERRIDES = {
660
916
  description: "Legacy Scale plan.",
661
917
  },
662
918
  };
663
- export function resolveBasePricingPlan(tier) {
919
+ export function resolveBasePricingPlan(tier, options = {}) {
664
920
  if (!tier)
665
921
  return null;
666
922
  // Plan KEYS before tiers: `subscriptions.tier` stores "oxygen_499", and the
@@ -668,9 +924,9 @@ export function resolveBasePricingPlan(tier) {
668
924
  // customer paid for.
669
925
  const oxygen = OXYGEN_PLAN_OVERRIDES[tier];
670
926
  if (oxygen)
671
- return oxygen;
927
+ return withRepricingHolds(tier, oxygen, options);
672
928
  if (tier in BASE_PRICING_PLANS) {
673
- return BASE_PRICING_PLANS[tier];
929
+ return withRepricingHolds(tier, BASE_PRICING_PLANS[tier], options);
674
930
  }
675
931
  const legacy = LEGACY_PRICING_PLAN_OVERRIDES[tier];
676
932
  return legacy ?? null;
@@ -803,6 +1059,41 @@ export const ANNUAL_BILLING_MONTHS_CHARGED = 10;
803
1059
  export function annualPlanPriceCents(monthlyPriceCents) {
804
1060
  return monthlyPriceCents * ANNUAL_BILLING_MONTHS_CHARGED;
805
1061
  }
1062
+ export const PLAN_BILLING_INTERVALS = ["month", "year"];
1063
+ /**
1064
+ * A billing interval as a customer or an agent types it. `month`/`monthly` and
1065
+ * `year`/`yearly`/`annual`/`annually` are accepted; anything else is null so the
1066
+ * caller can refuse it by name instead of silently billing monthly.
1067
+ */
1068
+ export function parsePlanBillingInterval(value) {
1069
+ if (typeof value !== "string")
1070
+ return null;
1071
+ const normalized = value.trim().toLowerCase();
1072
+ if (normalized === "month" || normalized === "monthly")
1073
+ return "month";
1074
+ if (normalized === "year"
1075
+ || normalized === "yearly"
1076
+ || normalized === "annual"
1077
+ || normalized === "annually") {
1078
+ return "year";
1079
+ }
1080
+ return null;
1081
+ }
1082
+ /**
1083
+ * What one charge of a rung costs on an interval, in cents: the monthly price,
1084
+ * or ten months' price for a year.
1085
+ */
1086
+ export function planChargeCents(monthlyPriceCents, interval) {
1087
+ return interval === "year" ? annualPlanPriceCents(monthlyPriceCents) : monthlyPriceCents;
1088
+ }
1089
+ /**
1090
+ * The per-month equivalent of one charge, in cents, rounded to the cent: $990 a
1091
+ * year is $82.50 a month. This is what `recurring_monthly` reports, so a yearly
1092
+ * plan is never compared as if it cost $990 a month.
1093
+ */
1094
+ export function planMonthlyEquivalentCents(chargeCents, interval) {
1095
+ return interval === "year" ? Math.round(chargeCents / 12) : chargeCents;
1096
+ }
806
1097
  const MS_PER_DAY = 24 * 60 * 60 * 1000;
807
1098
  function startOfUtcMonth(date) {
808
1099
  return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1));
@@ -79,11 +79,11 @@ export const OXYGEN_CAPABILITY_ROUTES = [
79
79
  notFor: "Authorizing a specific provider call or external write; that action still needs its own preview and approval.",
80
80
  execution: "Read billing state or set a hosted budget policy; exact action ceilings remain primitive-owned.",
81
81
  posture: "mixed",
82
- gatewayTools: ["oxygen_billing_balance", "oxygen_billing_usage", "oxygen_budget_list"],
83
- gatewayCommands: ["billing balance", "billing usage", "budget list"],
82
+ gatewayTools: ["oxygen_billing_balance", "oxygen_billing_usage", "oxygen_billing_estimate", "oxygen_budget_list"],
83
+ gatewayCommands: ["billing balance", "billing usage", "billing estimate", "budget list"],
84
84
  skills: ["oxygen-gtm", "oxygen-diagnostics"],
85
85
  endpointSections: ["billing", "budget", "limits"],
86
- intentTerms: ["billing", "credits", "credit balance", "usage", "spend", "cost", "budget", "subscription", "invoice", "cancel plan", "change plan", "upgrade", "downgrade"],
86
+ intentTerms: ["billing", "credits", "credit balance", "usage", "spend", "cost", "cost estimate", "monthly cost", "budget", "subscription", "invoice", "cancel plan", "change plan", "upgrade", "downgrade"],
87
87
  },
88
88
  {
89
89
  id: "support-and-feedback",
@@ -345,7 +345,7 @@ export const OXYGEN_CAPABILITY_ROUTES = [
345
345
  // email track's "Only 'instantly' is supported" and concluded Smartlead was
346
346
  // unreachable. The governed path for a campaign the customer already runs
347
347
  // elsewhere is `sequences push-external`; it is not a Sequence.
348
- execution: "Use the one-step gateway for new LinkedIn initiation or create/enroll/start a hosted campaign with bounded send authority. To push table rows into an existing Smartlead, Instantly, lemlist or HeyReach campaign you already run, use `sequences push-external` (dry_run preview, then --approved); the native email track of a Sequence stays Instantly-bound.",
348
+ execution: "Use the one-step gateway for new LinkedIn initiation or create/enroll/start a hosted campaign with bounded send authority. Native email sends through connected Google/Microsoft mailboxes, including Microsoft SMTP transport. To push table rows into an existing Smartlead, Instantly, lemlist or HeyReach campaign you already run, use `sequences push-external` (dry_run preview, then --approved).",
349
349
  posture: "external_write",
350
350
  gatewayTools: ["oxygen_sequences_send", "oxygen_sequences_create", "oxygen_sequences_enroll", "oxygen_sequences_start", "oxygen_sequences_list", "oxygen_sequences_push_external"],
351
351
  gatewayCommands: ["sequences send", "sequences create", "sequences enroll", "sequences start", "sequences list", "sequences push-external"],
@@ -739,12 +739,14 @@ function explicitCapabilityIntent(query) {
739
739
  if (isInboxAvatarIntent(query)) {
740
740
  return ROUTE_BY_ID.get("sending-infrastructure") ?? null;
741
741
  }
742
- if (isMailboxDeleteIntent(query)) {
742
+ if (isMailboxDeleteIntent(query) || isSendingDomainDeleteIntent(query)) {
743
743
  return ROUTE_BY_ID.get("sending-infrastructure") ?? null;
744
744
  }
745
745
  if (isMailboxOnboardingIntent(query)) {
746
746
  return ROUTE_BY_ID.get("sending-infrastructure") ?? null;
747
747
  }
748
+ if (isLinkedInAccountConnectIntent(query))
749
+ return ROUTE_BY_ID.get("connected-linkedin") ?? null;
748
750
  if (isLinkedInProfileWatcherIntent(query))
749
751
  return ROUTE_BY_PRIMITIVE.get("tables") ?? null;
750
752
  // Profile viewers exist only as a connected account's data, and their capture
@@ -974,6 +976,13 @@ function recommendationsFor(card, query) {
974
976
  exactCommand: LOCAL_BUSINESS_SCRAPE_COMMAND,
975
977
  };
976
978
  }
979
+ if (card.id === "connected-linkedin" && isLinkedInAccountConnectIntent(query)) {
980
+ return {
981
+ tools: ["oxygen_senders_connect", "oxygen_senders_list", "oxygen_billing_commitments"],
982
+ commands: ["senders connect", "senders list", "billing commitments"],
983
+ exactCommand: "oxygen senders connect --country <cc> --json",
984
+ };
985
+ }
977
986
  if (card.id === "connected-linkedin" && isLinkedInAccountLimitsIntent(query)) {
978
987
  return {
979
988
  tools: ["oxygen_senders_limits_get", "oxygen_senders_limits_set", "oxygen_senders_list"],
@@ -986,6 +995,14 @@ function recommendationsFor(card, query) {
986
995
  commands: ["tools get", "tools run", "senders list"],
987
996
  };
988
997
  }
998
+ // Whole domains go with every inbox on them, so the domain path leads and the
999
+ // selected-mailbox path (with --include-domains) follows.
1000
+ if (card.id === "sending-infrastructure" && isSendingDomainDeleteIntent(query)) {
1001
+ return {
1002
+ tools: ["oxygen_mailboxes_delete", "oxygen_domains_list", "oxygen_mailboxes_list"],
1003
+ commands: ["domains delete", "mailboxes delete", "domains list"],
1004
+ };
1005
+ }
989
1006
  if (card.id === "sending-infrastructure" && isMailboxDeleteIntent(query)) {
990
1007
  return {
991
1008
  tools: [
@@ -1329,6 +1346,22 @@ function isSenderProfileIntent(query) {
1329
1346
  && /\b(?:sender|sending|unif(?:y|ied))\b/.test(query));
1330
1347
  }
1331
1348
  /** The account's own working hours or daily caps (`senders limits`), not its network. */
1349
+ /**
1350
+ * Connecting, or asking what it takes to connect, the workspace's own LinkedIn
1351
+ * sending account. The 2026-09-26 repricing baseline asked "connect a LinkedIn
1352
+ * account" and was sent to Tables, and "LinkedIn on a free workspace" to
1353
+ * workspace access; neither named `senders connect`, the one command that does
1354
+ * it. Sending, network, profile and post asks keep their own owners.
1355
+ */
1356
+ function isLinkedInAccountConnectIntent(query) {
1357
+ if (!/\b(linkedin|unipile)\b/.test(query))
1358
+ return false;
1359
+ if (/^(linkedin|unipile)( accounts?)?$/.test(query))
1360
+ return true;
1361
+ if (/\b(send|sending|enroll|sequence|campaign|cadence|messages?|dms?|invites?|invitations?|posts?|profiles?|network|connections|followers|viewers|limits|hours|ads?|scrape|research|company|companies|url)\b/.test(query))
1362
+ return false;
1363
+ return /\b(connect|reconnect|link|add|accounts?|senders?|seats?|free|plan)\b/.test(query);
1364
+ }
1332
1365
  function isLinkedInAccountLimitsIntent(query) {
1333
1366
  return /\b(working hours|sending hours|business hours|office hours|weekdays?|time ?zone|linkedin limits|daily limits?)\b/.test(query);
1334
1367
  }
@@ -1350,6 +1383,23 @@ function isMailboxDeleteIntent(query) {
1350
1383
  const scopedDetach = /\b(remove|disconnect)\b.{0,48}\b(mailbox(?:es)?|inbox(?:es)?|sender accounts?|email accounts?)\b.{0,24}\b(?:from|in|on)\b.{0,24}\b(sequence|campaign|cadence|sender profile)\b/.test(query);
1351
1384
  return mailboxScope && removal && !scopedDetach;
1352
1385
  }
1386
+ /**
1387
+ * Deleting the workspace's own SENDING domains ("delete my domains", "remove the
1388
+ * inboxes and their domains", "shut down our cold email domains"). `domains` is
1389
+ * also a data word, so the ask must mark the domains as the workspace's own or
1390
+ * as sending infrastructure, and a table/list/suppression context is excluded.
1391
+ */
1392
+ function isSendingDomainDeleteIntent(query) {
1393
+ if (!/\b(delete|remove|shut ?down|tear down|get rid of|decommission)\b/.test(query))
1394
+ return false;
1395
+ if (/\b(suppress\w*|block ?list|blocklist|do not contact|dnc|tables?|rows?|columns?|duplicates?|dedupe|list of|compan(?:y|ies)|leads?|contacts?)\b/.test(query))
1396
+ return false;
1397
+ // Detaching a sender from a sequence/campaign/profile is not deletion.
1398
+ if (/\b(?:from|in|on)\b.{0,24}\b(sequence|campaign|cadence|sender profile)\b/.test(query))
1399
+ return false;
1400
+ return /\b(my|our|their|these|those|all|sending|cold email|cold outreach|outbound|email|mailbox|inbox|managed)\s+(sending\s+)?domains?\b/.test(query)
1401
+ || (/\bdomains?\b/.test(query) && /\b(mailbox(?:es)?|inbox(?:es)?)\b/.test(query));
1402
+ }
1353
1403
  // The composed inbound-led outbound motion: engagers → ICP gate → capped
1354
1404
  // sequence → CRM. Hyphens are already normalised to spaces by the caller.
1355
1405
  function isInboundLedOutboundIntent(query) {