@oxygen-agent/cli 1.681.3 → 1.686.3

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.681.3
37
+ Version: 1.686.3
package/dist/index.js CHANGED
@@ -891,8 +891,9 @@ function readJsonFileValue(path, inputName) {
891
891
  /**
892
892
  * Whitelist mailbox import fields before the local file crosses the network.
893
893
  * In particular, a credential cannot be silently sent through the ordinary
894
- * inline path: the caller must choose --from credentials (or the Hypertide
895
- * shortcut) so the server applies the encrypted transfer-vault contract.
894
+ * inline path: the caller must choose --from credentials so the server applies
895
+ * the encrypted transfer-vault contract. Historical aliases remain accepted by
896
+ * the parser for backward compatibility but are not advertised.
896
897
  */
897
898
  function normalizeMailboxImportFile(value, mode) {
898
899
  try {
@@ -902,7 +903,7 @@ function normalizeMailboxImportFile(value, mode) {
902
903
  if (error instanceof OxygenError &&
903
904
  error.message ===
904
905
  "This identity-only import contains a password. Choose a secure credential import for a compatible Google app-password export.") {
905
- throw new OxygenError("invalid_request", "Mailbox credentials require --from credentials --vendor <source> (or --from hypertide for a Hypertide export); keep --validate-only for a no-network preflight. An identity import never accepts or forwards passwords.", { exitCode: 1 });
906
+ throw new OxygenError("invalid_request", "Mailbox credentials require --from credentials --vendor <source>; keep --validate-only for a no-network preflight. An identity import never accepts or forwards passwords.", { exitCode: 1 });
906
907
  }
907
908
  throw error;
908
909
  }
@@ -922,7 +923,7 @@ async function readMailboxImportFile(path) {
922
923
  buffer = readFileSync(path);
923
924
  }
924
925
  catch {
925
- 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 (or --from hypertide). See https://oxygen-agent.com/docs/providers/mailbox-compatibility.`, { exitCode: 1 });
926
+ 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 });
926
927
  }
927
928
  if (buffer.byteLength > SHARED_MAILBOX_IMPORT_FILE_MAX_BYTES) {
928
929
  throw new OxygenError("invalid_request", `Mailbox import files must be ${SHARED_MAILBOX_IMPORT_FILE_MAX_BYTES / 1024 / 1024} MB or smaller.`, { exitCode: 1 });
@@ -1177,6 +1178,7 @@ function buildCrmRollupBody(options) {
1177
1178
  ...(object ? { object } : {}),
1178
1179
  ...(row ? { row } : {}),
1179
1180
  ...(limit ? { limit: Number.parseInt(limit, 10) } : {}),
1181
+ ...(options.includeLifecycle ? { include_lifecycle: true } : {}),
1180
1182
  };
1181
1183
  }
1182
1184
  function readCrmSetupObjects(value) {
@@ -2594,6 +2596,17 @@ export function createProgram() {
2594
2596
  : {}),
2595
2597
  },
2596
2598
  }));
2599
+ }))
2600
+ .addCommand(new Command("member-role")
2601
+ .description("Move a member between the admin and member workspace roles. ADMIN ONLY. This is the one Clerk membership change Oxygen makes — inviting people is still done in workspace settings. Use it to demote an admin before `approvals client grant`: a workspace admin's role overrides the restricted client role, so the grant is refused until they are a plain member.")
2602
+ .argument("<email>", "Workspace member's email.")
2603
+ .requiredOption("--role <role>", "admin or member.")
2604
+ .option("--json", "Print a JSON envelope.")
2605
+ .action(async (email, options) => {
2606
+ await handleCollabAction("orgs member-role", options, () => requestOxygen("/api/cli/collab/members/workspace-role", {
2607
+ method: "POST",
2608
+ body: { email, role: readOption(options.role) },
2609
+ }), formatCollabWriteResult);
2597
2610
  }));
2598
2611
  program
2599
2612
  .command("support")
@@ -3753,6 +3766,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
3753
3766
  .option("--limit <n>", "Maximum company rows to scan in one sweep. Defaults to 2000.")
3754
3767
  .option("--dry-run", "Report how many companies disagree with their deals, without writing. A deal with no stage set is not an open deal and is excluded from the comparison, so a blank-stage deal is never reported as drift.")
3755
3768
  .option("--live", "Write the recomputed Open Deal cells. Default is dry-run.")
3769
+ .option("--include-lifecycle", "Also recompute Account Stage from each company's people and deals. Account Stage only moves when a person or deal changes, so a workspace whose records were imported before that projection existed reads Prospect for everything. Free, and dry-run previews it like the rest.")
3756
3770
  .option("--json", "Print a JSON envelope.")
