@oxygen-agent/cli 1.717.11 → 1.739.0

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.
Files changed (44) hide show
  1. package/README.md +1 -1
  2. package/dist/command-manifest.js +27 -12
  3. package/dist/index.js +243 -72
  4. package/dist/skills.js +2 -2
  5. package/node_modules/@oxygen/shared/dist/axiom-field-budget.d.ts +99 -0
  6. package/node_modules/@oxygen/shared/dist/axiom-field-budget.js +112 -17
  7. package/node_modules/@oxygen/shared/dist/cli-result.js +2 -0
  8. package/node_modules/@oxygen/shared/dist/crm-activity-events.d.ts +20 -0
  9. package/node_modules/@oxygen/shared/dist/crm-activity-events.js +5 -0
  10. package/node_modules/@oxygen/shared/dist/future-signup-events.d.ts +27 -0
  11. package/node_modules/@oxygen/shared/dist/future-signup-events.js +90 -0
  12. package/node_modules/@oxygen/shared/dist/future-signup-lifecycle-projection.d.ts +320 -0
  13. package/node_modules/@oxygen/shared/dist/future-signup-lifecycle-projection.js +1233 -0
  14. package/node_modules/@oxygen/shared/dist/index.d.ts +7 -1
  15. package/node_modules/@oxygen/shared/dist/index.js +14 -1
  16. package/node_modules/@oxygen/shared/dist/notetaker-events.d.ts +4 -0
  17. package/node_modules/@oxygen/shared/dist/notetaker-events.js +23 -0
  18. package/node_modules/@oxygen/shared/dist/plain-support-events.d.ts +40 -0
  19. package/node_modules/@oxygen/shared/dist/plain-support-events.js +43 -0
  20. package/node_modules/@oxygen/shared/dist/redaction.js +6 -1
  21. package/node_modules/@oxygen/shared/dist/user-capability-routing.d.ts +2 -0
  22. package/node_modules/@oxygen/shared/dist/user-capability-routing.js +23 -0
  23. package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
  24. package/node_modules/@oxygen/shared/dist/version.js +1 -1
  25. package/node_modules/@oxygen/shared/dist/webhook-headers.d.ts +10 -0
  26. package/node_modules/@oxygen/shared/dist/webhook-headers.js +48 -0
  27. package/node_modules/@oxygen/shared/dist/workspace-event-catalog.d.ts +31 -1
  28. package/node_modules/@oxygen/shared/dist/workspace-event-catalog.js +112 -1
  29. package/node_modules/@oxygen/workflows/dist/graph/expression.js +7 -5
  30. package/node_modules/@oxygen/workflows/dist/graph/lint.d.ts +9 -0
  31. package/node_modules/@oxygen/workflows/dist/graph/lint.js +409 -7
  32. package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.d.ts +513 -0
  33. package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.js +104 -1
  34. package/node_modules/@oxygen/workflows/dist/graph/params.d.ts +2 -0
  35. package/node_modules/@oxygen/workflows/dist/graph/params.js +15 -0
  36. package/node_modules/@oxygen/workflows/dist/graph/remap.js +12 -3
  37. package/node_modules/@oxygen/workflows/dist/graph/topology.d.ts +9 -0
  38. package/node_modules/@oxygen/workflows/dist/graph/topology.js +33 -1
  39. package/node_modules/@oxygen/workflows/dist/graph/types.d.ts +123 -16
  40. package/node_modules/@oxygen/workflows/dist/graph/types.js +45 -0
  41. package/node_modules/@oxygen/workflows/dist/index.d.ts +94 -2
  42. package/node_modules/@oxygen/workflows/dist/index.js +169 -26
  43. package/node_modules/@oxygen/workflows/dist/portable.js +3 -1
  44. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
9
9
  import { Command, CommanderError, Option } from "commander";
10
10
  import { applyOxygenHelp } from "./help.js";
11
11
  import { buildCommandManifest, getCommandManifestEntry, searchCommandManifest, suggestCommandNames, } from "./command-manifest.js";
12
- import { AGENCY_DIRECTORY_REGIONS, AGENCY_DIRECTORY_SERVICES, COLLAB_GATE_KINDS, COLLAB_GATE_PROSE, COLLAB_SUBJECT_KINDS, COLLAB_SUBJECT_KINDS_PROSE, COLLAB_SUBJECT_LABELS, COLLAB_SUBJECT_PROSE, GATE_KIND_SUBJECTS, describeWorkflowStatusChange, formatCellForDisplay, formatPublicBudgetScopes, SUBJECT_PATH_FORMS_PROSE, formatSubjectPath, exitCodeForOxygenError, parseSubjectPath, parseSubjectRef, parseWorkflowStatusChange, isVersionGreater, isVersionLess, MAX_MCP_TOOL_NAME_LENGTH, OXYGEN_CAPABILITY_ROUTES, OXYGEN_VERSION, OxygenError, getCapabilityRouteMatch, inferCapabilityRoute, parseKnowledgePageMarkdown, PLAN_LIMITS, serializeCapabilityRoute, sleep, success, TAG_KINDS_PROSE, toFailure, workflowMcpToolName, } from "@oxygen/shared";
12
+ import { AGENCY_DIRECTORY_REGIONS, AGENCY_DIRECTORY_SERVICES, COLLAB_GATE_KINDS, COLLAB_GATE_PROSE, COLLAB_SUBJECT_KINDS, COLLAB_SUBJECT_KINDS_PROSE, COLLAB_SUBJECT_LABELS, COLLAB_SUBJECT_PROSE, GATE_KIND_SUBJECTS, describeWorkflowStatusChange, formatCellForDisplay, formatPublicBudgetScopes, SUBJECT_PATH_FORMS_PROSE, formatSubjectPath, exitCodeForOxygenError, parseSubjectPath, parseSubjectRef, parseWorkflowStatusChange, isVersionGreater, isVersionLess, MAX_MCP_TOOL_NAME_LENGTH, OXYGEN_CAPABILITY_ROUTES, OXYGEN_VERSION, OxygenError, getCapabilityRouteMatch, inferUserCapabilityRoute, parseKnowledgePageMarkdown, PLAN_LIMITS, serializeCapabilityRoute, sleep, success, TAG_KINDS_PROSE, toFailure, workflowMcpToolName, } from "@oxygen/shared";
13
13
  import { TAG_COLORS } from "@oxygen/shared/select-options";
14
14
  import { inferImportColumnLabels, inferRowsFileFormat, normalizeImportColumnKey, normalizeRowsForNewTable, normalizeRowsFormat, parseRowsFileBuffer, parseXlsxWorkbookBuffer, } from "@oxygen/shared/file-import";
15
15
  import { MAILBOX_IMPORT_FILE_MAX_BYTES as SHARED_MAILBOX_IMPORT_FILE_MAX_BYTES, MAILBOX_IMPORT_ROW_LIMIT as SHARED_MAILBOX_IMPORT_ROW_LIMIT, normalizeMailboxImportFile as normalizeSharedMailboxImportFile, normalizeMailboxImportVendor as normalizeSharedMailboxImportVendor, normalizeMailboxWorkbookRows, parseMailboxImportText, summarizeMailboxImportValidation as summarizeSharedMailboxImportValidation, } from "@oxygen/shared/mailbox-import";
@@ -736,6 +736,17 @@ function formatWorkspaceEventsList(data) {
736
736
  lines.push(` trigger: ${source}.${eventName} · ${workspaceEventFilterText(event)}`);
737
737
  if (typeof event.description === "string")
738
738
  lines.push(` ${event.description}`);
739
+ const readiness = isRecord(event.readiness) ? event.readiness : null;
740
+ if (readiness?.status === "needs_setup") {
741
+ const reason = typeof readiness.reason === "string" ? readiness.reason : "Setup is required before this event can fire.";
742
+ lines.push(` readiness: setup required — ${reason}`);
743
+ const action = isRecord(readiness.action) ? readiness.action : null;
744
+ if (action && typeof action.command === "string")
745
+ lines.push(` next: ${action.command}`);
746
+ }
747
+ else if (readiness?.status === "ready") {
748
+ lines.push(" readiness: ready");
749
+ }
739
750
  }
740
751
  }
