@oxygen-agent/cli 1.1011.974 → 1.1011.1091
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.js +18 -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 +272 -83
- package/dist/sender-limits-flags.d.ts +7 -0
- package/dist/sender-limits-flags.js +13 -0
- package/node_modules/@oxygen/shared/dist/attempt-provenance.d.ts +22 -0
- package/node_modules/@oxygen/shared/dist/attempt-provenance.js +83 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +146 -11
- 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 +3 -3
- package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/contracts/publishing-reads.js +1 -0
- package/node_modules/@oxygen/shared/dist/contracts/table-row-removal.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/contracts/table-row-removal.js +21 -0
- package/node_modules/@oxygen/shared/dist/knowledge-seed-content.js +2 -2
- 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/provider-balance-signal.js +3 -2
- 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/whatsapp-sender-limits.d.ts +42 -0
- package/node_modules/@oxygen/shared/dist/whatsapp-sender-limits.js +68 -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 +5 -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.")
|
|
@@ -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
|
}))
|
|
@@ -9736,8 +9835,8 @@ Examples:
|
|
|
9736
9835
|
.option("--reasoning-level <level>", REASONING_LEVEL_OPTION_HELP)
|
|
9737
9836
|
.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
9837
|
.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
9838
|
.option("--structured-output", "Undo --no-structured-output: let the prompt's named sections shape the answer again.")
|
|
9839
|
+
.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
9840
|
.option("--output-schema-json <json>", "AI column output JSON schema as inline JSON.")
|
|
9742
9841
|
.option("--output-schema-file <path>", "AI column output JSON schema read from a file path.")
|
|
9743
9842
|
.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 +10281,10 @@ Examples:
|
|
|
10182
10281
|
.option("--reasoning-level <level>", REASONING_LEVEL_OPTION_HELP)
|
|
10183
10282
|
.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
10283
|
.option("--run-condition-columns <csv>", "Comma-separated column keys referenced by --run-condition.")
|
|
10284
|
+
// Positive first keeps omission undefined; negative first defaults true
|
|
10285
|
+
// and silently overwrites a saved prose choice on an unrelated update.
|
|
10286
|
+
.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
10287
|
.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
10288
|
.option("--output-schema-json <json>", "AI column output JSON schema as inline JSON.")
|
|
10188
10289
|
.option("--output-schema-file <path>", "AI column output JSON schema read from a file path.")
|
|
10189
10290
|
.option("--research-query <template>", "Research columns: replace the per-row web search query template, e.g. \"{{company_name}} pricing page\".")
|
|
@@ -10210,7 +10311,8 @@ Examples:
|
|
|
10210
10311
|
readOption(options.reasoningLevel) ||
|
|
10211
10312
|
readOption(options.runCondition) ||
|
|
10212
10313
|
readOption(options.outputSchemaJson) ||
|
|
10213
|
-
readOption(options.outputSchemaFile)
|
|
10314
|
+
readOption(options.outputSchemaFile) ||
|
|
10315
|
+
typeof options.structuredOutput === "boolean";
|
|
10214
10316
|
let definition = options.definitionJson
|
|
10215
10317
|
? parseJsonObject(options.definitionJson)
|
|
10216
10318
|
: undefined;
|
|
@@ -11351,7 +11453,7 @@ Examples:
|
|
|
11351
11453
|
}));
|
|
11352
11454
|
}))
|
|
11353
11455
|
.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.")
|
|
11456
|
+
.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
11457
|
.option("--json", "Print a JSON envelope.")
|
|
11356
11458
|
.action(async (options) => {
|
|
11357
11459
|
await handleAsyncAction("billing balance", options, () => requestOxygen("/api/cli/billing/balance"));
|
|
@@ -11396,7 +11498,7 @@ Examples:
|
|
|
11396
11498
|
await handleAsyncAction("billing seats", options, () => requestOxygen("/api/cli/billing/seats"));
|
|
11397
11499
|
})
|
|
11398
11500
|
.addCommand(new Command("set")
|
|
11399
|
-
.description("
|
|
11501
|
+
.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
11502
|
.requiredOption("--seat-key <kind>", "linkedin, whatsapp, phone_number, or email_sender.")
|
|
11401
11503
|
.requiredOption("--quantity <n>", "Total seats to hold for this channel, 0 or more.")
|
|
11402
11504
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -11416,7 +11518,7 @@ Examples:
|
|
|
11416
11518
|
await handleAsyncAction("billing invoices", options, () => requestOxygen("/api/cli/billing/invoices"));
|
|
11417
11519
|
}))
|
|
11418
11520
|
.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.
|
|
11521
|
+
.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
11522
|
.option("--json", "Print a JSON envelope.")
|
|
11421
11523
|
.action(async (options) => {
|
|
11422
11524
|
await handleAsyncAction("billing allowance", options, () => requestOxygen("/api/cli/billing/allowance"));
|
|
@@ -11451,7 +11553,7 @@ Examples:
|
|
|
11451
11553
|
});
|
|
11452
11554
|
}))
|
|
11453
11555
|
.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
|
|
11556
|
+
.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
11557
|
.option("--days <n>", "Lookback window in days. Defaults to 365.")
|
|
11456
11558
|
.option("--from <iso>", "Only include ledger events at or after this ISO timestamp.")
|
|
11457
11559
|
.option("--to <iso>", "Only include ledger events at or before this ISO timestamp.")
|
|
@@ -11860,6 +11962,49 @@ Examples:
|
|
|
11860
11962
|
return requestOxygen(`/api/cli/admin/costs${suffix}`);
|
|
11861
11963
|
});
|
|
11862
11964
|
}))
|
|
11965
|
+
.addCommand(new Command("price-book")
|
|
11966
|
+
.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.")
|
|
11967
|
+
.addCommand(new Command("list")
|
|
11968
|
+
.description("List every pricing-book row with its stored value, what charging bills now, the committed default, and who reads it. Read-only.")
|
|
11969
|
+
.option("--json", "Print a JSON envelope.")
|
|
11970
|
+
.action(async (options) => {
|
|
11971
|
+
await handleAsyncAction("admin price-book list", options, () => requestOxygen("/api/cli/admin/costs?section=pricing_book"));
|
|
11972
|
+
}))
|
|
11973
|
+
.addCommand(new Command("set")
|
|
11974
|
+
.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.")
|
|
11975
|
+
.requiredOption("--table <table>", "charge, plan, or scalar.")
|
|
11976
|
+
.requiredOption("--key <key>", "The row key, e.g. addon.egress_dedicated_ip, oxygen_499, markup.copilot_inference.")
|
|
11977
|
+
.requiredOption("--field <field>", "credits | active (charge); monthly_credits | weekly_credits_limit | rollover_cap (plan); value (scalar).")
|
|
11978
|
+
.requiredOption("--value <value>", "A non-negative number, true/false for active, or none for no plan limit.")
|
|
11979
|
+
.option("--json", "Print a JSON envelope.")
|
|
11980
|
+
.action(async (options) => {
|
|
11981
|
+
await handleAsyncAction("admin price-book set", options, () => requestOxygen("/api/cli/admin/costs", {
|
|
11982
|
+
method: "POST",
|
|
11983
|
+
body: {
|
|
11984
|
+
price_table: options.table,
|
|
11985
|
+
price_key: options.key,
|
|
11986
|
+
price_field: options.field,
|
|
11987
|
+
value: readPriceBookValue(options.value),
|
|
11988
|
+
},
|
|
11989
|
+
}));
|
|
11990
|
+
}))
|
|
11991
|
+
.addCommand(new Command("restore")
|
|
11992
|
+
.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.")
|
|
11993
|
+
.requiredOption("--table <table>", "charge, plan, or scalar.")
|
|
11994
|
+
.requiredOption("--key <key>", "The row key.")
|
|
11995
|
+
.option("--field <field>", "Plan only: monthly_credits, weekly_credits_limit or rollover_cap.")
|
|
11996
|
+
.option("--json", "Print a JSON envelope.")
|
|
11997
|
+
.action(async (options) => {
|
|
11998
|
+
await handleAsyncAction("admin price-book restore", options, () => requestOxygen("/api/cli/admin/costs", {
|
|
11999
|
+
method: "POST",
|
|
12000
|
+
body: {
|
|
12001
|
+
price_table: options.table,
|
|
12002
|
+
price_key: options.key,
|
|
12003
|
+
...(options.field ? { price_field: options.field } : {}),
|
|
12004
|
+
restore: true,
|
|
12005
|
+
},
|
|
12006
|
+
}));
|
|
12007
|
+
})))
|
|
11863
12008
|
.addCommand(new Command("primary-providers")
|
|
11864
12009
|
.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
12010
|
.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 +13217,7 @@ Examples:
|
|
|
13072
13217
|
.option("--return <mode>", "Legacy response shape: raw, compact, or summary. Defaults to raw.")
|
|
13073
13218
|
.option("--return-mode <mode>", "Response shape: raw, compact, or summary. Prefer summary for large search responses.")
|
|
13074
13219
|
.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
|
|
13220
|
+
.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
13221
|
.option("--approved", "Required for live paid tools and external writes after inspecting dry-run output.")
|
|
13077
13222
|
.option("--json", "Print a JSON envelope.")
|
|
13078
13223
|
.action(async (toolId, options) => {
|
|
@@ -13205,7 +13350,7 @@ Examples:
|
|
|
13205
13350
|
.option("--company-domain <domain>", "Company apex domain, e.g. acme.com.")
|
|
13206
13351
|
.option("--company-name <name>", "Company name.")
|
|
13207
13352
|
.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.")
|
|
13353
|
+
.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
13354
|
.option("--json", "Print a JSON envelope.")
|
|
13210
13355
|
.action(async (options) => {
|
|
13211
13356
|
await handleAsyncAction("find email", options, () => requestOxygen("/api/cli/find/run", { method: "POST", body: buildFindBody("email", options) }));
|
|
@@ -13218,7 +13363,7 @@ Examples:
|
|
|
13218
13363
|
.option("--company-domain <domain>", "Company apex domain, e.g. acme.com.")
|
|
13219
13364
|
.option("--company-name <name>", "Company name.")
|
|
13220
13365
|
.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.")
|
|
13366
|
+
.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
13367
|
.option("--json", "Print a JSON envelope.")
|
|
13223
13368
|
.action(async (options) => {
|
|
13224
13369
|
await handleAsyncAction("find personal-email", options, () => requestOxygen("/api/cli/find/run", { method: "POST", body: buildFindBody("personal_email", options) }));
|
|
@@ -13232,7 +13377,7 @@ Examples:
|
|
|
13232
13377
|
.option("--company-name <name>", "Company name.")
|
|
13233
13378
|
.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
13379
|
.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.")
|
|
13380
|
+
.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
13381
|
.option("--json", "Print a JSON envelope.")
|
|
13237
13382
|
.action(async (options) => {
|
|
13238
13383
|
await handleAsyncAction("find phone", options, () => requestOxygen("/api/cli/find/run", { method: "POST", body: buildFindBody("phone", options) }));
|
|
@@ -13245,7 +13390,7 @@ Examples:
|
|
|
13245
13390
|
.option("--company-name <name>", "Company name.")
|
|
13246
13391
|
.option("--company-linkedin-url <url>", "Company LinkedIn URL.")
|
|
13247
13392
|
.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.")
|
|
13393
|
+
.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
13394
|
.option("--json", "Print a JSON envelope.")
|
|
13250
13395
|
.action(async (options) => {
|
|
13251
13396
|
await handleAsyncAction("find linkedin", options, () => requestOxygen("/api/cli/find/run", { method: "POST", body: buildFindBody("linkedin", options) }));
|
|
@@ -13263,7 +13408,7 @@ Examples:
|
|
|
13263
13408
|
.option("--company-name <name>", "Company name.")
|
|
13264
13409
|
.option("--include-raw", "Also return the provider's raw profile payload (profile_raw) for fields the profile block does not carry.")
|
|
13265
13410
|
.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.")
|
|
13411
|
+
.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
13412
|
.option("--json", "Print a JSON envelope.")
|
|
13268
13413
|
.action(async (options) => {
|
|
13269
13414
|
await handleAsyncAction("find person", options, () => requestOxygen("/api/cli/find/run", { method: "POST", body: buildFindBody("person", options) }));
|
|
@@ -13277,7 +13422,7 @@ Examples:
|
|
|
13277
13422
|
.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
13423
|
.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
13424
|
.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
|
|
13425
|
+
.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
13426
|
.option("--json", "Print a JSON envelope.")
|
|
13282
13427
|
.action(async (options) => {
|
|
13283
13428
|
await handleAsyncAction("find company", options, () => requestOxygen("/api/cli/find/run", { method: "POST", body: buildFindBody("company", options) }));
|
|
@@ -13716,7 +13861,7 @@ Examples:
|
|
|
13716
13861
|
await handleAsyncAction("senders checkpoints", options, () => requestOxygen("/api/cli/senders/checkpoints"));
|
|
13717
13862
|
}))
|
|
13718
13863
|
.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.
|
|
13864
|
+
.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
13865
|
.option("--reconnect <connection_id>", "Reconnect an existing connection instead of creating a new one. Accepts a connection id.")
|
|
13721
13866
|
.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
13867
|
.option("--sales-nav", "Request Classic + Sales Navigator access during hosted sign-in.")
|
|
@@ -13803,7 +13948,7 @@ Examples:
|
|
|
13803
13948
|
}));
|
|
13804
13949
|
}))
|
|
13805
13950
|
.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,
|
|
13951
|
+
.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
13952
|
.argument("<id>", "Sender account id, connection id, or Unipile account id.")
|
|
13808
13953
|
.option("--json", "Print a JSON envelope.")
|
|
13809
13954
|
.action(async (id, options) => {
|
|
@@ -13842,7 +13987,7 @@ Examples:
|
|
|
13842
13987
|
});
|
|
13843
13988
|
}))
|
|
13844
13989
|
.addCommand(new Command("disconnect")
|
|
13845
|
-
.description("Disconnect a LinkedIn sender account so it stops sending
|
|
13990
|
+
.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
13991
|
.argument("<id>", "Sender account id, connection id, or Unipile account id.")
|
|
13847
13992
|
.option("--json", "Print a JSON envelope.")
|
|
13848
13993
|
.action(async (id, options) => {
|
|
@@ -14467,7 +14612,7 @@ Examples:
|
|
|
14467
14612
|
program.addCommand(new Command("linkedin")
|
|
14468
14613
|
.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
14614
|
.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
|
|
14615
|
+
.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
14616
|
.addCommand(new Command("setup")
|
|
14472
14617
|
.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
14618
|
.option("--account <ref>", "Account to capture (sender id, connection id, or Unipile account id). Omit to capture every connected LinkedIn account.")
|
|
@@ -14676,7 +14821,7 @@ Examples:
|
|
|
14676
14821
|
});
|
|
14677
14822
|
}))));
|
|
14678
14823
|
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).")
|
|
14824
|
+
.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
14825
|
.addCommand(addContractFlags(new Command("accounts")
|
|
14681
14826
|
.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
14827
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -14725,7 +14870,7 @@ Examples:
|
|
|
14725
14870
|
});
|
|
14726
14871
|
}))
|
|
14727
14872
|
.addCommand(new Command("disconnect")
|
|
14728
|
-
.description("Disconnect a WhatsApp account so it stops sending
|
|
14873
|
+
.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
14874
|
.argument("<id>", "WhatsApp account id, connection id, or Unipile account id.")
|
|
14730
14875
|
.option("--json", "Print a JSON envelope.")
|
|
14731
14876
|
.action(async (id, options) => {
|
|
@@ -14740,14 +14885,9 @@ Examples:
|
|
|
14740
14885
|
.action(async (id, options) => {
|
|
14741
14886
|
await handleAsyncAction("whatsapp limits get", options, () => requestOxygen(`/api/cli/whatsapp/accounts/${encodeURIComponent(id)}/limits`));
|
|
14742
14887
|
}))
|
|
14743
|
-
.addCommand(new Command("set")
|
|
14888
|
+
.addCommand(addWhatsAppLimitFlags(new Command("set")
|
|
14744
14889
|
.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.")
|
|
14890
|
+
.argument("<id>", "WhatsApp account id, connection id, or Unipile account id."))
|
|
14751
14891
|
.option("--working-days <days>", "Allowed weekdays, e.g. mon-fri or 1,3,5.")
|
|
14752
14892
|
.option("--working-hours-start <HH:MM>", "Local start of the allowed sending window.")
|
|
14753
14893
|
.option("--working-hours-end <HH:MM>", "Local end of the allowed sending window.")
|
|
@@ -14985,7 +15125,7 @@ Examples:
|
|
|
14985
15125
|
});
|
|
14986
15126
|
}))
|
|
14987
15127
|
.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
|
|
15128
|
+
.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. For stored-only inspection, use messages query.")
|
|
14989
15129
|
.argument("<conversation>", "Conversation id, Unipile chat id, or Zapbox thread id.")
|
|
14990
15130
|
.option("--channel <channel>", "Inbox channel: all (default, resolves the id on any channel), email, linkedin, or whatsapp.")
|
|
14991
15131
|
.option("--message-limit <n>", "Maximum messages to return (1-500). Defaults to 100.")
|
|
@@ -15119,7 +15259,7 @@ Examples:
|
|
|
15119
15259
|
}));
|
|
15120
15260
|
}))
|
|
15121
15261
|
.addCommand(new Command("mark-read")
|
|
15122
|
-
.description("Mark a conversation and all its messages as read.")
|
|
15262
|
+
.description("Mark a conversation and all its messages as read (LinkedIn by default; use --channel email or whatsapp for those channels).")
|
|
15123
15263
|
.argument("<conversation>", "Conversation id, Unipile chat id, or (email) Zapbox thread id.")
|
|
15124
15264
|
.option("--channel <channel>", "Inbox channel: linkedin (default), whatsapp, or email.")
|
|
15125
15265
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -15195,6 +15335,7 @@ Examples:
|
|
|
15195
15335
|
params.set("needs_reply", "true");
|
|
15196
15336
|
return requestOxygen(`/api/cli/inbox/read-all?${params.toString()}`, {
|
|
15197
15337
|
method: "POST",
|
|
15338
|
+
previewOnly: !options.yes,
|
|
15198
15339
|
// Approval rides in the body: a bodyless POST reads as
|
|
15199
15340
|
// approved=false server-side, i.e. a preview that mutates
|
|
15200
15341
|
// nothing. --yes (not --approved) because this spends no
|
|
@@ -15357,6 +15498,7 @@ Examples:
|
|
|
15357
15498
|
.filter(Boolean);
|
|
15358
15499
|
return requestOxygen("/api/cli/inbox/bulk", {
|
|
15359
15500
|
method: "POST",
|
|
15501
|
+
previewOnly: !options.yes,
|
|
15360
15502
|
body: {
|
|
15361
15503
|
action,
|
|
15362
15504
|
channel,
|
|
@@ -15974,7 +16116,7 @@ Examples:
|
|
|
15974
16116
|
});
|
|
15975
16117
|
})))
|
|
15976
16118
|
.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.")
|
|
16119
|
+
.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
16120
|
.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
16121
|
.option("--json", "Print a JSON envelope."), SEQUENCE_ANALYTICS_READ_CONTRACT, {
|
|
15980
16122
|
range: { valueName: "<preset>", parse: readOption },
|
|
@@ -16258,7 +16400,7 @@ Examples:
|
|
|
16258
16400
|
await handleAsyncAction("sequences get", options, () => requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}${options.readiness ? "?include=readiness" : ""}`));
|
|
16259
16401
|
}))
|
|
16260
16402
|
.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.
|
|
16403
|
+
.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
16404
|
.argument("<sequence>", "Sequence id or slug.")
|
|
16263
16405
|
.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
16406
|
.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 +16491,7 @@ Examples:
|
|
|
16349
16491
|
}, formatSequencePushExternal);
|
|
16350
16492
|
}))
|
|
16351
16493
|
.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
|
|
16494
|
+
.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
16495
|
.argument("<sequence>", "Sequence id or slug.")
|
|
16354
16496
|
.option("--approved", "Approve and activate live. Without this flag, returns a preview only, including copy_preview rendered samples and blockers.")
|
|
16355
16497
|
.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 +16514,7 @@ Examples:
|
|
|
16372
16514
|
});
|
|
16373
16515
|
}))
|
|
16374
16516
|
.addCommand(new Command("preview")
|
|
16375
|
-
.description("The free launch check, as a read: the same preview
|
|
16517
|
+
.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
16518
|
.argument("<sequence>", "Sequence id or slug.")
|
|
16377
16519
|
.option("--json", "Print a JSON envelope.")
|
|
16378
16520
|
.action(async (sequence, options) => {
|
|
@@ -16410,7 +16552,7 @@ Examples:
|
|
|
16410
16552
|
}));
|
|
16411
16553
|
}))
|
|
16412
16554
|
.addCommand(new Command("resume")
|
|
16413
|
-
.description("Resume a paused sequence.")
|
|
16555
|
+
.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
16556
|
.argument("<sequence>", "Sequence id or slug.")
|
|
16415
16557
|
.option("--json", "Print a JSON envelope.")
|
|
16416
16558
|
.action(async (sequence, options) => {
|
|
@@ -16427,9 +16569,9 @@ Examples:
|
|
|
16427
16569
|
await handleAsyncAction("sequences delete", options, () => requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}${options.approved ? "?approved=true" : ""}`, { method: "DELETE" }));
|
|
16428
16570
|
}))
|
|
16429
16571
|
.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).")
|
|
16572
|
+
.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
16573
|
.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.")
|
|
16574
|
+
.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
16575
|
.option("--step <n>", "Only leads currently on this step (0-based index, as in the sequence's steps).")
|
|
16434
16576
|
.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
16577
|
.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 +16778,7 @@ Examples:
|
|
|
16636
16778
|
});
|
|
16637
16779
|
}))
|
|
16638
16780
|
.addCommand(new Command("reconcile")
|
|
16639
|
-
.description("Safely reconcile one ambiguous LinkedIn invite/message from provider reads. Preview-only unless --approved; never resends.")
|
|
16781
|
+
.description("Safely reconcile one ambiguous LinkedIn invite/message from provider reads. Zero credits. Preview-only unless --approved; never resends.")
|
|
16640
16782
|
.argument("<sequence>", "Sequence id or slug. The sequence must be paused.")
|
|
16641
16783
|
.requiredOption("--action <id>", "Failed/canceled live action UUID to reconcile.")
|
|
16642
16784
|
.option("--approved", "Apply an exact provider match to local state. No provider write or resend occurs.")
|
|
@@ -18283,7 +18425,7 @@ Examples:
|
|
|
18283
18425
|
: requestOxygen(`/api/cli/mailboxes/sync?${new URLSearchParams({ mailbox }).toString()}`));
|
|
18284
18426
|
}))
|
|
18285
18427
|
.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.
|
|
18428
|
+
.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
18429
|
.addHelpText("after", "\nDocs: https://oxygen-agent.com/docs/providers/mailboxes\n")
|
|
18288
18430
|
.option("--provider <provider>", "Mailbox provider: google or microsoft.")
|
|
18289
18431
|
.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.")
|
|
@@ -18513,6 +18655,29 @@ Examples:
|
|
|
18513
18655
|
},
|
|
18514
18656
|
});
|
|
18515
18657
|
});
|
|
18658
|
+
}))
|
|
18659
|
+
.addCommand(new Command("google")
|
|
18660
|
+
.description("Authorize one to ten connected Google inboxes for OXYGEN Warm-up. 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.")
|
|
18661
|
+
.requiredOption("--mailboxes <list>", "Exact Google inbox ids or addresses, comma-separated; maximum ten.")
|
|
18662
|
+
.option("--approved", "Create the previewed authorization links.")
|
|
18663
|
+
.option("--plan <hash>", "Fresh preview hash; required with approval.")
|
|
18664
|
+
.option("--max-credits <n>", "Exactly 0; required with approval.")
|
|
18665
|
+
.option("--status", "Verify completed Google authorizations; never starts warmup.")
|
|
18666
|
+
.option("--json", "Print a JSON envelope.")
|
|
18667
|
+
.action(async (options) => {
|
|
18668
|
+
await handleAsyncAction("mailboxes warmup google", options, () => {
|
|
18669
|
+
const mailboxes = readCsvOption(options.mailboxes);
|
|
18670
|
+
const planHash = readOption(options.plan);
|
|
18671
|
+
const maxCredits = options.maxCredits === undefined ? undefined : readNonNegativeNumber(options.maxCredits);
|
|
18672
|
+
if (options.approved && options.status)
|
|
18673
|
+
throw new Error("Status and approval cannot be combined.");
|
|
18674
|
+
if (options.approved && (!planHash || maxCredits !== 0))
|
|
18675
|
+
throw new Error("Approval requires a fresh --plan and --max-credits 0.");
|
|
18676
|
+
return requestOxygen("/api/cli/mailboxes/warmup/google", { method: "POST", body: {
|
|
18677
|
+
mailboxes, ...(options.approved ? { approved: true } : {}), ...(options.status ? { status: true } : {}),
|
|
18678
|
+
...(planHash ? { plan_hash: planHash } : {}), ...(maxCredits !== undefined ? { max_credits: maxCredits } : {}),
|
|
18679
|
+
} });
|
|
18680
|
+
});
|
|
18516
18681
|
}))
|
|
18517
18682
|
.addCommand(new Command("microsoft")
|
|
18518
18683
|
.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`.")
|
|
@@ -18850,11 +19015,11 @@ Examples:
|
|
|
18850
19015
|
program.addCommand(new Command("domains")
|
|
18851
19016
|
.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
19017
|
.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.")
|
|
19018
|
+
.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
19019
|
.option("--status <status>", "Filter by zone status: unknown, initializing, pending, active, moved, or deleted.")
|
|
18855
19020
|
.option("--registrar <registrar>", "Filter by registrar name.")
|
|
18856
19021
|
.option("--q <text>", "Filter by domain substring.")
|
|
18857
|
-
.option("--dns <state>", "Filter by
|
|
19022
|
+
.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
19023
|
.option("--expiring", "Only non-deleted domains expiring within 60 days.")
|
|
18859
19024
|
.option("--has-mailboxes <bool>", "true → only domains with sending mailboxes; false → only domains without.")
|
|
18860
19025
|
.option("--include-archived", "Include archived domains (hidden by default).")
|
|
@@ -19265,6 +19430,14 @@ Examples:
|
|
|
19265
19430
|
.action(async (callable, options) => {
|
|
19266
19431
|
await handleAsyncAction("callables disable", options, () => requestOxygen("/api/cli/callables/status", { method: "POST", body: { callable, status: "disabled" } }));
|
|
19267
19432
|
}));
|
|
19433
|
+
const workflowRunReadbackHelp = `
|
|
19434
|
+
Stored run values:
|
|
19435
|
+
oxygen workflows runs --run-id <run_id> --projection value --path /runs/0/output --json
|
|
19436
|
+
Canonical graph output is a steps map keyed by node id.
|
|
19437
|
+
Read the observed cost receipt with oxygen workflows run <run_id> --json.
|
|
19438
|
+
For large values, follow next_value_cursor with --value-cursor and the same selection.
|
|
19439
|
+
Append value_json fragments in offset order until next_value_cursor is null, then parse the combined JSON.
|
|
19440
|
+
`;
|
|
19268
19441
|
program
|
|
19269
19442
|
.command("workflows")
|
|
19270
19443
|
.description("Durable workflow automation commands.")
|
|
@@ -19294,8 +19467,10 @@ without --draft publishes the revision; calling or enabling it remains a separat
|
|
|
19294
19467
|
execution/authorization step.
|
|
19295
19468
|
|
|
19296
19469
|
Run completion:
|
|
19297
|
-
Calls enqueue asynchronously.
|
|
19470
|
+
Calls without --preview enqueue asynchronously. Use the returned data.run.id as <run_id>:
|
|
19298
19471
|
oxygen workflows tail <run_id>
|
|
19472
|
+
Preview calls return data.preview and create no run.
|
|
19473
|
+
${workflowRunReadbackHelp}
|
|
19299
19474
|
`)
|
|
19300
19475
|
.addCommand(new Command("templates")
|
|
19301
19476
|
.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 +19709,7 @@ Run completion:
|
|
|
19534
19709
|
await handleAsyncAction("workflows get", options, async () => {
|
|
19535
19710
|
const data = await requestOxygen("/api/cli/workflows/get", {
|
|
19536
19711
|
method: "POST",
|
|
19712
|
+
readOnly: true,
|
|
19537
19713
|
body: { workflow },
|
|
19538
19714
|
});
|
|
19539
19715
|
if (!options.json) {
|
|
@@ -19576,12 +19752,12 @@ Run completion:
|
|
|
19576
19752
|
}), options));
|
|
19577
19753
|
}))
|
|
19578
19754
|
.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
|
|
19755
|
+
.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
19756
|
.argument("[workflow]", "Workflow id, slug, or name.")
|
|
19581
19757
|
.option("--workflow <workflow>", "Workflow id, slug, or name.")
|
|
19582
19758
|
.option("--workflow-id <workflow_id>", "Workflow id or slug.")
|
|
19583
19759
|
.option("--workflow-name <workflow_name>", "Workflow name.")
|
|
19584
|
-
.option("--input-json <json>", "Workflow input object. Defaults to {}.")
|
|
19760
|
+
.option("--input-json <json>", "Workflow input object in every mode, including smoke-test. Defaults to {}.")
|
|
19585
19761
|
.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
19762
|
.option("--idempotency-key <key>", "Optional idempotency key.")
|
|
19587
19763
|
.option("--max-credits <n>", "Required credit ceiling for live calls.")
|
|
@@ -19592,7 +19768,8 @@ Run completion:
|
|
|
19592
19768
|
.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
19769
|
.option("--include-bundle", "Include durable recipe bundles in JSON output.")
|
|
19594
19770
|
.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")
|
|
19771
|
+
.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")
|
|
19772
|
+
.addHelpText("after", workflowRunReadbackHelp)
|
|
19596
19773
|
.action(async (workflowArg, options) => {
|
|
19597
19774
|
const maxCredits = readPositiveNumber(options.maxCredits);
|
|
19598
19775
|
const revisionVersion = readPositiveNumber(options.revision);
|
|
@@ -19606,15 +19783,18 @@ Run completion:
|
|
|
19606
19783
|
if (!options.preview && !readOption(options.mode)) {
|
|
19607
19784
|
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
19785
|
}
|
|
19786
|
+
const idempotencyKey = readOption(options.idempotencyKey);
|
|
19609
19787
|
return prepareWorkflowCliOutput(await requestOxygen("/api/cli/workflows/call", {
|
|
19610
19788
|
method: "POST",
|
|
19789
|
+
...(options.preview ? { previewOnly: true } : {}),
|
|
19790
|
+
...(idempotencyKey ? { idempotencyKey } : {}),
|
|
19611
19791
|
body: {
|
|
19612
19792
|
workflow: readOption(workflowArg) ?? readOption(options.workflow),
|
|
19613
19793
|
...(readOption(options.workflowId) ? { workflow_id: readOption(options.workflowId) } : {}),
|
|
19614
19794
|
...(readOption(options.workflowName) ? { workflow_name: readOption(options.workflowName) } : {}),
|
|
19615
19795
|
input: options.inputJson ? parseJsonObject(options.inputJson) : {},
|
|
19616
19796
|
...(readOption(options.mode) ? { mode: readOption(options.mode) } : {}),
|
|
19617
|
-
...(
|
|
19797
|
+
...(idempotencyKey ? { idempotency_key: idempotencyKey } : {}),
|
|
19618
19798
|
...(maxCredits !== undefined ? { max_credits: maxCredits } : {}),
|
|
19619
19799
|
...(options.approved ? { approved: true } : {}),
|
|
19620
19800
|
...(revisionVersion !== undefined ? { revision_version: revisionVersion } : {}),
|
|
@@ -19634,6 +19814,7 @@ Run completion:
|
|
|
19634
19814
|
.option("--json", "Print a JSON envelope.")
|
|
19635
19815
|
.action((workflow, options) => handleAsyncAction("workflows webhooks rotate", options, () => requestOxygen("/api/cli/workflows/webhooks/rotate", {
|
|
19636
19816
|
method: "POST",
|
|
19817
|
+
previewOnly: !options.approved,
|
|
19637
19818
|
body: {
|
|
19638
19819
|
workflow,
|
|
19639
19820
|
...(options.approved ? { approved: true } : {}),
|
|
@@ -19860,6 +20041,7 @@ Full trigger schema: oxygen workflows schema --subject trigger --json
|
|
|
19860
20041
|
.action(async (runId, options) => {
|
|
19861
20042
|
await handleAsyncAction("workflows run", options, async () => prepareWorkflowCliOutput(await requestOxygen("/api/cli/workflows/run", {
|
|
19862
20043
|
method: "POST",
|
|
20044
|
+
readOnly: true,
|
|
19863
20045
|
body: { run_id: runId },
|
|
19864
20046
|
}), options));
|
|
19865
20047
|
}))
|
|
@@ -19875,6 +20057,7 @@ Full trigger schema: oxygen workflows schema --subject trigger --json
|
|
|
19875
20057
|
const maxCredits = readPositiveNumber(options.maxCredits);
|
|
19876
20058
|
return prepareWorkflowCliOutput(await requestOxygen("/api/cli/workflows/retry", {
|
|
19877
20059
|
method: "POST",
|
|
20060
|
+
previewOnly: !options.approved,
|
|
19878
20061
|
body: {
|
|
19879
20062
|
run_id: runId,
|
|
19880
20063
|
...(options.approved ? { approved: true } : {}),
|
|
@@ -19885,11 +20068,12 @@ Full trigger schema: oxygen workflows schema --subject trigger --json
|
|
|
19885
20068
|
}))
|
|
19886
20069
|
.addCommand(new Command("tail")
|
|
19887
20070
|
.description("Poll until a workflow finishes or parks in waiting/awaiting_approval; clock waits resume automatically.")
|
|
19888
|
-
.argument("<run_id>", "Workflow run UUID.")
|
|
20071
|
+
.argument("<run_id>", "Workflow run UUID returned as data.run.id by workflows call; distinct from data.run.runId.")
|
|
19889
20072
|
.option("--timeout-seconds <n>", "Maximum time to wait. Defaults to 600.")
|
|
19890
20073
|
.option("--interval-seconds <n>", "Polling interval. Defaults to 2.")
|
|
19891
20074
|
.option("--include-bundle", "Include durable recipe bundles in JSON output.")
|
|
19892
20075
|
.option("--json", "Print a JSON envelope.")
|
|
20076
|
+
.addHelpText("after", workflowRunReadbackHelp)
|
|
19893
20077
|
.action(async (runId, options) => {
|
|
19894
20078
|
await handleAsyncAction("workflows tail", options, async () => prepareWorkflowCliOutput(await waitForWorkflowRun(runId, options), options));
|
|
19895
20079
|
}))
|
|
@@ -19902,6 +20086,7 @@ Full trigger schema: oxygen workflows schema --subject trigger --json
|
|
|
19902
20086
|
.action(async (runId, options) => {
|
|
19903
20087
|
await handleAsyncAction("workflows cancel", options, () => requestOxygen("/api/cli/workflows/cancel", {
|
|
19904
20088
|
method: "POST",
|
|
20089
|
+
...(options.preview ? { previewOnly: true } : {}),
|
|
19905
20090
|
body: {
|
|
19906
20091
|
run_id: runId,
|
|
19907
20092
|
...(options.preview ? { preview: true } : {}),
|
|
@@ -20041,6 +20226,7 @@ Full trigger schema: oxygen workflows schema --subject trigger --json
|
|
|
20041
20226
|
const single = workflows.length === 1;
|
|
20042
20227
|
const data = await requestOxygen("/api/cli/workflows/delete", {
|
|
20043
20228
|
method: "POST",
|
|
20229
|
+
previewOnly: !options.yes,
|
|
20044
20230
|
body: {
|
|
20045
20231
|
...(single ? { workflow: workflows[0] } : { workflows }),
|
|
20046
20232
|
...(options.yes ? { approved: true } : {}),
|
|
@@ -20774,6 +20960,7 @@ function waitForWorkflowRun(runId, options) {
|
|
|
20774
20960
|
fetchRun: async () => {
|
|
20775
20961
|
latestEnvelope = await requestOxygen("/api/cli/workflows/run", {
|
|
20776
20962
|
method: "POST",
|
|
20963
|
+
readOnly: true,
|
|
20777
20964
|
body: { run_id: runId },
|
|
20778
20965
|
});
|
|
20779
20966
|
return isRecord(latestEnvelope.run) ? latestEnvelope.run : latestEnvelope;
|
|
@@ -29459,8 +29646,10 @@ async function pushOneMirrorPage(dir, state, candidate) {
|
|
|
29459
29646
|
slug,
|
|
29460
29647
|
kind,
|
|
29461
29648
|
status: "proposal_created",
|
|
29462
|
-
|
|
29463
|
-
...(typeof details.
|
|
29649
|
+
// The API sends snake_case details; camelCase never matched, so push hid the proposal.
|
|
29650
|
+
...(typeof details.proposal_id === "string" ? { proposal_id: details.proposal_id } : {}),
|
|
29651
|
+
...(typeof details.proposal_slug === "string" ? { proposal_slug: details.proposal_slug } : {}),
|
|
29652
|
+
...(typeof details.proposal_web_url === "string" ? { proposal_web_url: details.proposal_web_url } : {}),
|
|
29464
29653
|
reason: typeof details.reason === "string" ? details.reason : "sensitive_page",
|
|
29465
29654
|
};
|
|
29466
29655
|
}
|