3757
3771
  .action(async (options) => {
3758
3772
  await handleAsyncAction("crm rollup", options, () => requestOxygen("/api/cli/crm/rollup", {
@@ -4192,11 +4206,24 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
4192
4206
  .addCommand(new Command("enrichment")
4193
4207
  .description("Choose which named enrichments run automatically on new CRM records. The saved set is a STANDING PERMISSION: every record written to the object (imports, inserts, upserts, and `crm assert`) enriches without per-record approval, bounded by the per-batch credit ceiling. Same storage as `oxygen tables auto-run`, in founder words.")
4194
4208
  .addCommand(new Command("list")
4195
- .description("Show the enrichments available per CRM object: which are armed, what each costs per record, the per-record total, the per-batch credit ceiling, and how much of the column allowance is spent. Read-only, free.")
4209
+ .description("Show the enrichments available per CRM object: which are armed, what each costs per record, the per-record total, the per-batch credit ceiling, how much of the column allowance is spent, and how many records already in the object have never been enriched. Read-only, free.")
4196
4210
  .option("--object <object>", "CRM object slug: companies or people. Omit for every object this workspace has.")
4197
4211
  .option("--json", "Print a JSON envelope.")
4198
4212
  .action(async (options) => {
4199
4213
  await handleCrmEnrichmentAction("crm enrichment list", options, () => requestOxygen(buildCrmEnrichmentListPath(options)));
4214
+ }))
4215
+ .addCommand(new Command("backfill")
4216
+ .description("Enrich the records you ALREADY have. Arming an enrichment only covers records written from that moment on, so the companies and people already in your CRM stay blank until you run this. Previews for free by default: what is still missing, what one batch costs, and what finishing costs. Add --live --approved --max-credits to actually run a batch; repeat it and the next batch picks up where the last one stopped.")
4217
+ .requiredOption("--object <object>", "CRM object slug: companies or people.")
4218
+ .option("--presets <csv>", "Comma-separated enrichment ids to backfill. Defaults to whatever is armed on the object.")
4219
+ .option("--limit <n>", "How many records this batch enriches. Defaults to 50, max 5000.")
4220
+ .option("--all", "Enrich every record still missing these enrichments, up to 5000 in one batch. Costs the whole `to finish` figure the preview quotes.")
4221
+ .option("--live", "Actually run the batch. Without it this is a free preview that spends nothing.")
4222
+ .option("--approved", "Approve THIS batch after reading the preview. A live paid batch fails with approval_required (exit 7) without it.")
4223
+ .option("--max-credits <n>", "Credit ceiling for this batch. Records beyond it are skipped with credit_limit_reached — the preview says how many that would be. Required for a live paid batch; unused credits are not spent.")
4224
+ .option("--json", "Print a JSON envelope.")
4225
+ .action(async (options) => {
4226
+ await handleCrmEnrichmentBackfill(options);
4200
4227
  }))
4201
4228
  .addCommand(new Command("enable")
4202
4229
  .description("Arm one enrichment on a CRM object and leave every other choice untouched. Takes effect immediately as a standing permission: new records enrich without per-record approval, capped per batch by the object's credit ceiling.")
@@ -6219,8 +6246,8 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
6219
6246
  .addCommand(new Command("client")
6220
6247
  .description("The restricted login an agency hands its client: read the work, comment on it, decide the approvals assigned to them, nothing else. ADMIN ONLY. Invite them to the workspace first — this grants the overlay on an existing membership.")
6221
6248
  .addCommand(new Command("grant")
6222
- .description("Restrict a member to the client role.")
6223
- .argument("<email>", "Workspace member's email.")
6249
+ .description("Restrict a member to the client role. An address that is INVITED but has not joined works too: the role is recorded against the invitation and applies the moment they accept, so you do not have to watch for the acceptance and come back.")
6250
+ .argument("<email>", "Workspace member's email, or a pending invitation's email.")
6224
6251
  .option("--json", "Print a JSON envelope.")
6225
6252
  .action(async (email, options) => {
6226
6253
  await handleCollabAction("approvals client", options, () => requestOxygen("/api/cli/collab/members/role", {
@@ -6229,8 +6256,8 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
6229
6256
  }), formatCollabWriteResult);
6230
6257
  }))
6231
6258
  .addCommand(new Command("revoke")
6232
- .description("Clear the client role and restore full workspace access.")
6233
- .argument("<email>", "Workspace member's email.")
6259
+ .description("Clear the client role and restore full workspace access. Works on a pending invitation too, clearing what that address would land as.")
6260
+ .argument("<email>", "Workspace member's email, or a pending invitation's email.")
6234
6261
  .option("--json", "Print a JSON envelope.")
6235
6262
  .action(async (email, options) => {
6236
6263
  await handleCollabAction("approvals client", options, () => requestOxygen("/api/cli/collab/members/role", {
@@ -6239,14 +6266,14 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
6239
6266
  }), formatCollabWriteResult);
6240
6267
  }))
6241
6268
  .addCommand(new Command("list")
6242
- .description("Every member of this workspace and what they may do — the workspace role plus the Oxygen client overlay. Run it after a grant to see the change: this is the same read the /collab/members page renders.")
6269
+ .description("Every member of this workspace and what they may do — the workspace role plus the Oxygen client overlay — followed by anyone invited who has not joined yet, and what they will land as. Also names, per member, why a client grant on them would be refused. Same read the /collab/members page renders.")
6243
6270
  .option("--json", "Print a JSON envelope.")
6244
6271
  .action(async (options) => {
6245
6272
  await handleCollabAction("approvals client list", options, () => requestOxygen("/api/cli/collab/members"), formatCollabMembers);
6246
6273
  })));
6247
6274
  program
6248
6275
  .command("blueprints")
6249
- .description("Scaffolding bundles: a workflow + tables + columns + prompts as shareable JSON. For guided GTM plays see `oxygen recipes`.")
6276
+ .description("Scaffolding bundles: a workflow + tables + columns + prompts as shareable JSON. For workflow-specific operating steps, start with `oxygen recipes list <goal> --json`.")
6250
6277
  .addCommand(new Command("list")
6251
6278
  .description("List Oxygen blueprints visible in this workspace (seeds + saved).")
6252
6279
  .argument("[query]", "Search text.")
@@ -6334,12 +6361,13 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
6334
6361
  });
6335
6362
  }))
6336
6363
  .addCommand(new Command("preflight")
6337
- .description("Preflight a blueprint (slug, local file, or shared URL) against this workspace. Price-aware seeds return a current runtime descriptor upper bound, cap adequacy, and an exact zero-credit apply command.")
6364
+ .description("Preflight a blueprint (slug, local file, or shared URL) against this workspace. Price-aware seeds return a current runtime descriptor upper bound, live/scheduled cap scope and remediation, a zero-spend contract, and an exact apply command.")
6338
6365
  .argument("[slug]", "Blueprint slug (for stored or seed blueprints).")
6339
6366
  .option("--file <path>", "Read a blueprint envelope from a local JSON file.")
6340
6367
  .option("--from-url <url>", "Fetch a shared blueprint envelope from a public Oxygen share URL.")
6341
6368
  .option("--input-json <json>", "Seed blueprint input (parameters) as JSON.")
6342
6369
  .option("--table-ref <ref=id...>", "Reuse an existing table for a blueprint ref (repeatable).", collectMultiple, [])
6370
+ .option("--workflow-id <id>", "Reapply to an existing non-default workflow by database UUID or manifest id/slug; preflight.next preserves the exact selector you supplied, including a database UUID. The shipped LinkedIn monitor reuses its active tables and apply publishes a new disabled revision. An unmatched value previews a separate workflow.")
6343
6371
  .option("--json", "Print a JSON envelope.")
6344
6372
  .action(async (slug, options) => {
6345
6373
  await handleAsyncAction("blueprints preflight", options, async () => {
@@ -6348,24 +6376,18 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
6348
6376
  });
6349
6377
  }))
6350
6378
  .addCommand(new Command("apply")
6351
- .description("Apply a blueprint: 0 credits and no provider calls or external writes, but creates workspace tables, columns, prompts, and a disabled workflow. It does not enable or run the workflow.")
6379
+ .description("Apply a blueprint: 0 credits and no provider calls or external writes, but creates workspace tables, columns, prompts, and a disabled workflow. Reapplying the shipped LinkedIn monitor reuses its active installed tables. It does not enable or run the workflow.")
6352
6380
  .argument("[slug]", "Blueprint slug (for stored or seed blueprints).")
6353
6381
  .option("--file <path>", "Read a blueprint envelope from a local JSON file.")
6354
6382
  .option("--from-url <url>", "Fetch a shared blueprint envelope from a public Oxygen share URL.")
6355
6383
  .option("--input-json <json>", "Seed blueprint input (parameters) as JSON.")
6356
6384
  .option("--table-ref <ref=id...>", "Reuse an existing table for a blueprint ref (repeatable).", collectMultiple, [])
6357
- .option("--workflow-id <id>", "Set the new workflow's manifest id/slug; this does not update or reuse an existing workflow.")
6385
+ .option("--workflow-id <id>", "Target an existing workflow by database UUID or manifest id/slug. The shipped LinkedIn monitor reuses its active tables and publishes a new disabled revision; an unmatched value creates a separate workflow.")
6358
6386
  .option("--workflow-name <name>", "Override the resulting workflow name.")
6359
6387
  .option("--json", "Print a JSON envelope.")
6360
6388
  .action(async (slug, options) => {
6361
6389
  await handleAsyncAction("blueprints apply", options, async () => {
6362
6390
  const body = await buildBlueprintRequestBody(slug, options);
6363
- const workflowId = readOption(options.workflowId);
6364
- if (workflowId)
6365
- body.workflow_id = workflowId;
6366
- const workflowName = readOption(options.workflowName);
6367
- if (workflowName)
6368
- body.workflow_name = workflowName;
6369
6391
  return requestOxygen("/api/cli/blueprints/apply", { method: "POST", body });
6370
6392
  });
6371
6393
  }))
@@ -8049,7 +8071,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
8049
8071
  .addCommand(new Command("topup")
8050
8072
  .description("Buy a custom amount of on-demand credits at $1.25 per 1,000. Run without an amount to inspect the allowed range; checkout happens in Stripe and purchased credits never expire.")
8051
8073
  .argument("[pack]", "Legacy pack alias in USD: 10, 25, 100, or 250.")
8052
- .option("--credits <n>", "Credits to buy (8,000-200,000 in 1,000-credit increments).")
8074
+ .option("--credits <n>", "Credits to buy (8,000-1,000,000 in 1,000-credit increments).")
8053
8075
  .option("--json", "Print a JSON envelope.")
8054
8076
  .action(async (pack, options) => {
8055
8077
  const credits = readOption(options.credits);
@@ -9504,7 +9526,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
9504
9526
  }));
9505
9527
  program
9506
9528
  .command("integrations")
9507
- .description("Integration connections and raw provider-webhook subscriptions. Trigger-ready workspace events live under `workflows events list`. Hypertide mailbox exports use `mailboxes import --from hypertide`, not this group.")
9529
+ .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.")
9508
9530
  .addCommand(new Command("events")
9509
9531
  .description("Inspect raw provider event definitions and manage subscriptions that feed workflows; use `workflows events list` for the trigger-ready workspace catalog.")
9510
9532
  .addCommand(new Command("list")
@@ -12436,7 +12458,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
12436
12458
  });
12437
12459
  })));
