@oxygen-agent/cli 1.632.3 → 1.638.1
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 +5 -1
- package/dist/index.js +150 -71
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +4 -4
- package/node_modules/@oxygen/shared/dist/social-capabilities.js +3 -3
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/command-manifest.js
CHANGED
|
@@ -23,7 +23,11 @@ const COMMAND_SEARCH_STOP_WORDS = new Set([
|
|
|
23
23
|
// trusting the default.
|
|
24
24
|
const MUTATING_VERBS = new Set([
|
|
25
25
|
"accept", "ack", "add", "adopt", "apply", "approve", "archive",
|
|
26
|
-
"assign", "attach", "backfill",
|
|
26
|
+
"assign", "attach", "backfill",
|
|
27
|
+
// Exact leaves, not the `billing` prefix — `orgs billing-owners` is a read.
|
|
28
|
+
// Both move a workspace's billing owner, so an agent must treat them as writes.
|
|
29
|
+
"billing-link", "billing-unlink",
|
|
30
|
+
"bind", "buy", "call", "cancel", "chat-action", "clear",
|
|
27
31
|
"comment", "configure", "connect", "create", "delete", "delist", "disable",
|
|
28
32
|
"disconnect", "dispatch", "draft", "duplicate", "edit", "emit", "enable", "enroll",
|
|
29
33
|
"file", "forward", "grant", "harvest", "history", "import", "insert", "interrupt", "invite",
|
package/dist/index.js
CHANGED
|
@@ -888,9 +888,9 @@ const MAILBOX_IMPORT_ROW_LIMIT = 500;
|
|
|
888
888
|
* Normalize common mailbox-vendor export labels locally, then send only
|
|
889
889
|
* Oxygen's canonical fields. Passwords are accepted only in credential mode
|
|
890
890
|
* and only become Google app passwords; Microsoft passwords are dropped because
|
|
891
|
-
*
|
|
892
|
-
* delegation keys are rejected in every mode. Host columns
|
|
893
|
-
* provider but never cross the request boundary.
|
|
891
|
+
* one Outlook OAuth consent per mailbox is its only warmup path. OAuth grants,
|
|
892
|
+
* MFA/TOTP seeds, and delegation keys are rejected in every mode. Host columns
|
|
893
|
+
* may prove the provider but never cross the request boundary.
|
|
894
894
|
*/
|
|
895
895
|
function normalizeMailboxExportRow(row, index, mode) {
|
|
896
896
|
const byHeader = new Map();
|
|
@@ -2040,8 +2040,14 @@ function buildPublishingAnalyticsPath(base, params) {
|
|
|
2040
2040
|
return suffix ? `${base}?${suffix}` : base;
|
|
2041
2041
|
}
|
|
2042
2042
|
function buildPublishingCommentsListPath(options) {
|
|
2043
|
+
const view = readOption(options.view);
|
|
2044
|
+
const status = readOption(options.status);
|
|
2045
|
+
if (view && status) {
|
|
2046
|
+
throw new OxygenError("conflicting_flags", "Pass either --view or --status, not both.", { exitCode: 1 });
|
|
2047
|
+
}
|
|
2043
2048
|
return buildPublishingAnalyticsPath("/api/cli/publishing/comments", {
|
|
2044
|
-
|
|
2049
|
+
view,
|
|
2050
|
+
status,
|
|
2045
2051
|
post_id: readOption(options.post),
|
|
2046
2052
|
assignee: readOption(options.assignee),
|
|
2047
2053
|
channel: readOption(options.channel),
|
|
@@ -2511,6 +2517,7 @@ function buildPromptTemplatesCommand(surface, description) {
|
|
|
2511
2517
|
export function createProgram() {
|
|
2512
2518
|
const program = new Command();
|
|
2513
2519
|
const binaryName = resolveCliBinaryName();
|
|
2520
|
+
const directoryDocsUrl = `${defaultApiUrl()}/docs/agencies`;
|
|
2514
2521
|
program
|
|
2515
2522
|
.name(binaryName)
|
|
2516
2523
|
.description("Revenue infrastructure for B2B startups — agent-operated GTM: tables, enrichment, sequences, workflows, CRM, knowledge. MCP + CLI first; every state-changing action returns a web_url deep-link.")
|
|
@@ -2774,7 +2781,7 @@ export function createProgram() {
|
|
|
2774
2781
|
});
|
|
2775
2782
|
program
|
|
2776
2783
|
.command("orgs")
|
|
2777
|
-
.description("
|
|
2784
|
+
.description("Switch between the organizations (workspaces) you belong to, and share one paid plan across them. If this workspace has no plan of its own, `orgs billing-link` puts it on a plan you already pay for somewhere else instead of buying a second subscription.")
|
|
2778
2785
|
.addCommand(new Command("list")
|
|
2779
2786
|
.description("List organizations available to the current CLI identity.")
|
|
2780
2787
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -2794,30 +2801,43 @@ export function createProgram() {
|
|
|
2794
2801
|
.option("--json", "Print a JSON envelope.")
|
|
2795
2802
|
.action(async (organization, options) => {
|
|
2796
2803
|
await handleOrgUseAction(organization, options, "orgs select");
|
|
2804
|
+
}))
|
|
2805
|
+
.addCommand(new Command("billing-owners")
|
|
2806
|
+
.description("List the organizations that could pay for this workspace: which of the organizations you belong to can cover another workspace's credits, and for each of the rest, why it cannot. Read-only, 0 Oxygen credits.")
|
|
2807
|
+
.option("--json", "Print a JSON envelope.")
|
|
2808
|
+
.action(async (options) => {
|
|
2809
|
+
await handleAsyncAction("orgs billing-owners", options, () => requestOxygen("/api/cli/orgs/billing-owners"));
|
|
2797
2810
|
}))
|
|
2798
2811
|
.addCommand(new Command("billing-link")
|
|
2799
|
-
.description("
|
|
2800
|
-
.
|
|
2812
|
+
.description("Cover a workspace with a plan you already pay for in another organization (one plan, many workspaces) instead of buying a second subscription. Omit --owner when exactly one of your organizations can pay and Oxygen will use it; `orgs billing-owners` lists them all. Credit billing moves, workspace data access does not.")
|
|
2813
|
+
.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.")
|
|
2801
2814
|
.option("--organization <organization>", "Workspace organization to link. Defaults to the active organization.")
|
|
2802
2815
|
.option("--organization-id <id>", "Alias for --organization.")
|
|
2803
|
-
.option("--monthly-credit-cap <credits>", "Optional monthly credit cap for this workspace.")
|
|
2816
|
+
.option("--monthly-credit-cap <credits>", "Optional monthly credit cap for this workspace. Omit it to leave any existing cap untouched.")
|
|
2804
2817
|
.option("--json", "Print a JSON envelope.")
|
|
2805
2818
|
.action(async (options) => {
|
|
2806
|
-
await handleAsyncAction("orgs billing-link", options, () =>
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2819
|
+
await handleAsyncAction("orgs billing-link", options, async () => {
|
|
2820
|
+
const billingOwner = readOption(options.owner) ?? await resolveSoleBillingOwnerRef();
|
|
2821
|
+
return requestOxygen("/api/cli/orgs/billing-link", {
|
|
2822
|
+
method: "POST",
|
|
2823
|
+
body: {
|
|
2824
|
+
billing_owner_organization_id: billingOwner,
|
|
2825
|
+
...(readOption(options.organization) || readOption(options.organizationId)
|
|
2826
|
+
? { organization_id: readOption(options.organization) ?? readOption(options.organizationId) }
|
|
2827
|
+
: {}),
|
|
2828
|
+
// The key is omitted, never sent as null, when the flag is
|
|
2829
|
+
// absent: the server treats a present key as an explicit write,
|
|
2830
|
+
// so sending it unconditionally would wipe an agency's
|
|
2831
|
+
// per-client cap on every re-link.
|
|
2832
|
+
...(readOption(options.monthlyCreditCap)
|
|
2833
|
+
? { monthly_credit_cap: readPositiveNumber(options.monthlyCreditCap) }
|
|
2834
|
+
: {}),
|
|
2835
|
+
},
|
|
2836
|
+
});
|
|
2837
|
+
});
|
|
2818
2838
|
}))
|
|
2819
2839
|
.addCommand(new Command("billing-unlink")
|
|
2820
|
-
.description("Return a workspace to owning its own billing.")
|
|
2840
|
+
.description("Return a workspace to owning its own billing: it stops being covered by another organization's plan and needs a subscription of its own again.")
|
|
2821
2841
|
.option("--organization <organization>", "Workspace organization to unlink. Defaults to the active organization.")
|
|
2822
2842
|
.option("--organization-id <id>", "Alias for --organization.")
|
|
2823
2843
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -3316,7 +3336,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
3316
3336
|
}));
|
|
3317
3337
|
program
|
|
3318
3338
|
.command("publishing")
|
|
3319
|
-
.description("Social publishing, performance, and public-comment operations.")
|
|
3339
|
+
.description("Social publishing, performance, and public-comment operations. Start Community triage with `oxygen publishing comments list --view unanswered --json`.")
|
|
3320
3340
|
.addCommand(new Command("mentions")
|
|
3321
3341
|
.description("Resolve LinkedIn identities for a publish-faithful post preview.")
|
|
3322
3342
|
.addCommand(new Command("resolve")
|
|
@@ -3644,10 +3664,11 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
3644
3664
|
}));
|
|
3645
3665
|
})))
|
|
3646
3666
|
.addCommand(new Command("comments")
|
|
3647
|
-
.description("
|
|
3667
|
+
.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 list reports the exact freshness plus empty_queue_is_current. If that field is false, do not conclude there are no comments: wait for next_sync_at. The worker retries automatically; there is no manual retry by design because it could loop against LinkedIn quota. Queue reads use 0 credits and make no provider call; only explicit approval can post a public reply.")
|
|
3648
3668
|
.addCommand(new Command("list")
|
|
3649
|
-
.description("List the
|
|
3650
|
-
.option("--
|
|
3669
|
+
.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 means every lifecycle state inside the rolling 30-day recent-owned-post scope, not lifetime LinkedIn history. Read-only: no provider call, 0 credits.")
|
|
3670
|
+
.option("--view <view>", "Community view: unanswered (default) or all; all is bounded to recent owned posts.")
|
|
3671
|
+
.option("--status <status>", "Advanced exact-state filter: needs_reply, draft, awaiting_approval, replied, resolved, ignored, spam, or action_unavailable. Cannot be combined with --view.")
|
|
3651
3672
|
.option("--post <post_id>", "Only comments on one scheduled Publishing post.")
|
|
3652
3673
|
.option("--assignee <actor>", "Only one actor id; pass me for your own assignments.")
|
|
3653
3674
|
.option("--channel <channel>", "Social channel. Defaults to linkedin.")
|
|
@@ -8176,14 +8197,15 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8176
8197
|
program
|
|
8177
8198
|
.command("directory")
|
|
8178
8199
|
.description("Manage this organization's public OXYGEN agency directory listing.")
|
|
8200
|
+
.addHelpText("after", `\nDocs: ${directoryDocsUrl}\n`)
|
|
8179
8201
|
.addCommand(new Command("get")
|
|
8180
|
-
.description("Read the listing,
|
|
8202
|
+
.description("Read-only, 0 credits. Read the listing, publish state, and public/settings deep-links.")
|
|
8181
8203
|
.option("--json", "Print a JSON envelope.")
|
|
8182
8204
|
.action(async (options) => {
|
|
8183
8205
|
await handleAsyncAction("directory get", options, () => requestOxygen("/api/cli/directory/listing"));
|
|
8184
8206
|
}))
|
|
8185
8207
|
.addCommand(new Command("update")
|
|
8186
|
-
.description("Update listing
|
|
8208
|
+
.description("Update listing fields. Changes to a published listing are public immediately. Only invited organizations can edit.")
|
|
8187
8209
|
.option("--name <text>", "Agency display name.")
|
|
8188
8210
|
.option("--slug <slug>", "Public URL slug (lowercase letters, digits, dashes).")
|
|
8189
8211
|
.option("--logo-url <url>", "Logo image URL.")
|
|
@@ -8214,7 +8236,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8214
8236
|
});
|
|
8215
8237
|
}))
|
|
8216
8238
|
.addCommand(new Command("publish")
|
|
8217
|
-
.description("Publish
|
|
8239
|
+
.description("Publish at https://oxygen-agent.com/agencies. Requires a tagline, description, and at least one contact method (website, booking link, email, or LinkedIn).")
|
|
8218
8240
|
.option("--json", "Print a JSON envelope.")
|
|
8219
8241
|
.action(async (options) => {
|
|
8220
8242
|
await handleAsyncAction("directory publish", options, () => requestOxygen("/api/cli/directory/listing/publish", { method: "POST", body: {} }));
|
|
@@ -9819,7 +9841,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
9819
9841
|
await handleAsyncAction("senders limits get", options, () => requestOxygen(`/api/cli/senders/${encodeURIComponent(id)}/limits`));
|
|
9820
9842
|
}))
|
|
9821
9843
|
.addCommand(new Command("set")
|
|
9822
|
-
.description("Adjust per-account daily action limits and the daily-reset timezone. Values are clamped to safe maximums (
|
|
9844
|
+
.description("Adjust per-account daily action limits and the daily-reset timezone. Values are clamped to safe maximums (30 invites/day and 40 messages/day). Send windows (time of day) are set per sequence in the campaign schedule, not per account. <id> accepts a sender account id, connection id, or Unipile account id.")
|
|
9823
9845
|
.argument("<id>", "Sender account id, connection id, or Unipile account id.")
|
|
9824
9846
|
.option("--invites-per-day <n>", "Daily LinkedIn connection invites cap.")
|
|
9825
9847
|
.option("--invites-per-week <n>", "Weekly LinkedIn connection invites cap.")
|
|
@@ -10053,10 +10075,10 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
10053
10075
|
});
|
|
10054
10076
|
})));
|
|
10055
10077
|
program.addCommand(new Command("posts")
|
|
10056
|
-
.description("Read and publish LinkedIn posts through a connected account. `get`, `comments`, and `reactions` are LIVE provider reads: they use 0 Oxygen credits but consume the sender's daily account-read allowance. Do not use them when a task forbids provider calls; use `publishing comments list` for the local
|
|
10078
|
+
.description("Read and publish LinkedIn posts through a connected account. `get`, `comments`, and `reactions` are LIVE provider reads: they use 0 Oxygen credits but consume the sender's daily account-read allowance. Do not use them when a task forbids provider calls; use `publishing comments list` for the local Community queue, which discovers recent posts owned by connected accounts in the background. `create` publishes a real post (approval-gated). The direct commands here address ONE post you already have an id for; aggregate analytics still cover posts Oxygen itself published (`oxygen publishing ...`).")
|
|
10057
10079
|
.addCommand(new Command("get")
|
|
10058
10080
|
.description("LIVE provider read of one LinkedIn post. Uses 0 Oxygen credits but consumes the sender's metered account-read allowance; do not run it when the task forbids provider calls. Returns the post and its composite social_id (reuse that social_id for `posts comments`, `posts reactions`, and `engagement harvest --source unipile` — NOT the raw activity URN). This does not populate or refresh the durable `publishing comments` queue.")
|
|
10059
|
-
.requiredOption("--post <id>", "Numeric activity id, activity URL, or composite social_id.
|
|
10081
|
+
.requiredOption("--post <id>", "Numeric activity id, activity URL, or composite social_id. For a direct live read, take the id from the post's LinkedIn URL or a scheduled post (`oxygen publishing posts get <post_id>` → provider_post_id); Community discovers recent owned posts separately in the background.")
|
|
10060
10082
|
.option("--account <ref>", "Sender account to read through (sender id, connection id, or Unipile account id). Omit for the org default.")
|
|
10061
10083
|
.option("--json", "Print a JSON envelope.")
|
|
10062
10084
|
.action(async (options) => {
|
|
@@ -10711,7 +10733,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
10711
10733
|
});
|
|
10712
10734
|
})));
|
|
10713
10735
|
program.addCommand(new Command("inbox")
|
|
10714
|
-
.description("Unified inbox (unibox): private email + LinkedIn DM + WhatsApp conversations in one stream (--channel all), or a single channel. Public comments on
|
|
10736
|
+
.description("Unified inbox (unibox): private email + LinkedIn DM + WhatsApp conversations in one stream (--channel all), or a single channel. Public comments on owned LinkedIn posts are not inbox messages; use `publishing comments`. Scan, read threads, and reply.")
|
|
10715
10737
|
.addCommand(new Command("list")
|
|
10716
10738
|
.description("List conversations newest first. --channel all merges email + LinkedIn + WhatsApp into one stream (narrow it with --channels); --channel email/linkedin/whatsapp lists a single channel. Primary excludes the negative tier (not_now, not_interested, lost, bounced); All includes every ordinary conversation.")
|
|
10717
10739
|
.option("--channel <channel>", "Inbox channel: all (merged), linkedin (default), whatsapp, or email.")
|
|
@@ -12596,7 +12618,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
12596
12618
|
});
|
|
12597
12619
|
})));
|
|
12598
12620
|
program.addCommand(new Command("mailboxes")
|
|
12599
|
-
.description("Native email sending pool: register/refresh Google/Microsoft mailboxes (including secure local Hypertide transfer), pause/disable inboxes, connect EmailGuard monitoring, and run managed
|
|
12621
|
+
.description("Native email sending pool: register/refresh Google/Microsoft mailboxes (including secure local Hypertide transfer), pause/disable inboxes, connect EmailGuard monitoring, and run managed SendKit warmup with explicit plans and credit caps. Safe import preflight: run `oxygen mailboxes compatibility --catalog-only --json`, then `oxygen mailboxes import --file <path> --validate-only --json` (add the credential source flags shown by import help when the file contains credentials).")
|
|
12600
12622
|
.addCommand(new Command("list")
|
|
12601
12623
|
.description("List the org's sending mailboxes with provider, status, warmup state, source (managed = bought through Oxygen, byok = bring-your-own), and a pool overview (including counts by source).")
|
|
12602
12624
|
.option("--status <status>", "Filter by status: active, paused, or disabled.")
|
|
@@ -12612,7 +12634,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
12612
12634
|
});
|
|
12613
12635
|
}))
|
|
12614
12636
|
.addCommand(new Command("get")
|
|
12615
|
-
.description("Get one sending mailbox's configuration/readiness detail (provider, status, daily cap, warmup state, auth mode, and source — managed vs bring-your-own) plus a one-row pool summary. This is not sent/replied/bounced performance; use `oxygen sequences analytics` for native Sequence attribution by mailbox/domain. An ineligible native-send transport includes transport_reason + transport_hint; it does not by itself block
|
|
12637
|
+
.description("Get one sending mailbox's configuration/readiness detail (provider, status, daily cap, warmup state, auth mode, and source — managed vs bring-your-own) plus a one-row pool summary. This is not sent/replied/bounced performance; use `oxygen sequences analytics` for native Sequence attribution by mailbox/domain. An ineligible native-send transport includes transport_reason + transport_hint; it does not by itself block SendKit warmup or EmailGuard monitoring. <mailbox> accepts a mailbox id or email address.")
|
|
12616
12638
|
.argument("<mailbox>", "Mailbox id or email address.")
|
|
12617
12639
|
.option("--json", "Print a JSON envelope.")
|
|
12618
12640
|
.action(async (mailbox, options) => {
|
|
@@ -12625,7 +12647,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
12625
12647
|
await handleAsyncAction("mailboxes health", options, () => requestOxygen("/api/cli/mailboxes/health"));
|
|
12626
12648
|
}))
|
|
12627
12649
|
.addCommand(new Command("compatibility")
|
|
12628
|
-
.description("Read-only compatibility report for every selected mailbox: origin vendor, real infrastructure tier (including InboxKit Azure), OXYGEN native-send connection quality,
|
|
12650
|
+
.description("Read-only compatibility report for every selected mailbox: origin vendor, real infrastructure tier (including InboxKit Azure), OXYGEN native-send connection quality, SendKit warmup path, EmailGuard monitoring path, and exact next actions. Native send distinguishes destination-bound OAuth, provider-managed API transport, admin delegation, and authorization_required. Use --catalog-only for the compact workspace-independent import contract. JSON data.compatibility contains checked rows; data.import_methods and data.import_fields describe accepted transfer inputs; data.provider_matrix is the complete current 18-pair product matrix; data.non_transferable_auth names credentials that must be reconnected; data.summary rolls up states; data.web_url opens the pool. Downstream states distinguish credential_required, consent_required, and vendor_blocked. Never decrypts a credential or calls a downstream provider.")
|
|
12629
12651
|
.option("--mailboxes <list>", "Comma-separated mailbox ids or addresses. Omit for the whole pool.")
|
|
12630
12652
|
.option("--catalog-only", "Return only the bounded import-method, field, auth-boundary, and 18-pair provider catalogs; do not read or return workspace mailbox rows.")
|
|
12631
12653
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -12647,7 +12669,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
12647
12669
|
});
|
|
12648
12670
|
}))
|
|
12649
12671
|
.addCommand(new Command("import")
|
|
12650
|
-
.description("Register (or refresh) sending mailboxes in bulk. Use --validate-only with a local file for a non-mutating, no-network preflight; without it, import is a 0-credit state mutation whose upsert is idempotent by mailbox address. CSV/JSON/JSONL/XLSX identity files from any vendor use --file plus optional --vendor. Compatible Google app-password exports use --from credentials --vendor <source>; --from hypertide remains a shortcut. Connected Zapmail inventories use --from zapmail. In credential files, app_password is Google-only; Microsoft password fields are discarded locally. OAuth grants, MFA/TOTP seeds, and delegation keys never transfer: Microsoft warmup requires
|
|
12672
|
+
.description("Register (or refresh) sending mailboxes in bulk. Use --validate-only with a local file for a non-mutating, no-network preflight; without it, import is a 0-credit state mutation whose upsert is idempotent by mailbox address. CSV/JSON/JSONL/XLSX identity files from any vendor use --file plus optional --vendor. Compatible Google app-password exports use --from credentials --vendor <source>; --from hypertide remains a shortcut. Connected Zapmail inventories use --from zapmail. In credential files, app_password is Google-only; Microsoft password fields are discarded locally. OAuth grants, MFA/TOTP seeds, and delegation keys never transfer: Microsoft warmup requires one Outlook OAuth authorization per mailbox (`mailboxes warmup microsoft`) and EmailGuard remains vendor_blocked for Microsoft. Google app passwords are sent only in the request body, encrypted server-side, and never returned.")
|
|
12651
12673
|
.addHelpText("after", [
|
|
12652
12674
|
"",
|
|
12653
12675
|
"Ordinary identity file contract (CSV / JSON / JSONL / XLSX):",
|
|
@@ -12975,7 +12997,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
12975
12997
|
});
|
|
12976
12998
|
})))
|
|
12977
12999
|
.addCommand(new Command("delegation")
|
|
12978
|
-
.description("Google sending-domain delegation only — not Microsoft OAuth and not a Hypertide handoff to
|
|
13000
|
+
.description("Google sending-domain delegation only — not Microsoft OAuth and not a Hypertide handoff to SendKit warmup or EmailGuard. Reports covered domains plus the exact client id/scopes; --probe mints a real token per mailbox. Read-only, sends no mail, 0 Oxygen credits.")
|
|
12979
13001
|
.option("--domain <domain>", "Only report this sending domain.")
|
|
12980
13002
|
.option("--probe", "Verify each delegated domain by minting a real delegated token per mailbox. Makes one Google token call per mailbox; still 0 Oxygen credits.")
|
|
12981
13003
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -12992,9 +13014,9 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
12992
13014
|
});
|
|
12993
13015
|
}))
|
|
12994
13016
|
.addCommand(new Command("warmup")
|
|
12995
|
-
.description("Mailbox warmup for the sending pool. Oxygen's native warmup runs on ONE rail:
|
|
13017
|
+
.description("Mailbox warmup for the sending pool — the weeks of low-volume conversational mail that make a new inbox look trusted before you cold-send from it. Oxygen's native warmup runs on ONE rail: SendKit, managed and credit-billed at 3,000 credits per warming inbox per month ($3). Oxygen enrolls the mailbox for you — preview first, then re-run with --approved to execute and bill. Google and Microsoft both warm here; a Microsoft inbox first needs one Outlook authorization of its own (`mailboxes warmup microsoft`). Inboxes already warming on the retired TrulyInbox rail keep reporting and are wound down here with `warmup disable`; they are never silently moved onto SendKit.")
|
|
12996
13018
|
.addCommand(new Command("enable")
|
|
12997
|
-
.description("
|
|
13019
|
+
.description("Enroll sending mailboxes in Oxygen's managed SendKit warmup and stamp each mailbox's warmup state. Targets the whole pool unless --mailboxes is given. Without --approved this is a 0-credit, no-provider-write priced preview: nothing is enrolled or billed. Preview one inbox with `oxygen mailboxes warmup enable --mailboxes sender@example.com --json`. Execute by echoing its --plan and --max-credits with --approved. Microsoft inboxes must finish `mailboxes warmup microsoft` (one Outlook authorization each) before they can enroll. New enrollments only ever land on SendKit — the retired TrulyInbox rail is refused here and only accepts teardown.")
|
|
12998
13020
|
.option("--mailboxes <list>", "Comma-separated mailbox ids or addresses to warm. Omit to warm the whole pool.")
|
|
12999
13021
|
.option("--approved", "Execute the PRICED enrollment. Without it the response is a priced preview and nothing is enrolled or billed.")
|
|
13000
13022
|
.option("--plan <hash>", "Hash from the fresh preview (required with --approved).")
|
|
@@ -13038,13 +13060,13 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
13038
13060
|
});
|
|
13039
13061
|
}))
|
|
13040
13062
|
.addCommand(new Command("microsoft")
|
|
13041
|
-
.description("
|
|
13063
|
+
.description("Authorize Microsoft/Outlook mailboxes for SendKit warmup — ONE browser consent PER MAILBOX, never a password or app password. 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 mailboxes and how many consent links are still outstanding, at 0 credits. Approve that fresh hash with --max-credits 0 to mint the links, open every returned consent_url, then re-run with --status to see which mailboxes came back authorized. Warmup itself stays OFF until a separately approved `warmup enable`.")
|
|
13042
13064
|
.option("--mailboxes <list>", "Comma-separated Microsoft mailbox ids or addresses. Omit to select every Microsoft mailbox in the pool.")
|
|
13043
|
-
.option("--tenant <id>", "
|
|
13044
|
-
.option("--approved", "
|
|
13065
|
+
.option("--tenant <id>", "Deprecated and ignored: SendKit consent is per mailbox, so there is no tenant-wide scope to bind. Still accepted so older scripts keep running.")
|
|
13066
|
+
.option("--approved", "Mint the per-mailbox consent links for the exact previewed mailbox scope. Still 0 credits — a human then has to open each link.")
|
|
13045
13067
|
.option("--plan <hash>", "Fresh setup preview hash (required with --approved).")
|
|
13046
13068
|
.option("--max-credits <n>", "Hard Oxygen credit cap; this setup requires exactly 0 (required with --approved).")
|
|
13047
|
-
.option("--status", "Poll the
|
|
13069
|
+
.option("--status", "Poll the per-mailbox consents instead of creating a new setup preview: reports which mailboxes are authorized and which are still waiting for someone to open their link.")
|
|
13048
13070
|
.option("--json", "Print a JSON envelope.")
|
|
13049
13071
|
.action(async (options) => {
|
|
13050
13072
|
await handleAsyncAction("mailboxes warmup microsoft", options, () => {
|
|
@@ -13125,7 +13147,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
13125
13147
|
});
|
|
13126
13148
|
}))
|
|
13127
13149
|
.addCommand(new Command("pause")
|
|
13128
|
-
.description("Pause warmup for TrulyInbox
|
|
13150
|
+
.description("Pause warmup at the rail that actually enrolled each mailbox — SendKit for current enrollments, TrulyInbox for inboxes still warming there. A mailbox left on a rail Oxygen no longer drives reports unsupported instead of being retargeted, so the reply never claims a pause that did not happen. Targets the whole pool unless --mailboxes is given.")
|
|
13129
13151
|
.option("--mailboxes <list>", "Comma-separated mailbox ids or addresses. Omit for the whole pool.")
|
|
13130
13152
|
.option("--json", "Print a JSON envelope.")
|
|
13131
13153
|
.action(async (options) => {
|
|
@@ -13138,7 +13160,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
13138
13160
|
});
|
|
13139
13161
|
}))
|
|
13140
13162
|
.addCommand(new Command("resume")
|
|
13141
|
-
.description("Resume paused warmup at each mailbox's recorded
|
|
13163
|
+
.description("Resume paused warmup at each mailbox's recorded rail (SendKit, or TrulyInbox for inboxes still warming there). A mailbox on a rail Oxygen no longer drives reports unsupported instead of being retargeted. Targets the whole pool unless --mailboxes is given.")
|
|
13142
13164
|
.option("--mailboxes <list>", "Comma-separated mailbox ids or addresses. Omit for the whole pool.")
|
|
13143
13165
|
.option("--json", "Print a JSON envelope.")
|
|
13144
13166
|
.action(async (options) => {
|
|
@@ -13151,7 +13173,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
13151
13173
|
});
|
|
13152
13174
|
}))
|
|
13153
13175
|
.addCommand(new Command("disable")
|
|
13154
|
-
.description("Disable warmup and unenroll each mailbox from
|
|
13176
|
+
.description("Disable warmup and unenroll each mailbox from the rail that actually enrolled it. On a managed rail — SendKit today, TrulyInbox for older enrollments — this also cancels that mailbox's warmup subscription, so billing stops with the vendor removal. This is how you wind an inbox off the retired TrulyInbox rail. Targets the whole pool unless --mailboxes is given.")
|
|
13155
13177
|
.option("--mailboxes <list>", "Comma-separated mailbox ids or addresses. Omit for the whole pool.")
|
|
13156
13178
|
.option("--json", "Print a JSON envelope.")
|
|
13157
13179
|
.action(async (options) => {
|
|
@@ -13164,9 +13186,9 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
13164
13186
|
});
|
|
13165
13187
|
}))
|
|
13166
13188
|
.addCommand(new Command("status")
|
|
13167
|
-
.description("
|
|
13189
|
+
.description("Read warmup analytics back from the rail each mailbox is enrolled on and update its state in the pool. Read-only at the vendor, 0 Oxygen credits. Targets the whole pool unless --mailboxes is given.")
|
|
13168
13190
|
.option("--mailboxes <list>", "Comma-separated mailbox ids or addresses to sync. Omit to sync the whole pool.")
|
|
13169
|
-
.option("--provider <name>", "Optional
|
|
13191
|
+
.option("--provider <name>", "Optional. sendkit is the rail every current enrollment uses; pass trulyinbox only to read inboxes still warming on the retired rail. Omit it to read each mailbox on the rail it is recorded against.")
|
|
13170
13192
|
.option("--dry-run", "Skip the provider call (mailboxes marked pending).")
|
|
13171
13193
|
.option("--json", "Print a JSON envelope.")
|
|
13172
13194
|
.action(async (options) => {
|
|
@@ -13189,28 +13211,6 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
13189
13211
|
const suffix = params.toString();
|
|
13190
13212
|
return requestOxygen(`/api/cli/mailboxes/warmup/status${suffix ? `?${suffix}` : ""}`);
|
|
13191
13213
|
});
|
|
13192
|
-
}))
|
|
13193
|
-
.addCommand(new Command("provision")
|
|
13194
|
-
.description("Legacy Zapmail-to-Instantly export for fleets already provisioned on that retired rail. It is not required for native TrulyInbox warmup; new Hypertide/Zapmail/InboxKit mailboxes use `mailboxes warmup enable`.")
|
|
13195
|
-
.option("--mailboxes <list>", "Comma-separated mailbox ids or addresses to provision. Omit for the whole pool.")
|
|
13196
|
-
.option("--connection <id>", "Zapmail connection id. Defaults to the org's active Zapmail connection (or the managed wallet).")
|
|
13197
|
-
.option("--force", "Re-export mailboxes already exporting/warming/active (default skips them).")
|
|
13198
|
-
.option("--dry-run", "Simulate without calling Zapmail (reports the mailboxes that would be exported).")
|
|
13199
|
-
.option("--json", "Print a JSON envelope.")
|
|
13200
|
-
.action(async (options) => {
|
|
13201
|
-
await handleAsyncAction("mailboxes warmup provision", options, () => {
|
|
13202
|
-
const mailboxes = readCsvOption(options.mailboxes);
|
|
13203
|
-
const connection = readOption(options.connection);
|
|
13204
|
-
return requestOxygen("/api/cli/mailboxes/warmup/provision", {
|
|
13205
|
-
method: "POST",
|
|
13206
|
-
body: {
|
|
13207
|
-
...(mailboxes.length > 0 ? { mailboxes } : {}),
|
|
13208
|
-
...(connection ? { connection_id: connection } : {}),
|
|
13209
|
-
...(options.force ? { force: true } : {}),
|
|
13210
|
-
...(options.dryRun ? { dry_run: true } : {}),
|
|
13211
|
-
},
|
|
13212
|
-
});
|
|
13213
|
-
});
|
|
13214
13214
|
})))
|
|
13215
13215
|
.addCommand(new Command("order")
|
|
13216
13216
|
.description("RETIRED — managed mailboxes are now purchased as monthly subscriptions via `oxygen managed-inboxes subscribe` (InboxKit-backed). This legacy Zapmail order path was never enabled anywhere and now always fails with code `gone`.")
|
|
@@ -16041,11 +16041,24 @@ async function requestColumnsRun(body, table, options) {
|
|
|
16041
16041
|
// Inline (formula) results carry no action_run_id and pass through as-is.
|
|
16042
16042
|
if (!options.background && typeof body.row_id === "string" && isRecord(result)) {
|
|
16043
16043
|
const actionRunId = readRecordString(result, "action_run_id");
|
|
16044
|
-
if (actionRunId)
|
|
16044
|
+
if (actionRunId) {
|
|
16045
|
+
writeSingleRowColumnRunReceipt(result, actionRunId);
|
|
16045
16046
|
return resolveSingleRowColumnRun(result, actionRunId);
|
|
16047
|
+
}
|
|
16046
16048
|
}
|
|
16047
16049
|
return result;
|
|
16048
16050
|
}
|
|
16051
|
+
// The durable run exists before the convenience auto-wait begins. Emit its
|
|
16052
|
+
// receipt on stderr immediately so strict JSON stdout remains one envelope while
|
|
16053
|
+
// a shell/agent timeout can still recover the charged run instead of reporting
|
|
16054
|
+
// a silent failure with no inspectable id.
|
|
16055
|
+
function writeSingleRowColumnRunReceipt(envelope, actionRunId) {
|
|
16056
|
+
const webUrl = readRecordString(envelope, "web_url")
|
|
16057
|
+
?? readRecordString(envelope, "run_web_url");
|
|
16058
|
+
const inspection = webUrl ? ` (${webUrl})` : "";
|
|
16059
|
+
process.stderr.write(`queued: column run ${actionRunId}${inspection}\n`);
|
|
16060
|
+
process.stderr.write(`hint: if this process stops, continue with oxygen table-runs wait ${actionRunId}\n`);
|
|
16061
|
+
}
|
|
16049
16062
|
/**
|
|
16050
16063
|
* Wait for an auto-backgrounded single-row column run to finish and return the
|
|
16051
16064
|
* terminal run merged with its item output (the cell value). On wait timeout
|
|
@@ -18204,6 +18217,72 @@ async function handleOrgUseAction(organization, options, command) {
|
|
|
18204
18217
|
emitCliFailure(command, error);
|
|
18205
18218
|
}
|
|
18206
18219
|
}
|
|
18220
|
+
// Why each organization cannot pay for another workspace, in the user's words.
|
|
18221
|
+
// The codes are the server's; the sentences exist so a blocked `billing-link`
|
|
18222
|
+
// says what to fix instead of printing an enum.
|
|
18223
|
+
const BILLING_OWNER_INELIGIBLE_REASONS = {
|
|
18224
|
+
not_admin: "You are not an admin of that organization.",
|
|
18225
|
+
no_plan: "It has no active paid plan.",
|
|
18226
|
+
trialing: "It is on a trial, and a trial only covers the workspace it started in.",
|
|
18227
|
+
already_linked: "Its own billing is already covered by another organization.",
|
|
18228
|
+
};
|
|
18229
|
+
// `--owner` is optional because the case that matters is the plainest one: the
|
|
18230
|
+
// user pays for exactly one organization and wants this workspace on it. The
|
|
18231
|
+
// eligibility rules (admin seat, active paid plan, no trial) stay on the server
|
|
18232
|
+
// — the CLI only picks when the answer is unambiguous, and turns "which one?"
|
|
18233
|
+
// into a typed failure rather than a guess.
|
|
18234
|
+
async function resolveSoleBillingOwnerRef() {
|
|
18235
|
+
const candidates = readBillingOwnerCandidates(await requestOxygen("/api/cli/orgs/billing-owners"));
|
|
18236
|
+
const eligible = candidates.filter((candidate) => candidate.eligible);
|
|
18237
|
+
if (eligible.length === 1)
|
|
18238
|
+
return eligible[0].id;
|
|
18239
|
+
if (eligible.length > 1) {
|
|
18240
|
+
throw new OxygenError("missing_billing_owner", `${eligible.length} of your organizations can pay for this workspace, so Oxygen will not choose for you. Re-run with --owner <organization>.`, {
|
|
18241
|
+
details: {
|
|
18242
|
+
eligible_billing_owners: eligible.map((candidate) => ({
|
|
18243
|
+
organization_id: candidate.id,
|
|
18244
|
+
name: candidate.name,
|
|
18245
|
+
})),
|
|
18246
|
+
next_step: `oxygen orgs billing-link --owner ${eligible[0].id}`,
|
|
18247
|
+
},
|
|
18248
|
+
});
|
|
18249
|
+
}
|
|
18250
|
+
throw new OxygenError("no_eligible_billing_owner", candidates.length === 0
|
|
18251
|
+
? "You do not belong to another organization, so there is no existing plan to put this workspace on. Start one at https://oxygen-agent.com/billing."
|
|
18252
|
+
: "None of your other organizations can pay for this workspace. Only an organization you administer that is on an active paid plan can cover another workspace — a trial cannot.", {
|
|
18253
|
+
details: {
|
|
18254
|
+
candidates: candidates.map((candidate) => ({
|
|
18255
|
+
organization_id: candidate.id,
|
|
18256
|
+
name: candidate.name,
|
|
18257
|
+
ineligible_reason: candidate.ineligible_reason,
|
|
18258
|
+
reason: candidate.ineligible_reason
|
|
18259
|
+
? BILLING_OWNER_INELIGIBLE_REASONS[candidate.ineligible_reason] ?? null
|
|
18260
|
+
: null,
|
|
18261
|
+
})),
|
|
18262
|
+
next_step: "Start a plan on the organization that should pay at https://oxygen-agent.com/billing, then re-run `oxygen orgs billing-link`.",
|
|
18263
|
+
},
|
|
18264
|
+
});
|
|
18265
|
+
}
|
|
18266
|
+
// Both spellings of the reason field are read, the same way the billing-link
|
|
18267
|
+
// route accepts either spelling of its body keys: the rows come straight out of
|
|
18268
|
+
// the billing-ownership library, and a casing change there must not silently
|
|
18269
|
+
// downgrade "we picked your plan" into "you have no eligible plan".
|
|
18270
|
+
function readBillingOwnerCandidates(payload) {
|
|
18271
|
+
const rows = isRecord(payload) && Array.isArray(payload.candidates) ? payload.candidates : [];
|
|
18272
|
+
const candidates = [];
|
|
18273
|
+
for (const row of rows) {
|
|
18274
|
+
if (!isRecord(row) || typeof row.id !== "string" || !row.id)
|
|
18275
|
+
continue;
|
|
18276
|
+
const reason = row.ineligible_reason ?? row.ineligibleReason;
|
|
18277
|
+
candidates.push({
|
|
18278
|
+
id: row.id,
|
|
18279
|
+
name: typeof row.name === "string" ? row.name : row.id,
|
|
18280
|
+
eligible: row.eligible === true,
|
|
18281
|
+
ineligible_reason: typeof reason === "string" ? reason : null,
|
|
18282
|
+
});
|
|
18283
|
+
}
|
|
18284
|
+
return candidates;
|
|
18285
|
+
}
|
|
18207
18286
|
async function handleProfilesListAction(options) {
|
|
18208
18287
|
try {
|
|
18209
18288
|
const state = await listCredentialProfiles();
|
|
@@ -188,7 +188,7 @@ export const OXYGEN_CAPABILITY_ROUTES = [
|
|
|
188
188
|
gatewayCommands: ["publishing comments list", "publishing analytics summary", "posts get", "posts create"],
|
|
189
189
|
skills: ["oxygen-linkedin-marketing"],
|
|
190
190
|
endpointSections: [],
|
|
191
|
-
intentTerms: ["post artifact", "social post", "post comments", "public comments", "comments needing reply", "post reactions", "post performance", "posting analytics", "broadcast content"],
|
|
191
|
+
intentTerms: ["post artifact", "social post", "post comments", "public comments", "unanswered comments", "comments needing reply", "owned posts", "post reactions", "post performance", "posting analytics", "broadcast content"],
|
|
192
192
|
},
|
|
193
193
|
{
|
|
194
194
|
id: "signals",
|
|
@@ -746,9 +746,9 @@ function isNetNewLinkedInInitiation(query) {
|
|
|
746
746
|
function isOwnedPostCommentIntent(query) {
|
|
747
747
|
if (!/\bcomments?\b/.test(query) || !/\b(linkedin|posts?|publishing)\b/.test(query))
|
|
748
748
|
return false;
|
|
749
|
-
const owned = /\b(my|our|own)\b|\boxygen[ -]published\b|\bpublished through oxygen\b/.test(query);
|
|
749
|
+
const owned = /\b(my|our|own|owned)\b|\boxygen[ -]published\b|\bpublished through oxygen\b/.test(query);
|
|
750
750
|
const operatorWork = /\b(needs?|needing|awaiting)\b.{0,20}\b(response|reply|answer)\b/.test(query)
|
|
751
|
-
|| /\b(reply|respond|answer|triage|queue)\b/.test(query);
|
|
751
|
+
|| /\b(reply|respond|answer|triage|queue|unanswered|unreplied)\b/.test(query);
|
|
752
752
|
return owned || operatorWork;
|
|
753
753
|
}
|
|
754
754
|
function isHostedWorkflowIntent(query) {
|
|
@@ -770,7 +770,7 @@ function isPublicLinkedInRead(query) {
|
|
|
770
770
|
return false;
|
|
771
771
|
const disclaimsConnectedAccount = /\b(without|no|not using|does not require)\b.{0,24}\bconnected(?: linkedin)? account\b/.test(query);
|
|
772
772
|
const connectedOwnership = !disclaimsConnectedAccount
|
|
773
|
-
&& /\b(my|our|own|connected|connections|followers|inbox|messages|recruiter|sales\s*navigator|salesnav|unipile|viewers)\b/.test(query);
|
|
773
|
+
&& /\b(my|our|own|owned|connected|connections|followers|inbox|messages|recruiter|sales\s*navigator|salesnav|unipile|viewers)\b/.test(query);
|
|
774
774
|
if (connectedOwnership)
|
|
775
775
|
return false;
|
|
776
776
|
return /\b(comments?|companies|company|competitor|cookieless|engagers?|harvest|posts?|profiles?|public|reactions?|reactors?|scrape|scraper|search|third[ -]party)\b/.test(query);
|
|
@@ -32,10 +32,10 @@ const LINKEDIN_UNIPILE_CAPABILITIES = {
|
|
|
32
32
|
operations: {
|
|
33
33
|
list_owned_posts: {
|
|
34
34
|
status: "limited",
|
|
35
|
-
source: "
|
|
36
|
-
provider_operation:
|
|
35
|
+
source: "provider",
|
|
36
|
+
provider_operation: "linkedin.users_posts",
|
|
37
37
|
external_write: false,
|
|
38
|
-
limitation: "
|
|
38
|
+
limitation: "recent_owned_posts_30_day_window",
|
|
39
39
|
},
|
|
40
40
|
read_post_metrics: {
|
|
41
41
|
status: "limited",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const OXYGEN_VERSION = "1.
|
|
1
|
+
export const OXYGEN_VERSION = "1.638.1";
|
|
2
2
|
// The GLOBAL CLI compatibility floor: the oldest CLI allowed to call any
|
|
3
3
|
// operational route. Raising it hard-rejects every older CLI from the entire
|
|
4
4
|
// product, so it obeys one law, enforced by scripts/ci/cli-min-version-gate.mjs:
|