@oxygen-agent/cli 1.662.0 → 1.677.10

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.662.0
37
+ Version: 1.677.10
package/dist/index.js CHANGED
@@ -6311,7 +6311,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
6311
6311
  }));
6312
6312
  program
6313
6313
  .command("tags")
6314
- .description(`Workspace tags: one vocabulary across ${TAG_KINDS_PROSE}. Tags normalize to lowercase (trimmed, deduped, max 50 per item; any characters — nothing is slugified, so "Q3 Outbound" becomes "q3 outbound"). ATTACH tags on the owning surface — each of those groups has its own \`tag\` subcommand (e.g. \`oxygen inbox tag\`, \`oxygen tables tag\`, \`oxygen domains tag\`) — then \`tags get <tag>\` returns every carrier with deep-links. CURATE the vocabulary here: \`tags create\` declares a tag (with a description, color, and pin) before anything carries it, \`tags update\` re-annotates one, and \`tags rename\`/\`merge\`/\`delete\` reshape it everywhere at once.`)
6314
+ .description(`Workspace tags: one vocabulary across ${TAG_KINDS_PROSE}. Tags normalize to lowercase (trimmed, deduped, max 50 per item; any characters — nothing is slugified, so "Q3 Outbound" becomes "q3 outbound"). ATTACH tags on the owning surface — each of those groups has its own \`tag\` subcommand (e.g. \`oxygen inbox tag\`, \`oxygen tables tag\`, \`oxygen domains tag\`) — then \`tags get <tag>\` returns every carrier with deep-links. CURATE the vocabulary here: \`tags create\` declares a tag (with a description, color, and pin) before anything carries it, \`tags update\` re-annotates one, and \`tags rename\`/\`merge\`/\`delete\` reshape it everywhere at once. Tags are free, inert metadata: attaching or removing one costs 0 credits, makes no provider call, and changes no sending, routing, or warm-up behaviour.`)
6315
6315
  .addCommand(new Command("list")
6316
6316
  .description("List every workspace tag with per-primitive counts, most-used first.")
6317
6317
  .option("--json", "Print a JSON envelope.")
@@ -8541,6 +8541,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
8541
8541
  // appearing in a command's flag strings, so an approval-shaped name
8542
8542
  // here would mislabel every command that carries it.
8543
8543
  .option("--move-existing", "Also re-home the mailboxes you already have onto the new dedicated IP. Off by default because moving an established mailbox throws away the sending warmup it accumulated on its current IP.")
8544
+ .option("--country <code>", "ISO 3166-1 alpha-2 country the dedicated IP should sit in (e.g. US, DE). Match it to where your mailboxes' owners plausibly sign in from — the IP's job is making the sign-in look ordinary, and an account that suddenly authenticates from another country is what gets it challenged. Omitted uses the default region. An unserviceable or out-of-stock country fails before anything is ordered or charged.")
8544
8545
  .option("--json", "Print a JSON envelope.")
8545
8546
  .action(async (options) => {
8546
8547
  await handleAsyncAction("egress dedicated request", options, () => requestOxygen("/api/cli/egress/dedicated", {
@@ -8548,6 +8549,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
8548
8549
  body: {
8549
8550
  ...(options.approved ? { approve: true } : {}),
8550
8551
  ...(options.moveExisting ? { move_existing: true } : {}),
8552
+ ...(readOption(options.country) ? { country: readOption(options.country) } : {}),
8551
8553
  },
8552
8554
  }));
8553
8555
  }))
@@ -10075,9 +10077,10 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
10075
10077
  });
10076
10078
  }))