12438
12460
  program.addCommand(new Command("managed-inboxes")
12439
- .description("Whitelabel sending inboxes bought through OXYGEN: subscribe a domain + N mailboxes (google/microsoft/azure) as a recurring MONTHLY subscription billed in USD to your Oxygen Email Infrastructure subscription, add-inboxes to a domain you already own, list/get your subscriptions, verify that Oxygen/Stripe/the vendor agree, and cancel. Subscribe/add-inboxes/cancel are approval-gated (preview → re-run with --approved --quote). The vendor is chosen for you; --vendor pins one.")
12461
+ .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. Warmup defaults on when available: its quoted recurring credits and exact future addresses are part of the order approval, and the worker activates them in SendKit after provisioning without a second warmup approval. Subscribe/add-inboxes/cancel are approval-gated (preview → re-run with --approved --quote). The vendor is chosen for you; --vendor pins one.")
12440
12462
  .addCommand(new Command("verify")
12441
12463
  .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.")
12442
12464
  .option("--json", "Print a JSON envelope.")
@@ -12450,7 +12472,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
12450
12472
  await handleAsyncAction("managed-inboxes registrant", options, () => requestOxygen("/api/cli/managed-inboxes/registrant"));
12451
12473
  }))
12452
12474
  .addCommand(new Command("subscribe")
12453
- .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. Prices come from the vendor's LIVE rate card and LIVE domain price, and are refused if they sit below vendor cost. Fails closed until the vendor key + founder-signed per-platform pricing are configured. 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.")
12475
+ .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. Warmup 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`. InboxKit warmup 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 SendKit 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.")
12454
12476
  .argument("[domain]", "Sending domain to register + host the mailboxes (e.g. send.acme.com). May also be passed as --domain.")
12455
12477
  .option("--domain <domain>", "Sending domain (alternative to the positional argument).")
12456
12478
  .requiredOption("--provider <provider>", "Mailbox PLATFORM: google, microsoft, or azure. (google/microsoft cap at 5 mailboxes per domain; azure allows 100.)")
@@ -12460,7 +12482,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
12460
12482
  .option("--years <n>", "Years to register the domain for (1-10). Defaults to 1.")
12461
12483
  .option("--redirect-url <url>", "Where the domain's web root redirects. Defaults to your workspace's company website; editable later with `oxygen domains forwarding set`.")
12462
12484
  .option("--billing <path>", "Path to a JSON file with the registrant/WHOIS contact (first_name, last_name, phone, country, city, state, address_line_one, postal_code). Required on your FIRST order; later orders reuse the registrant already on file.")
12463
- .option("--no-warmup", "Order WITHOUT managed email warm-up. Warm-up is included by default — a new domain needs 14-28 days of warm-up before any cold send, so opting out means bringing your own warm-up provider.")
12485
+ .option("--no-warmup", "Order WITHOUT automatic managed SendKit warm-up. Warm-up is included by default and starts after provisioning under this order approval for the exact quoted Google/Microsoft/Azure addresses; opting out means using BYO warm-up or a later standalone warmup approval.")
12464
12486
  .option("--no-placement", "Order WITHOUT inbox placement. Placement is included by default and covers inbox-placement (spam) testing, blacklist/reputation monitoring, and authentication checks on every inbox.")
12465
12487
  .option("--approved", "Place the order (requires --quote). Without it, a priced preview is returned.")
12466
12488
  .option("--quote <id>", "The quote_id from a fresh preview. Required with --approved.")
@@ -12517,7 +12539,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
12517
12539
  });
12518
12540
  }))
12519
12541
  .addCommand(new Command("add-inboxes")
12520
- .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. 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.")
12542
+ .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 warmup 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`. InboxKit warmup 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 SendKit 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.")
12521
12543
  .argument("[domain]", "A managed domain this workspace already owns (e.g. send.acme.com). May also be passed as --domain.")
12522
12544
  .option("--domain <domain>", "The managed inbox domain (alternative to the positional argument).")
12523
12545
  .option("--mailboxes <json>", "JSON array of mailboxes: [{\"username\",\"first_name\",\"last_name\"}]. The vendor stamps the names on each mailbox, so real names belong here.")
@@ -12635,7 +12657,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
12635
12657
  });
