@oxygen-agent/cli 1.1011.974 → 1.1012.2
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/command-manifest.d.ts +3 -1
- package/dist/command-manifest.js +29 -0
- package/dist/crm-support-flags.d.ts +1 -0
- package/dist/crm-support-flags.js +5 -1
- package/dist/customer-operation-commands.js +30 -1
- package/dist/help.js +10 -2
- package/dist/http-client.d.ts +4 -0
- package/dist/http-client.js +21 -10
- package/dist/index.js +326 -111
- package/dist/sender-limits-flags.d.ts +7 -0
- package/dist/sender-limits-flags.js +13 -0
- package/dist/transcript.js +1 -1
- package/node_modules/@oxygen/formula/dist/expression.js +16 -0
- package/node_modules/@oxygen/shared/dist/attempt-provenance.d.ts +30 -0
- package/node_modules/@oxygen/shared/dist/attempt-provenance.js +98 -0
- package/node_modules/@oxygen/shared/dist/byok-connect.js +2 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +8 -1
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +172 -13
- package/node_modules/@oxygen/shared/dist/cli-result.js +6 -0
- package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +19 -7
- package/node_modules/@oxygen/shared/dist/contracts/crm-attributes.d.ts +7 -1
- package/node_modules/@oxygen/shared/dist/contracts/crm-attributes.js +5 -1
- package/node_modules/@oxygen/shared/dist/contracts/crm-support.d.ts +14 -0
- package/node_modules/@oxygen/shared/dist/contracts/crm-support.js +4 -0
- package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +5 -5
- package/node_modules/@oxygen/shared/dist/contracts/geo.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/contracts/geo.js +138 -0
- package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/contracts/operations.js +4 -3
- package/node_modules/@oxygen/shared/dist/contracts/publishing-reads.js +1 -0
- package/node_modules/@oxygen/shared/dist/contracts/table-row-removal.d.ts +16 -0
- package/node_modules/@oxygen/shared/dist/contracts/table-row-removal.js +27 -0
- package/node_modules/@oxygen/shared/dist/cost-estimate.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/cost-estimate.js +23 -8
- package/node_modules/@oxygen/shared/dist/enrichment-intents.js +2 -1
- package/node_modules/@oxygen/shared/dist/external-effect-arming.d.ts +54 -0
- package/node_modules/@oxygen/shared/dist/external-effect-arming.js +90 -0
- package/node_modules/@oxygen/shared/dist/geo.d.ts +247 -0
- package/node_modules/@oxygen/shared/dist/geo.js +314 -0
- package/node_modules/@oxygen/shared/dist/knowledge-seed-content.js +2 -2
- package/node_modules/@oxygen/shared/dist/linkedin-countries.d.ts +9 -0
- package/node_modules/@oxygen/shared/dist/linkedin-countries.js +1 -1
- package/node_modules/@oxygen/shared/dist/linkedin-quota-denial.d.ts +48 -0
- package/node_modules/@oxygen/shared/dist/linkedin-quota-denial.js +94 -0
- package/node_modules/@oxygen/shared/dist/managed-email-branding.d.ts +15 -0
- package/node_modules/@oxygen/shared/dist/managed-email-branding.js +81 -0
- package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +20 -24
- package/node_modules/@oxygen/shared/dist/pricing-sheet.js +15 -16
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +3 -3
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +3 -3
- package/node_modules/@oxygen/shared/dist/product-doctrine.js +6 -5
- package/node_modules/@oxygen/shared/dist/provider-balance-signal.js +3 -2
- package/node_modules/@oxygen/shared/dist/recipes.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/recipes.js +1 -0
- package/node_modules/@oxygen/shared/dist/repricing.d.ts +4 -14
- package/node_modules/@oxygen/shared/dist/repricing.js +25 -64
- package/node_modules/@oxygen/shared/dist/research-depth.d.ts +11 -0
- package/node_modules/@oxygen/shared/dist/research-depth.js +7 -0
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
- 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 +1 -1
- package/node_modules/@oxygen/shared/dist/whatsapp-sender-limits.d.ts +88 -0
- package/node_modules/@oxygen/shared/dist/whatsapp-sender-limits.js +118 -0
- package/node_modules/@oxygen/shared/dist/workflow-trigger-metadata.d.ts +15 -0
- package/node_modules/@oxygen/shared/dist/workflow-trigger-metadata.js +33 -0
- package/node_modules/@oxygen/shared/package.json +15 -5
- package/node_modules/@oxygen/workflows/dist/usage-estimate.js +9 -5
- package/package.json +1 -1
- package/node_modules/@oxygen/shared/dist/vercel-sandbox-fetch.d.ts +0 -9
- package/node_modules/@oxygen/shared/dist/vercel-sandbox-fetch.js +0 -33
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import { registerConnectorsCommands } from "./connectors-commands.js";
|
|
|
10
10
|
import { addContractFlags, readContractFlags } from "./contract-flags.js";
|
|
11
11
|
import { clearCredentials, defaultApiUrl, listCredentialProfiles, loadCredentials, normalizeApiUrl, pickProfileNameForIdentity, pickProfileNameForUserSession, readEnvToken, resolveActiveProfile, saveCredentials, switchCredentialProfile, updateActiveOrganizationForProfile, } from "./credentials.js";
|
|
12
12
|
import { registerCrmFileReadCommands } from "./crm-file-read-commands.js";
|
|
13
|
-
import { registerCrmSupportFlags, readCrmNoteEditFields, readCrmPhotoFields, readCrmFileLabel, buildCrmObjectsPath, buildCrmConnectionsPath } from "./crm-support-flags.js";
|
|
13
|
+
import { registerCrmSupportFlags, readCrmNoteEditFields, readCrmPhotoFields, readCrmFileLabel, buildCrmObjectsPath, buildCrmConnectionsPath, buildCrmRecordPath } from "./crm-support-flags.js";
|
|
14
14
|
import { registerCustomerOperationCommands } from "./customer-operation-commands.js";
|
|
15
15
|
import { DATABASE_QUERY_FLAG_OVERLAY } from "./database-query-flags.js";
|
|
16
16
|
import { addFeedReadFlags, feedReadWindowParams } from "./feed-read-flags.js";
|
|
@@ -26,7 +26,7 @@ import { formatMailboxRampNotice } from "./mailbox-ramp-notice.js";
|
|
|
26
26
|
import { waitForCliRun } from "./run-wait.js";
|
|
27
27
|
import { resolveCliBinaryName } from "./runtime.js";
|
|
28
28
|
import { formatSearchAiFilterTotalsNotice } from "./search-ai-filter-notice.js";
|
|
29
|
-
import { buildSenderLimitsBody, parseWorkingDaysOption, SENDER_LIMITS_FLAG_OVERLAY } from "./sender-limits-flags.js";
|
|
29
|
+
import { addWhatsAppLimitFlags, buildSenderLimitsBody, parseWorkingDaysOption, SENDER_LIMITS_FLAG_OVERLAY } from "./sender-limits-flags.js";
|
|
30
30
|
import { addSessionOutput, addSessionStatus, getSessionUsage, startSession, updateSessionStep, } from "./session.js";
|
|
31
31
|
import { registerSettingsCommands } from "./settings-commands.js";
|
|
32
32
|
import { doctorAgentSkills, getAgentSkill, installAgentSkills, listAgentSkills, runAutomaticSkillsInstall, searchAgentSkills, } from "./skills.js";
|
|
@@ -68,6 +68,7 @@ import { WHATSAPP_ACCOUNTS_READ_CONTRACT } from "@oxygen/shared/contracts/whatsa
|
|
|
68
68
|
import { WORKFLOW_RUN_LIST_CONTRACT, workflowRunListQueryParams } from "@oxygen/shared/contracts/workflow-run-list";
|
|
69
69
|
import { MANAGED_DATA_SUPPLIERS } from "@oxygen/shared/data-suppliers";
|
|
70
70
|
import { MAX_BUFFERED_IMPORT_PARSE_BYTES, assertImportFormatWithinBufferedLimit, inferImportColumnLabels, inferRowsFileFormat, normalizeImportColumnKey, normalizeRowsForNewTable, normalizeRowsFormat, parseRowsFileBuffer, parseXlsxWorkbookBuffer, } from "@oxygen/shared/file-import";
|
|
71
|
+
import { KNOWLEDGE_PAGE_TYPES } from "@oxygen/shared/knowledge-constants";
|
|
71
72
|
import { LINKEDIN_SENDER_LIMIT_DEFAULTS, LINKEDIN_SENDER_LIMIT_MAXIMUMS, } from "@oxygen/shared/linkedin-sequences";
|
|
72
73
|
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";
|
|
73
74
|
import { PUBLIC_WEBHOOK_INGRESS_PER_TARGET_PER_MINUTE } from "@oxygen/shared/plan-band";
|
|
@@ -355,6 +356,20 @@ function suppressionListParams(options) {
|
|
|
355
356
|
// feed): `translateScheduleEvery` in apps/web resolves all four forms, so this
|
|
356
357
|
// string is the single place the CLI spells them out.
|
|
357
358
|
const CADENCE_FLAG_DESCRIPTION = "Shorthand schedule: hourly, every_6h, daily@<hour>, or weekly@<day><hour> (e.g. weekly@mon9). Hours 0-23 UTC.";
|
|
359
|
+
/** A --value for the pricing book: a number, true/false for a charge's active flag, or "none" for no plan limit. */
|
|
360
|
+
function readPriceBookValue(raw) {
|
|
361
|
+
if (raw === undefined)
|
|
362
|
+
return undefined;
|
|
363
|
+
const value = raw.trim();
|
|
364
|
+
if (value === "true")
|
|
365
|
+
return true;
|
|
366
|
+
if (value === "false")
|
|
367
|
+
return false;
|
|
368
|
+
if (value === "none" || value === "null")
|
|
369
|
+
return null;
|
|
370
|
+
const number = Number(value);
|
|
371
|
+
return value !== "" && Number.isFinite(number) ? number : value;
|
|
372
|
+
}
|
|
358
373
|
const BROWSER_LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
|
|
359
374
|
const BROWSER_LOGIN_FALLBACK_HINT_MS = 15 * 1000;
|
|
360
375
|
const OXYGEN_SPINNER_INTERVAL_MS = 90;
|
|
@@ -3810,6 +3825,7 @@ export function createProgram() {
|
|
|
3810
3825
|
const workspaceCommand = program
|
|
3811
3826
|
.command("workspace")
|
|
3812
3827
|
.description("Inspect this workspace or end its managed external infrastructure.")
|
|
3828
|
+
.addHelpText("before", "\nActive workspace and identity: oxygen whoami --summary-json (read-only, 0 credits).\n")
|
|
3813
3829
|
.addCommand(new Command("map")
|
|
3814
3830
|
.description("What this workspace actually contains: the newest tables, CRM objects, sequences, workflows, agents, wiki pages, posts and projects, each with its id, last activity, tags and link. Read-only, 0 credits. Capability search tells you what OXYGEN can do; this tells you what is here.")
|
|
3815
3831
|
.option("--kind <kinds>", "Comma-separated kinds to include: table, crm_object, sequence, workflow, agent, knowledge_page, post, project.")
|
|
@@ -4025,7 +4041,7 @@ export function createProgram() {
|
|
|
4025
4041
|
});
|
|
4026
4042
|
}))
|
|
4027
4043
|
.addCommand(new Command("billing-link")
|
|
4028
|
-
.description("Cover a workspace with
|
|
4044
|
+
.description("Cover a workspace with another organization's paid plan instead of buying a second subscription. Omit --owner when exactly one owner is eligible; orgs billing-owners lists them. You must administer both organizations, the workspace must not hold its own live subscription, and an org-bound key can link only its workspace. The owner funds credits; data access stays unchanged. billing estimate reports sender_billing: credit_reservations uses the owner's credits, including when plan_tier is free; legacy sending_seats uses the owner's paid plan and seats. BYOK and Instagram/TikTok/YouTube/Facebook publishing still need a paid plan. billing usage --group-by workspace reports each linked workspace's spend.")
|
|
4029
4045
|
.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.")
|
|
4030
4046
|
.option("--organization <organization>", "Workspace organization to link. Defaults to the active organization.")
|
|
4031
4047
|
.option("--organization-id <id>", "Alias for --organization.")
|
|
@@ -4053,7 +4069,7 @@ export function createProgram() {
|
|
|
4053
4069
|
});
|
|
4054
4070
|
}))
|
|
4055
4071
|
.addCommand(new Command("billing-unlink")
|
|
4056
|
-
.description("Return a workspace to
|
|
4072
|
+
.description("Return a workspace to its own billing owner and credit balance. Its own subscription state, plan and sender_billing model determine which actions need a plan.")
|
|
4057
4073
|
.option("--organization <organization>", "Workspace organization to unlink. Defaults to the active organization.")
|
|
4058
4074
|
.option("--organization-id <id>", "Alias for --organization.")
|
|
4059
4075
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -5202,9 +5218,9 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
5202
5218
|
}));
|
|
5203
5219
|
})))
|
|
5204
5220
|
.addCommand(new Command("comments")
|
|
5205
|
-
.description("Publishing Community for public comments on every recent post owned by a connected LinkedIn account, including posts created directly on LinkedIn. Start with `oxygen publishing comments list --view unanswered --json`. These are network conversations, not workspace review notes. The local queue targets a six-hour background discovery + comment poll; provider quota/backoff can delay it, and
|
|
5221
|
+
.description("Publishing Community for public comments on every recent post owned by a connected LinkedIn account, including posts created directly on LinkedIn. Start with `oxygen publishing comments list --view unanswered --json`. These are network conversations, not workspace review notes. The local queue targets a six-hour background discovery + comment poll; provider quota/backoff can delay it. Read freshness, empty_queue_is_current and guidance before trusting an empty queue. automatic_retry_at reports a future retry; next_sync_at alone does not confirm one. An unconfirmed provider read can pause refresh; contact support when guidance says to. No manual retry is available. Queue reads use 0 credits and make no provider call; only explicit approval can post a public reply.")
|
|
5206
5222
|
.addCommand(new Command("list")
|
|
5207
|
-
.description("List the Publishing Community work queue, oldest first, with polling freshness/backoff even when empty. Defaults to the Unanswered view: needs_reply, draft, awaiting_approval, and action_unavailable. All
|
|
5223
|
+
.description("List the Publishing Community work queue, oldest first, with polling freshness/backoff even when empty. Defaults to the Unanswered view: needs_reply, draft, awaiting_approval, and action_unavailable. All covers every lifecycle state on published owned posts within 30 days plus UGC-linked posts, not lifetime LinkedIn history. Completed coverage counts earlier successful polls and can overlap failed coverage after a later error. Read-only: no provider call, 0 credits.")
|
|
5208
5224
|
.option("--view <view>", "Community view: unanswered (default) or all; all is bounded to recent owned posts.")
|
|
5209
5225
|
.option("--status <status>", "Advanced exact-state filter: needs_reply, draft, awaiting_approval, replied, resolved, ignored, spam, or action_unavailable. Cannot be combined with --view.")
|
|
5210
5226
|
.option("--post <post_id>", "Only comments on one scheduled Publishing post.")
|
|
@@ -5300,7 +5316,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
5300
5316
|
}));
|
|
5301
5317
|
})))
|
|
5302
5318
|
.addCommand(new Command("analytics")
|
|
5303
|
-
.description("Stored LinkedIn and X engagement snapshots synced in the background. LinkedIn includes owned posts discovered from the provider; X includes only posts published through OXYGEN. Reading analytics makes no provider call and uses 0 credits. A snapshotted post completed a sync pass; use per-metric coverage to see which values were populated. Null is unknown, never zero.
|
|
5319
|
+
.description("Stored LinkedIn and X engagement snapshots synced in the background. LinkedIn includes owned posts discovered from the provider; X includes only posts published through OXYGEN. Reading analytics makes no provider call and uses 0 credits. A snapshotted post completed a sync pass; use per-metric coverage to see which values were populated. Null is unknown, never zero. Read freshness and guidance for delayed refresh; an unconfirmed provider read can pause it. No manual provider retry is available.")
|
|
5304
5320
|
.addCommand(new Command("summary")
|
|
5305
5321
|
.description("Workspace LinkedIn or X analytics. LinkedIn includes owned posts discovered from the provider; X currently includes posts published through OXYGEN. Uses each post's latest stored snapshot; snapshotted_posts (legacy measured_posts) counts sync passes, while coverage counts non-null values. Read-only: no provider call, 0 credits.")
|
|
5306
5322
|
.option("--channel <channel>", "Channel to summarize: linkedin (default) or x.")
|
|
@@ -5357,7 +5373,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
5357
5373
|
await handleAsyncAction("publishing analytics post", options, () => requestOxygen(publishingReadQueryPath(buildPublishingAnalyticsPath(`/api/cli/publishing/analytics/posts/${encodeURIComponent(postId)}`, { since: readOption(options.since) }), publishingAnalyticsQueryContract, options)));
|
|
5358
5374
|
}))
|
|
5359
5375
|
.addCommand(addContractFlags(new Command("account"), publishingAnalyticsQueryContract)
|
|
5360
|
-
.description("Stored per-account metric series (followers) for connected LinkedIn and X publishing accounts, read once a day in the background. Read-only: no provider call, 0 credits.")
|
|
5376
|
+
.description("Stored per-account metric series (followers) for connected LinkedIn and X publishing accounts, read once a day in the background. Each account reports its snapshot freshness; stale values remain available. Read-only: no provider call, 0 credits.")
|
|
5361
5377
|
.option("--provider <provider>", "Channel to read: linkedin or x. Omit for every connected channel.")
|
|
5362
5378
|
.option("--since <date>", "ISO date to start the series from.")
|
|
5363
5379
|
.option("--account <key>", PUBLISHING_ANALYTICS_ACCOUNT_HELP, collectRepeatable, [])
|
|
@@ -5526,7 +5542,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
5526
5542
|
.command("dashboards")
|
|
5527
5543
|
.description("Your saved dashboards, plus the built-in GTM funnel. `summary` reads the built-in Command Center funnel across the sequencer, unibox and CRM; everything else creates and edits dashboards you save yourself and open again later, from charts, saved table views, notes, embeds and live sources.")
|
|
5528
5544
|
.addCommand(new Command("summary")
|
|
5529
|
-
.description("Show the GTM funnel summary: touches → replies → positive → meetings → deals → won, with this-period revenue, channel split, and needs-action triage counts.")
|
|
5545
|
+
.description("Show the GTM funnel summary: touches → replies → positive → meetings → deals → won, with this-period revenue, channel split, and current needs-action triage. Unread replies across channels and interested email conversations are current inbox counts, independent of the selected range.")
|
|
5530
5546
|
.option("--range <range>", "Preset range: 7d, 14d, 28d, 30d, 90d, 180d, 365d, or all. Defaults to 14d.")
|
|
5531
5547
|
.option("--from <date>", "Custom start date (YYYY-MM-DD).")
|
|
5532
5548
|
.option("--to <date>", "Custom end date (YYYY-MM-DD).")
|
|
@@ -5914,7 +5930,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
5914
5930
|
.argument("<row_id>", "CRM record row id.")
|
|
5915
5931
|
.option("--json", "Print a JSON envelope.")
|
|
5916
5932
|
.action(async (object, rowId, options) => {
|
|
5917
|
-
await handleAsyncAction("crm get", options, () => requestOxygen(
|
|
5933
|
+
await handleAsyncAction("crm get", options, () => requestOxygen(buildCrmRecordPath(object, rowId, options)));
|
|
5918
5934
|
}))
|
|
5919
5935
|
.addCommand(new Command("connections")
|
|
5920
5936
|
.description("Show the conversations and sequence enrollments connected to one CRM record, resolved through its people and identities \u2014 never by fuzzy name or domain.")
|
|
@@ -5939,7 +5955,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
5939
5955
|
await handleAsyncAction("crm files list", options, () => requestOxygen(`/api/cli/crm/objects/${encodeURIComponent(object)}/records/${encodeURIComponent(rowId)}/files`));
|
|
5940
5956
|
}))
|
|
5941
5957
|
.addCommand(new Command("attach")
|
|
5942
|
-
.description("Upload a local file and attach it to a CRM record.
|
|
5958
|
+
.description("Upload a local file and attach it to a CRM record. Each call creates a new file and attachment, including identical retries. If a response is lost, inspect the record's files before retrying. No credits.")
|
|
5943
5959
|
.argument("<object>", "CRM object slug, such as companies or people.")
|
|
5944
5960
|
.argument("<record>", "CRM record row id, or an identity value such as acme.com or sarah@acme.com.")
|
|
5945
5961
|
.argument("<path>", "Local file path.")
|
|
@@ -6726,6 +6742,61 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6726
6742
|
return requestOxygen(`/api/cli/tables/sources${qs}`);
|
|
6727
6743
|
});
|
|
6728
6744
|
}))
|
|
6745
|
+
.addCommand(new Command("records")
|
|
6746
|
+
.description("Copy a Records object or saved view into a working Table, once or kept in sync. One-way: Records stay unchanged, the Table is keyed by record_id, cells edited in the Table are kept, and records that leave the source stay as rows with in_records=false.")
|
|
6747
|
+
.addCommand(new Command("import")
|
|
6748
|
+
.description("Create a Table from a Records object or saved view. --dry-run counts the records and lists the columns without creating anything. --refresh sync also arms a schedule (a Workflow you can pause) that repeats the copy. The copy costs 0 credits and calls no provider; each scheduled sync bills one workflow step (credits_per_scheduled_sync).")
|
|
6749
|
+
.requiredOption("--object <slug>", "Records object slug, such as companies, people or deals (`crm objects`).")
|
|
6750
|
+
.option("--view <id|name>", "Saved view on that object, by id or exact name (`tables views list <object table>`). Omit for every record.")
|
|
6751
|
+
.option("--name <table>", "Name for the new Table. Defaults to the object and view name.")
|
|
6752
|
+
.option("--project <project>", "Project id or slug. Defaults to General.")
|
|
6753
|
+
.option("--refresh <once|sync>", "once (default) copies now; sync also keeps it in sync (needs --approved).")
|
|
6754
|
+
.option("--every <cadence>", "With --refresh sync: hourly, every_6h, daily@<hour> or weekly@<day><hour> (UTC). Default daily@6.")
|
|
6755
|
+
.option("--dry-run", "Preview without creating the Table.")
|
|
6756
|
+
.option("--approved", "Approve the standing per-sync charge that --refresh sync arms.")
|
|
6757
|
+
.option("--json", "Print a JSON envelope.")
|
|
6758
|
+
.action(async (options) => {
|
|
6759
|
+
await handleAsyncAction("tables records import", options, () => requestOxygen("/api/cli/tables/records-import", {
|
|
6760
|
+
method: "POST",
|
|
6761
|
+
body: {
|
|
6762
|
+
object: options.object,
|
|
6763
|
+
...(readOption(options.view) ? { view: readOption(options.view) } : {}),
|
|
6764
|
+
...(readOption(options.name) ? { name: readOption(options.name) } : {}),
|
|
6765
|
+
...(readOption(options.project) ? { project: readOption(options.project) } : {}),
|
|
6766
|
+
...(readOption(options.refresh) ? { refresh: readOption(options.refresh) } : {}),
|
|
6767
|
+
...(readOption(options.every) ? { every: readOption(options.every) } : {}),
|
|
6768
|
+
...(options.dryRun ? { dry_run: true } : {}),
|
|
6769
|
+
...(options.approved ? { approved: true } : {}),
|
|
6770
|
+
},
|
|
6771
|
+
}));
|
|
6772
|
+
}))
|
|
6773
|
+
.addCommand(new Command("sync")
|
|
6774
|
+
.description("Copy a Records-imported Table's source again now: new records are added, changed cells updated, edited cells kept, and departed records marked in_records=false (they flip back if they return). Without --refresh the schedule stays as it is; --refresh sync --approved arms or retimes it, --refresh once removes it. preserved_cells counts copied-field cells left as someone edited them in the Table; columns you added are never touched and not counted. Running it now costs 0 credits; each scheduled sync bills one workflow step (credits_per_scheduled_sync in an import dry run).")
|
|
6775
|
+
.argument("<table>", "Table id, slug or URL.")
|
|
6776
|
+
.option("--refresh <once|sync>", "Omit to keep the schedule; sync (with --approved) arms or retimes it; once removes it.")
|
|
6777
|
+
.option("--every <cadence>", "With --refresh sync: hourly, every_6h, daily@<hour> or weekly@<day><hour> (UTC). Default daily@6.")
|
|
6778
|
+
.option("--dry-run", "Count what would change without writing.")
|
|
6779
|
+
.option("--approved", "Approve the standing per-sync charge that --refresh sync arms.")
|
|
6780
|
+
.option("--json", "Print a JSON envelope.")
|
|
6781
|
+
.action(async (table, options) => {
|
|
6782
|
+
await handleAsyncAction("tables records sync", options, () => requestOxygen("/api/cli/tables/records-sync", {
|
|
6783
|
+
method: "POST",
|
|
6784
|
+
body: {
|
|
6785
|
+
table,
|
|
6786
|
+
...(readOption(options.refresh) ? { refresh: readOption(options.refresh) } : {}),
|
|
6787
|
+
...(readOption(options.every) ? { every: readOption(options.every) } : {}),
|
|
6788
|
+
...(options.dryRun ? { dry_run: true } : {}),
|
|
6789
|
+
...(options.approved ? { approved: true } : {}),
|
|
6790
|
+
},
|
|
6791
|
+
}));
|
|
6792
|
+
}))
|
|
6793
|
+
.addCommand(new Command("status")
|
|
6794
|
+
.description("Show a Records-imported Table's source object and view, the last sync's counts and its schedule. Read-only, 0 credits.")
|
|
6795
|
+
.argument("<table>", "Table id, slug or URL.")
|
|
6796
|
+
.option("--json", "Print a JSON envelope.")
|
|
6797
|
+
.action(async (table, options) => {
|
|
6798
|
+
await handleAsyncAction("tables records status", options, () => requestOxygen(`/api/cli/tables/records-sync?table=${encodeURIComponent(table)}`));
|
|
6799
|
+
})))
|
|
6729
6800
|
.addCommand(new Command("hubspot")
|
|
6730
6801
|
.description("Import HubSpot contacts or companies into a new table or Records, optionally from a saved list and with do-not-contact suppression.")
|
|
6731
6802
|
.addCommand(new Command("lists")
|
|
@@ -7025,7 +7096,8 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7025
7096
|
});
|
|
7026
7097
|
}))
|
|
7027
7098
|
.addCommand(new Command("delete-rows")
|
|
7028
|
-
.description("Delete
|
|
7099
|
+
.description("Delete exact workspace row IDs. For a matching filtered selection, use tables rows remove-selection. Campaign enrollments stay enrolled; the response names those campaigns.")
|
|
7100
|
+
.addHelpText("after", "\nFor a matching filtered selection, use tables rows remove-selection --help.")
|
|
7029
7101
|
.argument("<table>", "Table id or slug.")
|
|
7030
7102
|
.option("--row-ids-json <json>", "JSON array of workspace row UUIDs.")
|
|
7031
7103
|
.option("--row-ids-file <path>", "File containing row UUIDs or rows with a _row_id column.")
|
|
@@ -7170,7 +7242,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7170
7242
|
.option("--filter-json <json>", "Legacy filter object or array, e.g. '{\"column\":\"mobile_phone_e164\",\"op\":\"is_null\"}'. Mutually exclusive with --filter-tree-json/--sort-json.")
|
|
7171
7243
|
.option("--filter-tree-json <json>", `Airtable-style filter group, e.g. '{\"type\":\"group\",\"conjunction\":\"and\",\"children\":[{\"type\":\"leaf\",\"columnKey\":\"stage\",\"operator\":\"is\",\"value\":\"won\"}]}'. Add \"path\":\"a.b\" to a leaf to match a key inside a JSON cell, e.g. the rows one Function run touched: {\"columnKey\":\"<function_column>\",\"path\":\"action_run_id\",\"operator\":\"is\",\"value\":\"<run_id>\"}. Leaf operators: ${FILTER_TREE_OPERATORS.join(", ")} (is_true/is_false need a checkbox column; --filter-json's is_null/is_not_null are is_empty/is_not_empty here). Mutually exclusive with --filter-json.`)
|
|
7172
7244
|
.option("--search <text>", "Find rows by one piece of text, matched against every text, number, date and checkbox column at once — the grid's magnifier. Combines with --filter-tree-json (both must hold). Use --filter-tree-json when you know which column to match.")
|
|
7173
|
-
.option("--formula-values <mode>", "Formula filters are refused by default because displayed formulas are evaluated live. Refresh the formula's stored cells with `columns run <table> <column> --force` (0 credits), then pass 'materialized'; filtering uses that stored snapshot and returns a freshness note.")
|
|
7245
|
+
.option("--formula-values <mode>", "Formula filters are refused by default because displayed formulas are evaluated live. Refresh the formula's stored cells with `columns run <table> <column> --force` (0 credits), then pass 'materialized'; filtering uses that stored snapshot and returns a freshness note. A fill-empty field a bundle adds (definition fill: \"empty\", such as Headcount or Job title) is stored, not live: its cell is the value, so 'materialized' filters it exactly.")
|
|
7174
7246
|
.option("--sort-json <json>", "Ordered sort rules, e.g. '[{\"columnKey\":\"_created_at\",\"direction\":\"desc\"}]'. Earlier rules dominate. Mutually exclusive with --filter-json.")
|
|
7175
7247
|
.option("--no-system-fields", "Omit _row_id, _created_at, and _updated_at from returned rows (included by default).")
|
|
7176
7248
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -7376,7 +7448,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7376
7448
|
}));
|
|
7377
7449
|
}))
|
|
7378
7450
|
.addCommand(new Command("recover-pending")
|
|
7379
|
-
.description("
|
|
7451
|
+
.description("Recover existing BetterContact email or phone requests and inspect terminal verification blockers; no new orders.")
|
|
7380
7452
|
.argument("<table>", "Table id or slug.")
|
|
7381
7453
|
.option("--max-items <n>", "Max pending cache rows to poll in this run. Defaults to 1000.")
|
|
7382
7454
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -7452,13 +7524,13 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7452
7524
|
});
|
|
7453
7525
|
}
|
|
7454
7526
|
const followersCommand = tablesCommand.command("followers")
|
|
7455
|
-
.description("Order a LinkedIn company page's followers as a table of leads — a competitor's audience. `check` is free and quotes the
|
|
7527
|
+
.description("Order a LinkedIn company page's followers as a table of leads — a competitor's audience. `check` is free and quotes the order; fee-positive own-key previews leave the follower count unknown. `order` places it. The order is fulfilled by ScrapeLi, so the table is created immediately and its rows land later; there is no published delivery time.");
|
|
7456
7528
|
followersCommand.command("check")
|
|
7457
|
-
.description("Free
|
|
7529
|
+
.description("Free order preview. Managed quotes the follower cost; your own key quotes placement and later check fees, with a total ceiling you choose. No spend or table; `orderable: false` means the order is unavailable.")
|
|
7458
7530
|
.requiredOption("--company <url>", "LinkedIn company page URL, e.g. https://www.linkedin.com/company/hubspot.")
|
|
7459
7531
|
.option("--records <n>", "How many followers to price. Minimum 100; defaults to 1000.")
|
|
7460
7532
|
.option("--package <package>", "Basic (default): profile, job title, location, employer. Advanced (10x the credits): also a verified work email and region.")
|
|
7461
|
-
.option("--credential-mode <mode>", "managed (default): Oxygen credits. user_api_key: your
|
|
7533
|
+
.option("--credential-mode <mode>", "managed (default): Oxygen credits. user_api_key: your saved ScrapeLi key, billed by ScrapeLi, plus Oxygen placement/recovery platform fees.")
|
|
7462
7534
|
.option("--json", "Print a JSON envelope.")
|
|
7463
7535
|
.action(async (options) => {
|
|
7464
7536
|
await handleAsyncAction("tables followers check", options, () => requestOxygen("/api/cli/tables/linkedin-company-followers", {
|
|
@@ -7473,16 +7545,16 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7473
7545
|
}));
|
|
7474
7546
|
});
|
|
7475
7547
|
followersCommand.command("order")
|
|
7476
|
-
.description("Place the follower order after checking it. Requires --approved
|
|
7548
|
+
.description("Place the follower order after checking it. Requires --approved and --max-credits: managed uses the quoted order cost; own-key uses a TOTAL placement/recovery platform-fee ceiling. A ceiling equal to placement cannot recover rows. Budget exhaustion lands no partial rows; refetched pages consume the same ceiling. Creates the Table now; rows arrive later.")
|
|
7477
7549
|
.requiredOption("--approved", "Approve this purchase. Required, as on every command that buys data.")
|
|
7478
7550
|
.requiredOption("--company <url>", "LinkedIn company page URL whose followers to order.")
|
|
7479
|
-
.option("--max-credits <credits>", "
|
|
7480
|
-
.requiredOption("--request-id <uuid>", "One stable UUID
|
|
7551
|
+
.option("--max-credits <credits>", "Approved order ceiling. On your key, TOTAL platform fees for placement and later checks; exhaustion stops recovery. At least the quoted placement fee. ScrapeLi charges separately.")
|
|
7552
|
+
.requiredOption("--request-id <uuid>", "One stable UUID and identical order payload for retries. A new ID can buy the audience again.")
|
|
7481
7553
|
.option("--records <n>", "How many followers to buy. Minimum 100; defaults to 1000.")
|
|
7482
7554
|
.option("--package <package>", "Basic (default) or Advanced (10x the credits; adds a verified work email and region).")
|
|
7483
7555
|
.option("--name <name>", "Table name. Defaults to \"<Company> Followers\".")
|
|
7484
7556
|
.option("--project <project>", "Project id or slug; defaults to General.")
|
|
7485
|
-
.option("--credential-mode <mode>", "managed (default): Oxygen credits. user_api_key: your
|
|
7557
|
+
.option("--credential-mode <mode>", "managed (default): Oxygen credits. user_api_key: your saved ScrapeLi key, billed by ScrapeLi, plus Oxygen placement/recovery platform fees.")
|
|
7486
7558
|
.option("--json", "Print a JSON envelope.")
|
|
7487
7559
|
.action(async (options) => {
|
|
7488
7560
|
await handleAsyncAction("tables followers order", options, () => requestOxygen("/api/cli/tables/linkedin-company-followers", {
|
|
@@ -7501,6 +7573,23 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7501
7573
|
},
|
|
7502
7574
|
}));
|
|
7503
7575
|
});
|
|
7576
|
+
followersCommand.command("status")
|
|
7577
|
+
.description("Read a fee-budgeted own-key follower order's total fee ceiling, allocated attempts, recorded fees and recovery outcome by its original request ID, including interrupted setup. Optional --table-id verifies the saved Table binding. Recorded fees may settle later. Makes no provider call.")
|
|
7578
|
+
.requiredOption("--request-id <uuid>", "Original order request ID.")
|
|
7579
|
+
.option("--table-id <id>", "Optional saved Table ID; verifies the order belongs to that Table.")
|
|
7580
|
+
.option("--json", "Print a JSON envelope.")
|
|
7581
|
+
.action(async (options) => {
|
|
7582
|
+
await handleAsyncAction("tables followers status", options, () => {
|
|
7583
|
+
const requestId = readOption(options.requestId);
|
|
7584
|
+
if (!requestId)
|
|
7585
|
+
throw new OxygenError("invalid_input", "--request-id must be the original nonblank order request ID.", { exitCode: 2 });
|
|
7586
|
+
const query = new URLSearchParams({ request_id: requestId });
|
|
7587
|
+
const tableId = readOption(options.tableId);
|
|
7588
|
+
if (tableId)
|
|
7589
|
+
query.set("table_id", tableId);
|
|
7590
|
+
return requestOxygen(`/api/cli/tables/linkedin-company-followers?${query}`, { method: "GET" });
|
|
7591
|
+
});
|
|
7592
|
+
});
|
|
7504
7593
|
tablesCommand.addCommand(new Command("relate")
|
|
7505
7594
|
.description("Relate two tables: define empty Tables-owned relation columns on the source and target. Then use `oxygen tables link` to populate row-to-row edges. Works on any workspace table; plain tables stay plain and are never registered as CRM objects. Defaults to dry-run.")
|
|
7506
7595
|
.argument("<table>", "Source table id or slug.")
|
|
@@ -8350,7 +8439,7 @@ Examples:
|
|
|
8350
8439
|
.option("--body <text>", "Asset body text.")
|
|
8351
8440
|
.option("--data-json <json>", "Flexible JSON object for structured asset data.")
|
|
8352
8441
|
.option("--asset-json <json>", "Full asset JSON object. CLI flags override matching fields.")
|
|
8353
|
-
.option("--default", "
|
|
8442
|
+
.option("--default", "Ask to pin as the canonical default for its type (auto-applied to copy generation). For brand, voice, positioning and playbooks the asset saves unpinned and the pin waits for approval of a proposal (pin.ok false; oxygen knowledge proposals approve <id>); other types pin at once.")
|
|
8354
8443
|
.option("--json", "Print a JSON envelope.")
|
|
8355
8444
|
.action(async (options) => {
|
|
8356
8445
|
await handleAsyncAction("context asset upsert", options, () => requestOxygen("/api/cli/context/assets/upsert", {
|
|
@@ -8359,7 +8448,7 @@ Examples:
|
|
|
8359
8448
|
}));
|
|
8360
8449
|
}))
|
|
8361
8450
|
.addCommand(new Command("set-default")
|
|
8362
|
-
.description("Pin a context asset as the canonical default for its type. The canonical brand/voice/positioning docs are auto-applied to AI copy generation.")
|
|
8451
|
+
.description("Pin a context asset as the canonical default for its type. The canonical brand/voice/positioning docs are auto-applied to AI copy generation. For brand, voice, positioning and playbooks a pin or unpin files a proposal a human approves (oxygen knowledge proposals approve <id>) and fails with knowledge_approval_required; other types pin at once.")
|
|
8363
8452
|
.argument("<asset_id>", "Context asset UUID.")
|
|
8364
8453
|
.option("--off", "Unpin instead of pin (clears the canonical default for this type).")
|
|
8365
8454
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -8370,7 +8459,7 @@ Examples:
|
|
|
8370
8459
|
}));
|
|
8371
8460
|
}))
|
|
8372
8461
|
.addCommand(new Command("archive")
|
|
8373
|
-
.description("Archive a context asset.")
|
|
8462
|
+
.description("Archive a context asset. Archiving the canonical brand, voice, positioning or playbook files an approval proposal and fails with knowledge_approval_required.")
|
|
8374
8463
|
.argument("<asset_id>", "Context asset UUID.")
|
|
8375
8464
|
.option("--json", "Print a JSON envelope.")
|
|
8376
8465
|
.action(async (assetId, options) => {
|
|
@@ -8463,14 +8552,14 @@ Examples:
|
|
|
8463
8552
|
.addCommand(new Command("page")
|
|
8464
8553
|
.description("Knowledge wiki pages (slug-addressed, revision-guarded).")
|
|
8465
8554
|
.addCommand(new Command("upsert")
|
|
8466
|
-
.description("Create or update a knowledge wiki page.")
|
|
8467
|
-
.option("--slug <slug>", "Stable page
|
|
8468
|
-
.option("--folder <folder_id>", "Folder UUID, or root to
|
|
8555
|
+
.description("Create or update a knowledge wiki page. Free internal write; canonical edits require proposal approval.")
|
|
8556
|
+
.option("--slug <slug>", "Stable page key. Omit to derive from title; an existing slug updates unless --create-only is set.")
|
|
8557
|
+
.option("--folder <folder_id>", "Folder UUID, or root to clear placement; omit to preserve. Moving creates a revision and changes data.knowledge_folder_id.")
|
|
8469
8558
|
.option("--create-only", "Fail if the page already exists instead of updating it.")
|
|
8470
|
-
.option("--id <page_id>", "Existing page UUID to update.
|
|
8471
|
-
.option("--type <type>",
|
|
8559
|
+
.option("--id <page_id>", "Existing page UUID to update. For a new file, omit and use --create-only.")
|
|
8560
|
+
.option("--type <type>", `Page type: ${KNOWLEDGE_PAGE_TYPES.join(", ")}. voice, brand and positioning are canonical copy once pinned. Defaults to other on create.`)
|
|
8472
8561
|
.option("--title <title>", "Page title. Required on create.")
|
|
8473
|
-
.option("--status <status>", "draft, active, or archived.")
|
|
8562
|
+
.option("--status <status>", "draft, active, or archived. Restore with draft and --expected-revision; omit --body to retain content.")
|
|
8474
8563
|
.option("--tags <csv>", "Comma-separated page tags.")
|
|
8475
8564
|
.option("--summary <text>", "Short page summary.")
|
|
8476
8565
|
.option("--body <text>", "Page body (Markdown, supports [[wikilinks]]).")
|
|
@@ -8489,7 +8578,7 @@ Examples:
|
|
|
8489
8578
|
});
|
|
8490
8579
|
}))
|
|
8491
8580
|
.addCommand(addContractFlags(new Command("get")
|
|
8492
|
-
.description("Read one knowledge wiki page by slug or UUID. Read-only, 0 credits.")
|
|
8581
|
+
.description("Read one knowledge wiki page by slug or UUID. Use --view sources for its evidence ledger or --view full for content, links and sources. Read-only, 0 credits.")
|
|
8493
8582
|
.argument("<slug_or_id>", "Page slug or UUID.")
|
|
8494
8583
|
.option("--json", "Print a JSON envelope."), knowledgePageReadContract, { slug: { omit: true }, id: { omit: true } })
|
|
8495
8584
|
.action(async (slugOrId, options) => {
|
|
@@ -8509,14 +8598,22 @@ Examples:
|
|
|
8509
8598
|
}));
|
|
8510
8599
|
}))
|
|
8511
8600
|
.addCommand(new Command("archive")
|
|
8512
|
-
.description("Archive a knowledge wiki page.")
|
|
8601
|
+
.description("Archive a knowledge wiki page while retaining its content and history. Links become unresolved. Restore with page upsert --status draft and the current --expected-revision.")
|
|
8513
8602
|
.argument("<slug_or_id>", "Page slug or UUID.")
|
|
8603
|
+
.option("--expected-revision <n>", "Fail if the current page revision differs from the version you reviewed.")
|
|
8514
8604
|
.option("--json", "Print a JSON envelope.")
|
|
8515
8605
|
.action(async (slugOrId, options) => {
|
|
8516
8606
|
await handleAsyncAction("knowledge page archive", options, async () => {
|
|
8607
|
+
const expectedRevision = readPositiveInt(options.expectedRevision);
|
|
8608
|
+
if (options.expectedRevision !== undefined && (expectedRevision === undefined || !Number.isSafeInteger(expectedRevision))) {
|
|
8609
|
+
throw new OxygenError("invalid_request", "--expected-revision must be a positive integer.", { exitCode: 2 });
|
|
8610
|
+
}
|
|
8517
8611
|
const data = await requestOxygen("/api/cli/knowledge/pages/archive", {
|
|
8518
8612
|
method: "POST",
|
|
8519
|
-
body:
|
|
8613
|
+
body: {
|
|
8614
|
+
...knowledgePageRefBody(slugOrId),
|
|
8615
|
+
...(expectedRevision !== undefined ? { expected_revision: expectedRevision } : {}),
|
|
8616
|
+
},
|
|
8520
8617
|
});
|
|
8521
8618
|
await markKnowledgeMirrorStaleAfterWrite();
|
|
8522
8619
|
return data;
|
|
@@ -8586,7 +8683,7 @@ Examples:
|
|
|
8586
8683
|
.description("Search wiki pages by meaning and keywords. Read-only, 0 credits. Plain language uses hybrid retrieval when available; check match_type and semantic_status. Quotes, OR, and exclusions stay lexical. If the platform-funded embedding allowance is unavailable, keyword search still works.")
|
|
8587
8684
|
.argument("<query>", "Search text. Supports quoted phrases, OR, and -exclude.")
|
|
8588
8685
|
.option("--type <type>", "Filter by page type.")
|
|
8589
|
-
.option("--status <status>", "Filter by draft, active, or archived.")
|
|
8686
|
+
.option("--status <status>", "Filter by draft, active, or archived. Omitted, archived pages are excluded.")
|
|
8590
8687
|
.option("--tags <csv>", "Comma-separated tags that must be present.")
|
|
8591
8688
|
.option("--limit <n>", "Maximum pages to return.")
|
|
8592
8689
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -8758,13 +8855,14 @@ Examples:
|
|
|
8758
8855
|
}))
|
|
8759
8856
|
.addCommand(knowledgeBootstrapCommand)
|
|
8760
8857
|
.addCommand(new Command("synthesize")
|
|
8761
|
-
.description("Synthesize knowledge from workspace evidence (one low-cost AI call, priced as one low-reasoning AI column cell): campaign learnings from auto-filed outreach outcomes, or your writing voice (a voice guide) learned from your sent LinkedIn and WhatsApp messages. Every run spends credits, the preview included, unless fewer than 3 sent messages exist (insufficient_source_messages, nothing charged); without --approved nothing is saved. To keep the guide a preview showed, pass its body back with --approved --body (no new AI call); --approved alone synthesizes and charges again. A
|
|
8858
|
+
.description("Synthesize knowledge from workspace evidence (one low-cost AI call, priced as one low-reasoning AI column cell): campaign learnings from auto-filed outreach outcomes, or your writing voice (a voice guide) learned from your sent LinkedIn and WhatsApp messages. Every run spends credits, the preview included, unless fewer than 3 sent messages exist (insufficient_source_messages, nothing charged); without --approved nothing is saved. To keep the guide a preview showed, pass its body back with --approved --body (no new AI call); --approved alone synthesizes and charges again. A saved voice guide becomes the canonical voice once a human approves its pin proposal (oxygen knowledge proposals approve <id>); it then grounds message, reply, sequence and post drafts and AI columns.")
|
|
8762
8859
|
.option("--kind <kind>", "What to synthesize: campaign_learnings (default) or voice (your writing voice from sent LinkedIn and WhatsApp messages).", "campaign_learnings")
|
|
8763
8860
|
.option("--sequence <sequence_id>", "Scope campaign learnings to one sequence's learnings page. Omit for the org-wide [[outreach-learnings]] page.")
|
|
8764
|
-
.option("--approved", "Write the synthesized result (learnings page revision /
|
|
8765
|
-
.option("--body <markdown>", "With --approved: save this body verbatim (the guide a preview showed, or your edit) with no new AI call.")
|
|
8861
|
+
.option("--approved", "Write the synthesized result (learnings page revision / voice guide saved with a pin proposal) instead of previewing.")
|
|
8862
|
+
.option("--body <markdown>", "With --approved: save this body verbatim (the guide a preview showed, or your edit) with no new AI call, at 0 credits.")
|
|
8766
8863
|
.option("--title <title>", "Optional title for a newly created page/asset.")
|
|
8767
8864
|
.option("--limit <n>", "Voice only: how many recent sent messages to learn from (max 200).")
|
|
8865
|
+
.option("--max-credits <n>", "Hard cap OXYGEN enforces on this call: it is charged exactly its estimate, so never more than n. A higher estimate is refused before any AI call, with nothing charged, and the refusal reports the estimate (--max-credits 0 shows it for free). --approved --body makes no AI call and needs none.")
|
|
8768
8866
|
.option("--json", "Print a JSON envelope.")
|
|
8769
8867
|
.action(async (options) => {
|
|
8770
8868
|
await handleAsyncAction("knowledge synthesize", options, () => requestOxygen("/api/cli/knowledge/synthesize", {
|
|
@@ -8776,6 +8874,7 @@ Examples:
|
|
|
8776
8874
|
...(readOption(options.body) ? { body: readOption(options.body) } : {}),
|
|
8777
8875
|
...(readOption(options.title) ? { title: readOption(options.title) } : {}),
|
|
8778
8876
|
...(readPositiveInt(options.limit) !== undefined ? { limit: readPositiveInt(options.limit) } : {}),
|
|
8877
|
+
...(readNonNegativeNumber(options.maxCredits) !== undefined ? { max_credits: readNonNegativeNumber(options.maxCredits) } : {}),
|
|
8779
8878
|
},
|
|
8780
8879
|
}));
|
|
8781
8880
|
}))
|
|
@@ -9294,13 +9393,14 @@ Examples:
|
|
|
9294
9393
|
}));
|
|
9295
9394
|
}))
|
|
9296
9395
|
.addCommand(new Command("describe")
|
|
9297
|
-
.description("Describe one blueprint (seed or saved) by slug.")
|
|
9396
|
+
.description("Describe one blueprint (seed or saved) by slug. The workflow's compiled bundle is left out (its size and sha256 are listed) unless --include-bundle is passed.")
|
|
9298
9397
|
.argument("<slug>", "Blueprint slug.")
|
|
9398
|
+
.option("--include-bundle", "Include each workflow's compiled bundle (often 300k+ characters).")
|
|
9299
9399
|
.option("--json", "Print a JSON envelope.")
|
|
9300
9400
|
.action(async (slug, options) => {
|
|
9301
9401
|
await handleAsyncAction("blueprints describe", options, () => requestOxygen("/api/cli/blueprints/get", {
|
|
9302
9402
|
method: "POST",
|
|
9303
|
-
body: { slug },
|
|
9403
|
+
body: { slug, ...(options.includeBundle ? { include_bundle: true } : {}) },
|
|
9304
9404
|
}));
|
|
9305
9405
|
}))
|
|
9306
9406
|
.addCommand(new Command("export")
|
|
@@ -9701,9 +9801,9 @@ Examples:
|
|
|
9701
9801
|
.command("columns")
|
|
9702
9802
|
.description("Workspace table column commands. Price first: `tools search --for-table <table>` lists every enrichment this table's columns support with its credit cost per row (0 credits). Ready-made templates (pricing pages, job posts, event pages, 10-Ks, website facts, market questions, text extraction, plus free 0-credit cleanups — company names, job titles, email type, text to list; person questions over an enriched LinkedIn profile and person appearances; company research questions — founders, parent company, funding, cloud provider, offers demos, industry, NAICS, HQ and footprint, LinkedIn/Crunchbase/careers page lookups): `columns catalog`, then `columns add <table> --prompt-key <key> --input <name>=<column>`. Column choice guide: the oxygen-tables skill (`oxygen skills get oxygen-tables --json`); enrichment presets and waterfalls: the oxygen-enrichment skill. With --json parse stdout only: the envelope is stdout, and the spend and cost notes (`estimated N credits for a live run`) are stderr, so merging the streams breaks the JSON.")
|
|
9703
9803
|
.addCommand(new Command("add")
|
|
9704
|
-
.description("Add a nullable column to a workspace table. Writes the definition only — this never runs the column and never spends credits; use `columns run` for that, with --dry-run first to see the cost. For a page, posting or filing the row already links to, or a company fact Oxygen already knows how to research (founders, parent company, funding, cloud provider, offers demos, industry, NAICS, HQ, a LinkedIn or Crunchbase page), pick a template from `columns catalog` (--prompt-key) instead of writing a prompt; or read a URL column with --kind research --research-url. The guide is the oxygen-tables skill (`oxygen skills get oxygen-tables --json`). For ICP fit, use Classify Score: `columns add <table> --label 'ICP fit' --score 'low,medium,high' --prompt 'Assess {{company_name}}: {{description}}, {{employees}} employees' --grounding workspace` (0.075 credits/row). A formula column needs no run at all: it evaluates on read from its current inputs, so `tables query` shows its values immediately.")
|
|
9804
|
+
.description("Add a nullable column, a catalog template (--prompt-key) or a multi-column enrichment bundle (--preset) to a workspace table. Writes the definition only — this never runs the column and never spends credits; use `columns run` for that, with --dry-run first to see the cost. For a page, posting or filing the row already links to, or a company fact Oxygen already knows how to research (founders, parent company, funding, cloud provider, offers demos, industry, NAICS, HQ, a LinkedIn or Crunchbase page), pick a template from `columns catalog` (--prompt-key) instead of writing a prompt; or read a URL column with --kind research --research-url. The guide is the oxygen-tables skill (`oxygen skills get oxygen-tables --json`). For ICP fit, use Classify Score: `columns add <table> --label 'ICP fit' --score 'low,medium,high' --prompt 'Assess {{company_name}}: {{description}}, {{employees}} employees' --grounding workspace` (0.075 credits/row). A formula column needs no run at all: it evaluates on read from its current inputs, so `tables query` shows its values immediately.")
|
|
9705
9805
|
.argument("<table>", "Table id or slug.")
|
|
9706
|
-
.option("--preset <preset>", "Add a pre-built column bundle instead of one column, which is what --prompt-key adds: a preset writes SEVERAL columns at once and needs no --input on a table whose identity column it can find, while --prompt-key adds one template column and binds each input you name. Presets: `person_enrich` (one LinkedIn profile lookup — Blitz first, the LinkedIn Scraper only when Blitz has no match — then first/last/full name, current company and its LinkedIn URL, job title, start date, education, school, headline, bio, location and followers as free stored fields that fill when the lookup runs; the person templates in `columns catalog --category people` all read its payload column), `company_enrich` (one company lookup from a domain or LinkedIn URL — Blitz first, the LinkedIn Scraper only when Blitz has no match, about 1 credit a company — keeping the whole profile in its JSON column plus name, domain, LinkedIn URL, headcount, industry, description, founded year and HQ country as free stored fields; no field choices; binds to the person preset's current_company_linkedin_url automatically), `company_tech_stack` (the technologies a company runs, from its website and hiring), `company_funding` (total raised, latest round, number of rounds and investors), `company_revenue` (an annual revenue estimate), `company_hiring` (the roles a company is hiring for, with a count) — each of these four is one paid lookup column that fills its own columns, priced per row in `tools search --for-table` — or `domain_check` (1 credit/row for a live/redirect/parked/not_found/dead verdict on every domain, cached 30 days, plus a free label column to filter on). For a single cleaned or classified column, use a template from `columns catalog` instead — that is one column, not a bundle. Name the input column with --input: `url` for person_enrich, `domain` and/or `linkedin_url` for the company presets. Any column works whatever it is called; without --input Oxygen guesses from column names. Creating the columns is free; run them afterwards, --dry-run first.")
|
|
9806
|
+
.option("--preset <preset>", "Add a pre-built column bundle instead of one column, which is what --prompt-key adds: a preset writes SEVERAL columns at once and needs no --input on a table whose identity column it can find, while --prompt-key adds one template column and binds each input you name. Presets: `person_enrich` (one LinkedIn profile lookup — Blitz first, the LinkedIn Scraper only when Blitz has no match — then first/last/full name, current company and its LinkedIn URL, job title, start date, education, school, headline, bio, location and followers as free stored fields that fill when the lookup runs, followers only on rows the LinkedIn Scraper answers because Blitz publishes none; the person templates in `columns catalog --category people` all read its payload column), `company_enrich` (one company lookup from a domain or LinkedIn URL — Blitz first, the LinkedIn Scraper only when Blitz has no match, about 1 credit a company — keeping the whole profile in its JSON column plus name, domain, LinkedIn URL, headcount, industry, description, founded year and HQ country as free stored fields; no field choices; binds to the person preset's current_company_linkedin_url automatically), `company_tech_stack` (the technologies a company runs, from its website and hiring), `company_funding` (total raised, latest round, number of rounds and investors), `company_revenue` (an annual revenue estimate), `company_hiring` (the roles a company is hiring for, with a count) — each of these four is one paid lookup column that fills its own columns, priced per row in `tools search --for-table` — or `domain_check` (1 credit/row for a live/redirect/parked/not_found/dead verdict on every domain, cached 30 days, plus a free label column to filter on). For a single cleaned or classified column, use a template from `columns catalog` instead — that is one column, not a bundle. Name the input column with --input: `url` for person_enrich, `domain` and/or `linkedin_url` for the company presets. Any column works whatever it is called; without --input Oxygen guesses from column names. Creating the columns is free; run them afterwards, --dry-run first.")
|
|
9707
9807
|
.option("--input <slot=column...>", "Bind a preset or template input to an exact column, e.g. --input url=pricing_url with --prompt-key, or --input domain=website --input linkedin_url=company_li with --preset. Repeatable. For a preset it is only needed when the automatic match is wrong or missing; for a template it names the column each declared input reads.", collectRepeatable, [])
|
|
9708
9808
|
// No preset takes a field list any more (founder, 2026-09-24). Hidden,
|
|
9709
9809
|
// not removed: an old script passing them gets the server's refusal,
|
|
@@ -9736,8 +9836,8 @@ Examples:
|
|
|
9736
9836
|
.option("--reasoning-level <level>", REASONING_LEVEL_OPTION_HELP)
|
|
9737
9837
|
.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}}.")
|
|
9738
9838
|
.option("--run-condition-columns <csv>", "Comma-separated column keys referenced by --run-condition.")
|
|
9739
|
-
.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.")
|
|
9740
9839
|
.option("--structured-output", "Undo --no-structured-output: let the prompt's named sections shape the answer again.")
|
|
9840
|
+
.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.")
|
|
9741
9841
|
.option("--output-schema-json <json>", "AI column output JSON schema as inline JSON.")
|
|
9742
9842
|
.option("--output-schema-file <path>", "AI column output JSON schema read from a file path.")
|
|
9743
9843
|
.addOption(new Option("--classify <labels>", "Classification column: label every row with one of these comma-separated labels (2-20), answering --prompt. Each cell holds the label, a 0-1 confidence and uncertain:true below --min-confidence (the best guess is kept). Flat ~0.075 credits/row; see `columns run --dry-run`."))
|
|
@@ -10182,8 +10282,10 @@ Examples:
|
|
|
10182
10282
|
.option("--reasoning-level <level>", REASONING_LEVEL_OPTION_HELP)
|
|
10183
10283
|
.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}}.")
|
|
10184
10284
|
.option("--run-condition-columns <csv>", "Comma-separated column keys referenced by --run-condition.")
|
|
10285
|
+
// Positive first keeps omission undefined; negative first defaults true
|
|
10286
|
+
// and silently overwrites a saved prose choice on an unrelated update.
|
|
10287
|
+
.option("--structured-output", "Restore the prompt's named JSON fields. Existing AI columns must store jsonb; for a text column, add a new AI column with --data-type jsonb.")
|
|
10185
10288
|
.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.")
|
|
10186
|
-
.option("--structured-output", "Undo --no-structured-output: let the prompt's named sections shape the answer again.")
|
|
10187
10289
|
.option("--output-schema-json <json>", "AI column output JSON schema as inline JSON.")
|
|
10188
10290
|
.option("--output-schema-file <path>", "AI column output JSON schema read from a file path.")
|
|
10189
10291
|
.option("--research-query <template>", "Research columns: replace the per-row web search query template, e.g. \"{{company_name}} pricing page\".")
|
|
@@ -10210,7 +10312,8 @@ Examples:
|
|
|
10210
10312
|
readOption(options.reasoningLevel) ||
|
|
10211
10313
|
readOption(options.runCondition) ||
|
|
10212
10314
|
readOption(options.outputSchemaJson) ||
|
|
10213
|
-
readOption(options.outputSchemaFile)
|
|
10315
|
+
readOption(options.outputSchemaFile) ||
|
|
10316
|
+
typeof options.structuredOutput === "boolean";
|
|
10214
10317
|
let definition = options.definitionJson
|
|
10215
10318
|
? parseJsonObject(options.definitionJson)
|
|
10216
10319
|
: undefined;
|
|
@@ -10393,10 +10496,10 @@ Examples:
|
|
|
10393
10496
|
});
|
|
10394
10497
|
}))
|
|
10395
10498
|
.addCommand(new Command("catalog")
|
|
10396
|
-
.description("List column templates and native Classify
|
|
10499
|
+
.description("List column templates and native Classify; multi-column enrichment bundles (presets such as person_enrich and company_enrich) are listed by `oxygen enrichment catalog`. For ICP fit, Classify Score (0.075 credits/row) appears in `data.native`, including with `--family classification`; `data.entries` lists separate prompt templates. Templates can be added with `columns add <table> --prompt-key <key>`: the extract templates (pricing-page, job-posting, event, 10-K, website-fact, market and text extractors) and the company research questions (founders, parent company, subsidiaries, funding rounds, market cap, cloud provider, is SaaS, offers demos, industry, NAICS, HQ / states / cities, competitors, LinkedIn / Crunchbase / Glassdoor / careers page lookups) and the person templates (--category people: skill set, grad school, location, job fit and current company read from the person_enrich payload for 15 credits; events, keynotes, podcasts and GitHub profile searched by name), each with the input it reads (bind it with --input <name>=<column_key>), the providers behind it, the answer fields, and the credits per row. Read-only and free. Website contacts (emails and phones from each row's site) is here as `--kind tool`; other tool-backed columns that read a page or call a provider are found with `oxygen tools search <words>`. Provider waterfalls and enrichment bundles are a different list with their own per-row prices: `oxygen enrichment catalog`.")
|
|
10397
10500
|
.option("--category <category>", "Only templates in one picker section: extract (page and text extraction), normalize, organize, summarize (free cleanups and classifications), people (person questions and appearances) or company (company research questions); comma-separate to combine (extract,people). The outreach prompts sit under copy, scoring, qa, research.")
|
|
10398
10501
|
.option("--family <family>", "Only one family. Extract: pricing, job_posting, events, filings, site_facts, market, portfolio, text_extraction. Cleanups: company, person, email, lists, classification. Company: web_presence, corporate_structure, funding, footprint, business_model, tech, market. People: profile, appearances.")
|
|
10399
|
-
.option("--kind <kind>", "Only one column kind: research (reads the web or a page, answer with sources), search (a managed web search whose cell is a page URL or the result list, fixed price), ai (reads row text),
|
|
10502
|
+
.option("--kind <kind>", "Only one column kind: research (reads the web or a page, answer with sources), search (a managed web search whose cell is a page URL or the result list, fixed price), ai (reads row text), formula (free), or tool (a native fetch over a website, such as website contacts).")
|
|
10400
10503
|
.option("--search <term>", "Match a word against template keys, titles and descriptions, e.g. pricing or 10-K.")
|
|
10401
10504
|
.option("--json", "Print a JSON envelope.")
|
|
10402
10505
|
.action(async (options) => {
|
|
@@ -10669,16 +10772,16 @@ Examples:
|
|
|
10669
10772
|
.command("table-ingestions")
|
|
10670
10773
|
.description("Durable background table ingestion commands. To find a table's ingestion runs: `runs list --table <table>`.")
|
|
10671
10774
|
.addCommand(new Command("create-tool-page")
|
|
10672
|
-
.description("Create a paged tool-source ingestion
|
|
10775
|
+
.description("Create a bounded paged tool-source ingestion. LinkedIn ads: --tool adyntel.linkedin_ads --rows-path response.ads --cursor-path response.continuation_token --cursor-request-key continuation_token --upsert-key ad_id. Completed means the approved bound finished; use table-ingestions items to check whether more pages remain.")
|
|
10673
10776
|
.argument("<table>", "Table id or slug.")
|
|
10674
10777
|
.option("--tool <tool_id>", "Tool id such as blitzapi.people_search or hubspot.contacts_list.")
|
|
10675
10778
|
.option("--tool-id <tool_id>", "Alias for --tool.")
|
|
10676
10779
|
.requiredOption("--request-json <json>", "Tool request JSON object for the first page.")
|
|
10677
|
-
.option("--rows-path <path>", "
|
|
10780
|
+
.option("--rows-path <path>", "Path from the whole tool result, including response. Defaults to response.results; LinkedIn/Google ads use response.ads.")
|
|
10678
10781
|
.option("--row-mapping-json <json>", "Object mapping target column keys to dotted source-row paths.")
|
|
10679
|
-
.option("--cursor-path <path>", "
|
|
10782
|
+
.option("--cursor-path <path>", "Path from the whole tool result; Adyntel ads use response.continuation_token.")
|
|
10680
10783
|
.option("--cursor-request-key <path>", "Dotted request key to receive the next cursor.")
|
|
10681
|
-
.option("--max-pages <n>", "Maximum pages
|
|
10784
|
+
.option("--max-pages <n>", "Maximum pages in this item's cursor chain. Defaults to 1; a page limit is not supplier exhaustion.")
|
|
10682
10785
|
.option("--upsert-key <key>", "Column key used to upsert instead of inserting.")
|
|
10683
10786
|
.option("--connection-id <connection_id>", "Optional provider integration connection id.")
|
|
10684
10787
|
.option("--max-concurrency <n>", "Maximum concurrent ingestion items for this run. Defaults to 5.")
|
|
@@ -10731,14 +10834,14 @@ Examples:
|
|
|
10731
10834
|
});
|
|
10732
10835
|
}))
|
|
10733
10836
|
.addCommand(new Command("get")
|
|
10734
|
-
.description("Get one durable table ingestion run.")
|
|
10837
|
+
.description("Get one durable table ingestion run. For provider pages, completed means its bounded work finished; inspect table-ingestions items for the source cursor and exhaustion.")
|
|
10735
10838
|
.argument("<run_id>", "Table ingestion run UUID.")
|
|
10736
10839
|
.option("--json", "Print a JSON envelope.")
|
|
10737
10840
|
.action(async (runId, options) => {
|
|
10738
10841
|
await handleAsyncAction("table-ingestions get", options, () => requestOxygen(`/api/cli/table-ingestion-runs/${encodeURIComponent(runId)}`));
|
|
10739
10842
|
}))
|
|
10740
10843
|
.addCommand(new Command("items")
|
|
10741
|
-
.description("List items
|
|
10844
|
+
.description("List ingestion items, including completed pages. For tool_page, item.output.has_next_cursor=true means more pages remain; item.output.stopped_by=provider_exhausted records cursor exhaustion. item.nextCursor is the saved continuation: start a new approved create-tool-page run in the same Table with that request cursor, the same filters/mapping/upsert key and a new credit cap. Inspect failed or unknown outcomes before retrying.")
|
|
10742
10845
|
.argument("<run_id>", "Table ingestion run UUID.")
|
|
10743
10846
|
.option("--status <status>", "Filter by pending, leased, completed, failed, skipped, or canceled.")
|
|
10744
10847
|
.option("--limit <n>", "Maximum items to return. Defaults to 100.")
|
|
@@ -10756,7 +10859,7 @@ Examples:
|
|
|
10756
10859
|
});
|
|
10757
10860
|
}))
|
|
10758
10861
|
.addCommand(new Command("wait")
|
|
10759
|
-
.description("Wait for a table ingestion run (read-only, no credits). Retries transient network failures, including deploy disconnects,
|
|
10862
|
+
.description("Wait for a table ingestion run (read-only, no credits). A completed bounded run may have more provider pages; inspect table-ingestions items for has_next_cursor, stopped_by and nextCursor. Retries transient network failures, including deploy disconnects, within the same timeout.")
|
|
10760
10863
|
.argument("<run_id>", "Table ingestion run UUID.")
|
|
10761
10864
|
.option("--timeout-seconds <n>", "Maximum time to wait. Defaults to 600.")
|
|
10762
10865
|
.option("--interval-seconds <n>", "Polling interval. Defaults to 5.")
|
|
@@ -11351,7 +11454,7 @@ Examples:
|
|
|
11351
11454
|
}));
|
|
11352
11455
|
}))
|
|
11353
11456
|
.addCommand(new Command("balance")
|
|
11354
|
-
.description("Compare all six subscription sizes in `available_plans`: monthly prices, yearly paid-monthly prices, credit allowances and savings. `annual_monthly_billing_available` says whether yearly paid monthly is offered; `annual_monthly_terms` explains the 12-month commitment, annual renewal, remaining payments after cancellation and plan-change timing. `first_month_offer` is the first-month offer this workspace can still claim: every size it applies to (`plans`, from `minimum_plan` up) with its first-month and regular price before tax, and `ends_at`, null until its 24-hour window starts. The window starts the first time the workspace meets the offer: the app showing it, signup from a browser that saw it on the website, or creating its Checkout with `billing change`. Also show the current plan, commitment, 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 subscription's recurring amount 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` is payment cadence; `commitment` distinguishes yearly paid monthly from monthly and legacy prepaid yearly plans. Credits are granted monthly; `next_credit_grant_at` is the next grant. For a yearly commitment, cancellation stops annual renewal but remaining monthly payments continue until commitment.ends_at. `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.")
|
|
11457
|
+
.description("Compare all six subscription sizes in `available_plans`: monthly prices, yearly paid-monthly prices, credit allowances and savings. `annual_monthly_billing_available` says whether yearly paid monthly is offered; `annual_monthly_terms` explains the 12-month commitment, annual renewal, remaining payments after cancellation and plan-change timing. `first_month_offer` is the first-month offer this workspace can still claim: every size it applies to (`plans`, from `minimum_plan` up) with its first-month and regular price before tax, and `ends_at`, null until its 24-hour window starts. The window starts the first time the workspace meets the offer: the app showing it, signup from a browser that saw it on the website, or creating its Checkout with `billing change`. Also show the current plan, commitment, 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 subscription's recurring amount 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` is payment cadence; `commitment` distinguishes yearly paid monthly from monthly and legacy prepaid yearly plans. Credits are granted monthly; `next_credit_grant_at` is the next grant. For a yearly commitment, cancellation stops annual renewal but remaining monthly payments continue until commitment.ends_at. `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. 0 credits; reserves nothing. Due plan grants or credit expiry may apply.")
|
|
11355
11458
|
.option("--json", "Print a JSON envelope.")
|
|
11356
11459
|
.action(async (options) => {
|
|
11357
11460
|
await handleAsyncAction("billing balance", options, () => requestOxygen("/api/cli/billing/balance"));
|
|
@@ -11396,7 +11499,7 @@ Examples:
|
|
|
11396
11499
|
await handleAsyncAction("billing seats", options, () => requestOxygen("/api/cli/billing/seats"));
|
|
11397
11500
|
})
|
|
11398
11501
|
.addCommand(new Command("set")
|
|
11399
|
-
.description("
|
|
11502
|
+
.description("Manage legacy dollar sending seats for one channel; check `billing seats` for retirement status. 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.")
|
|
11400
11503
|
.requiredOption("--seat-key <kind>", "linkedin, whatsapp, phone_number, or email_sender.")
|
|
11401
11504
|
.requiredOption("--quantity <n>", "Total seats to hold for this channel, 0 or more.")
|
|
11402
11505
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -11416,14 +11519,14 @@ Examples:
|
|
|
11416
11519
|
await handleAsyncAction("billing invoices", options, () => requestOxygen("/api/cli/billing/invoices"));
|
|
11417
11520
|
}))
|
|
11418
11521
|
.addCommand(new Command("allowance")
|
|
11419
|
-
.description("Show this billing cycle's credit pool as one breakdown: fixed spend, flexible spend, credits committed to the next renewal, and what is free to spend — the numbers behind the bar on the credit-usage page. The parts always sum to the total, with ONE deliberate exception: `renewals_past_due` (and its region=past_due segment) reports renewals that FAILED to charge, which is money not taken and therefore outside total_credits. fixed_spent_credits is what has ALREADY been charged this cycle, NOT your monthly run-rate — for the forward figure billed at the next renewal see `billing commitments`, which is normally much larger. For organization wallet scope, reserved_credits is in-flight spend already removed from the available balance: do not subtract it again from free_to_spend_credits. Reserved credits can exceed the remaining free balance without implying an overdraft. For workspace_cap scope, free_to_spend_credits is remaining allowance, not the billing owner's wallet; live execution still checks that wallet and spend policies. Every segment carries region=fixed|flexible.
|
|
11522
|
+
.description("Show this billing cycle's credit pool as one breakdown: fixed spend, flexible spend, credits committed to the next renewal, and what is free to spend — the numbers behind the bar on the credit-usage page. The parts always sum to the total, with ONE deliberate exception: `renewals_past_due` (and its region=past_due segment) reports renewals that FAILED to charge, which is money not taken and therefore outside total_credits. fixed_spent_credits is what has ALREADY been charged this cycle, NOT your monthly run-rate — for the forward figure billed at the next renewal see `billing commitments`, which is normally much larger. For organization wallet scope, reserved_credits is in-flight spend already removed from the available balance: do not subtract it again from free_to_spend_credits. Reserved credits can exceed the remaining free balance without implying an overdraft. For workspace_cap scope, free_to_spend_credits is remaining allowance, not the billing owner's wallet; live execution still checks that wallet and spend policies. Every segment carries region=fixed|flexible. 0 credits; reserves nothing. Due plan grants or credit expiry may apply.")
|
|
11420
11523
|
.option("--json", "Print a JSON envelope.")
|
|
11421
11524
|
.action(async (options) => {
|
|
11422
11525
|
await handleAsyncAction("billing allowance", options, () => requestOxygen("/api/cli/billing/allowance"));
|
|
11423
11526
|
}))
|
|
11424
11527
|
.addCommand(new Command("usage")
|
|
11425
11528
|
.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 connectivity 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. managed_credits_used counts captured credits; for where credits went by category, provider or tool, with shares, run `billing audit`.")
|
|
11426
|
-
.option("--meter <meter>", "credits, automation_actions, byok_calls, provider_seats (rolling-30d peak connected accounts and the flat per-account charge; each account's reservation is listed by `billing commitments`), or holds (each held reservation behind reserved_credits in this workspace: credits, run, why it is held (an unmarked LinkedIn lookup hold cannot say whether its result was unknown or delivered), and automatic_return:
|
|
11529
|
+
.option("--meter <meter>", "credits, automation_actions, byok_calls, provider_seats (rolling-30d peak connected accounts and the flat per-account charge; each account's reservation is listed by `billing commitments`), or holds (each held reservation behind reserved_credits in this workspace: credits, run, why it is held (an unmarked LinkedIn lookup hold cannot say whether its result was unknown or delivered), and automatic_return: LinkedIn lookup holds from 24 hours to 30 days old and explicitly unknown LeadMagic email-validation holds older than 24 hours are eligible while automatic return is switched on, and an hourly release returns it on a later pass with no fixed deadline; read-only, newest first, --days up to 365 and --limit apply). Defaults to credits.")
|
|
11427
11530
|
.option("--days <n>", "Lookback window in days. Defaults to 30.")
|
|
11428
11531
|
.option("--from <iso>", "Only include ledger events at or after this ISO timestamp.")
|
|
11429
11532
|
.option("--to <iso>", "Only include ledger events at or before this ISO timestamp.")
|
|
@@ -11451,7 +11554,7 @@ Examples:
|
|
|
11451
11554
|
});
|
|
11452
11555
|
}))
|
|
11453
11556
|
.addCommand(new Command("audit")
|
|
11454
|
-
.description("Summarize where managed credits were granted, reserved, captured, released, or spent. Every result lists spend_by_category (each spend area's credits and share of the window's spend, largest first) and totals fixed_credits_spent/flexible_credits_spent over the whole window, whatever --group-by is. groups_truncated says when --limit cut grouped rows, and managed_credits_spent_outside_groups how much they carried. Spent is reserved minus released, so it
|
|
11557
|
+
.description("Summarize where managed credits were granted, reserved, captured, released, or spent. Every result lists spend_by_category (each spend area's credits and share of the window's spend, largest first) and totals fixed_credits_spent/flexible_credits_spent over the whole window, whatever --group-by is. groups_truncated says when --limit cut grouped rows, and managed_credits_spent_outside_groups how much they carried. Spent is reserved minus released in the window, so it differs from totals.managed_credits_captured (`billing usage`'s managed_credits_used) by holds still open (reserved_credits) and holds settling across the window edge. byok_usage_events counts only nonzero rows unless --type byok. 0 credits; reserves nothing. Due plan grants or credit expiry may apply.")
|
|
11455
11558
|
.option("--days <n>", "Lookback window in days. Defaults to 365.")
|
|
11456
11559
|
.option("--from <iso>", "Only include ledger events at or after this ISO timestamp.")
|
|
11457
11560
|
.option("--to <iso>", "Only include ledger events at or before this ISO timestamp.")
|
|
@@ -11860,6 +11963,49 @@ Examples:
|
|
|
11860
11963
|
return requestOxygen(`/api/cli/admin/costs${suffix}`);
|
|
11861
11964
|
});
|
|
11862
11965
|
}))
|
|
11966
|
+
.addCommand(new Command("price-book")
|
|
11967
|
+
.description("The runtime prices charging reads from the control DB: recurring charges, plan allowances and scalars, each with its committed default and whether staff may edit it. An edit reaches every instance within 60 seconds. Staff only.")
|
|
11968
|
+
.addCommand(new Command("list")
|
|
11969
|
+
.description("List every pricing-book row with its stored value, what charging bills now, the committed default, and who reads it. Read-only.")
|
|
11970
|
+
.option("--json", "Print a JSON envelope.")
|
|
11971
|
+
.action(async (options) => {
|
|
11972
|
+
await handleAsyncAction("admin price-book list", options, () => requestOxygen("/api/cli/admin/costs?section=pricing_book"));
|
|
11973
|
+
}))
|
|
11974
|
+
.addCommand(new Command("set")
|
|
11975
|
+
.description("Set one editable pricing-book field. A charge takes --field credits or active; a plan takes monthly_credits, weekly_credits_limit or rollover_cap (value none for no limit); a scalar takes value. Rows charging does not read from the book are refused.")
|
|
11976
|
+
.requiredOption("--table <table>", "charge, plan, or scalar.")
|
|
11977
|
+
.requiredOption("--key <key>", "The row key, e.g. addon.egress_dedicated_ip, oxygen_499, markup.copilot_inference.")
|
|
11978
|
+
.requiredOption("--field <field>", "credits | active (charge); monthly_credits | weekly_credits_limit | rollover_cap (plan); value (scalar).")
|
|
11979
|
+
.requiredOption("--value <value>", "A non-negative number, true/false for active, or none for no plan limit.")
|
|
11980
|
+
.option("--json", "Print a JSON envelope.")
|
|
11981
|
+
.action(async (options) => {
|
|
11982
|
+
await handleAsyncAction("admin price-book set", options, () => requestOxygen("/api/cli/admin/costs", {
|
|
11983
|
+
method: "POST",
|
|
11984
|
+
body: {
|
|
11985
|
+
price_table: options.table,
|
|
11986
|
+
price_key: options.key,
|
|
11987
|
+
price_field: options.field,
|
|
11988
|
+
value: readPriceBookValue(options.value),
|
|
11989
|
+
},
|
|
11990
|
+
}));
|
|
11991
|
+
}))
|
|
11992
|
+
.addCommand(new Command("restore")
|
|
11993
|
+
.description("Put a pricing-book row back to its committed default. For a plan, --field restores one allowance field; without it all three are restored.")
|
|
11994
|
+
.requiredOption("--table <table>", "charge, plan, or scalar.")
|
|
11995
|
+
.requiredOption("--key <key>", "The row key.")
|
|
11996
|
+
.option("--field <field>", "Plan only: monthly_credits, weekly_credits_limit or rollover_cap.")
|
|
11997
|
+
.option("--json", "Print a JSON envelope.")
|
|
11998
|
+
.action(async (options) => {
|
|
11999
|
+
await handleAsyncAction("admin price-book restore", options, () => requestOxygen("/api/cli/admin/costs", {
|
|
12000
|
+
method: "POST",
|
|
12001
|
+
body: {
|
|
12002
|
+
price_table: options.table,
|
|
12003
|
+
price_key: options.key,
|
|
12004
|
+
...(options.field ? { price_field: options.field } : {}),
|
|
12005
|
+
restore: true,
|
|
12006
|
+
},
|
|
12007
|
+
}));
|
|
12008
|
+
})))
|
|
11863
12009
|
.addCommand(new Command("primary-providers")
|
|
11864
12010
|
.description("Board of the PRIMARY managed external data providers (enrichment, people/company search, web search, scraping, signals, AI-column web grounding, LLM inference): per provider health probe, 7d traffic, balance, 30d/7d COGS, rate policy, spend ceilings, breaker, and posture, plus the platform runaway guards and how old each snapshot is. Reads the snapshots the crons write; --refresh re-runs those same zero-credit snapshots first. Staff only.")
|
|
11865
12011
|
.option("--refresh", "Re-run the zero-credit snapshot work the three crons run (provider health probes, managed balances, cost snapshot) before reading the board. No paid provider call and no credit spend; takes up to a few minutes.")
|
|
@@ -13072,7 +13218,7 @@ Examples:
|
|
|
13072
13218
|
.option("--return <mode>", "Legacy response shape: raw, compact, or summary. Defaults to raw.")
|
|
13073
13219
|
.option("--return-mode <mode>", "Response shape: raw, compact, or summary. Prefer summary for large search responses.")
|
|
13074
13220
|
.option("--oxygen-cursor <cursor>", "Short Oxygen cursor returned as oxygen_next_cursor by a previous tool run.")
|
|
13075
|
-
.option("--max-credits <n>", "Credit ceiling for live paid tools; not needed for no-bill tools, and
|
|
13221
|
+
.option("--max-credits <n>", "Credit ceiling for live paid tools; not needed for no-bill tools. 0 runs no-bill operations, which may call providers, and reuses a managed lookup's cached result for the exact same input for free (the dry run's preview.cache says whether one exists). A paid cache miss at 0 calls and charges nothing.")
|
|
13076
13222
|
.option("--approved", "Required for live paid tools and external writes after inspecting dry-run output.")
|
|
13077
13223
|
.option("--json", "Print a JSON envelope.")
|
|
13078
13224
|
.action(async (toolId, options) => {
|
|
@@ -13205,7 +13351,7 @@ Examples:
|
|
|
13205
13351
|
.option("--company-domain <domain>", "Company apex domain, e.g. acme.com.")
|
|
13206
13352
|
.option("--company-name <name>", "Company name.")
|
|
13207
13353
|
.option("--mode <mode>", "dry_run (default) previews the plan and spends nothing; live spends credits and returns the answer.")
|
|
13208
|
-
.option("--max-credits <credits>", "Spend ceiling. Required for --mode live. Run the free dry_run first: it returns estimate.recommended_max_credits, the ceiling that lets the WHOLE resolved waterfall run, plus estimate.min_credits and a per-leg breakdown. When the resolved chain runs the guessed-address pass (the name+domain and first/last+domain profiles, not the LinkedIn-URL one) the block also carries estimate.pattern_guess_verification_credits — the verification checks that pass bills before any named provider is dialled. A lower ceiling does NOT stop the run: each lane priced above what is left of it is refused on its own (spend_cap_too_low) and the waterfall advances, so a cheaper lane further down the chain can still run and bill.")
|
|
13354
|
+
.option("--max-credits <credits>", "Spend ceiling in Oxygen credits; a lane that runs on your own provider key bills your provider account and is outside it (cap that with `oxygen limits provider set`). Required for --mode live. Run the free dry_run first: it returns estimate.recommended_max_credits, the ceiling that lets the WHOLE resolved waterfall run, plus estimate.min_credits and a per-leg breakdown. When the resolved chain runs the guessed-address pass (the name+domain and first/last+domain profiles, not the LinkedIn-URL one) the block also carries estimate.pattern_guess_verification_credits — the verification checks that pass bills before any named provider is dialled. A lower ceiling does NOT stop the run: each lane priced above what is left of it is refused on its own (spend_cap_too_low) and the waterfall advances, so a cheaper lane further down the chain can still run and bill.")
|
|
13209
13355
|
.option("--json", "Print a JSON envelope.")
|
|
13210
13356
|
.action(async (options) => {
|
|
13211
13357
|
await handleAsyncAction("find email", options, () => requestOxygen("/api/cli/find/run", { method: "POST", body: buildFindBody("email", options) }));
|
|
@@ -13218,7 +13364,7 @@ Examples:
|
|
|
13218
13364
|
.option("--company-domain <domain>", "Company apex domain, e.g. acme.com.")
|
|
13219
13365
|
.option("--company-name <name>", "Company name.")
|
|
13220
13366
|
.option("--mode <mode>", "dry_run (default) previews the plan and spends nothing; live spends credits and returns the answer.")
|
|
13221
|
-
.option("--max-credits <credits>", "Spend ceiling. Required for --mode live. Run the free dry_run first: it returns estimate.recommended_max_credits, the ceiling that lets the WHOLE resolved waterfall run, plus estimate.min_credits and a per-leg breakdown. A lower ceiling does NOT stop the run: each lane priced above what is left of it is refused on its own (spend_cap_too_low) and the waterfall advances, so a cheaper lane further down the chain can still run and bill.")
|
|
13367
|
+
.option("--max-credits <credits>", "Spend ceiling in Oxygen credits; a lane that runs on your own provider key bills your provider account and is outside it (cap that with `oxygen limits provider set`). Required for --mode live. Run the free dry_run first: it returns estimate.recommended_max_credits, the ceiling that lets the WHOLE resolved waterfall run, plus estimate.min_credits and a per-leg breakdown. A lower ceiling does NOT stop the run: each lane priced above what is left of it is refused on its own (spend_cap_too_low) and the waterfall advances, so a cheaper lane further down the chain can still run and bill.")
|
|
13222
13368
|
.option("--json", "Print a JSON envelope.")
|
|
13223
13369
|
.action(async (options) => {
|
|
13224
13370
|
await handleAsyncAction("find personal-email", options, () => requestOxygen("/api/cli/find/run", { method: "POST", body: buildFindBody("personal_email", options) }));
|
|
@@ -13232,7 +13378,7 @@ Examples:
|
|
|
13232
13378
|
.option("--company-name <name>", "Company name.")
|
|
13233
13379
|
.option("--verify", "Verify the found number with ClearoutPhone — adds line_type/carrier and keeps the number on a non-verdict (only a genuine 'not valid' is discarded).")
|
|
13234
13380
|
.option("--mode <mode>", "dry_run (default) previews the plan and spends nothing; live spends credits and returns the answer.")
|
|
13235
|
-
.option("--max-credits <credits>", "Spend ceiling. Required for --mode live. Run the free dry_run first: it returns estimate.recommended_max_credits, the ceiling that lets the WHOLE resolved waterfall run, plus estimate.min_credits and a per-leg breakdown. A lower ceiling does NOT stop the run: each lane priced above what is left of it is refused on its own (spend_cap_too_low) and the waterfall advances, so a cheaper lane further down the chain can still run and bill.")
|
|
13381
|
+
.option("--max-credits <credits>", "Spend ceiling in Oxygen credits; a lane that runs on your own provider key bills your provider account and is outside it (cap that with `oxygen limits provider set`). Required for --mode live. Run the free dry_run first: it returns estimate.recommended_max_credits, the ceiling that lets the WHOLE resolved waterfall run, plus estimate.min_credits and a per-leg breakdown. A lower ceiling does NOT stop the run: each lane priced above what is left of it is refused on its own (spend_cap_too_low) and the waterfall advances, so a cheaper lane further down the chain can still run and bill.")
|
|
13236
13382
|
.option("--json", "Print a JSON envelope.")
|
|
13237
13383
|
.action(async (options) => {
|
|
13238
13384
|
await handleAsyncAction("find phone", options, () => requestOxygen("/api/cli/find/run", { method: "POST", body: buildFindBody("phone", options) }));
|
|
@@ -13245,7 +13391,7 @@ Examples:
|
|
|
13245
13391
|
.option("--company-name <name>", "Company name.")
|
|
13246
13392
|
.option("--company-linkedin-url <url>", "Company LinkedIn URL.")
|
|
13247
13393
|
.option("--mode <mode>", "dry_run (default) previews the plan and spends nothing; live spends credits and returns the answer.")
|
|
13248
|
-
.option("--max-credits <credits>", "Spend ceiling. Required for --mode live. Run the free dry_run first: it returns estimate.recommended_max_credits, the ceiling that lets the WHOLE resolved waterfall run, plus estimate.min_credits and a per-leg breakdown. A lower ceiling does NOT stop the run: each lane priced above what is left of it is refused on its own (spend_cap_too_low) and the waterfall advances, so a cheaper lane further down the chain can still run and bill.")
|
|
13394
|
+
.option("--max-credits <credits>", "Spend ceiling in Oxygen credits; a lane that runs on your own provider key bills your provider account and is outside it (cap that with `oxygen limits provider set`). Required for --mode live. Run the free dry_run first: it returns estimate.recommended_max_credits, the ceiling that lets the WHOLE resolved waterfall run, plus estimate.min_credits and a per-leg breakdown. A lower ceiling does NOT stop the run: each lane priced above what is left of it is refused on its own (spend_cap_too_low) and the waterfall advances, so a cheaper lane further down the chain can still run and bill.")
|
|
13249
13395
|
.option("--json", "Print a JSON envelope.")
|
|
13250
13396
|
.action(async (options) => {
|
|
13251
13397
|
await handleAsyncAction("find linkedin", options, () => requestOxygen("/api/cli/find/run", { method: "POST", body: buildFindBody("linkedin", options) }));
|
|
@@ -13263,7 +13409,7 @@ Examples:
|
|
|
13263
13409
|
.option("--company-name <name>", "Company name.")
|
|
13264
13410
|
.option("--include-raw", "Also return the provider's raw profile payload (profile_raw) for fields the profile block does not carry.")
|
|
13265
13411
|
.option("--mode <mode>", "dry_run (default) previews the plan and spends nothing; live spends credits and returns the profile.")
|
|
13266
|
-
.option("--max-credits <credits>", "Spend ceiling. Required for --mode live. Run the free dry_run first: estimate.recommended_max_credits funds the profile lookup plus, for an email or name input, every lane of the LinkedIn URL waterfall that resolves it.")
|
|
13412
|
+
.option("--max-credits <credits>", "Spend ceiling in Oxygen credits; a lane that runs on your own provider key bills your provider account and is outside it (cap that with `oxygen limits provider set`). Required for --mode live. Run the free dry_run first: estimate.recommended_max_credits funds the profile lookup plus, for an email or name input, every lane of the LinkedIn URL waterfall that resolves it.")
|
|
13267
13413
|
.option("--json", "Print a JSON envelope.")
|
|
13268
13414
|
.action(async (options) => {
|
|
13269
13415
|
await handleAsyncAction("find person", options, () => requestOxygen("/api/cli/find/run", { method: "POST", body: buildFindBody("person", options) }));
|
|
@@ -13277,7 +13423,7 @@ Examples:
|
|
|
13277
13423
|
.option("--list-fields", "Print the field catalog (category, description, provider count, per-row credits: free / fixed / ~estimated) and exit. Needs no identity; spends nothing.")
|
|
13278
13424
|
.option("--check-technologies <names>", "Comma-separated technology names for the technology_check field, answered yes/no against the detected stack (e.g. hubspot,salesforce).")
|
|
13279
13425
|
.option("--mode <mode>", "dry_run (default) previews the plan and spends nothing; live spends credits and returns the answer.")
|
|
13280
|
-
.option("--max-credits <credits>", "Spend ceiling. Required for --mode live; 0
|
|
13426
|
+
.option("--max-credits <credits>", "Spend ceiling in Oxygen credits; a lane that runs on your own provider key bills your provider account and is outside it (cap that with `oxygen limits provider set`). Required for --mode live; 0 can still call providers on zero-credit lanes and reuse any priced lane this workspace already has a cached answer for (the other priced lanes are skipped as credit_ceiling_reached). Run the free dry_run first: it returns estimate.recommended_max_credits, the ceiling that funds every lane in the plan, and estimate.min_credits, what the plan costs if each field is answered by its first lane.")
|
|
13281
13427
|
.option("--json", "Print a JSON envelope.")
|
|
13282
13428
|
.action(async (options) => {
|
|
13283
13429
|
await handleAsyncAction("find company", options, () => requestOxygen("/api/cli/find/run", { method: "POST", body: buildFindBody("company", options) }));
|
|
@@ -13716,7 +13862,7 @@ Examples:
|
|
|
13716
13862
|
await handleAsyncAction("senders checkpoints", options, () => requestOxygen("/api/cli/senders/checkpoints"));
|
|
13717
13863
|
}))
|
|
13718
13864
|
.addCommand(new Command("connect")
|
|
13719
|
-
.description("Get a hosted sign-in link to connect a new LinkedIn account (or reconnect with --reconnect). This command creates a pending provider authorization. Inspect prices first with `oxygen billing commitments --view connection-prices --json`, which creates no authorization.
|
|
13865
|
+
.description("Get a hosted sign-in link to connect a new LinkedIn account (or reconnect with --reconnect). This command creates a pending provider authorization. Inspect prices first with `oxygen billing commitments --view connection-prices --json`, which creates no authorization. A covering dollar seat and a credit reservation are alternative billing rails. Read the seat coverage and actual commitment before relying on the next monthly billing date, especially on reconnect. Once the workspace is repriced that reservation is 3,400 credits ($34) monthly; with the credit gate active, a connect the balance cannot cover fails with insufficient_credits. Invites, messages and other LinkedIn actions cost 0 credits on top. `oxygen billing seats --json` shows held seats and next-connection eligibility; paying-seat coverage may remain unknown. Inspect `oxygen billing commitments --json` for an account’s actual reservation, and `oxygen billing estimate --linkedin-accounts <n> --json` prices n accounts against your plan. 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 in the hosted sign-in wizard; their secrets never pass through OXYGEN. Use --count for bulk onboarding; --count above 1 needs --owner-consent (each owner connects their own account). Links are shareable and valid for 10 minutes.")
|
|
13720
13866
|
.option("--reconnect <connection_id>", "Reconnect an existing connection instead of creating a new one. Accepts a connection id.")
|
|
13721
13867
|
.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); one of the 56 countries OXYGEN has LinkedIn proxies in.")
|
|
13722
13868
|
.option("--sales-nav", "Request Classic + Sales Navigator access during hosted sign-in.")
|
|
@@ -13803,7 +13949,7 @@ Examples:
|
|
|
13803
13949
|
}));
|
|
13804
13950
|
}))
|
|
13805
13951
|
.addCommand(new Command("health")
|
|
13806
|
-
.description("Show a one-call health snapshot for a LinkedIn sender: status, the latest error reason, any open security checkpoint, today's usage, the warm-up ramp,
|
|
13952
|
+
.description("Show a one-call health snapshot for a LinkedIn sender: status, the latest error reason, any open security checkpoint, today's usage, the warm-up ramp, when the daily quota resets, and inbox sync (last sync, latest failure, back-off). Read-only — no provider call, no credits. <id> accepts a sender account id, connection id, or Unipile account id.")
|
|
13807
13953
|
.argument("<id>", "Sender account id, connection id, or Unipile account id.")
|
|
13808
13954
|
.option("--json", "Print a JSON envelope.")
|
|
13809
13955
|
.action(async (id, options) => {
|
|
@@ -13842,7 +13988,7 @@ Examples:
|
|
|
13842
13988
|
});
|
|
13843
13989
|
}))
|
|
13844
13990
|
.addCommand(new Command("disconnect")
|
|
13845
|
-
.description("Disconnect a LinkedIn sender account so it stops sending
|
|
13991
|
+
.description("Disconnect a LinkedIn sender account so it stops sending. Works on an account that is already disconnected. <id> accepts a sender account id, connection id, or Unipile account id.")
|
|
13846
13992
|
.argument("<id>", "Sender account id, connection id, or Unipile account id.")
|
|
13847
13993
|
.option("--json", "Print a JSON envelope.")
|
|
13848
13994
|
.action(async (id, options) => {
|
|
@@ -14467,7 +14613,7 @@ Examples:
|
|
|
14467
14613
|
program.addCommand(new Command("linkedin")
|
|
14468
14614
|
.description("Put your own LinkedIn network in a table and keep it current. `linkedin network setup` captures everyone you are connected to and everyone who follows you into one self-refreshing 'LinkedIn Network' table (free, read-only on your account, paced so the account is never at risk); `linkedin network status` reports the pace and `linkedin network pause` switches it off. `linkedin invitations setup` does the same for open connection requests — received and sent — in one self-updating table. Also home to `linkedin intent` (engagement capture) and `linkedin ingestion status` (every LinkedIn read drip and each account's remaining read budget).")
|
|
14469
14615
|
.addCommand(new Command("network")
|
|
14470
|
-
.description("Capture everyone already in your LinkedIn network \u2014 the people you are connected to and the people who follow you \u2014 into one self-refreshing workspace table you can filter, enrich and enroll from. READ-ONLY on your LinkedIn account: it lists your own connections and followers and nothing else. It sends no messages, no connection requests, and does not visit anyone's profile. Runs as a slow background drip on its OWN per-account read budget, so it cannot slow down or starve anything else the account does: about 2,000 people a day while it first walks your network (40 reads of up to 50 people), then a light check a few times a day for new arrivals. Only inside each account's own 07:00-22:00 working hours, spaced minutes apart, never in a burst; a newly armed account eases in over its first two weeks. LinkedIn does not say how big a network is until the walk has been through it, so a large network takes days \u2014 `status` reports the rate, not a completion date it cannot know. Free \u2014 no Oxygen credits; it uses
|
|
14616
|
+
.description("Capture everyone already in your LinkedIn network \u2014 the people you are connected to and the people who follow you \u2014 into one self-refreshing workspace table you can filter, enrich and enroll from. READ-ONLY on your LinkedIn account: it lists your own connections and followers and nothing else. It sends no messages, no connection requests, and does not visit anyone's profile. Runs as a slow background drip on its OWN per-account read budget, so it cannot slow down or starve anything else the account does: about 2,000 people a day while it first walks your network (40 reads of up to 50 people), then a light check a few times a day for new arrivals. Only inside each account's own 07:00-22:00 working hours, spaced minutes apart, never in a burst; a newly armed account eases in over its first two weeks. LinkedIn does not say how big a network is until the walk has been through it, so a large network takes days \u2014 `status` reports the rate, not a completion date it cannot know. Free \u2014 no Oxygen credits for this read; it uses your connected LinkedIn account, whose seat or credit reservation has its own terms.")
|
|
14471
14617
|
.addCommand(new Command("setup")
|
|
14472
14618
|
.description("Arm (or re-arm) network capture. Creates or reuses one merged 'LinkedIn Network' table and starts a connections drip and a followers drip per account, both writing into that one table deduped on LinkedIn member id \u2014 somebody who is both shows as 'both' in the Relationship column. This is a STANDING permission: capture keeps running until you switch it off with `linkedin network pause`, and a LinkedIn account you connect later is armed automatically. A newly connected account eases into full read speed over its first two weeks. Nothing is sent and no credits are charged. The response names each account it armed (`senders[].display_name`) \u2014 check it is yours before leaving it running.")
|
|
14473
14619
|
.option("--account <ref>", "Account to capture (sender id, connection id, or Unipile account id). Omit to capture every connected LinkedIn account.")
|
|
@@ -14676,7 +14822,7 @@ Examples:
|
|
|
14676
14822
|
});
|
|
14677
14823
|
}))));
|
|
14678
14824
|
program.addCommand(new Command("whatsapp")
|
|
14679
|
-
.description("Manage the org's connected WhatsApp accounts for Sequencer: list, connect, sync, inspect, disconnect, tune daily limits, and warm-send into an existing conversation. Cold outbound runs through a WhatsApp sequence (sequences create --channels whatsapp).")
|
|
14825
|
+
.description("Manage the org's connected WhatsApp accounts for Sequencer: accounts (list), connect, sync, inspect, disconnect, tune daily limits, and warm-send into an existing conversation. Cold outbound runs through a WhatsApp sequence (sequences create --channels whatsapp).")
|
|
14680
14826
|
.addCommand(addContractFlags(new Command("accounts")
|
|
14681
14827
|
.description("Read stored WhatsApp account health. Disconnected accounts are hidden by default; --include-disconnected includes retained accounts, --status selects one status. Presence is not send readiness. Use summary and exact account-id/value for long public metadata."), WHATSAPP_ACCOUNTS_READ_CONTRACT, WHATSAPP_ACCOUNT_FLAGS)
|
|
14682
14828
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -14725,7 +14871,7 @@ Examples:
|
|
|
14725
14871
|
});
|
|
14726
14872
|
}))
|
|
14727
14873
|
.addCommand(new Command("disconnect")
|
|
14728
|
-
.description("Disconnect a WhatsApp account so it stops sending
|
|
14874
|
+
.description("Disconnect a WhatsApp account so it stops sending. Works on an account that is already disconnected. <id> accepts an account id, connection id, or Unipile account id.")
|
|
14729
14875
|
.argument("<id>", "WhatsApp account id, connection id, or Unipile account id.")
|
|
14730
14876
|
.option("--json", "Print a JSON envelope.")
|
|
14731
14877
|
.action(async (id, options) => {
|
|
@@ -14740,14 +14886,9 @@ Examples:
|
|
|
14740
14886
|
.action(async (id, options) => {
|
|
14741
14887
|
await handleAsyncAction("whatsapp limits get", options, () => requestOxygen(`/api/cli/whatsapp/accounts/${encodeURIComponent(id)}/limits`));
|
|
14742
14888
|
}))
|
|
14743
|
-
.addCommand(new Command("set")
|
|
14889
|
+
.addCommand(addWhatsAppLimitFlags(new Command("set")
|
|
14744
14890
|
.description("Adjust per-account WhatsApp daily limits and the daily-reset timezone. Values are clamped to safe maximums; the warm-up ceiling still applies to direct and Sequence traffic. <id> accepts an account id, connection id, or Unipile account id.")
|
|
14745
|
-
.argument("<id>", "WhatsApp account id, connection id, or Unipile account id.")
|
|
14746
|
-
.option("--messages-per-day <n>", "Daily WhatsApp messages cap across direct and Sequence traffic (subject to the warm-up ceiling).")
|
|
14747
|
-
.option("--total-actions-per-day <n>", "Daily cap across all WhatsApp action types.")
|
|
14748
|
-
.option("--messages-reads-per-day <n>", "Daily cap on chat and message-history reads.")
|
|
14749
|
-
.option("--min-spacing-seconds <n>", "Minimum seconds between actions.")
|
|
14750
|
-
.option("--spacing-jitter-seconds <n>", "Random jitter seconds added to action spacing.")
|
|
14891
|
+
.argument("<id>", "WhatsApp account id, connection id, or Unipile account id."))
|
|
14751
14892
|
.option("--working-days <days>", "Allowed weekdays, e.g. mon-fri or 1,3,5.")
|
|
14752
14893
|
.option("--working-hours-start <HH:MM>", "Local start of the allowed sending window.")
|
|
14753
14894
|
.option("--working-hours-end <HH:MM>", "Local end of the allowed sending window.")
|
|
@@ -14985,10 +15126,12 @@ Examples:
|
|
|
14985
15126
|
});
|
|
14986
15127
|
}))
|
|
14987
15128
|
.addCommand(new Command("get")
|
|
14988
|
-
.description("Get one conversation with its full message thread. <conversation> accepts a conversation id, Unipile chat id, or (email) Zapbox thread id, and is resolved on any channel by default (email first, then LinkedIn/WhatsApp). Opening a WhatsApp thread may fetch provider history and
|
|
15129
|
+
.description("Get one conversation with its full message thread. <conversation> accepts a conversation id, Unipile chat id, or (email) Zapbox thread id, and is resolved on any channel by default (email first, then LinkedIn/WhatsApp). Opening a LinkedIn or WhatsApp thread may fetch provider history and update the local mirror. WhatsApp also merges provider-confirmed phone/contact identities. This uses no Oxygen credits and sends no message; old conversation references remain usable. Use --stored-only to read the mirror without provider refresh. --sender-account-id retains that participant’s DM privacy window.")
|
|
14989
15130
|
.argument("<conversation>", "Conversation id, Unipile chat id, or Zapbox thread id.")
|
|
14990
15131
|
.option("--channel <channel>", "Inbox channel: all (default, resolves the id on any channel), email, linkedin, or whatsapp.")
|
|
14991
15132
|
.option("--message-limit <n>", "Maximum messages to return (1-500). Defaults to 100.")
|
|
15133
|
+
.option("--sender-account-id <ids>", "Scope DM history to participant UUIDs, comma-separated.")
|
|
15134
|
+
.option("--stored-only", "Read mirrored messages without provider refresh.")
|
|
14992
15135
|
.option("--json", "Print a JSON envelope.")
|
|
14993
15136
|
.action(async (conversation, options) => {
|
|
14994
15137
|
await handleAsyncAction("inbox get", options, () => {
|
|
@@ -14999,15 +15142,21 @@ Examples:
|
|
|
14999
15142
|
const messageLimit = readOption(options.messageLimit);
|
|
15000
15143
|
if (messageLimit)
|
|
15001
15144
|
params.set("message_limit", messageLimit);
|
|
15145
|
+
const senderAccountId = readOption(options.senderAccountId);
|
|
15146
|
+
if (senderAccountId)
|
|
15147
|
+
params.set("sender_account_id", senderAccountId);
|
|
15148
|
+
if (options.storedOnly)
|
|
15149
|
+
params.set("sync", "0");
|
|
15002
15150
|
const suffix = params.toString();
|
|
15003
15151
|
return requestOxygen(`/api/cli/inbox/${encodeURIComponent(conversation)}${suffix ? `?${suffix}` : ""}`);
|
|
15004
15152
|
});
|
|
15005
15153
|
}))
|
|
15006
15154
|
.addCommand(new Command("send")
|
|
15007
|
-
.description("Reply into a conversation on its own channel, resolved from the conversation when --channel is omitted: an email thread replies from its own mailbox (Zapbox/native Gmail/Graph, threaded); a WhatsApp thread gets a warm reply; a LinkedIn thread replies via Unipile. Sends a real message — requires --approved. Without it, returns a preview.")
|
|
15155
|
+
.description("Reply into a conversation on its own channel, resolved from the conversation when --channel is omitted: an email thread replies from its own mailbox (Zapbox/native Gmail/Graph, threaded); a WhatsApp thread gets a warm reply; a LinkedIn thread replies via Unipile. Shared LinkedIn threads require --sender-account-id. Sends a real message — requires --approved. Without it, returns a preview.")
|
|
15008
15156
|
.argument("<conversation>", "Conversation id, Unipile chat id, or (email) Zapbox thread id.")
|
|
15009
15157
|
.requiredOption("--text <message>", "Reply text to send.")
|
|
15010
15158
|
.option("--channel <channel>", "Optional: email, linkedin, or whatsapp. Resolved from the conversation when omitted.")
|
|
15159
|
+
.option("--sender-account-id <id>", "LinkedIn participant UUID from the thread’s senderAccountIds. Required for a shared thread.")
|
|
15011
15160
|
.option("--approved", "Approve and send the message. Without this flag, returns a preview only.")
|
|
15012
15161
|
.option("--draft-id <id>", "When approving an AI reply-agent draft (email or LinkedIn), its id (marks it sent on success). Use the matching --channel.")
|
|
15013
15162
|
.option("--attach <path-or-url>", "Email: local PDF, DOCX, XLSX, PPTX, TXT, or CSV path; preview uploads it for 24 hours without sending. LinkedIn: public URL. Repeatable, up to 5 files and 10 MiB total for email.", collectRepeatable, [])
|
|
@@ -15069,6 +15218,7 @@ Examples:
|
|
|
15069
15218
|
...(channel ? { channel } : {}),
|
|
15070
15219
|
...(options.approved ? { approved: true } : {}),
|
|
15071
15220
|
...(draftId ? { draft_id: draftId } : {}),
|
|
15221
|
+
...(readOption(options.senderAccountId) ? { sender_account_id: readOption(options.senderAccountId) } : {}),
|
|
15072
15222
|
...(attachments.length > 0 ? { attachments } : {}),
|
|
15073
15223
|
...(attachmentIds.length > 0 ? { attachment_ids: attachmentIds } : {}),
|
|
15074
15224
|
},
|
|
@@ -15119,14 +15269,23 @@ Examples:
|
|
|
15119
15269
|
}));
|
|
15120
15270
|
}))
|
|
15121
15271
|
.addCommand(new Command("mark-read")
|
|
15122
|
-
.description("Mark a conversation
|
|
15272
|
+
.description("Mark visible messages in a conversation as read (LinkedIn by default; use --channel email or whatsapp for those channels). Sender filters retain the selected DM history window.")
|
|
15123
15273
|
.argument("<conversation>", "Conversation id, Unipile chat id, or (email) Zapbox thread id.")
|
|
15124
15274
|
.option("--channel <channel>", "Inbox channel: linkedin (default), whatsapp, or email.")
|
|
15275
|
+
.option("--sender-account-id <ids>", "Scope DM read state to participant UUIDs, comma-separated.")
|
|
15276
|
+
.option("--sender-profile-id <ids>", "Scope DM read state to sender profile UUIDs, comma-separated.")
|
|
15125
15277
|
.option("--json", "Print a JSON envelope.")
|
|
15126
15278
|
.action(async (conversation, options) => {
|
|
15127
15279
|
await handleAsyncAction("inbox mark-read", options, () => {
|
|
15128
15280
|
const channel = readOption(options.channel);
|
|
15129
|
-
|
|
15281
|
+
const params = new URLSearchParams();
|
|
15282
|
+
const senderAccountId = readOption(options.senderAccountId), senderProfileId = readOption(options.senderProfileId);
|
|
15283
|
+
if (senderAccountId)
|
|
15284
|
+
params.set("sender_account_id", senderAccountId);
|
|
15285
|
+
if (senderProfileId)
|
|
15286
|
+
params.set("sender_profile_id", senderProfileId);
|
|
15287
|
+
const suffix = params.toString();
|
|
15288
|
+
return requestOxygen(`/api/cli/inbox/${encodeURIComponent(conversation)}/read${suffix ? `?${suffix}` : ""}`, {
|
|
15130
15289
|
method: "POST",
|
|
15131
15290
|
body: channel ? { channel } : {},
|
|
15132
15291
|
});
|
|
@@ -15195,6 +15354,7 @@ Examples:
|
|
|
15195
15354
|
params.set("needs_reply", "true");
|
|
15196
15355
|
return requestOxygen(`/api/cli/inbox/read-all?${params.toString()}`, {
|
|
15197
15356
|
method: "POST",
|
|
15357
|
+
previewOnly: !options.yes,
|
|
15198
15358
|
// Approval rides in the body: a bodyless POST reads as
|
|
15199
15359
|
// approved=false server-side, i.e. a preview that mutates
|
|
15200
15360
|
// nothing. --yes (not --approved) because this spends no
|
|
@@ -15357,6 +15517,7 @@ Examples:
|
|
|
15357
15517
|
.filter(Boolean);
|
|
15358
15518
|
return requestOxygen("/api/cli/inbox/bulk", {
|
|
15359
15519
|
method: "POST",
|
|
15520
|
+
previewOnly: !options.yes,
|
|
15360
15521
|
body: {
|
|
15361
15522
|
action,
|
|
15362
15523
|
channel,
|
|
@@ -15974,7 +16135,7 @@ Examples:
|
|
|
15974
16135
|
});
|
|
15975
16136
|
})))
|
|
15976
16137
|
.addCommand(addContractFlags(new Command("analytics")
|
|
15977
|
-
.description("Show selected-window sequencer analytics with persisted status kept separate from operational state, fleet marked-active versus operationally-live counts, current open work, sent/terminal-failed/deferral totals, failure classes, and action-kind breakdowns. Also includes per-sequence funnels and native email attribution under analytics.emailAttribution: byMailbox, byDomain, and explicit unattributed facts. With --sequence, analytics.senderPerformance groups channel-conditional counts and rates by sending person; analytics.messagePerformance reports each copy-bearing deterministic step variant, target/actual distribution, contacted recipients, and type-specific accepted/reply/positive outcomes (absent for note-less connect-only campaigns). Message-level reply outcomes require preserved step/variant provenance, so older unattributed replies remain in campaign totals without being guessed into a message row. Provider-owned campaigns appear only after their telemetry is normalized into Oxygen's native Sequence ledgers. Also reports whether the reply → CRM automation is armed, with a link to the workflow.")
|
|
16138
|
+
.description("Show selected-window sequencer analytics with persisted status kept separate from operational state, fleet marked-active versus operationally-live counts, current open work, sent/terminal-failed/deferral totals, failure classes, and action-kind breakdowns. Also includes per-sequence funnels and native email attribution under analytics.emailAttribution: byMailbox, byDomain, and explicit unattributed facts. With --sequence, analytics.senderPerformance groups channel-conditional counts and rates by sending person; analytics.messagePerformance reports each copy-bearing deterministic step variant, target/actual distribution, contacted recipients, and type-specific accepted/reply/positive outcomes (absent for note-less connect-only campaigns). Message-level reply outcomes require preserved step/variant provenance, so older unattributed replies remain in campaign totals without being guessed into a message row. Provider-owned campaigns appear only after their telemetry is normalized into Oxygen's native Sequence ledgers. Also reports whether the reply → CRM automation is armed, with a link to the workflow. Omit --projection for all sections in one read; --projection value --path /analytics selects all analytics sections. value_json is escaped JSON text, paged losslessly with value_cursor.")
|
|
15978
16139
|
.option("--sender <id>", "Filter to one sender from `oxygen senders list` or `oxygen senders profiles list`, or a key from this output's senders list. Repeatable.", collectRepeatable, [])
|
|
15979
16140
|
.option("--json", "Print a JSON envelope."), SEQUENCE_ANALYTICS_READ_CONTRACT, {
|
|
15980
16141
|
range: { valueName: "<preset>", parse: readOption },
|
|
@@ -16258,7 +16419,7 @@ Examples:
|
|
|
16258
16419
|
await handleAsyncAction("sequences get", options, () => requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}${options.readiness ? "?include=readiness" : ""}`));
|
|
16259
16420
|
}))
|
|
16260
16421
|
.addCommand(new Command("enroll")
|
|
16261
|
-
.description("Enroll leads into a sequence. Enrolling spends no credits. An active sequence contacts the new leads on its next dispatch tick; a draft or paused one holds them until `sequences start`. --from-table enrolls every not-yet-enrolled row of the sequence's bound source table (up to 500 per run; the response's from_table.has_more says whether to run again) — for a LinkedIn sequence the profile is read from the bound table's URL column, where a full /in/ URL and a bare public handle carrying a hyphen or digit (ada-lovelace) both work, so no pre-resolved provider ids are needed; each handle Oxygen canonicalized is echoed in linkedin_handles_canonicalized (value + url) to check before start, and every row that still cannot be resolved is named in unresolved_linkedin_lead_details (row, value, reason) rather than only counted in unresolved_linkedin_leads. In a LinkedIn + email sequence a row with no usable profile but a recipient email is still enrolled for the email steps (its LinkedIn steps are skipped as no_linkedin_profile, a connection wait passes at once and a connection branch takes its not-connected path) and is listed in linkedin_unavailable_lead_details instead; unresolved_linkedin_leads names only rows that were not enrolled. --leads-file enrolls an explicit JSON list { leads: [...] }: for an email/WhatsApp sequence a lead identified only by an email (row_values.email) or phone (row_values.phone) is auto-filed as a row in the sequence's leads table (auto-creating and binding one if there is none), deduped by email/phone, so no table has to exist first (the table is returned as leads_table with a web_url); a LinkedIn + email sequence auto-files an email-only lead the same way; a LinkedIn lead needs a lead_provider_id, a lead_profile_url (a /in/ URL or a bare public handle with a hyphen or digit), or a table_row_id whose row carries one of those. A CRM-only lead needs a stable table_row_id or lead_provider_id in addition to mapped row_values; a HubSpot contact id used in crm_task associations identifies the destination record, not the Oxygen enrollment. When the sequence is bound to a source table, a lead's table_row_id auto-snapshots that row's columns (incl. AI/tool outputs) into row_values for {{column}} copy — explicit row_values win. Idempotent per table row (and per email/phone for auto-filed leads). WhatsApp-only campaigns also enroll each normalized number once across all source rows and enrollment statuses; mixed campaigns keep other channels and skip duplicate WhatsApp actions. Duplicate protection defaults to this campaign; --whatsapp-duplicate-scope on create/update can additionally block identical copy across all campaigns. The org do-not-contact list is always enforced; --exclude-contacted and --suppress-list add further opt-in skips (reported under skipped_by_reason). Leads already owned by a sender account (from an earlier real send) are routed back to that same account; a lead owned by a sender NOT on this sequence is skipped (bound_to_other_sender) unless --ignore-sender-bindings. This enrolls into an OXYGEN Sequence; to put the same rows into a campaign you already run in Smartlead, Instantly, lemlist or HeyReach, use `sequences push-external`. esp.
|
|
16422
|
+
.description("Enroll leads into a sequence. Enrolling spends no credits. An active sequence contacts the new leads on its next dispatch tick; a draft or paused one holds them until `sequences start`. --from-table enrolls every not-yet-enrolled row of the sequence's bound source table (up to 500 per run; the response's from_table.has_more says whether to run again) — for a LinkedIn sequence the profile is read from the bound table's URL column, where a full /in/ URL and a bare public handle carrying a hyphen or digit (ada-lovelace) both work, so no pre-resolved provider ids are needed; each handle Oxygen canonicalized is echoed in linkedin_handles_canonicalized (value + url) to check before start, and every row that still cannot be resolved is named in unresolved_linkedin_lead_details (row, value, reason) rather than only counted in unresolved_linkedin_leads. In a LinkedIn + email sequence a row with no usable profile but a recipient email is still enrolled for the email steps (its LinkedIn steps are skipped as no_linkedin_profile, a connection wait passes at once and a connection branch takes its not-connected path) and is listed in linkedin_unavailable_lead_details instead; unresolved_linkedin_leads names only rows that were not enrolled. --leads-file enrolls an explicit JSON list { leads: [...] }: for an email/WhatsApp sequence a lead identified only by an email (row_values.email) or phone (row_values.phone) is auto-filed as a row in the sequence's leads table (auto-creating and binding one if there is none), deduped by email/phone, so no table has to exist first (the table is returned as leads_table with a web_url); a LinkedIn + email sequence auto-files an email-only lead the same way; a LinkedIn lead needs a lead_provider_id, a lead_profile_url (a /in/ URL or a bare public handle with a hyphen or digit), or a table_row_id whose row carries one of those. A CRM-only lead needs a stable table_row_id or lead_provider_id in addition to mapped row_values; a HubSpot contact id used in crm_task associations identifies the destination record, not the Oxygen enrollment. When the sequence is bound to a source table, a lead's table_row_id auto-snapshots that row's columns (incl. AI/tool outputs) into row_values for {{column}} copy — explicit row_values win. Idempotent per table row (and per email/phone for auto-filed leads). WhatsApp-only campaigns also enroll each normalized number once across all source rows and enrollment statuses; mixed campaigns keep other channels and skip duplicate WhatsApp actions. Duplicate protection defaults to this campaign; --whatsapp-duplicate-scope on create/update can additionally block identical copy across all campaigns. The org do-not-contact list is always enforced; --exclude-contacted and --suppress-list add further opt-in skips (reported under skipped_by_reason). Leads already owned by a sender account (from an earlier real send) are routed back to that same account; a lead owned by a sender NOT on this sequence is skipped (bound_to_other_sender) unless --ignore-sender-bindings. This enrolls into an OXYGEN Sequence; to put the same rows into a campaign you already run in Smartlead, Instantly, lemlist or HeyReach, use `sequences push-external`. esp.pool_scope identifies the active workspace inventory used for routing and strict_deferred_leads estimates; these estimates omit sequence sender restrictions, readiness and daily limits. A zero estimate does not guarantee readiness. Review sequences preview before launch.")
|
|
16262
16423
|
.argument("<sequence>", "Sequence id or slug.")
|
|
16263
16424
|
.option("--leads-file <path>", "Path to a JSON file: { \"leads\": [{ row_values: { email }, lead_name }] } for cold email; { lead_provider_id, lead_name, table_row_id, row_values } for LinkedIn/existing rows; CRM-only leads require table_row_id or a stable lead_provider_id alongside mapped row_values. A CRM association object id is not the enrollment identity. Exactly one of --leads-file or --from-table.")
|
|
16264
16425
|
.option("--from-table", "Enroll every not-yet-enrolled row of the sequence's bound source table (up to 500 per run; re-run to continue). Exactly one of --leads-file or --from-table.")
|
|
@@ -16349,7 +16510,7 @@ Examples:
|
|
|
16349
16510
|
}, formatSequencePushExternal);
|
|
16350
16511
|
}))
|
|
16351
16512
|
.addCommand(new Command("start")
|
|
16352
|
-
.description("Preview or start a sequence. Without --approved this is the launch check and changes nothing: it returns launch_readiness (blockers
|
|
16513
|
+
.description("Preview or start a sequence. Without --approved this is the launch check and changes nothing: it returns launch_readiness (blockers and warnings), sender_journey (each step's From, candidate inboxes and assignment; no leads needed), rendered pending-lead LinkedIn/WhatsApp copy, character counts/limits, copy blockers, per-kind scope, sender/mailbox capacity, CRM readiness and safety caps. Email copy is not rendered; inspect its saved templates and unsubscribe settings with sequences get. Live start requires --approved; --max-credits is optional. Email/WhatsApp/CRM-task journeys additionally require --max-live-sends because their external actions cost 0 Oxygen credits. --dry-run starts the sequence in simulated mode (it leaves draft), with no send, provider campaign, CRM task or credits. Live start enables the standing reply → CRM automation only if it has never been configured. Planned counts include only leads each step's channel can reach; skipped_actions_by_reason and channel_coverage show the rest.")
|
|
16353
16514
|
.argument("<sequence>", "Sequence id or slug.")
|
|
16354
16515
|
.option("--approved", "Approve and activate live. Without this flag, returns a preview only, including copy_preview rendered samples and blockers.")
|
|
16355
16516
|
.option("--max-credits <n>", "Optional positive credit ceiling. Omit for the free launch preview. Live start uses the saved ceiling when omitted; an unset ceiling means unbounded credits, not recipient approval.")
|
|
@@ -16372,7 +16533,7 @@ Examples:
|
|
|
16372
16533
|
});
|
|
16373
16534
|
}))
|
|
16374
16535
|
.addCommand(new Command("preview")
|
|
16375
|
-
.description("The free launch check, as a read: the same preview
|
|
16536
|
+
.description("The free launch check, as a read: the same preview sequences start returns without --approved. It returns launch_readiness (blockers and warnings), sender_journey (who sends each step and how an inbox is picked), copy_preview (rendered pending-lead LinkedIn/WhatsApp copy, character counts and scan), caps and estimated credits. Email copy is not rendered; inspect its saved templates and unsubscribe settings with sequences get. Changes nothing and costs 0 credits; it never starts, resumes or dry-runs the sequence. Start with sequences start <sequence> --approved after review. Planned counts include only leads each step's channel can reach; skipped_actions_by_reason, channel_coverage and copy_preview.skipped_leads show the rest.")
|
|
16376
16537
|
.argument("<sequence>", "Sequence id or slug.")
|
|
16377
16538
|
.option("--json", "Print a JSON envelope.")
|
|
16378
16539
|
.action(async (sequence, options) => {
|
|
@@ -16410,7 +16571,7 @@ Examples:
|
|
|
16410
16571
|
}));
|
|
16411
16572
|
}))
|
|
16412
16573
|
.addCommand(new Command("resume")
|
|
16413
|
-
.description("Resume a paused sequence.")
|
|
16574
|
+
.description("Resume a paused sequence. A campaign paused at its live send cap pauses again at once: raise the cap and resume in one step with `sequences start <sequence> --max-live-sends <n>` (preview, then --approved).")
|
|
16414
16575
|
.argument("<sequence>", "Sequence id or slug.")
|
|
16415
16576
|
.option("--json", "Print a JSON envelope.")
|
|
16416
16577
|
.action(async (sequence, options) => {
|
|
@@ -16427,9 +16588,9 @@ Examples:
|
|
|
16427
16588
|
await handleAsyncAction("sequences delete", options, () => requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}${options.approved ? "?approved=true" : ""}`, { method: "DELETE" }));
|
|
16428
16589
|
}))
|
|
16429
16590
|
.addCommand(new Command("enrollments")
|
|
16430
|
-
.description("List per-lead enrollment state plus the latest action status, error, attempts, and retry time. --view compact answers 'when is each lead's next message due' per lead: lead, status, invite (accepted, pending, not_invited), accepted_at, current_step and next_due. next_due.summary says it in words, at_local/gives_up_at_local give the time in the campaign's timezone (its schedule's, else UTC) and at/gives_up_at in UTC. Reasons: step_scheduled (planned send time; held_by names what holds it, e.g. send_cap_reached), planner_due (when the planner next looks at the lead), not_started (begins when the campaign is started or resumed), nothing_scheduled (enrolled, no step will run), waiting_for_connection / waiting_for_signal (no send time; acceptance makes the next step due at once; gives_up_at is when the wait ends, and on_timeout says whether the lead then leaves or moves on), finished. missing_source_row is true for a lead whose source-table row was deleted: still enrolled and contacted (remove with `contacts-remove --missing-source-rows`); null when the campaign has no live source table to check against, or the check failed. channels says which of the sequence's channels reach each lead: usable, and unavailable with a reason (no_linkedin_profile = enrolled for email only, its LinkedIn steps are skipped; no_recipient_email; no_phone_number).")
|
|
16591
|
+
.description("List per-lead enrollment state plus the latest action status, error, attempts, and retry time. --view compact answers 'when is each lead's next message due' per lead: lead, status, invite (accepted, pending, not_invited), accepted_at, current_step and next_due. next_due.summary says it in words, at_local/gives_up_at_local give the time in the campaign's timezone (its schedule's, else UTC) and at/gives_up_at in UTC. Reasons: step_scheduled (planned send time; held_by names what holds it, e.g. send_cap_reached), planner_due (when the planner next looks at the lead), not_started (begins when the campaign is started or resumed), nothing_scheduled (enrolled, no step will run), waiting_for_connection / waiting_for_signal (no send time; acceptance makes the next step due at once; gives_up_at is when the wait ends, and on_timeout says whether the lead then leaves or moves on), finished. A failed lead carries failure: the cause in words (cause_recorded false when OXYGEN never recorded one) and provider_contact (made; none = the step never started; nothing_sent; unknown). missing_source_row is true for a lead whose source-table row was deleted: still enrolled and contacted (remove with `contacts-remove --missing-source-rows`); null when the campaign has no live source table to check against, or the check failed. channels says which of the sequence's channels reach each lead: usable, and unavailable with a reason (no_linkedin_profile = enrolled for email only, its LinkedIn steps are skipped; no_recipient_email; no_phone_number).")
|
|
16431
16592
|
.argument("<sequence>", "Sequence id or slug.")
|
|
16432
|
-
.option("--status <status>", "Filter by enrollment status (active, waiting_connection, replied, completed, stopped, failed). waiting_connection = invite sent, not accepted yet. Status is the lead's progress, not its invite: use --invite accepted for the leads that accepted.")
|
|
16593
|
+
.option("--status <status>", "Filter by enrollment status (pending, active, waiting_connection, waiting_signal, replied, completed, stopped, failed, excluded). pending = enrolled, not started (next_due not_started); waiting_connection = invite sent, not accepted yet. Status is the lead's progress, not its invite: use --invite accepted for the leads that accepted.")
|
|
16433
16594
|
.option("--step <n>", "Only leads currently on this step (0-based index, as in the sequence's steps).")
|
|
16434
16595
|
.option("--invite <state>", "Only leads in this LinkedIn invite state: accepted (invited and connected), pending (invited, not accepted yet) or not_invited (no invite: already connected before, suppressed or email-only).")
|
|
16435
16596
|
.option("--view <view>", "full (default, every enrollment field) or compact (one short row per lead with next_due in words and local time).")
|
|
@@ -16636,7 +16797,7 @@ Examples:
|
|
|
16636
16797
|
});
|
|
16637
16798
|
}))
|
|
16638
16799
|
.addCommand(new Command("reconcile")
|
|
16639
|
-
.description("Safely reconcile one ambiguous LinkedIn invite/message from provider reads. Preview-only unless --approved; never resends.")
|
|
16800
|
+
.description("Safely reconcile one ambiguous LinkedIn invite/message from provider reads. Zero credits. Preview-only unless --approved; never resends.")
|
|
16640
16801
|
.argument("<sequence>", "Sequence id or slug. The sequence must be paused.")
|
|
16641
16802
|
.requiredOption("--action <id>", "Failed/canceled live action UUID to reconcile.")
|
|
16642
16803
|
.option("--approved", "Apply an exact provider match to local state. No provider write or resend occurs.")
|
|
@@ -17688,7 +17849,7 @@ Examples:
|
|
|
17688
17849
|
await handleAsyncAction("managed-inboxes registrant", options, () => requestOxygen("/api/cli/managed-inboxes/registrant"));
|
|
17689
17850
|
}))
|
|
17690
17851
|
.addCommand(new Command("subscribe")
|
|
17691
|
-
.description("Buy managed monthly inboxes on a new domain or your existing Cloudflare domain. Use --existing-domain for Cloudflare: the read-only preview shows ownership, occupancy, DNS scope and blockers, with zero registration charge. --max-per-domain saves a stricter limit with the approved order, including existing addresses; later additions honor it. Preview does not save the limit or change workspace data. 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
|
|
17852
|
+
.description("Buy managed monthly inboxes on a new domain or your existing Cloudflare domain. Use --existing-domain for Cloudflare: the read-only preview shows ownership, occupancy, DNS scope and blockers, with zero registration charge. --max-per-domain saves a stricter limit with the approved order, including existing addresses; later additions honor it. Preview does not save the limit or change workspace data. 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 Workspace or Microsoft 365 inbox is one flat mailbox line plus the 100-credit mailbox connection, warm-up included, which `connection_monthly_credits` shows and your balance reserves once the inbox connects (400 credits a month all-in at the committed price), and an Azure tenant is one flat charge for its domain (4,000 a month) plus each inbox's connection; `managed-inboxes list --json` reports the prices in force under `prices`. 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`.")
|
|
17692
17853
|
.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.")
|
|
17693
17854
|
.option("--domain <domain>", "Sending domain (alternative to the positional argument).")
|
|
17694
17855
|
.requiredOption("--provider <provider>", "Mailbox PLATFORM: google, microsoft, or azure. (google/microsoft cap at 5 mailboxes per domain; azure allows 100.)")
|
|
@@ -18283,7 +18444,7 @@ Examples:
|
|
|
18283
18444
|
: requestOxygen(`/api/cli/mailboxes/sync?${new URLSearchParams({ mailbox }).toString()}`));
|
|
18284
18445
|
}))
|
|
18285
18446
|
.addCommand(new Command("connect-oauth")
|
|
18286
|
-
.description("Connect Gmail/Microsoft 365 with --vendor composio to import all mail into Unibox and business activity into CRM.
|
|
18447
|
+
.description("Connect Gmail/Microsoft 365 with --vendor composio to import all mail into Unibox and business activity into CRM. The listed recurring rate is 100 credits per mailbox per month including history, sync and analysis; previews cost 0, with no extra execution fee. Before consent, inspect current monthly connection prices and first-debit timing with billing commitments --view connection-prices --json. Quote a mailbox count with billing estimate --mailboxes <n> --json; the managed preview shows the listed rate; an unavailable managed connection directs you to OXYGEN support. Managed OAuth requires provider rollout readiness and exact registered addresses, max 10. Use mailboxes sync for progress and pause/resume/disconnect. Composio uses individual owner consent only. Native --vendor oxygen retains its existing polling; full history/CRM requires managed reconnection. Warm-up retains its existing eligibility and pricing. Preview by default; pass --approved only after reviewing exact candidates. --vendor oxygen requires an exact address list and never discovers a domain directory. For --vendor oxygen only, Microsoft supports --authorization-mode tenant (one administrator approval, up to 500 exact selected addresses, recommended for dedicated sending tenants) or individual (one account sign-in per mailbox, max 10 per run, recommended for ordinary company/personal mailboxes). For Microsoft's built-in roles, tenant mode requires a Global Administrator or Privileged Role Administrator because it requests the Microsoft Graph application Mail.Send and Mail.Read permissions; Application Administrator and Cloud Application Administrator cannot consent to Microsoft Graph application roles. Microsoft's application grant is tenant-wide at the provider; OXYGEN—not Microsoft—enforces the selected-address boundary at verification and send time unless the tenant separately configures Exchange Application RBAC. For non-managed Microsoft mailboxes, native-send approval does not authorize OXYGEN Warm-up: every mailbox still needs its own `mailboxes warmup microsoft` Outlook consent before standalone enrollment. Connecting authorization does not change whether a mailbox is active or disabled. Google uses individual OAuth. Source tokens, passwords, authenticator seeds, and one-time codes never transfer. Managed authorization paths remain available through their vendor options.")
|
|
18287
18448
|
.addHelpText("after", "\nDocs: https://oxygen-agent.com/docs/providers/mailboxes\n")
|
|
18288
18449
|
.option("--provider <provider>", "Mailbox provider: google or microsoft.")
|
|
18289
18450
|
.option("--vendor <vendor>", "Authorization path: composio for customer-owned Gmail/Microsoft 365 with managed consent, history and CRM; oxygen for native/tenant consent; zapmail (default) or inboxkit for provisioned inboxes.")
|
|
@@ -18350,7 +18511,7 @@ Examples:
|
|
|
18350
18511
|
.addCommand(new Command("emailguard")
|
|
18351
18512
|
.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.")
|
|
18352
18513
|
.addCommand(new Command("connect")
|
|
18353
|
-
.description("Preview or connect selected mailboxes to EmailGuard.
|
|
18514
|
+
.description("Preview or connect selected mailboxes to EmailGuard. Managed mode is 100 credits/inbox-month, with two placement tests per inbox per cycle included; an extra managed test costs 66 credits. 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.")
|
|
18354
18515
|
.option("--mailboxes <list>", "Comma-separated mailbox ids or addresses. Omit for the whole pool.")
|
|
18355
18516
|
.option("--approved", "Perform the external EmailGuard account write.")
|
|
18356
18517
|
.option("--plan <hash>", "Exact hash from the fresh preview (required with --approved).")
|
|
@@ -18470,7 +18631,7 @@ Examples:
|
|
|
18470
18631
|
.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. On existing active paid seats, Google credential repair uses its own enabled-by-default switch. When the separate worker recovery mode is enforce (the unset default), Microsoft OAuth or tenant-consent seats get a credential-free vendor reactivation, and stalled dispatch or DNS readiness is rechecked. Shadow reports candidates in those enabled lanes without their vendor writes; off skips those lanes. Google credential repair can still run in shadow/off. Correct bad DNS records yourself; an active unpaused paid seat in enforce mode needs no manual resume after that correction. OXYGEN never invents consent, resumes a human or safety pause, starts a new paid enrollment, or blindly repeats an ambiguous provider write. Check `warmup status` and `mailboxes list` for the exact next action.")
|
|
18471
18632
|
.addHelpText("after", "\nDocs: https://oxygen-agent.com/docs/providers/mailboxes\n")
|
|
18472
18633
|
.addCommand(new Command("enable")
|
|
18473
|
-
.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.")
|
|
18634
|
+
.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. Gmail connected through Composio uses that connection after approval of standard mailbox access (send, read and organize), without a second Google consent. The preview names this access and disabling warm-up revokes it. Existing vendor-consent seats are preserved. Eligible InboxKit-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.")
|
|
18474
18635
|
.addHelpText("after", "\nDocs: https://oxygen-agent.com/docs/providers/mailboxes\n")
|
|
18475
18636
|
.option("--mailboxes <list>", "Comma-separated mailbox ids or addresses to warm. Omit to warm the whole pool.")
|
|
18476
18637
|
.option("--approved", "Execute the PRICED enrollment. Without it the response is a priced preview and nothing is enrolled or billed.")
|
|
@@ -18513,6 +18674,29 @@ Examples:
|
|
|
18513
18674
|
},
|
|
18514
18675
|
});
|
|
18515
18676
|
});
|
|
18677
|
+
}))
|
|
18678
|
+
.addCommand(new Command("google")
|
|
18679
|
+
.description("Legacy Google owner consent for one to ten connected inboxes. Gmail connected through Composio uses warmup enable to review standard mailbox access without a second Google consent. This legacy flow is separate from native sending: no passwords or OXYGEN tokens transfer. Preview costs 0 credits and creates no link. Approve its hash with --max-credits 0, open each consent_url as that exact inbox, then poll --status. Google shows the warm-up provider's consent application. Warmup stays off until a separately approved warmup enable; its fresh preview shows the price, including 0 extra credits when included in the mailbox connection.")
|
|
18680
|
+
.requiredOption("--mailboxes <list>", "Exact Google inbox ids or addresses, comma-separated; maximum ten.")
|
|
18681
|
+
.option("--approved", "Create the previewed authorization links.")
|
|
18682
|
+
.option("--plan <hash>", "Fresh preview hash; required with approval.")
|
|
18683
|
+
.option("--max-credits <n>", "Exactly 0; required with approval.")
|
|
18684
|
+
.option("--status", "Verify completed Google authorizations; never starts warmup.")
|
|
18685
|
+
.option("--json", "Print a JSON envelope.")
|
|
18686
|
+
.action(async (options) => {
|
|
18687
|
+
await handleAsyncAction("mailboxes warmup google", options, () => {
|
|
18688
|
+
const mailboxes = readCsvOption(options.mailboxes);
|
|
18689
|
+
const planHash = readOption(options.plan);
|
|
18690
|
+
const maxCredits = options.maxCredits === undefined ? undefined : readNonNegativeNumber(options.maxCredits);
|
|
18691
|
+
if (options.approved && options.status)
|
|
18692
|
+
throw new Error("Status and approval cannot be combined.");
|
|
18693
|
+
if (options.approved && (!planHash || maxCredits !== 0))
|
|
18694
|
+
throw new Error("Approval requires a fresh --plan and --max-credits 0.");
|
|
18695
|
+
return requestOxygen("/api/cli/mailboxes/warmup/google", { method: "POST", body: {
|
|
18696
|
+
mailboxes, ...(options.approved ? { approved: true } : {}), ...(options.status ? { status: true } : {}),
|
|
18697
|
+
...(planHash ? { plan_hash: planHash } : {}), ...(maxCredits !== undefined ? { max_credits: maxCredits } : {}),
|
|
18698
|
+
} });
|
|
18699
|
+
});
|
|
18516
18700
|
}))
|
|
18517
18701
|
.addCommand(new Command("microsoft")
|
|
18518
18702
|
.description("NON-INBOXKIT FALLBACK ONLY: authorize eligible Microsoft/Outlook mailboxes for OXYGEN Warm-up with ONE browser consent PER MAILBOX, never a password or app password. Do not run this for managed InboxKit mailboxes: fresh Microsoft/Azure orders activate native InboxKit warm-up exports automatically when the approved order includes warmup; opted-out or later separate managed enrollment uses standalone `warmup enable`. For the non-InboxKit fallback there is no tenant-admin shortcut: a 40-inbox Microsoft pool means 40 separate authorizations, each opened by whoever can sign in to that mailbox. The preview names the exact fallback mailboxes and outstanding links at 0 credits. Approve that fresh hash with --max-credits 0, open every returned consent_url, then re-run with --status. Warmup itself stays OFF until standalone `warmup enable`.")
|
|
@@ -18770,15 +18954,22 @@ Examples:
|
|
|
18770
18954
|
}));
|
|
18771
18955
|
})));
|
|
18772
18956
|
program.addCommand(new Command("deliverability")
|
|
18773
|
-
.description("External email deliverability: fleet reputation health and DIRECTIONAL inbox-placement (spam) tests via EmailGuard, Zapmail, or an explicitly configured
|
|
18957
|
+
.description("External email deliverability: fleet reputation health and DIRECTIONAL inbox-placement (spam) tests via EmailGuard, Zapmail, or an explicitly configured OXYGEN placement evaluation pilot. Start with `placement-test list` and `placement-test get` to inspect existing results for 0 credits without creating or sending a test. For continuous EmailGuard account monitoring use `oxygen mailboxes emailguard connect`. Placement tests are approval-gated paid runs (managed bills Oxygen credits; BYOK = 0 Oxygen credits — Zapmail BYOK bills your Zapmail wallet ~$2/test). OXYGEN placement evaluation is never auto-selected or generally available: it is a limited pilot with one 0-credit test for its configured eligible connected sender.")
|
|
18774
18958
|
.addCommand(new Command("placement-test")
|
|
18775
18959
|
.description("Inspect saved directional inbox-placement results with list/get (0 credits; no test email is sent). New tests use preview and approval through run/send.")
|
|
18776
18960
|
.addCommand(new Command("run")
|
|
18777
|
-
.description("Create a placement test for one sending mailbox. Without --approved this returns a cost PREVIEW. EmailGuard creation returns exact seeds + phrase but sends nothing; next run `placement-test send <id>` to preview and approve that external email. Zapmail owns its probe delivery and completes async (2-24h).
|
|
18961
|
+
.description("Create a placement test for one sending mailbox. Without --approved this returns a cost PREVIEW. EmailGuard creation returns exact seeds + phrase but sends nothing; next run `placement-test send <id>` to preview and approve that external email. Zapmail owns its probe delivery and completes async (2-24h). OXYGEN placement evaluation requires explicit --provider oxygen_placement plus --subject and --body, and is available only for the eligible connected sender enabled for the limited pilot. Its approval creates AND sends one probe with no separate send step: preview first, then re-run the same mailbox, provider, subject, and body with --approved --max-credits 0 --plan <plan_hash>.")
|
|
18778
18962
|
.argument("<mailbox>", "Sending mailbox address to test (e.g. ada@send.acme.com).")
|
|
18779
|
-
.addOption(new Option("--provider <provider>", "Health provider: emailguard, zapmail, or
|
|
18780
|
-
.
|
|
18781
|
-
|
|
18963
|
+
.addOption(new Option("--provider <provider>", "Health provider: emailguard, zapmail, or oxygen_placement. Omit to auto-resolve EmailGuard/Zapmail. OXYGEN placement evaluation is never auto-selected and requires the sender enabled for the limited pilot.").choices(["emailguard", "zapmail", "oxygen_placement"])
|
|
18964
|
+
.argParser((value) => {
|
|
18965
|
+
const provider = value === "sendkit" ? "oxygen_placement" : value;
|
|
18966
|
+
if (!["emailguard", "zapmail", "oxygen_placement"].includes(provider)) {
|
|
18967
|
+
throw new CommanderError(2, "commander.invalidArgument", "Choose emailguard, zapmail, or oxygen_placement.");
|
|
18968
|
+
}
|
|
18969
|
+
return provider;
|
|
18970
|
+
}))
|
|
18971
|
+
.option("--subject <subject>", "Seed-message subject. Required with --provider oxygen_placement; Zapmail ignores it beyond labeling the test.")
|
|
18972
|
+
.option("--body <text>", "Plain-text seed-message body. Required with --provider oxygen_placement; ignored by Zapmail.")
|
|
18782
18973
|
.option("--allow-paused-probe", "Opt in to one diagnostic probe while preserving campaign pause; repeat at both create and send approvals. Never permits disabled senders.")
|
|
18783
18974
|
.option("--approved", "Actually run the test (otherwise a preview is returned).")
|
|
18784
18975
|
.option("--max-credits <n>", "Credit cap the caller accepts (must be >= credits_required for managed runs).")
|
|
@@ -18850,11 +19041,11 @@ Examples:
|
|
|
18850
19041
|
program.addCommand(new Command("domains")
|
|
18851
19042
|
.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). Your own Cloudflare account bills purchases to its payment method; domains registered by OXYGEN cost credits at the vendor price. Preview `domains buy` before approval.")
|
|
18852
19043
|
.addCommand(new Command("list")
|
|
18853
|
-
.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.")
|
|
19044
|
+
.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. summary.by_dns_state gives all DNS-state counts in one read. volume.outbound_30d counts outbound email messages in the synced inbox ledger, including mail outside Sequences. Reads the cache only — run `domains sync` to refresh.")
|
|
18854
19045
|
.option("--status <status>", "Filter by zone status: unknown, initializing, pending, active, moved, or deleted.")
|
|
18855
19046
|
.option("--registrar <registrar>", "Filter by registrar name.")
|
|
18856
19047
|
.option("--q <text>", "Filter by domain substring.")
|
|
18857
|
-
.option("--dns <state>", "Filter by
|
|
19048
|
+
.option("--dns <state>", "Filter by one derived DNS state: ok, issues, not_checked, check_failed, check_incomplete, or no_zone; summary.by_dns_state gives all state counts.")
|
|
18858
19049
|
.option("--expiring", "Only non-deleted domains expiring within 60 days.")
|
|
18859
19050
|
.option("--has-mailboxes <bool>", "true → only domains with sending mailboxes; false → only domains without.")
|
|
18860
19051
|
.option("--include-archived", "Include archived domains (hidden by default).")
|
|
@@ -19265,6 +19456,14 @@ Examples:
|
|
|
19265
19456
|
.action(async (callable, options) => {
|
|
19266
19457
|
await handleAsyncAction("callables disable", options, () => requestOxygen("/api/cli/callables/status", { method: "POST", body: { callable, status: "disabled" } }));
|
|
19267
19458
|
}));
|
|
19459
|
+
const workflowRunReadbackHelp = `
|
|
19460
|
+
Stored run values:
|
|
19461
|
+
oxygen workflows runs --run-id <run_id> --projection value --path /runs/0/output --json
|
|
19462
|
+
Canonical graph output is a steps map keyed by node id.
|
|
19463
|
+
Read the observed cost receipt with oxygen workflows run <run_id> --json.
|
|
19464
|
+
For large values, follow next_value_cursor with --value-cursor and the same selection.
|
|
19465
|
+
Append value_json fragments in offset order until next_value_cursor is null, then parse the combined JSON.
|
|
19466
|
+
`;
|
|
19268
19467
|
program
|
|
19269
19468
|
.command("workflows")
|
|
19270
19469
|
.description("Durable workflow automation commands.")
|
|
@@ -19294,8 +19493,10 @@ without --draft publishes the revision; calling or enabling it remains a separat
|
|
|
19294
19493
|
execution/authorization step.
|
|
19295
19494
|
|
|
19296
19495
|
Run completion:
|
|
19297
|
-
Calls enqueue asynchronously.
|
|
19496
|
+
Calls without --preview enqueue asynchronously. Use the returned data.run.id as <run_id>:
|
|
19298
19497
|
oxygen workflows tail <run_id>
|
|
19498
|
+
Preview calls return data.preview and create no run.
|
|
19499
|
+
${workflowRunReadbackHelp}
|
|
19299
19500
|
`)
|
|
19300
19501
|
.addCommand(new Command("templates")
|
|
19301
19502
|
.description("Deprecated aliases for `oxygen blueprints` — these route through the canonical blueprint surface (built-in seed templates plus this workspace's saved blueprints).")
|
|
@@ -19534,6 +19735,7 @@ Run completion:
|
|
|
19534
19735
|
await handleAsyncAction("workflows get", options, async () => {
|
|
19535
19736
|
const data = await requestOxygen("/api/cli/workflows/get", {
|
|
19536
19737
|
method: "POST",
|
|
19738
|
+
readOnly: true,
|
|
19537
19739
|
body: { workflow },
|
|
19538
19740
|
});
|
|
19539
19741
|
if (!options.json) {
|
|
@@ -19576,12 +19778,12 @@ Run completion:
|
|
|
19576
19778
|
}), options));
|
|
19577
19779
|
}))
|
|
19578
19780
|
.addCommand(new Command("call")
|
|
19579
|
-
.description("Run a workflow — or preview it first. `--preview` shows every step that leaves Oxygen, the accounts it would use, and what one run costs
|
|
19781
|
+
.description("Run a workflow — or preview it first. `--preview` returns data.preview without creating a run: it shows every step that leaves Oxygen, the accounts it would use, and what one run costs. Calls without --preview enqueue asynchronously without simulating the trigger; use the returned data.run.id as <run_id> in `oxygen workflows tail <run_id>`.")
|
|
19580
19782
|
.argument("[workflow]", "Workflow id, slug, or name.")
|
|
19581
19783
|
.option("--workflow <workflow>", "Workflow id, slug, or name.")
|
|
19582
19784
|
.option("--workflow-id <workflow_id>", "Workflow id or slug.")
|
|
19583
19785
|
.option("--workflow-name <workflow_name>", "Workflow name.")
|
|
19584
|
-
.option("--input-json <json>", "Workflow input object. Defaults to {}.")
|
|
19786
|
+
.option("--input-json <json>", "Workflow input object in every mode, including smoke-test. Defaults to {}.")
|
|
19585
19787
|
.option("--mode <mode>", "Execution mode. smoke-test/dry-run: 0 credits, no paid provider calls or external writes; internal reads still execute. live: may spend/write and requires approval + cap. Not needed with --preview, which runs nothing.")
|
|
19586
19788
|
.option("--idempotency-key <key>", "Optional idempotency key.")
|
|
19587
19789
|
.option("--max-credits <n>", "Required credit ceiling for live calls.")
|
|
@@ -19592,7 +19794,8 @@ Run completion:
|
|
|
19592
19794
|
.option("--preview", "Show what a live run of this exact version would do — every step that leaves Oxygen, the accounts it would use, and the minimum number of billable steps and credits it would cost — without running anything. AI and enrichment steps bill per row and can cost more than that floor. Needs no --mode: it always describes a live run.")
|
|
19593
19795
|
.option("--include-bundle", "Include durable recipe bundles in JSON output.")
|
|
19594
19796
|
.option("--json", "Print a JSON envelope.")
|
|
19595
|
-
.addHelpText("after", "\nSafety: smoke_test and dry_run share one boundary: 0 credits, no paid provider calls, no external writes. Oxygen internal reads use current workspace data, and oxygen.http_json_request may make a real outbound GET. Every mode creates an inspectable Workflow run record.\n\n--node scopes a test to one step. Oxygen plans the slice on the server from the exact saved version and records it on the run, so the run shows precisely which nodes were allowed to execute; you never submit a node list. Selecting the trigger, a disabled node, or a node unreachable from the trigger is refused with the reason. Only canonical graph workflows support it.\n")
|
|
19797
|
+
.addHelpText("after", "\nSafety: smoke_test and dry_run share one boundary: 0 credits, no paid provider calls, no external writes. Oxygen internal reads use current workspace data, and oxygen.http_json_request may make a real outbound GET. Every execution mode creates an inspectable Workflow run record; --preview creates none.\n\n--node scopes a test to one step. Oxygen plans the slice on the server from the exact saved version and records it on the run, so the run shows precisely which nodes were allowed to execute; you never submit a node list. Selecting the trigger, a disabled node, or a node unreachable from the trigger is refused with the reason. Only canonical graph workflows support it.\n")
|
|
19798
|
+
.addHelpText("after", workflowRunReadbackHelp)
|
|
19596
19799
|
.action(async (workflowArg, options) => {
|
|
19597
19800
|
const maxCredits = readPositiveNumber(options.maxCredits);
|
|
19598
19801
|
const revisionVersion = readPositiveNumber(options.revision);
|
|
@@ -19606,15 +19809,18 @@ Run completion:
|
|
|
19606
19809
|
if (!options.preview && !readOption(options.mode)) {
|
|
19607
19810
|
throw new OxygenError("invalid_request", "--mode is required. Use smoke-test, dry-run, or live. To see what a live run would do without running it, use --preview — it runs nothing and needs no mode.", { exitCode: 2 });
|
|
19608
19811
|
}
|
|
19812
|
+
const idempotencyKey = readOption(options.idempotencyKey);
|
|
19609
19813
|
return prepareWorkflowCliOutput(await requestOxygen("/api/cli/workflows/call", {
|
|
19610
19814
|
method: "POST",
|
|
19815
|
+
...(options.preview ? { previewOnly: true } : {}),
|
|
19816
|
+
...(idempotencyKey ? { idempotencyKey } : {}),
|
|
19611
19817
|
body: {
|
|
19612
19818
|
workflow: readOption(workflowArg) ?? readOption(options.workflow),
|
|
19613
19819
|
...(readOption(options.workflowId) ? { workflow_id: readOption(options.workflowId) } : {}),
|
|
19614
19820
|
...(readOption(options.workflowName) ? { workflow_name: readOption(options.workflowName) } : {}),
|
|
19615
19821
|
input: options.inputJson ? parseJsonObject(options.inputJson) : {},
|
|
19616
19822
|
...(readOption(options.mode) ? { mode: readOption(options.mode) } : {}),
|
|
19617
|
-
...(
|
|
19823
|
+
...(idempotencyKey ? { idempotency_key: idempotencyKey } : {}),
|
|
19618
19824
|
...(maxCredits !== undefined ? { max_credits: maxCredits } : {}),
|
|
19619
19825
|
...(options.approved ? { approved: true } : {}),
|
|
19620
19826
|
...(revisionVersion !== undefined ? { revision_version: revisionVersion } : {}),
|
|
@@ -19634,6 +19840,7 @@ Run completion:
|
|
|
19634
19840
|
.option("--json", "Print a JSON envelope.")
|
|
19635
19841
|
.action((workflow, options) => handleAsyncAction("workflows webhooks rotate", options, () => requestOxygen("/api/cli/workflows/webhooks/rotate", {
|
|
19636
19842
|
method: "POST",
|
|
19843
|
+
previewOnly: !options.approved,
|
|
19637
19844
|
body: {
|
|
19638
19845
|
workflow,
|
|
19639
19846
|
...(options.approved ? { approved: true } : {}),
|
|
@@ -19860,6 +20067,7 @@ Full trigger schema: oxygen workflows schema --subject trigger --json
|
|
|
19860
20067
|
.action(async (runId, options) => {
|
|
19861
20068
|
await handleAsyncAction("workflows run", options, async () => prepareWorkflowCliOutput(await requestOxygen("/api/cli/workflows/run", {
|
|
19862
20069
|
method: "POST",
|
|
20070
|
+
readOnly: true,
|
|
19863
20071
|
body: { run_id: runId },
|
|
19864
20072
|
}), options));
|
|
19865
20073
|
}))
|
|
@@ -19875,6 +20083,7 @@ Full trigger schema: oxygen workflows schema --subject trigger --json
|
|
|
19875
20083
|
const maxCredits = readPositiveNumber(options.maxCredits);
|
|
19876
20084
|
return prepareWorkflowCliOutput(await requestOxygen("/api/cli/workflows/retry", {
|
|
19877
20085
|
method: "POST",
|
|
20086
|
+
previewOnly: !options.approved,
|
|
19878
20087
|
body: {
|
|
19879
20088
|
run_id: runId,
|
|
19880
20089
|
...(options.approved ? { approved: true } : {}),
|
|
@@ -19885,11 +20094,12 @@ Full trigger schema: oxygen workflows schema --subject trigger --json
|
|
|
19885
20094
|
}))
|
|
19886
20095
|
.addCommand(new Command("tail")
|
|
19887
20096
|
.description("Poll until a workflow finishes or parks in waiting/awaiting_approval; clock waits resume automatically.")
|
|
19888
|
-
.argument("<run_id>", "Workflow run UUID.")
|
|
20097
|
+
.argument("<run_id>", "Workflow run UUID returned as data.run.id by workflows call; distinct from data.run.runId.")
|
|
19889
20098
|
.option("--timeout-seconds <n>", "Maximum time to wait. Defaults to 600.")
|
|
19890
20099
|
.option("--interval-seconds <n>", "Polling interval. Defaults to 2.")
|
|
19891
20100
|
.option("--include-bundle", "Include durable recipe bundles in JSON output.")
|
|
19892
20101
|
.option("--json", "Print a JSON envelope.")
|
|
20102
|
+
.addHelpText("after", workflowRunReadbackHelp)
|
|
19893
20103
|
.action(async (runId, options) => {
|
|
19894
20104
|
await handleAsyncAction("workflows tail", options, async () => prepareWorkflowCliOutput(await waitForWorkflowRun(runId, options), options));
|
|
19895
20105
|
}))
|
|
@@ -19902,6 +20112,7 @@ Full trigger schema: oxygen workflows schema --subject trigger --json
|
|
|
19902
20112
|
.action(async (runId, options) => {
|
|
19903
20113
|
await handleAsyncAction("workflows cancel", options, () => requestOxygen("/api/cli/workflows/cancel", {
|
|
19904
20114
|
method: "POST",
|
|
20115
|
+
...(options.preview ? { previewOnly: true } : {}),
|
|
19905
20116
|
body: {
|
|
19906
20117
|
run_id: runId,
|
|
19907
20118
|
...(options.preview ? { preview: true } : {}),
|
|
@@ -20041,6 +20252,7 @@ Full trigger schema: oxygen workflows schema --subject trigger --json
|
|
|
20041
20252
|
const single = workflows.length === 1;
|
|
20042
20253
|
const data = await requestOxygen("/api/cli/workflows/delete", {
|
|
20043
20254
|
method: "POST",
|
|
20255
|
+
previewOnly: !options.yes,
|
|
20044
20256
|
body: {
|
|
20045
20257
|
...(single ? { workflow: workflows[0] } : { workflows }),
|
|
20046
20258
|
...(options.yes ? { approved: true } : {}),
|
|
@@ -20774,6 +20986,7 @@ function waitForWorkflowRun(runId, options) {
|
|
|
20774
20986
|
fetchRun: async () => {
|
|
20775
20987
|
latestEnvelope = await requestOxygen("/api/cli/workflows/run", {
|
|
20776
20988
|
method: "POST",
|
|
20989
|
+
readOnly: true,
|
|
20777
20990
|
body: { run_id: runId },
|
|
20778
20991
|
});
|
|
20779
20992
|
return isRecord(latestEnvelope.run) ? latestEnvelope.run : latestEnvelope;
|
|
@@ -23042,7 +23255,7 @@ function readPeopleSearchRunBody(options) {
|
|
|
23042
23255
|
if (!options.sourceFiltersJson) {
|
|
23043
23256
|
throw new OxygenError("invalid_request", "--source people_search needs --source-filters-json (the same filters as people search preview).", { exitCode: 1 });
|
|
23044
23257
|
}
|
|
23045
|
-
const maxCreditsNative =
|
|
23258
|
+
const maxCreditsNative = readCreditCeilingOrZero(options.maxCredits);
|
|
23046
23259
|
const nativeTarget = readPositiveInt(options.targetCount);
|
|
23047
23260
|
// Forwarded so the server refuses them by name; the native lane reads only
|
|
23048
23261
|
// --source-filters-json (OXP-1.39.2 S6).
|
|
@@ -23069,7 +23282,7 @@ function readPeopleSearchRunBody(options) {
|
|
|
23069
23282
|
throw new OxygenError("invalid_request", "Pass --prompt, --plan-json, --from-table with --linkedin-url-column, or --source people_search with --source-filters-json.", { exitCode: 1 });
|
|
23070
23283
|
}
|
|
23071
23284
|
const maxPages = readPeopleSearchMaxPages(options);
|
|
23072
|
-
const maxCredits =
|
|
23285
|
+
const maxCredits = readCreditCeilingOrZero(options.maxCredits);
|
|
23073
23286
|
const targetCount = readPositiveInt(options.targetCount);
|
|
23074
23287
|
const filters = prompt ? readPeopleSearchFilters(options) : null;
|
|
23075
23288
|
const sourceOverrides = source ? {} : readPeopleSearchCompanyWindow(options);
|
|
@@ -29459,8 +29672,10 @@ async function pushOneMirrorPage(dir, state, candidate) {
|
|
|
29459
29672
|
slug,
|
|
29460
29673
|
kind,
|
|
29461
29674
|
status: "proposal_created",
|
|
29462
|
-
|
|
29463
|
-
...(typeof details.
|
|
29675
|
+
// The API sends snake_case details; camelCase never matched, so push hid the proposal.
|
|
29676
|
+
...(typeof details.proposal_id === "string" ? { proposal_id: details.proposal_id } : {}),
|
|
29677
|
+
...(typeof details.proposal_slug === "string" ? { proposal_slug: details.proposal_slug } : {}),
|
|
29678
|
+
...(typeof details.proposal_web_url === "string" ? { proposal_web_url: details.proposal_web_url } : {}),
|
|
29464
29679
|
reason: typeof details.reason === "string" ? details.reason : "sensitive_page",
|
|
29465
29680
|
};
|
|
29466
29681
|
}
|