10077
10079
  .addCommand(new Command("destinations")
10078
- .description("List where a connected integration can send — for Microsoft Teams, the teams you belong to, and one team's channels with --team-id. Free: these are read-only lookups, so no approval or credit cap is needed.")
10080
+ .description("List where a connected integration can send — for Microsoft Teams, your teams and your chats, plus one team's channels with --team-id. Free: these are read-only lookups, so no approval or credit cap is needed.")
10079
10081
  .argument("<integration_id>", "Integration id. Supported: 'microsoft_teams'.")
10080
- .option("--team-id <id>", "List this team's channels as well as your teams.")
10082
+ .option("--team-id <id>", "List this team's channels as well.")
10083
+ .option("--kind <kind>", "Narrow to one of: teams, channels, chats. Default lists teams and chats.")
10081
10084
  .option("--connection-id <id>", "Use a specific connection when several are active.")
10082
10085
  .option("--json", "Print a JSON envelope.")
10083
10086
  .action(async (integrationId, options) => {
@@ -10086,6 +10089,9 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
10086
10089
  const teamId = readOption(options.teamId);
10087
10090
  if (teamId)
10088
10091
  params.set("team_id", teamId);
10092
+ const kind = readOption(options.kind);
10093
+ if (kind)
10094
+ params.set("kind", kind);
10089
10095
  const connectionId = readOption(options.connectionId);
10090
10096
  if (connectionId)
10091
10097
  params.set("connection_id", connectionId);
@@ -10129,6 +10135,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
10129
10135
  .description("List connected LinkedIn sender accounts with health status, rate limits, and today's usage.")
10130
10136
  .option("--status <status>", "Filter by sender status: active, paused, disconnected, restricted, or credentials_required.")
10131
10137
  .option("--no-usage", "Skip today's per-account usage counts for a faster, lighter response.")
10138
+ .option("--tag <tags>", "Comma-separated workspace tags — matches sender accounts carrying ANY of these tags (see `oxygen tags list`).")
10132
10139
  .option("--json", "Print a JSON envelope.")
10133
10140
  .action(async (options) => {
10134
10141
  await handleAsyncAction("senders list", options, () => {
@@ -10138,6 +10145,9 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
10138
10145
  const status = readOption(options.status);
10139
10146
  if (status)
10140
10147
  params.set("status", status);
10148
+ const tags = splitCommaList(options.tag);
10149
+ if (tags.length > 0)
10150
+ params.set("tag", tags.join(","));
10141
10151
  const suffix = params.toString();
10142
10152
  return requestOxygen(`/api/cli/senders${suffix ? `?${suffix}` : ""}`);
10143
10153
  });
@@ -10313,6 +10323,9 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
10313
10323
  .option("--total-reads-per-day <n>", "Daily cap across all read units.")
10314
10324
  .option("--min-spacing-seconds <n>", "Minimum seconds between actions.")
10315
10325
  .option("--spacing-jitter-seconds <n>", "Random jitter seconds added to action spacing.")
10326
+ .option("--interactive-min-spacing-seconds <n>", "Minimum seconds between human-sent Unibox actions.")
10327
+ .option("--interactive-spacing-jitter-seconds <n>", "Random jitter seconds added to human-sent Unibox action spacing.")
10328
+ .option("--interactive-sends-per-hour <n>", "Hourly cap on human-sent Unibox actions.")
10316
10329
  .option("--timezone <tz>", "IANA timezone the daily action counters reset in, e.g. America/New_York.")
10317
10330
  .option("--warmup-restart", "Start (or restart) the warm-up ramp now — gradually raises this account's invite + message caps to full over ~2 weeks.")
10318
10331
  .option("--warmup-disable", "Turn off warm-up for this account (treat it as already warm and use its full configured caps).")
@@ -10335,12 +10348,31 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
10335
10348
  .addCommand(new Command("list")
10336
10349
  .description("List sender profiles with their per-channel account counts (LinkedIn / WhatsApp / inboxes).")
10337
10350
  .option("--status <status>", "Filter by status: active, paused, or archived.")
10351
+ .option("--tag <tags>", "Comma-separated workspace tags — matches sender profiles carrying ANY of these tags (see `oxygen tags list`).")
10338
10352
  .option("--json", "Print a JSON envelope.")
10339
10353
  .action(async (options) => {
10340
10354
  await handleAsyncAction("senders profiles list", options, () => {
10355
+ const params = new URLSearchParams();
10341
10356
  const status = readOption(options.status);
10342
- return requestOxygen(`/api/cli/senders/profiles${status ? `?status=${encodeURIComponent(status)}` : ""}`);
10357
+ if (status)
10358
+ params.set("status", status);
10359
+ const tags = splitCommaList(options.tag);
10360
+ if (tags.length > 0)
10361
+ params.set("tag", tags.join(","));
10362
+ const suffix = params.toString();
10363
+ return requestOxygen(`/api/cli/senders/profiles${suffix ? `?${suffix}` : ""}`);
10343
10364
  });
10365
+ }))
10366
+ .addCommand(new Command("tag")
10367
+ .description("Replace a sender profile's workspace tags (whole set; `--tags \"\"` clears) — one edit pools the whole sending identity behind a campaign tag. Member accounts keep their own tags. See `oxygen tags list` for the vocabulary.")
10368
+ .argument("<id>", "Sender profile id.")
10369
+ .requiredOption("--tags <tags>", "Comma-separated workspace tags (replaces the whole set; empty clears).")
10370
+ .option("--json", "Print a JSON envelope.")
10371
+ .action(async (id, options) => {
10372
+ await handleAsyncAction("senders profiles tag", options, () => requestOxygen(`/api/cli/senders/profiles/${encodeURIComponent(id)}/tags`, {
10373
+ method: "POST",
10374
+ body: { tags: splitCommaList(options.tags) },
10375
+ }));
10344
10376
  }))
10345
10377
  .addCommand(new Command("get")
10346
10378
  .description("Get one sender profile with the LinkedIn/WhatsApp senders and inboxes attached to it.")
@@ -10864,6 +10896,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
10864
10896
  .description("List connected WhatsApp accounts with health status, warm-up ramp state, limits, and today's usage.")
10865
10897
  .option("--status <status>", "Filter by account status: active, paused, disconnected, restricted, or credentials_required.")
10866
10898
  .option("--no-usage", "Skip today's per-account usage counts.")
10899
+ .option("--tag <tags>", "Comma-separated workspace tags — matches WhatsApp accounts carrying ANY of these tags (see `oxygen tags list`).")
10867
10900
  .option("--json", "Print a JSON envelope.")
10868
10901
  .action(async (options) => {
10869
10902
  await handleAsyncAction("whatsapp accounts", options, () => {
@@ -10873,6 +10906,9 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
10873
10906
  const status = readOption(options.status);
10874
10907
  if (status)
10875
10908
  params.set("status", status);
10909
+ const tags = splitCommaList(options.tag);
10910
+ if (tags.length > 0)
10911
+ params.set("tag", tags.join(","));
10876
10912
  const suffix = params.toString();
10877
10913
  return requestOxygen(`/api/cli/whatsapp/accounts${suffix ? `?${suffix}` : ""}`);
10878
10914
  });
@@ -12508,12 +12544,31 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
12508
12544
  });
12509
12545
  })))
12510
12546
  .addCommand(new Command("numbers")
12511
- .description("The org's dialing pool: list | release. Buying is done from the web shop, which prices and previews the recurring order before it is placed.")
12547
+ .description("The org's dialing pool: list | tag | release. Buying is done from the web shop, which prices and previews the recurring order before it is placed.")
12512
12548
  .addCommand(new Command("list")
12513
- .description("List the org's phone numbers with warm-up state, daily cap, and what each one costs per month.")
12549
+ .description("List the org's phone numbers with warm-up state, daily cap, and what each one costs per month. Optional --tag narrows to one campaign's numbers.")
12550
+ .option("--tag <tags>", "Comma-separated workspace tags — matches phone numbers carrying ANY of these tags (see `oxygen tags list`).")
12514
12551
  .option("--json", "Print a JSON envelope.")
12515
12552
  .action(async (options) => {
12516
- await handleAsyncAction("voice numbers list", options, () => requestOxygen("/api/cli/voice/numbers"));
12553
+ await handleAsyncAction("voice numbers list", options, () => {
12554
+ const params = new URLSearchParams();
12555
+ const tags = splitCommaList(options.tag);
12556
+ if (tags.length > 0)
12557
+ params.set("tag", tags.join(","));
12558
+ const suffix = params.toString();
12559
+ return requestOxygen(`/api/cli/voice/numbers${suffix ? `?${suffix}` : ""}`);
12560
+ });
12561
+ }))
12562
+ .addCommand(new Command("tag")
12563
+ .description("Replace a phone number's workspace tags (whole set; `--tags \"\"` clears) — e.g. pool the numbers dialing one campaign behind its tag. See `oxygen tags list` for the vocabulary.")
12564
+ .argument("<number>", "Phone number in E.164 (e.g. +14155550142) or its id.")
12565
+ .requiredOption("--tags <tags>", "Comma-separated workspace tags (replaces the whole set; empty clears).")
12566
+ .option("--json", "Print a JSON envelope.")
12567
+ .action(async (number, options) => {
12568
+ await handleAsyncAction("voice numbers tag", options, () => requestOxygen(`/api/cli/voice/numbers/${encodeURIComponent(number)}/tags`, {
12569
+ method: "POST",
12570
+ body: { tags: splitCommaList(options.tags) },
12571
+ }));
12517
12572
  }))
12518
12573
  .addCommand(new Command("release")
12519
12574
  .description("Give a number back to the carrier and stop its monthly charge. IRREVERSIBLE — the number returns to the carrier's pool and can be taken by someone else within minutes; you cannot get it back. Previews by default; pass --approved to actually release.")
@@ -12984,10 +13039,11 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
12984
13039
  });
12985
13040
  })));