12636
12658
  })));
12637
12659
  program.addCommand(new Command("mailboxes")
12638
- .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. Managed InboxKit Google and Microsoft mailboxes use InboxKit's native Sequencer export during the approved warmup action; SendKit warms them but 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).")
12660
+ .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 managed SendKit warmup. Fresh managed InboxKit Google/Microsoft/Azure orders activate exact native Sequencer exports automatically after provisioning under their approved default-on add-on. Standalone warmup plans and credit caps are for BYOK/imported mailboxes, managed opt-outs, or later separate enrollment. SendKit warms them but 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).")
12639
12661
  .addCommand(new Command("list")
12640
12662
  .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).")
12641
12663
  .option("--status <status>", "Filter by status: active, paused, or disabled.")
@@ -12668,9 +12690,9 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
12668
12690
  await handleAsyncAction("mailboxes health", options, () => requestOxygen("/api/cli/mailboxes/health"));
12669
12691
  }))
12670
12692
  .addCommand(new Command("compatibility")
12671
- .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.")
12693
+ .description("Read-only compatibility report for every selected mailbox: generic origin class, 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 current public 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.")
12672
12694
  .option("--mailboxes <list>", "Comma-separated mailbox ids or addresses. Omit for the whole pool.")
12673
- .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.")
12695
+ .option("--catalog-only", "Return only the bounded import-method, field, auth-boundary, and public provider catalogs; do not read or return workspace mailbox rows.")
12674
12696
  .option("--json", "Print a JSON envelope.")
12675
12697
  .action(async (options) => {
12676
12698
  await handleAsyncAction("mailboxes compatibility", options, () => {
@@ -12690,14 +12712,14 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
12690
12712
  });
12691
12713
  }))
12692
12714
  .addCommand(new Command("import")
12693
- .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: managed InboxKit Google/Microsoft warmup uses native InboxKit Sequencer export during `warmup enable`; eligible non-InboxKit Microsoft warmup uses the separate 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.")
12715
+ .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.")
12694
12716
  .addHelpText("after", [
12695
12717
  "",
12696
12718
  "Ordinary identity file contract (CSV / JSON / JSONL / XLSX):",
12697
12719
  " Canonical fields: email_address, provider, workspace_external_id?, infrastructure_platform?, tenant_id?. Common vendor aliases such as Email, From Email, ESP, Mailbox ID, and Entra Tenant ID are mapped locally. Credentials are rejected.",
12698
12720
  "",
12699
12721
  "All local file imports:",
12700
- " Limits: 500 rows / 5 MB for identity, credential, and Hypertide files.",
12722
+ " Limits: 500 rows / 5 MB for identity and credential files.",
12701
12723
  "",
12702
12724
  "Credential file contract:",
12703
12725
  ' JSON: {"mailboxes":[{"email_address":"ada@send-acme.com","provider":"google","app_password":"<Google mailbox app password>"}]}',
@@ -12708,7 +12730,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
12708
12730
  "",
12709
12731
  ].join("\n"))
12710
12732
  .option("--file <path>", "Local CSV/JSON/JSONL/XLSX file. Common mailbox-vendor headers are normalized locally; only canonical allowlisted fields cross the network.")
12711
- .option("--from <source>", "Import source: 'credentials' for a compatible Google app-password export, 'hypertide' as its provider shortcut, or 'zapmail' to pull a connected workspace.")
12733
+ .option("--from <source>", "Import source: 'credentials' for a compatible Google app-password export, or 'zapmail' to pull a connected workspace.")
12712
12734
  .option("--vendor <slug>", "Non-secret source provenance (for example instantly, mailforge, or smartlead). Required with --from credentials; optional for identity files.")
12713
12735
  .option("--connection <id>", "Zapmail connection id (--from zapmail). Defaults to the org's active Zapmail connection.")
12714
12736
  .option("--provider <provider>", "Zapmail pool to pull (--from zapmail): google or microsoft. Zapmail's mailbox list is provider-scoped, so the Microsoft pool is only reachable with --provider microsoft; Microsoft mailboxes get their Entra tenant id stamped on import.")
@@ -12726,7 +12748,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
12726
12748
  from !== "zapmail" &&
12727
12749
  from !== "hypertide" &&
12728
12750
  from !== "credentials") {
12729
- throw new Error("--from must be credentials, hypertide, or zapmail.");
12751
+ throw new Error("--from must be credentials or zapmail.");
12730
12752
  }