741
752
  if (events.length > WORKSPACE_EVENTS_HUMAN_LIMIT) {
@@ -760,6 +771,15 @@ function formatWorkspaceEventDetail(data) {
760
771
  lines.push(`Kind: ${event.kind}`);
761
772
  if (typeof event.description === "string")
762
773
  lines.push(`When it fires: ${event.description}`);
774
+ const readiness = isRecord(event.readiness) ? event.readiness : null;
775
+ if (readiness?.status === "ready")
776
+ lines.push("Readiness: ready");
777
+ if (readiness?.status === "needs_setup") {
778
+ lines.push(`Readiness: setup required${typeof readiness.reason === "string" ? ` — ${readiness.reason}` : ""}`);
779
+ const action = isRecord(readiness.action) ? readiness.action : null;
780
+ if (action && typeof action.command === "string")
781
+ lines.push(`Next: ${action.command}`);
782
+ }
763
783
  if (isRecord(data.trigger)) {
764
784
  lines.push("", "Trigger:", ...JSON.stringify(data.trigger, null, 2).split("\n").map((line) => ` ${line}`));
765
785
  }
@@ -939,7 +959,7 @@ async function readMailboxImportFile(path) {
939
959
  buffer = readFileSync(path);
940
960
  }
941
961
  catch {
942
- throw new OxygenError("mailbox_import_file_unreadable", `Couldn't read --file '${basename(path)}'. Check that the path exists and is readable. CSV/JSON/JSONL/XLSX identity files are accepted; credential exports require --from credentials --vendor <source>. See https://oxygen-agent.com/docs/providers/mailbox-compatibility.`, { exitCode: 1 });
962
+ throw new OxygenError("mailbox_import_file_unreadable", `Couldn't read --file '${basename(path)}'. Check that the path exists and is readable. CSV/JSON/JSONL/XLSX identity files are accepted; credential exports require --from credentials --vendor <source>. See https://oxygen-agent.com/docs/providers/mailboxes.`, { exitCode: 1 });
943
963
  }
944
964
  if (buffer.byteLength > SHARED_MAILBOX_IMPORT_FILE_MAX_BYTES) {
945
965
  throw new OxygenError("invalid_request", `Mailbox import files must be ${SHARED_MAILBOX_IMPORT_FILE_MAX_BYTES / 1024 / 1024} MB or smaller.`, { exitCode: 1 });
@@ -2487,7 +2507,7 @@ export function createProgram() {
2487
2507
  const query = queryParts.join(" ");
2488
2508
  return {
2489
2509
  query,
2490
- route: serializeCapabilityRoute(inferCapabilityRoute(query)),
2510
+ route: serializeCapabilityRoute(inferUserCapabilityRoute(query)),
2491
2511
  hint: "Hydrate one exact CLI command with `oxygen commands get <exact-command> --json`, or one MCP tool with `oxygen_capabilities_schema`.",
2492
2512
  };
2493
2513
  });
@@ -2635,9 +2655,9 @@ export function createProgram() {
2635
2655
  }));
2636
2656
  program
2637
2657
  .command("support")
2638
- .description("Open and track Plain support conversations for the active OXYGEN organization.")
2658
+ .description("Open and track Plain support conversations for the active OXYGEN organization. Filing is a zero-credit write to the canonical Plain queue.")
2639
2659
  .addCommand(new Command("file")
2640
- .description("Create a Plain support Thread. Use when you're stuck on an OXYGEN operation.")
2660
+ .description("Create a real, zero-credit Plain support Thread immediately. There is no preview: review the exact subject, body, category, and severity before running it. Use when you're stuck on an OXYGEN operation.")
2641
2661
  .requiredOption("--subject <subject>", "One-line summary of the problem.")
2642
2662
  .option("--body <body>", "What you were doing, what happened, and what you tried.")
2643
2663
  .option("--severity <severity>", "low | normal | high. Defaults to normal.")
@@ -2695,7 +2715,108 @@ export function createProgram() {
2695
2715
  }));
2696
2716
  }), { hidden: true })
2697
2717
  .addCommand(new Command("admin")
2698
- .description("Legacy commands; staff triage now uses Plain Inbox or Plain MCP.")
2718
+ .description("Operate the live Plain support queue as OXYGEN staff. Public replies are previewed first and sent as the shared Oxygen Support identity.")
2719
+ .addCommand(new Command("list")
2720
+ .description("List live Plain Threads across customer Tenants (staff only).")
2721
+ .option("--status <status>", "Filter by status.")
2722
+ .option("--limit <n>", "Max tickets to return.")
2723
+ .option("--json", "Print a JSON envelope.")
2724
+ .action(async (options) => {
2725
+ await handleAsyncAction("support admin list", options, () => requestOxygen(withSupportListQuery("/api/cli/admin/support/tickets", options)));
2726
+ }))
2727
+ .addCommand(new Command("get")
2728
+ .description("Show one live Plain Thread, its routing metadata, and customer-visible messages (staff only).")
2729
+ .argument("<ticketId>", "Plain Thread ID (th_...).")
2730
+ .option("--json", "Print a JSON envelope.")
2731
+ .action(async (ticketId, options) => {
2732
+ await handleAsyncAction("support admin get", options, () => requestOxygen(`/api/cli/admin/support/tickets/${encodeURIComponent(ticketId)}`));
2733
+ }))
2734
+ .addCommand(new Command("open")
2735
+ .description("Return the exact Plain Inbox URL for one live Thread (staff only).")
2736
+ .argument("<ticketId>", "Plain Thread ID (th_...).")
2737
+ .option("--json", "Print a JSON envelope.")
2738
+ .action(async (ticketId, options) => {
2739
+ await handleAsyncAction("support admin open", options, () => requestOxygen(`/api/cli/admin/support/tickets/${encodeURIComponent(ticketId)}`)
2740
+ .then((data) => supportAdminOpenResult(data)));
2741
+ }))
2742
+ .addCommand(new Command("claim")
2743
+ .description("Claim a live Plain Thread and move it to In progress without overwriting another assignee (staff only).")
2744
+ .argument("<ticketId>", "Plain Thread ID (th_...).")
2745
+ .option("--agent", "Claim as the authenticated Oxygen Support machine user instead of the signed-in human.")
2746
+ .option("--json", "Print a JSON envelope.")
2747
+ .action(async (ticketId, options) => {
2748
+ await handleAsyncAction("support admin claim", options, () => requestOxygen(`/api/cli/admin/support/tickets/${encodeURIComponent(ticketId)}/update`, {
2749
+ method: "POST",
2750
+ body: { action: "claim", as_machine_user: options.agent === true },
2751
+ }));
2752
+ }))
2753
+ .addCommand(new Command("priority")
2754
+ .description("Set the native Plain priority for a live Thread (staff only).")
2755
+ .argument("<ticketId>", "Plain Thread ID (th_...).")
2756
+ .requiredOption("--priority <priority>", "urgent | high | normal | low")
2757
+ .option("--json", "Print a JSON envelope.")
2758
+ .action(async (ticketId, options) => {
2759
+ await handleSupportAdminUpdateRequest("priority", ticketId, options, {
2760
+ priority: readOption(options.priority),
2761
+ });
2762
+ }))
2763
+ .addCommand(new Command("label-add")
2764
+ .description("Add an active Plain label by external ID (staff only).")
2765
+ .argument("<ticketId>", "Plain Thread ID (th_...).")
2766
+ .requiredOption("--label <externalId>", "Plain label external ID, for example oxygen_request_bug.")
2767
+ .option("--json", "Print a JSON envelope.")
2768
+ .action(async (ticketId, options) => {
2769
+ await handleSupportAdminUpdateRequest("label_add", ticketId, options, {
2770
+ label_external_id: readOption(options.label),
2771
+ });
2772
+ }))
2773
+ .addCommand(new Command("label-remove")
2774
+ .description("Remove a Plain label by external ID (staff only).")
2775
+ .argument("<ticketId>", "Plain Thread ID (th_...).")
2776
+ .requiredOption("--label <externalId>", "Plain label external ID.")
2777
+ .option("--json", "Print a JSON envelope.")
2778
+ .action(async (ticketId, options) => {
2779
+ await handleSupportAdminUpdateRequest("label_remove", ticketId, options, {
2780
+ label_external_id: readOption(options.label),
2781
+ });
2782
+ }))
2783
+ .addCommand(new Command("note")
2784
+ .description("Add a team-only internal note to a live Plain Thread (staff only).")
2785
+ .argument("<ticketId>", "Plain Thread ID (th_...).")
2786
+ .requiredOption("--body <body>", "Internal note body.")
2787
+ .option("--json", "Print a JSON envelope.")
2788
+ .action(async (ticketId, options) => {
2789
+ await handleSupportAdminUpdateRequest("note", ticketId, options, {
2790
+ body: readOption(options.body),
2791
+ });
2792
+ }))
2793
+ .addCommand(new Command("draft")
2794
+ .description("Stage a reply as a team-only Plain note; this never sends to the customer (staff and agents).")
2795
+ .argument("<ticketId>", "Plain Thread ID (th_...).")
2796
+ .requiredOption("--body <body>", "Proposed customer reply.")
2797
+ .option("--json", "Print a JSON envelope.")
2798
+ .action(async (ticketId, options) => {
2799
+ await handleSupportAdminUpdateRequest("draft", ticketId, options, {
2800
+ body: readOption(options.body),
2801
+ });
2802
+ }))
2803
+ .addCommand(new Command("reply")
2804
+ .description("Preview or send a public reply on the Thread's native Plain channel (human staff only; sent as Oxygen Support).")
2805
+ .argument("<ticketId>", "Plain Thread ID (th_...).")
2806
+ .requiredOption("--body <body>", "Customer-visible reply body.")
2807
+ .option("--confirm-ref <ref>", "Send only when this exactly matches the previewed Plain ref (for example T-10). Omit to preview without sending.")
2808
+ .option("--json", "Print a JSON envelope.")
2809
+ .action(async (ticketId, options) => {
2810
+ await handleAsyncAction("support admin reply", options, () => requestOxygen(`/api/cli/admin/support/tickets/${encodeURIComponent(ticketId)}/messages`, {
2811
+ method: "POST",
2812
+ body: {
2813
+ body: readOption(options.body),
2814
+ ...(readOption(options.confirmRef)
2815
+ ? { confirm_ref: readOption(options.confirmRef) }
2816
+ : {}),
2817
+ },
2818
+ }));
2819
+ }))
2699
2820
  .addCommand(new Command("file")
2700
2821
  .description("Retired legacy write; always returns the Plain-cutover error (staff only).")
2701
2822
  .requiredOption("--organization <organization>", "Target organization id or slug the ticket belongs to.")
@@ -2721,15 +2842,7 @@ export function createProgram() {
2721
2842
  organization: readOption(options.organization),
2722
2843
  },
2723
2844
  }));
