@oxygen-agent/cli 1.1010.721 → 1.1010.950
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/contract-flags.d.ts +42 -0
- package/dist/contract-flags.js +220 -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 +622 -325
- package/dist/run-wait.d.ts +3 -1
- package/dist/run-wait.js +19 -5
- package/dist/sender-limits-flags.d.ts +14 -0
- package/dist/sender-limits-flags.js +157 -0
- package/dist/streamed-file-import.d.ts +58 -0
- package/dist/streamed-file-import.js +115 -0
- package/dist/ugc-commands.js +16 -2
- 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 +6 -6
- package/node_modules/@oxygen/cli-ugc/dist/field-parser.d.ts +4 -0
- package/node_modules/@oxygen/cli-ugc/dist/field-parser.js +20 -8
- package/node_modules/@oxygen/cli-ugc/dist/index.d.ts +1 -0
- package/node_modules/@oxygen/cli-ugc/dist/index.js +1 -0
- 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/contracts/operations.d.ts +39 -0
- package/node_modules/@oxygen/shared/dist/contracts/operations.js +11 -0
- package/node_modules/@oxygen/shared/dist/contracts/schema.d.ts +186 -0
- package/node_modules/@oxygen/shared/dist/contracts/schema.js +260 -0
- package/node_modules/@oxygen/shared/dist/contracts/sender-limits.d.ts +67 -0
- package/node_modules/@oxygen/shared/dist/contracts/sender-limits.js +88 -0
- 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/linkedin-sequences.d.ts +11 -0
- package/node_modules/@oxygen/shared/dist/linkedin-sequences.js +41 -0
- 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/product-briefing-rules.js +4 -2
- 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 +35 -5
- package/node_modules/@oxygen/shared/dist/table-capacity.js +40 -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 +17 -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
package/dist/run-wait.d.ts
CHANGED
|
@@ -8,7 +8,9 @@ export interface CliRunWaitConfig {
|
|
|
8
8
|
defaultTimeoutSeconds: number;
|
|
9
9
|
defaultIntervalSeconds: number;
|
|
10
10
|
/** Fetches the latest run snapshot to inspect this poll. */
|
|
11
|
-
fetchRun: () => Promise<Record<string, unknown>>;
|
|
11
|
+
fetchRun: (signal?: AbortSignal) => Promise<Record<string, unknown>>;
|
|
12
|
+
/** Opt in only for read-only polls; retries share the original deadline. */
|
|
13
|
+
retryTransientNetworkErrors?: boolean;
|
|
12
14
|
/** True when the run status is terminal for this resource. */
|
|
13
15
|
isTerminal: (status: string | null) => boolean;
|
|
14
16
|
/** Builds the response once a terminal status is observed. */
|
package/dist/run-wait.js
CHANGED
|
@@ -8,12 +8,26 @@ export async function waitForCliRun(config) {
|
|
|
8
8
|
const startedAt = Date.now();
|
|
9
9
|
const deadline = startedAt + timeoutSeconds * 1000;
|
|
10
10
|
let polls = 0;
|
|
11
|
+
let latestRun = null;
|
|
12
|
+
let status = null;
|
|
11
13
|
while (true) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
if (Date.now() < deadline) {
|
|
15
|
+
polls += 1;
|
|
16
|
+
const signal = config.retryTransientNetworkErrors
|
|
17
|
+
? AbortSignal.timeout(Math.max(1, deadline - Date.now())) : undefined;
|
|
18
|
+
try {
|
|
19
|
+
latestRun = await config.fetchRun(signal);
|
|
20
|
+
status = readRecordString(latestRun, "status");
|
|
21
|
+
if (config.isTerminal(status)) {
|
|
22
|
+
return config.shapeTerminal(latestRun, status, polls, Date.now() - startedAt);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
const transient = error instanceof OxygenError
|
|
27
|
+
&& (error.code === "network_error" || error.code === "network_timeout");
|
|
28
|
+
if (!config.retryTransientNetworkErrors || (!transient && !signal?.aborted))
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
17
31
|
}
|
|
18
32
|
const remainingMs = deadline - Date.now();
|
|
19
33
|
if (remainingMs <= 0) {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type ContractFlagOverlay } from "./contract-flags.js";
|
|
2
|
+
/**
|
|
3
|
+
* `--working-days` → ISO weekday numbers. Accepts day names or 1-7, comma
|
|
4
|
+
* separated, and ranges (`mon-fri`, `1-5`). A token it cannot read is sent as
|
|
5
|
+
* given so the server's invalid_working_hours error names it.
|
|
6
|
+
*/
|
|
7
|
+
export declare function parseWorkingDaysOption(value: string): unknown[];
|
|
8
|
+
export declare const SENDER_LIMITS_FLAG_OVERLAY: ContractFlagOverlay;
|
|
9
|
+
/**
|
|
10
|
+
* The limits PATCH body for the flags given. A warm-up start date wins over
|
|
11
|
+
* --warmup-restart/--warmup-disable (the route reads it first anyway), so the
|
|
12
|
+
* body carries only the one it will act on. Throws when no flag was given.
|
|
13
|
+
*/
|
|
14
|
+
export declare function buildSenderLimitsBody(options: Record<string, unknown>): Record<string, unknown>;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// `oxygen senders limits set` (and `whatsapp limits set`, which shares the body
|
|
2
|
+
// builder): flags generated from SENDER_LIMITS_PATCH_CONTRACT, the contract the
|
|
3
|
+
// MCP tool and the route use too. This overlay only keeps the flag names and
|
|
4
|
+
// help text the command shipped with before the contract existed.
|
|
5
|
+
import { OxygenError } from "@oxygen/shared/cli-result";
|
|
6
|
+
import { LINKEDIN_SENDER_LIMIT_DEFAULTS, LINKEDIN_SENDER_LIMIT_MAXIMUMS, } from "@oxygen/shared/linkedin-sequences";
|
|
7
|
+
import { SENDER_LIMITS_PATCH_CONTRACT } from "@oxygen/shared/contracts/sender-limits";
|
|
8
|
+
import { readContractFlags } from "./contract-flags.js";
|
|
9
|
+
/** "<text> (default N, max M)." for a LinkedIn sender limit flag, from the enforced constants. */
|
|
10
|
+
function linkedInLimitHelp(key, text) {
|
|
11
|
+
return `${text} (default ${LINKEDIN_SENDER_LIMIT_DEFAULTS[key]}, max ${LINKEDIN_SENDER_LIMIT_MAXIMUMS[key]}).`;
|
|
12
|
+
}
|
|
13
|
+
const LIMIT_FLAG_HELP = {
|
|
14
|
+
invites_per_day: "Daily LinkedIn connection invites cap",
|
|
15
|
+
invites_per_week: "Weekly LinkedIn connection invites cap",
|
|
16
|
+
messages_per_day: "Daily direct messages cap",
|
|
17
|
+
inmails_per_day: "Daily InMail cap",
|
|
18
|
+
inmails_per_month: "InMail cap over a rolling 30 days",
|
|
19
|
+
profile_views_per_day: "Daily profile views cap; the member is notified",
|
|
20
|
+
profile_lookups_per_day: "Daily invisible profile and company lookups cap",
|
|
21
|
+
endorsements_per_day: "Daily skill endorsements cap",
|
|
22
|
+
withdrawals_per_day: "Daily cap on withdrawing sent invitations; reading the sent list uses the read budgets",
|
|
23
|
+
posts_per_day: "Daily posts to the account's own feed, from every caller; not counted in the total",
|
|
24
|
+
follows_per_day: "Daily follows cap",
|
|
25
|
+
likes_per_day: "Daily likes cap",
|
|
26
|
+
comments_per_day: "Daily comments cap",
|
|
27
|
+
total_actions_per_day: "Daily cap across all send/action types except posts",
|
|
28
|
+
relations_reads_per_day: "Daily cap on relations/connections list reads, the scrape ban vector",
|
|
29
|
+
messages_reads_per_day: "Daily cap on chat and message-history reads",
|
|
30
|
+
searches_per_day: "Daily cap on LinkedIn search executions",
|
|
31
|
+
sales_nav_search_results_per_day: "Daily cap on Sales Navigator search result rows fetched",
|
|
32
|
+
api_reads_per_day: "Daily cap on all other LinkedIn API reads",
|
|
33
|
+
total_reads_per_day: "Daily cap across all read units",
|
|
34
|
+
min_action_spacing_seconds: "Minimum seconds between actions",
|
|
35
|
+
action_spacing_jitter_seconds: "Random jitter seconds added to action spacing",
|
|
36
|
+
interactive_min_spacing_seconds: "Minimum seconds between human-sent Unibox actions",
|
|
37
|
+
interactive_spacing_jitter_seconds: "Random jitter seconds added to human-sent Unibox action spacing",
|
|
38
|
+
interactive_sends_per_hour: "Hourly cap on human-sent Unibox actions",
|
|
39
|
+
relations_ingest_reads_per_day: "Daily cap on background connection, follower and sent-invite list reads (imports and the withdrawal drip)",
|
|
40
|
+
api_ingest_reads_per_day: "Daily cap on background engagement, post-metric and profile-viewer reads",
|
|
41
|
+
messages_ingest_reads_per_day: "Daily cap on background message-history backfill reads",
|
|
42
|
+
network_backfill_reads_per_day: "Daily network-capture reads while the first walk of connections and followers runs",
|
|
43
|
+
network_delta_reads_per_day: "Daily network-capture reads once that first walk is done",
|
|
44
|
+
};
|
|
45
|
+
const LIMIT_FLAG_NAMES = {
|
|
46
|
+
min_action_spacing_seconds: "min-spacing-seconds",
|
|
47
|
+
action_spacing_jitter_seconds: "spacing-jitter-seconds",
|
|
48
|
+
};
|
|
49
|
+
const WORKING_DAY_NAMES = {
|
|
50
|
+
mon: 1, monday: 1, tue: 2, tues: 2, tuesday: 2, wed: 3, wednesday: 3, thu: 4, thur: 4, thurs: 4, thursday: 4,
|
|
51
|
+
fri: 5, friday: 5, sat: 6, saturday: 6, sun: 7, sunday: 7,
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* `--working-days` → ISO weekday numbers. Accepts day names or 1-7, comma
|
|
55
|
+
* separated, and ranges (`mon-fri`, `1-5`). A token it cannot read is sent as
|
|
56
|
+
* given so the server's invalid_working_hours error names it.
|
|
57
|
+
*/
|
|
58
|
+
export function parseWorkingDaysOption(value) {
|
|
59
|
+
const day = (token) => {
|
|
60
|
+
const lower = token.trim().toLowerCase();
|
|
61
|
+
if (/^[1-7]$/.test(lower))
|
|
62
|
+
return Number(lower);
|
|
63
|
+
return WORKING_DAY_NAMES[lower] ?? null;
|
|
64
|
+
};
|
|
65
|
+
const days = [];
|
|
66
|
+
for (const token of value.split(",").map((part) => part.trim()).filter(Boolean)) {
|
|
67
|
+
const [from, to, ...rest] = token.split("-");
|
|
68
|
+
const start = from !== undefined ? day(from) : null;
|
|
69
|
+
const end = to !== undefined ? day(to) : null;
|
|
70
|
+
if (rest.length === 0 && start !== null && end !== null && start <= end) {
|
|
71
|
+
for (let current = start; current <= end; current += 1)
|
|
72
|
+
days.push(current);
|
|
73
|
+
}
|
|
74
|
+
else if (to === undefined && start !== null) {
|
|
75
|
+
days.push(start);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
days.push(token);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return days;
|
|
82
|
+
}
|
|
83
|
+
const limitFlags = Object.fromEntries(Object.keys(LIMIT_FLAG_HELP).map((key) => [
|
|
84
|
+
`limits.${key}`,
|
|
85
|
+
{
|
|
86
|
+
...(LIMIT_FLAG_NAMES[key] ? { flag: LIMIT_FLAG_NAMES[key] } : {}),
|
|
87
|
+
valueName: "<n>",
|
|
88
|
+
help: linkedInLimitHelp(key, LIMIT_FLAG_HELP[key]),
|
|
89
|
+
},
|
|
90
|
+
]));
|
|
91
|
+
export const SENDER_LIMITS_FLAG_OVERLAY = {
|
|
92
|
+
limits: { flatten: true },
|
|
93
|
+
...limitFlags,
|
|
94
|
+
"working_hours.timezone": {
|
|
95
|
+
flag: "timezone",
|
|
96
|
+
valueName: "<tz>",
|
|
97
|
+
help: "IANA timezone for the working hours and the daily counter reset, e.g. Europe/Berlin. Defaults to the zone of the country chosen at connect.",
|
|
98
|
+
},
|
|
99
|
+
"working_hours.days": {
|
|
100
|
+
flag: "working-days",
|
|
101
|
+
valueName: "<days>",
|
|
102
|
+
parse: parseWorkingDaysOption,
|
|
103
|
+
help: "Days LinkedIn actions may run: mon-fri, mon,wed,fri, or ISO numbers 1-7 (1 = Monday). A weekday-only window leaves a visible two-day gap every week.",
|
|
104
|
+
},
|
|
105
|
+
"working_hours.start": { valueName: "<HH:MM>", help: "Local time working hours start, e.g. 09:00." },
|
|
106
|
+
"working_hours.end": { valueName: "<HH:MM>", help: "Local time working hours end (exclusive), e.g. 18:00. Must be after the start." },
|
|
107
|
+
"warmup.enabled": {
|
|
108
|
+
pair: {
|
|
109
|
+
on: "warmup-restart",
|
|
110
|
+
onHelp: "Start (or restart) the warm-up ramp now — gradually raises this account's invite + message caps to full over ~2 weeks.",
|
|
111
|
+
off: "warmup-disable",
|
|
112
|
+
offHelp: "Turn off warm-up for this account (treat it as already warm and use its full configured caps).",
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
// `--warmup-disable` already sends the disable; no separate --clear flag.
|
|
116
|
+
"warmup.start_date": {
|
|
117
|
+
valueName: "<date>",
|
|
118
|
+
noClear: true,
|
|
119
|
+
help: "Set the warm-up start date (ISO, e.g. 2026-01-31). A past date credits prior warming and advances the ramp.",
|
|
120
|
+
},
|
|
121
|
+
read_warmup: {
|
|
122
|
+
pair: {
|
|
123
|
+
on: "read-warmup-restart",
|
|
124
|
+
onHelp: "Restart the READ warm-up ramp from today, so network capture eases back in from 5 reads a day. Also clears --read-warmup-disable.",
|
|
125
|
+
off: "read-warmup-disable",
|
|
126
|
+
offHelp: "Turn off the READ warm-up ramp for this account. Network capture (`oxygen linkedin network setup`) eases a newly armed account in at 5 → 10 → 20 reads a day over two weeks; this runs it at the full read budget immediately. Separate from --warmup-disable, which governs invites and messages.",
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
warmup_preset: {
|
|
130
|
+
valueName: "<preset>",
|
|
131
|
+
help: "Warm-up ramp curve: conservative (~3 weeks), standard (~2 weeks, default), or fast (~1 week for an aged account).",
|
|
132
|
+
},
|
|
133
|
+
randomize_daily_caps: {
|
|
134
|
+
flag: "randomize-caps",
|
|
135
|
+
help: "Randomise this account's daily send caps to 80-100% of configured so daily volume varies instead of hitting the same cap every day.",
|
|
136
|
+
negateHelp: "Turn off randomised daily caps (use the exact configured caps every day).",
|
|
137
|
+
},
|
|
138
|
+
weekend_full_volume: {
|
|
139
|
+
help: "Keep full daily send caps on Saturday and Sunday (by default they run at half).",
|
|
140
|
+
negateHelp: "Run daily send caps at half on Saturday and Sunday again (the default).",
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* The limits PATCH body for the flags given. A warm-up start date wins over
|
|
145
|
+
* --warmup-restart/--warmup-disable (the route reads it first anyway), so the
|
|
146
|
+
* body carries only the one it will act on. Throws when no flag was given.
|
|
147
|
+
*/
|
|
148
|
+
export function buildSenderLimitsBody(options) {
|
|
149
|
+
const body = readContractFlags(options, SENDER_LIMITS_PATCH_CONTRACT, SENDER_LIMITS_FLAG_OVERLAY);
|
|
150
|
+
const warmup = body.warmup;
|
|
151
|
+
if (warmup && warmup.start_date !== undefined)
|
|
152
|
+
delete warmup.enabled;
|
|
153
|
+
if (Object.keys(body).length === 0) {
|
|
154
|
+
throw new OxygenError("invalid_request", "Pass at least one limit flag (e.g. --invites-per-day), a working-hours flag (--timezone, --working-days, --working-hours-start, --working-hours-end), a warm-up flag (--warmup-restart / --warmup-disable / --warmup-start-date), --read-warmup-disable / --read-warmup-restart, --warmup-preset, --randomize-caps, or --weekend-full-volume.", { exitCode: 1 });
|
|
155
|
+
}
|
|
156
|
+
return body;
|
|
157
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { type RowsFileFormat } from "@oxygen/shared/file-import";
|
|
2
|
+
/**
|
|
3
|
+
* Large-file imports (decision F.8, 2026-09-27: up to 5 GiB on the largest plans).
|
|
4
|
+
*
|
|
5
|
+
* The ordinary `tables import --file` path reads the whole file into memory and
|
|
6
|
+
* parses every row into an object before it uploads anything. That is fine at a
|
|
7
|
+
* few MiB and impossible at a few GiB: a V8 string tops out near 1 GiB and the
|
|
8
|
+
* parsed rows weigh many times the file. Above this threshold a CSV or JSONL
|
|
9
|
+
* file therefore never enters memory whole. One streaming pass counts its rows,
|
|
10
|
+
* hashes it and collects its columns; the file is then PUT to object storage
|
|
11
|
+
* straight from disk, and the worker streams it again from there.
|
|
12
|
+
*
|
|
13
|
+
* JSON and XLSX cannot stream (a JSON array has no record boundary, an XLSX is a
|
|
14
|
+
* zip), so they never take this path: they are capped at
|
|
15
|
+
* MAX_BUFFERED_IMPORT_PARSE_BYTES before anything is read.
|
|
16
|
+
*/
|
|
17
|
+
export declare const STREAMED_FILE_IMPORT_MIN_BYTES: number;
|
|
18
|
+
/** Rows kept from the head of the file to infer a new table's column types from. */
|
|
19
|
+
export declare const STREAMED_IMPORT_SAMPLE_ROWS = 2000;
|
|
20
|
+
/** An upload that moves no bytes for this long is abandoned, whatever its size. */
|
|
21
|
+
export declare const STREAMED_UPLOAD_STALL_TIMEOUT_MS = 120000;
|
|
22
|
+
export declare function shouldStreamFileImport(format: RowsFileFormat, fileBytes: number): boolean;
|
|
23
|
+
export type StreamedImportScan = {
|
|
24
|
+
rowCount: number;
|
|
25
|
+
sha256: string;
|
|
26
|
+
/** Every column the file names, in first-seen order (the whole file, not the sample). */
|
|
27
|
+
labels: string[];
|
|
28
|
+
sampleRows: Record<string, unknown>[];
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Read the file once, as a stream: count its rows with the same parser the
|
|
32
|
+
* worker will use (so a malformed file fails here, before any upload), hash it,
|
|
33
|
+
* and collect its column labels. JSONL rows may each name different keys, so the
|
|
34
|
+
* labels come from every row; only the type-inference sample is bounded.
|
|
35
|
+
*/
|
|
36
|
+
export declare function scanStreamedImportFile(path: string, format: RowsFileFormat, options?: {
|
|
37
|
+
sampleRows?: number;
|
|
38
|
+
}): Promise<StreamedImportScan>;
|
|
39
|
+
/**
|
|
40
|
+
* The rows a target is prepared from: the sample, plus one empty row naming any
|
|
41
|
+
* column that first appears after it, so a created table gets every column the
|
|
42
|
+
* file has and an existing table's mapping sees every header.
|
|
43
|
+
*/
|
|
44
|
+
export declare function rowsForStreamedImportTarget(scan: StreamedImportScan): Record<string, unknown>[];
|
|
45
|
+
/**
|
|
46
|
+
* PUT a file to a presigned URL straight from disk. Content-Length is sent
|
|
47
|
+
* explicitly (S3 refuses a chunked PUT, and the presigner signed the length),
|
|
48
|
+
* and the request is abandoned only when it stops moving, never for being big:
|
|
49
|
+
* a 5 GiB upload on a slow link is legitimate, a silent socket is not.
|
|
50
|
+
*/
|
|
51
|
+
export declare function putImportFileStream(input: {
|
|
52
|
+
uploadUrl: string;
|
|
53
|
+
path: string;
|
|
54
|
+
byteLength: number;
|
|
55
|
+
contentType: string;
|
|
56
|
+
stallTimeoutMs?: number;
|
|
57
|
+
fetchImpl?: typeof fetch;
|
|
58
|
+
}): Promise<Response>;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { createReadStream } from "node:fs";
|
|
3
|
+
import { Readable } from "node:stream";
|
|
4
|
+
import { iterateRowsFileStreamBatches, } from "@oxygen/shared/file-import";
|
|
5
|
+
/**
|
|
6
|
+
* Large-file imports (decision F.8, 2026-09-27: up to 5 GiB on the largest plans).
|
|
7
|
+
*
|
|
8
|
+
* The ordinary `tables import --file` path reads the whole file into memory and
|
|
9
|
+
* parses every row into an object before it uploads anything. That is fine at a
|
|
10
|
+
* few MiB and impossible at a few GiB: a V8 string tops out near 1 GiB and the
|
|
11
|
+
* parsed rows weigh many times the file. Above this threshold a CSV or JSONL
|
|
12
|
+
* file therefore never enters memory whole. One streaming pass counts its rows,
|
|
13
|
+
* hashes it and collects its columns; the file is then PUT to object storage
|
|
14
|
+
* straight from disk, and the worker streams it again from there.
|
|
15
|
+
*
|
|
16
|
+
* JSON and XLSX cannot stream (a JSON array has no record boundary, an XLSX is a
|
|
17
|
+
* zip), so they never take this path: they are capped at
|
|
18
|
+
* MAX_BUFFERED_IMPORT_PARSE_BYTES before anything is read.
|
|
19
|
+
*/
|
|
20
|
+
export const STREAMED_FILE_IMPORT_MIN_BYTES = 64 * 1024 * 1024;
|
|
21
|
+
/** Rows kept from the head of the file to infer a new table's column types from. */
|
|
22
|
+
export const STREAMED_IMPORT_SAMPLE_ROWS = 2_000;
|
|
23
|
+
/** An upload that moves no bytes for this long is abandoned, whatever its size. */
|
|
24
|
+
export const STREAMED_UPLOAD_STALL_TIMEOUT_MS = 120_000;
|
|
25
|
+
export function shouldStreamFileImport(format, fileBytes) {
|
|
26
|
+
return (format === "csv" || format === "jsonl") && fileBytes > STREAMED_FILE_IMPORT_MIN_BYTES;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Read the file once, as a stream: count its rows with the same parser the
|
|
30
|
+
* worker will use (so a malformed file fails here, before any upload), hash it,
|
|
31
|
+
* and collect its column labels. JSONL rows may each name different keys, so the
|
|
32
|
+
* labels come from every row; only the type-inference sample is bounded.
|
|
33
|
+
*/
|
|
34
|
+
export async function scanStreamedImportFile(path, format, options = {}) {
|
|
35
|
+
const sampleLimit = options.sampleRows ?? STREAMED_IMPORT_SAMPLE_ROWS;
|
|
36
|
+
const hash = createHash("sha256");
|
|
37
|
+
async function* hashedChunks() {
|
|
38
|
+
for await (const chunk of createReadStream(path)) {
|
|
39
|
+
const bytes = chunk;
|
|
40
|
+
hash.update(bytes);
|
|
41
|
+
yield bytes;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
let rowCount = 0;
|
|
45
|
+
const labels = [];
|
|
46
|
+
const seen = new Set();
|
|
47
|
+
const sampleRows = [];
|
|
48
|
+
for await (const batch of iterateRowsFileStreamBatches(hashedChunks(), format, { batchSize: 1_000 })) {
|
|
49
|
+
rowCount += batch.length;
|
|
50
|
+
for (const row of batch) {
|
|
51
|
+
if (sampleRows.length < sampleLimit)
|
|
52
|
+
sampleRows.push(row);
|
|
53
|
+
for (const key of Object.keys(row)) {
|
|
54
|
+
if (key.startsWith("_") || seen.has(key))
|
|
55
|
+
continue;
|
|
56
|
+
seen.add(key);
|
|
57
|
+
labels.push(key);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { rowCount, sha256: hash.digest("hex"), labels, sampleRows };
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The rows a target is prepared from: the sample, plus one empty row naming any
|
|
65
|
+
* column that first appears after it, so a created table gets every column the
|
|
66
|
+
* file has and an existing table's mapping sees every header.
|
|
67
|
+
*/
|
|
68
|
+
export function rowsForStreamedImportTarget(scan) {
|
|
69
|
+
const sampled = new Set(scan.sampleRows.flatMap((row) => Object.keys(row)));
|
|
70
|
+
const late = scan.labels.filter((label) => !sampled.has(label));
|
|
71
|
+
if (late.length === 0)
|
|
72
|
+
return scan.sampleRows;
|
|
73
|
+
return [...scan.sampleRows, Object.fromEntries(late.map((label) => [label, null]))];
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* PUT a file to a presigned URL straight from disk. Content-Length is sent
|
|
77
|
+
* explicitly (S3 refuses a chunked PUT, and the presigner signed the length),
|
|
78
|
+
* and the request is abandoned only when it stops moving, never for being big:
|
|
79
|
+
* a 5 GiB upload on a slow link is legitimate, a silent socket is not.
|
|
80
|
+
*/
|
|
81
|
+
export async function putImportFileStream(input) {
|
|
82
|
+
const stallTimeoutMs = input.stallTimeoutMs ?? STREAMED_UPLOAD_STALL_TIMEOUT_MS;
|
|
83
|
+
const controller = new AbortController();
|
|
84
|
+
let timer = null;
|
|
85
|
+
const arm = () => {
|
|
86
|
+
if (timer)
|
|
87
|
+
clearTimeout(timer);
|
|
88
|
+
timer = setTimeout(() => controller.abort(new Error("The upload stopped making progress.")), stallTimeoutMs);
|
|
89
|
+
};
|
|
90
|
+
async function* progressing() {
|
|
91
|
+
for await (const chunk of createReadStream(input.path)) {
|
|
92
|
+
arm();
|
|
93
|
+
yield chunk;
|
|
94
|
+
}
|
|
95
|
+
// The last byte is out; the store now has one stall window to answer.
|
|
96
|
+
arm();
|
|
97
|
+
}
|
|
98
|
+
arm();
|
|
99
|
+
try {
|
|
100
|
+
return await (input.fetchImpl ?? fetch)(input.uploadUrl, {
|
|
101
|
+
method: "PUT",
|
|
102
|
+
headers: {
|
|
103
|
+
"content-type": input.contentType,
|
|
104
|
+
"content-length": String(input.byteLength),
|
|
105
|
+
},
|
|
106
|
+
body: Readable.toWeb(Readable.from(progressing())),
|
|
107
|
+
duplex: "half",
|
|
108
|
+
signal: controller.signal,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
if (timer)
|
|
113
|
+
clearTimeout(timer);
|
|
114
|
+
}
|
|
115
|
+
}
|
package/dist/ugc-commands.js
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
|
-
import { registerUgcCommands as registerCommands, } from "@oxygen/cli-ugc";
|
|
1
|
+
import { registerUgcCommands as registerCommands, UgcUsageError, } from "@oxygen/cli-ugc";
|
|
2
|
+
import { OxygenError } from "@oxygen/shared/cli-result";
|
|
2
3
|
import { requestOxygen } from "./http-client.js";
|
|
3
4
|
export function registerUgcCommands(program, handle) {
|
|
4
|
-
registerCommands(program, {
|
|
5
|
+
registerCommands(program, {
|
|
6
|
+
request: requestOxygen,
|
|
7
|
+
// A malformed flag is the caller's input, so it reports as invalid_request.
|
|
8
|
+
handle: (command, options, action) => handle(command, options, async () => {
|
|
9
|
+
try {
|
|
10
|
+
return await action();
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
if (error instanceof UgcUsageError)
|
|
14
|
+
throw new OxygenError("invalid_request", error.message, { exitCode: 1 });
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
}),
|
|
18
|
+
});
|
|
5
19
|
}
|
package/dist/update.d.ts
CHANGED
|
@@ -12,9 +12,17 @@ export type UpdateOptions = {
|
|
|
12
12
|
json?: boolean;
|
|
13
13
|
package?: string;
|
|
14
14
|
dryRun?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Leave the skills refresh to the newly installed binary. An automatic update
|
|
17
|
+
* runs inside the OLD process, which the API may be refusing outright — its
|
|
18
|
+
* own skills request would be rejected for the very version being replaced.
|
|
19
|
+
*/
|
|
20
|
+
skipSkillsInstall?: boolean;
|
|
15
21
|
};
|
|
16
22
|
type UpdateResult = {
|
|
17
23
|
current_version: string;
|
|
24
|
+
/** Read back from the installed package after npm finished; null when unknown. */
|
|
25
|
+
installed_version?: string | null;
|
|
18
26
|
package: string;
|
|
19
27
|
command: string;
|
|
20
28
|
dry_run: boolean;
|
|
@@ -24,10 +32,31 @@ type UpdateResult = {
|
|
|
24
32
|
export declare function updateCli(options: UpdateOptions, runtime?: {
|
|
25
33
|
env?: NodeJS.ProcessEnv;
|
|
26
34
|
platform?: NodeJS.Platform;
|
|
35
|
+
argv?: readonly string[];
|
|
27
36
|
modulePath?: string;
|
|
28
37
|
spawnSync?: SpawnSyncLike;
|
|
29
38
|
execFileSync?: ExecFileSyncLike;
|
|
30
39
|
} & SkillsSourceRuntime): Promise<UpdateResult>;
|
|
40
|
+
export type CliInstall =
|
|
41
|
+
/** `npm install -g`: npm can replace it in place with `--prefix`. */
|
|
42
|
+
{
|
|
43
|
+
kind: "global";
|
|
44
|
+
prefix: string;
|
|
45
|
+
packageDir: string;
|
|
46
|
+
}
|
|
47
|
+
/** A project's own node_modules (npx, `npm install` in a directory, a pinned capsule). */
|
|
48
|
+
| {
|
|
49
|
+
kind: "local";
|
|
50
|
+
projectDir: string;
|
|
51
|
+
packageDir: string;
|
|
52
|
+
}
|
|
53
|
+
/** Running from a source checkout. */
|
|
54
|
+
| {
|
|
55
|
+
kind: "source";
|
|
56
|
+
};
|
|
57
|
+
export declare function detectCliInstall(modulePath: string): CliInstall;
|
|
31
58
|
export declare function detectCliInstallPrefixFromPath(modulePath: string): string | null;
|
|
59
|
+
/** The version npm actually left on disk for the running package. */
|
|
60
|
+
export declare function readInstalledCliVersion(packageDir: string): string | null;
|
|
32
61
|
export declare function resolveNpmExecutables(platform?: NodeJS.Platform): string[];
|
|
33
62
|
export {};
|
package/dist/update.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
2
4
|
import { fileURLToPath } from "node:url";
|
|
3
5
|
import { OXYGEN_VERSION, OxygenError } from "@oxygen/shared";
|
|
4
6
|
import { runAutomaticSkillsInstall, skippedAutomaticSkillsInstall, } from "./skills.js";
|
|
7
|
+
import { resolveCliBinaryName } from "./runtime.js";
|
|
5
8
|
import { readOption } from "./util.js";
|
|
6
9
|
import { quoteWindowsCmdArg } from "./windows-shim.js";
|
|
7
10
|
const DEFAULT_CLI_PACKAGE_SPEC = "@oxygen-agent/cli@latest";
|
|
@@ -11,7 +14,9 @@ export async function updateCli(options, runtime = {}) {
|
|
|
11
14
|
const platform = runtime.platform ?? process.platform;
|
|
12
15
|
const spawn = runtime.spawnSync ?? spawnSync;
|
|
13
16
|
const packageSpec = readOption(options.package) ?? DEFAULT_CLI_PACKAGE_SPEC;
|
|
14
|
-
const
|
|
17
|
+
const modulePath = runtime.modulePath ?? fileURLToPath(import.meta.url);
|
|
18
|
+
const install = detectCliInstall(modulePath);
|
|
19
|
+
const prefix = install.kind === "global" ? install.prefix : null;
|
|
15
20
|
const args = prefix
|
|
16
21
|
? ["install", "-g", "--prefix", prefix, packageSpec]
|
|
17
22
|
: ["install", "-g", packageSpec];
|
|
@@ -19,6 +24,8 @@ export async function updateCli(options, runtime = {}) {
|
|
|
19
24
|
// output). Quote args that contain shell-significant characters so a
|
|
20
25
|
// copy-paste of the line works on install prefixes with spaces.
|
|
21
26
|
const command = ["npm", ...args].map(quoteForDisplay).join(" ");
|
|
27
|
+
if (!options.dryRun)
|
|
28
|
+
assertUpdatableInstall(install, env, runtime.argv ?? process.argv);
|
|
22
29
|
if (options.dryRun) {
|
|
23
30
|
return {
|
|
24
31
|
current_version: OXYGEN_VERSION,
|
|
@@ -49,17 +56,20 @@ export async function updateCli(options, runtime = {}) {
|
|
|
49
56
|
if (!result.error && result.status === 0) {
|
|
50
57
|
return {
|
|
51
58
|
current_version: OXYGEN_VERSION,
|
|
59
|
+
installed_version: install.kind === "global" ? readInstalledCliVersion(install.packageDir) : null,
|
|
52
60
|
package: packageSpec,
|
|
53
61
|
command,
|
|
54
62
|
dry_run: false,
|
|
55
63
|
updated: true,
|
|
56
|
-
skills_install:
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
64
|
+
skills_install: options.skipSkillsInstall
|
|
65
|
+
? skippedAutomaticSkillsInstall("deferred_to_updated_cli")
|
|
66
|
+
: await runAutomaticSkillsInstall({
|
|
67
|
+
env,
|
|
68
|
+
platform,
|
|
69
|
+
...(runtime.credentials !== undefined ? { credentials: runtime.credentials } : {}),
|
|
70
|
+
...(runtime.fetch ? { fetch: runtime.fetch } : {}),
|
|
71
|
+
...(runtime.execFileSync ? { execFileSync: runtime.execFileSync } : {}),
|
|
72
|
+
}),
|
|
63
73
|
};
|
|
64
74
|
}
|
|
65
75
|
break;
|
|
@@ -77,10 +87,7 @@ export async function updateCli(options, runtime = {}) {
|
|
|
77
87
|
exitCode: 1,
|
|
78
88
|
});
|
|
79
89
|
}
|
|
80
|
-
function
|
|
81
|
-
return detectCliInstallPrefixFromPath(modulePath);
|
|
82
|
-
}
|
|
83
|
-
export function detectCliInstallPrefixFromPath(modulePath) {
|
|
90
|
+
export function detectCliInstall(modulePath) {
|
|
84
91
|
const normalizedPath = modulePath.replace(/\\/g, "/");
|
|
85
92
|
const markers = [
|
|
86
93
|
"/node_modules/@oxygen-agent/cli/dist/",
|
|
@@ -91,11 +98,50 @@ export function detectCliInstallPrefixFromPath(modulePath) {
|
|
|
91
98
|
if (markerIndex === -1)
|
|
92
99
|
continue;
|
|
93
100
|
const packageParent = normalizedPath.slice(0, markerIndex);
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
101
|
+
const packageDir = normalizedPath.slice(0, markerIndex + marker.length - "/dist/".length);
|
|
102
|
+
if (packageParent.endsWith("/lib")) {
|
|
103
|
+
return { kind: "global", prefix: packageParent.slice(0, -"/lib".length), packageDir };
|
|
104
|
+
}
|
|
105
|
+
// npm's Windows global layout has no lib/: <prefix>\node_modules.
|
|
106
|
+
if (/^[A-Za-z]:\//.test(normalizedPath))
|
|
107
|
+
return { kind: "global", prefix: packageParent, packageDir };
|
|
108
|
+
// Anywhere else, `npm install -g --prefix <dir>` writes <dir>/lib/node_modules — a
|
|
109
|
+
// different tree from the one running — and reports success while nothing changed.
|
|
110
|
+
return { kind: "local", projectDir: packageParent, packageDir };
|
|
111
|
+
}
|
|
112
|
+
return { kind: "source" };
|
|
113
|
+
}
|
|
114
|
+
export function detectCliInstallPrefixFromPath(modulePath) {
|
|
115
|
+
const install = detectCliInstall(modulePath);
|
|
116
|
+
return install.kind === "global" ? install.prefix : null;
|
|
117
|
+
}
|
|
118
|
+
/** The version npm actually left on disk for the running package. */
|
|
119
|
+
export function readInstalledCliVersion(packageDir) {
|
|
120
|
+
try {
|
|
121
|
+
const parsed = JSON.parse(readFileSync(join(packageDir, "package.json"), "utf8"));
|
|
122
|
+
return typeof parsed.version === "string" ? parsed.version : null;
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function assertUpdatableInstall(install, env, argv) {
|
|
129
|
+
if (resolveCliBinaryName(env, argv) === "oxygen-dev") {
|
|
130
|
+
throw new OxygenError("cli_update_unsupported_install", "oxygen-dev is built from the dev branch, not installed from npm. "
|
|
131
|
+
+ "Rebuild it with `npm run install-dev-cli` from the latest dev checkout.", { details: { install: "dev_build", update_command: "npm run install-dev-cli" }, exitCode: 1 });
|
|
132
|
+
}
|
|
133
|
+
if (install.kind === "local") {
|
|
134
|
+
throw new OxygenError("cli_update_unsupported_install", `This Oxygen CLI runs from a project-local install in ${install.projectDir}, which \`oxygen update\` cannot replace. `
|
|
135
|
+
+ "Run `npm install @oxygen-agent/cli@latest` in that directory, or install globally with "
|
|
136
|
+
+ "`npm install -g @oxygen-agent/cli`.", {
|
|
137
|
+
details: {
|
|
138
|
+
install: "local",
|
|
139
|
+
install_dir: install.projectDir,
|
|
140
|
+
update_command: "npm install @oxygen-agent/cli@latest",
|
|
141
|
+
},
|
|
142
|
+
exitCode: 1,
|
|
143
|
+
});
|
|
97
144
|
}
|
|
98
|
-
return null;
|
|
99
145
|
}
|
|
100
146
|
export function resolveNpmExecutables(platform = process.platform) {
|
|
101
147
|
return platform === "win32" ? ["cmd.exe"] : ["npm"];
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the plan does to one workflow's trigger, as stderr lines beside the
|
|
3
|
+
* `workflows get` / `workflows apply` envelope: a cron the scheduler fires at
|
|
4
|
+
* the plan's minimum interval instead of as written (repricing L4.2, P-47), and
|
|
5
|
+
* the deliveries a minute a webhook trigger's URL accepts (L4.4). The envelope
|
|
6
|
+
* carries both as data (`workflow.trigger.slowedToPlanMinimum`, `rate_limit`).
|
|
7
|
+
*/
|
|
8
|
+
export declare function formatWorkflowPlanLimitNotices(data: unknown): string[];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the plan does to one workflow's trigger, as stderr lines beside the
|
|
3
|
+
* `workflows get` / `workflows apply` envelope: a cron the scheduler fires at
|
|
4
|
+
* the plan's minimum interval instead of as written (repricing L4.2, P-47), and
|
|
5
|
+
* the deliveries a minute a webhook trigger's URL accepts (L4.4). The envelope
|
|
6
|
+
* carries both as data (`workflow.trigger.slowedToPlanMinimum`, `rate_limit`).
|
|
7
|
+
*/
|
|
8
|
+
export function formatWorkflowPlanLimitNotices(data) {
|
|
9
|
+
const block = asRecord(data);
|
|
10
|
+
const workflow = asRecord(block?.workflow);
|
|
11
|
+
const trigger = asRecord(workflow?.trigger);
|
|
12
|
+
if (!block || !trigger)
|
|
13
|
+
return [];
|
|
14
|
+
const slug = typeof workflow?.slug === "string" ? workflow.slug : "workflow";
|
|
15
|
+
const notices = [];
|
|
16
|
+
const minutes = asRecord(trigger.slowedToPlanMinimum)?.planMinimumIntervalMinutes;
|
|
17
|
+
if (typeof minutes === "number") {
|
|
18
|
+
notices.push(`${slug}: schedule slowed — your plan runs a schedule at most every ${minutes} minutes, so this one fires every ${minutes} minutes instead of as written. Upgrade (\`oxygen billing change\`) or change the schedule.`);
|
|
19
|
+
}
|
|
20
|
+
const perWindow = asRecord(block.rate_limit)?.requests_per_window;
|
|
21
|
+
if (trigger.type === "webhook" && typeof perWindow === "number") {
|
|
22
|
+
notices.push(`${slug}: webhook accepts up to ${perWindow.toLocaleString("en-US")} deliveries a minute on your plan; more get HTTP 429 with Retry-After (\`oxygen limits show\`).`);
|
|
23
|
+
}
|
|
24
|
+
return notices;
|
|
25
|
+
}
|
|
26
|
+
function asRecord(value) {
|
|
27
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
28
|
+
}
|