12731
12753
  if (from === "zapmail") {
12732
12754
  if (validateOnly) {
@@ -12754,7 +12776,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
12754
12776
  }
12755
12777
  const sourceProvider = normalizeMailboxImportVendor(vendor, from);
12756
12778
  if (!filePath)
12757
- throw new Error("Provide --file <path>, --from credentials --vendor <source> --file <path>, --from hypertide --file <path>, or --from zapmail.");
12779
+ throw new Error("Provide --file <path>, --from credentials --vendor <source> --file <path>, or --from zapmail.");
12758
12780
  const mailboxes = normalizeMailboxImportFile(await readMailboxImportFile(resolve(filePath)), from === "hypertide" || from === "credentials"
12759
12781
  ? "credential"
12760
12782
  : "identity");
@@ -12901,7 +12923,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
12901
12923
  });
12902
12924
  }))
12903
12925
  .addCommand(new Command("connect-oauth")
12904
- .description("Connect Google/Microsoft mailboxes to OXYGEN native send with fresh destination-bound OAuth. Preview by default; pass --approved only after reviewing exact candidates. --vendor oxygen handles imported, Hypertide, external, or manual mailboxes: it requires an exact list (max 10), returns one OXYGEN browser link per candidate, and stores a grant only after that exact mailbox completes provider consent/MFA. Source tokens, passwords, authenticator seeds, and one-time codes never transfer. --vendor zapmail (default) hands a provisioned pool to Zapmail Custom OAuth; --vendor inboxkit requests domain-gated consent with one canary on unproven domains and a 10-write live cap. All paths cost 0 Oxygen credits. Poll --status <id>; connected means an encrypted refresh token actually landed, never merely that a request was accepted.")
12926
+ .description("Connect Google/Microsoft mailboxes to OXYGEN native send with fresh destination-bound OAuth. Preview by default; pass --approved only after reviewing exact candidates. --vendor oxygen handles imported, external, or manual mailboxes: it requires an exact address list (max 10 per command; no domain or directory discovery), returns one OXYGEN browser link per candidate, and stores a grant only after that exact mailbox completes provider consent/MFA. Source tokens, passwords, authenticator seeds, and one-time codes never transfer. --vendor zapmail (default) hands a provisioned pool to Zapmail Custom OAuth; --vendor inboxkit requests domain-gated consent with one canary on unproven domains and a 10-write live cap. All paths cost 0 Oxygen credits. Poll --status <id>; connected means an encrypted refresh token actually landed, never merely that a request was accepted.")
12905
12927
  .option("--provider <provider>", "Mailbox provider to provision: google or microsoft.")
12906
12928
  .option("--vendor <vendor>", "Authorization path: oxygen for imported/manual mailboxes, zapmail (default), or inboxkit.")
12907
12929
  .option("--mailboxes <list>", "Comma-separated mailbox addresses. Required for vendor=oxygen (max 10); omit for vendor-provisioned whole-pool flows.")
@@ -12960,7 +12982,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
12960
12982
  await handleAsyncAction("mailboxes oauth-health", options, () => requestOxygen("/api/cli/mailboxes/oauth-health"));
12961
12983
  }))
12962
12984
  .addCommand(new Command("emailguard")
12963
- .description("Assess every mailbox origin for EmailGuard and connect credential-capable Google Workspace mailboxes from InboxKit, CMR, Hypertide, or Zapmail. Manual/native Google rows report credential_required; Microsoft 365/Azure reports vendor_blocked. Preview never materializes a password; approved execution fetches it just in time and never prints it.")
12985
+ .description("Assess every mailbox origin for EmailGuard and connect credential-capable Google Workspace mailboxes from managed, Zapmail, or compatible encrypted external imports. Manual/native Google rows report credential_required; Microsoft 365/Azure reports vendor_blocked. Preview never materializes a password; approved execution fetches it just in time and never prints it.")
12964
12986
  .addCommand(new Command("connect")
12965
12987
  .description("Preview or connect selected mailboxes to EmailGuard. Managed mode is 1,000 credits/inbox-month ($1); BYOK is 0 Oxygen credits. Google Workspace requires a real app password, fetched only after approval. Microsoft 365/Azure is explicitly vendor-blocked because EmailGuard exposes neither Microsoft OAuth nor tenant consent; Oxygen never downgrades it to password auth. Starts with a 0-credit, no-provider-write preview. Account connection sends no placement probe.")
12966
12988
  .option("--mailboxes <list>", "Comma-separated mailbox ids or addresses. Omit for the whole pool.")
@@ -13018,7 +13040,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
13018
13040
  });
13019
13041
  })))
13020
13042
  .addCommand(new Command("delegation")
13021
- .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.")
13043
+ .description("Google sending-domain delegation only — not Microsoft OAuth and not a credential-file 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.")
13022
13044
  .option("--domain <domain>", "Only report this sending domain.")
13023
13045
  .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.")
13024
13046
  .option("--json", "Print a JSON envelope.")
@@ -13035,9 +13057,9 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
13035
13057
  });
13036
13058
  }))
