@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
package/dist/index.js
CHANGED
|
@@ -12,15 +12,18 @@ import { registerKnowledgeRepositoryCommands } from "./knowledge-repository-comm
|
|
|
12
12
|
import { registerVisualCommands } from "./visual-commands.js";
|
|
13
13
|
import { renderPrimaryProviderBoard } from "./admin-primary-providers-render.js";
|
|
14
14
|
import { registerFunctionsCommands } from "./functions-commands.js";
|
|
15
|
+
import { STREAMED_FILE_IMPORT_MIN_BYTES, putImportFileStream, rowsForStreamedImportTarget, scanStreamedImportFile, shouldStreamFileImport, } from "./streamed-file-import.js";
|
|
15
16
|
import { applyOxygenHelp, enableCommandSuggestions, unknownCommandHint, unknownOptionHint } from "./help.js";
|
|
16
17
|
import { buildCommandManifest, getCommandManifestEntry, searchCommandManifest, suggestCommandNames, } from "./command-manifest.js";
|
|
17
18
|
import { AGENCY_DIRECTORY_REGIONS, AGENCY_DIRECTORY_SERVICES, COLLAB_GATE_KINDS, COLLAB_GATE_PROSE, COLLAB_SUBJECT_KINDS, COLLAB_SUBJECT_KINDS_PROSE, COLLAB_SUBJECT_LABELS, COLLAB_SUBJECT_PROSE, GATE_KIND_SUBJECTS, describeWorkflowStatusChange, formatCellForDisplay, formatCopilotPlanDuration, formatCopilotPlanSeconds, formatPublicBudgetScopes, SUBJECT_PATH_FORMS_PROSE, formatSubjectPath, exitCodeForOxygenError, parseSubjectPath, parseSubjectRef, parseWorkflowStatusChange, chunk, isVersionGreater, isVersionLess, KNOWLEDGE_BOOTSTRAP_MAX_CREDITS, MAX_CLI_JSON_BODY_BYTES, MAX_MCP_TOOL_NAME_LENGTH, normalizeCopilotPlanStepStatus, OXYGEN_CAPABILITY_ROUTES, OXYGEN_VERSION, OxygenError, getCapabilityRouteMatch, inferUserCapabilityRoute, parseKnowledgePageMarkdown, PLAN_LIMITS, serializeCapabilityRoute, sleep, success, TAG_KINDS_PROSE, toFailure, workflowMcpToolName, } from "@oxygen/shared";
|
|
18
|
-
import { PURCHASABLE_PLAN_KEYS, resolveBasePricingPlan } from "@oxygen/shared/billing";
|
|
19
|
+
import { PURCHASABLE_PLAN_KEYS, planVolumeBonusPercent, resolveBasePricingPlan } from "@oxygen/shared/billing";
|
|
19
20
|
import { TAG_COLORS } from "@oxygen/shared/select-options";
|
|
21
|
+
import { PUBLIC_WEBHOOK_INGRESS_PER_TARGET_PER_MINUTE } from "@oxygen/shared/plan-band";
|
|
22
|
+
import { AI_COLUMN_CREDITS, AI_COLUMN_WEB_SEARCH_TYPICAL_CREDITS } from "@oxygen/shared/pricing-sheet";
|
|
20
23
|
import { LINKEDIN_SENDER_LIMIT_DEFAULTS, LINKEDIN_SENDER_LIMIT_MAXIMUMS, } from "@oxygen/shared/linkedin-sequences";
|
|
21
24
|
import { readColumnDecisionFlags } from "./column-decision-options.js";
|
|
22
25
|
import { MANAGED_DATA_SUPPLIERS } from "@oxygen/shared/data-suppliers";
|
|
23
|
-
import { inferImportColumnLabels, inferRowsFileFormat, normalizeImportColumnKey, normalizeRowsForNewTable, normalizeRowsFormat, parseRowsFileBuffer, parseXlsxWorkbookBuffer, } from "@oxygen/shared/file-import";
|
|
26
|
+
import { MAX_BUFFERED_IMPORT_PARSE_BYTES, assertImportFormatWithinBufferedLimit, inferImportColumnLabels, inferRowsFileFormat, normalizeImportColumnKey, normalizeRowsForNewTable, normalizeRowsFormat, parseRowsFileBuffer, parseXlsxWorkbookBuffer, } from "@oxygen/shared/file-import";
|
|
24
27
|
import { MAILBOX_IMPORT_FILE_MAX_BYTES as SHARED_MAILBOX_IMPORT_FILE_MAX_BYTES, MAILBOX_IMPORT_ROW_LIMIT as SHARED_MAILBOX_IMPORT_ROW_LIMIT, normalizeMailboxImportFile as normalizeSharedMailboxImportFile, normalizeMailboxImportVendor as normalizeSharedMailboxImportVendor, normalizeMailboxWorkbookRows, parseMailboxImportText, summarizeMailboxImportValidation as summarizeSharedMailboxImportValidation, } from "@oxygen/shared/mailbox-import";
|
|
25
28
|
import { assertRecipeBundleSafe, assertPortableWorkflowDefinition, assertWorkflowGraphManifest, assertWorkflowManifest, buildRecipeManifest, compileWorkflowDefinition, isAnyWorkflowManifest, isRecipeManifest, isWorkflowDefinition, isWorkflowGraphManifest, isWorkflowManifest, hashPortableWorkflowGraphManifest, WORKFLOW_GRAPH_COMPILER_VERSION, WORKFLOW_GRAPH_MANIFEST_VERSION, } from "@oxygen/workflows";
|
|
26
29
|
import { isRecipeDefinition } from "@oxygen/recipe-sdk";
|
|
@@ -34,11 +37,13 @@ import { formatAiPromptPreviewNotice, formatColumnReferenceNotices, } from "./co
|
|
|
34
37
|
import { runLocalCustomHttpColumn } from "./local-custom-http-column.js";
|
|
35
38
|
import { formatSearchAiFilterTotalsNotice } from "./search-ai-filter-notice.js";
|
|
36
39
|
import { formatInboxNeedsReplyNotice } from "./inbox-needs-reply-notice.js";
|
|
40
|
+
import { formatWorkflowPlanLimitNotices } from "./workflow-plan-limit-notices.js";
|
|
37
41
|
import { captureCurrentTranscript, collectFeedbackEnvironment, TranscriptCaptureError, } from "./transcript.js";
|
|
38
42
|
import { addSessionOutput, addSessionStatus, getSessionUsage, startSession, updateSessionStep, } from "./session.js";
|
|
39
43
|
import { doctorAgentSkills, getAgentSkill, installAgentSkills, listAgentSkills, runAutomaticSkillsInstall, searchAgentSkills, } from "./skills.js";
|
|
40
44
|
import { resolveCliBinaryName } from "./runtime.js";
|
|
41
45
|
import { updateCli } from "./update.js";
|
|
46
|
+
import { markStdinConsumed, maybeScheduleBackgroundUpdate, readAutoUpdateStatus, runBackgroundUpdate, } from "./auto-update.js";
|
|
42
47
|
import { isRecord, readClearableOption, readErrorMessage, readOption } from "./util.js";
|
|
43
48
|
const AGENT_MODEL_POLICY_HELP = "Model policy JSON: level low (Fast), medium (Balanced, default), or high (Max); credential_mode managed or organization_byok.";
|
|
44
49
|
const AGENT_TOOL_POLICY_HELP = "Tool policy JSON. Effects: read, internal_write, paid, external_write. Ask-before-changes preset: {\"mode\":\"scoped\",\"rules\":[{\"decision\":\"allow\",\"effect\":\"read\"},{\"decision\":\"ask\",\"effect\":\"internal_write\"},{\"decision\":\"ask\",\"effect\":\"paid\"},{\"decision\":\"ask\",\"effect\":\"external_write\"}]}. Full mode requires --confirm-full-access.";
|
|
@@ -267,6 +272,17 @@ function buildFindBody(capability, options) {
|
|
|
267
272
|
body.verify = true;
|
|
268
273
|
return body;
|
|
269
274
|
}
|
|
275
|
+
/** The shared search/buy filters, in the snake_case the voice numbers route reads. */
|
|
276
|
+
function voiceNumberFilters(options) {
|
|
277
|
+
const areaCode = readOption(options.areaCode);
|
|
278
|
+
const region = readOption(options.region);
|
|
279
|
+
const country = readOption(options.country);
|
|
280
|
+
return {
|
|
281
|
+
...(areaCode ? { area_code: areaCode } : {}),
|
|
282
|
+
...(region ? { in_region: region } : {}),
|
|
283
|
+
...(country ? { iso_country: country.toUpperCase() } : {}),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
270
286
|
function suppressionListParams(options) {
|
|
271
287
|
const params = new URLSearchParams();
|
|
272
288
|
const reason = readOption(options.reason);
|
|
@@ -315,7 +331,10 @@ const SAFE_IMPORT_WRITE_BATCH_SIZE = 500;
|
|
|
315
331
|
// per-request chunk) and the background threshold, so users read 500 as a
|
|
316
332
|
// per-file cap. Echo the shared row ceiling and the tiered byte ceilings instead
|
|
317
333
|
// of restating either as a chunk-size constraint.
|
|
318
|
-
|
|
334
|
+
// Webhook ingress scales with plan size (repricing 2026-09, decision L4.4).
|
|
335
|
+
const TABLE_WEBHOOK_RATE_HELP = `${PUBLIC_WEBHOOK_INGRESS_PER_TARGET_PER_MINUTE.free.toLocaleString("en-US")}–${PUBLIC_WEBHOOK_INGRESS_PER_TARGET_PER_MINUTE["1999"].toLocaleString("en-US")} requests a minute by plan size, per endpoint and sender IP; 429 returns Retry-After; \`oxygen limits show\` lists yours`;
|
|
336
|
+
const WEBHOOK_TRIGGER_RATE_HELP = `${PUBLIC_WEBHOOK_INGRESS_PER_TARGET_PER_MINUTE.free.toLocaleString("en-US")}–${PUBLIC_WEBHOOK_INGRESS_PER_TARGET_PER_MINUTE["1999"].toLocaleString("en-US")} deliveries a minute by plan size, per trigger and sender IP; 429 returns Retry-After`;
|
|
337
|
+
const IMPORT_FILE_LIMIT_HELP = `Per-file row limit: your plan's rows-per-Table limit, since one file can fill an empty Table (\`oxygen limits show\` lists it for every plan size). File-size limit: ${formatImportFileSizeLimit("free")} on free, ${formatImportFileSizeLimit("starter")} on $49-$99, ${formatImportFileSizeLimit("pro")} on $199-$499, ${formatImportFileSizeLimit("scale")} on $999 and up. JSON and XLSX are read whole, so they stop at ${formatImportByteSize(MAX_BUFFERED_IMPORT_PARSE_BYTES)}; save a larger file as CSV or JSONL. Files over ${formatImportByteSize(STREAMED_FILE_IMPORT_MIN_BYTES)} upload straight from disk and always load in the background.`;
|
|
319
338
|
const TABLE_ACTION_RUN_WAIT_DEFAULT_TIMEOUT_SECONDS = 600;
|
|
320
339
|
const TABLE_ACTION_RUN_WAIT_DEFAULT_INTERVAL_SECONDS = 5;
|
|
321
340
|
// Single-row paid runs are auto-backgrounded server-side; the CLI waits this
|
|
@@ -739,6 +758,12 @@ function writeMailboxPoolOverview(data, view) {
|
|
|
739
758
|
// explains why no stalled clause appeared.
|
|
740
759
|
if (!warmup)
|
|
741
760
|
out("(this server does not report warm-up pool counts)");
|
|
761
|
+
// Above the rows, like NEEDS ACTION: mailboxes that stopped sending because
|
|
762
|
+
// their monthly credits were not covered, and the date they disconnect.
|
|
763
|
+
if (data.reservations_paused?.message) {
|
|
764
|
+
out();
|
|
765
|
+
writeWrappedPoolProse(out, "PAUSED ", data.reservations_paused.message, " ");
|
|
766
|
+
}
|
|
742
767
|
out();
|
|
743
768
|
if (rows.length === 0) {
|
|
744
769
|
out(view.filtered ? "No mailbox matches that filter." : "No mailboxes in this workspace yet.");
|
|
@@ -993,6 +1018,14 @@ function writeBillingNotices(command, data) {
|
|
|
993
1018
|
const payload = asPayloadRecord(data);
|
|
994
1019
|
if (!payload)
|
|
995
1020
|
return;
|
|
1021
|
+
// Any list that carries connected accounts (senders, WhatsApp numbers, mailboxes,
|
|
1022
|
+
// billing commitments) says so when some stopped sending because their monthly
|
|
1023
|
+
// credit reservation could not be renewed (decision 2.8). Before the envelope, for
|
|
1024
|
+
// the same reason as the renewal notices below.
|
|
1025
|
+
const paused = asPayloadRecord(payload.reservations_paused);
|
|
1026
|
+
if (paused && typeof paused.message === "string" && paused.message.trim()) {
|
|
1027
|
+
process.stderr.write(`! ${paused.message}\n`);
|
|
1028
|
+
}
|
|
996
1029
|
if (command === "billing balance") {
|
|
997
1030
|
for (const warning of readWarningMessages(payload))
|
|
998
1031
|
process.stderr.write(`! ${warning}\n`);
|
|
@@ -1070,6 +1103,11 @@ function writeCreditsReceipt(data) {
|
|
|
1070
1103
|
}
|
|
1071
1104
|
}
|
|
1072
1105
|
}
|
|
1106
|
+
function writeWorkflowPlanLimitNotices(data) {
|
|
1107
|
+
for (const notice of formatWorkflowPlanLimitNotices(data)) {
|
|
1108
|
+
process.stderr.write(`${notice}\n`);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1073
1111
|
// A disabled workflow is a customer's automation at zero, and `status:
|
|
1074
1112
|
// "disabled"` alone never said who did that or why (OXY-4124). Mirror the
|
|
1075
1113
|
// recorded transition as one stderr line per disabled workflow, so `workflows
|
|
@@ -1213,11 +1251,11 @@ function writeKnowledgeIndexCapNotice(data) {
|
|
|
1213
1251
|
process.stderr.write(`note: listing ${shown}, most recently updated first; counts cover the whole wiki — raise --limit (max 1000) or pass --all.\n`);
|
|
1214
1252
|
}
|
|
1215
1253
|
// Arming a cron commits recurring spend, so `workflows enable` mirrors its
|
|
1216
|
-
// `automation` block as stderr lines: what the schedule burns per day
|
|
1217
|
-
// share of the monthly allowance that is, and when it runs
|
|
1218
|
-
// (this workflow's own billed runs) is the honest number and
|
|
1219
|
-
// manifest floor is a MINIMUM and is labelled as one, because
|
|
1220
|
-
//
|
|
1254
|
+
// `automation` block as stderr lines: what the schedule burns per day in steps
|
|
1255
|
+
// and credits, what share of the monthly allowance that is, and when it runs
|
|
1256
|
+
// out. Observed burn (this workflow's own billed runs) is the honest number and
|
|
1257
|
+
// is preferred; the manifest floor is a MINIMUM and is labelled as one, because
|
|
1258
|
+
// recipe checkpoints and retries add steps. Writing rows is free.
|
|
1221
1259
|
function writeAutomationProjection(data) {
|
|
1222
1260
|
if (!data || typeof data !== "object" || Array.isArray(data))
|
|
1223
1261
|
return;
|
|
@@ -1251,8 +1289,13 @@ function writeAutomationProjection(data) {
|
|
|
1251
1289
|
: ` (${sharePct < 1 ? "<1" : Math.round(sharePct)}% of the ${included.toLocaleString("en-US")}/mo allowance)`;
|
|
1252
1290
|
const basis = fromHistory
|
|
1253
1291
|
? `based on ${typeof observed?.runSample === "number" ? observed.runSample : 0} past billed runs`
|
|
1254
|
-
: "MINIMUM —
|
|
1255
|
-
|
|
1292
|
+
: "MINIMUM — recipe checkpoints and retries can add steps";
|
|
1293
|
+
// Older servers send no creditsPerStep; say nothing about credits rather than guess.
|
|
1294
|
+
const creditsPerStep = typeof block.creditsPerStep === "number" ? block.creditsPerStep : null;
|
|
1295
|
+
const credits = creditsPerStep === null
|
|
1296
|
+
? ""
|
|
1297
|
+
: ` (~${(Math.round(projected * creditsPerStep * 100) / 3_000).toLocaleString("en-US", { maximumFractionDigits: 2 })} credits/day at ${creditsPerStep} per step; writing rows is free)`;
|
|
1298
|
+
process.stderr.write(`schedule: ${runsPer30Days.toLocaleString("en-US")} runs/30d, ~${perDay.toLocaleString("en-US")} workflow steps/day${credits}${share}\n`);
|
|
1256
1299
|
process.stderr.write(` ${basis}\n`);
|
|
1257
1300
|
const exhaustion = observed?.projectedExhaustionAt;
|
|
1258
1301
|
if (typeof exhaustion === "string") {
|
|
@@ -1486,12 +1529,20 @@ function writeMaxCreditsHint(error) {
|
|
|
1486
1529
|
// customer's own words, says what still works without upgrading, and gives
|
|
1487
1530
|
// the URL. A raw `upgrade_required` code with a JSON blob is the worst
|
|
1488
1531
|
// possible version of the single most important refusal in the product.
|
|
1532
|
+
// The BYOK run gate keeps its own code but carries the same upgrade fields,
|
|
1533
|
+
// including once a free workspace's seven-day key grace has ended. Without
|
|
1534
|
+
// an upgrade_url (an older server) it has no hint, as before.
|
|
1535
|
+
case "byok_requires_paid_plan":
|
|
1489
1536
|
case "upgrade_required": {
|
|
1537
|
+
if (error.code === "byok_requires_paid_plan" && !readDetailsString(error.details, "upgrade_url"))
|
|
1538
|
+
return;
|
|
1490
1539
|
const label = readDetailsString(error.details, "capability_label")
|
|
1491
1540
|
?? "This capability";
|
|
1492
1541
|
const upgradeUrl = readDetailsString(error.details, "upgrade_url");
|
|
1493
1542
|
const nextStep = readDetailsString(error.details, "next_step");
|
|
1494
|
-
|
|
1543
|
+
// BYOK names the smallest plan that includes it ("the $99 plan or above").
|
|
1544
|
+
const requirement = readDetailsString(error.details, "minimum_plan_label") ?? "a paid plan";
|
|
1545
|
+
process.stderr.write(`hint: ${label} needs ${requirement}.\n`);
|
|
1495
1546
|
if (nextStep)
|
|
1496
1547
|
process.stderr.write(`hint: ${nextStep}\n`);
|
|
1497
1548
|
if (upgradeUrl)
|
|
@@ -1502,6 +1553,27 @@ function writeMaxCreditsHint(error) {
|
|
|
1502
1553
|
process.stderr.write("hint: verify every affected external destination in error.details, then re-run with --approved-effect-unknown only if another execution is safe\n");
|
|
1503
1554
|
return;
|
|
1504
1555
|
}
|
|
1556
|
+
// A connect refused because the balance cannot carry the account's first
|
|
1557
|
+
// monthly credit reservation. Only that shape carries reservation_credits;
|
|
1558
|
+
// every other insufficient_credits keeps its own message.
|
|
1559
|
+
case "insufficient_credits": {
|
|
1560
|
+
const reservation = readDetailsNumber(error.details, "reservation_credits");
|
|
1561
|
+
if (reservation === null)
|
|
1562
|
+
return;
|
|
1563
|
+
const nextStep = readDetailsString(error.details, "next_step");
|
|
1564
|
+
const topupUrl = readDetailsString(error.details, "topup_url");
|
|
1565
|
+
process.stderr.write(`hint: this connect reserves ${reservation} credits a month; add credits, then connect again\n`);
|
|
1566
|
+
if (nextStep)
|
|
1567
|
+
process.stderr.write(`hint: ${nextStep}\n`);
|
|
1568
|
+
if (topupUrl)
|
|
1569
|
+
process.stderr.write(`hint: top up at ${topupUrl}\n`);
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
case "seats_retired": {
|
|
1573
|
+
const nextStep = readDetailsString(error.details, "next_step");
|
|
1574
|
+
process.stderr.write(`hint: sending seats are retired; connected accounts reserve credits instead${nextStep ? ` — see \`${nextStep}\`` : ""}\n`);
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1505
1577
|
case "max_credits_required": {
|
|
1506
1578
|
const recommended = readDetailsNumber(error.details, "recommended_max_credits");
|
|
1507
1579
|
if (recommended === null)
|
|
@@ -1532,6 +1604,14 @@ function writeMaxCreditsHint(error) {
|
|
|
1532
1604
|
process.stderr.write(`hint: ${requestUrl}\n`);
|
|
1533
1605
|
return;
|
|
1534
1606
|
}
|
|
1607
|
+
// An Agent run or trigger asked for more than the default Agent run
|
|
1608
|
+
// ceiling: the one extra flag is the approval, so name it with the numbers.
|
|
1609
|
+
if (readDetailsString(error.details, "approval_kind") === "agent_ceiling_above_default") {
|
|
1610
|
+
const requested = readDetailsNumber(error.details, "requested_max_credits");
|
|
1611
|
+
const defaultMax = readDetailsNumber(error.details, "default_max_credits");
|
|
1612
|
+
process.stderr.write(`hint: ${requested ?? "this"} credits is above your default Agent run ceiling of ${defaultMax ?? "your plan"}; re-run with --approve-above-default to approve it, or lower --max-credits\n`);
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1535
1615
|
if (error.message.startsWith("Public replies require --approved")) {
|
|
1536
1616
|
process.stderr.write("hint: inspect the exact preview, then re-run with its --content-hash <sha256> and --approved\n");
|
|
1537
1617
|
return;
|
|
@@ -3275,7 +3355,7 @@ function isUuid(value) {
|
|
|
3275
3355
|
function requireDomainArg(positional, option) {
|
|
3276
3356
|
const domain = readOption(positional) ?? readOption(option);
|
|
3277
3357
|
if (!domain) {
|
|
3278
|
-
throw new
|
|
3358
|
+
throw new OxygenError("invalid_request", "A domain is required \u2014 pass it as an argument or with --domain. `oxygen managed-inboxes list` shows your managed domains.", { exitCode: 2, details: { next_step: "oxygen managed-inboxes list" } });
|
|
3279
3359
|
}
|
|
3280
3360
|
return domain;
|
|
3281
3361
|
}
|
|
@@ -3535,10 +3615,21 @@ export function createProgram() {
|
|
|
3535
3615
|
});
|
|
3536
3616
|
program
|
|
3537
3617
|
.command("update")
|
|
3538
|
-
.description("Update the Oxygen CLI from npm
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3618
|
+
.description("Update the Oxygen CLI from npm, then refresh the Oxygen agent skills in the global skill folders of "
|
|
3619
|
+
+ "Codex, Claude Code and Cursor (OXYGEN_SKIP_SKILLS=1 skips that). The CLI also keeps itself current: "
|
|
3620
|
+
+ "once a day it installs a newer release in the background, and when the API says it is too old it "
|
|
3621
|
+
+ "updates and re-runs your command. Automatic updates need a global npm install (npm install -g "
|
|
3622
|
+
+ "@oxygen-agent/cli); they are off for npx and project-local installs, under CI, for development builds, "
|
|
3623
|
+
+ "and after --disable-auto or OXYGEN_NO_AUTO_UPDATE=1.")
|
|
3624
|
+
.option("--status", "Show the installed and newest published version and whether automatic updates are on.")
|
|
3625
|
+
.option("--enable-auto", "Turn automatic updates on for this computer.")
|
|
3626
|
+
.option("--disable-auto", "Turn automatic updates off for this computer, e.g. on a build server that must stay pinned.")
|
|
3627
|
+
.option("--package <npm_spec>", "Install this npm spec instead of the latest, e.g. @oxygen-agent/cli@1.1010.721. To pin a build server, "
|
|
3628
|
+
+ "install the version you want and turn automatic updates off.")
|
|
3629
|
+
.option("--dry-run", "Print the update command without running it; with --enable-auto or --disable-auto, show the result without saving it.")
|
|
3630
|
+
.option("--json", "Print a JSON envelope.")
|
|
3631
|
+
.addOption(new Option("--background", "Run the daily automatic update check.").hideHelp())
|
|
3632
|
+
.addOption(new Option("--refresh-skills", "Refresh product skills after an automatic update.").hideHelp())
|
|
3542
3633
|
.action(async (options) => {
|
|
3543
3634
|
await handleUpdateAction(options);
|
|
3544
3635
|
});
|
|
@@ -3714,7 +3805,9 @@ export function createProgram() {
|
|
|
3714
3805
|
}));
|
|
3715
3806
|
program
|
|
3716
3807
|
.command("status")
|
|
3717
|
-
.description("Compare the local Oxygen CLI version against the active profile's deployed Oxygen API."
|
|
3808
|
+
.description("Compare the local Oxygen CLI version against the active profile's deployed Oxygen API. "
|
|
3809
|
+
+ "`compatible` is the field that matters: false means commands are refused until the CLI updates. "
|
|
3810
|
+
+ "`in_sync` and `skew` only report version distance; a CLI ahead of the API is normal.")
|
|
3718
3811
|
.option("--json", "Print a JSON envelope.")
|
|
3719
3812
|
.action(async (options) => {
|
|
3720
3813
|
await handleAsyncAction("status", options, async () => {
|
|
@@ -3784,7 +3877,7 @@ export function createProgram() {
|
|
|
3784
3877
|
await handleAsyncAction("orgs billing-owners", options, () => requestOxygen("/api/cli/orgs/billing-owners"));
|
|
3785
3878
|
}))
|
|
3786
3879
|
.addCommand(new Command("billing-link")
|
|
3787
|
-
.description("Cover a workspace with a plan you already pay for in another organization (one plan, many workspaces) instead of buying a second subscription. Omit --owner when exactly one of your organizations can pay and Oxygen will use it; `orgs billing-owners` lists them all. You must be an admin of BOTH organizations, the workspace being linked must not hold a live subscription of its own, and an org-bound key (oxy_live_…) can only link the workspace it is bound to. Credit billing moves, workspace data access does not; plan gates and sending seats then resolve through the owner, so the linked workspace connects senders against the owner's seats and seats are bought there. `plan_tier` in the response is the plan the workspace now runs on; `free` means the owner holds no active paid plan, so connecting senders stays blocked.")
|
|
3880
|
+
.description("Cover a workspace with a plan you already pay for in another organization (one plan, many workspaces) instead of buying a second subscription. Omit --owner when exactly one of your organizations can pay and Oxygen will use it; `orgs billing-owners` lists them all. You must be an admin of BOTH organizations, the workspace being linked must not hold a live subscription of its own, and an org-bound key (oxy_live_…) can only link the workspace it is bound to. Credit billing moves, workspace data access does not; plan gates and sending seats then resolve through the owner, so the linked workspace connects senders against the owner's seats and seats are bought there. `plan_tier` in the response is the plan the workspace now runs on; `free` means the owner holds no active paid plan, so connecting senders stays blocked. From the owner, `billing usage --group-by workspace` reports the credits each linked workspace spent.")
|
|
3788
3881
|
.option("--owner <organization>", "Billing owner organization id, Clerk org id, or slug. Optional — when omitted, Oxygen uses your one eligible organization, and refuses to pick if there is more than one.")
|
|
3789
3882
|
.option("--organization <organization>", "Workspace organization to link. Defaults to the active organization.")
|
|
3790
3883
|
.option("--organization-id <id>", "Alias for --organization.")
|
|
@@ -4603,7 +4696,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
4603
4696
|
.option("--sender <sender_account_id>", "LinkedIn sender account id. Required for LinkedIn before the worker can publish.")
|
|
4604
4697
|
.option("--provider-connection <connection_id>", "Oxygen integration connection id for Composio-backed providers. Find it with `integrations list` (each integration's connection); create one with `integrations connect <provider>` (an OAuth redirect URL, or Settings > Connections on the web).")
|
|
4605
4698
|
.option("--title <title>", "Internal title for the queue.")
|
|
4606
|
-
.option("--text <text>", "Post text.
|
|
4699
|
+
.option("--text <text>", "Post text. On LinkedIn, tag a person with @<public-identifier> and a company page with @<handle> from its linkedin.com/company/<handle> URL.")
|
|
4607
4700
|
.option("--text-file <path>", "Read post text from a local file.")
|
|
4608
4701
|
.option("--content-json <json>", "Structured content. Use media_asset_ids for Oxygen uploads. LinkedIn attachments require [{content:<base64>,content_type:<MIME>,filename:<name>}]; content.mentions is rejected, so put verified @<public-identifier> values in --text. X options have their own flags (--thread-post, --poll-option, --reply-settings, --alt-text, --photo-tag); raw X API keys can still ride in composio.arguments.")
|
|
4609
4702
|
.option("--composio-action <slug>", "Override the Composio action slug for this scheduled post.")
|
|
@@ -4629,7 +4722,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
4629
4722
|
}));
|
|
4630
4723
|
}))
|
|
4631
4724
|
.addCommand(new Command("get")
|
|
4632
|
-
.description("Get one scheduled post with resolved media previews, its LinkedIn video cover, and attempt history. For an X post, publish_plan lists every post that will be sent, in order, with media, alt text, poll and reply settings (free). Open web_url to preview the post as it will look on LinkedIn or X.")
|
|
4725
|
+
.description("Get one scheduled post with resolved media previews, its LinkedIn video cover, and attempt history. For an X post, publish_plan lists every post that will be sent, in order, with media, alt text, poll and reply settings (free); for a LinkedIn post that tags a company page, it shows the text sent, each company tag as its page id. Open web_url to preview the post as it will look on LinkedIn or X.")
|
|
4633
4726
|
.argument("<post_id>", "Scheduled post id.")
|
|
4634
4727
|
.option("--json", "Print a JSON envelope.")
|
|
4635
4728
|
.action(async (postId, options) => {
|
|
@@ -4643,7 +4736,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
4643
4736
|
.option("--sender <sender_account_id>", "LinkedIn sender account id.")
|
|
4644
4737
|
.option("--provider-connection <connection_id>", "Oxygen integration connection id for Composio-backed providers.")
|
|
4645
4738
|
.option("--title <title>", "Internal title for the queue.")
|
|
4646
|
-
.option("--text <text>", "Post text.
|
|
4739
|
+
.option("--text <text>", "Post text. On LinkedIn, tag a person with @<public-identifier> and a company page with @<handle> from its linkedin.com/company/<handle> URL.")
|
|
4647
4740
|
.option("--text-file <path>", "Read post text from a local file.")
|
|
4648
4741
|
.option("--content-json <json>", "Structured content. Use media_asset_ids for Oxygen uploads. LinkedIn attachments require [{content:<base64>,content_type:<MIME>,filename:<name>}]; content.mentions is rejected, so put verified @<public-identifier> values in --text. This replaces the existing content object; use --content-merge-json or the X flags to change single keys.")
|
|
4649
4742
|
.option("--composio-action <slug>", "Override the Composio action slug for this scheduled post.")
|
|
@@ -5460,7 +5553,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
5460
5553
|
.description("Create or repair standard CRM object-backed tables. Defaults to dry-run.")
|
|
5461
5554
|
.option("--objects <objects>", "Comma-separated standard CRM objects to set up. Defaults to companies,people,deals.")
|
|
5462
5555
|
.option("--project <project>", "Project id or slug for created CRM tables.")
|
|
5463
|
-
.option("--with-enrichment", "Also arm the standing cheap-enrichment auto-run on objects that already existed (fresh-created tables arm it by default). New records then auto-enrich within the per-batch credit cap, whether they arrive by import, insert, upsert, or `crm assert`. Pick which enrichments run with `oxygen crm enrichment`.")
|
|
5556
|
+
.option("--with-enrichment", "Also arm the standing cheap-enrichment auto-run on objects that already existed (fresh-created tables arm it by default). New records then auto-enrich within the per-batch credit cap, whether they arrive by import, insert, upsert, or `crm assert`. If your plan runs fewer columns per record than the defaults, the columns left out are listed under autoRunColumnsOverLimit. Pick which enrichments run with `oxygen crm enrichment`.")
|
|
5464
5557
|
.option("--dry-run", "Preview CRM setup without creating or repairing tables.")
|
|
5465
5558
|
.option("--live", "Apply CRM setup changes. Default is dry-run.")
|
|
5466
5559
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -6289,8 +6382,8 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6289
6382
|
.addCommand(new Command("list")
|
|
6290
6383
|
.description("List the raw GTM signal-event feed newest-first (website_visit / profile_view / post_reaction / new_follower), each resolved onto the person it attached to. Read-only.")
|
|
6291
6384
|
.option("--types <types>", "Comma-separated signal types to include. Defaults to all four.")
|
|
6292
|
-
.option("--since <days>", "Look-back window in days. Defaults to 7,
|
|
6293
|
-
.option("--limit <limit>", "Maximum events to return. Defaults to 25,
|
|
6385
|
+
.option("--since <days>", "Look-back window in days. Defaults to 7; your plan's maximum (30 on free, up to 365) is in `oxygen limits show`.")
|
|
6386
|
+
.option("--limit <limit>", "Maximum events to return. Defaults to 25; your plan's maximum (100 on free, up to 1,000) is in `oxygen limits show`.")
|
|
6294
6387
|
.option("--json", "Print a JSON envelope.")
|
|
6295
6388
|
.action(async (options) => {
|
|
6296
6389
|
await handleAsyncAction("signals list", options, () => requestOxygen(buildSignalsListPath(options)));
|
|
@@ -7476,7 +7569,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7476
7569
|
return requestOxygen(`/api/cli/tables/webhooks?${params.toString()}`);
|
|
7477
7570
|
})))
|
|
7478
7571
|
.addCommand(new Command("create")
|
|
7479
|
-
.description(
|
|
7572
|
+
.description(`Create a direct webhook endpoint that writes inbound JSON into a table (${TABLE_WEBHOOK_RATE_HELP}).`)
|
|
7480
7573
|
.argument("<table>", "Table id or slug.")
|
|
7481
7574
|
.option("--name <name>", "Display name for the webhook.")
|
|
7482
7575
|
.option("--mode <mode>", "insert or upsert. Defaults to upsert when --upsert-key is set, otherwise insert.")
|
|
@@ -7561,7 +7654,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7561
7654
|
.description("List direct table webhook deliveries and auto-run enqueue status.")
|
|
7562
7655
|
.argument("[endpoint_id]", "Optional webhook endpoint id, such as tw_...")
|
|
7563
7656
|
.option("--table <table>", "Filter by table id or slug.")
|
|
7564
|
-
.option("--status <status>", "Filter by received, completed, failed, or rejected (rejected = arrived while the feed was paused and was turned away; no rows written).")
|
|
7657
|
+
.option("--status <status>", "Filter by received, completed, failed, or rejected (rejected = arrived while the feed was paused and was turned away; no rows written). A delivery refused with HTTP 429 for exceeding the plan's rate is not recorded here: the sender redelivers it.")
|
|
7565
7658
|
.option("--auto-run-status <status>", "Filter by not_configured, queued, skipped, or failed_to_enqueue.")
|
|
7566
7659
|
.option("--limit <n>", "Maximum deliveries to return. Defaults to 50.")
|
|
7567
7660
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -7904,7 +7997,7 @@ Examples:
|
|
|
7904
7997
|
.description("List the shared table delivery ledger: pull-feed sync cycles and inbound webhook deliveries land in the same place. Filter by --table (the identifier `feeds list` gives you) or by a webhook endpoint id. Read-only.")
|
|
7905
7998
|
.argument("[endpoint_id]", "Optional webhook endpoint id, such as tw_...")
|
|
7906
7999
|
.option("--table <table>", "Table id or slug whose ledger to read.")
|
|
7907
|
-
.option("--status <status>", "Filter by received, completed, failed, or rejected (rejected = arrived while the feed was paused and was turned away; no rows written).")
|
|
8000
|
+
.option("--status <status>", "Filter by received, completed, failed, or rejected (rejected = arrived while the feed was paused and was turned away; no rows written). A delivery refused with HTTP 429 for exceeding the plan's rate is not recorded here: the sender redelivers it.")
|
|
7908
8001
|
.option("--limit <n>", "Maximum deliveries to return. Defaults to 50.")
|
|
7909
8002
|
.option("--json", "Print a JSON envelope.")
|
|
7910
8003
|
.action((endpointId, options) => handleAsyncAction("feeds deliveries", options, () => requestOxygen(tableWebhookDeliveriesPath({
|
|
@@ -9289,21 +9382,23 @@ Examples:
|
|
|
9289
9382
|
.option("--key <key>", "Optional stable column key. Defaults to a normalized label.")
|
|
9290
9383
|
.option("--data-type <type>", "Column data type: text, numeric, boolean, jsonb, or timestamptz.")
|
|
9291
9384
|
.option("--kind <kind>", "Column kind: manual, research (the Web Research Agent), ai, formula, enrichment, tool, bind, or lookup. Defaults to manual. Use research for anything you would look up on the web (an AI column cannot search; `columns convert --to agent` turns one into an agent); one research column answers several fields at once when its prompt names them as sections — one row, one read, one charge — so never add a second column for the same page. Enrichment columns always hold a jsonb cell, and --data-type is set for you \u2014 use --capability to seed a ready-to-run one. `lookup` reads a value out of another table; to LINK two tables row-to-row use `oxygen tables relate` instead \u2014 relation columns are two-sided and cannot be added here.")
|
|
9292
|
-
|
|
9293
|
-
|
|
9294
|
-
.option("--
|
|
9385
|
+
// Research flags sit directly under --kind: agents read help with head/tail,
|
|
9386
|
+
// and the depth choice (and its price) must land on the first screen.
|
|
9387
|
+
.option("--research-depth <depth>", RESEARCH_DEPTH_OPTION_HELP)
|
|
9388
|
+
.option("--research-url <column_key>", "Research columns: read the ONE page whose URL is in this column for each row instead of searching the web — a pricing page, a job posting, an event page, a 10-K. The page's content is the only evidence, the answer cites it as its source, and the row costs the fetch plus the model (see --dry-run). A URL ending in .pdf is read by the PDF-capable fetch lane first.")
|
|
9295
9389
|
.option("--research-query <template>", "Research columns: the per-row web search query, templated like the prompt (e.g. \"{{company_name}} pricing page\"). Omit to derive the query from the row's values and the prompt.")
|
|
9296
9390
|
.option("--research-domains <csv>", "Research columns: comma-separated domains to search within, e.g. techcrunch.com,sec.gov. Omit to search the whole web.")
|
|
9297
9391
|
.option("--research-exclude-domains <csv>", "Research columns: comma-separated domains to exclude from results.")
|
|
9298
9392
|
.option("--research-mode <mode>", "Research columns: how tightly the answer is bound to the sources. Default: summarize and combine what they say. strict: answer only from what a source states verbatim (for figures and identifiers). estimate: reason to a figure from the evidence (for revenue or headcount).")
|
|
9299
9393
|
.option("--research-results <n>", "Research columns: how many search results to ground each row on (1-25, default 5). More evidence costs no extra search, but a larger prompt.")
|
|
9300
9394
|
.option("--research-engine <engine>", "Research columns: pin the search provider (serper or exa), or with --research-url the page-fetch provider (firecrawl or exa). Defaults to serper for search and firecrawl for fetch, falling back automatically. Most users should not set this.")
|
|
9301
|
-
.option("--
|
|
9302
|
-
.option("--
|
|
9395
|
+
.option("--semantic-type <type>", "Optional semantic type such as company_domain.")
|
|
9396
|
+
.option("--definition-json <json>", "Optional JSON object with column definition metadata. Required for --kind formula as a flat object: {\"expression\":\"trim(company_name)\"}. Check the expression with `formulas validate` first.")
|
|
9397
|
+
.option("--prompt <text-or-file>", "AI or research column prompt, or a path to a prompt file — a value that resolves to a readable file is read as one, matching --prompt everywhere else in this CLI. On its own it sets kind=ai; pair it with --kind research to make it a Web Research Agent that searches the web per row. If the prompt names its output sections — a line reading 'Return the following sections:' followed by 'Score: ...', 'Reasoning: ...' — the column answers in exactly that shape and each section becomes a referenceable sub-column; otherwise it answers in plain text. Use --no-structured-output to keep it plain text either way. Reference other columns inline as {{column_key}} — no --input-mapping needed; unknown keys are rejected here instead of failing per row. Merges into --definition-json (the escape hatch for everything else); a `prompt` in both is an error.")
|
|
9303
9398
|
.option("--prompt-key <key>", "Add a column template from `oxygen columns catalog` (e.g. pricing_plan_summary_v1 reads a pricing-page URL column and returns plan count, price range and enterprise plan; company_founders_v1 reads a domain column and returns the founders with sources; person_skill_set_v1 reads the person_enrich payload column with --input profile=person_enrich). Materializes the template's prompt, output schema and grounding and sets its kind (ai, research, formula, or search — a managed web search whose cell is a page URL). Bind each declared input with --input <name>=<column_key>.")
|
|
9304
9399
|
.option("--input-mapping <json>", "JSON alternative to --input for --prompt-key: an object mapping template input names to columns, e.g. '{\"url\":{\"column\":\"pricing_url\"}}' (a bare column-key string or {\"literal\": ...} also works). Copy templates (email_draft_v1, subject_line_variants_v1, ...) reject mappings built only from manual name/title/company columns — ground them on a research or enrichment column, or write a freeform prompt with --prompt instead.")
|
|
9305
|
-
.option("--model <id>", "
|
|
9306
|
-
.option("--reasoning-level <level>",
|
|
9400
|
+
.option("--model <openrouter-model-id>", "BYOK only: the model to run on your own OpenRouter key. Managed columns pick a tier with --reasoning-level.")
|
|
9401
|
+
.option("--reasoning-level <level>", REASONING_LEVEL_OPTION_HELP)
|
|
9307
9402
|
.option("--run-condition <formula>", "Formula expression gating whether the AI column runs per row. A formula over bare column keys (eu_israel = true), not {{tokens}}.")
|
|
9308
9403
|
.option("--run-condition-columns <csv>", "Comma-separated column keys referenced by --run-condition.")
|
|
9309
9404
|
.option("--no-structured-output", "Keep an AI column answering in plain text even when its prompt names output sections. Use this for copy — an email body is one answer, not a set of fields.")
|
|
@@ -9584,7 +9679,7 @@ Examples:
|
|
|
9584
9679
|
.option("--max-concurrency <n>", "Maximum concurrent row items for a background run (1-250). Defaults to 250 for AI columns and 50 otherwise (160 for Firecrawl scrape columns).")
|
|
9585
9680
|
.option("--local", "Run a custom HTTP column in this CLI process, where {env:...} secrets read your own machine's environment. Without it the column runs in the background on Oxygen and the same reference resolves from this workspace's registered custom integration.")
|
|
9586
9681
|
.option("--local-concurrency <n>", "Maximum concurrent custom HTTP requests for --local. Defaults to 3.")
|
|
9587
|
-
.option("--dry-run", "Preview
|
|
9682
|
+
.option("--dry-run", "Preview the model tier, credit estimate, run-condition posture, and — for an AI column — the prompt rendered with one real row's values, without spending any credits.")
|
|
9588
9683
|
.option("--json", "Print a JSON envelope.")
|
|
9589
9684
|
.action(async (table, column, options) => {
|
|
9590
9685
|
const limit = readPositiveInt(options.limit);
|
|
@@ -9749,8 +9844,8 @@ Examples:
|
|
|
9749
9844
|
.option("--data-type <type>", "New output data type for a lookup column (text, numeric, boolean, jsonb, timestamptz). Discards the cached cells; other column kinds use `oxygen columns retype`.")
|
|
9750
9845
|
.option("--prompt <text-or-file>", "Replacement AI column prompt, or a path to a prompt file. Reference other columns inline as {{column_key}}; unknown keys are rejected here instead of failing per row. Merges into --definition-json; a `prompt` in both is an error.")
|
|
9751
9846
|
.option("--input-mapping <json>", "Replace the column's named inputs wholesale. Row columns belong in the prompt as {{column_key}}; use this for the inputs a prompt cannot name on its own — literals and workspace context ({\"icp\":{\"type\":\"context_profile\",\"path\":\"icp\"}}), which the prompt then reads as {{icp}}. Pass {} to clear it; --definition-json can only add mapping keys, never remove one.")
|
|
9752
|
-
.option("--model <id>", "
|
|
9753
|
-
.option("--reasoning-level <level>",
|
|
9847
|
+
.option("--model <openrouter-model-id>", "BYOK only: the model to run on your own OpenRouter key. Managed columns pick a tier with --reasoning-level.")
|
|
9848
|
+
.option("--reasoning-level <level>", REASONING_LEVEL_OPTION_HELP)
|
|
9754
9849
|
.option("--run-condition <formula>", "Formula expression gating whether the AI column runs per row. A formula over bare column keys (eu_israel = true), not {{tokens}}.")
|
|
9755
9850
|
.option("--run-condition-columns <csv>", "Comma-separated column keys referenced by --run-condition.")
|
|
9756
9851
|
.option("--no-structured-output", "Keep an AI column answering in plain text even when its prompt names output sections. Use this for copy — an email body is one answer, not a set of fields.")
|
|
@@ -10326,7 +10421,7 @@ Examples:
|
|
|
10326
10421
|
});
|
|
10327
10422
|
}))
|
|
10328
10423
|
.addCommand(new Command("wait")
|
|
10329
|
-
.description("
|
|
10424
|
+
.description("Wait for a table ingestion run (read-only, no credits). Retries transient network failures, including deploy disconnects, at the polling interval within the same timeout.")
|
|
10330
10425
|
.argument("<run_id>", "Table ingestion run UUID.")
|
|
10331
10426
|
.option("--timeout-seconds <n>", "Maximum time to wait. Defaults to 600.")
|
|
10332
10427
|
.option("--interval-seconds <n>", "Polling interval. Defaults to 5.")
|
|
@@ -10829,7 +10924,7 @@ Examples:
|
|
|
10829
10924
|
.command("limits")
|
|
10830
10925
|
.description("Plan-tier limits, spend-safety defaults, per-channel sending limits, and storage capacity posture.")
|
|
10831
10926
|
.addCommand(new Command("show")
|
|
10832
|
-
.description("Show your plan band and its limits beside every plan size's (API rate, storage, spend defaults), the per-channel sending limits every plan shares (emails per mailbox, WhatsApp, LinkedIn, calls), plus Tables capacity usage. Read-only; spends no credits. Recovery: https://oxygen-agent.com/docs/safety/billing.")
|
|
10927
|
+
.description("Compare every plan size: price, monthly credits, volume bonus and price per 100 credits (`plan_bands`). Show your plan band and its limits beside every plan size's (API rate, webhook ingress, storage, import file size, columns per auto-run, signals feed, spend defaults incl. Agent and Copilot caps, fastest workflow schedule), the per-channel sending limits every plan shares (emails per mailbox, WhatsApp, LinkedIn, calls) and its workflow Code-step and run-attempt limits, plus Tables capacity usage naming the largest Table. Each plan size's storage says whether it is in force, scheduled to change on a date, or awaiting its rows-per-Table capacity test, with the values it moves to. `spend_safety_defaults.applies_to` says what each spend default bounds: an unattended trigger delivery, a table auto-run batch, one Agent run, or the attended Copilot. `web_url` opens the same view in the web app (Settings → Plan limits), including any scheduled change. Read-only; spends no credits. Recovery: https://oxygen-agent.com/docs/safety/billing.")
|
|
10833
10928
|
.option("--json", "Print a JSON envelope.")
|
|
10834
10929
|
.action(async (options) => {
|
|
10835
10930
|
await handleAsyncAction("limits show", options, () => requestOxygen("/api/cli/limits"));
|
|
@@ -10878,46 +10973,84 @@ Examples:
|
|
|
10878
10973
|
})));
|
|
10879
10974
|
program
|
|
10880
10975
|
.command("billing")
|
|
10881
|
-
.description("Plan and managed credit commands. Spend splits into FLEXIBLE (ad-hoc: enrichment, AI, automation — drawn from your free-to-spend balance) and FIXED recurring per-resource monthly commitments blocked out of it; see `billing commitments`. THREE CLOCKS, deliberately different: the CREDIT CYCLE that `billing allowance` reports against (your plan's monthly grant window); each resource's own COMMITMENT RENEWAL, anchored to the day you connected it, so `billing commitments --json` next_due_at rarely matches the cycle end; and your SUBSCRIPTION PERIOD in `billing balance` (annual plans span many credit cycles). A number from one clock will not reconcile against another. Failed-payment grace, suspension, and recovery: https://oxygen-agent.com/docs/safety/billing.")
|
|
10976
|
+
.description("Plan and managed credit commands. Spend splits into FLEXIBLE (ad-hoc: enrichment, AI, automation — drawn from your free-to-spend balance) and FIXED recurring per-resource monthly commitments blocked out of it; see `billing commitments`. THREE CLOCKS, deliberately different: the CREDIT CYCLE that `billing allowance` reports against (your plan's monthly grant window); each resource's own COMMITMENT RENEWAL, anchored to the day you connected it, so `billing commitments --json` next_due_at rarely matches the cycle end; and your SUBSCRIPTION PERIOD in `billing balance` (annual plans span many credit cycles). A number from one clock will not reconcile against another. On a plan that pays for several workspaces, `billing usage --group-by workspace` totals what each one spent. To price a month of outreach before building it (leads, enrichment, sequence steps, senders), run `billing estimate`. Failed-payment grace, suspension, and recovery: https://oxygen-agent.com/docs/safety/billing.")
|
|
10882
10977
|
.addCommand(new Command("change")
|
|
10883
|
-
.description("
|
|
10978
|
+
.description("Start a first plan or preview an upgrade, downgrade or billing-interval change, and return the Stripe link to approve it (`approval_url`), with the target plan's monthly credits (`target_plan`). On a workspace with no plan (`mode: first_subscription`) the link is the same Stripe Checkout the web Subscribe button opens, and credits arrive once Stripe confirms the first payment; subscribing needs an organization admin. Nothing is charged or changed until you confirm in Stripe. `interval` is how the plan bills after the change; `recurring_amount` is what each interval bills and `recurring_monthly` its per-month equivalent (a yearly $99 plan: $990, $82.50). Without --interval the plan keeps its current interval. Moving to yearly applies on confirmation with proration; moving to monthly waits for the period end. To compare every plan's credits, volume bonus and price per 100 credits without creating a link, run `limits show`.")
|
|
10884
10979
|
.requiredOption("--to <plan>",
|
|
10885
10980
|
// Derived from the catalog, never retyped: this help text is the only
|
|
10886
10981
|
// place a customer's agent learns which plans exist, and it named
|
|
10887
10982
|
// three retired ones for as long as it was a literal. The price sits
|
|
10888
10983
|
// beside each key because nothing else on the CLI prints a
|
|
10889
|
-
// plan-to-price table (blind baseline, 2026-09-22).
|
|
10984
|
+
// plan-to-price table (blind baseline, 2026-09-22). The credits sit
|
|
10985
|
+
// there too, bonus included, because the $499+ rungs are no longer
|
|
10986
|
+
// price x 100 (repricing 2026-09, decision 1.3).
|
|
10890
10987
|
`Target plan: ${PURCHASABLE_PLAN_KEYS.map((key) => {
|
|
10891
|
-
const
|
|
10892
|
-
|
|
10988
|
+
const plan = resolveBasePricingPlan(key);
|
|
10989
|
+
const cents = plan?.monthlyPriceCents;
|
|
10990
|
+
if (typeof cents !== "number")
|
|
10991
|
+
return key;
|
|
10992
|
+
const credits = plan?.monthlyCredits;
|
|
10993
|
+
const bonus = planVolumeBonusPercent(key);
|
|
10994
|
+
const creditText = typeof credits === "number"
|
|
10995
|
+
? `, ${credits.toLocaleString("en-US")} credits${bonus > 0 ? ` incl. a ${bonus}% volume bonus` : ""}`
|
|
10996
|
+
: "";
|
|
10997
|
+
return `${key} ($${cents / 100}/mo${creditText})`;
|
|
10893
10998
|
}).join(", ")}.`)
|
|
10999
|
+
.option("--interval <interval>", "Bill monthly (`month`) or yearly (`year`: ten months' price for twelve, credits still granted monthly). Omit to keep the current interval (monthly for a first plan). Monthly to yearly adds no extra month: credits already granted for the rest of the month count toward the first yearly month. Yearly is refused with `annual_billing_unavailable` until it is offered.")
|
|
10894
11000
|
.option("--json", "Print a JSON envelope.")
|
|
10895
11001
|
.action(async (options) => {
|
|
10896
11002
|
await handleAsyncAction("billing change", options, () => requestOxygen("/api/cli/billing/change", {
|
|
10897
11003
|
method: "POST",
|
|
10898
|
-
|
|
11004
|
+
// Sent only when asked: the server keeps the subscriber's interval otherwise.
|
|
11005
|
+
body: { tier: options.to, ...(options.interval ? { interval: options.interval } : {}) },
|
|
10899
11006
|
}));
|
|
10900
11007
|
}))
|
|
10901
11008
|
.addCommand(new Command("balance")
|
|
10902
|
-
.description("Show the current plan, subscription entitlement, and managed credit balance: available and reserved credits, FIXED credits committed to recurring per-resource charges, and the FLEXIBLE free-to-spend remainder. `next_renewal` answers \"will I be charged?\": read `will_charge` first — true only when this workspace's own Stripe plan is going to charge its card, in which case `charge_at` and the plan's monthly list price in money say when and roughly how much (the exact amount, with any promotion code or tax, is on the invoice); `period_ends_at` is the trial or period end either way, `cancellation_scheduled` says whether a cancellation is already set, and `stop_command` names the one command that changes it (`billing cancel`, undone by `billing resume`) or is null when nothing here can stop it (staff-invoiced, billed through another workspace, not active). `renewals_past_due` lists infrastructure renewals (managed-inbox domains, warm-ups) OXYGEN could NOT charge this month, what they cost, and the exact top-up that clears them — nothing is cancelled and billing retries automatically after a top-up. `warnings` also flags a recurring_shortfall when your connected infrastructure costs more per month than the plan grants. After failed-payment grace expires, mutations stop while read/export and billing recovery remain available. Recovery: https://oxygen-agent.com/billing. Policy: https://oxygen-agent.com/docs/safety/billing.")
|
|
11009
|
+
.description("Show the current plan, subscription entitlement, and managed credit balance: available and reserved credits, FIXED credits committed to recurring per-resource charges, and the FLEXIBLE free-to-spend remainder. `next_renewal` answers \"will I be charged?\": read `will_charge` first — true only when this workspace's own Stripe plan is going to charge its card, in which case `charge_at` and the plan's monthly list price in money say when and roughly how much (the exact amount, with any promotion code or tax, is on the invoice); `period_ends_at` is the trial or period end either way, `cancellation_scheduled` says whether a cancellation is already set, and `stop_command` names the one command that changes it (`billing cancel`, undone by `billing resume`) or is null when nothing here can stop it (staff-invoiced, billed through another workspace, not active). `billing_interval` says whether the plan bills monthly or yearly; a yearly plan still grants its credits monthly, and `next_credit_grant_at` says when the next monthly grant arrives. `renewals_past_due` lists infrastructure renewals (managed-inbox domains, warm-ups) OXYGEN could NOT charge this month, what they cost, and the exact top-up that clears them — nothing is cancelled and billing retries automatically after a top-up. `warnings` also flags a recurring_shortfall when your connected infrastructure costs more per month than the plan grants. `partner_bonus` says whether this workspace earns the agency partner bonus (+15% credits a month for a published directory partner on $99 or above), how many credits, and why not when it does not; the bonus arrives as its own ledger line (`billing usage --category partner_bonus`), never inside plan.monthly_credits. A workspace linked to another organization (`orgs billing-link`) reads its billing at that owner: the balance is the owner's pool, `shared_billing` is true and `billing_owner_organization_id` names the owner. After failed-payment grace expires, mutations stop while read/export and billing recovery remain available. Recovery: https://oxygen-agent.com/billing. Policy: https://oxygen-agent.com/docs/safety/billing.")
|
|
10903
11010
|
.option("--json", "Print a JSON envelope.")
|
|
10904
11011
|
.action(async (options) => {
|
|
10905
11012
|
await handleAsyncAction("billing balance", options, () => requestOxygen("/api/cli/billing/balance"));
|
|
10906
11013
|
}))
|
|
10907
11014
|
.addCommand(new Command("commitments")
|
|
10908
|
-
.description("List the fixed monthly credit commitments blocked at your subscription renewal — per connected sending mailbox, OXYGEN-sold mailbox, warm-up, deliverability, connected LinkedIn, WhatsApp and X account, and rented phone number — with unit price, quantity, and next-due date. This is the FORWARD run-rate: what your currently-connected resources will cost at their next renewal. It is NOT this cycle's charges — compare `billing allowance` fixed_spent_credits for that, and expect the two to differ. Each resource renews on its OWN anchor (the day it was connected), so next_due_at is per-resource and rarely lines up with the credit cycle. Read-only, 0 Oxygen credits.")
|
|
11015
|
+
.description("List the fixed monthly credit commitments blocked at your subscription renewal — per connected sending mailbox, OXYGEN-sold mailbox, warm-up, deliverability, connected LinkedIn, WhatsApp and X account, and rented phone number — with unit price, quantity, and next-due date. When your balance cannot cover an account's renewal, sending from that account pauses at once and it disconnects 7 days later (a phone number is released after 30): paused_quantity, past_due_since, disconnects_at and topup_url say which kind, since when and by when to top up; a top-up resumes it within minutes. This is the FORWARD run-rate: what your currently-connected resources will cost at their next renewal. It is NOT this cycle's charges — compare `billing allowance` fixed_spent_credits for that, and expect the two to differ. Each resource renews on its OWN anchor (the day it was connected), so next_due_at is per-resource and rarely lines up with the credit cycle. Read-only, 0 Oxygen credits.")
|
|
10909
11016
|
.option("--json", "Print a JSON envelope.")
|
|
10910
11017
|
.action(async (options) => {
|
|
10911
11018
|
await handleAsyncAction("billing commitments", options, () => requestOxygen("/api/cli/billing/commitments"));
|
|
11019
|
+
}))
|
|
11020
|
+
.addCommand(new Command("estimate")
|
|
11021
|
+
.description("Estimate what a month of outreach costs before you build it: leads × enrichment × sequence steps × senders. Returns one line per cost (enrichment credits expected and worst case, each mailbox and LinkedIn account at its monthly price, sends at 0 credits), `totals`, `plan_fit` (does your plan cover it, and the top-up if not), `recommended_plan` (the cheapest plan plus top-ups that covers it) and `limits` (emails per mailbox, LinkedIn invites and messages per account; `limits_hit` names each one the scenario exceeds and `senders_needed` how many senders it takes). Steps are counted per channel: a 3-step sequence of two emails and one LinkedIn invite is `--email-steps 2 --linkedin-steps 1`, and `summary` restates what was priced. `monthly_usd` is all in: plan, top-ups and dollar seats. Prices are the ones in force; `sender_billing` says whether senders are paid as dollar seats or monthly credit reservations. Example: `oxygen billing estimate --leads 500 --enrich work_email --email-steps 2 --linkedin-steps 1 --mailboxes 1 --linkedin-accounts 1 --json`. `web_url` opens the same estimate in Settings → Plan limits. Read-only, 0 Oxygen credits.")
|
|
11022
|
+
.option("--leads <n>", "Leads you work through a month.")
|
|
11023
|
+
.option("--enrich <id>", "An enrichment run on every lead, by id from `oxygen enrichment catalog --json` (work_email, mobile_phone, person_enrich, capability:verify_email …). Repeat for several.", collectRepeatable, [])
|
|
11024
|
+
.option("--email-steps <n>", "Email steps per lead (only the email ones; LinkedIn steps go in --linkedin-steps).")
|
|
11025
|
+
.option("--linkedin-steps <n>", "LinkedIn steps per lead; the first is a connection invite, later ones messages.")
|
|
11026
|
+
.option("--mailboxes <n>", "Mailboxes you connect yourself (Google, Microsoft, SMTP).")
|
|
11027
|
+
.option("--managed-mailboxes <n>", "Mailboxes you buy from OXYGEN.")
|
|
11028
|
+
.option("--linkedin-accounts <n>", "LinkedIn accounts you connect.")
|
|
11029
|
+
.option("--plan <plan>", `Check against this plan instead of your own: free, ${PURCHASABLE_PLAN_KEYS.join(", ")}.`)
|
|
11030
|
+
.option("--json", "Print a JSON envelope.")
|
|
11031
|
+
.action(async (options) => {
|
|
11032
|
+
await handleAsyncAction("billing estimate", options, () => requestOxygen("/api/cli/billing/estimate", {
|
|
11033
|
+
method: "POST",
|
|
11034
|
+
body: {
|
|
11035
|
+
leads_per_month: readPositiveNumberOrZero(options.leads) ?? 0,
|
|
11036
|
+
enrichments: options.enrich.flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean),
|
|
11037
|
+
email_steps: readPositiveNumberOrZero(options.emailSteps) ?? 0,
|
|
11038
|
+
linkedin_steps: readPositiveNumberOrZero(options.linkedinSteps) ?? 0,
|
|
11039
|
+
mailboxes: readPositiveNumberOrZero(options.mailboxes) ?? 0,
|
|
11040
|
+
managed_mailboxes: readPositiveNumberOrZero(options.managedMailboxes) ?? 0,
|
|
11041
|
+
linkedin_accounts: readPositiveNumberOrZero(options.linkedinAccounts) ?? 0,
|
|
11042
|
+
...(readOption(options.plan) ? { plan: readOption(options.plan) } : {}),
|
|
11043
|
+
},
|
|
11044
|
+
}));
|
|
10912
11045
|
}))
|
|
10913
11046
|
.addCommand(new Command("seats")
|
|
10914
|
-
.description("Show sending capacity per channel: how many seats are purchased, how many are grandfathered, how many senders are connected, and how many are left. Sending seats are what let you CONNECT a sender — a LinkedIn account, WhatsApp number, phone number, or email mailbox — and they are billed in dollars on their own subscription, separate from your plan and separate from credits. You do not need a plan to buy seats. Seats belong to the billing owner: a workspace linked to another organization's plan with `orgs billing-link` connects senders against that organization's seats and buys them there. A grandfathered value of null means unlimited for that channel. Email mailbox allocation is read from this workspace's own database; when it cannot be read, the email seat's allocated/available/can_connect read null and `email_sender_allocation` is `tenant_unavailable`. Read-only, 0 Oxygen credits.")
|
|
11047
|
+
.description("Show sending capacity per channel: how many seats are purchased, how many are grandfathered, how many senders are connected, and how many are left. Sending seats are what let you CONNECT a sender — a LinkedIn account, WhatsApp number, phone number, or email mailbox — and they are billed in dollars on their own subscription, separate from your plan and separate from credits. You do not need a plan to buy seats. Seats belong to the billing owner: a workspace linked to another organization's plan with `orgs billing-link` connects senders against that organization's seats and buys them there. A grandfathered value of null means unlimited for that channel. Email mailbox allocation is read from this workspace's own database; when it cannot be read, the email seat's allocated/available/can_connect read null and `email_sender_allocation` is `tenant_unavailable`. When seats are retired because connected accounts reserve credits instead, the envelope says `status: seats_retired` with a `next_step`; seats already held keep covering their accounts until they end at renewal (`held_seats_end_at`), and from then on each of those accounts reserves credits instead, never both (`after_seats_end` on each channel gives the monthly reservation). Read-only, 0 Oxygen credits.")
|
|
10915
11048
|
.option("--json", "Print a JSON envelope.")
|
|
10916
11049
|
.action(async (options) => {
|
|
10917
11050
|
await handleAsyncAction("billing seats", options, () => requestOxygen("/api/cli/billing/seats"));
|
|
10918
11051
|
})
|
|
10919
11052
|
.addCommand(new Command("set")
|
|
10920
|
-
.description("Set how many sending seats of one channel this workspace holds. The quantity is ABSOLUTE, not a delta: `--quantity 3` means you end up with three, so a retried command cannot buy extra. Increasing charges the card pro rata immediately; decreasing credits you pro rata. Reducing below the number of senders you have connected is REFUSED and names how many to disconnect first, because an orphaned sender is one you keep paying a provider for while Oxygen has stopped honouring it. An email_sender reduction is refused with `seat_allocation_unavailable` (503) while the workspace's mailbox count cannot be read; retry it in a moment. Charges real money.")
|
|
11053
|
+
.description("Set how many sending seats of one channel this workspace holds. The quantity is ABSOLUTE, not a delta: `--quantity 3` means you end up with three, so a retried command cannot buy extra. Increasing charges the card pro rata immediately; decreasing credits you pro rata. Reducing below the number of senders you have connected is REFUSED and names how many to disconnect first, because an orphaned sender is one you keep paying a provider for while Oxygen has stopped honouring it. An email_sender reduction is refused with `seat_allocation_unavailable` (503) while the workspace's mailbox count cannot be read; retry it in a moment. Once seats are retired, any increase is refused with `seats_retired` and lowering held seats still works. Charges real money.")
|
|
10921
11054
|
.requiredOption("--seat-key <kind>", "linkedin, whatsapp, phone_number, or email_sender.")
|
|
10922
11055
|
.requiredOption("--quantity <n>", "Total seats to hold for this channel, 0 or more.")
|
|
10923
11056
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -10931,7 +11064,7 @@ Examples:
|
|
|
10931
11064
|
}));
|
|
10932
11065
|
})))
|
|
10933
11066
|
.addCommand(new Command("invoices")
|
|
10934
|
-
.description("List every invoice Stripe has issued for this workspace, newest first, each with a direct PDF link. Covers ALL charges, not just the plan — sending seats, managed inboxes, and credit top-ups appear here too, which is why this works on the free tier: a workspace with no plan can still be a paying customer. A workspace
|
|
11067
|
+
.description("List every invoice Stripe has issued for this workspace, newest first, each with a direct PDF link. Covers ALL charges, not just the plan — sending seats, managed inboxes, and credit top-ups appear here too, which is why this works on the free tier: a workspace with no plan can still be a paying customer. A workspace linked to another organization (`orgs billing-link`) reads its billing at that owner, so it sees an empty list and shared_billing=true; switch to the billing owner to read its invoices. pdf_url and hosted_url are Stripe's own links. Read-only, 0 Oxygen credits.")
|
|
10935
11068
|
.option("--json", "Print a JSON envelope.")
|
|
10936
11069
|
.action(async (options) => {
|
|
10937
11070
|
await handleAsyncAction("billing invoices", options, () => requestOxygen("/api/cli/billing/invoices"));
|
|
@@ -10943,24 +11076,30 @@ Examples:
|
|
|
10943
11076
|
await handleAsyncAction("billing allowance", options, () => requestOxygen("/api/cli/billing/allowance"));
|
|
10944
11077
|
}))
|
|
10945
11078
|
.addCommand(new Command("usage")
|
|
10946
|
-
.description("Show credit ledger events (each tagged with its FIXED/FLEXIBLE spend_class),
|
|
10947
|
-
.option("--meter <meter>", "credits, automation_actions, or provider_seats (rolling-30d peak connected accounts
|
|
11079
|
+
.description("Show credit ledger events (each tagged with its FIXED/FLEXIBLE spend_class), workflow step usage (automation_actions: credits per step, credits billed; writing rows is free), calls with your own API keys (byok_calls: the platform fee per call and the fee credits this month; your provider bills the call itself), or connected LinkedIn and WhatsApp accounts (provider_seats: the rolling-30d peak and its implied Unipile cost; what each account is charged is its credit reservation in `billing commitments`). On a plan that pays for several workspaces (an agency covering client workspaces), `--group-by workspace` totals the credits each workspace spent (`by_workspace`) and `--workspace <id|slug>` narrows the ledger to one of them; a linked client workspace sees only its own spend.")
|
|
11080
|
+
.option("--meter <meter>", "credits, automation_actions, byok_calls, or provider_seats (rolling-30d peak connected accounts and implied Unipile COGS; the per-account charge is listed by `billing commitments`). Defaults to credits.")
|
|
10948
11081
|
.option("--days <n>", "Lookback window in days. Defaults to 30.")
|
|
10949
11082
|
.option("--from <iso>", "Only include ledger events at or after this ISO timestamp.")
|
|
10950
11083
|
.option("--to <iso>", "Only include ledger events at or before this ISO timestamp.")
|
|
10951
11084
|
.option("--limit <n>", "Maximum events to return. Defaults to 50.")
|
|
10952
11085
|
.option("--cursor <cursor>", "Opaque cursor from a previous usage response.")
|
|
10953
|
-
.option("--type <type>", "Filter by transaction type, such as reserve, capture, release, grant, or byok_usage.")
|
|
10954
|
-
.option("--category <category>", "Filter by transaction category, such as managed_enrichment, managed_ai, byok, subscription, or admin.")
|
|
11086
|
+
.option("--type <type>", "Filter by transaction type, such as reserve, capture, release, grant, or byok_usage (alias byok).")
|
|
11087
|
+
.option("--category <category>", "Filter by transaction category, such as managed_enrichment, managed_ai, byok, subscription, partner_bonus (the agency partner bonus), or admin.")
|
|
10955
11088
|
.option("--provider <provider>", "Filter by provider id. With --meter provider_seats, selects the seat provider: linkedin (default) or whatsapp.")
|
|
10956
11089
|
.option("--source <source_id>", "Filter by source_id/provider operation.")
|
|
10957
11090
|
.option("--source-id <source_id>", "Alias for --source.")
|
|
10958
11091
|
.option("--run-id <run_id>", "Filter by run id recorded in ledger metadata. A table run's reserve and release events carry it, but its settled charges may not yet: read what a table run billed from `table-runs get <run>` (creditsUsed), or per row from `table-runs items`.")
|
|
10959
11092
|
.option("--nonzero", "Only include events that changed available or reserved credits.")
|
|
11093
|
+
.option("--workspace <id|slug>", "Only one workspace's spend on a plan that pays for several (credits meter).")
|
|
11094
|
+
.option("--group-by <key>", "workspace: credits spent per workspace on a pooled plan, in by_workspace, for the same filters (credits meter).")
|
|
10960
11095
|
.option("--json", "Print a JSON envelope.")
|
|
10961
11096
|
.action(async (options) => {
|
|
10962
11097
|
await handleAsyncAction("billing usage", options, () => {
|
|
10963
11098
|
const params = buildBillingLedgerParams(options);
|
|
11099
|
+
if (readOption(options.workspace))
|
|
11100
|
+
params.set("workspace", readOption(options.workspace));
|
|
11101
|
+
if (readOption(options.groupBy))
|
|
11102
|
+
params.set("group_by", readOption(options.groupBy));
|
|
10964
11103
|
const suffix = params.toString() ? `?${params.toString()}` : "";
|
|
10965
11104
|
return requestOxygen(`/api/cli/billing/usage${suffix}`);
|
|
10966
11105
|
});
|
|
@@ -10972,8 +11111,8 @@ Examples:
|
|
|
10972
11111
|
.option("--to <iso>", "Only include ledger events at or before this ISO timestamp.")
|
|
10973
11112
|
.option("--limit <n>", "Maximum grouped rows to return. Defaults to 100.")
|
|
10974
11113
|
.option("--group-by <keys>", "Comma-separated grouping keys (day, type, category, provider, source, tool, model, run, workspace). Defaults to provider,source.")
|
|
10975
|
-
.option("--type <type>", "Filter by transaction type, such as reserve, capture, release, grant, or byok_usage.")
|
|
10976
|
-
.option("--category <category>", "Filter by transaction category, such as managed_enrichment, managed_ai, byok, subscription, or admin.")
|
|
11114
|
+
.option("--type <type>", "Filter by transaction type, such as reserve, capture, release, grant, or byok_usage (alias byok).")
|
|
11115
|
+
.option("--category <category>", "Filter by transaction category, such as managed_enrichment, managed_ai, byok, subscription, partner_bonus (the agency partner bonus), or admin.")
|
|
10977
11116
|
.option("--provider <provider>", "Filter by provider id.")
|
|
10978
11117
|
.option("--source <source_id>", "Filter by source_id/provider operation.")
|
|
10979
11118
|
.option("--source-id <source_id>", "Alias for --source.")
|
|
@@ -11038,6 +11177,7 @@ Examples:
|
|
|
11038
11177
|
.option("--rollover <n>", "Max balance credits roll over to. Defaults to the monthly credits.")
|
|
11039
11178
|
.option("--base-tier <tier>", "Base pricing tier to inherit feature flags from. Defaults to growth_250.")
|
|
11040
11179
|
.option("--byok", "Enable bring-your-own-key integrations for the plan.")
|
|
11180
|
+
.option("--agent-run-default-credits <n>", "The contract's own default Agent run ceiling in credits (at most 25,000). Omitted keeps the plan's default; re-running set-plan without it clears it.")
|
|
11041
11181
|
.option("--note <text>", "Internal note stored on the contract.")
|
|
11042
11182
|
.option("--json", "Print a JSON envelope.")
|
|
11043
11183
|
.action(async (options) => {
|
|
@@ -11056,6 +11196,9 @@ Examples:
|
|
|
11056
11196
|
: {}),
|
|
11057
11197
|
...(readOption(options.baseTier) ? { base_tier: readOption(options.baseTier) } : {}),
|
|
11058
11198
|
...(options.byok ? { byok: true } : {}),
|
|
11199
|
+
...(readOption(options.agentRunDefaultCredits)
|
|
11200
|
+
? { agent_run_default_credits: readPositiveNumber(options.agentRunDefaultCredits) }
|
|
11201
|
+
: {}),
|
|
11059
11202
|
...(readOption(options.note) ? { note: readOption(options.note) } : {}),
|
|
11060
11203
|
},
|
|
11061
11204
|
}));
|
|
@@ -11079,8 +11222,8 @@ Examples:
|
|
|
11079
11222
|
}));
|
|
11080
11223
|
}))
|
|
11081
11224
|
.addCommand(new Command("topup")
|
|
11082
|
-
.description("Buy a custom amount of on-demand credits
|
|
11083
|
-
.argument("[pack]", "
|
|
11225
|
+
.description("Buy a custom amount of on-demand credits. Run without an amount to see the current price per 100 credits (`usd_per_100_credits`), the allowed range and the packs; checkout happens in Stripe, the price is shown before you pay, and purchased credits never expire. Plan credits cost less. `usd_per_1k_credits` is a deprecated name for the same per-100 price, not a price per 1,000 credits.")
|
|
11226
|
+
.argument("[pack]", "A pack id from the list shown when you run without an amount.")
|
|
11084
11227
|
.option("--credits <n>", "Credits to buy (800-100,000 in 100-credit increments).")
|
|
11085
11228
|
.option("--json", "Print a JSON envelope.")
|
|
11086
11229
|
.action(async (pack, options) => {
|
|
@@ -11099,7 +11242,7 @@ Examples:
|
|
|
11099
11242
|
.command("budget")
|
|
11100
11243
|
.description("Standing credit caps (org/table daily/monthly hard-blocks) beyond per-run max_credits.")
|
|
11101
11244
|
.addCommand(new Command("list")
|
|
11102
|
-
.description("List the organization's standing budget policies.")
|
|
11245
|
+
.description("List the organization's standing budget policies, plus the plan defaults that apply without one: the daily spend guard (until you set your own org daily budget) and the per-run ceilings of one Agent run, trigger delivery or auto-run batch. Every spend default: `oxygen limits show`.")
|
|
11103
11246
|
.option("--scope <scope>", `Filter by scope: ${formatPublicBudgetScopes()}.`)
|
|
11104
11247
|
.option("--status <status>", "Filter by status. Defaults to all.")
|
|
11105
11248
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -11867,6 +12010,7 @@ Examples:
|
|
|
11867
12010
|
.option("--file-ids <csv>", "Comma-separated workspace file UUIDs attached to context and eligible for sandbox input.")
|
|
11868
12011
|
.requiredOption("--max-credits <credits>", "Hard credit ceiling for this run.")
|
|
11869
12012
|
.requiredOption("--approved", "Explicitly approve managed inference up to --max-credits.")
|
|
12013
|
+
.option("--approve-above-default", "Also approve a --max-credits above your default Agent run ceiling (`oxygen limits show`: spend_safety_defaults.agent_run_max_total_credits), for this run only.")
|
|
11870
12014
|
.option("--json", "Print a JSON envelope.")
|
|
11871
12015
|
.action(async (slug, options) => {
|
|
11872
12016
|
await handleAsyncAction("agent run", options, () => {
|
|
@@ -11884,6 +12028,7 @@ Examples:
|
|
|
11884
12028
|
...(options.fileIds ? { file_ids: options.fileIds.split(",").map((value) => value.trim()).filter(Boolean) } : {}),
|
|
11885
12029
|
max_credits: maxCredits,
|
|
11886
12030
|
approved: options.approved === true,
|
|
12031
|
+
...(options.approveAboveDefault ? { approve_above_default: true } : {}),
|
|
11887
12032
|
},
|
|
11888
12033
|
});
|
|
11889
12034
|
});
|
|
@@ -11922,7 +12067,7 @@ Examples:
|
|
|
11922
12067
|
await handleAsyncAction("agent approval decide", options, () => requestOxygen(`/api/cli/agent/${encodeURIComponent(slug)}/runs/${encodeURIComponent(runId)}/approvals/${encodeURIComponent(approvalId)}`, { method: "POST", body: { decision: options.decision } }));
|
|
11923
12068
|
}))
|
|
11924
12069
|
.addCommand(new Command("trigger-create")
|
|
11925
|
-
.description(
|
|
12070
|
+
.description(`Create a cron, internal event, or signed-webhook trigger for the Workspace Agent. A webhook trigger returns its URL, its secret once, and rate_limit: ${WEBHOOK_TRIGGER_RATE_HELP}.`)
|
|
11926
12071
|
.argument("<slug>", "Currently: workspace.")
|
|
11927
12072
|
.requiredOption("--type <cron|event|webhook>", "Trigger type.")
|
|
11928
12073
|
.requiredOption("--instruction <text>", "Goal template run for each delivery.")
|
|
@@ -11933,6 +12078,7 @@ Examples:
|
|
|
11933
12078
|
.option("--thread-mode <isolated|persistent>", "Reuse a bounded thread across deliveries. Defaults to isolated.")
|
|
11934
12079
|
.requiredOption("--max-credits <credits>", "Standing per-delivery credit ceiling.")
|
|
11935
12080
|
.requiredOption("--approved", "Authorize each trigger delivery up to --max-credits.")
|
|
12081
|
+
.option("--approve-above-default", "Also approve a per-delivery --max-credits above your default Agent run ceiling (`oxygen limits show`: spend_safety_defaults.agent_run_max_total_credits), for every delivery of this trigger.")
|
|
11936
12082
|
.option("--json", "Print a JSON envelope.")
|
|
11937
12083
|
.action(async (slug, options) => {
|
|
11938
12084
|
await handleAsyncAction("agent trigger create", options, () => {
|
|
@@ -11948,6 +12094,7 @@ Examples:
|
|
|
11948
12094
|
thread_mode: options.threadMode ?? "isolated",
|
|
11949
12095
|
max_credits: maxCredits,
|
|
11950
12096
|
approved: options.approved === true,
|
|
12097
|
+
...(options.approveAboveDefault ? { approve_above_default: true } : {}),
|
|
11951
12098
|
} });
|
|
11952
12099
|
});
|
|
11953
12100
|
}))
|
|
@@ -12612,7 +12759,7 @@ Examples:
|
|
|
12612
12759
|
.command("enrich-column")
|
|
12613
12760
|
.description("High-level table enrichment helpers. To see every enrichment a table's columns support, with per-row prices, before choosing one, run `tools search --for-table <table>` (0 credits).")
|
|
12614
12761
|
.addCommand(new Command("preview")
|
|
12615
|
-
.description("Preflight an enrichment column without provider calls or credit usage.")
|
|
12762
|
+
.description("Preflight an enrichment column without provider calls or credit usage. A lane on your own key (BYOK) is quoted at OXYGEN's platform fee per call, hit or miss (provider_cost_breakdown[].byok_platform_fee_credits_per_call).")
|
|
12616
12763
|
.argument("<table>", "Table id or slug.")
|
|
12617
12764
|
.option("--source-column <column>", "Source column key or id. For mobile_phone this is the LinkedIn URL column.")
|
|
12618
12765
|
.option("--full-name-column <column>", "Column key or id containing the person's full name for work_email. Pair with company-domain/name for Prospeo, RocketReach, LeadMagic, Hunter, Dropleads, ContactOut, BetterContact, or People Data Labs.")
|
|
@@ -12631,7 +12778,7 @@ Examples:
|
|
|
12631
12778
|
.option("--email-pattern-validation <mode>", "Work-email pattern pre-step: leadmagic_valid_only or disabled.")
|
|
12632
12779
|
.option("--phone-waterfall-profile <profile>", "Phone waterfall profile: auto (input-aware), linkedin_url, email, or name_domain. Auto picks the cheapest cost-ordered provider set for each row's inputs (mobile match rate ~30-60%).")
|
|
12633
12780
|
.option("--verify-phone", "Validate found phone numbers with ClearoutPhone (adds phone_line_type + phone_carrier; filter phone_line_type=mobile for mobile-only).")
|
|
12634
|
-
.option("--allow-premium-lanes", "Opt in to managed lanes billing over 50 credits per call. Only linkedin_url has one (LeadMagic email_to_profile, 122.5cr); it is off by default so an unattended run can't bill-shock. No effect on mobile_phone, work_email or verify_email.")
|
|
12781
|
+
.option("--allow-premium-lanes", "Opt in to managed lanes billing over 50 credits per call. Only linkedin_url has one (LeadMagic email_to_profile, 122.5cr), until the 2026-09 repricing date, when a LinkedIn URL starts billing only on a hit; it is off by default so an unattended run can't bill-shock. No effect on mobile_phone, work_email or verify_email.")
|
|
12635
12782
|
.option("--phone-verification-credential-mode <mode>", "ClearoutPhone credential mode for phone verification: managed or user_api_key.")
|
|
12636
12783
|
.option("--limit <n>", "Rows to estimate. Defaults to 10.")
|
|
12637
12784
|
.option("--all", "Estimate all rows.")
|
|
@@ -12666,7 +12813,7 @@ Examples:
|
|
|
12666
12813
|
.option("--email-pattern-validation <mode>", "Work-email pattern pre-step: leadmagic_valid_only or disabled.")
|
|
12667
12814
|
.option("--phone-waterfall-profile <profile>", "Phone waterfall profile: auto (input-aware), linkedin_url, email, or name_domain. Auto picks the cheapest cost-ordered provider set for each row's inputs (mobile match rate ~30-60%).")
|
|
12668
12815
|
.option("--verify-phone", "Validate found phone numbers with ClearoutPhone (adds phone_line_type + phone_carrier; filter phone_line_type=mobile for mobile-only).")
|
|
12669
|
-
.option("--allow-premium-lanes", "Opt in to managed lanes billing over 50 credits per call. Only linkedin_url has one (LeadMagic email_to_profile, 122.5cr); it is off by default so an unattended run can't bill-shock. No effect on mobile_phone, work_email or verify_email.")
|
|
12816
|
+
.option("--allow-premium-lanes", "Opt in to managed lanes billing over 50 credits per call. Only linkedin_url has one (LeadMagic email_to_profile, 122.5cr), until the 2026-09 repricing date, when a LinkedIn URL starts billing only on a hit; it is off by default so an unattended run can't bill-shock. No effect on mobile_phone, work_email or verify_email.")
|
|
12670
12817
|
.option("--phone-verification-credential-mode <mode>", "ClearoutPhone credential mode for phone verification: managed or user_api_key.")
|
|
12671
12818
|
.option("--limit <n>", "Rows to queue.")
|
|
12672
12819
|
.option("--all", "Queue all rows.")
|
|
@@ -12735,7 +12882,7 @@ Examples:
|
|
|
12735
12882
|
await handleAsyncAction("find phone", options, () => requestOxygen("/api/cli/find/run", { method: "POST", body: buildFindBody("phone", options) }));
|
|
12736
12883
|
}))
|
|
12737
12884
|
.addCommand(new Command("linkedin")
|
|
12738
|
-
.description("Resolve a person's LinkedIn URL from name + company (or email), with identity validation.")
|
|
12885
|
+
.description("Resolve a person's LinkedIn URL from name + company (or email), with identity validation. Each lane tried bills until the 2026-09 repricing date; from it a URL found that matches the person costs a flat 5 credits and a miss costs 0.")
|
|
12739
12886
|
.option("--full-name <name>", "Person full name.")
|
|
12740
12887
|
.option("--email <email>", "Known email.")
|
|
12741
12888
|
.option("--company-domain <domain>", "Company apex domain.")
|
|
@@ -12857,6 +13004,7 @@ Examples:
|
|
|
12857
13004
|
await handleAsyncAction("supabase connect", options, () => {
|
|
12858
13005
|
if (options.databaseUrlStdin !== true)
|
|
12859
13006
|
throw new Error("--database-url-stdin is required.");
|
|
13007
|
+
markStdinConsumed();
|
|
12860
13008
|
const databaseUrl = readFileSync(0, "utf8").trim();
|
|
12861
13009
|
if (!databaseUrl)
|
|
12862
13010
|
throw new Error("No Supabase database URL was provided on stdin.");
|
|
@@ -13199,7 +13347,7 @@ Examples:
|
|
|
13199
13347
|
await handleAsyncAction("senders checkpoints", options, () => requestOxygen("/api/cli/senders/checkpoints"));
|
|
13200
13348
|
}))
|
|
13201
13349
|
.addCommand(new Command("connect")
|
|
13202
|
-
.description("Get a Unipile hosted-auth URL to connect a new LinkedIn account (or reconnect with --reconnect). Each connected LinkedIn account
|
|
13350
|
+
.description("Get a Unipile hosted-auth URL to connect a new LinkedIn account (or reconnect with --reconnect). Each connected LinkedIn account has one monthly price, never two: a LinkedIn sending seat ($30/month, billed in dollars to the billing owner) while seats are sold, or a monthly credit reservation once they are retired, when a connect the balance cannot cover fails with insufficient_credits. Invites, messages and other LinkedIn actions cost 0 credits on top. Check `oxygen billing seats --json` before connecting: it says which applies and the price. A workspace linked to another organization's plan is billed at that organization. New accounts require --country (the owner's normal LinkedIn login country) and default to syncing only conversations OXYGEN starts; use --inbox-scope all to opt into the full LinkedIn inbox. Use --cookie-auth and --custom-proxy to expose those inputs inside Unipile's hosted wizard; their secrets never pass through OXYGEN. Use --count for bulk onboarding. LinkedIn restricts automated activity: every connection needs --acknowledge-risk (only a reconnect that already recorded one is exempt), and --count above 1 also needs --owner-consent (each owner connects their own account). Links are shareable and valid for 10 minutes.")
|
|
13203
13351
|
.option("--reconnect <connection_id>", "Reconnect an existing connection instead of creating a new one. Accepts a connection id.")
|
|
13204
13352
|
.option("--country <code>", "Required for new accounts: ISO 3166-1 alpha-2 code for the account owner's normal LinkedIn login country (for example DE or US).")
|
|
13205
13353
|
.option("--sales-nav", "Request Classic + Sales Navigator access during Unipile hosted authentication.")
|
|
@@ -13354,7 +13502,7 @@ Examples:
|
|
|
13354
13502
|
await handleAsyncAction("senders limits get", options, () => requestOxygen(`/api/cli/senders/${encodeURIComponent(id)}/limits`));
|
|
13355
13503
|
}))
|
|
13356
13504
|
.addCommand(new Command("set")
|
|
13357
|
-
.description("Adjust per-account action limits and working hours. LinkedIn actions only run inside the account's working hours (default 07:00-22:00 every day, in the account's timezone); work due outside them is deferred to the next window, not failed. Set them with --timezone, --working-days, --working-hours-start and --working-hours-end; flags you omit keep their current value. A sequence's own schedule can narrow the window for its LinkedIn steps, never widen it; email steps follow the sequence's email send window instead. Every limit flag shows its default and hard maximum; values above the maximum are clamped, and higher caps raise the risk of a LinkedIn restriction. Each action has its own cap (InMails per day and per rolling 30 days, profile views, profile lookups, endorsements, invitation withdrawals, posts), and every send except posts also counts toward --total-actions-per-day. Daily send caps run at half on Saturday and Sunday in the account's timezone unless --weekend-full-volume is set. A withdrawn invitation cannot be re-sent to the same person for 21 days. Own-feed posts and your own Unibox replies ignore working hours. A refused action names its reason (outside_working_hours defers it; linkedin_rate_limited names the limit and resets_at). Changing limits is free. <id> accepts a sender account id, connection id, or Unipile account id.")
|
|
13505
|
+
.description("Adjust per-account action limits and working hours. LinkedIn actions only run inside the account's working hours (default 07:00-22:00 every day, in the account's timezone); work due outside them is deferred to the next window, not failed. Set them with --timezone, --working-days, --working-hours-start and --working-hours-end; flags you omit keep their current value. A sequence's own schedule (sequences update --linkedin-timezone ...) can narrow the window for its LinkedIn steps, never widen it; email steps follow the sequence's email send window instead. Every limit flag shows its default and hard maximum; values above the maximum are clamped, and higher caps raise the risk of a LinkedIn restriction. Each action has its own cap (InMails per day and per rolling 30 days, profile views, profile lookups, endorsements, invitation withdrawals, posts), and every send except posts also counts toward --total-actions-per-day. Daily send caps run at half on Saturday and Sunday in the account's timezone unless --weekend-full-volume is set. A withdrawn invitation cannot be re-sent to the same person for 21 days. Own-feed posts and your own Unibox replies ignore working hours. A refused action names its reason (outside_working_hours defers it; linkedin_rate_limited names the limit and resets_at). Changing limits is free. <id> accepts a sender account id, connection id, or Unipile account id.")
|
|
13358
13506
|
.argument("<id>", "Sender account id, connection id, or Unipile account id.")
|
|
13359
13507
|
.option("--invites-per-day <n>", linkedInLimitHelp("invites_per_day", "Daily LinkedIn connection invites cap"))
|
|
13360
13508
|
.option("--invites-per-week <n>", linkedInLimitHelp("invites_per_week", "Weekly LinkedIn connection invites cap"))
|
|
@@ -15351,7 +15499,7 @@ Examples:
|
|
|
15351
15499
|
}, formatSequenceAnalyticsHealth);
|
|
15352
15500
|
}))
|
|
15353
15501
|
.addCommand(new Command("create")
|
|
15354
|
-
.description("Create a draft multichannel sequence from a steps JSON file. Supports LinkedIn, email, WhatsApp, human call_task, and connected-CRM crm_task journeys. Assign LinkedIn senders with --senders (optional at create — a draft can sit senderless, but enroll/start require at least one for LinkedIn journeys)
|
|
15502
|
+
.description("Create a draft multichannel sequence from a steps JSON file. Supports LinkedIn, email, WhatsApp, human call_task, and connected-CRM crm_task journeys. Assign LinkedIn senders with --senders (optional at create — a draft can sit senderless, but enroll/start require at least one for LinkedIn journeys). Native email sends through connected Google/Microsoft mailboxes, including Microsoft SMTP transport; optional --email-* flags bind an Instantly campaign. Install/read the oxygen-sequencer skill for complete mapped CRM task examples.")
|
|
15355
15503
|
.requiredOption("--name <name>", "Human-readable sequence name.")
|
|
15356
15504
|
.requiredOption("--slug <slug>", "Unique slug for the sequence.")
|
|
15357
15505
|
.requiredOption("--steps-file <path>", "Path to a JSON file: { \"steps\": [...] }. LinkedIn steps (visit_profile | invite | wait_for_connection | message | inmail | follow | like_post | comment_post | withdraw_invite), email steps (email_send | email_enroll | email_move | email_stop), WhatsApp (whatsapp_message), human call tasks (call_task), connected-CRM tasks (crm_task; channel crm; exact identity record_mappings + explicit create/update policy + record_links + dynamic task property_mappings/associations), and control steps (wait | wait_for_signal | branch | stop), each with an `id`. Copy templates support {{column}} interpolation from the lead's row, {{column|fallback}} inline fallbacks, deterministic spintax — {{RANDOM|a|b|c}} or industry-standard bare {Hi|Hey|Hello}, nestable like {Would {Tuesday|Thursday} work|next week?} — and {% if column %}…{% endif %} conditionals; a bare {…} region is spintax only when it contains a top-level |, so literal braces (CSS/JSON) pass through. Native email sends (email_send) also expose three reserved sender variables from the sending mailbox: {{sender_name}} (mailbox display name), {{sender_first_name}} (its first word), and {{sender_email}} (the from address); a row column of the same name WINS on collision, and a missing display name renders empty. comment_post takes text_template and/or ai_prompt — a KG-grounded comment generated at send time (a paid AI call) that falls back to text_template if generation fails. A `branch` routes on signals (then/else) or the legacy connection_accepted/already_connected/open_profile sugar (then_id/else_id). A signal condition can also branch on the LEAD'S DATA: a data leaf { has_column: \"email\" } is true when that row_values column has a non-empty value (add present:false for \"missing\") — e.g. route leads that have an email down an email arm and the rest down a LinkedIn arm. MINIMAL EMAIL STEP SHAPE: { \"id\": \"s1\", \"channel\": \"email\", \"kind\": \"email_send\", \"subject_template\": \"...\", \"body_template\": \"...\" } — body_template is REQUIRED on email_send; subject_template is OPTIONAL and is the one control that decides threading: give a step its own subject and it goes out as a NEW email, omit it and the step continues the lead's previous email in the same thread, going out as \"Re: <the previous email's subject>\" (what the retired email_reply kind used to be — still accepted on input, rewritten into this shape, and reported under `warnings` in the response). The FIRST email step of a sequence must carry a subject, since it has no earlier thread to continue. MINIMAL WHATSAPP STEP SHAPE: { \"id\": \"s1\", \"channel\": \"whatsapp\", \"kind\": \"whatsapp_message\", \"template\": \"Hi {{first_name|there}} …\" } — template is REQUIRED, and a live WhatsApp start also needs the sequence's --whatsapp-cold-initiate opt-in. A/B tests use an explicit `variants` array of copy partials on the step (up to 25 alternates, a–z; spintax varies wording INSIDE one variant and is not A/B-tracked). For the complete replay-safe crm_task upsert mapping shape, install/read the oxygen-sequencer skill.")
|
|
@@ -15363,20 +15511,24 @@ Examples:
|
|
|
15363
15511
|
.option("--from-table <id>", "Alias of --table: the source table id or slug this sequence draws its leads and {{column}} values from.")
|
|
15364
15512
|
.option("--enroll", "After creating the sequence, immediately enroll every row of the bound table (same as running `sequences enroll --from-table` next). Requires --table/--from-table. Enrolling spends no credits and sends nothing.")
|
|
15365
15513
|
.option("--url-column <key>", "Column key holding each lead's LinkedIn URL/provider id.")
|
|
15366
|
-
.option("--email-provider <provider>", "
|
|
15514
|
+
.option("--email-provider <provider>", "Omit for native email through connected Google/Microsoft mailboxes, including Microsoft SMTP transport. Set 'instantly' to bind an Instantly campaign with --email-connection and --email-definition-file. To push rows into a campaign you already run in Smartlead, lemlist, HeyReach or Instantly, use `oxygen sequences push-external`.")
|
|
15367
15515
|
.option("--email-connection <id>", "Instantly connection id for the email track. Defaults to the org's active Instantly connection.")
|
|
15368
15516
|
.option("--email-definition-file <path>", "Path to a JSON file with the email content spec (subjects/bodies/delays/subsequences) compiled to an Instantly campaign on start.")
|
|
15369
15517
|
.option("--max-credits <n>", "Credit cap for the LinkedIn track (also set when starting).")
|
|
15370
15518
|
.option("--max-live-sends <n>", "External-action ceiling for 0-credit email/WhatsApp/CRM-task tracks (positive integer). Required to start any of them live.")
|
|
15371
15519
|
.option("--max-emails-per-mailbox-per-day <n>", "Per-mailbox daily email cap (positive integer) applied across the sending pool; also sets each mailbox's send pace (send window ÷ cap, catching up when a slot is missed). Daily capacity = this cap × sendable mailboxes; `sequences stats` reports it under daily_budget.")
|
|
15372
|
-
.option("--send-window-file <path>", "
|
|
15373
|
-
.option("--
|
|
15374
|
-
.option("--
|
|
15520
|
+
.option("--send-window-file <path>", "Email window JSON: { timezone, days?, start, end, timezone_mode?, recipient_timezone_column? }; days are ISO 1-7. New sequences default to weekdays 09:00-17:00 in the workspace timezone when available, otherwise UTC. Gates email steps only; LinkedIn/WhatsApp steps use --linkedin-timezone/-days/-hours-start/-hours-end.")
|
|
15521
|
+
.option("--linkedin-timezone <tz>", "LinkedIn/WhatsApp sending schedule (the web Sending schedule): IANA zone, e.g. Europe/Zurich. Set with --linkedin-days, --linkedin-hours-start and --linkedin-hours-end; the window narrows each sender's working hours.")
|
|
15522
|
+
.option("--linkedin-days <days>", "LinkedIn sending schedule days: mon-fri, mon,wed,fri, or ISO numbers 1-7 (1 = Monday).")
|
|
15523
|
+
.option("--linkedin-hours-start <HH:MM>", "LinkedIn sending schedule start, local to --linkedin-timezone, e.g. 06:45.")
|
|
15524
|
+
.option("--linkedin-hours-end <HH:MM>", "LinkedIn sending schedule end (exclusive), e.g. 18:30. Must be after the start. The window only narrows each sender's working hours (default 07:00-22:00, see senders limits get), never widens them.")
|
|
15525
|
+
.option("--max-emails-per-day <n>", "Sequence-wide daily live-send cap across all senders/mailboxes. Unset means no additional sequence-wide cap; mailbox caps still apply. Omit on update to keep the current setting.")
|
|
15526
|
+
.option("--max-new-enrollments-per-day <n>", "Daily cap on NEW first-touch leads. Unset means no additional intake cap; sending limits still apply. Omit on update to keep the current setting.")
|
|
15375
15527
|
.option("--sequence-prioritization <mode>", "Under a tight daily budget, serve 'followups' or 'new_leads' first.")
|
|
15376
15528
|
.option("--esp-matching <mode>", "Native-email ESP matching: 'prefer' (DEFAULT) biases toward a mailbox on the recipient's own provider, falling back to any; 'off' rotates mailboxes freely; 'strict' requires a same-provider mailbox and defers the send when none exists.")
|
|
15377
15529
|
.option("--esp-routing-file <path>", "Path to a JSON file holding the WHOLE native-email routing policy: { mode?, routes?, exclude? }. `routes` is keyed by the RECIPIENT's provider and its values are relative send shares per sending provider, so { \"google\": { \"microsoft\": 100 } } sends Google-hosted recipients from Microsoft inboxes. `exclude` bars a sending domain outright: [{ \"domain\": \"burned.example\", \"from_recipients\": [\"google\"] }]. Replaces --esp-matching and the file is the complete policy, not a patch.")
|
|
15378
15530
|
.option("--sender-failover <mode>", "What happens when an enrollment's LinkedIn/WhatsApp sender goes unavailable: 'wait' (default) resumes when the sender recovers; 'rebind' moves UNTOUCHED enrollments (no thread, no pending invite, no lead binding) to the least-loaded healthy sender in the pool after ~5h of confirmed outage.")
|
|
15379
|
-
.option("--email-min-gap-minutes <n>", "Minimum
|
|
15531
|
+
.option("--email-min-gap-minutes <n>", "Minimum same-mailbox gap, default 12 minutes (integer 0-720; 0 disables the extra gap). Sends are spread across the window and may be farther apart. Daily caps and windows still apply.")
|
|
15380
15532
|
.option("--opportunity-value <usd>", "Estimated USD value of one positive-reply opportunity (>= 0). Analytics-only: sequence stats multiply it by the positive-reply count to report pipeline $ (stats.opportunities). Never gates a send or spends a credit.")
|
|
15381
15533
|
.option("--mailboxes <ids>", "Per-sequence sending-account allowlist for native email (Instantly's 'Accounts to use'): comma-separated mailbox ids. Native email sends rotate ONLY over these mailboxes instead of the whole pool. Omit for the whole pool. Note: a narrow allowlist plus --esp-matching strict can starve sends when no allowed same-provider mailbox exists (surfaced as blocked defer reasons in stats).")
|
|
15382
15534
|
.option("--sender-profiles <ids>", "Send from these sender profiles (unified LinkedIn/WhatsApp/inbox identities): comma-separated profile ids. The server resolves each into its senders + inboxes. Live start is blocked if a selected profile lacks an account for a channel the journey uses.")
|
|
@@ -15384,11 +15536,15 @@ Examples:
|
|
|
15384
15536
|
.option("--exclude-contacted", "Default every enroll into this sequence to cross-campaign exclusion — skip leads any OTHER active sequence is already contacting. A per-enroll `sequences enroll --exclude-contacted` / `--no-exclude-contacted` always overrides this default. Off by default.")
|
|
15385
15537
|
.option("--no-exclude-contacted", "Turn the sequence-level exclude-contacted default back OFF (enrollments then exclude cross-campaign only when a call opts in).")
|
|
15386
15538
|
.option("--stop-on-reply-scope <scope>", "Native-email reply stop: 'lead' (default) stops the replying enrollment; 'company_domain' also stops active enrollments at the same non-freemail company domain.")
|
|
15539
|
+
.option("--bounce-protection <json>", "Sequence bounce policy JSON (partial updates merge): enabled=true, warning_rate=.02, pause_rate=.03, min_sends=20, window_days=7. Hard bounces / accepted sends in the rolling window; pauses this sequence, not the fleet. Rates are fractions; mailbox protection is separate.")
|
|
15540
|
+
.option("--include-unsubscribe-headers", "Opt in to one-click unsubscribe headers (default off); visible link is separate.")
|
|
15541
|
+
.option("--no-include-unsubscribe-headers", "Omit one-click headers.")
|
|
15542
|
+
.option("--reply-to-stop-text <text>", "Personal opt-out text (default empty); empty clears it.")
|
|
15387
15543
|
.option("--include-unsubscribe-link", "Append OXYGEN's visible recipient-bound unsubscribe footer to first-touch cold emails. Explicit opt-in; off by default. Plain-text messages show the URL, while HTML messages show a linked Unsubscribe label.")
|
|
15388
15544
|
.option("--no-include-unsubscribe-link", "Do not append OXYGEN's visible unsubscribe footer (the default). Sends the authored body unchanged and never removes suppression from recipients who already opted out.")
|
|
15389
|
-
.option("--tracking-opens", "
|
|
15545
|
+
.option("--tracking-opens", "Opt in to open tracking (default off). Requires a verified tracking domain and deployment tracking secret.")
|
|
15390
15546
|
.option("--no-tracking-opens", "Turn OFF the open pixel for this sequence's native email sends (a deliverability knob — a pixel is spam-filter surface).")
|
|
15391
|
-
.option("--tracking-clicks", "
|
|
15547
|
+
.option("--tracking-clicks", "Opt in to click tracking (default off). Requires a verified tracking domain and deployment tracking secret.")
|
|
15392
15548
|
.option("--no-tracking-clicks", "Turn OFF click-link rewriting for this sequence's native email sends (links go out untouched).")
|
|
15393
15549
|
.option("--tags <csv>", "Comma-separated workspace tags linking this sequence to tables, workflows, and knowledge.")
|
|
15394
15550
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -15473,21 +15629,25 @@ Examples:
|
|
|
15473
15629
|
.option("--linkedin-url-column-key <key>", "LinkedIn: row_values key holding each lead's profile URL (else falls back to linkedin_url/linkedinUrl/linkedin/profile_url). \"\" clears it back to auto-detect.")
|
|
15474
15630
|
.option("--senders <ids>", "Comma-separated LinkedIn or WhatsApp sender account ids (or connection / Unipile ids; `linkedin senders` / `whatsapp accounts` list them).")
|
|
15475
15631
|
.option("--source-table <idOrSlug>", "Bind the table this sequence enrolls leads from (id or slug). Only lands while the sequence has no source table yet (first-writer-wins); its rows become enrollable leads. Draft/paused only.")
|
|
15476
|
-
.option("--email-provider <provider>", "
|
|
15632
|
+
.option("--email-provider <provider>", "Optional linked-campaign provider: 'instantly'. Omit to preserve the existing binding; --clear-email removes it on a draft. Native email needs no provider binding and sends through connected Google/Microsoft mailboxes, including Microsoft SMTP transport.")
|
|
15477
15633
|
.option("--email-connection <id>", "Instantly connection id for the email track.")
|
|
15478
15634
|
.option("--email-definition-file <path>", "Path to a JSON file with the email content spec (subjects/bodies/delays/subsequences).")
|
|
15479
15635
|
.option("--clear-email", "Remove the email binding from the sequence (draft only).")
|
|
15480
15636
|
.option("--max-credits <n>", "Credit cap for the LinkedIn track (draft only).")
|
|
15481
15637
|
.option("--max-live-sends <n>", "Draft-time external-action ceiling for 0-credit email/WhatsApp/CRM-task tracks (positive integer). After first start, change it through `sequences start`.")
|
|
15482
15638
|
.option("--max-emails-per-mailbox-per-day <n>", "Per-mailbox daily email cap (positive integer) applied across the sending pool; also sets each mailbox's send pace (send window ÷ cap, catching up when a slot is missed). Daily capacity = this cap × sendable mailboxes; `sequences stats` reports it under daily_budget.")
|
|
15483
|
-
.option("--send-window-file <path>", "
|
|
15484
|
-
.option("--
|
|
15485
|
-
.option("--
|
|
15639
|
+
.option("--send-window-file <path>", "Email window JSON: { timezone, days?, start, end, timezone_mode?, recipient_timezone_column? }; days are ISO 1-7. New sequences default to weekdays 09:00-17:00 in the workspace timezone when available, otherwise UTC. Gates email steps only; LinkedIn/WhatsApp steps use --linkedin-timezone/-days/-hours-start/-hours-end.")
|
|
15640
|
+
.option("--linkedin-timezone <tz>", "LinkedIn/WhatsApp sending schedule (the web Sending schedule): IANA zone, e.g. Europe/Zurich. Set with --linkedin-days, --linkedin-hours-start and --linkedin-hours-end; the window narrows each sender's working hours.")
|
|
15641
|
+
.option("--linkedin-days <days>", "LinkedIn sending schedule days: mon-fri, mon,wed,fri, or ISO numbers 1-7 (1 = Monday).")
|
|
15642
|
+
.option("--linkedin-hours-start <HH:MM>", "LinkedIn sending schedule start, local to --linkedin-timezone, e.g. 06:45.")
|
|
15643
|
+
.option("--linkedin-hours-end <HH:MM>", "LinkedIn sending schedule end (exclusive), e.g. 18:30. Must be after the start. The window only narrows each sender's working hours (default 07:00-22:00, see senders limits get), never widens them.")
|
|
15644
|
+
.option("--max-emails-per-day <n>", "Sequence-wide daily live-send cap across all senders/mailboxes. Unset means no additional sequence-wide cap; mailbox caps still apply. Omit on update to keep the current setting.")
|
|
15645
|
+
.option("--max-new-enrollments-per-day <n>", "Daily cap on NEW first-touch leads. Unset means no additional intake cap; sending limits still apply. Omit on update to keep the current setting.")
|
|
15486
15646
|
.option("--sequence-prioritization <mode>", "Under a tight daily budget, serve 'followups' or 'new_leads' first.")
|
|
15487
15647
|
.option("--esp-matching <mode>", "Native-email ESP matching: 'prefer' (DEFAULT) biases toward a mailbox on the recipient's own provider, falling back to any; 'off' rotates mailboxes freely; 'strict' requires a same-provider mailbox and defers the send when none exists.")
|
|
15488
15648
|
.option("--esp-routing-file <path>", "Path to a JSON file holding the WHOLE native-email routing policy: { mode?, routes?, exclude? }. `routes` is keyed by the RECIPIENT's provider and its values are relative send shares per sending provider, so { \"google\": { \"microsoft\": 100 } } sends Google-hosted recipients from Microsoft inboxes. `exclude` bars a sending domain outright: [{ \"domain\": \"burned.example\", \"from_recipients\": [\"google\"] }]. Replaces --esp-matching and the file is the complete policy, not a patch.")
|
|
15489
15649
|
.option("--sender-failover <mode>", "What happens when an enrollment's LinkedIn/WhatsApp sender goes unavailable: 'wait' (default) resumes when the sender recovers; 'rebind' moves UNTOUCHED enrollments (no thread, no pending invite, no lead binding) to the least-loaded healthy sender in the pool after ~5h of confirmed outage.")
|
|
15490
|
-
.option("--email-min-gap-minutes <n>", "Minimum
|
|
15650
|
+
.option("--email-min-gap-minutes <n>", "Minimum same-mailbox gap, default 12 minutes (integer 0-720; 0 disables the extra gap). Sends are spread across the window and may be farther apart. Daily caps and windows still apply.")
|
|
15491
15651
|
.option("--opportunity-value <usd>", "Estimated USD value of one positive-reply opportunity (>= 0). Analytics-only: sequence stats multiply it by the positive-reply count to report pipeline $ (stats.opportunities). Never gates a send or spends a credit.")
|
|
15492
15652
|
.option("--mailboxes <ids>", "Per-sequence sending-account allowlist for native email (Instantly's 'Accounts to use'): comma-separated mailbox ids. Native email sends rotate ONLY over these mailboxes instead of the whole pool. Note: a narrow allowlist plus --esp-matching strict can starve sends when no allowed same-provider mailbox exists (surfaced as blocked defer reasons in stats).")
|
|
15493
15653
|
.option("--sender-profiles <ids>", "Send from these sender profiles (unified LinkedIn/WhatsApp/inbox identities): comma-separated profile ids. The server resolves each into its senders + inboxes. Live start is blocked if a selected profile lacks an account for a channel the journey uses.")
|
|
@@ -15495,11 +15655,15 @@ Examples:
|
|
|
15495
15655
|
.option("--exclude-contacted", "Default every enroll into this sequence to cross-campaign exclusion — skip leads any OTHER active sequence is already contacting. A per-enroll `sequences enroll --exclude-contacted` / `--no-exclude-contacted` always overrides this default. Off by default.")
|
|
15496
15656
|
.option("--no-exclude-contacted", "Turn the sequence-level exclude-contacted default back OFF (enrollments then exclude cross-campaign only when a call opts in).")
|
|
15497
15657
|
.option("--stop-on-reply-scope <scope>", "Native-email reply stop: 'lead' (default) stops the replying enrollment; 'company_domain' also stops active enrollments at the same non-freemail company domain.")
|
|
15658
|
+
.option("--bounce-protection <json>", "Sequence bounce policy JSON (partial updates merge): enabled=true, warning_rate=.02, pause_rate=.03, min_sends=20, window_days=7. Hard bounces / accepted sends in the rolling window; pauses this sequence, not the fleet. Rates are fractions; mailbox protection is separate.")
|
|
15659
|
+
.option("--include-unsubscribe-headers", "Opt in to one-click unsubscribe headers (default off); visible link is separate.")
|
|
15660
|
+
.option("--no-include-unsubscribe-headers", "Omit one-click headers.")
|
|
15661
|
+
.option("--reply-to-stop-text <text>", "Personal opt-out text (default empty); empty clears it.")
|
|
15498
15662
|
.option("--include-unsubscribe-link", "Append OXYGEN's visible recipient-bound unsubscribe footer to first-touch cold emails. Explicit opt-in; off by default. Plain-text messages show the URL, while HTML messages show a linked Unsubscribe label.")
|
|
15499
15663
|
.option("--no-include-unsubscribe-link", "Turn the visible unsubscribe footer OFF. Future first-touch emails send the authored body unchanged; existing suppression records remain enforced.")
|
|
15500
|
-
.option("--tracking-opens", "
|
|
15664
|
+
.option("--tracking-opens", "Opt in to open tracking (default off). Requires a verified tracking domain and deployment tracking secret.")
|
|
15501
15665
|
.option("--no-tracking-opens", "Turn OFF the open pixel for this sequence's native email sends (a deliverability knob — a pixel is spam-filter surface).")
|
|
15502
|
-
.option("--tracking-clicks", "
|
|
15666
|
+
.option("--tracking-clicks", "Opt in to click tracking (default off). Requires a verified tracking domain and deployment tracking secret.")
|
|
15503
15667
|
.option("--no-tracking-clicks", "Turn OFF click-link rewriting for this sequence's native email sends (links go out untouched).")
|
|
15504
15668
|
.option("--tags <csv>", "Replace the sequence's workspace tags (comma-separated; \"\" clears them). The ONE field an ARCHIVED sequence still accepts — tag finished campaigns to link their learnings across the workspace.")
|
|
15505
15669
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -15555,7 +15719,7 @@ Examples:
|
|
|
15555
15719
|
body.email = email;
|
|
15556
15720
|
}
|
|
15557
15721
|
if (Object.keys(body).length === 0) {
|
|
15558
|
-
throw new Error("Provide at least one field to update (--name, --steps-file, --source-table, --channels, --senders, --tags, --email-*, --clear-email, --max-credits, --max-live-sends, --max-emails-per-mailbox-per-day, --send-window-file, --stop-on-reply-scope, --[no-]include-unsubscribe-link, --no-stop-on-bounce, --[no-]exclude-contacted, or --[no-]tracking-opens / --[no-]tracking-clicks).");
|
|
15722
|
+
throw new Error("Provide at least one field to update (--name, --steps-file, --source-table, --channels, --senders, --tags, --email-*, --clear-email, --max-credits, --max-live-sends, --max-emails-per-mailbox-per-day, --send-window-file, --linkedin-* schedule, --stop-on-reply-scope, --[no-]include-unsubscribe-link, --no-stop-on-bounce, --[no-]exclude-contacted, or --[no-]tracking-opens / --[no-]tracking-clicks).");
|
|
15559
15723
|
}
|
|
15560
15724
|
return requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}`, {
|
|
15561
15725
|
method: "PATCH",
|
|
@@ -15916,7 +16080,7 @@ Examples:
|
|
|
15916
16080
|
await handleSequenceVariantsAction(sequence, options);
|
|
15917
16081
|
})));
|
|
15918
16082
|
program.addCommand(new Command("voice")
|
|
15919
|
-
.description("The call channel. `tasks` works the call queue — leads waiting for a HUMAN to dial, queued by sequence call steps, CRM records, table rows, and replies. Claiming takes a lease so two reps never dial the same prospect. Nothing here places a call:
|
|
16083
|
+
.description("The call channel. `tasks` works the call queue — leads waiting for a HUMAN to dial, queued by sequence call steps, CRM records, table rows, and replies. Claiming takes a lease so two reps never dial the same prospect. `numbers` manages the dialing pool: list, search, buy, tag, cap, release. Nothing here places a call: a call is live audio a rep holds in the web softphone at /sequencer/calls (a beta feature: Settings → Beta features; 5 credits per connected minute), so the terminal works everything around it. Only `numbers buy --approved` starts a charge.")
|
|
15920
16084
|
.addCommand(new Command("tasks")
|
|
15921
16085
|
.description("Work the call queue: queue | list | claim | complete | extend | skip | override.\n\nA call that nobody answers is dispositioned for you from the call itself, which\nalso resumes any sequence enrollment parked on it — so `complete` and `skip`\nreport already_recorded:true rather than failing when that has happened. You log\nonly what a person said.\n\nWhat blocks a dial, and what you can do about it:\n calling_window lead's local time outside 09:00-20:00 waivable per lead\n unknown_timezone no timezone on the lead, so the window\n cannot be checked (fails closed) waivable per lead\n daily_cap the chosen number hit today's dial cap waivable per lead\n suppressed on the workspace do-not-call list NEVER waivable\n destination_blocked your numbers cannot reach that country NEVER waivable\n\n`list` reports dialable, block_reason and block_detail per lead, plus\ndialable_count and blocked_by_reason for the page. Waive one with `override`.")
|
|
15922
16086
|
.addCommand(new Command("queue")
|
|
@@ -16118,7 +16282,7 @@ Examples:
|
|
|
16118
16282
|
});
|
|
16119
16283
|
})))
|
|
16120
16284
|
.addCommand(new Command("numbers")
|
|
16121
|
-
.description("The org's dialing pool: list |
|
|
16285
|
+
.description("The org's dialing pool: list | search | requirements | buy | tag | cap | release. Searching is free; buying previews the recurring monthly charge until you pass --approved.")
|
|
16122
16286
|
.addCommand(new Command("list")
|
|
16123
16287
|
.description("List the org's phone numbers with warm-up state, daily cap, and what each one costs per month. Optional --tag narrows to one campaign's numbers.")
|
|
16124
16288
|
.option("--tag <tags>", "Comma-separated workspace tags — matches phone numbers carrying ANY of these tags (see `oxygen tags list`).")
|
|
@@ -16132,6 +16296,71 @@ Examples:
|
|
|
16132
16296
|
const suffix = params.toString();
|
|
16133
16297
|
return requestOxygen(`/api/cli/voice/numbers${suffix ? `?${suffix}` : ""}`);
|
|
16134
16298
|
});
|
|
16299
|
+
}))
|
|
16300
|
+
.addCommand(new Command("search")
|
|
16301
|
+
.description("Find numbers you could buy, by area code or region. Free: searching buys nothing and reserves nothing, so a listed number can be gone by the time you buy it.")
|
|
16302
|
+
.option("--area-code <code>", "Area code, e.g. 415 (US/CA).")
|
|
16303
|
+
.option("--region <code>", "State or region, e.g. CA.")
|
|
16304
|
+
.option("--country <iso>", "ISO country code (default US). Run `voice numbers requirements` first for a country outside US/CA.")
|
|
16305
|
+
.option("--limit <n>", "How many to return (1-30; default 10).")
|
|
16306
|
+
.option("--json", "Print a JSON envelope.")
|
|
16307
|
+
.action(async (options) => {
|
|
16308
|
+
await handleAsyncAction("voice numbers search", options, () => {
|
|
16309
|
+
const limit = readOption(options.limit);
|
|
16310
|
+
return requestOxygen("/api/cli/voice/numbers", {
|
|
16311
|
+
method: "POST",
|
|
16312
|
+
body: {
|
|
16313
|
+
action: "search",
|
|
16314
|
+
...voiceNumberFilters(options),
|
|
16315
|
+
...(limit ? { limit: Number(limit) } : {}),
|
|
16316
|
+
},
|
|
16317
|
+
});
|
|
16318
|
+
});
|
|
16319
|
+
}))
|
|
16320
|
+
.addCommand(new Command("requirements")
|
|
16321
|
+
.description("What a country demands before it sells a number (business details, documents). Free, and read live from the carrier.")
|
|
16322
|
+
.option("--country <iso>", "ISO country code (default US).")
|
|
16323
|
+
.option("--end-user <type>", "business (default) or individual.")
|
|
16324
|
+
.option("--json", "Print a JSON envelope.")
|
|
16325
|
+
.action(async (options) => {
|
|
16326
|
+
await handleAsyncAction("voice numbers requirements", options, () => {
|
|
16327
|
+
const country = readOption(options.country);
|
|
16328
|
+
const endUser = readOption(options.endUser);
|
|
16329
|
+
return requestOxygen("/api/cli/voice/numbers", {
|
|
16330
|
+
method: "POST",
|
|
16331
|
+
body: {
|
|
16332
|
+
action: "requirements",
|
|
16333
|
+
...(country ? { iso_country: country.toUpperCase() } : {}),
|
|
16334
|
+
...(endUser ? { end_user_type: endUser } : {}),
|
|
16335
|
+
},
|
|
16336
|
+
});
|
|
16337
|
+
});
|
|
16338
|
+
}))
|
|
16339
|
+
.addCommand(new Command("buy")
|
|
16340
|
+
.description("Buy phone numbers for the dialer. A number is a RECURRING monthly charge until you release it, and it starts in warm-up on a reduced daily cap. Previews by default: the preview names the numbers and the monthly credits, and nothing is bought until you pass --approved. At most 10 per order.")
|
|
16341
|
+
.option("--count <n>", "How many numbers to buy (1-10). Not needed with --number.")
|
|
16342
|
+
.option("--number <e164s>", "Exact numbers to buy, comma-separated (from `voice numbers search`).")
|
|
16343
|
+
.option("--area-code <code>", "Pick numbers in this area code.")
|
|
16344
|
+
.option("--region <code>", "Pick numbers in this state or region.")
|
|
16345
|
+
.option("--country <iso>", "ISO country code (default US).")
|
|
16346
|
+
.option("--approved", "Actually buy. Without this you get a preview and nothing is bought or charged.")
|
|
16347
|
+
.option("--json", "Print a JSON envelope.")
|
|
16348
|
+
.action(async (options) => {
|
|
16349
|
+
await handleAsyncAction("voice numbers buy", options, () => {
|
|
16350
|
+
const numbers = splitCommaList(options.number);
|
|
16351
|
+
const count = readOption(options.count);
|
|
16352
|
+
if (numbers.length === 0 && !count)
|
|
16353
|
+
throw new Error("Pass --count <n> or --number <e164,...>.");
|
|
16354
|
+
return requestOxygen("/api/cli/voice/numbers", {
|
|
16355
|
+
method: "POST",
|
|
16356
|
+
body: {
|
|
16357
|
+
action: "buy",
|
|
16358
|
+
...voiceNumberFilters(options),
|
|
16359
|
+
...(numbers.length > 0 ? { phone_numbers: numbers } : { count: Number(count) }),
|
|
16360
|
+
...(options.approved === true ? { approved: true } : {}),
|
|
16361
|
+
},
|
|
16362
|
+
});
|
|
16363
|
+
});
|
|
16135
16364
|
}))
|
|
16136
16365
|
.addCommand(new Command("tag")
|
|
16137
16366
|
.description("Replace a phone number's workspace tags (whole set; `--tags \"\"` clears) — e.g. pool the numbers dialing one campaign behind its tag. See `oxygen tags list` for the vocabulary.")
|
|
@@ -16145,9 +16374,9 @@ Examples:
|
|
|
16145
16374
|
}));
|
|
16146
16375
|
}))
|
|
16147
16376
|
.addCommand(new Command("cap")
|
|
16148
|
-
.description("
|
|
16377
|
+
.description("Adjust one number's daily dial cap. `voice numbers list` shows every number's cap, today's ceiling and dials left.")
|
|
16149
16378
|
.addCommand(new Command("set")
|
|
16150
|
-
.description("Set one phone number's daily dial cap (dials per day, 1-300; new numbers get 100). A warming number still dials at its ramp's ceiling until it has warmed. Consumes no credits.")
|
|
16379
|
+
.description("Set one phone number's daily dial cap (dials per day, 1-300; new numbers get 100). A warming number still dials at its ramp's ceiling until it has warmed. Consumes no credits. Example: `oxygen voice numbers cap set +14155550142 --daily-cap 150`.")
|
|
16151
16380
|
.argument("<number>", "Phone number in E.164 (e.g. +14155550142) or its id.")
|
|
16152
16381
|
.requiredOption("--daily-cap <n>", "New daily dial cap, a whole number from 1 to 300.")
|
|
16153
16382
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -16746,7 +16975,7 @@ Examples:
|
|
|
16746
16975
|
});
|
|
16747
16976
|
})));
|
|
16748
16977
|
program.addCommand(new Command("managed-inboxes")
|
|
16749
|
-
.description("Managed sending inboxes bought through OXYGEN: subscribe a domain + N InboxKit mailboxes (google/microsoft/azure) as a recurring monthly Oxygen-credit purchase, add inboxes to a domain you already own, inspect/verify, and cancel. OXYGEN Warm-up defaults on when available: its quoted recurring credits and exact future addresses are part of the order approval, and the worker activates it after provisioning without a second warmup approval. Warm-up does not prove native-send authorization: verify each exact address with `mailboxes oauth-health --json`. Subscribe/add-inboxes
|
|
16978
|
+
.description("Managed sending inboxes bought through OXYGEN: subscribe a domain + N InboxKit mailboxes (google/microsoft/azure) as a recurring monthly Oxygen-credit purchase, add inboxes to a domain you already own, inspect/verify, and cancel. OXYGEN Warm-up defaults on when available: its quoted recurring credits and exact future addresses are part of the order approval, and the worker activates it after provisioning without a second warmup approval. Warm-up does not prove native-send authorization: verify each exact address with `mailboxes oauth-health --json`. Subscribe/add-inboxes preview first (re-run with --approved --quote); cancel previews first (re-run with --approved --confirm <domain>). To remove several domains with their inboxes, use `domains delete`. The inbox vendor is chosen for you; --vendor pins one.")
|
|
16750
16979
|
.addCommand(new Command("verify")
|
|
16751
16980
|
.description("Check that OXYGEN, STRIPE, and the VENDOR agree about what this org is buying. The truth about a managed inbox lives in three systems — what the customer asked for, what they are charged, and what is actually running — and a 200 from any one of them proves nothing. Reports every disagreement with WHO IS LOSING MONEY while it stands (customer_overbilled first, then oxygen_pays). Read-only, no writes, 0 Oxygen credits.")
|
|
16752
16981
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -16760,11 +16989,11 @@ Examples:
|
|
|
16760
16989
|
await handleAsyncAction("managed-inboxes registrant", options, () => requestOxygen("/api/cli/managed-inboxes/registrant"));
|
|
16761
16990
|
}))
|
|
16762
16991
|
.addCommand(new Command("subscribe")
|
|
16763
|
-
.description("Subscribe a NEW domain + mailboxes as a managed monthly inbox subscription. WITHOUT --approved this prints a priced PREVIEW with a quote_id (nothing ordered, nothing charged); re-run with --approved --quote <id> to place the order. OXYGEN Warm-up defaults on for Google, Microsoft, and Azure when available: preview and approved responses carry `warmup_activation` with the exact `mailboxes` and `scope=warmup_only`; it is null when warmup is disabled or unavailable. For InboxKit, Google carries `transport=inboxkit_managed_google_handoff`; Microsoft/Azure carries `transport=inboxkit_sequencer_export`. Both carry `mailbox_credentials_required=false` and `native_send_oauth_separate=true`, so managed warm-up needs no customer-supplied OAuth or mailbox password. Google retrieves the existing managed credential only during activation and never persists or returns it; Microsoft/Azure uses native export. Native sending authorization is separate and may still be required before OXYGEN can send. Verify each exact address with `mailboxes oauth-health --json`; InboxKit's unattended Google domain approval usually lands within minutes and the worker keeps retrying, so a waiting row is not a reason to ask the customer to sign in. This order authorizes automatic OXYGEN Warm-up activation after provisioning, so no second `mailboxes warmup enable` approval is needed.
|
|
16764
|
-
.argument("[domain]", "Sending domain to register + host the mailboxes (e.g. send.acme.com). May also be passed as --domain.")
|
|
16992
|
+
.description("Subscribe a NEW domain + mailboxes as a managed monthly inbox subscription. WITHOUT --approved this prints a priced PREVIEW with a quote_id (nothing ordered, nothing charged); re-run with --approved --quote <id> to place the order. OXYGEN Warm-up defaults on for Google, Microsoft, and Azure when available: preview and approved responses carry `warmup_activation` with the exact `mailboxes` and `scope=warmup_only`; it is null when warmup is disabled or unavailable. For InboxKit, Google carries `transport=inboxkit_managed_google_handoff`; Microsoft/Azure carries `transport=inboxkit_sequencer_export`. Both carry `mailbox_credentials_required=false` and `native_send_oauth_separate=true`, so managed warm-up needs no customer-supplied OAuth or mailbox password. Google retrieves the existing managed credential only during activation and never persists or returns it; Microsoft/Azure uses native export. Native sending authorization is separate and may still be required before OXYGEN can send. Verify each exact address with `mailboxes oauth-health --json`; InboxKit's unattended Google domain approval usually lands within minutes and the worker keeps retrying, so a waiting row is not a reason to ask the customer to sign in. This order authorizes automatic OXYGEN Warm-up activation after provisioning, so no second `mailboxes warmup enable` approval is needed. The preview shows the price in force: from the 2026-09 repricing date a Google inbox is 500 credits a month all-in (a flat 400 mailbox line plus the 100-credit mailbox connection, warm-up included, which `connection_monthly_credits` shows and your balance reserves once the inbox connects); Microsoft and Azure mailboxes are priced from the vendor's LIVE rate card. Domains are priced from the vendor's LIVE domain price, and any price below vendor cost is refused. A domain you bought on its own through Oxygen (`domains buy`) takes its first inboxes here too: the preview says `domain_source: owned`, nothing is registered again and `one_off_credits` is 0 (Google or Microsoft; omit --years). To add mailboxes to a domain that already has inboxes use `managed-inboxes add-inboxes`.")
|
|
16993
|
+
.argument("[domain]", "Sending domain to register + host the mailboxes (e.g. send.acme.com), or one you already bought through Oxygen. May also be passed as --domain.")
|
|
16765
16994
|
.option("--domain <domain>", "Sending domain (alternative to the positional argument).")
|
|
16766
16995
|
.requiredOption("--provider <provider>", "Mailbox PLATFORM: google, microsoft, or azure. (google/microsoft cap at 5 mailboxes per domain; azure allows 100.)")
|
|
16767
|
-
.option("--vendor <vendor>", "Pin the vendor: inboxkit
|
|
16996
|
+
.option("--vendor <vendor>", "Pin the vendor: inboxkit. Omit to let OXYGEN choose. A named vendor with no credential FAILS rather than falling back to another.")
|
|
16768
16997
|
.option("--sender <id>", "Sender profile id (from `oxygen senders profiles list`) that OWNS every mailbox in this order: its first name, last name, and profile picture are stamped on each one, so you never retype them. A sender profile is one person's sending identity — their LinkedIn/WhatsApp accounts and email inboxes under one name and photo. One sender can own several addresses on the same domain (see --locals). Any first_name/last_name/profile_picture_url in --mailboxes/--file is dropped for the sender's own values.")
|
|
16769
16998
|
.option("--locals <list>", "Comma-separated local parts to create for --sender, e.g. `--locals talia,talia.rosen,t.rosen` on send.acme.com orders talia@, talia.rosen@, and t.rosen@ — all owned by that one person. Requires --sender, because a bare local part carries no name and the vendor's stamp is permanent.")
|
|
16770
16999
|
.option("--mailboxes <json>", "JSON array of mailboxes: [{\"username\",\"first_name\",\"last_name\",\"profile_picture_url\"}], or [{\"username\",\"sender_profile_id\"}] to take the identity from a sender profile per item (mix freely). profile_picture_url is optional and must be a PUBLIC, PERMANENT https URL — the vendor fetches it server-side at provisioning, so an expiring CDN link (e.g. a media.licdn.com URL with e=<epoch>) ships the mailbox faceless. Use `oxygen managed-inboxes upload-avatar <path>` to host one, or --sender / sender_profile_id, whose photo Oxygen already mirrors.")
|
|
@@ -16773,7 +17002,7 @@ Examples:
|
|
|
16773
17002
|
.option("--redirect-url <url>", "Where the domain's web root redirects. Defaults to your workspace's company website; editable later with `oxygen domains forwarding set`.")
|
|
16774
17003
|
.option("--billing <path>", "Path to a JSON file with the registrant/WHOIS contact (first_name, last_name, phone, country, city, state, address_line_one, postal_code). Required on your FIRST order; later orders reuse the registrant already on file.")
|
|
16775
17004
|
.option("--no-warmup", "Order WITHOUT automatic OXYGEN Warm-up. Warm-up is included by default and starts after provisioning under this order approval for the exact quoted Google/Microsoft/Azure addresses; opting out means using BYO warm-up or a later standalone warmup approval.")
|
|
16776
|
-
.option("--
|
|
17005
|
+
.option("--placement", "Add inbox placement to every inbox (optional, billed monthly per inbox from provisioning): inbox-placement (spam) testing, blacklist/reputation monitoring, and authentication checks. Off unless you pass this flag.")
|
|
16777
17006
|
.option("--approved", "Place the order (requires --quote). Without it, a priced preview is returned.")
|
|
16778
17007
|
.option("--quote <id>", "The quote_id from a fresh preview. Required with --approved.")
|
|
16779
17008
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -16808,11 +17037,15 @@ Examples:
|
|
|
16808
17037
|
...(years ? { years: Number(years) } : {}),
|
|
16809
17038
|
...(redirectUrl ? { redirect_url: redirectUrl } : {}),
|
|
16810
17039
|
...(billing ? { billing } : {}),
|
|
16811
|
-
//
|
|
16812
|
-
//
|
|
16813
|
-
//
|
|
16814
|
-
//
|
|
16815
|
-
|
|
17040
|
+
// Warm-up is on unless --no-warmup. Placement is opt-in: it is sent only
|
|
17041
|
+
// when --placement was passed, so an untouched flag leaves the server's
|
|
17042
|
+
// default (off) in force. Older CLIs sent `placement: true` on every
|
|
17043
|
+
// order. The selection is hashed into quote_id, so --approved must
|
|
17044
|
+
// repeat whatever the preview was run with.
|
|
17045
|
+
addons: {
|
|
17046
|
+
warmup: options.warmup !== false,
|
|
17047
|
+
...(options.placement === true ? { placement: true } : {}),
|
|
17048
|
+
},
|
|
16816
17049
|
...(options.approved ? { approved: true } : {}),
|
|
16817
17050
|
...(quote ? { quote_id: quote } : {}),
|
|
16818
17051
|
},
|
|
@@ -16861,7 +17094,7 @@ Examples:
|
|
|
16861
17094
|
});
|
|
16862
17095
|
}))
|
|
16863
17096
|
.addCommand(new Command("list")
|
|
16864
|
-
.description("List the org's managed inbox orders: vendor, platform, live inbox count, lifecycle status, internal billing posture, and each order's FULL monthly cost — the inbox line
|
|
17097
|
+
.description("List the org's managed inbox orders: vendor, platform, live inbox count, lifecycle status, internal billing posture, and each order's FULL monthly cost — the inbox line, each inbox's mailbox connection once inboxes are sold all-in (`orders[].connection`), and the warm-up and inbox-placement add-ons billing per inbox on top of it (`orders[].total_monthly_credits`, `total_monthly_credits` across the workspace). Read-only, 0 Oxygen credits.")
|
|
16865
17098
|
.option("--status <status>", "Filter by status: pending, active, renewing, past_due, expired, cancelled, or failed.")
|
|
16866
17099
|
.option("--json", "Print a JSON envelope.")
|
|
16867
17100
|
.action(async (options) => {
|
|
@@ -16957,7 +17190,7 @@ Examples:
|
|
|
16957
17190
|
});
|
|
16958
17191
|
})));
|
|
16959
17192
|
program.addCommand(new Command("mailboxes")
|
|
16960
|
-
.description("Native email sending pool: register/refresh Google/Microsoft mailboxes (including secure local credential-file transfer), pause
|
|
17193
|
+
.description("Native email sending pool: register/refresh Google/Microsoft mailboxes (including Microsoft SMTP transport and secure local credential-file transfer), pause, disable or delete inboxes, connect EmailGuard monitoring, and inspect/control OXYGEN Warm-up. Fresh managed InboxKit Google/Microsoft/Azure orders activate exact-scope warm-up automatically after provisioning under their approved default-on add-on. Google reuses the managed credential just in time; Microsoft/Azure uses native export. Standalone warm-up plans and credit caps are for BYOK/imported mailboxes, managed opt-outs, or later separate enrollment. OXYGEN Warm-up never owns campaign dispatch. Every inbox here always belongs to a sender profile (the person it sends as) — list, create, and attach those with `oxygen senders profiles`. Safe import preflight: run `oxygen mailboxes compatibility --catalog-only --json`, then `oxygen mailboxes import --file <path> --validate-only --json` (add the credential source flags shown by import help when the file contains credentials).")
|
|
16961
17194
|
.addCommand(new Command("list")
|
|
16962
17195
|
.description("List the org's sending mailboxes with provider, status, warmup state, source (managed = bought through Oxygen, byok = bring-your-own), worker-owned connectionRepair and warmupRepair state, and a pool overview (including counts by source). Read each mailbox's warmupTruth for warm-up (state, last_send, pause, dispatch, next_action with the exact command); warmupState is only the stored rail token and reads `error` for a provider-paused seat. mode=automatic means OXYGEN owns the next bounded retry — do not ask for browser consent or disable/re-enable an existing warm-up seat. To see only Google/Microsoft mailboxes that still need OAuth connection, including attempt budgets and manual remedies, use `oxygen mailboxes oauth-health --json`. Read the fleet from `summary` — `summary.by_warmup`, `by_status`, `by_provider`, `by_auth_mode`, `by_transport`, and `by_source` already aggregate every mailbox, so you never need to iterate the `mailboxes` array to count them. `summary.warmup` adds the normalized pool counts — by_truth_state, by_next_action, enrolled, stalled (enrolled with a measured zero warm-up sends), and needs_action — and `--warmup <states>`, `--next-action <codes>`, and `--stalled` narrow the list to exactly those mailboxes, so \"which of my inboxes are not sending\" is one command rather than a local transform over every row. Without `--json` this prints a readable pool: the counts, then every mailbox that owes an action with its cause and the exact command to run.")
|
|
16963
17196
|
.option("--status <status>", "Filter by status: active, paused, disabled, or provisioning (ordered, still being set up).")
|
|
@@ -17010,6 +17243,7 @@ Examples:
|
|
|
17010
17243
|
if (!result)
|
|
17011
17244
|
return;
|
|
17012
17245
|
if (options.json) {
|
|
17246
|
+
writeBillingNotices("mailboxes list", result);
|
|
17013
17247
|
emitSuccess("mailboxes list", result, options);
|
|
17014
17248
|
return;
|
|
17015
17249
|
}
|
|
@@ -17023,12 +17257,13 @@ Examples:
|
|
|
17023
17257
|
await handleAsyncAction("mailboxes get", options, () => requestOxygen(`/api/cli/mailboxes/${encodeURIComponent(mailbox)}`));
|
|
17024
17258
|
}))
|
|
17025
17259
|
.addCommand(new Command("delete")
|
|
17026
|
-
.description("Preview or approve removal of exact mailboxes from Oxygen at 0 credits. Deletion immediately removes them from sending but never deletes the underlying Google Workspace or Microsoft 365 accounts. It preserves conversation/message/delivery history and stops any separately listed 100-credit connected-mailbox commitment for future renewals; that line is currently built but not charged, and it is not the 100-credit OXYGEN Warm-up subscription. The current period is not refunded. Listed active/paused sequences keep their status but lose these senders and are not automatically paused. Without --cascade, a mailbox whose own warm-up or deliverability monitoring is still connected fails closed with its exact teardown steps; with --cascade, those add-ons are disconnected as part of the delete (their recurring credits stop too) and nothing is deleted unless every teardown is confirmed. A live managed DOMAIN subscription
|
|
17260
|
+
.description("Preview or approve removal of exact mailboxes from Oxygen at 0 credits. Deletion immediately removes them from sending but never deletes the underlying Google Workspace or Microsoft 365 accounts. It preserves conversation/message/delivery history and stops any separately listed 100-credit connected-mailbox commitment for future renewals; that line is currently built but not charged, and it is not the 100-credit OXYGEN Warm-up subscription. The current period is not refunded. Listed active/paused sequences keep their status but lose these senders and are not automatically paused. Without --cascade, a mailbox whose own warm-up or deliverability monitoring is still connected fails closed with its exact teardown steps; with --cascade, those add-ons are disconnected as part of the delete (their recurring credits stop too) and nothing is deleted unless every teardown is confirmed. A live managed DOMAIN subscription covers other mailboxes, so it fails closed unless the whole domain goes too: `oxygen domains delete <domain>`, or --include-domains when every inbox on it is selected.")
|
|
17027
17261
|
.requiredOption("--mailboxes <list>", "Comma-separated mailbox ids or addresses (maximum 500).")
|
|
17028
17262
|
.option("--approved", "Execute the fresh preview. Requires --plan-hash and --confirmation.")
|
|
17029
17263
|
.option("--plan-hash <hash>", "Fresh preview plan_hash.")
|
|
17030
17264
|
.option("--confirmation <phrase>", "Exact confirmation_phrase returned by the fresh preview.")
|
|
17031
17265
|
.option("--cascade", "Disconnect each mailbox's OWN warm-up and deliverability-monitoring add-ons as part of the delete, instead of failing closed until you tear them down by hand. Stops their recurring credits too. Pass it on the preview as well as the approval: the plan hash is bound to it, so an approval cannot turn it on. A live managed domain subscription still fails closed.")
|
|
17266
|
+
.option("--include-domains", "Also delete each domain whose inboxes are ALL selected: an OXYGEN-bought domain is cancelled, your own is archived (DNS and registrar untouched). Partly selected domains are kept and listed in preview.domains_kept. Pass it on the preview as well as the approval.")
|
|
17032
17267
|
.option("--json", "Print a JSON envelope.")
|
|
17033
17268
|
.action(async (options) => {
|
|
17034
17269
|
await handleAsyncAction("mailboxes delete", options, () => {
|
|
@@ -17047,6 +17282,7 @@ Examples:
|
|
|
17047
17282
|
mailboxes,
|
|
17048
17283
|
...(options.approved === true ? { approved: true } : {}),
|
|
17049
17284
|
...(options.cascade === true ? { cascade: true } : {}),
|
|
17285
|
+
...(options.includeDomains === true ? { include_domains: true } : {}),
|
|
17050
17286
|
...(planHash ? { plan_hash: planHash } : {}),
|
|
17051
17287
|
...(confirmation ? { confirmation } : {}),
|
|
17052
17288
|
},
|
|
@@ -17054,7 +17290,7 @@ Examples:
|
|
|
17054
17290
|
});
|
|
17055
17291
|
}))
|
|
17056
17292
|
.addCommand(new Command("health")
|
|
17057
|
-
.description("Fleet email-health: the sending pool rolled up by external deliverability reputation (healthy/degraded/critical/unknown), per-mailbox scores, connected health providers, and DIRECTIONAL recommendations — plus OXYGEN's OWN evidence for the last 7 days, which needs no provider connected: the bounce notifications each mailbox received classified as provider rejection / dead address / mailbox full / delay (a Gmail reputation block shows up as provider_rejected), distinct recipients
|
|
17293
|
+
.description("Fleet email-health: the sending pool rolled up by external deliverability reputation (healthy/degraded/critical/unknown), per-mailbox scores, connected health providers, and DIRECTIONAL recommendations — plus OXYGEN's OWN evidence for the last 7 days, which needs no provider connected: the bounce notifications each mailbox received classified as provider rejection / dead address / mailbox full / delay (a Gmail reputation block shows up as provider_rejected), distinct recipients with a hard bounce in the window (legacy facts retain their first observation time), sequence sends OXYGEN logged (source=sequence only), warm-up day, health score and stop cause, each mailbox's configured daily cap, and the same rollup per sending domain ranked worst-first. Notifications whose body was never stored count as signals.dsn7d.unclassified, which means OXYGEN could not look — not that nothing was wrong. Read health_coverage to see how many mailboxes an external provider has actually scored. Start from the fleet read instead of scanning the mailbox array: warmup.byCause (why warm-up stopped, counted once for the whole pool). Every count is scoped, and the scope is stamped into the payload as signals.window (7d) and signals.sendSource (sequence sends only), on the fleet object and on every mailbox: a 0 means nothing was recorded in that window for that send source, NOT that the fleet is un-blocklisted everywhere. Pure read — 0 credits; never pauses a mailbox or changes a cap.")
|
|
17058
17294
|
.option("--json", "Print a JSON envelope.")
|
|
17059
17295
|
.action(async (options) => {
|
|
17060
17296
|
await handleAsyncAction("mailboxes health", options, () => requestOxygen("/api/cli/mailboxes/health"));
|
|
@@ -17210,7 +17446,7 @@ Examples:
|
|
|
17210
17446
|
.addCommand(new Command("cap")
|
|
17211
17447
|
.description("View and adjust a single mailbox's daily send cap (throttle a problem inbox or ramp a newly warmed one without pausing it).")
|
|
17212
17448
|
.addCommand(new Command("set")
|
|
17213
|
-
.description("Set one sending mailbox's daily send cap (sends per day; new mailboxes get 25). --daily-cap must be a positive whole number; `oxygen limits show` reports the maximum in force.
|
|
17449
|
+
.description("Set one sending mailbox's daily send cap (sends per day; new mailboxes get 25). --daily-cap must be a positive whole number; `oxygen limits show` reports the maximum in force. The separate campaign ramp starts at 5, 10, then 20/day, advancing on completed UTC days with accepted campaign sends. Optional provider warm-up is configured with `mailboxes warmup config`. Consumes no Oxygen credits. <mailbox> accepts a mailbox id or email address.")
|
|
17214
17450
|
.argument("<mailbox>", "Mailbox id or email address.")
|
|
17215
17451
|
.requiredOption("--daily-cap <n>", "New daily send cap (a positive whole number of sends per day).")
|
|
17216
17452
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -17226,15 +17462,15 @@ Examples:
|
|
|
17226
17462
|
});
|
|
17227
17463
|
})))
|
|
17228
17464
|
.addCommand(new Command("ramp")
|
|
17229
|
-
.description("Set or clear
|
|
17465
|
+
.description("Set or clear one mailbox's campaign sending-day ramp. Completed UTC days with accepted campaign sends advance it; optional provider warm-up has separate controls under `mailboxes warmup config`.")
|
|
17230
17466
|
.addCommand(new Command("set")
|
|
17231
|
-
.description("Set one mailbox's
|
|
17467
|
+
.description("Set one mailbox's campaign sending-day ramp: --start-per-day (initial ceiling), --increase-every-days (completed UTC days with accepted campaign sends), --step, --cap. Import age and provider warm-up do not advance it. --clear restores the default 5/10/20 tiers. Free configuration write; provider warm-up uses `mailboxes warmup config`.")
|
|
17232
17468
|
.argument("<mailbox>", "Mailbox id or email address.")
|
|
17233
|
-
.option("--start-per-day <n>", "
|
|
17234
|
-
.option("--increase-every-days <n>", "
|
|
17469
|
+
.option("--start-per-day <n>", "Initial campaign send ceiling (non-negative whole number).")
|
|
17470
|
+
.option("--increase-every-days <n>", "Completed UTC days with accepted campaign sends between increases (whole number >= 1).")
|
|
17235
17471
|
.option("--step <n>", "Sends added to the ceiling each period (non-negative whole number).")
|
|
17236
17472
|
.option("--cap <n>", "The ramp's ceiling; once reached the ramp stops climbing (non-negative whole number).")
|
|
17237
|
-
.option("--clear", "Remove the
|
|
17473
|
+
.option("--clear", "Remove the override and restore the default campaign sending-day tiers.")
|
|
17238
17474
|
.option("--json", "Print a JSON envelope.")
|
|
17239
17475
|
.action(async (mailbox, options) => {
|
|
17240
17476
|
await handleAsyncAction("mailboxes ramp set", options, () => {
|
|
@@ -17273,12 +17509,15 @@ Examples:
|
|
|
17273
17509
|
.option("--display-name <name>", "From display name (e.g. \"Ada from Acme\"). Pass '' to clear it.")
|
|
17274
17510
|
.option("--signature-html <html>", "HTML signature appended to every send from this mailbox. Pass '' to clear it.")
|
|
17275
17511
|
.option("--signature-file <path>", "Read the HTML signature from a file instead of --signature-html.")
|
|
17512
|
+
.option("--bounce-protection <json>", "Mailbox bounce policy (partial updates merge): enabled=true, warning_rate=.02, pause_rate=.03, min_sends=20, window_days=7. Hard bounces / accepted sends in the rolling window; pauses this mailbox. Rates are fractions; sequence protection is separate.")
|
|
17276
17513
|
.option("--json", "Print a JSON envelope.")
|
|
17277
17514
|
.action(async (mailbox, options) => {
|
|
17278
17515
|
await handleAsyncAction("mailboxes update", options, () => {
|
|
17279
17516
|
// Detect PRESENCE (options.x !== undefined), not truthiness, so passing
|
|
17280
17517
|
// an empty string clears the field rather than being dropped.
|
|
17281
17518
|
const patch = {};
|
|
17519
|
+
if (options.bounceProtection !== undefined)
|
|
17520
|
+
patch.bounce_protection = JSON.parse(options.bounceProtection);
|
|
17282
17521
|
if (options.displayName !== undefined)
|
|
17283
17522
|
patch.display_name = options.displayName;
|
|
17284
17523
|
if (options.signatureHtml !== undefined) {
|
|
@@ -17364,7 +17603,7 @@ Examples:
|
|
|
17364
17603
|
.addCommand(new Command("emailguard")
|
|
17365
17604
|
.description("Assess every mailbox origin for EmailGuard and connect credential-capable Google Workspace mailboxes from managed, Zapmail, or compatible encrypted external imports. Manual/native Google rows report credential_required; Microsoft 365/Azure reports vendor_blocked. Preview never materializes a password; approved execution fetches it just in time and never prints it.")
|
|
17366
17605
|
.addCommand(new Command("connect")
|
|
17367
|
-
.description("Preview or connect selected mailboxes to EmailGuard. Managed mode is 100 credits/inbox-month (
|
|
17606
|
+
.description("Preview or connect selected mailboxes to EmailGuard. Managed mode is 100 credits/inbox-month (135 from the 2026-09 repricing's effective date; the preview shows the price in force); BYOK is 0 Oxygen credits. Google Workspace requires a real app password, fetched only after approval. Microsoft 365/Azure is explicitly vendor-blocked because EmailGuard exposes neither Microsoft OAuth nor tenant consent; Oxygen never downgrades it to password auth. Starts with a 0-credit, no-provider-write preview. Account connection sends no placement probe.")
|
|
17368
17607
|
.option("--mailboxes <list>", "Comma-separated mailbox ids or addresses. Omit for the whole pool.")
|
|
17369
17608
|
.option("--approved", "Perform the external EmailGuard account write.")
|
|
17370
17609
|
.option("--plan <hash>", "Exact hash from the fresh preview (required with --approved).")
|
|
@@ -17481,10 +17720,10 @@ Examples:
|
|
|
17481
17720
|
});
|
|
17482
17721
|
}))
|
|
17483
17722
|
.addCommand(new Command("warmup")
|
|
17484
|
-
.description("OXYGEN Warm-up for the sending pool — the weeks of low-volume conversational mail that make a new inbox look trusted before you cold-send from it. It
|
|
17723
|
+
.description("OXYGEN Warm-up for the sending pool — the weeks of low-volume conversational mail that make a new inbox look trusted before you cold-send from it. It costs 100 credits per warming inbox per month ($1), or nothing once it is included in your own mailbox's 100-credit monthly connection (2026-09 repricing); the preview shows which applies. Fresh managed InboxKit Google/Microsoft/Azure orders with the default-on warm-up add-on activate automatically after provisioning under the approved order quote; do not run a second `warmup enable`. Google reuses the InboxKit-held credential only during activation and never stores or returns it; Microsoft/Azure uses exact native export. Standalone preview → approval is for BYOK/imported mailboxes, a managed opt-out, or later separate enrollment. InboxKit auto-export stays off for Microsoft/Azure because Oxygen submits only the exact authorized UIDs, and any non-cancelled InboxKit warm-up blocks either handoff so one mailbox cannot warm twice. OXYGEN Warm-up never owns campaign dispatch: native OXYGEN Sequences still own campaigns and sends. Eligible non-InboxKit Microsoft mailboxes use the separate `mailboxes warmup microsoft` OAuth fallback. Inboxes already warming on the retired TrulyInbox rail keep reporting and are wound down there with `warmup disable`; they are never silently moved to the current managed rail.")
|
|
17485
17724
|
.addHelpText("after", "\nDocs: https://oxygen-agent.com/docs/providers/mailboxes\n")
|
|
17486
17725
|
.addCommand(new Command("enable")
|
|
17487
|
-
.description("STANDALONE ENROLLMENT: enroll BYOK/imported mailboxes, a managed order that opted out, or mailboxes being added to warmup later. Fresh managed InboxKit Google/Microsoft/Azure orders whose approved quote included warmup activate automatically after provisioning and do not need this command. Targets the whole pool unless --mailboxes is given; one synchronous preview/approval is capped at 220 mailboxes, so split larger pools into explicit batches. Without --approved this is a 0-credit, no-provider-write priced preview: nothing is handed off, enrolled, or billed. The
|
|
17726
|
+
.description("Optional provider warm-up defaults to 1/day, +1/day, max 10/day (dedicated-tenant profiles stay stricter); see `mailboxes warmup config` for the effective plan and unsupported OXYGEN Warm-up reply-rate control. This is separate from the 5/10/20 campaign ramp. STANDALONE ENROLLMENT: enroll BYOK/imported mailboxes, a managed order that opted out, or mailboxes being added to warmup later. Fresh managed InboxKit Google/Microsoft/Azure orders whose approved quote included warmup activate automatically after provisioning and do not need this command. Targets the whole pool unless --mailboxes is given; one synchronous preview/approval is capped at 220 mailboxes, so split larger pools into explicit batches. Without --approved this is a 0-credit, no-provider-write priced preview: nothing is handed off, enrolled, or billed. The price is 100 credits per warming mailbox-month, or 0 once warm-up is included in the mailbox's 100-credit monthly connection (2026-09 repricing; the preview's included_in_connection says which, and its max_credits is then 0); the preview returns the exact first-cycle ceiling. Preview one inbox with `oxygen mailboxes warmup enable --mailboxes sender@example.com --json`. Execute by echoing its --plan and --max-credits with --approved. Eligible managed Google targets use the InboxKit-held credential just in time without storing or returning it; Microsoft/Azure targets use native InboxKit Sequencer export with exact UIDs and auto-export off. Any non-cancelled InboxKit warmup must be cancelled before either handoff (pausing is not enough). Only eligible non-InboxKit Microsoft mailboxes need `mailboxes warmup microsoft`. OXYGEN Warm-up retains no campaign authority — OXYGEN Sequences own enrollment and dispatch. New enrollments only ever land on the managed OXYGEN rail; the retired TrulyInbox rail is refused here and only accepts teardown.")
|
|
17488
17727
|
.addHelpText("after", "\nDocs: https://oxygen-agent.com/docs/providers/mailboxes\n")
|
|
17489
17728
|
.option("--mailboxes <list>", "Comma-separated mailbox ids or addresses to warm. Omit to warm the whole pool.")
|
|
17490
17729
|
.option("--approved", "Execute the PRICED enrollment. Without it the response is a priced preview and nothing is enrolled or billed.")
|
|
@@ -17571,13 +17810,13 @@ Examples:
|
|
|
17571
17810
|
});
|
|
17572
17811
|
}))
|
|
17573
17812
|
.addCommand(new Command("config")
|
|
17574
|
-
.description("
|
|
17813
|
+
.description("Read or override optional provider warm-up traffic, separate from the campaign ramp. Standard defaults: start 1/day, add 1/day, maximum 10/day; dedicated-tenant profiles stay stricter. No flags reads the effective plan. Override with all four knobs, or --reset for defaults. OXYGEN Warm-up does not support reply-rate control: the value is saved but not applied there. Active warm-up syncs immediately without re-enrollment or rebilling; provider_sync reports the result. 0 Oxygen credits.")
|
|
17575
17814
|
.argument("<mailbox>", "Mailbox id or email address.")
|
|
17576
17815
|
.option("--initial-per-day <n>", "Warmup emails sent on day one.")
|
|
17577
17816
|
.option("--increase-per-day <n>", "Warmup emails added to the daily volume each day.")
|
|
17578
17817
|
.option("--max-per-day <n>", "The warmup daily ceiling; climbing stops here.")
|
|
17579
|
-
.option("--reply-rate <percent>", "
|
|
17580
|
-
.option("--reset", "Drop the override and
|
|
17818
|
+
.option("--reply-rate <percent>", "Requested warm-up reply rate (0-100); saved but unsupported by OXYGEN Warm-up.")
|
|
17819
|
+
.option("--reset", "Drop the override and restore the default provider warm-up plan.")
|
|
17581
17820
|
.option("--json", "Print a JSON envelope.")
|
|
17582
17821
|
.action(async (mailbox, options) => {
|
|
17583
17822
|
await handleAsyncAction("mailboxes warmup config", options, () => {
|
|
@@ -17617,7 +17856,7 @@ Examples:
|
|
|
17617
17856
|
});
|
|
17618
17857
|
}))
|
|
17619
17858
|
.addCommand(new Command("profile")
|
|
17620
|
-
.description("Pin the SENDING ARCHITECTURE a domain is run as, which every inbox on it inherits: `standard` (a few mailboxes on a real business tenant, each climbing to a real number), `dedicated_tenant` (infrastructure tenant, volume from breadth, per-inbox number stays small), or `entry` (first-touch inboxes that need presence, not volume). What you state is the DOMAIN's daily budget; OXYGEN divides it by how many inboxes share that domain, so adding an inbox lowers the others instead of raising the domain's total — the thing a flat per-mailbox cap cannot do. A profile is a CEILING: it never raises a cap above what you configured, and the strictest of {configured cap, per-sequence override,
|
|
17859
|
+
.description("Pin the SENDING ARCHITECTURE a domain is run as, which every inbox on it inherits: `standard` (a few mailboxes on a real business tenant, each climbing to a real number), `dedicated_tenant` (infrastructure tenant, volume from breadth, per-inbox number stays small), or `entry` (first-touch inboxes that need presence, not volume). What you state is the DOMAIN's daily budget; OXYGEN divides it by how many inboxes share that domain, so adding an inbox lowers the others instead of raising the domain's total — the thing a flat per-mailbox cap cannot do. A profile is a CEILING: it never raises a cap above what you configured, and the strictest of {configured cap, per-sequence override, sending-day ramp, profile share} still wins. 0 credits, no provider call. Without --approved this previews the before/after per domain. Omit every selector and profile to list the available profiles.")
|
|
17621
17860
|
.addHelpText("after", "\nDocs: https://oxygen-agent.com/docs/providers/mailboxes\n")
|
|
17622
17861
|
// NOT --profile: the program declares a GLOBAL --profile (the stored
|
|
17623
17862
|
// CLI credential profile), and Commander resolves the value to that
|
|
@@ -17629,7 +17868,7 @@ Examples:
|
|
|
17629
17868
|
.option("--all", "Bind every domain that currently holds a mailbox.")
|
|
17630
17869
|
.option("--cold-send-per-day <n>", "Override the profile's DOMAIN budget for cold sends per day (not a per-inbox number).")
|
|
17631
17870
|
.option("--warmup-per-day <n>", "Override the profile's DOMAIN budget for warm-up emails per day (not a per-inbox number).")
|
|
17632
|
-
.option("--clear", "Remove the profile, restoring inference and the
|
|
17871
|
+
.option("--clear", "Remove the profile, restoring inference and the sending-day ramp alone.")
|
|
17633
17872
|
.option("--approved", "Apply the previewed change.")
|
|
17634
17873
|
.option("--json", "Print a JSON envelope.")
|
|
17635
17874
|
.action(async (options) => {
|
|
@@ -17841,7 +18080,7 @@ Examples:
|
|
|
17841
18080
|
}));
|
|
17842
18081
|
}))
|
|
17843
18082
|
.addCommand(new Command("list")
|
|
17844
|
-
.description("Inspect recent directional inbox-placement tests, their status, results, and credits used. The returned web link opens Accounts; use get for an exact result link. Costs 0 credits and never creates or sends a test; active tests may refresh their observations.")
|
|
18083
|
+
.description("Inspect recent directional inbox-placement tests, their status, results, and credits used, plus `pricing`: the EmailGuard monitoring credits per inbox-month and the managed test price in force (0 on your own EmailGuard key). The returned web link opens Accounts; use get for an exact result link. Costs 0 credits and never creates or sends a test; active tests may refresh their observations.")
|
|
17845
18084
|
.option("--limit <n>", "Max rows (default 50, cap 200).")
|
|
17846
18085
|
.option("--json", "Print a JSON envelope.")
|
|
17847
18086
|
.action(async (options) => {
|
|
@@ -17858,7 +18097,7 @@ Examples:
|
|
|
17858
18097
|
await handleAsyncAction("deliverability placement-test get", options, () => requestOxygen(`/api/cli/deliverability/placement-tests/${encodeURIComponent(id)}`));
|
|
17859
18098
|
}))));
|
|
17860
18099
|
program.addCommand(new Command("domains")
|
|
17861
|
-
.description("Cold-email
|
|
18100
|
+
.description("Cold-email domains: sync zones from your own Cloudflare account (BYOK), inspect age/warmup/DNS health, check availability and pricing, buy domains, and archive or delete domains with their inboxes (OXYGEN-bought domains included). Purchases bill your Cloudflare payment method, never Oxygen credits.")
|
|
17862
18101
|
.addCommand(new Command("list")
|
|
17863
18102
|
.description("List the org's cached Cloudflare domains with cold-email metadata (age, mailboxes, warmup, sending volume, DNS health). Each row's `dns` object explains the status (ok, issues, not checked, check failed, partial check, no zone) with a reason and fix path; `managed_domains` lists InboxKit-managed bundle domains (vendor-registered + vendor-DNS'd — manage via `managed-inboxes`), each carrying `internal_billing_status` (ok|past_due) and `last_billed_cycle_key` — a domain the vendor keeps live can still be one OXYGEN could not renew. Reads the cache only — run `domains sync` to refresh.")
|
|
17864
18103
|
.option("--status <status>", "Filter by zone status: unknown, initializing, pending, active, moved, or deleted.")
|
|
@@ -17918,6 +18157,35 @@ Examples:
|
|
|
17918
18157
|
method: "POST",
|
|
17919
18158
|
body: { archived: false },
|
|
17920
18159
|
}));
|
|
18160
|
+
}))
|
|
18161
|
+
.addCommand(new Command("delete")
|
|
18162
|
+
.description("Delete domains and every inbox on them: cancels OXYGEN inbox orders; a domain bought through Oxygen on its own (`domains buy`) stops renewing and leaves the workspace, staying registered until expiry; archives your own (DNS and registrar untouched). The preview lists every inbox, each domain's action and the recurring credits that stop; 0 credits, and the current period is not refunded.")
|
|
18163
|
+
.argument("<domains...>", "Domain names, space- or comma-separated.")
|
|
18164
|
+
.option("--approved", "Execute the fresh preview. Requires --plan-hash and --confirmation.")
|
|
18165
|
+
.option("--plan-hash <hash>", "Fresh preview plan_hash.")
|
|
18166
|
+
.option("--confirmation <phrase>", "Exact confirmation_phrase returned by the fresh preview.")
|
|
18167
|
+
.option("--json", "Print a JSON envelope.")
|
|
18168
|
+
.action(async (domainArgs, options) => {
|
|
18169
|
+
await handleAsyncAction("domains delete", options, () => {
|
|
18170
|
+
const domains = domainArgs.flatMap((value) => splitCommaList(value));
|
|
18171
|
+
if (domains.length === 0) {
|
|
18172
|
+
throw new OxygenError("invalid_request", "Name at least one domain, such as acme.com. `oxygen domains list` shows your domains.", { exitCode: 2 });
|
|
18173
|
+
}
|
|
18174
|
+
const planHash = readOption(options.planHash);
|
|
18175
|
+
const confirmation = readOption(options.confirmation);
|
|
18176
|
+
if (options.approved === true && (!planHash || !confirmation)) {
|
|
18177
|
+
throw new OxygenError("invalid_request", "--approved requires --plan-hash and --confirmation from a fresh deletion preview.", { exitCode: 2 });
|
|
18178
|
+
}
|
|
18179
|
+
return requestOxygen("/api/cli/domains/delete", {
|
|
18180
|
+
method: "POST",
|
|
18181
|
+
body: {
|
|
18182
|
+
domains,
|
|
18183
|
+
...(options.approved === true ? { approved: true } : {}),
|
|
18184
|
+
...(planHash ? { plan_hash: planHash } : {}),
|
|
18185
|
+
...(confirmation ? { confirmation } : {}),
|
|
18186
|
+
},
|
|
18187
|
+
});
|
|
18188
|
+
});
|
|
17921
18189
|
}))
|
|
17922
18190
|
.addCommand(new Command("tag")
|
|
17923
18191
|
.description("Replace a domain's workspace tags (whole set; `--tags \"\"` clears) — e.g. pool the domains behind one campaign tag. See `oxygen tags list` for the vocabulary. A later `domains sync` never clobbers them.")
|
|
@@ -18047,7 +18315,7 @@ Examples:
|
|
|
18047
18315
|
await handleAsyncAction("domains add", options, () => requestOxygen("/api/cli/domains/add", { method: "POST", body: { domain } }));
|
|
18048
18316
|
}))
|
|
18049
18317
|
.addCommand(new Command("search")
|
|
18050
|
-
.description("
|
|
18318
|
+
.description("Search available domains with their price. Free; nothing is purchased. With your own Cloudflare account connected it searches your Registrar (USD, your card); otherwise it lists names Oxygen can register, priced in credits at the registrar's cost per year (a .com is 1,250).")
|
|
18051
18319
|
.argument("<query>", "Search text, such as a brand or keyword.")
|
|
18052
18320
|
.option("--json", "Print a JSON envelope.")
|
|
18053
18321
|
.action(async (query, options) => {
|
|
@@ -18057,13 +18325,13 @@ Examples:
|
|
|
18057
18325
|
});
|
|
18058
18326
|
}))
|
|
18059
18327
|
.addCommand(new Command("check")
|
|
18060
|
-
.description("
|
|
18328
|
+
.description("Check availability and price for up to 20 exact domains. Free; nothing is purchased. With your own Cloudflare account connected it checks your Registrar (USD, your card); otherwise the names Oxygen can register, in credits at the registrar's cost per year (a .com is 1,250). A domain you already own through Oxygen comes back with reason `owned`.")
|
|
18061
18329
|
.argument("<domains...>", "Domain names to check (max 20).")
|
|
18062
18330
|
.option("--json", "Print a JSON envelope.")
|
|
18063
18331
|
.action(async (domains, options) => {
|
|
18064
18332
|
await handleAsyncAction("domains check", options, () => {
|
|
18065
18333
|
if (domains.length > DOMAINS_CHECK_MAX_DOMAINS) {
|
|
18066
|
-
throw new Error(`
|
|
18334
|
+
throw new Error(`domains check takes at most ${DOMAINS_CHECK_MAX_DOMAINS} domains per call (got ${domains.length}). Split the list and re-run.`);
|
|
18067
18335
|
}
|
|
18068
18336
|
return requestOxygen("/api/cli/domains/check", {
|
|
18069
18337
|
method: "POST",
|
|
@@ -18072,17 +18340,37 @@ Examples:
|
|
|
18072
18340
|
});
|
|
18073
18341
|
}))
|
|
18074
18342
|
.addCommand(new Command("buy")
|
|
18075
|
-
.description("
|
|
18343
|
+
.description("Buy sending domains. NON-REFUNDABLE. Without --approved, returns a free priced preview plus quote_id. Without your own Cloudflare account, Oxygen registers the domain and charges credits at the registrar's cost for one year (a .com is 1,250), then renews it yearly at the renewal cost, charged 30 days before expiry (turn off with `domains renewal <domain> --off`). With your own Cloudflare account connected, it buys through your Registrar on your Cloudflare card for 0 credits. For a domain with inboxes use `managed-inboxes subscribe`.")
|
|
18076
18344
|
.argument("<domains...>", "Domain names to buy.")
|
|
18077
18345
|
.option("--approved", "Execute the purchase. Without this flag, returns a preview only.")
|
|
18078
18346
|
.option("--quote <id>", "Quote id from the preview (required with --approved).")
|
|
18079
18347
|
.option("--accept-premium", "Acknowledge premium-tier pricing for premium domains.")
|
|
18080
|
-
.option("--auto-renew", "
|
|
18348
|
+
.option("--auto-renew", "Renew the new registrations yearly (the default).")
|
|
18349
|
+
.option("--no-auto-renew", "Do not renew: the domain lapses when its year ends.")
|
|
18350
|
+
.option("--billing <path>", "JSON file with the registrant (WHOIS) contact: first_name, last_name, phone, country, city, state, address_line_one, postal_code. Needed once, when the workspace has none on file.")
|
|
18351
|
+
.option("--redirect-url <url>", "Where the domain's web root forwards visitors. Defaults to your company website.")
|
|
18081
18352
|
.option("--no-privacy", "Disable WHOIS privacy/redaction on the new registrations.")
|
|
18082
18353
|
.option("--wait", "After an approved purchase, poll each pending registration to a terminal state (up to 10 minutes per domain).")
|
|
18083
18354
|
.option("--json", "Print a JSON envelope.")
|
|
18084
18355
|
.action(async (domains, options) => {
|
|
18085
18356
|
await handleAsyncAction("domains buy", options, () => runDomainsBuy(domains, options));
|
|
18357
|
+
}))
|
|
18358
|
+
.addCommand(new Command("renewal")
|
|
18359
|
+
.description("Turn the yearly renewal of a domain bought through Oxygen on or off. No charge. On: the registrar's renewal cost is charged in credits 30 days before expiry. Off: the domain lapses when it expires.")
|
|
18360
|
+
.argument("<domain>", "A domain from `domains list` (registered_domains).")
|
|
18361
|
+
.option("--on", "Renew yearly.")
|
|
18362
|
+
.option("--off", "Stop renewing; the domain lapses at expiry.")
|
|
18363
|
+
.option("--json", "Print a JSON envelope.")
|
|
18364
|
+
.action(async (domain, options) => {
|
|
18365
|
+
await handleAsyncAction("domains renewal", options, () => {
|
|
18366
|
+
if (Boolean(options.on) === Boolean(options.off)) {
|
|
18367
|
+
throw new Error("Pass exactly one of --on or --off.");
|
|
18368
|
+
}
|
|
18369
|
+
return requestOxygen(`/api/cli/domains/${encodeURIComponent(domain)}/renewal`, {
|
|
18370
|
+
method: "POST",
|
|
18371
|
+
body: { auto_renew: options.on === true },
|
|
18372
|
+
});
|
|
18373
|
+
});
|
|
18086
18374
|
}))
|
|
18087
18375
|
.addCommand(new Command("adopt")
|
|
18088
18376
|
.description("STAFF LEGACY RECOVERY: attach domains that already exist in the shared Oxygen-managed Cloudflare account to their workspace. FREE; no purchase. New managed domain + inbox bundles use InboxKit. Without --yes, previews only.")
|
|
@@ -18395,6 +18683,8 @@ Run completion:
|
|
|
18395
18683
|
},
|
|
18396
18684
|
}), options);
|
|
18397
18685
|
writeScheduleWarnings(data);
|
|
18686
|
+
if (!options.json)
|
|
18687
|
+
writeWorkflowPlanLimitNotices(data);
|
|
18398
18688
|
return data;
|
|
18399
18689
|
});
|
|
18400
18690
|
}))
|
|
@@ -18493,8 +18783,10 @@ Run completion:
|
|
|
18493
18783
|
method: "POST",
|
|
18494
18784
|
body: { workflow },
|
|
18495
18785
|
});
|
|
18496
|
-
if (!options.json)
|
|
18786
|
+
if (!options.json) {
|
|
18497
18787
|
writeDisabledWorkflowNotices(data);
|
|
18788
|
+
writeWorkflowPlanLimitNotices(data);
|
|
18789
|
+
}
|
|
18498
18790
|
return prepareWorkflowCliOutput(data, options);
|
|
18499
18791
|
});
|
|
18500
18792
|
}))
|
|
@@ -18579,7 +18871,7 @@ Run completion:
|
|
|
18579
18871
|
});
|
|
18580
18872
|
}))
|
|
18581
18873
|
.addCommand(new Command("webhooks")
|
|
18582
|
-
.description("Workflow webhook trigger utilities.")
|
|
18874
|
+
.description("Workflow webhook trigger utilities. A trigger's inbound URL and the deliveries a minute it accepts (`rate_limit`) are in `oxygen workflows get <workflow>`.")
|
|
18583
18875
|
.addCommand(new Command("rotate")
|
|
18584
18876
|
.description("Replace a workflow webhook's shared secret. Previews by default; the old sender secret stops working immediately after approval.")
|
|
18585
18877
|
.argument("<workflow>", "Webhook workflow id, slug, or name.")
|
|
@@ -20595,6 +20887,14 @@ function applyAiColumnConfig(definition, options) {
|
|
|
20595
20887
|
// these against the tenant-db unions so the next stale rebase fails the gate
|
|
20596
20888
|
// instead of reaching a customer.
|
|
20597
20889
|
const RESEARCH_DEPTHS = new Set(["quick", "standard", "deep"]);
|
|
20890
|
+
// A quick research row pays the tier's model price plus the typical grounding
|
|
20891
|
+
// read; derived from the pricing sheet so a re-price cannot orphan the help.
|
|
20892
|
+
function quickResearchRowCredits(level) {
|
|
20893
|
+
return Math.round((AI_COLUMN_CREDITS[level] + AI_COLUMN_WEB_SEARCH_TYPICAL_CREDITS) * 10) / 10;
|
|
20894
|
+
}
|
|
20895
|
+
const REASONING_LEVEL_OPTION_HELP = "AI column reasoning level: low, medium, or high (shown in the web UI as Oxygen Fast, Oxygen Balanced, and Oxygen Max). "
|
|
20896
|
+
+ `On a research column it applies only at --research-depth quick (typical row: low about ${quickResearchRowCredits("low")}, `
|
|
20897
|
+
+ `medium about ${quickResearchRowCredits("medium")}, high about ${quickResearchRowCredits("high")} credits); standard and deep choose their own tier and ignore it.`;
|
|
20598
20898
|
const RESEARCH_DEPTH_OPTION_HELP = "Research columns: how hard each row searches. quick: one search and one page read (about 2.1 credits/row on the default tier; --reasoning-level applies to quick only). standard: the agent plans a few searches and page reads on Oxygen Fast (about 5.5; the default for new columns). deep: more searches and subpages on Oxygen Balanced (about 10). Standard and deep cost the same whatever --reasoning-level says. Each row reserves a higher maximum; `columns run --dry-run` quotes this column's typical and max.";
|
|
20599
20899
|
/** `--research-depth`, validated before the round trip; null when not passed. */
|
|
20600
20900
|
function readResearchDepthOption(value) {
|
|
@@ -22260,6 +22560,13 @@ async function importRows(table, options) {
|
|
|
22260
22560
|
}
|
|
22261
22561
|
async function importRowsFromFile(table, options) {
|
|
22262
22562
|
const format = normalizeRowsFormat(options.format, inferRowsFileFormat(options.file));
|
|
22563
|
+
// Refuse a JSON or XLSX file too large to hold in memory before reading it,
|
|
22564
|
+
// and send a large CSV or JSONL file down the streamed path, which never does.
|
|
22565
|
+
const fileBytes = statSync(options.file).size;
|
|
22566
|
+
assertImportFormatWithinBufferedLimit(format, fileBytes);
|
|
22567
|
+
if (shouldStreamFileImport(format, fileBytes)) {
|
|
22568
|
+
return withImportWaitNextStep(await importLargeFileStreamed(table, options, format, fileBytes));
|
|
22569
|
+
}
|
|
22263
22570
|
const parsedRows = await readRowsFile(options.file, format, options.sheet);
|
|
22264
22571
|
if (parsedRows.length === 0) {
|
|
22265
22572
|
throw new OxygenError("invalid_rows", "Import file did not contain any rows.", {
|
|
@@ -22278,9 +22585,13 @@ async function importRowsFromFile(table, options) {
|
|
|
22278
22585
|
// Prefer the object-storage fast path: upload the raw file once via a
|
|
22279
22586
|
// presigned URL (no request-body limit) so the worker COPY-loads it.
|
|
22280
22587
|
// Falls back to the inline multipart import when storage is unconfigured.
|
|
22281
|
-
const
|
|
22588
|
+
const fileBuffer = readFileSync(options.file);
|
|
22589
|
+
const staged = await tryEnqueueStagedFileImport(table, options, format, {
|
|
22590
|
+
byteLength: fileBuffer.byteLength,
|
|
22591
|
+
read: () => Promise.resolve({ targetRows: parsedRows, rowCount: parsedRows.length, sha256: sourceHash }),
|
|
22592
|
+
put: (uploadUrl, contentType) => putImportFile(uploadUrl, fileBuffer, contentType),
|
|
22593
|
+
}, {
|
|
22282
22594
|
autoBackground: !options.background,
|
|
22283
|
-
sourceHash,
|
|
22284
22595
|
batchSize,
|
|
22285
22596
|
}, preparedBackgroundTarget);
|
|
22286
22597
|
if (staged)
|
|
@@ -22565,7 +22876,7 @@ function isRequestTooLargeError(error) {
|
|
|
22565
22876
|
return error instanceof OxygenError && error.code === "request_too_large";
|
|
22566
22877
|
}
|
|
22567
22878
|
async function tryEnqueueStagedFileImport(// skipcq: JS-R1005
|
|
22568
|
-
table, options, format,
|
|
22879
|
+
table, options, format, file, context, preparedTarget = null) {
|
|
22569
22880
|
if (options.create && table) {
|
|
22570
22881
|
throw new OxygenError("invalid_import_target", "Pass either a table argument or --create, not both.", {
|
|
22571
22882
|
exitCode: 1,
|
|
@@ -22576,7 +22887,6 @@ table, options, format, parsedRows, context, preparedTarget = null) {
|
|
|
22576
22887
|
exitCode: 1,
|
|
22577
22888
|
});
|
|
22578
22889
|
}
|
|
22579
|
-
const fileBuffer = readFileSync(options.file);
|
|
22580
22890
|
const filename = basename(options.file);
|
|
22581
22891
|
const contentType = importFileContentType(format);
|
|
22582
22892
|
const traceId = randomUUID();
|
|
@@ -22590,7 +22900,8 @@ table, options, format, parsedRows, context, preparedTarget = null) {
|
|
|
22590
22900
|
body: {
|
|
22591
22901
|
file_name: filename,
|
|
22592
22902
|
content_type: contentType,
|
|
22593
|
-
byte_length:
|
|
22903
|
+
byte_length: file.byteLength,
|
|
22904
|
+
format,
|
|
22594
22905
|
trace_id: traceId,
|
|
22595
22906
|
},
|
|
22596
22907
|
});
|
|
@@ -22605,10 +22916,15 @@ table, options, format, parsedRows, context, preparedTarget = null) {
|
|
|
22605
22916
|
let presigned = await requestPresign();
|
|
22606
22917
|
if (!presigned)
|
|
22607
22918
|
return null;
|
|
22919
|
+
const contents = await file.read();
|
|
22608
22920
|
// Create the table for --create (or resolve the existing ref) before the
|
|
22609
22921
|
// upload so a presign success always pairs with a real target.
|
|
22610
|
-
const target = preparedTarget ?? await prepareImportTarget(table, options,
|
|
22611
|
-
|
|
22922
|
+
const target = preparedTarget ?? await prepareImportTarget(table, options, contents.targetRows);
|
|
22923
|
+
const put = (current) => {
|
|
22924
|
+
const uploadUrl = readRecordString(current, "upload_url");
|
|
22925
|
+
return uploadUrl ? file.put(uploadUrl, contentType) : Promise.resolve(new Response(null, { status: 500 }));
|
|
22926
|
+
};
|
|
22927
|
+
let putResponse = await put(presigned);
|
|
22612
22928
|
// A direct-upload 403 most commonly means a stale/mismatched signature.
|
|
22613
22929
|
// Refresh once under the same client trace so a transient presign never
|
|
22614
22930
|
// strands a durable import, while persistent policy failures stay explicit.
|
|
@@ -22616,7 +22932,7 @@ table, options, format, parsedRows, context, preparedTarget = null) {
|
|
|
22616
22932
|
const refreshed = await requestPresign();
|
|
22617
22933
|
if (refreshed) {
|
|
22618
22934
|
presigned = refreshed;
|
|
22619
|
-
putResponse = await
|
|
22935
|
+
putResponse = await put(presigned);
|
|
22620
22936
|
}
|
|
22621
22937
|
}
|
|
22622
22938
|
if (!putResponse.ok) {
|
|
@@ -22637,9 +22953,9 @@ table, options, format, parsedRows, context, preparedTarget = null) {
|
|
|
22637
22953
|
storage_key: storageKey,
|
|
22638
22954
|
format,
|
|
22639
22955
|
file_name: filename,
|
|
22640
|
-
byte_length:
|
|
22641
|
-
sha256:
|
|
22642
|
-
row_count:
|
|
22956
|
+
byte_length: file.byteLength,
|
|
22957
|
+
sha256: contents.sha256,
|
|
22958
|
+
row_count: contents.rowCount,
|
|
22643
22959
|
batch_size: context.batchSize,
|
|
22644
22960
|
...(readPositiveInt(options.maxConcurrency)
|
|
22645
22961
|
? { max_concurrency: readPositiveInt(options.maxConcurrency) }
|
|
@@ -22659,10 +22975,42 @@ table, options, format, parsedRows, context, preparedTarget = null) {
|
|
|
22659
22975
|
storage_provider: storageProvider,
|
|
22660
22976
|
};
|
|
22661
22977
|
}
|
|
22662
|
-
|
|
22663
|
-
|
|
22664
|
-
|
|
22665
|
-
|
|
22978
|
+
/**
|
|
22979
|
+
* A CSV or JSONL file over STREAMED_FILE_IMPORT_MIN_BYTES: never read whole.
|
|
22980
|
+
* One streaming pass counts, hashes and collects its columns, the upload streams
|
|
22981
|
+
* from disk, and the import always runs in the background. There is no inline
|
|
22982
|
+
* fallback: a file this large cannot travel in one API request.
|
|
22983
|
+
*/
|
|
22984
|
+
async function importLargeFileStreamed(table, options, format, fileBytes) {
|
|
22985
|
+
if (options.sync) {
|
|
22986
|
+
process.stderr.write(`note: files over ${Math.round(STREAMED_FILE_IMPORT_MIN_BYTES / (1024 * 1024))} MiB always import in the background; --sync is ignored\n`);
|
|
22987
|
+
}
|
|
22988
|
+
const staged = await tryEnqueueStagedFileImport(table, options, format, {
|
|
22989
|
+
byteLength: fileBytes,
|
|
22990
|
+
read: async () => {
|
|
22991
|
+
process.stderr.write(`note: reading ${basename(options.file)} (${formatMebibytes(fileBytes)}) to count its rows before uploading\n`);
|
|
22992
|
+
const scan = await scanStreamedImportFile(options.file, format);
|
|
22993
|
+
if (scan.rowCount === 0) {
|
|
22994
|
+
throw new OxygenError("invalid_rows", "Import file did not contain any rows.", { exitCode: 1 });
|
|
22995
|
+
}
|
|
22996
|
+
return { targetRows: rowsForStreamedImportTarget(scan), rowCount: scan.rowCount, sha256: scan.sha256 };
|
|
22997
|
+
},
|
|
22998
|
+
put: (uploadUrl, contentType) => putImportFileStream({ uploadUrl, path: options.file, byteLength: fileBytes, contentType }),
|
|
22999
|
+
}, {
|
|
23000
|
+
autoBackground: !options.background,
|
|
23001
|
+
batchSize: normalizeImportBatchSize(options.batchSize),
|
|
23002
|
+
});
|
|
23003
|
+
if (staged)
|
|
23004
|
+
return { ...staged, streamed_upload: true };
|
|
23005
|
+
throw new OxygenError("object_storage_not_configured", `Files over ${Math.round(STREAMED_FILE_IMPORT_MIN_BYTES / (1024 * 1024))} MiB upload straight to file storage, which this OXYGEN server has not configured.`, {
|
|
23006
|
+
details: { file_bytes: fileBytes, streamed_import_min_bytes: STREAMED_FILE_IMPORT_MIN_BYTES },
|
|
23007
|
+
exitCode: 1,
|
|
23008
|
+
});
|
|
23009
|
+
}
|
|
23010
|
+
function formatMebibytes(bytes) {
|
|
23011
|
+
return `${Math.round(bytes / (1024 * 1024)).toLocaleString("en-US")} MiB`;
|
|
23012
|
+
}
|
|
23013
|
+
async function putImportFile(uploadUrl, fileBuffer, contentType) {
|
|
22666
23014
|
const controller = new AbortController();
|
|
22667
23015
|
const timer = setTimeout(() => controller.abort(), 300_000);
|
|
22668
23016
|
try {
|
|
@@ -22755,7 +23103,8 @@ function buildGetSurfaceRunWaitConfig(params) {
|
|
|
22755
23103
|
requestedIntervalSeconds: params.requestedIntervalSeconds,
|
|
22756
23104
|
defaultTimeoutSeconds: params.defaultTimeoutSeconds,
|
|
22757
23105
|
defaultIntervalSeconds: params.defaultIntervalSeconds,
|
|
22758
|
-
|
|
23106
|
+
retryTransientNetworkErrors: params.retryTransientNetworkErrors === true,
|
|
23107
|
+
fetchRun: (signal) => requestOxygen(`${params.endpoint}/${encodeURIComponent(params.runId)}`, signal ? { signal } : {}),
|
|
22759
23108
|
isTerminal: params.isTerminal,
|
|
22760
23109
|
shapeTerminal: (run, status, polls, elapsedMs) => {
|
|
22761
23110
|
const webUrl = readRecordString(run, "web_url");
|
|
@@ -22778,6 +23127,7 @@ function waitForTableIngestionRun(runId, options) {
|
|
|
22778
23127
|
return waitForCliRun(buildGetSurfaceRunWaitConfig({
|
|
22779
23128
|
runId,
|
|
22780
23129
|
endpoint: "/api/cli/table-ingestion-runs",
|
|
23130
|
+
retryTransientNetworkErrors: true,
|
|
22781
23131
|
requestedTimeoutSeconds: options.timeoutSeconds,
|
|
22782
23132
|
requestedIntervalSeconds: options.intervalSeconds,
|
|
22783
23133
|
defaultTimeoutSeconds: TABLE_INGESTION_WAIT_DEFAULT_TIMEOUT_SECONDS,
|
|
@@ -22786,7 +23136,7 @@ function waitForTableIngestionRun(runId, options) {
|
|
|
22786
23136
|
runKey: "ingestionRun",
|
|
22787
23137
|
runIdKey: "ingestionRunId",
|
|
22788
23138
|
timeoutCode: "table_ingestion_wait_timeout",
|
|
22789
|
-
timeoutMessage: "Timed out waiting for table ingestion run to finish.",
|
|
23139
|
+
timeoutMessage: "Timed out waiting for table ingestion run to finish. Check its latest status with `oxygen table-ingestions get <run_id>`.",
|
|
22790
23140
|
timeoutDetailIdKey: "ingestion_run_id",
|
|
22791
23141
|
}));
|
|
22792
23142
|
}
|
|
@@ -22881,11 +23231,17 @@ async function runDomainsBuy(domains, options) {
|
|
|
22881
23231
|
if (options.approved && !quoteId) {
|
|
22882
23232
|
throw new Error("--quote <id> is required with --approved. Run the same command without --approved first to get the preview and its quote_id.");
|
|
22883
23233
|
}
|
|
23234
|
+
const billingPath = readOption(options.billing);
|
|
23235
|
+
const billing = billingPath ? readJsonFileValue(resolve(billingPath), "--billing") : undefined;
|
|
23236
|
+
const redirectUrl = readOption(options.redirectUrl);
|
|
22884
23237
|
const data = await requestOxygen("/api/cli/domains/buy", {
|
|
22885
23238
|
method: "POST",
|
|
22886
23239
|
body: {
|
|
22887
23240
|
domains,
|
|
22888
|
-
...(options.autoRenew ? { auto_renew: true } : {}),
|
|
23241
|
+
...(options.autoRenew === true ? { auto_renew: true } : {}),
|
|
23242
|
+
...(options.autoRenew === false ? { auto_renew: false } : {}),
|
|
23243
|
+
...(billing ? { billing } : {}),
|
|
23244
|
+
...(redirectUrl ? { redirect_url: redirectUrl } : {}),
|
|
22889
23245
|
...(options.privacy === false ? { privacy: false } : {}),
|
|
22890
23246
|
...(options.approved ? { approved: true, quote_id: quoteId } : {}),
|
|
22891
23247
|
...(options.acceptPremium ? { accept_premium_pricing: true } : {}),
|
|
@@ -22928,7 +23284,9 @@ function domainsBuyRerunCommand(domains, quoteId, options, data) {
|
|
|
22928
23284
|
"--approved",
|
|
22929
23285
|
`--quote ${quoteId}`,
|
|
22930
23286
|
...(needsPremiumAck ? ["--accept-premium"] : []),
|
|
22931
|
-
...(options.autoRenew ? ["--auto-renew"] : []),
|
|
23287
|
+
...(options.autoRenew === false ? ["--no-auto-renew"] : []),
|
|
23288
|
+
...(readOption(options.billing) ? [`--billing ${readOption(options.billing)}`] : []),
|
|
23289
|
+
...(readOption(options.redirectUrl) ? [`--redirect-url ${readOption(options.redirectUrl)}`] : []),
|
|
22932
23290
|
...(options.privacy === false ? ["--no-privacy"] : []),
|
|
22933
23291
|
];
|
|
22934
23292
|
return `${resolveCliBinaryName()} domains buy ${domains.join(" ")} ${flags.join(" ")}`;
|
|
@@ -23978,9 +24336,11 @@ function tableWebUrl(tableIdOrSlug) {
|
|
|
23978
24336
|
return `${defaultApiUrl().replace(/\/+$/, "")}/tables/${encodeURIComponent(tableIdOrSlug)}`;
|
|
23979
24337
|
}
|
|
23980
24338
|
function formatImportFileSizeLimit(tier) {
|
|
23981
|
-
|
|
23982
|
-
|
|
23983
|
-
|
|
24339
|
+
return formatImportByteSize(PLAN_LIMITS[tier].import.maxFileBytes);
|
|
24340
|
+
}
|
|
24341
|
+
function formatImportByteSize(bytes) {
|
|
24342
|
+
const mebibytes = bytes / (1024 * 1024);
|
|
24343
|
+
return mebibytes >= 1024 && mebibytes % 1024 === 0 ? `${mebibytes / 1024} GiB` : `${Math.round(mebibytes)} MiB`;
|
|
23984
24344
|
}
|
|
23985
24345
|
// Help copy reads the shared ceiling so the number can never drift from the
|
|
23986
24346
|
// one the server enforces and the import writer splits against.
|
|
@@ -24496,7 +24856,31 @@ async function handleLogoutAction(options) {
|
|
|
24496
24856
|
}
|
|
24497
24857
|
}
|
|
24498
24858
|
async function handleUpdateAction(options) {
|
|
24859
|
+
// The detached daily worker and the post-update skills refresh print nothing:
|
|
24860
|
+
// their parent is gone, and the next run reports the outcome.
|
|
24861
|
+
if (options.background) {
|
|
24862
|
+
await runBackgroundUpdate().catch(() => undefined);
|
|
24863
|
+
return;
|
|
24864
|
+
}
|
|
24865
|
+
if (options.refreshSkills) {
|
|
24866
|
+
await runAutomaticSkillsInstall().catch(() => undefined);
|
|
24867
|
+
return;
|
|
24868
|
+
}
|
|
24499
24869
|
try {
|
|
24870
|
+
const modes = [options.status, options.enableAuto, options.disableAuto].filter(Boolean).length;
|
|
24871
|
+
if (modes > 1) {
|
|
24872
|
+
throw new OxygenError("conflicting_flags", "Pass only one of --status, --enable-auto, or --disable-auto.", { exitCode: 2 });
|
|
24873
|
+
}
|
|
24874
|
+
if (modes === 1) {
|
|
24875
|
+
const setting = options.enableAuto ? "on" : options.disableAuto ? "off" : undefined;
|
|
24876
|
+
const status = await readAutoUpdateStatus(setting ? { setting, dryRun: options.dryRun === true } : {});
|
|
24877
|
+
if (options.json) {
|
|
24878
|
+
writeJson(success("update", status));
|
|
24879
|
+
return;
|
|
24880
|
+
}
|
|
24881
|
+
process.stdout.write(formatAutoUpdateStatus(status));
|
|
24882
|
+
return;
|
|
24883
|
+
}
|
|
24500
24884
|
const result = await updateCli(options);
|
|
24501
24885
|
if (options.json) {
|
|
24502
24886
|
writeJson(success("update", result));
|
|
@@ -26927,9 +27311,12 @@ function formatWhoami(identity, context) {
|
|
|
26927
27311
|
const sourceLabel = describeProfileSource(context.source);
|
|
26928
27312
|
const profileName = context.resolution.exists ? context.resolution.name : "(no stored profile)";
|
|
26929
27313
|
const apiUrl = context.resolution.credentials?.apiUrl ?? defaultApiUrl();
|
|
27314
|
+
// Every Oxygen size shares plan_tier "oxygen"; the name says which size.
|
|
27315
|
+
const planName = identity.organization.plan_name ?? null;
|
|
26930
27316
|
const rows = [
|
|
26931
27317
|
["Account", email],
|
|
26932
27318
|
["Organization", org],
|
|
27319
|
+
...(planName ? [["Plan", planName]] : []),
|
|
26933
27320
|
["Profile", `${profileName} ${styles.dim(`(${sourceLabel})`)}`],
|
|
26934
27321
|
["API", apiUrl],
|
|
26935
27322
|
];
|
|
@@ -27014,6 +27401,31 @@ function formatUpdateSuccess(result) {
|
|
|
27014
27401
|
...formatAutomaticSkillsInstallStatusLines(result.skills_install),
|
|
27015
27402
|
].join("\n");
|
|
27016
27403
|
} // skipcq: JS-C1002
|
|
27404
|
+
function formatAutoUpdateStatus(status) {
|
|
27405
|
+
const styles = ansi(output.isTTY === true && !process.env.NO_COLOR);
|
|
27406
|
+
const auto = status.auto_update;
|
|
27407
|
+
const latest = status.latest_version
|
|
27408
|
+
? `${status.latest_version}${status.update_available ? " (newer version available)" : " (up to date)"}`
|
|
27409
|
+
: `unknown (${status.latest_error ?? "npm registry unreachable"})`;
|
|
27410
|
+
const last = auto.last_result
|
|
27411
|
+
? `${auto.last_result.ok ? "updated" : "failed"} ${auto.last_result.from}${auto.last_result.to ? ` -> ${auto.last_result.to}` : ""} `
|
|
27412
|
+
+ `at ${auto.last_result.at}${auto.last_result.error ? ` (${auto.last_result.error})` : ""}`
|
|
27413
|
+
: "none yet";
|
|
27414
|
+
return [
|
|
27415
|
+
"",
|
|
27416
|
+
`${styles.bold(status.dry_run ? `Oxygen CLI updates (dry run: automatic updates would be turned ${auto.setting}; nothing saved)` : "Oxygen CLI updates")}`,
|
|
27417
|
+
"",
|
|
27418
|
+
` ${styles.dim("Installed")} ${status.current_version}`,
|
|
27419
|
+
` ${styles.dim("Latest (npm)")} ${latest}`,
|
|
27420
|
+
` ${styles.dim("Automatic")} ${auto.active ? "on (checks daily; updates when the API requires it)" : auto.reason_text ?? "off"}`,
|
|
27421
|
+
` ${styles.dim("Last update")} ${last}`,
|
|
27422
|
+
"",
|
|
27423
|
+
auto.active
|
|
27424
|
+
? ` Turn off: ${auto.disable}`
|
|
27425
|
+
: ` Next step: ${auto.next_step ?? "oxygen update"}`,
|
|
27426
|
+
"",
|
|
27427
|
+
].join("\n");
|
|
27428
|
+
}
|
|
27017
27429
|
function formatAutomaticSkillsInstallStatusLines(result) {
|
|
27018
27430
|
if (!result)
|
|
27019
27431
|
return [];
|
|
@@ -28384,6 +28796,21 @@ const WORKING_DAY_NAMES = {
|
|
|
28384
28796
|
* separated, and ranges (`mon-fri`, `1-5`). A token it cannot read is sent as
|
|
28385
28797
|
* given so the server's invalid_working_hours error names it.
|
|
28386
28798
|
*/
|
|
28799
|
+
// The four --linkedin-* flags write settings.schedule, the per-sequence window
|
|
28800
|
+
// the LinkedIn/WhatsApp dispatcher enforces on top of each sender's working
|
|
28801
|
+
// hours. The server stores the window whole, so the four flags go together.
|
|
28802
|
+
function readLinkedinScheduleOptions(options) {
|
|
28803
|
+
const timezone = readOption(options.linkedinTimezone);
|
|
28804
|
+
const days = readOption(options.linkedinDays);
|
|
28805
|
+
const start = readOption(options.linkedinHoursStart);
|
|
28806
|
+
const end = readOption(options.linkedinHoursEnd);
|
|
28807
|
+
if (!timezone && !days && !start && !end)
|
|
28808
|
+
return undefined;
|
|
28809
|
+
if (!timezone || !days || !start || !end) {
|
|
28810
|
+
throw new OxygenError("invalid_linkedin_schedule", "Set the LinkedIn sending schedule with all four flags: --linkedin-timezone, --linkedin-days, --linkedin-hours-start and --linkedin-hours-end.", { exitCode: 1 });
|
|
28811
|
+
}
|
|
28812
|
+
return { timezone, days: parseWorkingDaysOption(days), start, end };
|
|
28813
|
+
}
|
|
28387
28814
|
function parseWorkingDaysOption(value) {
|
|
28388
28815
|
const day = (token) => {
|
|
28389
28816
|
const lower = token.trim().toLowerCase();
|
|
@@ -28528,6 +28955,10 @@ function readSequenceSettings(options) {
|
|
|
28528
28955
|
if (windowPath) {
|
|
28529
28956
|
settings.email_send_window = readJsonFileValue(resolve(windowPath), "--send-window-file");
|
|
28530
28957
|
}
|
|
28958
|
+
const linkedinSchedule = readLinkedinScheduleOptions(options);
|
|
28959
|
+
if (linkedinSchedule) {
|
|
28960
|
+
settings.schedule = linkedinSchedule;
|
|
28961
|
+
}
|
|
28531
28962
|
if (options.whatsappColdInitiate === true) {
|
|
28532
28963
|
settings.whatsapp_cold_initiate = true;
|
|
28533
28964
|
}
|
|
@@ -28554,6 +28985,12 @@ function readSequenceSettings(options) {
|
|
|
28554
28985
|
// A visible footer mutates outbound copy, so it is opt-in and tri-state:
|
|
28555
28986
|
// absent leaves the stored value alone; either explicit flag persists the
|
|
28556
28987
|
// chosen boolean. Missing/false is interpreted as OFF by the dispatcher.
|
|
28988
|
+
if (options.bounceProtection !== undefined)
|
|
28989
|
+
settings.bounce_protection = JSON.parse(options.bounceProtection);
|
|
28990
|
+
if (options.includeUnsubscribeHeaders !== undefined)
|
|
28991
|
+
settings.include_unsubscribe_headers = options.includeUnsubscribeHeaders;
|
|
28992
|
+
if (options.replyToStopText !== undefined)
|
|
28993
|
+
settings.reply_to_stop_text = options.replyToStopText;
|
|
28557
28994
|
if (options.includeUnsubscribeLink !== undefined) {
|
|
28558
28995
|
settings.include_unsubscribe_link = options.includeUnsubscribeLink;
|
|
28559
28996
|
}
|
|
@@ -28641,6 +29078,7 @@ process.stdout.on("error", (error) => {
|
|
|
28641
29078
|
process.stderr.write(`stdout error: ${error.message}\n`);
|
|
28642
29079
|
process.exit(1);
|
|
28643
29080
|
});
|
|
29081
|
+
maybeScheduleBackgroundUpdate();
|
|
28644
29082
|
const program = createProgram();
|
|
28645
29083
|
installCommanderExitOverride(program);
|
|
28646
29084
|
try {
|