@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 CHANGED
@@ -34,4 +34,4 @@ oxygen update
34
34
 
35
35
  For product documentation, visit https://oxygen-agent.com/docs. For support, visit https://oxygen-agent.com.
36
36
 
37
- Version: 1.632.3
37
+ Version: 1.638.1
@@ -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", "bind", "buy", "call", "cancel", "chat-action", "clear",
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
- * tenant consent is its only warmup path. OAuth grants, MFA/TOTP seeds, and
892
- * delegation keys are rejected in every mode. Host columns may prove the
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
- status: readOption(options.status),
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("Organization selection commands.")
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("Bill a workspace through another organization you administer.")
2800
- .requiredOption("--owner <organization>", "Billing owner organization id, Clerk org id, or slug.")
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, () => requestOxygen("/api/cli/orgs/billing-link", {
2807
- method: "POST",
2808
- body: {
2809
- billing_owner_organization_id: readOption(options.owner),
2810
- ...(readOption(options.organization) || readOption(options.organizationId)
2811
- ? { organization_id: readOption(options.organization) ?? readOption(options.organizationId) }
2812
- : {}),
2813
- ...(readOption(options.monthlyCreditCap)
2814
- ? { monthly_credit_cap: readPositiveNumber(options.monthlyCreditCap) }
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("Public comments on posts published through OXYGEN. These are network conversations, not workspace review notes. The local queue targets a six-hour background 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.")
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 local public-comment work queue, oldest first, with polling freshness/backoff even when empty. Defaults to LinkedIn comments needing a reply. Read-only: no provider call, 0 credits.")
3650
- .option("--status <status>", "Filter by needs_reply, draft, awaiting_approval, replied, resolved, ignored, spam, or action_unavailable.")
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, its publish state, and its public/settings deep-links.")
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 profile fields. Only invited organizations can edit.")
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 the listing to the public directory at https://oxygen-agent.com/agencies.")
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 (e.g. max 80 invites/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.")
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 cross-post queue. `create` publishes a real post (approval-gated). Every command here addresses ONE post you already have an id for — Oxygen cannot enumerate an account's own LinkedIn history, so analytics only cover posts Oxygen itself published (`oxygen publishing ...`).")
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. There is no way to LIST your own posts: take the id from the post's LinkedIn URL, or from a post you scheduled (`oxygen publishing posts get <post_id>` → provider_post_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 OXYGEN-published posts are not inbox messages; use `publishing comments`. Scan, read threads, and reply.")
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 TrulyInbox 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).")
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 TrulyInbox warmup or EmailGuard monitoring. <mailbox> accepts a mailbox id or email address.")
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, TrulyInbox 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.")
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 Entra tenant-admin consent and EmailGuard remains vendor_blocked for Microsoft. Google app passwords are sent only in the request body, encrypted server-side, and never returned.")
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 TrulyInbox 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.")
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: TrulyInbox, managed and credit-billed per warming-inbox-month. Oxygen enrolls the mailbox for you — preview first, then re-run with --approved to execute and bill. (Instantly and Warmforge remain BYOK integrations elsewhere in Oxygen; they are no longer warmup rails.)")
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("Enable warmup for sending mailboxes 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.")
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("Set up Microsoft/Entra mailboxes for TrulyInbox through tenant-admin consent — never through a mailbox password. Preview returns the consent URL and exact 0-credit sync plan. If it is not executable yet, open consent and re-preview with --tenant <directory-guid>; approve only that fresh hash with --max-credits 0. Warmup remains OFF until a separately approved `warmup enable`.")
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>", "Entra directory/tenant GUID. Usually inferred from mailbox metadata or the consent response.")
13044
- .option("--approved", "Register/sync the exact previewed tenant scope after the admin opened the consent URL.")
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 persisted TrulyInbox workspace sync instead of creating a new setup preview.")
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-enrolled mailboxes. Rows left on a retired rail report unsupported rather than being retargeted. Targets the whole pool unless --mailboxes is given.")
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 provider. Targets the whole pool unless --mailboxes is given.")
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 its recorded provider. On the managed trulyinbox rail this also cancels the warmup subscription — billing stops with the vendor removal. Targets the whole pool unless --mailboxes is given.")
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("Sync TrulyInbox warmup analytics into the pool, updating each mailbox's state. Targets the whole pool unless --mailboxes is given.")
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; trulyinbox is the only native warmup rail.")
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: "oxygen_store",
36
- provider_operation: null,
35
+ source: "provider",
36
+ provider_operation: "linkedin.users_posts",
37
37
  external_write: false,
38
- limitation: "oxygen_published_posts_only",
38
+ limitation: "recent_owned_posts_30_day_window",
39
39
  },
40
40
  read_post_metrics: {
41
41
  status: "limited",
@@ -1,3 +1,3 @@
1
- export declare const OXYGEN_VERSION = "1.632.3";
1
+ export declare const OXYGEN_VERSION = "1.638.1";
2
2
  export declare const OXYGEN_MINIMUM_CLI_VERSION = "1.181.0";
3
3
  export declare const MANAGED_INBOX_MINIMUM_CLI_VERSION = "1.326.2";
@@ -1,4 +1,4 @@
1
- export const OXYGEN_VERSION = "1.632.3";
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:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxygen-agent/cli",
3
- "version": "1.632.3",
3
+ "version": "1.638.1",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",