12986
13041
  program.addCommand(new Command("mailboxes")
12987
- .description("Native email sending pool: register/refresh Google/Microsoft mailboxes (including secure local Hypertide transfer), pause/disable inboxes, connect EmailGuard monitoring, and run managed SendKit warmup with explicit plans and credit caps. Safe import preflight: run `oxygen mailboxes compatibility --catalog-only --json`, then `oxygen mailboxes import --file <path> --validate-only --json` (add the credential source flags shown by import help when the file contains credentials).")
13042
+ .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).")
12988
13043
  .addCommand(new Command("list")
12989
13044
  .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).")
12990
13045
  .option("--status <status>", "Filter by status: active, paused, or disabled.")
13046
+ .option("--tag <tags>", "Comma-separated workspace tags — matches mailboxes carrying ANY of these tags (see `oxygen tags list`).")
12991
13047
  .option("--json", "Print a JSON envelope.")
12992
13048
  .action(async (options) => {
12993
13049
  await handleAsyncAction("mailboxes list", options, () => {
@@ -12995,6 +13051,9 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
12995
13051
  const status = readOption(options.status);
12996
13052
  if (status)
12997
13053
  params.set("status", status);
13054
+ const tags = splitCommaList(options.tag);
13055
+ if (tags.length > 0)
13056
+ params.set("tag", tags.join(","));
12998
13057
  const suffix = params.toString();
12999
13058
  return requestOxygen(`/api/cli/mailboxes${suffix ? `?${suffix}` : ""}`);
13000
13059
  });
@@ -13035,7 +13094,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
13035
13094
  });
