@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.
- package/README.md +1 -1
- package/dist/auto-update.d.ts +129 -0
- package/dist/auto-update.js +392 -0
- package/dist/command-manifest.js +14 -0
- package/dist/credentials.d.ts +2 -0
- package/dist/credentials.js +6 -3
- package/dist/functions-commands.js +1 -1
- package/dist/http-client.js +28 -4
- package/dist/index.js +583 -145
- package/dist/run-wait.d.ts +3 -1
- package/dist/run-wait.js +19 -5
- package/dist/streamed-file-import.d.ts +58 -0
- package/dist/streamed-file-import.js +115 -0
- package/dist/update.d.ts +29 -0
- package/dist/update.js +62 -16
- package/dist/workflow-plan-limit-notices.d.ts +8 -0
- package/dist/workflow-plan-limit-notices.js +28 -0
- package/node_modules/@oxygen/cli-ugc/dist/commands.js +3 -3
- package/node_modules/@oxygen/shared/dist/billing-anchors.d.ts +17 -0
- package/node_modules/@oxygen/shared/dist/billing-anchors.js +27 -0
- package/node_modules/@oxygen/shared/dist/billing.d.ts +191 -35
- package/node_modules/@oxygen/shared/dist/billing.js +333 -42
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +55 -5
- package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +2 -2
- package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +2 -2
- package/node_modules/@oxygen/shared/dist/cost-estimate-view.d.ts +50 -0
- package/node_modules/@oxygen/shared/dist/cost-estimate-view.js +90 -0
- package/node_modules/@oxygen/shared/dist/cost-estimate.d.ts +167 -0
- package/node_modules/@oxygen/shared/dist/cost-estimate.js +361 -0
- package/node_modules/@oxygen/shared/dist/credit-gate.d.ts +26 -0
- package/node_modules/@oxygen/shared/dist/credit-gate.js +65 -0
- package/node_modules/@oxygen/shared/dist/email-deliverability-policy.d.ts +51 -0
- package/node_modules/@oxygen/shared/dist/email-deliverability-policy.js +101 -0
- package/node_modules/@oxygen/shared/dist/email-hard-bounce.d.ts +3 -1
- package/node_modules/@oxygen/shared/dist/email-hard-bounce.js +3 -3
- package/node_modules/@oxygen/shared/dist/error-redaction.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/error-redaction.js +1 -1
- package/node_modules/@oxygen/shared/dist/feature-gates.d.ts +4 -0
- package/node_modules/@oxygen/shared/dist/feature-gates.js +5 -0
- package/node_modules/@oxygen/shared/dist/file-import.d.ts +13 -1
- package/node_modules/@oxygen/shared/dist/file-import.js +33 -6
- package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +73 -3
- package/node_modules/@oxygen/shared/dist/hosted-ai.js +246 -24
- package/node_modules/@oxygen/shared/dist/import-limits.d.ts +25 -1
- package/node_modules/@oxygen/shared/dist/import-limits.js +35 -2
- package/node_modules/@oxygen/shared/dist/index.d.ts +2 -22
- package/node_modules/@oxygen/shared/dist/index.js +2 -42
- package/node_modules/@oxygen/shared/dist/object-storage.d.ts +9 -0
- package/node_modules/@oxygen/shared/dist/object-storage.js +17 -0
- package/node_modules/@oxygen/shared/dist/operational-telemetry.d.ts +41 -0
- package/node_modules/@oxygen/shared/dist/operational-telemetry.js +55 -0
- package/node_modules/@oxygen/shared/dist/plan-band.d.ts +117 -1
- package/node_modules/@oxygen/shared/dist/plan-band.js +175 -10
- package/node_modules/@oxygen/shared/dist/plan-capabilities.d.ts +77 -7
- package/node_modules/@oxygen/shared/dist/plan-capabilities.js +87 -7
- package/node_modules/@oxygen/shared/dist/plan-limits-view.d.ts +219 -0
- package/node_modules/@oxygen/shared/dist/plan-limits-view.js +330 -0
- package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +204 -6
- package/node_modules/@oxygen/shared/dist/plan-limits.js +197 -15
- package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +80 -36
- package/node_modules/@oxygen/shared/dist/pricing-sheet.js +80 -31
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +38 -20
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +47 -34
- package/node_modules/@oxygen/shared/dist/provider-http-error.d.ts +10 -0
- package/node_modules/@oxygen/shared/dist/provider-http-error.js +27 -0
- package/node_modules/@oxygen/shared/dist/repricing.d.ts +127 -0
- package/node_modules/@oxygen/shared/dist/repricing.js +407 -6
- package/node_modules/@oxygen/shared/dist/semver.d.ts +21 -0
- package/node_modules/@oxygen/shared/dist/semver.js +41 -0
- package/node_modules/@oxygen/shared/dist/sending-limits.d.ts +5 -7
- package/node_modules/@oxygen/shared/dist/sending-limits.js +10 -16
- package/node_modules/@oxygen/shared/dist/sending-seats.d.ts +18 -15
- package/node_modules/@oxygen/shared/dist/sending-seats.js +22 -17
- package/node_modules/@oxygen/shared/dist/spend-safety.d.ts +57 -8
- package/node_modules/@oxygen/shared/dist/spend-safety.js +64 -11
- package/node_modules/@oxygen/shared/dist/stripe-price-catalog.d.ts +15 -7
- package/node_modules/@oxygen/shared/dist/stripe-price-catalog.js +18 -5
- package/node_modules/@oxygen/shared/dist/table-capacity.d.ts +34 -5
- package/node_modules/@oxygen/shared/dist/table-capacity.js +25 -8
- package/node_modules/@oxygen/shared/dist/telemetry-export-observer.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/telemetry-export-observer.js +13 -5
- package/node_modules/@oxygen/shared/dist/telemetry-resource.d.ts +40 -0
- package/node_modules/@oxygen/shared/dist/telemetry-resource.js +35 -0
- package/node_modules/@oxygen/shared/dist/telemetry.js +5 -0
- package/node_modules/@oxygen/shared/dist/ugc.d.ts +15 -0
- package/node_modules/@oxygen/shared/dist/ugc.js +29 -0
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -3
- package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
- package/node_modules/@oxygen/shared/dist/version.js +14 -27
- package/node_modules/@oxygen/shared/dist/workspace-file-storage.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/workspace-file-storage.js +5 -0
- package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.d.ts +3 -3
- package/node_modules/@oxygen/workflows/dist/graph/types.d.ts +15 -1
- package/node_modules/@oxygen/workflows/dist/graph/types.js +15 -1
- package/node_modules/@oxygen/workflows/dist/index.d.ts +45 -0
- package/node_modules/@oxygen/workflows/dist/index.js +152 -2
- package/node_modules/@oxygen/workflows/dist/usage-estimate.d.ts +10 -1
- package/node_modules/@oxygen/workflows/dist/usage-estimate.js +33 -29
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
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
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
// credits
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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
|
|
108
|
-
* at. Append when the rate changes; never remove one, because an unpaid
|
|
109
|
-
* quoted at it can still be paid. The first entry is also the rate of
|
|
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 =
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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)
|
|
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) {
|