13037
13059
  .addCommand(new Command("warmup")
13038
- .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. Managed InboxKit Google and Microsoft mailboxes are exported through InboxKit's native Sequencer integration inside that approved action; InboxKit auto-export stays off, and any non-cancelled InboxKit warmup blocks the handoff so one mailbox cannot warm twice. SendKit is warmup-only: 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 here with `warmup disable`; they are never silently moved onto SendKit.")
13060
+ .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). Fresh managed InboxKit Google/Microsoft/Azure orders with the default-on warmup 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 warmup blocks the handoff so one mailbox cannot warm twice. SendKit is warmup-only: 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 here with `warmup disable`; they are never silently moved onto SendKit.")
13039
13061
  .addCommand(new Command("enable")
13040
- .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; 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. Managed InboxKit Google and Microsoft mailboxes use InboxKit's native Sequencer export here by default; 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`. SendKit supplies warmup only — OXYGEN Sequences retain campaign enrollment and dispatch. New enrollments only ever land on SendKit; the retired TrulyInbox rail is refused here and only accepts teardown.")
13062
+ .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`. SendKit supplies warmup only — OXYGEN Sequences retain campaign enrollment and dispatch. New enrollments only ever land on SendKit; the retired TrulyInbox rail is refused here and only accepts teardown.")
13041
13063
  .option("--mailboxes <list>", "Comma-separated mailbox ids or addresses to warm. Omit to warm the whole pool.")
13042
13064
  .option("--approved", "Execute the PRICED enrollment. Without it the response is a priced preview and nothing is enrolled or billed.")
13043
13065
  .option("--plan <hash>", "Hash from the fresh preview (required with --approved).")
@@ -13081,7 +13103,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
13081
13103
  });
13082
13104
  }))
13083
13105
  .addCommand(new Command("microsoft")
13084
- .description("NON-INBOXKIT FALLBACK ONLY: authorize eligible Microsoft/Outlook mailboxes for SendKit warmup with ONE browser consent PER MAILBOX, never a password or app password. Do not run this for managed InboxKit mailboxes: both Google and Microsoft use InboxKit's native Sequencer export during the separately approved `warmup enable`. For the 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 `warmup enable`.")
13106
+ .description("NON-INBOXKIT FALLBACK ONLY: authorize eligible Microsoft/Outlook mailboxes for SendKit warmup 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 Sequencer 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`.")
13085
13107
  .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.")
13086
13108
  .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.")
13087
13109
  .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.")
@@ -13764,7 +13786,7 @@ Run completion:
13764
13786
  .description("Deprecated alias for `oxygen blueprints apply`. Creates a disabled workflow (plus its tables, columns, and prompts) from a blueprint.")
13765
13787
  .argument("<template_id>", "Blueprint slug (formerly workflow template id).")
13766
13788
  .requiredOption("--input-json <json>", "Blueprint input as a JSON object.")
13767
- .option("--workflow-id <workflow_id>", "Set the new workflow's manifest id/slug; this does not update or reuse an existing workflow.")
13789
+ .option("--workflow-id <workflow_id>", "Select an existing workflow by database UUID or manifest id/slug; an unmatched value creates a separate workflow.")
13768
13790
  .option("--workflow-name <workflow_name>", "Override the resulting workflow name.")
13769
13791
  .option("--mode <mode>", "Deprecated and ignored: the workflow is created disabled.")
13770
13792
  .option("--max-credits <credits>", "Credit ceiling; folded into inputs.max_credits.")
@@ -19563,8 +19585,25 @@ function collabWriteHeadline(data) {
19563
19585
  if (isRecord(data.request)) {
19564
19586
  return `Approval request ${collabText(data.request.id)} is ${collabText(data.request.status)}.`;
19565
19587
  }
19588
+ // `orgs member-role` — the Clerk workspace role, not the Oxygen overlay. It
19589
+ // must be checked BEFORE the overlay branch below: both write payloads are
19590
+ // flat and both carry `email`, so falling through would report a promotion as
19591
+ // "back to full workspace access".
19592
+ if (typeof data.email === "string" && typeof data.workspace_role_choice === "string") {
19593
+ return data.workspace_role_choice === "admin"
19594
+ ? `${data.email} is now a workspace admin.`
19595
+ : `${data.email} is now a workspace member.`;
19596
+ }
19566
19597
  // The member-role write returns the membership fields flat, not nested.
19567
19598
  if (typeof data.email === "string") {
19599
+ // Granting on a PENDING INVITATION changes nobody's access yet. Saying "is
19600
+ // now a client-role member" would tell an agency the client is restricted
19601
+ // while that client has not accepted and holds no membership at all.
19602
+ if (data.pending_invitation === true) {
19603
+ return data.granted === true
19604
+ ? `${data.email} has not joined yet — they will land restricted to the client role.`
19605
+ : `${data.email} has not joined yet — they will land with full workspace access.`;
19606
+ }
19568
19607
  return data.granted === true
19569
19608
  ? `${data.email} is now a client-role member of this workspace.`
19570
19609
  : `${data.email} is back to full workspace access.`;
@@ -19732,6 +19771,31 @@ function formatCollabMembers(data) {
19732
19771
  member.oxygen_role === "client" ? "client" : "—",
19733
19772
  collabText(member.access),
19734
19773
  ])));
19774
+ // Why a grant on a given row would be refused. The payload has carried this
19775
+ // since the web page needed it; printing it is what stops the terminal from
19776
+ // being the surface that only finds out by failing — on an all-admin
19777
+ // workspace EVERY grant is refused, and the table alone never says so.
19778
+ const blocked = members.filter((member) => member.can_restrict === false);
19779
+ if (blocked.length > 0) {
19780
+ lines.push("", ` ${styles.dim("Cannot be restricted to the client role:")}`);
19781
+ for (const member of blocked) {
19782
+ lines.push(` ${styles.yellow("!")} ${collabText(member.restrict_blocked_reason)}`);
19783
+ }
19784
+ }
19785
+ }
19786
+ // Invited but not joined. They hold nothing yet, so they are listed apart from
19787
+ // the members rather than mixed in with a blank role that reads as access.
19788
+ const invitations = Array.isArray(data.pending_invitations)
19789
+ ? data.pending_invitations.filter(isRecord)
19790
+ : [];
19791
+ if (invitations.length > 0) {
19792
+ lines.push("", styles.bold(`Invited, not joined yet (${invitations.length})`));
19793
+ lines.push(...renderTextTable(["EMAIL", "INVITED AS", "ON JOIN", "INVITED"], invitations.map((invitation) => [
19794
+ collabText(invitation.email),
19795
+ collabText(invitation.workspace_role),
19796
+ invitation.oxygen_role_on_join === "client" ? "client" : "full access",
19797
+ formatCollabAge(invitation.invited_at),
19798
+ ])));
19735
19799
  }
19736
19800
  lines.push(...collabTrailer(data));
19737
19801
  lines.push("");
@@ -20031,11 +20095,35 @@ function pushCrmEnrichmentObject(lines, styles, view) {
20031
20095
  const note = view.custom_columns_uncosted ? ` ${styles.yellow("(cost not included above)")}` : "";
20032
20096
  lines.push(` ${styles.dim("Extra".padEnd(summaryWidth))} ${view.custom_columns.join(", ")}${note}`);
20033
20097
  }
20098
+ // The half the config alone cannot show: an armed set says nothing about the
20099
+ // records that were already there when it was armed. Without this line a
20100
+ // workspace reads a correct configuration and never learns that 303 of its
20101
+ // 317 companies are still blank.
20102
+ const backfillLine = formatCrmEnrichmentBacklog(view, styles);
20103
+ if (backfillLine) {
20104
+ lines.push(` ${styles.dim("Not enriched".padEnd(summaryWidth))} ${backfillLine}`);
20105
+ }
20106
+ if (view.custom_columns.length > 0) {
20107
+ // A hand-tuned config carries columns no preset owns, so the per-record
20108
+ // figure above can be a floor rather than the bill.
20109
+ const note = view.custom_columns_uncosted ? ` ${styles.yellow("(cost not included above)")}` : "";
20110
+ lines.push(` ${styles.dim("Extra".padEnd(summaryWidth))} ${view.custom_columns.join(", ")}${note}`);
20111
+ }
20034
20112
  if (view.changed) {
20035
20113
  lines.push("", ...formatCrmEnrichmentChange(view.changed, styles));
20036
20114
  }