13036
13095
  }))
13037
13096
  .addCommand(new Command("import")
13038
- .description("Register (or refresh) sending mailboxes in bulk. Use --validate-only with a local file for a non-mutating, no-network preflight; without it, import is a 0-credit state mutation whose upsert is idempotent by mailbox address. CSV/JSON/JSONL/XLSX identity files from any vendor use --file plus optional --vendor. Compatible Google app-password exports use --from credentials --vendor <source>; --from hypertide remains a shortcut. Connected Zapmail inventories use --from zapmail. In credential files, app_password is Google-only; Microsoft password fields are discarded locally. OAuth grants, MFA/TOTP seeds, and delegation keys never transfer: Microsoft warmup requires one Outlook OAuth authorization per mailbox (`mailboxes warmup microsoft`) and EmailGuard remains vendor_blocked for Microsoft. Google app passwords are sent only in the request body, encrypted server-side, and never returned.")
13097
+ .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.")
13039
13098
  .addHelpText("after", [
13040
13099
  "",
13041
13100
  "Ordinary identity file contract (CSV / JSON / JSONL / XLSX):",
@@ -13380,9 +13439,9 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
13380
13439
  });
13381
13440
  }))
13382
13441
  .addCommand(new Command("warmup")
13383
- .description("Mailbox warmup for the sending pool — the weeks of low-volume conversational mail that make a new inbox look trusted before you cold-send from it. Oxygen's native warmup runs on ONE rail: SendKit, managed and credit-billed at 3,000 credits per warming inbox per month ($3). Oxygen enrolls the mailbox for you — preview first, then re-run with --approved to execute and bill. Google and Microsoft both warm here; a Microsoft inbox first needs one Outlook authorization of its own (`mailboxes warmup microsoft`). Inboxes already warming on the retired TrulyInbox rail keep reporting and are wound down here with `warmup disable`; they are never silently moved onto SendKit.")
13442
+ .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.")
13384
13443
  .addCommand(new Command("enable")
13385
- .description("Enroll sending mailboxes in Oxygen's managed SendKit warmup and stamp each mailbox's warmup state. Targets the whole pool unless --mailboxes is given. Without --approved this is a 0-credit, no-provider-write priced preview: nothing is enrolled or billed. Preview one inbox with `oxygen mailboxes warmup enable --mailboxes sender@example.com --json`. Execute by echoing its --plan and --max-credits with --approved. Microsoft inboxes must finish `mailboxes warmup microsoft` (one Outlook authorization each) before they can enroll. New enrollments only ever land on SendKit — the retired TrulyInbox rail is refused here and only accepts teardown.")
13444
+ .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.")
13386
13445
  .option("--mailboxes <list>", "Comma-separated mailbox ids or addresses to warm. Omit to warm the whole pool.")
13387
13446
  .option("--approved", "Execute the PRICED enrollment. Without it the response is a priced preview and nothing is enrolled or billed.")
13388
13447
  .option("--plan <hash>", "Hash from the fresh preview (required with --approved).")
@@ -13426,8 +13485,8 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
13426
13485
  });
13427
13486
  }))