2724
- }))
2725
- .addCommand(new Command("list")
2726
- .description("Read the legacy OXYGEN ticket archive across organizations; Plain is the live queue (staff only).")
2727
- .option("--status <status>", "Filter by status.")
2728
- .option("--limit <n>", "Max tickets to return.")
2729
- .option("--json", "Print a JSON envelope.")
2730
- .action(async (options) => {
2731
- await handleAsyncAction("support admin list", options, () => requestOxygen(withSupportListQuery("/api/cli/admin/support/tickets", options)));
2732
- }))
2845
+ }), { hidden: true })
2733
2846
  .addCommand(new Command("events")
2734
2847
  .description("Read-only poll of legacy OXYGEN archive events for cutover compatibility; never use it as the live queue (staff only).")
2735
2848
  .option("--after <cursor>", "If has_more=true, pass next_cursor to continue that scan. If false, persist the non-null watermark_cursor for later polls. A watermark poll intentionally replays the settled 24h window, not only unseen events; dedupe every page by (ticket_ref, version).")
@@ -2752,26 +2865,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
2752
2865
  `)
2753
2866
  .action(async (options) => {
2754
2867
  await handleAsyncAction("support admin events", options, () => requestOxygen(withSupportEventsQuery("/api/cli/admin/support/events", options)));
2755
- }))
2756
- .addCommand(new Command("get")
2757
- .description("Show one legacy archived ticket snapshot; Plain is authoritative for live cases (staff only).")
2758
- .argument("<ticketId>", "Ticket UUID.")
2759
- .option("--if-version <version>", "Require the exact decimal version returned by support admin events; stale versions return a conflict without exposing the thread.")
2760
- .option("--json", "Print a JSON envelope.")
2761
- .action(async (ticketId, options) => {
2762
- await handleAsyncAction("support admin get", options, () => requestOxygen(withSupportAdminGetQuery(`/api/cli/admin/support/tickets/${encodeURIComponent(ticketId)}`, options)));
2763
- }))
2764
- .addCommand(new Command("reply")
2765
- .description("Retired legacy write; always returns the Plain-cutover error.")
2766
- .argument("<ticketId>", "Ticket UUID.")
2767
- .requiredOption("--body <body>", "Reply body.")
2768
- .option("--json", "Print a JSON envelope.")
2769
- .action(async (ticketId, options) => {
2770
- await handleAsyncAction("support admin reply", options, () => requestOxygen(`/api/cli/admin/support/tickets/${encodeURIComponent(ticketId)}/messages`, {
2771
- method: "POST",
2772
- body: { body: readOption(options.body) },
2773
- }));
2774
- }))
2868
+ }), { hidden: true })
2775
2869
  .addCommand(new Command("resolve")
2776
2870
  .description("Retired legacy write; always returns the Plain-cutover error (staff only).")
2777
2871
  .argument("<ticketId>", "Ticket UUID.")
@@ -2782,7 +2876,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
2782
2876
  method: "POST",
2783
2877
  body: { resolution: readOption(options.resolution) },
2784
2878
  }));
2785
- }))
2879
+ }), { hidden: true })
2786
2880
  .addCommand(new Command("workflow")
2787
2881
  .description("Retired legacy write; Plain notes and downstream engineering work own triage (staff only).")
2788
2882
  .argument("<ticketId>", "Ticket UUID.")
@@ -2795,7 +2889,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
2795
2889
  .option("--json", "Print a JSON envelope.")
2796
2890
  .action(async (ticketId, options) => {
2797
2891
  await handleSupportAdminWorkflowAction(ticketId, options);
2798
- }))
2892
+ }), { hidden: true })
2799
2893
  .addCommand(new Command("update")
2800
2894
  .description("Retired legacy write; Plain owns status, assignee, and notes (staff only).")
2801
2895
  .argument("<ticketId>", "Ticket UUID.")
@@ -2806,10 +2900,10 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
2806
2900
  .option("--json", "Print a JSON envelope.")
2807
2901
  .action(async (ticketId, options) => {
2808
2902
  await handleSupportAdminUpdateAction(ticketId, options);
2809
- })), { hidden: true });
2903
+ }), { hidden: true }));
2810
2904
  program
2811
2905
  .command("feedback")
2812
- .description("Send feedback or a bug report to the OXYGEN team. Creates the canonical Plain support Thread; transcripts are never attached unless explicitly requested.")
2906
+ .description("Send feedback or a bug report to the OXYGEN team. This is the same immediate, zero-credit canonical Plain Thread write as `support file`; transcripts are never attached unless explicitly requested.")
2813
2907
  .option("-m, --message <message>", "Your feedback or bug report.")
2814
2908
  .option("--severity <severity>", "low | normal | high. Defaults to normal.")
2815
2909
  .option("--category <category>", "Optional category label. Defaults to 'feedback'.")
@@ -9577,7 +9671,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
9577
9671
  }));
9578
9672
  program
9579
9673
  .command("integrations")
9580
- .description("Integration connections and raw provider-webhook subscriptions. Trigger-ready workspace events live under `workflows events list`. Local mailbox exports use `mailboxes import`, not this group.")
9674
+ .description("Connect third-party tools such as PostHog, Slack, and HubSpot, and manage raw provider-webhook subscriptions. Start with `integrations list` to confirm any provider is supported, see its auth mode, and distinguish free connection setup from per-action credit estimates. Trigger-ready workspace events live under `workflows events list`. Local mailbox exports use `mailboxes import`, not this group.")
9581
9675
  .addCommand(new Command("events")
9582
9676
  .description("Inspect raw provider event definitions and manage subscriptions that feed workflows; use `workflows events list` for the trigger-ready workspace catalog.")
9583
9677
  .addCommand(new Command("list")
@@ -9680,9 +9774,10 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
9680
9774
  await handleAsyncAction("integrations list", options, () => requestOxygen("/api/cli/integrations/composio/list"));
9681
9775
  }))
9682
9776
  .addCommand(new Command("connect")
9683
- .description("Connect an integration. OAuth toolkits return a redirect URL; API-key integrations accept --api-key. Run without --api-key first to preview the provider's credential requirements (mode: needs_api_key) — e.g. Instantly keys must be created with all scopes enabled. LinkedIn and WhatsApp return a Unipile hosted-auth URL instead (`oxygen senders connect` / `oxygen whatsapp connect` are the canonical paths).")
9777
+ .description("Connect an integration. OAuth toolkits return a redirect URL; API-key integrations accept --api-key. Run without credentials first for a 0-credit preview of the provider's exact fields (mode: needs_api_key): no credential is submitted, nothing is stored, and no provider call is made. Use repeatable --credential name=value when a provider also requires a host, subdomain, or other named value. To keep a secret out of shell history and process arguments, open the preview's web_url and submit it in Connections; --api-key is for controlled noninteractive use. Credential submission creates or replaces the connection immediately and needs no separate --approved flag. LinkedIn and WhatsApp return a Unipile hosted-auth URL instead (`oxygen senders connect` / `oxygen whatsapp connect` are the canonical paths).")
9684
9778
  .argument("<integration_id>", "Integration id, such as 'slack' or 'serpapi'.")
9685
- .option("--api-key <value>", "API key for Composio API-key toolkits (e.g. SerpAPI, Resend).")
9779
+ .option("--api-key <value>", "API key for controlled noninteractive use. Literal values can appear in shell history and process arguments; prefer the preview web_url for manual entry.")
9780
+ .option("--credential <name=value>", "Named provider credential field. Repeat for multi-field integrations (for example, PostHog: --credential subdomain=us).", collectRepeatable, [])
9686
9781
  .option("--account-id <id>", "Provider account id for integrations that span multiple accounts (e.g. Cloudflare when the token can access more than one account).")
9687
9782
  .option("--country <code>", "ISO 3166-1 alpha-2 country where the LinkedIn account owner normally signs in (required for `connect linkedin`; picks the egress proxy).")
9688
9783
  .option("--pairing-phone-number <e164>", "WhatsApp only: pair by code sent to this E.164 number instead of showing a QR.")
@@ -9690,6 +9785,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
9690
9785
  .action(async (integrationId, options) => {
9691
9786
  await handleAsyncAction("integrations connect", options, () => {
9692
9787
  const apiKey = readOption(options.apiKey)?.trim();
9788
+ const credentials = readNamedCredentials(options.credential);
9693
9789
  const accountId = readOption(options.accountId);
9694
9790
  const country = readOption(options.country)?.trim();
9695
9791
  const pairingPhoneNumber = readOption(options.pairingPhoneNumber)?.trim();
@@ -9698,6 +9794,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
9698
9794
  body: {
9699
9795
  integration_id: integrationId,
9700
9796
  ...(apiKey ? { api_key: apiKey } : {}),
9797
+ ...(credentials ? { credentials } : {}),
9701
9798
  ...(accountId ? { account_id: accountId } : {}),
9702
9799
  ...(country ? { country } : {}),
9703
9800
  ...(pairingPhoneNumber ? { pairing_phone_number: pairingPhoneNumber } : {}),
@@ -12761,7 +12858,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
12761
12858
  });
12762
12859
  })));
12763
12860
  program.addCommand(new Command("managed-inboxes")
12764
- .description("Managed sending inboxes bought through OXYGEN: subscribe a domain + N InboxKit mailboxes (google/microsoft/azure) as a recurring monthly Oxygen-credit purchase, add inboxes to a domain you already own, inspect/verify, and cancel. OXYGEN Warm-up defaults on when available: its quoted recurring credits and exact future addresses are part of the order approval, and the worker activates it after provisioning without a second warmup approval. Subscribe/add-inboxes/cancel are approval-gated (preview → re-run with --approved --quote). The inbox vendor is chosen for you; --vendor pins one.")
12861
+ .description("Managed sending inboxes bought through OXYGEN: subscribe a domain + N InboxKit mailboxes (google/microsoft/azure) as a recurring monthly Oxygen-credit purchase, add inboxes to a domain you already own, inspect/verify, and cancel. OXYGEN Warm-up defaults on when available: its quoted recurring credits and exact future addresses are part of the order approval, and the worker activates it after provisioning without a second warmup approval. Warm-up does not prove native-send authorization: verify each exact address with `mailboxes oauth-health --json`. Subscribe/add-inboxes/cancel are approval-gated (preview → re-run with --approved --quote). The inbox vendor is chosen for you; --vendor pins one.")
12765
12862
  .addCommand(new Command("verify")
12766
12863
  .description("Check that OXYGEN, STRIPE, and the VENDOR agree about what this org is buying. The truth about a managed inbox lives in three systems — what the customer asked for, what they are charged, and what is actually running — and a 200 from any one of them proves nothing. Reports every disagreement with WHO IS LOSING MONEY while it stands (customer_overbilled first, then oxygen_pays). Read-only, no writes, 0 Oxygen credits.")
12767
12864
  .option("--json", "Print a JSON envelope.")
@@ -12775,7 +12872,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
12775
12872
  await handleAsyncAction("managed-inboxes registrant", options, () => requestOxygen("/api/cli/managed-inboxes/registrant"));
12776
12873
  }))
12777
12874
  .addCommand(new Command("subscribe")
12778
- .description("Subscribe a NEW domain + mailboxes as a managed monthly inbox subscription. WITHOUT --approved this prints a priced PREVIEW with a quote_id (nothing ordered, nothing charged); re-run with --approved --quote <id> to place the order. OXYGEN Warm-up defaults on for Google, Microsoft, and Azure when available: preview and approved responses carry `warmup_activation` with the exact `mailboxes` and `scope=warmup_only`; it is null when warmup is disabled or unavailable. For InboxKit, that object also carries `transport=inboxkit_sequencer_export`, `mailbox_credentials_required=false`, and `native_send_oauth_separate=true`. Managed warm-up needs no Google/Microsoft OAuth or mailbox password. Native sending authorization is separate and may still be required before OXYGEN can send. This order authorizes automatic OXYGEN Warm-up activation after provisioning, so no second `mailboxes warmup enable` approval is needed. Prices come from the vendor's LIVE rate card and LIVE domain price, and are refused if they sit below vendor cost. To add mailboxes to a domain you ALREADY own use `managed-inboxes add-inboxes` — this command always registers a new domain and fails on one you own.")
12875
+ .description("Subscribe a NEW domain + mailboxes as a managed monthly inbox subscription. WITHOUT --approved this prints a priced PREVIEW with a quote_id (nothing ordered, nothing charged); re-run with --approved --quote <id> to place the order. OXYGEN Warm-up defaults on for Google, Microsoft, and Azure when available: preview and approved responses carry `warmup_activation` with the exact `mailboxes` and `scope=warmup_only`; it is null when warmup is disabled or unavailable. For InboxKit, Google carries `transport=inboxkit_managed_google_handoff`; Microsoft/Azure carries `transport=inboxkit_sequencer_export`. Both carry `mailbox_credentials_required=false` and `native_send_oauth_separate=true`, so managed warm-up needs no customer-supplied OAuth or mailbox password. Google retrieves the existing managed credential only during activation and never persists or returns it; Microsoft/Azure uses native export. Native sending authorization is separate and may still be required before OXYGEN can send. Verify each exact address with `mailboxes oauth-health --json`; InboxKit's unattended Google domain approval usually lands within minutes and the worker keeps retrying, so a waiting row is not a reason to ask the customer to sign in. This order authorizes automatic OXYGEN Warm-up activation after provisioning, so no second `mailboxes warmup enable` approval is needed. Prices come from the vendor's LIVE rate card and LIVE domain price, and are refused if they sit below vendor cost. To add mailboxes to a domain you ALREADY own use `managed-inboxes add-inboxes` — this command always registers a new domain and fails on one you own.")
12779
12876
  .argument("[domain]", "Sending domain to register + host the mailboxes (e.g. send.acme.com). May also be passed as --domain.")
12780
12877
  .option("--domain <domain>", "Sending domain (alternative to the positional argument).")
12781
12878
  .requiredOption("--provider <provider>", "Mailbox PLATFORM: google, microsoft, or azure. (google/microsoft cap at 5 mailboxes per domain; azure allows 100.)")
@@ -12842,7 +12939,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
12842
12939
  });
12843
12940
  }))
12844
12941
  .addCommand(new Command("add-inboxes")
12845
- .description("Add mailboxes to a managed domain you ALREADY own — no new domain is registered and there is NO domain registration charge, only the extra inboxes' monthly rate (plus their add-ons). WITHOUT --approved this prints a priced PREVIEW with a quote_id and orders nothing; re-run with --approved --quote <id> to place the order. When the standing OXYGEN Warm-up add-on is enabled and available, preview and approved responses carry `warmup_activation` for only the exact newly added `mailboxes`, with `scope=warmup_only`; it is null when warmup is disabled or unavailable. For InboxKit, that object also carries `transport=inboxkit_sequencer_export`, `mailbox_credentials_required=false`, and `native_send_oauth_separate=true`. Managed warm-up needs no Google/Microsoft OAuth or mailbox password. Native sending authorization is separate and may still be required before OXYGEN can send. The quote authorizes automatic OXYGEN Warm-up activation after provisioning, so do not run a second warmup approval. Capped per domain by the platform the domain was bought on (5 google/microsoft, 100 azure) COUNTING the inboxes already on it. To register a NEW domain use `managed-inboxes subscribe` instead.")
12942
+ .description("Add mailboxes to a managed domain you ALREADY own — no new domain is registered and there is NO domain registration charge, only the extra inboxes' monthly rate (plus their add-ons). WITHOUT --approved this prints a priced PREVIEW with a quote_id and orders nothing; re-run with --approved --quote <id> to place the order. When the standing OXYGEN Warm-up add-on is enabled and available, preview and approved responses carry `warmup_activation` for only the exact newly added `mailboxes`, with `scope=warmup_only`; it is null when warmup is disabled or unavailable. For InboxKit, Google carries `transport=inboxkit_managed_google_handoff`; Microsoft/Azure carries `transport=inboxkit_sequencer_export`. Both carry `mailbox_credentials_required=false` and `native_send_oauth_separate=true`, so managed warm-up needs no customer-supplied OAuth or mailbox password. Google retrieves the existing managed credential only during activation and never persists or returns it; Microsoft/Azure uses native export. Native sending authorization is separate and may still be required before OXYGEN can send. Verify each exact address with `mailboxes oauth-health --json`; InboxKit's unattended Google domain approval usually lands within minutes and the worker keeps retrying, so a waiting row is not a reason to ask the customer to sign in. The quote authorizes automatic OXYGEN Warm-up activation after provisioning, so do not run a second warmup approval. Capped per domain by the platform the domain was bought on (5 google/microsoft, 100 azure) COUNTING the inboxes already on it. To register a NEW domain use `managed-inboxes subscribe` instead.")
12846
12943
  .argument("[domain]", "A managed domain this workspace already owns (e.g. send.acme.com). May also be passed as --domain.")
12847
12944
  .option("--domain <domain>", "The managed inbox domain (alternative to the positional argument).")
12848
12945
  .option("--mailboxes <json>", "JSON array of mailboxes: [{\"username\",\"first_name\",\"last_name\",\"profile_picture_url\"}]. The vendor stamps the names on each mailbox, so real names belong here. profile_picture_url is optional and must be a PUBLIC, PERMANENT https URL the vendor can fetch at provisioning; `oxygen managed-inboxes upload-avatar <path>` returns one.")
@@ -13005,7 +13102,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
13005
13102
  });
13006
13103
  })));
13007
13104
  program.addCommand(new Command("mailboxes")
13008
- .description("Native email sending pool: register/refresh Google/Microsoft mailboxes (including secure local credential-file transfer), pause/disable inboxes, connect EmailGuard monitoring, and inspect/control OXYGEN Warm-up. Fresh managed InboxKit Google/Microsoft/Azure orders activate exact native warm-up exports automatically after provisioning under their approved default-on add-on. Standalone warm-up plans and credit caps are for BYOK/imported mailboxes, managed opt-outs, or later separate enrollment. OXYGEN Warm-up never owns campaign dispatch. 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).")
13105
+ .description("Native email sending pool: register/refresh Google/Microsoft mailboxes (including secure local credential-file transfer), pause/disable inboxes, connect EmailGuard monitoring, and inspect/control OXYGEN Warm-up. Fresh managed InboxKit Google/Microsoft/Azure orders activate exact-scope warm-up automatically after provisioning under their approved default-on add-on. Google reuses the managed credential just in time; Microsoft/Azure uses native export. Standalone warm-up plans and credit caps are for BYOK/imported mailboxes, managed opt-outs, or later separate enrollment. OXYGEN Warm-up never owns campaign dispatch. 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).")
13009
13106
  .addCommand(new Command("list")
13010
13107
  .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). To see only Google/Microsoft mailboxes that still need OAuth connection and the right remedy for each, use `oxygen mailboxes oauth-health --json`.")
13011
13108
  .option("--status <status>", "Filter by status: active, paused, or disabled.")
@@ -13032,7 +13129,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
13032
13129
  await handleAsyncAction("mailboxes get", options, () => requestOxygen(`/api/cli/mailboxes/${encodeURIComponent(mailbox)}`));
13033
13130
  }))
13034
13131
  .addCommand(new Command("delete")
13035
- .description("Preview or approve removal of exact mailboxes from Oxygen at 0 credits. Deletion immediately removes them from sending but never deletes the underlying Google Workspace or Microsoft 365 accounts. It preserves conversation/message/delivery history and stops the 1,000-credit mailbox commitment for future renewals; the current period is not refunded. Listed active/paused sequences keep their status but lose these senders and are not automatically paused. Managed mailboxes and live warmup/monitoring add-ons fail closed with their exact address-scoped teardown steps.")
13132
+ .description("Preview or approve removal of exact mailboxes from Oxygen at 0 credits. Deletion immediately removes them from sending but never deletes the underlying Google Workspace or Microsoft 365 accounts. It preserves conversation/message/delivery history and stops any separately listed 1,000-credit connected-mailbox commitment for future renewals; that line is currently built but not charged, and it is not the 3,000-credit OXYGEN Warm-up subscription. The current period is not refunded. Listed active/paused sequences keep their status but lose these senders and are not automatically paused. Managed mailboxes and live warmup/monitoring add-ons fail closed with their exact address-scoped teardown steps.")
13036
13133
  .requiredOption("--mailboxes <list>", "Comma-separated mailbox ids or addresses (maximum 500).")
13037
13134
  .option("--approved", "Execute the fresh preview. Requires --plan-hash and --confirmation.")
13038
13135
  .option("--plan-hash <hash>", "Fresh preview plan_hash.")
@@ -13089,7 +13186,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
13089
13186
  });
13090
13187
  }))
13091
13188
  .addCommand(new Command("import")
13092
- .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>. 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: fresh managed InboxKit Google/Microsoft/Azure orders use automatic native InboxKit Sequencer export when their approved order includes warmup; opted-out/separate managed enrollment uses standalone `warmup enable`; eligible non-InboxKit Microsoft warmup uses the Outlook OAuth fallback (`mailboxes warmup microsoft`); EmailGuard remains vendor_blocked for Microsoft. Google app passwords are sent only in the request body, encrypted server-side, and never returned.")
13189
+ .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>. 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 from customer files: fresh managed InboxKit Google orders reuse the vendor-held credential just in time for automatic warmup, while fresh managed Microsoft/Azure orders use native InboxKit Sequencer export; opted-out/separate managed enrollment uses standalone `warmup enable`; eligible non-InboxKit Microsoft warmup uses the Outlook OAuth fallback (`mailboxes warmup microsoft`); EmailGuard remains vendor_blocked for Microsoft. Google app passwords supplied through an authorized credential import are sent only in the request body, encrypted server-side, and never returned.")
13093
13190
  .addHelpText("after", [
13094
13191
  "",
13095
13192
  "Ordinary identity file contract (CSV / JSON / JSONL / XLSX):",
@@ -13102,7 +13199,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
13102
13199
  ' JSON: {"mailboxes":[{"email_address":"ada@send-acme.com","provider":"google","app_password":"<Google mailbox app password>"}]}',
13103
13200
  " Only Google app passwords enter the encrypted seven-day transfer vault. Microsoft rows remain identity-only. Generic SMTP passwords and OAuth/MFA/delegation secrets are rejected.",
13104
13201
  " Validation: add --validate-only to parse the complete real file and return safe aggregate counts plus the provider-specific exact-account OAuth review plan without authentication, a network request, or a workspace write. The plan groups only supplied addresses into reviews of at most 10; it never discovers a domain, and the later online preview may skip existing grants. Any parse, shape, provider, platform, tenant, secret-policy, or duplicate-conflict error rejects the entire file before the first mailbox write and names mailboxes[index]; validation-only never writes. A later import infrastructure failure may interrupt the upsert; re-run the same file because import is idempotent by address.",
13105
- " Docs: https://oxygen-agent.com/docs/providers/mailbox-compatibility",
13202
+ " Docs: https://oxygen-agent.com/docs/providers/mailboxes",
13106
13203
  " Skill: oxygen-email-infra (`oxygen skills install --skill oxygen-email-infra`).",
13107
13204
  "",
13108
13205
  ].join("\n"))
@@ -13300,8 +13397,8 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
13300
13397
  });
13301
13398
  }))
13302
13399
  .addCommand(new Command("connect-oauth")
13303
- .description("Connect Google/Microsoft mailboxes to OXYGEN native send. Preview by default; pass --approved only after reviewing exact candidates. --vendor oxygen requires an exact address list and never discovers a domain directory. Microsoft supports --authorization-mode tenant (one administrator approval, up to 500 exact selected addresses, recommended for dedicated sending tenants) or individual (one account sign-in per mailbox, max 10 per run, recommended for ordinary company/personal mailboxes). Microsoft's application grant is tenant-wide at the provider; OXYGEN—not Microsoft—enforces the selected-address boundary at verification and send time unless the tenant separately configures Exchange Application RBAC. Connecting authorization does not change whether a mailbox is active or disabled. Google uses individual OAuth. Source tokens, passwords, authenticator seeds, and one-time codes never transfer. Managed authorization paths remain available through their vendor options. All paths cost 0 Oxygen credits.")
13304
- .addHelpText("after", "\nDocs: https://oxygen-agent.com/docs/providers/mailbox-compatibility\n")
13400
+ .description("Connect Google/Microsoft mailboxes to OXYGEN native send. Preview by default; pass --approved only after reviewing exact candidates. --vendor oxygen requires an exact address list and never discovers a domain directory. Microsoft supports --authorization-mode tenant (one administrator approval, up to 500 exact selected addresses, recommended for dedicated sending tenants) or individual (one account sign-in per mailbox, max 10 per run, recommended for ordinary company/personal mailboxes). For Microsoft's built-in roles, tenant mode requires a Global Administrator or Privileged Role Administrator because it requests the Microsoft Graph application Mail.Send and Mail.Read permissions; Application Administrator and Cloud Application Administrator cannot consent to Microsoft Graph application roles. Microsoft's application grant is tenant-wide at the provider; OXYGEN—not Microsoft—enforces the selected-address boundary at verification and send time unless the tenant separately configures Exchange Application RBAC. For non-managed Microsoft mailboxes, native-send approval does not authorize OXYGEN Warm-up: every mailbox still needs its own `mailboxes warmup microsoft` Outlook consent before standalone enrollment. Connecting authorization does not change whether a mailbox is active or disabled. Google uses individual OAuth. Source tokens, passwords, authenticator seeds, and one-time codes never transfer. Managed authorization paths remain available through their vendor options. All paths cost 0 Oxygen credits.")
13401
+ .addHelpText("after", "\nDocs: https://oxygen-agent.com/docs/providers/mailboxes\n")
13305
13402
  .option("--provider <provider>", "Mailbox provider to provision: google or microsoft.")
13306
13403
  .option("--vendor <vendor>", "Authorization path: oxygen for imported/manual mailboxes, zapmail (default), or inboxkit.")
13307
13404
  .option("--mailboxes <list>", "Comma-separated exact mailbox addresses. Required for vendor=oxygen (tenant mode max 500; individual mode max 10).")
@@ -13440,11 +13537,11 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
13440
13537
  });
13441
13538
  }))
13442
13539
  .addCommand(new Command("warmup")
13443
- .description("OXYGEN Warm-up for the sending pool — the weeks of low-volume conversational mail that make a new inbox look trusted before you cold-send from it. It is managed and credit-billed at 3,000 credits per warming inbox per month ($3). Fresh managed InboxKit Google/Microsoft/Azure orders with the default-on warm-up add-on activate automatically after provisioning under the approved order quote; do not run a second `warmup enable`. Standalone preview → approval is for BYOK/imported mailboxes, a managed opt-out, or later separate enrollment. InboxKit auto-export stays off because Oxygen submits only the exact authorized UIDs, and any non-cancelled InboxKit warm-up blocks the handoff so one mailbox cannot warm twice. OXYGEN Warm-up never owns campaign dispatch: native OXYGEN Sequences still own campaigns and sends. Eligible non-InboxKit Microsoft mailboxes use the separate `mailboxes warmup microsoft` OAuth fallback. Inboxes already warming on the retired TrulyInbox rail keep reporting and are wound down there with `warmup disable`; they are never silently moved to the current managed rail.")
13444
- .addHelpText("after", "\nDocs: https://oxygen-agent.com/docs/providers/mailbox-compatibility\n")
13540
+ .description("OXYGEN Warm-up for the sending pool — the weeks of low-volume conversational mail that make a new inbox look trusted before you cold-send from it. It is managed and credit-billed at 3,000 credits per warming inbox per month ($3). Fresh managed InboxKit Google/Microsoft/Azure orders with the default-on warm-up add-on activate automatically after provisioning under the approved order quote; do not run a second `warmup enable`. Google reuses the InboxKit-held credential only during activation and never stores or returns it; Microsoft/Azure uses exact native export. Standalone preview → approval is for BYOK/imported mailboxes, a managed opt-out, or later separate enrollment. InboxKit auto-export stays off for Microsoft/Azure because Oxygen submits only the exact authorized UIDs, and any non-cancelled InboxKit warm-up blocks either handoff so one mailbox cannot warm twice. OXYGEN Warm-up never owns campaign dispatch: native OXYGEN Sequences still own campaigns and sends. Eligible non-InboxKit Microsoft mailboxes use the separate `mailboxes warmup microsoft` OAuth fallback. Inboxes already warming on the retired TrulyInbox rail keep reporting and are wound down there with `warmup disable`; they are never silently moved to the current managed rail.")
13541
+ .addHelpText("after", "\nDocs: https://oxygen-agent.com/docs/providers/mailboxes\n")
13445
13542
  .addCommand(new Command("enable")
13446
- .description("STANDALONE ENROLLMENT: enroll BYOK/imported mailboxes, a managed order that opted out, or mailboxes being added to warmup later. Fresh managed InboxKit Google/Microsoft/Azure orders whose approved quote included warmup activate automatically after provisioning and do not need this command. Targets the whole pool unless --mailboxes is given; one synchronous preview/approval is capped at 220 mailboxes, so split larger pools into explicit batches. Without --approved this is a 0-credit, no-provider-write priced preview: nothing is exported, 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. Eligible managed InboxKit targets still use InboxKit's native Sequencer export with exact UIDs; auto-export remains off, and any non-cancelled InboxKit warmup must be cancelled before export (pausing is not enough). Only eligible non-InboxKit Microsoft mailboxes need `mailboxes warmup microsoft`. OXYGEN Warm-up retains no campaign authority — OXYGEN Sequences own enrollment and dispatch. New enrollments only ever land on the managed OXYGEN rail; the retired TrulyInbox rail is refused here and only accepts teardown.")
13447
- .addHelpText("after", "\nDocs: https://oxygen-agent.com/docs/providers/mailbox-compatibility\n")
13543
+ .description("STANDALONE ENROLLMENT: enroll BYOK/imported mailboxes, a managed order that opted out, or mailboxes being added to warmup later. Fresh managed InboxKit Google/Microsoft/Azure orders whose approved quote included warmup activate automatically after provisioning and do not need this command. Targets the whole pool unless --mailboxes is given; one synchronous preview/approval is capped at 220 mailboxes, so split larger pools into explicit batches. Without --approved this is a 0-credit, no-provider-write priced preview: nothing is handed off, enrolled, or billed. The current public price is 3,000 credits per warming mailbox-month; the preview returns the exact first-cycle ceiling. Preview one inbox with `oxygen mailboxes warmup enable --mailboxes sender@example.com --json`. Execute by echoing its --plan and --max-credits with --approved. Eligible managed Google targets use the InboxKit-held credential just in time without storing or returning it; Microsoft/Azure targets use native InboxKit Sequencer export with exact UIDs and auto-export off. Any non-cancelled InboxKit warmup must be cancelled before either handoff (pausing is not enough). Only eligible non-InboxKit Microsoft mailboxes need `mailboxes warmup microsoft`. OXYGEN Warm-up retains no campaign authority — OXYGEN Sequences own enrollment and dispatch. New enrollments only ever land on the managed OXYGEN rail; the retired TrulyInbox rail is refused here and only accepts teardown.")
13544
+ .addHelpText("after", "\nDocs: https://oxygen-agent.com/docs/providers/mailboxes\n")
13448
13545
  .option("--mailboxes <list>", "Comma-separated mailbox ids or addresses to warm. Omit to warm the whole pool.")
13449
13546
  .option("--approved", "Execute the PRICED enrollment. Without it the response is a priced preview and nothing is enrolled or billed.")
13450
13547
  .option("--plan <hash>", "Hash from the fresh preview (required with --approved).")
@@ -13489,7 +13586,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
13489
13586
  }))
13490
13587
  .addCommand(new Command("microsoft")
13491
13588
  .description("NON-INBOXKIT FALLBACK ONLY: authorize eligible Microsoft/Outlook mailboxes for OXYGEN Warm-up with ONE browser consent PER MAILBOX, never a password or app password. Do not run this for managed InboxKit mailboxes: fresh Microsoft/Azure orders activate native InboxKit warm-up exports automatically when the approved order includes warmup; opted-out or later separate managed enrollment uses standalone `warmup enable`. For the non-InboxKit fallback there is no tenant-admin shortcut: a 40-inbox Microsoft pool means 40 separate authorizations, each opened by whoever can sign in to that mailbox. The preview names the exact fallback mailboxes and outstanding links at 0 credits. Approve that fresh hash with --max-credits 0, open every returned consent_url, then re-run with --status. Warmup itself stays OFF until standalone `warmup enable`.")
13492
- .addHelpText("after", "\nDocs: https://oxygen-agent.com/docs/providers/mailbox-compatibility\n")
13589
+ .addHelpText("after", "\nDocs: https://oxygen-agent.com/docs/providers/mailboxes\n")
13493
13590
  .option("--mailboxes <list>", "Comma-separated eligible non-InboxKit Microsoft mailbox ids or addresses. Omit to select the pool; the command refuses a scope containing managed InboxKit rows and points to warmup enable.")
13494
13591
  .option("--tenant <id>", "Deprecated and ignored: OXYGEN Warm-up consent is per mailbox, so there is no tenant-wide scope to bind. Still accepted so older scripts keep running.")
13495
13592
  .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.")
@@ -13831,7 +13928,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
13831
13928
  }));
13832
13929
  })))
13833
13930
  .addCommand(new Command("sync")
13834
- .description("Refresh the domain cache from Cloudflare: zone pages, registrar metadata, RDAP age backfill, and DNS health. Partial (paginated) syncs auto-continue until complete.")
13931
+ .description("Refresh Cloudflare-backed domains only: zone pages, registrar metadata, RDAP age backfill, and DNS health. Vendor-managed bundle domains are skipped here and read live with `domains get` / `domains dns`. Partial (paginated) syncs auto-continue until complete.")
13835
13932
  .option("--full", "Restart the sweep from the first zone page instead of resuming the cursor.")
13836
13933
  .option("--json", "Print a JSON envelope.")
13837
13934
  .action(async (options) => {
@@ -14125,6 +14222,13 @@ Authoring guide:
14125
14222
  Install or refresh the focused product skill:
14126
14223
  oxygen skills install --skill oxygen-workflow-authoring
14127
14224
 
14225
+ Web creation chooser:
14226
+ Open /workspaces/<workspace-id>/workflows and click Create workflow.
14227
+ Choose Blank workflow, Workflow template (one workflow), Blueprint
14228
+ (a multi-resource motion), or Import definition. The chooser opens on the
14229
+ collection page; it has no separate public URL.
14230
+ Guide: https://oxygen-agent.com/docs/execution/workflows
14231
+
14128
14232
  Fastest editable graph:
14129
14233
  1. oxygen workflows events list --search "<outcome>" --kind builtin --json
14130
14234
  2. oxygen workflows init --id my-workflow
@@ -14244,7 +14348,7 @@ Run completion:
14244
14348
  });
14245
14349
  }))
14246
14350
  .addCommand(new Command("apply")
14247
- .description("Compile and publish a workflow definition. Apply costs 0 credits, calls no graph nodes, creates no run, and preserves disabled status; call or enable separately to execute.")
14351
+ .description("Compile and save a workflow definition; --draft appends an editable revision without publishing it. Apply costs 0 credits, calls no graph nodes, creates no run, and preserves disabled status; call or enable separately to execute.")
14248
14352
  .requiredOption("--file <path>", "Workflow module or manifest JSON file.")
14249
14353
  .option("--approved", "Authorize autonomous tool calls for this revision.")
14250
14354
  .option("--max-credits <n>", "Optional explicit positive ceiling for each autonomous delivery; omit to use the plan-tier default.")
@@ -14387,19 +14491,20 @@ Run completion:
14387
14491
  }), options));
14388
14492
  }))
14389
14493
  .addCommand(new Command("call")
14390
- .description("Enqueue a workflow run asynchronously through its API trigger. Follow the returned run_id with `oxygen workflows tail <run_id>`.")
14494
+ .description("Enqueue a workflow run asynchronously and directly without simulating its trigger. Follow the returned run_id with `oxygen workflows tail <run_id>`.")
14391
14495
  .argument("[workflow]", "Workflow id, slug, or name.")
14392
14496
  .option("--workflow <workflow>", "Workflow id, slug, or name.")
14393
14497
  .option("--workflow-id <workflow_id>", "Workflow id or slug.")
14394
14498
  .option("--workflow-name <workflow_name>", "Workflow name.")
14395
14499
  .option("--input-json <json>", "Workflow input object. Defaults to {}.")
14396
- .requiredOption("--mode <mode>", "Execution mode: live, dry-run (dry_run), or smoke-test (smoke_test).")
14500
+ .requiredOption("--mode <mode>", "Execution mode. smoke-test/dry-run: 0 credits, no paid provider calls or external writes; internal reads still execute. live: may spend/write and requires approval + cap.")
14397
14501
  .option("--idempotency-key <key>", "Optional idempotency key.")
14398
14502
  .option("--max-credits <n>", "Required credit ceiling for live calls.")
14399
14503
  .option("--approved", "Required for live calls after inspecting a dry run.")
14400
14504
  .option("--revision <n>", "Run a specific saved version instead of the live one. Dry-run and smoke-test only: publish a version to run it live.")
14401
14505
  .option("--include-bundle", "Include durable recipe bundles in JSON output.")
14402
14506
  .option("--json", "Print a JSON envelope.")
14507
+ .addHelpText("after", "\nSafety: smoke_test and dry_run share one boundary: 0 credits, no paid provider calls, no external writes. Oxygen internal reads use current workspace data, and oxygen.http_json_request may make a real outbound GET. Every mode creates an inspectable Workflow run record.\n")
14403
14508
  .action(async (workflowArg, options) => {
14404
14509
  const maxCredits = readPositiveNumber(options.maxCredits);
14405
14510
  const revisionVersion = readPositiveNumber(options.revision);
@@ -14420,11 +14525,35 @@ Run completion:
14420
14525
  }))
14421
14526
  .addCommand(new Command("webhooks")
14422
14527
  .description("Workflow webhook trigger utilities.")
14528
+ .addCommand(new Command("rotate")
14529
+ .description("Replace a workflow webhook's shared secret. Previews by default; the old sender secret stops working immediately after approval.")
14530
+ .argument("<workflow>", "Webhook workflow id, slug, or name.")
14531
+ .option("--approved", "Confirm replacement after reviewing the preview. The new secret is shown once.")
14532
+ .option("--json", "Print a JSON envelope.")
14533
+ .action((workflow, options) => handleAsyncAction("workflows webhooks rotate", options, () => requestOxygen("/api/cli/workflows/webhooks/rotate", {
14534
+ method: "POST",
14535
+ body: {
14536
+ workflow,
14537
+ ...(options.approved ? { approved: true } : {}),
14538
+ },
14539
+ }))))
14540
+ .addCommand(new Command("replay")
14541
+ .description("Create a new dry run from one authenticated delivery's exact original revision and payload. Never resends the webhook; uses 0 credits, no paid provider calls, and no external writes. Internal reads and outbound HTTP GETs can still execute.")
14542
+ .argument("<delivery_id>", "Delivery UUID from `oxygen workflows webhooks deliveries`.")
14543
+ .option("--request-key <key>", "Stable key for safely retrying the same replay request.")
14544
+ .option("--json", "Print a JSON envelope.")
14545
+ .action((deliveryId, options) => handleAsyncAction("workflows webhooks replay", options, () => requestOxygen("/api/cli/workflows/webhooks/deliveries/replay", {
14546
+ method: "POST",
14547
+ body: {
14548
+ delivery_id: deliveryId,
14549
+ request_key: readOption(options.requestKey) ?? randomUUID(),
14550
+ },
14551
+ }))))
14423
14552
  .addCommand(new Command("deliveries")
14424
14553
  .description("List workflow webhook deliveries: what arrived, whether it verified, and the run it started.")
14425
14554
  .argument("[trigger_id]", "Optional webhook trigger name, such as lead-created.")
14426
14555
  .option("--workflow-id <id>", "Filter to deliveries that started a run of this workflow.")
14427
- .option("--outcome <outcome>", "Filter by rejected (bad signature), blocked (verified, no run), or ran.")
14556
+ .option("--outcome <outcome>", "Filter by rejected (sender authentication failed), blocked (authenticated, no run), or ran.")
14428
14557
  .option("--limit <n>", "Maximum deliveries to return. Defaults to 50.")
14429
14558
  .option("--json", "Print a JSON envelope.")
14430
14559
  .action((triggerId, options) => handleAsyncAction("workflows webhooks deliveries", options, () => requestOxygen(workflowWebhookDeliveriesPath({
@@ -14478,7 +14607,7 @@ Run completion:
14478
14607
  .addCommand(new Command("events")
14479
14608
  .description("Workflow event trigger utilities.")
14480
14609
  .addCommand(new Command("list")
14481
- .description("Search and filter every app-native and connected event this workspace can use as a workflow trigger.")
14610
+ .description("Search and filter every registered app-native and connected event this workspace can use as a workflow trigger, including setup-required events.")
14482
14611
  .option("--search <text>", "Match labels, catalog ids, descriptions, endpoints, filters, or payload fields.")
14483
14612
  .option("--source <source>", "Match one event source exactly, such as sequencer or crm.")
14484
14613
  .option("--kind <kind>", "Match builtin, integration, or existing events.")
@@ -14499,6 +14628,11 @@ Kinds:
14499
14628
  existing Raw source/event retained from an existing workflow trigger;
14500
14629
  no preset filter is required.
14501
14630
 
14631
+ Readiness:
14632
+ Events that need setup remain selectable and draft-saveable. Their readiness
14633
+ reason and exact setup action are returned here; publish/enable stays blocked
14634
+ until the prerequisite is ready.
14635
+
14502
14636
  Example trigger:
14503
14637
  {"type":"event","source":"crm","event":"activity_created",
14504
14638
  "filters":[{"path":"activity_type","op":"eq","value":"meeting_booked"}]}
@@ -14622,8 +14756,8 @@ Full trigger schema: oxygen workflows schema --subject trigger --json
14622
14756
  });
14623
14757
  }))
14624
14758
  .addCommand(new Command("run")
14625
- .description("Get one workflow run, including waiting and awaiting_approval pause state.")
14626
- .argument("<run_id>", "Workflow run UUID.")
14759
+ .description("Read one workflow run, including waiting and awaiting_approval pause state. This command changes nothing.")
14760
+ .argument("<run_id>", "Workflow run UUID returned as data.run.id by workflows call/tail; not the separate provenance run id.")
14627
14761
  .option("--include-bundle", "Include durable recipe bundles in JSON output.")
14628
14762
  .option("--json", "Print a JSON envelope.")
14629
14763
  .action(async (runId, options) => {
@@ -15532,7 +15666,7 @@ function waitForWorkflowRun(runId, options) {
15532
15666
  parked: true,
15533
15667
  ...(resumeAt ? { resumes_at: resumeAt } : {}),
15534
15668
  message: resumeAt
15535
- ? `Run is parked until ${resumeAt} (a provider quota or a retry backoff). It resumes automatically — no action needed.`
15669
+ ? `Run is parked until ${resumeAt} and resumes automatically — no action needed.`
15536
15670
  : "Run is parked and resumes automatically — no action needed.",
15537
15671
  }
15538
15672
  : {}),
@@ -21001,6 +21135,30 @@ function ansi(enabled) {
21001
21135
  function collectRepeatable(value, previous) {
21002
21136
  return [...previous, value];
21003
21137
  }
21138
+ function readNamedCredentials(entries) {
21139
+ if (!entries || entries.length === 0)
21140
+ return undefined;
21141
+ const credentials = {};
21142
+ for (const entry of entries) {
21143
+ const separator = entry.indexOf("=");
21144
+ if (separator <= 0) {
21145
+ throw new Error("--credential must use name=value (for example, --credential subdomain=us).");
21146
+ }
21147
+ const name = entry.slice(0, separator).trim();
21148
+ const value = entry.slice(separator + 1).trim();
21149
+ if (!/^[A-Za-z][A-Za-z0-9_.-]{0,99}$/.test(name)) {
21150
+ throw new Error("--credential has an invalid field name.");
21151
+ }
21152
+ if (!value) {
21153
+ throw new Error(`--credential ${name}= requires a non-empty value.`);
21154
+ }
21155
+ if (Object.hasOwn(credentials, name)) {
21156
+ throw new Error(`--credential field '${name}' was provided more than once.`);
21157
+ }
21158
+ credentials[name] = value;
21159
+ }
21160
+ return credentials;
21161
+ }
21004
21162
  // Shared query string for `posts comments` / `posts reactions` (post social_id +
21005
21163
  // optional comment scope, pagination, sort, and account selection).
21006
21164
  function buildPostEngagementQuery(options) {
@@ -21127,13 +21285,6 @@ function withSupportEventsQuery(path, options) {
21127
21285
  const query = params.toString();
21128
21286
  return query ? `${path}?${query}` : path;
21129
21287
  }
21130
- function withSupportAdminGetQuery(path, options) {
21131
- const ifVersion = readOption(options.ifVersion);
21132
- if (!ifVersion)
21133
- return path;
21134
- const params = new URLSearchParams({ if_version: ifVersion });
21135
- return `${path}?${params.toString()}`;
21136
- }
21137
21288
  // Workflow text flags (--verify-notes/--plan/--draft) bypass readOption: an
21138
21289
  // explicitly-passed empty string clears the field server-side, while an absent
21139
21290
  // flag leaves it untouched, so "" must survive to the request body.
@@ -21182,6 +21333,26 @@ function buildSupportAdminUpdateBody(options) {
21182
21333
  }
21183
21334
  return body;
21184
21335
  }
21336
+ function supportAdminOpenResult(data) {
21337
+ if (!isRecord(data) || !isRecord(data.ticket)) {
21338
+ throw new OxygenError("invalid_response", "The support admin response did not include a Plain Thread.", { exitCode: 1 });
21339
+ }
21340
+ const plainUrl = data.ticket.plain_url;
21341
+ if (typeof plainUrl !== "string" || !plainUrl.startsWith("https://app.plain.com/")) {
21342
+ throw new OxygenError("invalid_response", "The support admin response did not include a valid Plain Inbox URL.", { exitCode: 1 });
21343
+ }
21344
+ return {
21345
+ thread_id: data.ticket.id ?? null,
21346
+ thread_ref: data.ticket.ref ?? null,
21347
+ plain_url: plainUrl,
21348
+ };
21349
+ }
21350
+ async function handleSupportAdminUpdateRequest(action, ticketId, options, fields) {
21351
+ await handleAsyncAction(`support admin ${action}`, options, () => requestOxygen(`/api/cli/admin/support/tickets/${encodeURIComponent(ticketId)}/update`, {
21352
+ method: "POST",
21353
+ body: { action, ...fields },
21354
+ }));
21355
+ }
21185
21356
  async function handleSupportAdminWorkflowAction(ticketId, options) {
21186
21357
  try {
21187
21358
  const body = buildSupportAdminWorkflowBody(options);