20037
20115
  lines.push("", styles.dim(`View: ${view.deepLink}`), styles.dim(`Runs: ${view.runs_web_url}`));
20038
20116
  }
20117
+ function formatCrmEnrichmentBacklog(view, styles) {
20118
+ const backfill = view.backfill;
20119
+ if (!backfill)
20120
+ return null;
20121
+ if (backfill.remaining_rows === 0) {
20122
+ return styles.green(`none — all ${backfill.total_rows.toLocaleString("en-US")} records carry these enrichments`);
20123
+ }
20124
+ return `${styles.yellow(`${backfill.remaining_rows.toLocaleString("en-US")} of ${backfill.total_rows.toLocaleString("en-US")} records`)}`
20125
+ + ` ${styles.dim(`— fill them with \`oxygen crm enrichment backfill --object ${view.object}\``)}`;
20126
+ }
20039
20127
  function pushCrmEnrichmentPreset(lines, styles, preset, labelWidth, armed) {
20040
20128
  // Pad the raw marker before colouring it: `[off]` is one character wider than
20041
20129
  // `[on]`, and an ANSI-wrapped padEnd would count the escape codes.
@@ -20088,6 +20176,104 @@ function formatCrmEnrichmentSources(sources) {
20088
20176
  const assertCovered = sources.includes("rows_insert") || sources.includes("rows_upsert");
20089
20177
  return `${labels.join(", ")}${assertCovered ? " (includes `crm assert` writes)" : ""}`;
20090
20178
  }
20179
+ function buildCrmEnrichmentBackfillBody(options) {
20180
+ const presets = readCsvOption(options.presets);
20181
+ const limit = readPositiveInt(options.limit);
20182
+ return {
20183
+ object: readOption(options.object) ?? "",
20184
+ // An ABSENT --presets means "whatever is armed"; an explicitly empty one is
20185
+ // still a caller-supplied set, so the two must not collapse.
20186
+ ...(options.presets !== undefined ? { presets } : {}),
20187
+ ...(limit !== undefined ? { limit } : {}),
20188
+ ...(options.all === true ? { all: true } : {}),
20189
+ ...(options.live === true ? { live: true } : {}),
20190
+ ...(options.approved === true ? { approved: true } : {}),
20191
+ ...(readPositiveNumber(options.maxCredits) !== undefined
20192
+ ? { max_credits: readPositiveNumber(options.maxCredits) }
20193
+ : {}),
20194
+ };
20195
+ }
20196
+ // Backfill prints its own rendering rather than reusing the preset editor's:
20197
+ // this command's whole job is to say what is missing and what filling it costs,
20198
+ // and a JSON dump buries the one number that decides whether to run it.
20199
+ async function handleCrmEnrichmentBackfill(options) {
20200
+ const command = "crm enrichment backfill";
20201
+ try {
20202
+ const data = await requestOxygen("/api/cli/crm/enrichment/backfill", { method: "POST", body: buildCrmEnrichmentBackfillBody(options) });
20203
+ if (options.json) {
20204
+ writeJson(success(command, data));
20205
+ return;
20206
+ }
20207
+ process.stdout.write(formatCrmEnrichmentBackfill(data));
20208
+ }
20209
+ catch (error) {
20210
+ emitCliFailure(command, error);
20211
+ }
20212
+ }
20213
+ function formatCrmEnrichmentBackfill(view) {
20214
+ const styles = ansi(output.isTTY === true && !process.env.NO_COLOR);
20215
+ const lines = [
20216
+ "",
20217
+ `${styles.bold(`Backfill — ${view.object}`)} ${view.live ? styles.green("queued") : styles.dim("preview")}`,
20218
+ ` ${styles.dim("Arming an enrichment only covers records written from then on. This fills the ones already here.")}`,
20219
+ "",
20220
+ ` ${formatBackfillHeadline(view, styles)}`,
20221
+ "",
20222
+ ];
20223
+ const labelWidth = Math.max(...view.columns.map((column) => column.label.length), 0);
20224
+ for (const column of view.columns) {
20225
+ const rate = column.credits_per_row === null
20226
+ ? styles.yellow("cost unknown")
20227
+ : formatCrmEnrichmentCredits(column.credits_per_row, styles, "credits/record");
20228
+ const pending = column.billable
20229
+ ? `${column.pending_rows.toLocaleString("en-US")} missing`
20230
+ : styles.dim("derived — fills itself, free");
20231
+ lines.push(` ${column.label.padEnd(labelWidth)} ${pending.padEnd(18)} ${rate}`);
20232
+ }
20233
+ const summary = [
20234
+ ["This batch", `${view.batch.rows.toLocaleString("en-US")} records · ${formatBackfillCredits(view.batch.estimated_credits, styles)}`],
20235
+ ["To finish", `${view.to_finish.rows.toLocaleString("en-US")} records · ${formatBackfillCredits(view.to_finish.estimated_credits, styles)} · ${view.to_finish.batches_at_this_size} batch${view.to_finish.batches_at_this_size === 1 ? "" : "es"} this size`],
20236
+ ["Spendable", view.spendable_credits === null
20237
+ ? styles.dim("unknown")
20238
+ : `${view.spendable_credits.toLocaleString("en-US")} credits ${styles.dim("(balance minus fixed monthly commitments)")}`],
20239
+ ];
20240
+ if (view.batch.recommended_max_credits !== null) {
20241
+ summary.push(["Suggested cap", `${view.batch.recommended_max_credits.toLocaleString("en-US")} credits for this batch`]);
20242
+ }
20243
+ if (view.run) {
20244
+ summary.push(["Run", `${view.run.status} · ${view.run.item_count.toLocaleString("en-US")} items · ${view.run.action_run_id}`]);
20245
+ }
20246
+ if (view.remaining_after_batch !== undefined) {
20247
+ summary.push(["Left after this", `${view.remaining_after_batch.toLocaleString("en-US")} records`]);
20248
+ }
20249
+ const summaryWidth = Math.max(...summary.map(([label]) => label.length));
20250
+ lines.push("", ...summary.map(([label, value]) => ` ${styles.dim(label.padEnd(summaryWidth))} ${value}`));
20251
+ if (view.to_finish.uncosted_columns.length > 0) {
20252
+ lines.push(` ${styles.yellow(`Cost unknown for: ${view.to_finish.uncosted_columns.join(", ")} — the figures above are a floor.`)}`);
20253
+ }
20254
+ // The silent failure this command exists to make loud: a ceiling that
20255
+ // truncates the run leaves the rest terminally skipped while the run still
20256
+ // reports completed.
20257
+ if (view.skipped_credit_limit.rows > 0) {
20258
+ lines.push("", ` ${styles.yellow(view.skipped_credit_limit.message)}`);
20259
+ }
20260
+ if (view.credit_guidance && view.credit_posture !== "sufficient") {
20261
+ lines.push("", ` ${styles.yellow(view.credit_guidance)}`);
20262
+ }
20263
+ lines.push("", ` ${styles.dim("Next")} ${view.next_step}`, ...(view.run_all_step === view.next_step ? [] : [` ${styles.dim("All ")} ${view.run_all_step}`]), "", styles.dim(`View: ${view.deepLink}`), styles.dim(`Runs: ${view.run?.web_url ?? view.runs_web_url}`), "");
20264
+ return lines.join("\n");
20265
+ }
20266
+ function formatBackfillHeadline(view, styles) {
20267
+ if (view.remaining_rows === 0) {
20268
+ return styles.green(`All ${view.total_rows.toLocaleString("en-US")} records already carry these enrichments.`);
20269
+ }
20270
+ return `${styles.bold(view.remaining_rows.toLocaleString("en-US"))} of ${view.total_rows.toLocaleString("en-US")} records are still missing at least one of these enrichments.`;
20271
+ }
20272
+ function formatBackfillCredits(credits, styles) {
20273
+ if (credits === null)
20274
+ return styles.yellow("cost unknown");
20275
+ return credits === 0 ? styles.green("free") : `${credits.toLocaleString("en-US")} credits`;
20276
+ }
20091
20277
  function formatCrmEnrichmentChange(changed, styles) {
20092
20278
  if (changed.enabled_added.length === 0 && changed.enabled_removed.length === 0) {
20093
20279
  return [` ${styles.dim("No change — that set was already armed.")}`];
@@ -20623,6 +20809,12 @@ slug, options) {
20623
20809
  if (Object.keys(refs).length > 0)
20624
20810
  body.table_refs = refs;
20625
20811
  }
20812
+ const workflowId = readOption(options.workflowId);
20813
+ if (workflowId)
20814
+ body.workflow_id = workflowId;
20815
+ const workflowName = readOption(options.workflowName);
20816
+ if (workflowName)
20817
+ body.workflow_name = workflowName;
20626
20818
  if (!body.slug && !body.envelope) {
20627
20819
  throw new Error("Pass a blueprint slug, --file, or --from-url.");
20628
20820
  }
@@ -29,7 +29,7 @@ export declare const CONTACT_SALES_URL = "https://cal.com/tim-scheuer-mxbib9/45"
29
29
  export declare const CREDITS_PER_USD = 1000;
30
30
  export declare const TOPUP_USD_CENTS_PER_1K_CREDITS = 125;
31
31
  export declare const CREDIT_TOPUP_MIN_CREDITS = 8000;
32
- export declare const CREDIT_TOPUP_MAX_CREDITS = 200000;
32
+ export declare const CREDIT_TOPUP_MAX_CREDITS = 1000000;
33
33
  export declare const CREDIT_TOPUP_STEP_CREDITS = 1000;
34
34
  export declare const CREDIT_TOPUP_DEFAULT_CREDITS = 20000;
35
35
  export declare function isValidCreditTopupCredits(credits: number): boolean;
@@ -9,10 +9,12 @@ export const CONTACT_SALES_URL = "https://cal.com/tim-scheuer-mxbib9/45";
9
9
  // credits bought on demand.
10
10
  export const CREDITS_PER_USD = 1_000;
11
11
  export const TOPUP_USD_CENTS_PER_1K_CREDITS = 125;
12
- // Keep on-demand purchases inside the same $10-$250 spend envelope as the
13
- // original fixed packs, but let customers choose any 1,000-credit increment.
12
+ // On-demand purchases start at the original fixed packs' $10 floor and run up
13
+ // to a $1,250 ceiling, in any 1,000-credit increment. The ceiling is a
14
+ // single-checkout guard rail, not a spend cap: a workspace that needs more
15
+ // tops up again (purchased credits never expire).
14
16
  export const CREDIT_TOPUP_MIN_CREDITS = 8_000;
15
- export const CREDIT_TOPUP_MAX_CREDITS = 200_000;
17
+ export const CREDIT_TOPUP_MAX_CREDITS = 1_000_000;
16
18
  export const CREDIT_TOPUP_STEP_CREDITS = 1_000;
17
19
  export const CREDIT_TOPUP_DEFAULT_CREDITS = 20_000;
18
20
  export function isValidCreditTopupCredits(credits) {
@@ -208,7 +208,7 @@ export function summarizeMailboxImportValidation(mailboxes, input) {
208
208
  export function normalizeMailboxImportVendor(raw, from) {
209
209
  if (from === "hypertide") {
210
210
  if (raw && raw.trim().toLowerCase() !== "hypertide") {
211
- throw new OxygenError("invalid_request", "Hypertide import provenance is fixed to hypertide.", { exitCode: 1 });
211
+ throw new OxygenError("invalid_request", "This legacy credential-file import is fixed to its original provenance.", { exitCode: 1 });
212
212
  }
213
213
  return null;
214
214
  }
@@ -1,3 +1,3 @@
1
- export declare const OXYGEN_VERSION = "1.681.3";
1
+ export declare const OXYGEN_VERSION = "1.686.3";
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.681.3";
1
+ export const OXYGEN_VERSION = "1.686.3";
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.681.3",
3
+ "version": "1.686.3",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",