13428
13487
  .addCommand(new Command("microsoft")
13429
- .description("Authorize Microsoft/Outlook mailboxes for SendKit warmup — ONE browser consent PER MAILBOX, never a password or app password. There is no tenant-admin shortcut: a 40-inbox Microsoft pool means 40 separate authorizations, each opened by whoever can sign in to that mailbox. The preview names the exact mailboxes and how many consent links are still outstanding, at 0 credits. Approve that fresh hash with --max-credits 0 to mint the links, open every returned consent_url, then re-run with --status to see which mailboxes came back authorized. Warmup itself stays OFF until a separately approved `warmup enable`.")
13430
- .option("--mailboxes <list>", "Comma-separated Microsoft mailbox ids or addresses. Omit to select every Microsoft mailbox in the pool.")
13488
+ .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`.")
13489
+ .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.")
13431
13490
  .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.")
13432
13491
  .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.")
13433
13492
  .option("--plan <hash>", "Fresh setup preview hash (required with --approved).")
@@ -13678,6 +13737,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
13678
13737
  .option("--expiring", "Only non-deleted domains expiring within 60 days.")
13679
13738
  .option("--has-mailboxes <bool>", "true → only domains with sending mailboxes; false → only domains without.")
13680
13739
  .option("--include-archived", "Include archived domains (hidden by default).")
13740
+ .option("--tag <tags>", "Comma-separated workspace tags — matches domains carrying ANY of these tags (see `oxygen tags list`).")
13681
13741
  .option("--json", "Print a JSON envelope.")
13682
13742
  .action(async (options) => {
13683
13743
  await handleAsyncAction("domains list", options, () => {
@@ -13701,6 +13761,9 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
13701
13761
  params.set("has_mailboxes", hasMailboxes);
13702
13762
  if (options.includeArchived)
13703
13763
  params.set("include_archived", "true");
13764
+ const tags = splitCommaList(options.tag);
13765
+ if (tags.length > 0)
13766
+ params.set("tag", tags.join(","));
13704
13767
  const suffix = params.toString();
13705
13768
  return requestOxygen(`/api/cli/domains${suffix ? `?${suffix}` : ""}`);
13706
13769
  });
@@ -21731,6 +21794,9 @@ options) {
21731
21794
  setLimit("total_reads_per_day", options.totalReadsPerDay);
21732
21795
  setLimit("min_action_spacing_seconds", options.minSpacingSeconds);
21733
21796
  setLimit("action_spacing_jitter_seconds", options.spacingJitterSeconds);
21797
+ setLimit("interactive_min_spacing_seconds", options.interactiveMinSpacingSeconds);
21798
+ setLimit("interactive_spacing_jitter_seconds", options.interactiveSpacingJitterSeconds);
21799
+ setLimit("interactive_sends_per_hour", options.interactiveSendsPerHour);
21734
21800
  // Send windows are campaign-scoped; the only per-account time setting is the
21735
21801
  // timezone the daily counters reset in.
21736
21802
  const workingHours = {};
@@ -294,7 +294,12 @@ export const OXYGEN_CAPABILITY_ROUTES = [
294
294
  gatewayCommands: ["sequences send", "sequences create", "sequences enroll", "sequences start", "sequences list"],
295
295
  skills: ["oxygen-sequencer"],
296
296
  endpointSections: ["senders", "sequencer", "sequences", "spintax", "suppressions", "voice"],
297
- intentTerms: ["sequence", "campaign", "cadence", "enroll", "outreach", "linkedin message", "linkedin dm", "nurture", "follow up", "sender rotation", "stop on reply"],
297
+ // "sender identity", "sender profile" and "phone number" are here because a
298
+ // blind-user eval (2026-08-09) asked for "our sender identity" and got routed
299
+ // to workspace-access — "identity" matched the AUTH sense. The nouns a user
300
+ // reaches for when naming a sending identity or the dialing pool have to land
301
+ // on the group that actually owns `senders profiles` and `voice numbers`.
302
+ intentTerms: ["sequence", "campaign", "cadence", "enroll", "outreach", "linkedin message", "linkedin dm", "nurture", "follow up", "sender rotation", "stop on reply", "sender identity", "sending identity", "sender profile", "phone number", "dialing", "dialer"],
298
303
  },
299
304
  {
300
305
  id: "publishing",
@@ -4,14 +4,15 @@ import { type TagKind } from "./tags.js";
4
4
  *
5
5
  * Seeded from {@link TAG_KINDS}: anything worth labelling for a campaign is
6
6
  * worth discussing with the client who signs that campaign off. The list is
7
- * RESTATED rather than aliased because it is also a stored value — the
8
- * `ox_collab` CHECK constraint and existing rows pin it — so widening the tag
9
- * vocabulary must be a deliberate decision here, not a silent side effect that
10
- * lets the API accept a kind the database rejects. The alignment is enforced
7
+ * RESTATED rather than aliased because it is also a stored value — existing
8
+ * `ox_collab` rows pin it — so widening the tag vocabulary must be a deliberate
9
+ * decision here, not a silent side effect. (`subject_kind` deliberately carries
10
+ * no enum CHECK; 0182_collab.sql explains why, so adding a kind here needs no
11
+ * tenant migration.) The alignment is enforced
11
12
  * both ways: {@link EveryTagKindIsCommentable} fails the build if a taggable
12
13
  * kind is missing, and the paired test pins today's list exactly.
13
14
  */
14
- export declare const COLLAB_SUBJECT_KINDS: readonly ["knowledge_page", "publishing_post", "sequence", "table", "workflow", "recipe", "conversation", "mailbox", "sender", "record", "domain", "project"];
15
+ export declare const COLLAB_SUBJECT_KINDS: readonly ["knowledge_page", "publishing_post", "sequence", "table", "workflow", "recipe", "conversation", "mailbox", "sender", "record", "domain", "project", "sender_profile", "voice_number"];
15
16
  export type CollabSubjectKind = (typeof COLLAB_SUBJECT_KINDS)[number];
16
17
  /** Compile-time assertion helper: instantiating with `false` is a type error. */
17
18
  type AssertTrue<T extends true> = T;
@@ -22,10 +22,11 @@ import { OxygenError } from "./cli-result.js";
22
22
  *
23
23
  * Seeded from {@link TAG_KINDS}: anything worth labelling for a campaign is
24
24
  * worth discussing with the client who signs that campaign off. The list is
25
- * RESTATED rather than aliased because it is also a stored value — the
26
- * `ox_collab` CHECK constraint and existing rows pin it — so widening the tag
27
- * vocabulary must be a deliberate decision here, not a silent side effect that
28
- * lets the API accept a kind the database rejects. The alignment is enforced
25
+ * RESTATED rather than aliased because it is also a stored value — existing
26
+ * `ox_collab` rows pin it — so widening the tag vocabulary must be a deliberate
27
+ * decision here, not a silent side effect. (`subject_kind` deliberately carries
28
+ * no enum CHECK; 0182_collab.sql explains why, so adding a kind here needs no
29
+ * tenant migration.) The alignment is enforced
29
30
  * both ways: {@link EveryTagKindIsCommentable} fails the build if a taggable
30
31
  * kind is missing, and the paired test pins today's list exactly.
31
32
  */
@@ -42,6 +43,8 @@ export const COLLAB_SUBJECT_KINDS = [
42
43
  "record",
43
44
  "domain",
44
45
  "project",
46
+ "sender_profile",
47
+ "voice_number",
45
48
  ];
46
49
  export function isCollabSubjectKind(value) {
47
50
  return typeof value === "string" && COLLAB_SUBJECT_KINDS.includes(value);
@@ -69,6 +72,8 @@ export const COLLAB_SUBJECT_LABELS = {
69
72
  record: { one: "CRM record", many: "CRM records" },
70
73
  domain: { one: "sending domain", many: "sending domains" },
71
74
  project: { one: "project", many: "projects" },
75
+ sender_profile: { one: "sender profile", many: "sender profiles" },
76
+ voice_number: { one: "phone number", many: "phone numbers" },
72
77
  };
73
78
  /**
74
79
  * What a thread on each kind is usually about, for `--help` and MCP tool
@@ -93,6 +98,8 @@ export const COLLAB_SUBJECT_PROSE = {
93
98
  record: "a CRM record's truth",
94
99
  domain: "a sending domain's DNS and warmup",
95
100
  project: "a project's scope",
101
+ sender_profile: "a sending identity's accounts and inboxes",
102
+ voice_number: "a dialing number's caps and warmup",
96
103
  };
97
104
  /** Oxford-comma list: ["a"] -> "a"; ["a","b"] -> "a and b"; ["a","b","c"] -> "a, b, and c". */
98
105
  function formatProseList(items) {
@@ -5,9 +5,18 @@ export type CreditGuidance = {
5
5
  available_credits: number | null;
6
6
  credit_posture: CreditPosture;
7
7
  credit_guidance: string;
8
+ /** Present only when the caller supplied an expected (average) figure. */
9
+ expected_credits?: number;
8
10
  };
9
11
  export declare function buildCreditGuidance(input: {
10
12
  estimatedCredits: number | null | undefined;
11
13
  availableCredits: number | null | undefined;
12
14
  headroomMultiplier?: number;
15
+ /** The average a run actually costs. When given, the ceiling is sized from this. */
16
+ expectedCredits?: number | null | undefined;
17
+ /** Dearest single lane a row can reach — the tail the band covers. */
18
+ worstCaseCreditsPerRow?: number | null | undefined;
19
+ rowCount?: number | null | undefined;
20
+ /** True when estimatedCredits is a genuine per-row bound (bill-on-hit lanes). */
21
+ worstCaseIsHardBound?: boolean;
13
22
  }): CreditGuidance;
@@ -1,10 +1,66 @@
1
1
  const DEFAULT_HEADROOM_MULTIPLIER = 1.25;
2
+ /**
3
+ * Size a ceiling from the EXPECTED cost plus a variance band, rather than from
4
+ * the worst case.
5
+ *
6
+ * A waterfall lane bills only on a hit and a row stops at the first lane that
7
+ * resolves it, so the worst case is the dearest lane on EVERY row — a tail event,
8
+ * not a price. Quoting it made a 4.8k job read as 31.6k and told workspaces with
9
+ * ample balance they were "tight".
10
+ *
11
+ * The band is `expected + worst_row x tailRows`, where `tailRows` scales like
12
+ * sqrt(n): unlucky rows are a sample of the tail, and their count grows with the
13
+ * square root of the run, not linearly. `+1` guarantees a run can always afford at
14
+ * least one row above the mean, and `min(n, ...)` collapses the whole thing to the
15
+ * true worst case on tiny runs, where it is cheap to just cover everything.
16
+ *
17
+ * `expected x 1.25` remains the floor, so a flat/cheap chain with no meaningful
18
+ * tail keeps exactly the old headroom.
19
+ */
20
+ function expectedCreditCeiling(input) {
21
+ const floor = input.expectedCredits * input.headroomMultiplier;
22
+ let ceiling = floor;
23
+ if (input.worstCaseCreditsPerRow !== null && input.rowCount !== null && input.rowCount > 0) {
24
+ const tailRows = Math.min(input.rowCount, Math.ceil(Math.sqrt(input.rowCount)) + 1);
25
+ ceiling = Math.max(floor, input.expectedCredits + input.worstCaseCreditsPerRow * tailRows);
26
+ }
27
+ // Only clamp where the worst case is a REAL per-row bound — bill-on-hit lanes,
28
+ // where one lane is the most a row can cost. An AI/tool column's "estimate" is a
29
+ // token guess that real usage can exceed, so clamping there would reintroduce
30
+ // mid-run truncation on exactly the columns that have no ceiling of their own.
31
+ //
32
+ // Clamp to the worst case WITH its headroom, not to the bare figure: the row
33
+ // count is itself an estimate for a filtered selection, and the multiplier is
34
+ // what absorbs an undercount. Clamping to the bare total would make a small run
35
+ // strictly tighter than before this change, which is the opposite of the point.
36
+ if (input.worstCaseIsHardBound && input.worstCaseTotal !== null) {
37
+ ceiling = Math.min(ceiling, input.worstCaseTotal * input.headroomMultiplier);
38
+ }
39
+ return Math.max(1, Math.ceil(ceiling));
40
+ }
2
41
  export function buildCreditGuidance(input) {
3
42
  const estimatedCredits = normalizeCreditValue(input.estimatedCredits);
4
43
  const availableCredits = normalizeCreditValue(input.availableCredits);
5
- const recommendedMaxCredits = recommendedCreditCeiling(estimatedCredits, input.headroomMultiplier ?? DEFAULT_HEADROOM_MULTIPLIER);
44
+ const expectedCredits = normalizeCreditValue(input.expectedCredits);
45
+ const headroomMultiplier = normalizeMultiplier(input.headroomMultiplier);
46
+ // No expected figure supplied => byte-identical to the pre-existing behaviour,
47
+ // so publishing / sequences / AI callers are untouched by this change.
48
+ const recommendedMaxCredits = expectedCredits === null
49
+ ? recommendedCreditCeiling(estimatedCredits, headroomMultiplier)
50
+ : expectedCreditCeiling({
51
+ expectedCredits,
52
+ worstCaseCreditsPerRow: normalizeCreditValue(input.worstCaseCreditsPerRow),
53
+ worstCaseTotal: estimatedCredits,
54
+ rowCount: typeof input.rowCount === "number" && Number.isFinite(input.rowCount)
55
+ ? Math.max(0, Math.floor(input.rowCount))
56
+ : null,
57
+ headroomMultiplier,
58
+ worstCaseIsHardBound: input.worstCaseIsHardBound === true,
59
+ });
60
+ const expectedFields = expectedCredits === null ? {} : { expected_credits: expectedCredits };
6
61
  if (estimatedCredits === null) {
7
62
  return {
63
+ ...expectedFields,
8
64
  estimated_credits: null,
9
65
  recommended_max_credits: null,
10
66
  available_credits: availableCredits,
@@ -14,6 +70,7 @@ export function buildCreditGuidance(input) {
14
70
  }
15
71
  if (availableCredits === null) {
16
72
  return {
73
+ ...expectedFields,
17
74
  estimated_credits: estimatedCredits,
18
75
  recommended_max_credits: recommendedMaxCredits,
19
76
  available_credits: null,
@@ -22,7 +79,30 @@ export function buildCreditGuidance(input) {
22
79
  };
23
80
  }
24
81
  if (recommendedMaxCredits !== null && recommendedMaxCredits <= availableCredits) {
82
+ // When an expected figure drove the ceiling, the copy has to describe THAT,
83
+ // not "the estimate plus 25% headroom" — the old sentence would misstate how
84
+ // the number was derived. The tail warning is added only when a full
85
+ // fall-through really could outrun the balance; saying it otherwise invents
86
+ // a risk that does not exist.
87
+ if (expectedCredits !== null) {
88
+ const tailWarning = estimatedCredits > availableCredits
89
+ ? " Only a run where nearly every row falls through to the dearest provider could exceed your"
90
+ + " balance — the run keeps going and stops when the balance runs out, keeping every row it"
91
+ + " has already completed."
92
+ : "";
93
+ return {
94
+ ...expectedFields,
95
+ estimated_credits: estimatedCredits,
96
+ recommended_max_credits: recommendedMaxCredits,
97
+ available_credits: availableCredits,
98
+ credit_posture: "sufficient",
99
+ credit_guidance: `Credits cover the expected cost (≈${formatCreditsForCopy(expectedCredits)} credits) with `
100
+ + "headroom for the rows that fall through to a pricier provider."
101
+ + `${tailWarning} Unused credits are not spent.`,
102
+ };
103
+ }
25
104
  return {
105
+ ...expectedFields,
26
106
  estimated_credits: estimatedCredits,
27
107
  recommended_max_credits: recommendedMaxCredits,
28
108
  available_credits: availableCredits,
@@ -33,6 +113,7 @@ export function buildCreditGuidance(input) {
33
113
  };
34
114
  }
35
115
  return {
116
+ ...expectedFields,
36
117
  estimated_credits: estimatedCredits,
37
118
  recommended_max_credits: availableCredits > 0 ? roundCreditValue(availableCredits) : null,
38
119
  available_credits: availableCredits,
@@ -51,6 +132,14 @@ function recommendedCreditCeiling(estimatedCredits, headroomMultiplier = DEFAULT
51
132
  : DEFAULT_HEADROOM_MULTIPLIER;
52
133
  return Math.max(1, Math.ceil(normalized * multiplier));
53
134
  }
135
+ function normalizeMultiplier(value) {
136
+ return Number.isFinite(value) && (value ?? 0) > 0
137
+ ? value
138
+ : DEFAULT_HEADROOM_MULTIPLIER;
139
+ }
140
+ function formatCreditsForCopy(value) {
141
+ return Math.round(value).toLocaleString("en-US");
142
+ }
54
143
  function normalizeCreditValue(value) {
55
144
  if (typeof value !== "number" || !Number.isFinite(value) || value < 0)
56
145
  return null;
@@ -1,6 +1,6 @@
1
1
  import type { TagColor } from "./select-options.js";
2
2
  /** Primitive kinds that participate in the workspace tag union today. */
3
- export declare const TAG_KINDS: readonly ["knowledge_page", "publishing_post", "sequence", "table", "workflow", "recipe", "conversation", "mailbox", "sender", "record", "domain", "project"];
3
+ export declare const TAG_KINDS: readonly ["knowledge_page", "publishing_post", "sequence", "table", "workflow", "recipe", "conversation", "mailbox", "sender", "record", "domain", "project", "sender_profile", "voice_number"];
4
4
  export type TagKind = (typeof TAG_KINDS)[number];
5
5
  /**
6
6
  * The taggable kinds in prose, for CLI `--help` and MCP tool descriptions.
@@ -24,6 +24,8 @@ export const TAG_KINDS = [
24
24
  "record",
25
25
  "domain",
26
26
  "project",
27
+ "sender_profile",
28
+ "voice_number",
27
29
  ];
28
30
  /**
29
31
  * The taggable kinds in prose, for CLI `--help` and MCP tool descriptions.
@@ -37,7 +39,7 @@ export const TAG_KINDS = [
37
39
  */
38
40
  export const TAG_KINDS_PROSE = "publishing posts, knowledge pages, sequences, tables, workflows, recipes, " +
39
41
  "inbox conversations, CRM records, mailboxes, sender accounts, sending domains, " +
40
- "and projects";
42
+ "projects, sender profiles, and phone numbers";
41
43
  /**
42
44
  * The phrase each kind must contribute to {@link TAG_KINDS_PROSE}. Typed as a
43
45
  * total map over TagKind, so adding a kind is a compile error until it is named
@@ -56,6 +58,8 @@ export const TAG_KIND_PROSE_MARKERS = {
56
58
  record: "CRM record",
57
59
  domain: "sending domain",
58
60
  project: "project",
61
+ sender_profile: "sender profile",
62
+ voice_number: "phone number",
59
63
  };
60
64
  export function isTagKind(value) {
61
65
  return typeof value === "string" && TAG_KINDS.includes(value);
@@ -85,6 +89,8 @@ export const TAG_KIND_LABELS = {
85
89
  record: { one: "CRM record", many: "CRM records" },
86
90
  domain: { one: "domain", many: "domains" },
87
91
  project: { one: "project", many: "projects" },
92
+ sender_profile: { one: "sender profile", many: "sender profiles" },
93
+ voice_number: { one: "phone number", many: "phone numbers" },
88
94
  };
89
95
  /** Inline label for a count: `tagKindLabel("sequence", 1)` → "sequence". */
90
96
  export function tagKindLabel(kind, count) {
@@ -1,3 +1,3 @@
1
- export declare const OXYGEN_VERSION = "1.662.0";
1
+ export declare const OXYGEN_VERSION = "1.677.10";
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.662.0";
1
+ export const OXYGEN_VERSION = "1.677.10";
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.662.0",
3
+ "version": "1.677.10",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",