@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
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How `GET /api/cli/limits` reads to a person: the seven plan sizes side by
|
|
3
|
+
* side, the workspace's own spend defaults, and what the 2026-09 repricing
|
|
4
|
+
* schedule moves (decision F.2: the value in force and the value pending for
|
|
5
|
+
* it). The MCP `oxygen.plan-limits` widget and the web Settings → Plan limits
|
|
6
|
+
* tab both render from here, so the two cannot disagree on a label or a
|
|
7
|
+
* pending value. Pure and browser-safe: it formats an envelope it is handed and
|
|
8
|
+
* reads nothing else.
|
|
9
|
+
*/
|
|
10
|
+
/** What a band's storage moves to (`plan_bands[].storage.target`, decision F.2). */
|
|
11
|
+
export type PlanLimitsStorageTarget = {
|
|
12
|
+
table_row_limit?: number;
|
|
13
|
+
workspace_row_limit?: number;
|
|
14
|
+
workspace_database_limit_bytes?: number;
|
|
15
|
+
import_max_rows_per_file?: number;
|
|
16
|
+
};
|
|
17
|
+
export type PlanLimitsBandRow = {
|
|
18
|
+
band?: string;
|
|
19
|
+
label?: string;
|
|
20
|
+
limits_tier?: string;
|
|
21
|
+
current?: boolean;
|
|
22
|
+
monthly_price_usd?: number | null;
|
|
23
|
+
monthly_credits?: number | null;
|
|
24
|
+
volume_bonus_percent?: number | null;
|
|
25
|
+
usd_per_100_credits?: number | null;
|
|
26
|
+
api?: {
|
|
27
|
+
org_requests_per_minute?: number;
|
|
28
|
+
key_requests_per_minute?: number;
|
|
29
|
+
tenant_read_requests_per_key_per_minute?: number;
|
|
30
|
+
tenant_write_requests_per_key_per_minute?: number;
|
|
31
|
+
live_action_requests_per_key_per_minute?: number;
|
|
32
|
+
};
|
|
33
|
+
storage?: {
|
|
34
|
+
table_row_limit?: number;
|
|
35
|
+
workspace_row_limit?: number;
|
|
36
|
+
workspace_database_limit_bytes?: number;
|
|
37
|
+
import_max_rows_per_file?: number;
|
|
38
|
+
import_max_file_bytes?: number;
|
|
39
|
+
status?: string;
|
|
40
|
+
effective_at?: string | null;
|
|
41
|
+
target?: PlanLimitsStorageTarget | null;
|
|
42
|
+
};
|
|
43
|
+
spend?: {
|
|
44
|
+
trigger_run_credit_ceiling?: number | null;
|
|
45
|
+
auto_run_batch_credit_ceiling?: number | null;
|
|
46
|
+
agent_run_max_total_credits?: number;
|
|
47
|
+
copilot_approval_free_per_call_credits?: number;
|
|
48
|
+
copilot_approval_free_per_session_credits?: number;
|
|
49
|
+
org_daily_guard_block_credits?: number | null;
|
|
50
|
+
byok_column_run_max_rows?: number | null;
|
|
51
|
+
};
|
|
52
|
+
tables?: {
|
|
53
|
+
max_rows_per_write_request?: number;
|
|
54
|
+
max_columns_per_run?: number;
|
|
55
|
+
};
|
|
56
|
+
signals?: {
|
|
57
|
+
max_events?: number;
|
|
58
|
+
max_window_days?: number;
|
|
59
|
+
};
|
|
60
|
+
webhooks?: {
|
|
61
|
+
ingress_requests_per_target_per_minute?: number;
|
|
62
|
+
};
|
|
63
|
+
workflows?: {
|
|
64
|
+
cron_min_interval_minutes?: number;
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
export type PlanLimitsWorkflowLimits = {
|
|
68
|
+
cron_min_interval_minutes?: number;
|
|
69
|
+
code_node_max_timeout_seconds?: number;
|
|
70
|
+
run_attempt_timeout_minutes?: number;
|
|
71
|
+
};
|
|
72
|
+
export type PlanLimitsStorageCapacity = {
|
|
73
|
+
platform_guard?: {
|
|
74
|
+
table_row_limit?: number;
|
|
75
|
+
workspace_row_limit?: number;
|
|
76
|
+
workspace_database_limit_bytes?: number;
|
|
77
|
+
};
|
|
78
|
+
usage?: {
|
|
79
|
+
workspace_rows?: number | null;
|
|
80
|
+
largest_table_rows?: number | null;
|
|
81
|
+
largest_table?: {
|
|
82
|
+
id?: string;
|
|
83
|
+
name?: string;
|
|
84
|
+
} | null;
|
|
85
|
+
workspace_database_bytes?: number;
|
|
86
|
+
} | null;
|
|
87
|
+
};
|
|
88
|
+
export type PlanLimitsScheduledChange = {
|
|
89
|
+
key?: string;
|
|
90
|
+
kind?: string;
|
|
91
|
+
unit?: string;
|
|
92
|
+
description?: string;
|
|
93
|
+
value_before?: unknown;
|
|
94
|
+
value_after?: unknown;
|
|
95
|
+
};
|
|
96
|
+
export type PlanLimitsPricingSchedule = {
|
|
97
|
+
effective_at?: string;
|
|
98
|
+
status?: string;
|
|
99
|
+
changes?: PlanLimitsScheduledChange[];
|
|
100
|
+
};
|
|
101
|
+
type ChannelLimit = {
|
|
102
|
+
default_per_day?: number;
|
|
103
|
+
max_per_day?: number | null;
|
|
104
|
+
};
|
|
105
|
+
export type PlanLimitsSendingLimits = {
|
|
106
|
+
email_per_mailbox?: ChannelLimit;
|
|
107
|
+
whatsapp_per_number?: ChannelLimit;
|
|
108
|
+
voice_per_number?: ChannelLimit;
|
|
109
|
+
linkedin_per_account?: {
|
|
110
|
+
invites_per_day?: number;
|
|
111
|
+
invites_per_day_max?: number;
|
|
112
|
+
messages_per_day?: number;
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
export type PlanLimitsSpendDefaults = {
|
|
116
|
+
trigger_run_credit_ceiling?: number | null;
|
|
117
|
+
auto_run_batch_credit_ceiling?: number | null;
|
|
118
|
+
agent_run_max_total_credits?: number;
|
|
119
|
+
agent_run_max_inference_credits?: number;
|
|
120
|
+
/** The most a run approved above the default may spend (decision F.4). */
|
|
121
|
+
agent_run_approved_max_credits?: number;
|
|
122
|
+
copilot_approval_free_per_call_credits?: number;
|
|
123
|
+
copilot_approval_free_per_session_credits?: number;
|
|
124
|
+
copilot_attended_turn_backstop_credits?: number;
|
|
125
|
+
org_daily_spend_guard?: {
|
|
126
|
+
applies?: boolean;
|
|
127
|
+
source?: string;
|
|
128
|
+
warn_credits?: number | null;
|
|
129
|
+
block_credits?: number | null;
|
|
130
|
+
spent_today?: number | null;
|
|
131
|
+
};
|
|
132
|
+
applies_to?: Record<string, string>;
|
|
133
|
+
};
|
|
134
|
+
/** The slice of the `limits show` envelope this module reads. */
|
|
135
|
+
export type PlanLimitsEnvelope = {
|
|
136
|
+
plan_band?: string;
|
|
137
|
+
plan_band_label?: string;
|
|
138
|
+
limits_tier?: string;
|
|
139
|
+
plan_bands?: PlanLimitsBandRow[];
|
|
140
|
+
sending_limits?: PlanLimitsSendingLimits;
|
|
141
|
+
workflow_limits?: PlanLimitsWorkflowLimits;
|
|
142
|
+
shared_billing?: boolean;
|
|
143
|
+
storage_capacity?: PlanLimitsStorageCapacity;
|
|
144
|
+
spend_safety_defaults?: PlanLimitsSpendDefaults;
|
|
145
|
+
pricing_schedule?: PlanLimitsPricingSchedule;
|
|
146
|
+
deepLink?: string;
|
|
147
|
+
web_url?: string;
|
|
148
|
+
billing_url?: string;
|
|
149
|
+
};
|
|
150
|
+
export declare function formatLimitCount(value: number | null | undefined): string;
|
|
151
|
+
export declare function formatLimitCredits(value: number | null | undefined): string;
|
|
152
|
+
export declare function formatLimitBytes(value: number | null | undefined): string;
|
|
153
|
+
/**
|
|
154
|
+
* `storage.<band>.<field>` / `workflows.…` / `spend.…` / `agents.…` → the
|
|
155
|
+
* value it takes once the scheduled change is in force. Empty unless the
|
|
156
|
+
* schedule is still ahead (`status: "scheduled"`).
|
|
157
|
+
*/
|
|
158
|
+
export declare function scheduledLimitChanges(schedule: PlanLimitsPricingSchedule | undefined): Map<string, number>;
|
|
159
|
+
export type PlanLimitsLadder = {
|
|
160
|
+
bands: Array<{
|
|
161
|
+
band: string;
|
|
162
|
+
label: string;
|
|
163
|
+
current: boolean;
|
|
164
|
+
}>;
|
|
165
|
+
rows: Array<{
|
|
166
|
+
label: string;
|
|
167
|
+
cells: string[];
|
|
168
|
+
}>;
|
|
169
|
+
/** What `→` and `†` mean, when a cell uses them. */
|
|
170
|
+
notes: string[];
|
|
171
|
+
};
|
|
172
|
+
/** Every plan size's limits side by side, with the pending value after `→`. */
|
|
173
|
+
export declare function planLimitsLadder(payload: PlanLimitsEnvelope): PlanLimitsLadder;
|
|
174
|
+
/**
|
|
175
|
+
* The workspace's own usage against the limits it enforces, so "how close am
|
|
176
|
+
* I?" sits beside "what would a bigger plan give me?". The largest Table is
|
|
177
|
+
* named, so a full one is identifiable (decision F.2).
|
|
178
|
+
*/
|
|
179
|
+
export declare function planLimitsStorageUsageLine(storage: PlanLimitsStorageCapacity | undefined): string | null;
|
|
180
|
+
/** Sending limits are the same on every plan: one row per channel. */
|
|
181
|
+
export declare function planLimitsSendingRows(sending: PlanLimitsSendingLimits | undefined): Array<{
|
|
182
|
+
label: string;
|
|
183
|
+
defaultPerDay: string;
|
|
184
|
+
maximumPerDay: string;
|
|
185
|
+
}>;
|
|
186
|
+
/**
|
|
187
|
+
* The Code step and run attempt limits are the same on every plan; the fastest
|
|
188
|
+
* schedule, which differs, is a row of the ladder.
|
|
189
|
+
*/
|
|
190
|
+
export declare function planLimitsWorkflowNote(workflows: PlanLimitsWorkflowLimits | undefined): string | null;
|
|
191
|
+
export type PlanLimitsSpendDefaultRow = {
|
|
192
|
+
label: string;
|
|
193
|
+
value: string;
|
|
194
|
+
/** The value from the scheduled date, when one is set and it differs. */
|
|
195
|
+
next: string | null;
|
|
196
|
+
/** What the default bounds, from `spend_safety_defaults.applies_to`. */
|
|
197
|
+
scope: string | null;
|
|
198
|
+
};
|
|
199
|
+
/**
|
|
200
|
+
* The workspace's own spend defaults, each with what it bounds and the value
|
|
201
|
+
* it takes on the scheduled date (L5.1–L5.4). Read from the plan size the
|
|
202
|
+
* workspace is on (`plan_band`, `limits_tier`), so the pending value is the
|
|
203
|
+
* one this workspace will get.
|
|
204
|
+
*/
|
|
205
|
+
export declare function planLimitsSpendDefaults(payload: PlanLimitsEnvelope): PlanLimitsSpendDefaultRow[];
|
|
206
|
+
export type PlanLimitsScheduleRow = {
|
|
207
|
+
key: string;
|
|
208
|
+
description: string;
|
|
209
|
+
before: string;
|
|
210
|
+
after: string;
|
|
211
|
+
};
|
|
212
|
+
export type PlanLimitsSchedule = {
|
|
213
|
+
effectiveAt: string;
|
|
214
|
+
inForce: boolean;
|
|
215
|
+
rows: PlanLimitsScheduleRow[];
|
|
216
|
+
};
|
|
217
|
+
/** Every price and limit the dated repricing moves, before and after. */
|
|
218
|
+
export declare function planLimitsSchedule(schedule: PlanLimitsPricingSchedule | undefined): PlanLimitsSchedule | null;
|
|
219
|
+
export {};
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How `GET /api/cli/limits` reads to a person: the seven plan sizes side by
|
|
3
|
+
* side, the workspace's own spend defaults, and what the 2026-09 repricing
|
|
4
|
+
* schedule moves (decision F.2: the value in force and the value pending for
|
|
5
|
+
* it). The MCP `oxygen.plan-limits` widget and the web Settings → Plan limits
|
|
6
|
+
* tab both render from here, so the two cannot disagree on a label or a
|
|
7
|
+
* pending value. Pure and browser-safe: it formats an envelope it is handed and
|
|
8
|
+
* reads nothing else.
|
|
9
|
+
*/
|
|
10
|
+
// Agent run ceilings are scheduled per limits rung (`agents.<rung>.<field>`),
|
|
11
|
+
// the rung each plan size enforces at.
|
|
12
|
+
const AGENT_LIMITS_TIER_BY_BAND = {
|
|
13
|
+
free: "free",
|
|
14
|
+
"49": "starter",
|
|
15
|
+
"99": "starter",
|
|
16
|
+
"199": "pro",
|
|
17
|
+
"499": "pro",
|
|
18
|
+
"999": "team",
|
|
19
|
+
"1999": "scale",
|
|
20
|
+
};
|
|
21
|
+
const GIB = 1024 ** 3;
|
|
22
|
+
const MIB = 1024 ** 2;
|
|
23
|
+
export function formatLimitCount(value) {
|
|
24
|
+
return typeof value === "number" ? value.toLocaleString("en-US") : "—";
|
|
25
|
+
}
|
|
26
|
+
export function formatLimitCredits(value) {
|
|
27
|
+
return typeof value === "number" ? `${value.toLocaleString("en-US")} cr` : "—";
|
|
28
|
+
}
|
|
29
|
+
function everyMinutes(value) {
|
|
30
|
+
if (typeof value !== "number")
|
|
31
|
+
return "—";
|
|
32
|
+
return value === 1 ? "every minute" : `every ${formatLimitCount(value)} min`;
|
|
33
|
+
}
|
|
34
|
+
export function formatLimitBytes(value) {
|
|
35
|
+
if (typeof value !== "number")
|
|
36
|
+
return "—";
|
|
37
|
+
return value >= GIB ? `${Number((value / GIB).toFixed(2))} GiB` : `${Number((value / MIB).toFixed(0))} MiB`;
|
|
38
|
+
}
|
|
39
|
+
function storageKey(field) {
|
|
40
|
+
return (band) => `storage.${band}.${field}`;
|
|
41
|
+
}
|
|
42
|
+
// One line per limit a founder asks about before sizing a plan: API rate,
|
|
43
|
+
// storage, and what unattended work may spend without asking. The full matrix
|
|
44
|
+
// stays in the JSON (`limits`, `spend_safety_defaults`).
|
|
45
|
+
const LINES = [
|
|
46
|
+
{ label: "Price / month", value: (row) => (typeof row.monthly_price_usd === "number" ? `$${formatLimitCount(row.monthly_price_usd)}` : "$0") },
|
|
47
|
+
{ label: "Credits / month", value: (row) => formatLimitCredits(row.monthly_credits) },
|
|
48
|
+
{ label: "Volume bonus", value: (row) => (typeof row.volume_bonus_percent === "number" && row.volume_bonus_percent > 0 ? `+${row.volume_bonus_percent}%` : "—") },
|
|
49
|
+
{ label: "Price / 100 credits", value: (row) => (typeof row.usd_per_100_credits === "number" ? `$${row.usd_per_100_credits.toFixed(row.usd_per_100_credits === 1 ? 2 : 4)}` : "—") },
|
|
50
|
+
{ label: "API requests / min (workspace)", value: (row) => formatLimitCount(row.api?.org_requests_per_minute) },
|
|
51
|
+
{ label: "API requests / min (per key)", value: (row) => formatLimitCount(row.api?.key_requests_per_minute) },
|
|
52
|
+
{ label: "Table reads / min (per key)", value: (row) => formatLimitCount(row.api?.tenant_read_requests_per_key_per_minute) },
|
|
53
|
+
{ label: "Table writes / min (per key)", value: (row) => formatLimitCount(row.api?.tenant_write_requests_per_key_per_minute) },
|
|
54
|
+
{ label: "Live tool and AI calls / min (per key)", value: (row) => formatLimitCount(row.api?.live_action_requests_per_key_per_minute) },
|
|
55
|
+
{ label: "Webhook deliveries / min (per target)", value: (row) => formatLimitCount(row.webhooks?.ingress_requests_per_target_per_minute) },
|
|
56
|
+
{
|
|
57
|
+
label: "Rows per table",
|
|
58
|
+
value: (row) => formatLimitCount(row.storage?.table_row_limit),
|
|
59
|
+
scheduledKey: storageKey("table_row_limit"),
|
|
60
|
+
target: (target) => target.table_row_limit,
|
|
61
|
+
format: formatLimitCount,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
label: "Rows per workspace",
|
|
65
|
+
value: (row) => formatLimitCount(row.storage?.workspace_row_limit),
|
|
66
|
+
scheduledKey: storageKey("workspace_row_limit"),
|
|
67
|
+
target: (target) => target.workspace_row_limit,
|
|
68
|
+
format: formatLimitCount,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
label: "Workspace database",
|
|
72
|
+
value: (row) => formatLimitBytes(row.storage?.workspace_database_limit_bytes),
|
|
73
|
+
scheduledKey: storageKey("workspace_database_limit_bytes"),
|
|
74
|
+
target: (target) => target.workspace_database_limit_bytes,
|
|
75
|
+
format: formatLimitBytes,
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
label: "Rows per import file",
|
|
79
|
+
value: (row) => formatLimitCount(row.storage?.import_max_rows_per_file),
|
|
80
|
+
scheduledKey: storageKey("table_row_limit"),
|
|
81
|
+
target: (target) => target.import_max_rows_per_file,
|
|
82
|
+
format: formatLimitCount,
|
|
83
|
+
},
|
|
84
|
+
{ label: "Import file size", value: (row) => formatLimitBytes(row.storage?.import_max_file_bytes) },
|
|
85
|
+
{ label: "Rows per write request", value: (row) => formatLimitCount(row.tables?.max_rows_per_write_request) },
|
|
86
|
+
{ label: "Columns per auto-run", value: (row) => formatLimitCount(row.tables?.max_columns_per_run) },
|
|
87
|
+
{
|
|
88
|
+
label: "Unattended trigger default cap",
|
|
89
|
+
value: (row) => formatLimitCredits(row.spend?.trigger_run_credit_ceiling),
|
|
90
|
+
scheduledKey: (band) => `spend.${band}.default_delivery_credit_ceiling`,
|
|
91
|
+
format: formatLimitCredits,
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
label: "Table auto-run default cap",
|
|
95
|
+
value: (row) => formatLimitCredits(row.spend?.auto_run_batch_credit_ceiling),
|
|
96
|
+
scheduledKey: (band) => `spend.${band}.default_delivery_credit_ceiling`,
|
|
97
|
+
format: formatLimitCredits,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
label: "Agent run default cap (approve more per run)",
|
|
101
|
+
value: (row) => formatLimitCredits(row.spend?.agent_run_max_total_credits),
|
|
102
|
+
scheduledKey: agentRunKey("max_total_credits"),
|
|
103
|
+
format: formatLimitCredits,
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
label: "Copilot without approval (step / chat)",
|
|
107
|
+
value: (row) => `${formatLimitCount(row.spend?.copilot_approval_free_per_call_credits)} / ${formatLimitCredits(row.spend?.copilot_approval_free_per_session_credits)}`,
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
label: "Daily spend block",
|
|
111
|
+
value: (row) => formatLimitCredits(row.spend?.org_daily_guard_block_credits),
|
|
112
|
+
scheduledKey: (band) => `spend.${band}.org_daily_guard_block_credits`,
|
|
113
|
+
format: formatLimitCredits,
|
|
114
|
+
},
|
|
115
|
+
{ label: "BYOK column-run rows", value: (row) => formatLimitCount(row.spend?.byok_column_run_max_rows) },
|
|
116
|
+
{
|
|
117
|
+
label: "Signals feed",
|
|
118
|
+
value: (row) => `${formatLimitCount(row.signals?.max_events)} / ${formatLimitCount(row.signals?.max_window_days)} d`,
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
label: "Fastest workflow schedule",
|
|
122
|
+
value: (row) => everyMinutes(row.workflows?.cron_min_interval_minutes),
|
|
123
|
+
scheduledKey: (band) => `workflows.${band}.cron_min_interval_minutes`,
|
|
124
|
+
format: everyMinutes,
|
|
125
|
+
},
|
|
126
|
+
];
|
|
127
|
+
function agentRunKey(field) {
|
|
128
|
+
return (band) => `agents.${AGENT_LIMITS_TIER_BY_BAND[band] ?? band}.${field}`;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* `storage.<band>.<field>` / `workflows.…` / `spend.…` / `agents.…` → the
|
|
132
|
+
* value it takes once the scheduled change is in force. Empty unless the
|
|
133
|
+
* schedule is still ahead (`status: "scheduled"`).
|
|
134
|
+
*/
|
|
135
|
+
export function scheduledLimitChanges(schedule) {
|
|
136
|
+
const changes = new Map();
|
|
137
|
+
if (schedule?.status !== "scheduled")
|
|
138
|
+
return changes;
|
|
139
|
+
for (const change of schedule.changes ?? []) {
|
|
140
|
+
if (typeof change.key === "string"
|
|
141
|
+
&& /^(storage|workflows|spend|agents)\./.test(change.key)
|
|
142
|
+
&& typeof change.value_after === "number") {
|
|
143
|
+
changes.set(change.key, change.value_after);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return changes;
|
|
147
|
+
}
|
|
148
|
+
const AWAITING_TEST_MARK = "†";
|
|
149
|
+
function awaitingCapacityTest(row) {
|
|
150
|
+
return row.storage?.status === "awaiting_capacity_test";
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* The value in force, then `→` and the value pending for it: the band's own
|
|
154
|
+
* storage target (a scheduled change, or a raise awaiting its capacity test,
|
|
155
|
+
* marked †), else the dated repricing schedule.
|
|
156
|
+
*/
|
|
157
|
+
function cellText(line, row, scheduled) {
|
|
158
|
+
const current = line.value(row);
|
|
159
|
+
if (!line.format)
|
|
160
|
+
return current;
|
|
161
|
+
const target = row.storage?.target;
|
|
162
|
+
const fromTarget = line.target && target && (row.storage?.status === "scheduled" || awaitingCapacityTest(row))
|
|
163
|
+
? line.target(target)
|
|
164
|
+
: undefined;
|
|
165
|
+
if (fromTarget !== undefined) {
|
|
166
|
+
const next = line.format(fromTarget);
|
|
167
|
+
if (next === current)
|
|
168
|
+
return current;
|
|
169
|
+
return `${current} → ${next}${awaitingCapacityTest(row) ? ` ${AWAITING_TEST_MARK}` : ""}`;
|
|
170
|
+
}
|
|
171
|
+
const next = line.scheduledKey ? scheduled.get(line.scheduledKey(row.band ?? "")) : undefined;
|
|
172
|
+
return next === undefined ? current : `${current} → ${line.format(next)}`;
|
|
173
|
+
}
|
|
174
|
+
/** Every plan size's limits side by side, with the pending value after `→`. */
|
|
175
|
+
export function planLimitsLadder(payload) {
|
|
176
|
+
const bands = Array.isArray(payload.plan_bands) ? payload.plan_bands : [];
|
|
177
|
+
const scheduled = scheduledLimitChanges(payload.pricing_schedule);
|
|
178
|
+
const notes = [];
|
|
179
|
+
if (scheduled.size > 0 && payload.pricing_schedule?.effective_at) {
|
|
180
|
+
notes.push(`"→" marks a limit that changes on ${payload.pricing_schedule.effective_at.slice(0, 10)}.`);
|
|
181
|
+
}
|
|
182
|
+
if (bands.some(awaitingCapacityTest)) {
|
|
183
|
+
notes.push(`"${AWAITING_TEST_MARK}" rises once the rows-per-table capacity test at that plan size passes.`);
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
bands: bands.map((row) => ({ band: row.band ?? "", label: row.label ?? row.band ?? "", current: row.current === true })),
|
|
187
|
+
rows: bands.length === 0
|
|
188
|
+
? []
|
|
189
|
+
: LINES.map((line) => ({ label: line.label, cells: bands.map((row) => cellText(line, row, scheduled)) })),
|
|
190
|
+
notes,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* The workspace's own usage against the limits it enforces, so "how close am
|
|
195
|
+
* I?" sits beside "what would a bigger plan give me?". The largest Table is
|
|
196
|
+
* named, so a full one is identifiable (decision F.2).
|
|
197
|
+
*/
|
|
198
|
+
export function planLimitsStorageUsageLine(storage) {
|
|
199
|
+
const guard = storage?.platform_guard;
|
|
200
|
+
const usage = storage?.usage;
|
|
201
|
+
if (!guard || !usage)
|
|
202
|
+
return null;
|
|
203
|
+
const largest = usage.largest_table?.name ? `Largest table “${usage.largest_table.name}”` : "Largest table";
|
|
204
|
+
const parts = [
|
|
205
|
+
`${largest} ${formatLimitCount(usage.largest_table_rows)} / ${formatLimitCount(guard.table_row_limit)} rows`,
|
|
206
|
+
`workspace ${formatLimitCount(usage.workspace_rows)} / ${formatLimitCount(guard.workspace_row_limit)} rows`,
|
|
207
|
+
`database ${formatLimitBytes(usage.workspace_database_bytes)} / ${formatLimitBytes(guard.workspace_database_limit_bytes)}`,
|
|
208
|
+
];
|
|
209
|
+
return `Your storage: ${parts.join(" · ")}`;
|
|
210
|
+
}
|
|
211
|
+
/** Sending limits are the same on every plan: one row per channel. */
|
|
212
|
+
export function planLimitsSendingRows(sending) {
|
|
213
|
+
if (!sending)
|
|
214
|
+
return [];
|
|
215
|
+
const maximum = (limit) => limit?.max_per_day === null ? "no maximum" : formatLimitCount(limit?.max_per_day);
|
|
216
|
+
return [
|
|
217
|
+
{ label: "Email per mailbox", defaultPerDay: formatLimitCount(sending.email_per_mailbox?.default_per_day), maximumPerDay: maximum(sending.email_per_mailbox) },
|
|
218
|
+
{ label: "WhatsApp per number", defaultPerDay: formatLimitCount(sending.whatsapp_per_number?.default_per_day), maximumPerDay: maximum(sending.whatsapp_per_number) },
|
|
219
|
+
{
|
|
220
|
+
label: "LinkedIn invites per account",
|
|
221
|
+
defaultPerDay: formatLimitCount(sending.linkedin_per_account?.invites_per_day),
|
|
222
|
+
maximumPerDay: formatLimitCount(sending.linkedin_per_account?.invites_per_day_max),
|
|
223
|
+
},
|
|
224
|
+
{ label: "Calls per number", defaultPerDay: formatLimitCount(sending.voice_per_number?.default_per_day), maximumPerDay: maximum(sending.voice_per_number) },
|
|
225
|
+
];
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* The Code step and run attempt limits are the same on every plan; the fastest
|
|
229
|
+
* schedule, which differs, is a row of the ladder.
|
|
230
|
+
*/
|
|
231
|
+
export function planLimitsWorkflowNote(workflows) {
|
|
232
|
+
if (!workflows)
|
|
233
|
+
return null;
|
|
234
|
+
const parts = [
|
|
235
|
+
typeof workflows.code_node_max_timeout_seconds === "number"
|
|
236
|
+
? `a Code step may run ${formatLimitCount(workflows.code_node_max_timeout_seconds)} s`
|
|
237
|
+
: null,
|
|
238
|
+
typeof workflows.run_attempt_timeout_minutes === "number"
|
|
239
|
+
? `one run attempt ${formatLimitCount(workflows.run_attempt_timeout_minutes)} min, then it resumes from its last checkpoint`
|
|
240
|
+
: null,
|
|
241
|
+
].filter((part) => part !== null);
|
|
242
|
+
if (parts.length === 0)
|
|
243
|
+
return null;
|
|
244
|
+
return `Workflows on every plan: ${parts.join("; ")}.`;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* The workspace's own spend defaults, each with what it bounds and the value
|
|
248
|
+
* it takes on the scheduled date (L5.1–L5.4). Read from the plan size the
|
|
249
|
+
* workspace is on (`plan_band`, `limits_tier`), so the pending value is the
|
|
250
|
+
* one this workspace will get.
|
|
251
|
+
*/
|
|
252
|
+
export function planLimitsSpendDefaults(payload) {
|
|
253
|
+
const spend = payload.spend_safety_defaults;
|
|
254
|
+
if (!spend)
|
|
255
|
+
return [];
|
|
256
|
+
const scheduled = scheduledLimitChanges(payload.pricing_schedule);
|
|
257
|
+
const band = payload.plan_band ?? "";
|
|
258
|
+
const agentKey = (field) => `agents.${payload.limits_tier ?? AGENT_LIMITS_TIER_BY_BAND[band] ?? band}.${field}`;
|
|
259
|
+
const appliesTo = spend.applies_to ?? {};
|
|
260
|
+
const row = (label, value, scopeKey, scheduledKey) => {
|
|
261
|
+
const next = scheduledKey ? scheduled.get(scheduledKey) : undefined;
|
|
262
|
+
return {
|
|
263
|
+
label,
|
|
264
|
+
value: formatLimitCredits(value),
|
|
265
|
+
next: next === undefined || next === value ? null : formatLimitCredits(next),
|
|
266
|
+
scope: appliesTo[scopeKey] ?? null,
|
|
267
|
+
};
|
|
268
|
+
};
|
|
269
|
+
const rows = [
|
|
270
|
+
row("Unattended trigger default cap", spend.trigger_run_credit_ceiling, "trigger_run_credit_ceiling", `spend.${band}.default_delivery_credit_ceiling`),
|
|
271
|
+
row("Table auto-run default cap", spend.auto_run_batch_credit_ceiling, "auto_run_batch_credit_ceiling", `spend.${band}.default_delivery_credit_ceiling`),
|
|
272
|
+
row("Agent run default cap", spend.agent_run_max_total_credits, "agent_run_max_total_credits", agentKey("max_total_credits")),
|
|
273
|
+
row("Agent model calls", spend.agent_run_max_inference_credits, "agent_run_max_inference_credits", agentKey("max_inference_credits")),
|
|
274
|
+
row("Copilot without approval, per step", spend.copilot_approval_free_per_call_credits, "copilot_approval_free_per_call_credits"),
|
|
275
|
+
row("Copilot without approval, per chat", spend.copilot_approval_free_per_session_credits, "copilot_approval_free_per_session_credits"),
|
|
276
|
+
row("Copilot turn limit", spend.copilot_attended_turn_backstop_credits, "copilot_attended_turn_backstop_credits"),
|
|
277
|
+
];
|
|
278
|
+
if (typeof spend.agent_run_approved_max_credits === "number") {
|
|
279
|
+
rows.push({
|
|
280
|
+
label: "Agent run with approval, at most",
|
|
281
|
+
value: formatLimitCredits(spend.agent_run_approved_max_credits),
|
|
282
|
+
next: null,
|
|
283
|
+
scope: "A run or trigger you approve above the default may spend up to this.",
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
const guard = spend.org_daily_spend_guard;
|
|
287
|
+
if (guard) {
|
|
288
|
+
const next = scheduled.get(`spend.${band}.org_daily_guard_block_credits`);
|
|
289
|
+
rows.push({
|
|
290
|
+
label: "Daily spend block",
|
|
291
|
+
value: guard.applies && typeof guard.block_credits === "number"
|
|
292
|
+
? `${formatLimitCredits(guard.block_credits)} (warns at ${formatLimitCredits(guard.warn_credits)})`
|
|
293
|
+
: guard.source === "explicit_org_daily_policy"
|
|
294
|
+
? "Your own daily budget"
|
|
295
|
+
: "—",
|
|
296
|
+
next: next === undefined || next === guard.block_credits ? null : formatLimitCredits(next),
|
|
297
|
+
scope: "Managed spend stops for the rest of the UTC day at this amount.",
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
return rows;
|
|
301
|
+
}
|
|
302
|
+
/** Every price and limit the dated repricing moves, before and after. */
|
|
303
|
+
export function planLimitsSchedule(schedule) {
|
|
304
|
+
if (!schedule?.effective_at || (schedule.status !== "scheduled" && schedule.status !== "in_force"))
|
|
305
|
+
return null;
|
|
306
|
+
return {
|
|
307
|
+
effectiveAt: schedule.effective_at.slice(0, 10),
|
|
308
|
+
inForce: schedule.status === "in_force",
|
|
309
|
+
rows: (schedule.changes ?? []).map((change) => ({
|
|
310
|
+
key: change.key ?? "",
|
|
311
|
+
description: change.description ?? change.key ?? "",
|
|
312
|
+
before: formatScheduledValue(change.value_before, change.unit),
|
|
313
|
+
after: formatScheduledValue(change.value_after, change.unit),
|
|
314
|
+
})),
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
function formatScheduledValue(value, unit) {
|
|
318
|
+
if (value === null || value === undefined)
|
|
319
|
+
return "no limit";
|
|
320
|
+
if (typeof value === "boolean")
|
|
321
|
+
return value ? "yes" : "no";
|
|
322
|
+
if (typeof value !== "number")
|
|
323
|
+
return String(value);
|
|
324
|
+
if (unit === "bytes")
|
|
325
|
+
return formatLimitBytes(value);
|
|
326
|
+
if (unit === "usd_cents_per_100_credits")
|
|
327
|
+
return `$${(value / 100).toFixed(2)} per 100 credits`;
|
|
328
|
+
const words = (unit ?? "").replace(/_/g, " ").trim();
|
|
329
|
+
return words ? `${formatLimitCount(value)} ${words}` : formatLimitCount(value);
|
|
330
|
+
}
|