@oxygen-agent/cli 1.1014.1035 → 1.1014.1132

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +1 -1
  2. package/dist/command-manifest.js +1 -0
  3. package/dist/commands/columns.js +1 -1
  4. package/dist/commands/deliverability.js +2 -0
  5. package/dist/commands/sequences.js +30 -2
  6. package/dist/commands/viewers.js +1 -2
  7. package/dist/credentials.js +1 -1
  8. package/dist/customer-operation-commands.js +62 -3
  9. package/dist/help.js +1 -1
  10. package/dist/index.d.ts +0 -97
  11. package/dist/index.js +0 -98
  12. package/dist/workspace-placement-commands.d.ts +3 -0
  13. package/dist/workspace-placement-commands.js +42 -0
  14. package/node_modules/@oxygen/shared/dist/agent-sessions.d.ts +6 -1
  15. package/node_modules/@oxygen/shared/dist/agent-sessions.js +18 -6
  16. package/node_modules/@oxygen/shared/dist/agents-enabled.d.ts +8 -9
  17. package/node_modules/@oxygen/shared/dist/agents-enabled.js +12 -8
  18. package/node_modules/@oxygen/shared/dist/capability-discovery.js +24 -6
  19. package/node_modules/@oxygen/shared/dist/column-autofill.js +2 -2
  20. package/node_modules/@oxygen/shared/dist/contracts/agent-sessions.js +1 -1
  21. package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +1 -1
  22. package/node_modules/@oxygen/shared/dist/contracts/guided-activation.js +5 -5
  23. package/node_modules/@oxygen/shared/dist/contracts/guided-company-files.d.ts +30 -0
  24. package/node_modules/@oxygen/shared/dist/contracts/guided-company-files.js +53 -0
  25. package/node_modules/@oxygen/shared/dist/contracts/guided-first-sourcing.d.ts +120 -0
  26. package/node_modules/@oxygen/shared/dist/contracts/guided-first-sourcing.js +48 -0
  27. package/node_modules/@oxygen/shared/dist/contracts/guided-onboarding-journey.d.ts +267 -0
  28. package/node_modules/@oxygen/shared/dist/contracts/guided-onboarding-journey.js +63 -0
  29. package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +6 -0
  30. package/node_modules/@oxygen/shared/dist/contracts/operations.js +9 -4
  31. package/node_modules/@oxygen/shared/dist/contracts/table-query-raw.d.ts +2 -0
  32. package/node_modules/@oxygen/shared/dist/contracts/table-query-raw.js +3 -0
  33. package/node_modules/@oxygen/shared/dist/contracts/teammates.js +9 -8
  34. package/node_modules/@oxygen/shared/dist/data-suppliers.d.ts +32 -2
  35. package/node_modules/@oxygen/shared/dist/data-suppliers.js +44 -4
  36. package/node_modules/@oxygen/shared/dist/email-tracking-token.d.ts +2 -0
  37. package/node_modules/@oxygen/shared/dist/email-tracking-token.js +1 -0
  38. package/node_modules/@oxygen/shared/dist/feature-gates.d.ts +3 -4
  39. package/node_modules/@oxygen/shared/dist/feature-gates.js +3 -4
  40. package/node_modules/@oxygen/shared/dist/file-import.d.ts +46 -0
  41. package/node_modules/@oxygen/shared/dist/file-import.js +138 -0
  42. package/node_modules/@oxygen/shared/dist/guided-activation.d.ts +8 -0
  43. package/node_modules/@oxygen/shared/dist/guided-company-files.d.ts +13 -0
  44. package/node_modules/@oxygen/shared/dist/guided-company-files.js +14 -0
  45. package/node_modules/@oxygen/shared/dist/guided-first-sourcing.d.ts +116 -0
  46. package/node_modules/@oxygen/shared/dist/guided-first-sourcing.js +11 -0
  47. package/node_modules/@oxygen/shared/dist/guided-onboarding-answers.d.ts +4 -0
  48. package/node_modules/@oxygen/shared/dist/guided-onboarding-answers.js +42 -0
  49. package/node_modules/@oxygen/shared/dist/guided-onboarding-company-context.d.ts +22 -0
  50. package/node_modules/@oxygen/shared/dist/guided-onboarding-company-context.js +38 -0
  51. package/node_modules/@oxygen/shared/dist/guided-onboarding-journey.d.ts +147 -0
  52. package/node_modules/@oxygen/shared/dist/guided-onboarding-journey.fixture.d.ts +12 -0
  53. package/node_modules/@oxygen/shared/dist/guided-onboarding-journey.fixture.js +83 -0
  54. package/node_modules/@oxygen/shared/dist/guided-onboarding-journey.js +44 -0
  55. package/node_modules/@oxygen/shared/dist/guided-onboarding-prospects.d.ts +7 -0
  56. package/node_modules/@oxygen/shared/dist/guided-onboarding-prospects.js +13 -0
  57. package/node_modules/@oxygen/shared/dist/guided-onboarding-signals.d.ts +4 -0
  58. package/node_modules/@oxygen/shared/dist/guided-onboarding-signals.js +34 -0
  59. package/node_modules/@oxygen/shared/dist/guided-trial-billing.d.ts +26 -0
  60. package/node_modules/@oxygen/shared/dist/guided-trial-billing.js +19 -0
  61. package/node_modules/@oxygen/shared/dist/hosted-ai.js +108 -118
  62. package/node_modules/@oxygen/shared/dist/knowledge-links.d.ts +29 -0
  63. package/node_modules/@oxygen/shared/dist/knowledge-links.js +84 -1
  64. package/node_modules/@oxygen/shared/dist/linkedin-sequences.d.ts +5 -14
  65. package/node_modules/@oxygen/shared/dist/linkedin-sequences.js +22 -5
  66. package/node_modules/@oxygen/shared/dist/managed-scraper-backend.d.ts +18 -0
  67. package/node_modules/@oxygen/shared/dist/managed-scraper-backend.js +17 -0
  68. package/node_modules/@oxygen/shared/dist/teammates.d.ts +7 -5
  69. package/node_modules/@oxygen/shared/dist/teammates.js +11 -20
  70. package/node_modules/@oxygen/shared/dist/user-capability-routing.js +88 -0
  71. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  72. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  73. package/node_modules/@oxygen/shared/dist/workspace-placement-view.d.ts +150 -0
  74. package/node_modules/@oxygen/shared/dist/workspace-placement-view.js +102 -0
  75. package/node_modules/@oxygen/shared/dist/workspace-placement.d.ts +84 -0
  76. package/node_modules/@oxygen/shared/dist/workspace-placement.js +107 -0
  77. package/package.json +1 -1
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.1014.1035
37
+ Version: 1.1014.1132
@@ -262,6 +262,7 @@ const PREVIEW_BY_DEFAULT_COMMANDS = new Set([
262
262
  "domains dns apply",
263
263
  "mailboxes warmup enable",
264
264
  "mailboxes warmup google",
265
+ "deliverability workspace-test run",
265
266
  "deliverability placement-test run",
266
267
  "deliverability placement-test send",
267
268
  "managed-inboxes recover",
@@ -830,7 +830,7 @@ export function registerColumnsFamily(program, handleAsyncAction) {
830
830
  .addCommand(new Command("add")
831
831
  .description("Add a nullable column, a catalog template (--prompt-key) or a multi-column enrichment bundle (--preset) to a workspace table. Writes the definition only — this never runs the column and never spends credits; use `columns run` for that, with --dry-run first to see the cost. For a page, posting or filing the row already links to, or a company fact Oxygen already knows how to research (founders, parent company, funding, cloud provider, offers demos, industry, NAICS, HQ, a LinkedIn or Crunchbase page), pick a template from `columns catalog` (--prompt-key) instead of writing a prompt; or read a URL column with --kind research --research-url. The guide is the oxygen-tables skill (`oxygen skills get oxygen-tables --json`). For ICP fit, use Classify Score: `columns add <table> --label 'ICP fit' --score 'low,medium,high' --prompt 'Assess {{company_name}}: {{description}}, {{employees}} employees' --grounding workspace` (0.075 credits/row). A formula column needs no run at all: it evaluates on read from its current inputs, so `tables query` shows its values immediately. A plain field is --type: text, number, checkbox, date, json, markdown, image, or a choice column — select, multi_select or status, with --options \"Lead,Contacted,Won\". To link another table row to row, use `oxygen tables relate <table> --to <other-table> --live`; to read a value out of one, --lookup-table.")
832
832
  .argument("<table>", "Table id or slug.")
833
- .option("--preset <preset>", "Add a pre-built column bundle instead of one column, which is what --prompt-key adds: a preset writes SEVERAL columns at once and needs no --input on a table whose identity column it can find, while --prompt-key adds one template column and binds each input you name. Presets: `person_enrich` (one LinkedIn profile lookup — Blitz first, Professional Network Data (Up2Data) only when Blitz has no match — then first/last/full name, current company and its LinkedIn URL, job title, start date, education, school, headline, bio, location and followers as free stored fields that fill when the lookup runs, followers only on rows Professional Network Data answers because Blitz publishes none; the person templates in `columns catalog --category people` all read its payload column), `company_enrich` (one company lookup from a domain or LinkedIn URL — Blitz first, Professional Network Data (Up2Data) only when Blitz has no match, about 1 credit a company — keeping the whole profile in its JSON column plus name, domain, LinkedIn URL, headcount, industry, description, founded year and HQ country as free stored fields; no field choices; binds to the person preset's current_company_linkedin_url automatically), `company_tech_stack` (the technologies a company runs, from its website and hiring), `company_funding` (total raised, latest round, number of rounds and investors), `company_revenue` (an annual revenue estimate), `company_hiring` (the roles a company is hiring for, with a count) — each of these four is one paid lookup column that fills its own columns, priced per row in `tools search --for-table` — plus `company_lookalikes` (one managed lookup from --input company_domain=website; keeps similar companies separate from the seed; dry-run for price) — or `domain_check` (0.1 credits/row at list price for a live/redirect/parked/not_found/dead verdict on every domain, cached 30 days, plus a free label column to filter on). For a single cleaned or classified column, use a template from `columns catalog` instead — that is one column, not a bundle. Name the input column with --input: `url` for person_enrich, `domain` and/or `linkedin_url` for the company presets. Any column works whatever it is called; without --input Oxygen guesses from column names. Creating the columns is free; run them afterwards, --dry-run first.")
833
+ .option("--preset <preset>", "Add a pre-built column bundle instead of one column, which is what --prompt-key adds: a preset writes SEVERAL columns at once and needs no --input on a table whose identity column it can find, while --prompt-key adds one template column and binds each input you name. Presets: `person_enrich` (one LinkedIn profile lookup — Blitz first, Professional Network Data only when Blitz has no match — then first/last/full name, current company and its LinkedIn URL, job title, start date, education, school, headline, bio, location and followers as free stored fields that fill when the lookup runs, followers only on rows Professional Network Data answers because Blitz publishes none; the person templates in `columns catalog --category people` all read its payload column), `company_enrich` (one company lookup from a domain or LinkedIn URL — Blitz first, Professional Network Data only when Blitz has no match, about 1 credit a company — keeping the whole profile in its JSON column plus name, domain, LinkedIn URL, headcount, industry, description, founded year and HQ country as free stored fields; no field choices; binds to the person preset's current_company_linkedin_url automatically), `company_tech_stack` (the technologies a company runs, from its website and hiring), `company_funding` (total raised, latest round, number of rounds and investors), `company_revenue` (an annual revenue estimate), `company_hiring` (the roles a company is hiring for, with a count) — each of these four is one paid lookup column that fills its own columns, priced per row in `tools search --for-table` — plus `company_lookalikes` (one managed lookup from --input company_domain=website; keeps similar companies separate from the seed; dry-run for price) — or `domain_check` (0.1 credits/row at list price for a live/redirect/parked/not_found/dead verdict on every domain, cached 30 days, plus a free label column to filter on). For a single cleaned or classified column, use a template from `columns catalog` instead — that is one column, not a bundle. Name the input column with --input: `url` for person_enrich, `domain` and/or `linkedin_url` for the company presets. Any column works whatever it is called; without --input Oxygen guesses from column names. Creating the columns is free; run them afterwards, --dry-run first.")
834
834
  .option("--input <slot=column...>", "Bind a preset or template input to an exact column, e.g. --input url=pricing_url with --prompt-key, or --input domain=website --input linkedin_url=company_li with --preset. Repeatable. For a preset it is only needed when the automatic match is wrong or missing; for a template it names the column each declared input reads.", collectRepeatable, [])
835
835
  // No preset takes a field list any more (founder, 2026-09-24). Hidden,
836
836
  // not removed: an old script passing them gets the server's refusal,
@@ -1,3 +1,4 @@
1
+ import { registerWorkspacePlacementCommands } from "../workspace-placement-commands.js";
1
2
  import { requestOxygen } from "../http-client.js";
2
3
  import { readOption } from "../util.js";
3
4
  import { Command, CommanderError, Option } from "commander";
@@ -95,4 +96,5 @@ export function registerDeliverabilityFamily(program, handleAsyncAction) {
95
96
  .action(async (id, options) => {
96
97
  await handleAsyncAction("deliverability placement-test get", options, () => requestOxygen(`/api/cli/deliverability/placement-tests/${encodeURIComponent(id)}`));
97
98
  }))));
99
+ registerWorkspacePlacementCommands(program.commands.find(c => c.name() === "deliverability"), handleAsyncAction);
98
100
  }
@@ -1593,6 +1593,7 @@ export function registerSequencesFamily(program, handleAsyncAction) {
1593
1593
  .option("--senders <ids>", "Comma-separated LinkedIn or WhatsApp sender account ids (or connection / Unipile ids; `senders list` / `whatsapp accounts` list them). Optional at create; enroll and start require at least one when the journey has LinkedIn or WhatsApp steps (attach later with `sequences update --senders`).")
1594
1594
  .option("--table <id>", "Source table id or slug whose rows supply {{column}} template values.")
1595
1595
  .option("--activation-id <id>", "Guided authored LinkedIn draft: bind the creator's activation and exact First leads Table. Free; retries reuse the native draft.")
1596
+ .option("--activation-revision <n>", "Reviewed current activation revision, a positive safe integer; requires --activation-id. Native first creation checks it before mutation.")
1596
1597
  .option("--from-table <id>", "Alias of --table: the source table id or slug this sequence draws its leads and {{column}} values from.")
1597
1598
  .option("--enroll", "After creating the sequence, immediately enroll every row of the bound table (same as running `sequences enroll --from-table` next). Requires --table/--from-table. Enrolling spends no credits and sends nothing.")
1598
1599
  .option("--url-column <key>", "Column key holding each lead's LinkedIn URL/provider id.")
@@ -1649,6 +1650,11 @@ export function registerSequencesFamily(program, handleAsyncAction) {
1649
1650
  // so it names the same thing here rather than being a second
1650
1651
  // concept; `--table` stays for every existing script.
1651
1652
  const sourceTable = readOption(options.fromTable) ?? readOption(options.table);
1653
+ const activationId = readOption(options.activationId);
1654
+ const activationRevision = readPositiveInt(options.activationRevision);
1655
+ if (options.activationRevision !== undefined && (!activationId || !Number.isSafeInteger(activationRevision))) {
1656
+ throw new OxygenError("invalid_input", "--activation-revision needs --activation-id and a positive safe integer from current onboarding.", { exitCode: 1 });
1657
+ }
1652
1658
  if (options.enroll && !sourceTable) {
1653
1659
  throw new Error("--enroll needs a source table: pass --from-table <table> (or --table <table>).");
1654
1660
  }
@@ -1662,7 +1668,8 @@ export function registerSequencesFamily(program, handleAsyncAction) {
1662
1668
  ...(senders.length > 0 ? { senders } : {}),
1663
1669
  ...(tags.length > 0 ? { tags } : {}),
1664
1670
  ...(sourceTable ? { source_table_id: sourceTable } : {}),
1665
- ...(readOption(options.activationId) ? { activation_id: readOption(options.activationId) } : {}),
1671
+ ...(activationId ? { activation_id: activationId } : {}),
1672
+ ...(activationRevision !== undefined ? { activation_revision: activationRevision } : {}),
1666
1673
  ...(options.enroll ? { enroll_from_table: true } : {}),
1667
1674
  ...(readOption(options.urlColumn) ? { linkedin_url_column_key: readOption(options.urlColumn) } : {}),
1668
1675
  ...(email ? { email } : {}),
@@ -1709,6 +1716,9 @@ export function registerSequencesFamily(program, handleAsyncAction) {
1709
1716
  .description("Update a sequence. Journey structure/channels/email binding are draft-only so revised scope receives a fresh launch approval; a started sequence may only re-time waits it already has (clear a delay by zeroing it, never by deleting the step — enrollments track position by index); sender pools can change while draft or paused; launch caps change through `sequences start` after first start. Name, throttles, the sending schedule, tags, the unsubscribe option (off by default), and open/click tracking toggles remain editable as allowed by the server. Pass only the fields you want to change.")
1710
1717
  .addHelpText("after", "\nSenders: email_send.sender_profile_id sends one email from any inbox of a selected person (a lead already assigned to that person keeps its own inbox), and email_send.sender_mailbox_id from one exact inbox of a selected sender; a LinkedIn step's sender_profile_id sends it from a selected person (invite, message, inmail and withdraw_invite share one or none). Omit both to use the lead's assigned person. An email whose From changes needs its own subject_template. `sequences start <sequence>` without --approved shows who sends each step; nothing changes.\n")
1711
1718
  .argument("<sequence>", "Sequence id or slug.")
1719
+ .option("--expected-revision <revision>", "Exact sequence_revision from sequences get; stale edits refuse. Not launch approval.")
1720
+ .option("--activation-id <id>", "Guided activation UUID; requires --activation-revision and --expected-revision.")
1721
+ .option("--activation-revision <revision>", "Current guided activation revision; requires --activation-id and --expected-revision.")
1712
1722
  .option("--name <name>", "New human-readable sequence name.")
1713
1723
  .option("--steps-file <path>", "Path to a JSON file: { \"steps\": [...] } replacing the journey. Once started, only wait timing may change: same steps, same order (zero a delay, never delete its step). Copy supports {{column}} interpolation; every step also takes {{sender_first_name}} / {{sender_last_name}} / {{sender_full_name}} for the person who sends it, and email adds {{sender_email}} (a same-named row column wins). A `branch` can also route on the lead's data with a data leaf { has_column: \"email\" } (true when that row_values column is non-empty; present:false for \"missing\").")
1714
1724
  .option("--channels <list>", "Draft only: comma-separated channels: linkedin,email,whatsapp,call,crm.")
@@ -1771,6 +1781,18 @@ export function registerSequencesFamily(program, handleAsyncAction) {
1771
1781
  .action(async (sequence, options) => {
1772
1782
  await handleAsyncAction("sequences update", options, () => {
1773
1783
  const body = {};
1784
+ const expectedRevision = options.expectedRevision;
1785
+ const activationId = options.activationId;
1786
+ const activationRevision = readPositiveInt(options.activationRevision);
1787
+ if (options.activationRevision !== undefined && activationRevision === undefined) {
1788
+ throw new OxygenError("invalid_number", "Expected a positive activation revision.", { exitCode: 1 });
1789
+ }
1790
+ if (expectedRevision !== undefined)
1791
+ body.expected_revision = expectedRevision;
1792
+ if (activationId !== undefined)
1793
+ body.activation_id = activationId;
1794
+ if (activationRevision !== undefined)
1795
+ body.activation_revision = activationRevision;
1774
1796
  const name = readOption(options.name);
1775
1797
  if (name)
1776
1798
  body.name = name;
@@ -1834,9 +1856,15 @@ export function registerSequencesFamily(program, handleAsyncAction) {
1834
1856
  .description("Get a sequence's definition, senders, status, and credit usage. --readiness adds the launch check — whether it CAN go live, why not, and the mailbox pool it would send from — without touching it; `sequences preview <sequence>` (a read; 0 credits) shows who sends each step (sender_journey).")
1835
1857
  .argument("<sequence>", "Sequence id or slug.")
1836
1858
  .option("--readiness", "Add launch_readiness: blockers (e.g. a missing send cap), warnings, and the resolved sending mailboxes. Read-only — sends nothing, changes nothing, spends nothing. Skips the LinkedIn rendered-copy scan, which stays in `sequences start` without --approved.")
1859
+ .addOption(new Option("--include <fields>", "Add readiness or your retained guided_proposal, comma-separated. Reading a proposal does not apply it or start the campaign.")
1860
+ .choices(["readiness", "guided_proposal", "readiness,guided_proposal", "guided_proposal,readiness"]))
1837
1861
  .option("--json", "Print a JSON envelope.")
1838
1862
  .action(async (sequence, options) => {
1839
- await handleAsyncAction("sequences get", options, () => requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}${options.readiness ? "?include=readiness" : ""}`));
1863
+ const include = new Set(options.include?.split(",") ?? []);
1864
+ if (options.readiness)
1865
+ include.add("readiness");
1866
+ const query = include.size ? `?include=${[...include].join(",")}` : "";
1867
+ await handleAsyncAction("sequences get", options, () => requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}${query}`));
1840
1868
  }))
1841
1869
  .addCommand(new Command("enroll")
1842
1870
  .description("Enroll leads into a sequence. Enrolling spends no credits. An active sequence contacts the new leads on its next dispatch tick; a draft or paused one holds them until `sequences start`. --from-table enrolls every not-yet-enrolled row of the sequence's bound source table (up to 500 per run; the response's from_table.has_more says whether to run again) — for a LinkedIn sequence the profile is read from the bound table's URL column, where a full /in/ URL and a bare public handle carrying a hyphen or digit (ada-lovelace) both work, so no pre-resolved provider ids are needed; each handle Oxygen canonicalized is echoed in linkedin_handles_canonicalized (value + url) to check before start, and every row that still cannot be resolved is named in unresolved_linkedin_lead_details (row, value, reason) rather than only counted in unresolved_linkedin_leads. In a LinkedIn + email sequence a row with no usable profile but a recipient email is still enrolled for the email steps (its LinkedIn steps are skipped as no_linkedin_profile, a connection wait passes at once and a connection branch takes its not-connected path) and is listed in linkedin_unavailable_lead_details instead; unresolved_linkedin_leads names only rows that were not enrolled. --leads-file enrolls an explicit JSON list { leads: [...] }: for an email/WhatsApp sequence a lead identified only by an email (row_values.email) or phone (row_values.phone) is auto-filed as a row in the sequence's leads table (auto-creating and binding one if there is none), deduped by email/phone, so no table has to exist first (the table is returned as leads_table with a web_url); a LinkedIn + email sequence auto-files an email-only lead the same way; a LinkedIn lead needs a lead_provider_id, a lead_profile_url (a /in/ URL or a bare public handle with a hyphen or digit), or a table_row_id whose row carries one of those. A CRM-only lead needs a stable table_row_id or lead_provider_id in addition to mapped row_values; a HubSpot contact id used in crm_task associations identifies the destination record, not the Oxygen enrollment. When the sequence is bound to a source table, a lead's table_row_id auto-snapshots that row's columns (incl. AI/tool outputs) into row_values for {{column}} copy — explicit row_values win. Idempotent per table row (and per email/phone for auto-filed leads). WhatsApp-only campaigns also enroll each normalized number once across all source rows and enrollment statuses; mixed campaigns keep other channels and skip duplicate WhatsApp actions. Duplicate protection defaults to this campaign; --whatsapp-duplicate-scope on create/update can additionally block identical copy across all campaigns. The org do-not-contact list is always enforced; --exclude-contacted and --suppress-list add further opt-in skips (reported under skipped_by_reason). Leads already owned by a sender account (from an earlier real send) are routed back to that same account; a lead owned by a sender NOT on this sequence is skipped (bound_to_other_sender) unless --ignore-sender-bindings. This enrolls into an OXYGEN Sequence; to put the same rows into a campaign you already run in Smartlead, Instantly, lemlist or HeyReach, use `sequences push-external`. esp.pool_scope identifies the active workspace inventory used for routing and strict_deferred_leads estimates; these estimates omit sequence sender restrictions, readiness and daily limits. A zero estimate does not guarantee readiness. Review sequences preview before launch.")
@@ -1,6 +1,5 @@
1
1
  import { requestOxygen } from "../http-client.js";
2
2
  import { readOption } from "../util.js";
3
- import { MANAGED_DATA_SUPPLIERS } from "@oxygen/shared/data-suppliers";
4
3
  import { Command } from "commander";
5
4
  export function registerViewersFamily(program, handleAsyncAction) {
6
5
  program.addCommand(new Command("viewers")
@@ -54,7 +53,7 @@ export function registerViewersFamily(program, handleAsyncAction) {
54
53
  });
55
54
  }))
56
55
  .addCommand(new Command("enrichment")
57
- .description("Turn automatic full-profile enrichment of NEW profile viewers on or off for the viewers table (" + MANAGED_DATA_SUPPLIERS.scraper + " profile: about, current role and company, experience, education, location). This is STANDING credit spend, charged credits_per_person credits per enriched viewer, applied only to viewers first seen after `enrichment.enabled_at`, each delivery bounded by `enrichment.max_credits_per_delivery` (the plan default). Switching on adds the table's Person profile columns (a paid lookup plus its extractors); they stay, with their data, when switched off. Existing rows are never charged by it — enrich them on demand by running the Person profile column. `viewers status` shows `enrichment` (on, credits_per_person, enabled_at, max_credits_per_delivery, next_capture_at: when new viewers can next arrive); the runs appear on the table's Runs page. Requires exactly one of --on or --off.")
56
+ .description("Turn automatic full-profile enrichment of NEW profile viewers on or off for the viewers table (managed LinkedIn profile: about, current role and company, experience, education, location). This is STANDING credit spend, charged credits_per_person credits per enriched viewer, applied only to viewers first seen after `enrichment.enabled_at`, each delivery bounded by `enrichment.max_credits_per_delivery` (the plan default). Switching on adds the table's Person profile columns (a paid lookup plus its extractors); they stay, with their data, when switched off. Existing rows are never charged by it — enrich them on demand by running the Person profile column. `viewers status` shows `enrichment` (on, credits_per_person, enabled_at, max_credits_per_delivery, next_capture_at: when new viewers can next arrive); the runs appear on the table's Runs page. Requires exactly one of --on or --off.")
58
57
  .option("--on", "Switch standing enrichment on for new viewers.")
59
58
  .option("--off", "Switch standing enrichment off.")
60
59
  .option("--json", "Print a JSON envelope.")
@@ -3,7 +3,7 @@ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync,
3
3
  import { homedir } from "node:os";
4
4
  import { dirname, join } from "node:path";
5
5
  import { promisify } from "node:util";
6
- import { OxygenError } from "@oxygen/shared";
6
+ import { OxygenError } from "@oxygen/shared/cli-result";
7
7
  import { sameApiOrigin } from "./runtime.js";
8
8
  const execFileAsync = promisify(execFile);
9
9
  const SERVICE_NAME = "oxygen-cli";
@@ -1,3 +1,4 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { readFile, writeFile } from "node:fs/promises";
2
3
  import { basename, extname, resolve } from "node:path";
3
4
  import { OxygenError } from "@oxygen/shared/cli-result";
@@ -31,8 +32,50 @@ const GROUP_PURPOSES = {
31
32
  "notetaker notes": "Edit saved meeting notes.",
32
33
  onboarding: "Save workspace discovery choices and resume guided onboarding.",
33
34
  "onboarding activation": "Resume your dev-pilot activation and its native resources.",
35
+ "onboarding journey": "Resume private onboarding, save answers and navigate using native revisions.",
36
+ "onboarding first-sourcing": "Read and review the native first-leads search before approving a capped run.",
37
+ };
38
+ const GUIDED_JOURNEY_FLAGS = {
39
+ activationId: { flag: "activation-id", help: "Exact activation ID returned by onboarding activation start or status." },
40
+ revision: { help: "Current activation revision returned by onboarding journey get; refresh after a conflict." },
41
+ mutation_id: { help: "Stable ID for this exact change; reuse only to recover the same request." },
42
+ step: { help: "Current saved step from onboarding journey get; use the revision you reviewed." },
43
+ answers: { help: "JSON object keyed by the current step, containing that step's reviewed answers." },
44
+ paused: { help: "Pause at this saved step; --no-paused resumes without advancing.", negateHelp: "Resume the saved step and tour position without changing visited history." },
45
+ tour_active_stop: { help: "Exact tour panel to return to, separate from visited history; only at the tour step." },
46
+ };
47
+ const GUIDED_FIRST_SOURCING_FLAGS = {
48
+ activationId: GUIDED_JOURNEY_FLAGS.activationId,
49
+ revision: { help: "Current activation revision from onboarding first-sourcing get; refresh and review after a conflict." },
50
+ generation: { help: "Current journey generation from onboarding journey get; supply revision, generation and --timezone-hint together for nightly defaults." },
51
+ timezoneHint: { flag: "timezone-hint", aliases: ["timezoneHint"], help: "Provisional customer/browser IANA timezone; supply revision, generation and --timezone-hint together. Does not confirm or save a schedule." },
52
+ "bounds.targetCount": { flag: "bounds-target-count", help: "Positive lead target, at most 50000; must match the stored preview for run." },
53
+ "bounds.maxPages": { flag: "bounds-max-pages", help: "Positive provider-page ceiling, at most 1000; must match the stored preview for run." },
54
+ "bounds.maxCredits": { flag: "bounds-max-credits", help: "Positive credit ceiling; native current pricing and the stored preview apply." },
55
+ previewId: { flag: "preview-id", help: "Exact stored previewId returned by onboarding first-sourcing preview; pass unchanged." },
56
+ previewDigest: { flag: "preview-digest", help: "Exact lowercase previewDigest from the stored native preview; pass unchanged." },
57
+ approved: { help: "Approve this exact stored preview and its bounds; native billing, access and current revision still apply." },
34
58
  };
35
59
  const OPERATION_FLAGS = {
60
+ "onboarding first-sourcing get": GUIDED_FIRST_SOURCING_FLAGS,
61
+ "onboarding first-sourcing preview": GUIDED_FIRST_SOURCING_FLAGS,
62
+ "onboarding first-sourcing run": GUIDED_FIRST_SOURCING_FLAGS,
63
+ "onboarding company-files upload": {
64
+ activationId: GUIDED_JOURNEY_FLAGS.activationId,
65
+ revision: GUIDED_JOURNEY_FLAGS.revision,
66
+ upload_id: { help: "Stable UUID for these exact original file bytes; reuse only to recover the same upload." },
67
+ sha256: { help: "Lowercase SHA-256 of the original file bytes; native storage verifies it." },
68
+ size_bytes: { help: "Exact original file byte length, before base64 encoding." },
69
+ },
70
+ "onboarding journey get": GUIDED_JOURNEY_FLAGS,
71
+ "onboarding journey save": GUIDED_JOURNEY_FLAGS,
72
+ "onboarding journey advance": GUIDED_JOURNEY_FLAGS,
73
+ "onboarding journey back": GUIDED_JOURNEY_FLAGS,
74
+ "onboarding journey restart": GUIDED_JOURNEY_FLAGS,
75
+ "onboarding journey navigation": GUIDED_JOURNEY_FLAGS,
76
+ "onboarding journey prospects-preview": GUIDED_JOURNEY_FLAGS,
77
+ "onboarding journey billing": GUIDED_JOURNEY_FLAGS,
78
+ "onboarding journey checkout": GUIDED_JOURNEY_FLAGS,
36
79
  "agent session activate": {
37
80
  revision: { help: "Current session revision returned by agent session preview; must match the reviewed configuration." },
38
81
  preview_token: { help: "Exact preview_token from agent session preview, binding reviewed resources, actions, schedule and credit caps. Pass unchanged; --approved is separate. Preview again after changes." },
@@ -85,12 +128,14 @@ export function registerCustomerOperationCommands(program, handle) {
85
128
  if (spec.binary)
86
129
  command.requiredOption("--output <path>", "Write the exact downloaded bytes to this local file.");
87
130
  if (spec.multipart)
88
- command.option("--file <path>", "Upload bytes from this local file. Omit the base64 flag; file name and content type default from the path.");
131
+ command.option("--file <path>", "Upload bytes from this local file. Omit the base64 flag; file name and content type default from the path."
132
+ + (spec.multipart.fileMetadata ? " Original SHA-256 and size are derived; explicit assertions must match." : ""));
89
133
  for (const name of contract.required ?? []) {
90
134
  const property = contract.properties[name];
91
135
  const flag = `--${overlay[name]?.flag ?? name.replace(/_/g, "-")}`;
92
136
  const option = command.options.find((candidate) => candidate.long === flag || candidate.long === `${flag}-json`);
93
- const multipartAlternative = spec.multipart && [spec.multipart.contentField, spec.multipart.nameField, spec.multipart.typeField].includes(name);
137
+ const multipartAlternative = spec.multipart && [spec.multipart.contentField, spec.multipart.nameField, spec.multipart.typeField,
138
+ ...(spec.multipart.fileMetadata ? [spec.multipart.fileMetadata.sha256Field, spec.multipart.fileMetadata.sizeBytesField] : [])].includes(name);
94
139
  if (multipartAlternative) {
95
140
  if (option)
96
141
  option.description = `${option.description} Required unless supplied by --file.`.trim();
@@ -113,9 +158,22 @@ export function registerCustomerOperationCommands(program, handle) {
113
158
  if (input[spec.multipart.contentField] !== undefined)
114
159
  throw new OxygenError("invalid_request", "Choose --file or the base64 content flag, not both.", { exitCode: 2 });
115
160
  const filePath = String(options.file);
116
- input[spec.multipart.contentField] = (await readFile(filePath)).toString("base64");
161
+ const bytes = await readFile(filePath);
162
+ input[spec.multipart.contentField] = bytes.toString("base64");
117
163
  input[spec.multipart.nameField] ??= basename(filePath);
118
164
  input[spec.multipart.typeField] ??= uploadContentType(filePath);
165
+ const metadata = spec.multipart.fileMetadata;
166
+ if (metadata) {
167
+ const digest = createHash("sha256").update(bytes).digest("hex");
168
+ if (input[metadata.sha256Field] !== undefined && input[metadata.sha256Field] !== digest) {
169
+ throw new OxygenError("invalid_request", `${metadata.sha256Field} must match the original file bytes.`, { exitCode: 2 });
170
+ }
171
+ if (input[metadata.sizeBytesField] !== undefined && input[metadata.sizeBytesField] !== bytes.length) {
172
+ throw new OxygenError("invalid_request", `${metadata.sizeBytesField} must equal the original decoded byte length.`, { exitCode: 2 });
173
+ }
174
+ input[metadata.sha256Field] ??= digest;
175
+ input[metadata.sizeBytesField] ??= bytes.length;
176
+ }
119
177
  }
120
178
  const issues = operationInputIssues(spec, input);
121
179
  if (issues.length)
@@ -188,6 +246,7 @@ function uploadContentType(filePath) {
188
246
  const types = {
189
247
  ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp", ".svg": "image/svg+xml",
190
248
  ".csv": "text/csv", ".json": "application/json", ".jsonl": "application/x-ndjson",
249
+ ".txt": "text/plain", ".md": "text/markdown", ".markdown": "text/markdown", ".pdf": "application/pdf",
191
250
  ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
192
251
  };
193
252
  return types[extname(filePath).toLowerCase()] ?? "application/octet-stream";
package/dist/help.js CHANGED
@@ -170,7 +170,7 @@ export function applyOxygenHelp(program, binaryName, docsUrl = "https://oxygen-a
170
170
  // first-touch surface names the skill that teaches the GTM loops.
171
171
  ` 2. ${binaryName} skills install --json load the skills that teach the GTM loops (automatic after login)`,
172
172
  " Add --project to keep the skills inside the current project, including confined agent sessions.",
173
- ` 3. ${binaryName} context resolve --purpose onboarding --json load workspace context and the next reads`,
173
+ ` 3. ${binaryName} context resolve --purpose onboarding --json load context; may migrate the workspace schema`,
174
174
  // Populated workspaces need the same first-win guidance as empty ones.
175
175
  ` Getting started? ${binaryName} skills get oxygen-onboarding --json`,
176
176
  " It sharpens your offer and ICP, then ranks the warmest leads you can reach now.",
package/dist/index.d.ts CHANGED
@@ -1,99 +1,2 @@
1
- import "./admin-lead-magnet-commands.js";
2
- import "./admin-primary-providers-render.js";
3
- import "./auto-update.js";
4
- import "./browser-login.js";
5
- import "./cli-values.js";
6
- import "./column-decision-options.js";
7
- import "./column-run-notices.js";
8
- import "./command-manifest.js";
9
- import "./compact-list-flags.js";
10
- import "./connectors-commands.js";
11
- import "./contract-flags.js";
12
- import "./credentials.js";
13
- import "./crm-file-read-commands.js";
14
- import "./crm-support-flags.js";
15
- import "./customer-operation-commands.js";
16
- import "./database-query-flags.js";
17
- import "./diagnostics.js";
18
- import "./feed-read-flags.js";
19
- import "./functions-commands.js";
20
- import "./help.js";
21
- import "./home-standup.js";
22
- import "./http-client.js";
23
- import "./inbox-needs-reply-notice.js";
24
- import "./json-output.js";
25
- import "./knowledge-mirror.js";
26
- import "./knowledge-repository-commands.js";
27
- import "./local-custom-http-column.js";
28
- import "./mailbox-ramp-notice.js";
29
- import "./run-wait.js";
30
- import "./runtime.js";
31
- import "./search-ai-filter-notice.js";
32
- import "./sender-limits-flags.js";
33
- import "./session.js";
34
- import "./settings-commands.js";
35
- import "./skills.js";
36
- import "./streamed-file-import.js";
37
- import "./tool-catalog-detail-command.js";
38
- import "./tool-catalog-search-window.js";
39
- import "./transcript.js";
40
- import "./ugc-commands.js";
41
- import "./update.js";
42
- import "./util.js";
43
- import "./visual-commands.js";
44
- import "./whatsapp-account-flags.js";
45
- import "./workflow-plan-limit-notices.js";
46
- import "./workflow-run-list-flags.js";
47
- import "./workspace-skill-commands.js";
48
- import "./workspace-utility-flags.js";
49
- import "@oxygen/recipe-sdk";
50
- import "@oxygen/shared";
51
- import "@oxygen/shared/billing";
52
- import "@oxygen/shared/company-reply-pause";
53
- import "@oxygen/shared/contracts/billing-owner-read";
54
- import "@oxygen/shared/contracts/copilot-sessions";
55
- import "@oxygen/shared/contracts/crm-enrichment-read";
56
- import "@oxygen/shared/contracts/database";
57
- import "@oxygen/shared/contracts/inbox-drafts-read";
58
- import "@oxygen/shared/contracts/integration-connect";
59
- import "@oxygen/shared/contracts/knowledge-content-reads";
60
- import "@oxygen/shared/contracts/limits-read";
61
- import "@oxygen/shared/contracts/linkedin-invites-list";
62
- import "@oxygen/shared/contracts/operations";
63
- import "@oxygen/shared/contracts/pipedrive-catalog";
64
- import "@oxygen/shared/contracts/publishing-reads";
65
- import "@oxygen/shared/contracts/sender-limits";
66
- import "@oxygen/shared/contracts/sequence-analytics-read";
67
- import "@oxygen/shared/contracts/sequence-list";
68
- import "@oxygen/shared/contracts/table-feed-update";
69
- import "@oxygen/shared/contracts/table-list";
70
- import "@oxygen/shared/contracts/table-query-grid";
71
- import "@oxygen/shared/contracts/table-query-raw";
72
- import "@oxygen/shared/contracts/whatsapp-reads";
73
- import "@oxygen/shared/contracts/workflow-run-list";
74
- import "@oxygen/shared/copilot-questions";
75
- import "@oxygen/shared/data-suppliers";
76
- import "@oxygen/shared/file-import";
77
- import "@oxygen/shared/knowledge-constants";
78
- import "@oxygen/shared/linkedin-sequences";
79
- import "@oxygen/shared/mailbox-import";
80
- import "@oxygen/shared/plan-band";
81
- import "@oxygen/shared/pricing-sheet";
82
- import "@oxygen/shared/select-options";
83
- import "@oxygen/shared/sequence-lifecycle";
84
- import "@oxygen/shared/table-field-types";
85
- import "@oxygen/shared/table-filter-operators";
86
- import "@oxygen/shared/table-run-summary";
87
- import "@oxygen/shared/user-capability-routing";
88
- import "@oxygen/workflows";
89
- import "commander";
90
- import "node:child_process";
91
- import "node:crypto";
92
- import "node:fs";
93
- import "node:os";
94
- import "node:path";
95
- import "node:process";
96
- import "node:readline/promises";
97
- import "node:url";
98
1
  export { createProgram } from "./command-families/program.js";
99
2
  export { formatDisabledWorkflowNotices } from "./command-support.js";
package/dist/index.js CHANGED
@@ -1,101 +1,3 @@
1
- // Preserve pre-extraction runtime import initialization order.
2
- import "./admin-lead-magnet-commands.js";
3
- import "./admin-primary-providers-render.js";
4
- import "./auto-update.js";
5
- import "./browser-login.js";
6
- import "./cli-values.js";
7
- import "./column-decision-options.js";
8
- import "./column-run-notices.js";
9
- import "./command-manifest.js";
10
- import "./compact-list-flags.js";
11
- import "./connectors-commands.js";
12
- import "./contract-flags.js";
13
- import "./credentials.js";
14
- import "./crm-file-read-commands.js";
15
- import "./crm-support-flags.js";
16
- import "./customer-operation-commands.js";
17
- import "./database-query-flags.js";
18
- import "./diagnostics.js";
19
- import "./feed-read-flags.js";
20
- import "./functions-commands.js";
21
- import "./help.js";
22
- import "./home-standup.js";
23
- import "./http-client.js";
24
- import "./inbox-needs-reply-notice.js";
25
- import "./json-output.js";
26
- import "./knowledge-mirror.js";
27
- import "./knowledge-repository-commands.js";
28
- import "./local-custom-http-column.js";
29
- import "./mailbox-ramp-notice.js";
30
- import "./run-wait.js";
31
- import "./runtime.js";
32
- import "./search-ai-filter-notice.js";
33
- import "./sender-limits-flags.js";
34
- import "./session.js";
35
- import "./settings-commands.js";
36
- import "./skills.js";
37
- import "./streamed-file-import.js";
38
- import "./tool-catalog-detail-command.js";
39
- import "./tool-catalog-search-window.js";
40
- import "./transcript.js";
41
- import "./ugc-commands.js";
42
- import "./update.js";
43
- import "./util.js";
44
- import "./visual-commands.js";
45
- import "./whatsapp-account-flags.js";
46
- import "./workflow-plan-limit-notices.js";
47
- import "./workflow-run-list-flags.js";
48
- import "./workspace-skill-commands.js";
49
- import "./workspace-utility-flags.js";
50
- import "@oxygen/recipe-sdk";
51
- import "@oxygen/shared";
52
- import "@oxygen/shared/billing";
53
- import "@oxygen/shared/company-reply-pause";
54
- import "@oxygen/shared/contracts/billing-owner-read";
55
- import "@oxygen/shared/contracts/copilot-sessions";
56
- import "@oxygen/shared/contracts/crm-enrichment-read";
57
- import "@oxygen/shared/contracts/database";
58
- import "@oxygen/shared/contracts/inbox-drafts-read";
59
- import "@oxygen/shared/contracts/integration-connect";
60
- import "@oxygen/shared/contracts/knowledge-content-reads";
61
- import "@oxygen/shared/contracts/limits-read";
62
- import "@oxygen/shared/contracts/linkedin-invites-list";
63
- import "@oxygen/shared/contracts/operations";
64
- import "@oxygen/shared/contracts/pipedrive-catalog";
65
- import "@oxygen/shared/contracts/publishing-reads";
66
- import "@oxygen/shared/contracts/sender-limits";
67
- import "@oxygen/shared/contracts/sequence-analytics-read";
68
- import "@oxygen/shared/contracts/sequence-list";
69
- import "@oxygen/shared/contracts/table-feed-update";
70
- import "@oxygen/shared/contracts/table-list";
71
- import "@oxygen/shared/contracts/table-query-grid";
72
- import "@oxygen/shared/contracts/table-query-raw";
73
- import "@oxygen/shared/contracts/whatsapp-reads";
74
- import "@oxygen/shared/contracts/workflow-run-list";
75
- import "@oxygen/shared/copilot-questions";
76
- import "@oxygen/shared/data-suppliers";
77
- import "@oxygen/shared/file-import";
78
- import "@oxygen/shared/knowledge-constants";
79
- import "@oxygen/shared/linkedin-sequences";
80
- import "@oxygen/shared/mailbox-import";
81
- import "@oxygen/shared/plan-band";
82
- import "@oxygen/shared/pricing-sheet";
83
- import "@oxygen/shared/select-options";
84
- import "@oxygen/shared/sequence-lifecycle";
85
- import "@oxygen/shared/table-field-types";
86
- import "@oxygen/shared/table-filter-operators";
87
- import "@oxygen/shared/table-run-summary";
88
- import "@oxygen/shared/user-capability-routing";
89
- import "@oxygen/workflows";
90
- import "commander";
91
- import "node:child_process";
92
- import "node:crypto";
93
- import "node:fs";
94
- import "node:os";
95
- import "node:path";
96
- import "node:process";
97
- import "node:readline/promises";
98
- import "node:url";
99
1
  import { maybeScheduleBackgroundUpdate } from "./auto-update.js";
100
2
  import { emitCliFailure, writeJson } from "./cli-output.js";
101
3
  import { createProgram } from "./command-families/program.js";
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ import { type ExtractedCommandActionHandler } from "./command-support.js";
3
+ export declare function registerWorkspacePlacementCommands(root: Command, handle: ExtractedCommandActionHandler): void;
@@ -0,0 +1,42 @@
1
+ import { Command } from "commander";
2
+ import { resolve } from "node:path";
3
+ import { readJsonFileValue } from "./command-support.js";
4
+ import { requestOxygen } from "./http-client.js";
5
+ export function registerWorkspacePlacementCommands(root, handle) {
6
+ root.addCommand(new Command("workspace-test").description("Compare email copy placement in this workspace's connected Google/Microsoft receiving inboxes: Gmail Primary/Promotions and Microsoft Focused/Other. Native sends, 0 additional credits; preview and exact message approval required.")
7
+ .addCommand(new Command("run").description("Preview an internal or cross-workspace copy test. Input JSON: sender_mailbox_ids, receiver_mailbox_ids, variants [{key,subject,body_text,body_html?}]. Approve the same input with returned request_id/plan_hash/rendered_at and max_live_sends. Up to five variants and 50 messages; never buys provider tests or resumes campaigns.")
8
+ .option("--receiver-scope <scope>", "workspace or network; network privately selects participating receivers.").option("--receiver-providers <providers>", "Comma-separated google,microsoft for network tests.")
9
+ .requiredOption("--input <file>", "JSON test input file.").option("--approved", "Execute the exact reviewed matrix.")
10
+ .option("--rendered-at <timestamp>", "Rendering timestamp returned by preview; reuse when approving.").option("--tracking-opens", "Preview with open tracking on verified workspace domain.").option("--tracking-clicks", "Preview with click tracking on verified workspace domain.")
11
+ .option("--request-id <id>", "Request UUID from preview.").option("--plan <hash>", "Exact preview plan hash.").option("--max-live-sends <n>", "Maximum approved diagnostic emails (1–50).")
12
+ .option("--preview-cursor <id>", "Read the next manifest page using the same input, request ID and rendering time.").option("--preview-limit <n>", "Preview manifest page size (1–20); never use with approval.")
13
+ .option("--preview-probe-id <id>", "Read exact rendered copy for a preview probe; reuse request ID and rendering time.").option("--preview-part <part>", "body_text or body_html.").option("--preview-offset <n>", "Unicode character offset returned by copy preview.").option("--preview-copy-limit <n>", "Copy page size (1–3000 characters).")
14
+ .option("--allow-paused-probe", "Explicitly allow diagnostic sends from paused senders, preserving pauses.").option("--json", "Print JSON.")
15
+ .action(async (options) => handle("deliverability workspace-test run", options, () => {
16
+ const input = readJsonFileValue(resolve(options.input), "--input");
17
+ if (!input || typeof input !== "object" || Array.isArray(input))
18
+ throw new Error("--input must contain a JSON object.");
19
+ return requestOxygen("/api/cli/deliverability/workspace-tests", { method: "POST", body: { ...input, approved: options.approved === true,
20
+ ...(options.approved ? { approved: true } : {}), ...(options.requestId ? { request_id: options.requestId } : {}),
21
+ ...(options.plan ? { plan_hash: options.plan } : {}), ...(options.renderedAt ? { rendered_at: options.renderedAt } : {}), ...(options.trackingOpens ? { tracking_opens: true } : {}), ...(options.trackingClicks ? { tracking_clicks: true } : {}), ...(options.maxLiveSends ? { max_live_sends: Number(options.maxLiveSends) } : {}),
22
+ ...(options.allowPausedProbe ? { allow_paused_probe: true } : {}), ...(options.receiverScope ? { receiver_scope: options.receiverScope } : {}), ...(options.receiverProviders ? { receiver_providers: String(options.receiverProviders).split(",") } : {}),
23
+ ...(options.previewCursor ? { preview_cursor: options.previewCursor } : {}), ...(options.previewLimit !== undefined ? { preview_limit: Number(options.previewLimit) } : {}),
24
+ ...(options.previewProbeId ? { preview_probe_id: options.previewProbeId } : {}), ...(options.previewPart ? { preview_part: options.previewPart } : {}), ...(options.previewOffset !== undefined ? { preview_offset: Number(options.previewOffset) } : {}), ...(options.previewCopyLimit !== undefined ? { preview_copy_limit: Number(options.previewCopyLimit) } : {}) } });
25
+ })))
26
+ .addCommand(new Command("network").description("Read or manage default-on receiving-pool participation. Administrators can opt out or accept the current disclosure. Does not send mail.")
27
+ .option("--enabled <boolean>", "true or false; omit to read.").option("--accept-disclosure", "Accept real-copy delivery to participating mailboxes and metadata processing.").option("--json", "Print JSON.")
28
+ .action(async (options) => handle("deliverability workspace-test network", options, () => {
29
+ if (options.enabled === undefined)
30
+ return requestOxygen("/api/cli/deliverability/workspace-tests?action=network");
31
+ if (options.enabled !== "true" && options.enabled !== "false")
32
+ throw new Error("--enabled must be true or false.");
33
+ return requestOxygen("/api/cli/deliverability/workspace-tests", { method: "POST", body: { operation: "participation", network_enabled: options.enabled === "true", accept_network_disclosure: options.acceptDisclosure === true } });
34
+ })))
35
+ .addCommand(new Command("copy").description("Read a Sequence email step and its copy variants for workspace placement testing. Resolve personalization before preview; no send or charge.").argument("<sequence-id>").option("--step-index <n>", "Zero-based email step index.", "0").option("--json", "Print JSON.")
36
+ .action(async (id, options) => handle("deliverability workspace-test copy", options, () => requestOxygen(`/api/cli/deliverability/workspace-tests/copy?sequence_id=${encodeURIComponent(id)}&step_index=${encodeURIComponent(options.stepIndex)}`))))
37
+ .addCommand(new Command("list").description("Read this workspace's recent copy/Primary/Focused placement tests. Saved state only: no send, provider refresh or charge.").option("--cursor <id>", "Next cursor returned by list.").option("--limit <n>", "Tests per page (1–20).").option("--json", "Print JSON.")
38
+ .action(async (options) => handle("deliverability workspace-test list", options, () => requestOxygen(`/api/cli/deliverability/workspace-tests?${new URLSearchParams({ ...options.cursor ? { cursor: options.cursor } : {}, ...options.limit !== undefined ? { limit: String(options.limit) } : {} })}`))))
39
+ .addCommand(new Command("get").description("Read one workspace copy test: variant comparison, per-receiver category, partial results and unknowns. No send or provider refresh.").argument("<id>").option("--cursor <id>", "Next receiver-result cursor.").option("--limit <n>", "Receiver results per page (1–20).")
40
+ .option("--probe <id>", "Read this probe's frozen message copy.").option("--part <part>", "body_text or body_html.").option("--offset <n>", "Returned Unicode character offset.").option("--copy-limit <n>", "Copy page size (1–3000 characters).").option("--json", "Print JSON.")
41
+ .action(async (id, options) => handle("deliverability workspace-test get", options, () => requestOxygen(`/api/cli/deliverability/workspace-tests/${encodeURIComponent(id)}?${new URLSearchParams({ ...options.cursor ? { cursor: options.cursor } : {}, ...options.limit !== undefined ? { limit: String(options.limit) } : {}, ...options.probe ? { probe_id: options.probe } : {}, ...options.part ? { body_part: options.part } : {}, ...options.offset !== undefined ? { offset: String(options.offset) } : {}, ...options.copyLimit !== undefined ? { copy_limit: String(options.copyLimit) } : {} })}`)))));
42
+ }
@@ -40,7 +40,12 @@ export type AgentSessionTeammateRoutine = {
40
40
  };
41
41
  /** The routine marker of a session's settings, or null for a general session. */
42
42
  export declare function agentSessionTeammateRoutine(config: Pick<AgentSessionConfig, "teammate_routine">): AgentSessionTeammateRoutine | null;
43
- /** Staff only: on dev, and in production's staff workspaces (`agentsEnabledIn`; Tim, 2026-10-16). */
43
+ /**
44
+ * Who has Ongoing Agent sessions, and so Oxygent routines: "staff" for staff
45
+ * emails, "all" for every member who is not a client login (the customer
46
+ * launch, Tim, 2026-10-17), in a workspace Agents run in (`agentsEnabledIn`).
47
+ */
48
+ export declare function agentSessionsAudience(env?: Record<string, string | undefined>): "off" | "staff" | "all";
44
49
  export declare function agentSessionsEnabled(env?: Record<string, string | undefined>): boolean;
45
50
  /** Whether this workspace has Ongoing Agent sessions, and so Oxygent routines. */
46
51
  export declare function agentSessionsEnabledIn(organizationId: string | null | undefined, env?: Record<string, string | undefined>): boolean;
@@ -11,18 +11,30 @@ export function agentSessionTeammateRoutine(config) {
11
11
  const marker = config.teammate_routine;
12
12
  return marker && typeof marker.chat_id === "string" ? marker : null;
13
13
  }
14
- /** Staff only: on dev, and in production's staff workspaces (`agentsEnabledIn`; Tim, 2026-10-16). */
14
+ /**
15
+ * Who has Ongoing Agent sessions, and so Oxygent routines: "staff" for staff
16
+ * emails, "all" for every member who is not a client login (the customer
17
+ * launch, Tim, 2026-10-17), in a workspace Agents run in (`agentsEnabledIn`).
18
+ */
19
+ export function agentSessionsAudience(env = process.env) {
20
+ if (env.OXYGEN_DEPLOY_ENV !== "selfhost-dev" && env.OXYGEN_DEPLOY_ENV !== "selfhost-prod")
21
+ return "off";
22
+ const flag = env.OXYGEN_AGENT_SESSIONS_ENABLED;
23
+ if ((flag !== "staff" && flag !== "all") || !agentsEnabled(env))
24
+ return "off";
25
+ return flag;
26
+ }
15
27
  export function agentSessionsEnabled(env = process.env) {
16
- return (env.OXYGEN_DEPLOY_ENV === "selfhost-dev" || env.OXYGEN_DEPLOY_ENV === "selfhost-prod")
17
- && env.OXYGEN_AGENT_SESSIONS_ENABLED === "staff" && agentsEnabled(env);
28
+ return agentSessionsAudience(env) !== "off";
18
29
  }
19
30
  /** Whether this workspace has Ongoing Agent sessions, and so Oxygent routines. */
20
31
  export function agentSessionsEnabledIn(organizationId, env = process.env) {
21
32
  return agentSessionsEnabled(env) && agentsEnabledIn(organizationId, env);
22
33
  }
23
34
  export function agentSessionsAvailableTo(env, viewer) {
24
- return agentSessionsEnabledIn(viewer.organizationId, env) && viewer.member && viewer.oxygenRole !== "client"
25
- && (guidedActivationEnabled(env) || isStaffEmail(viewer.email, env));
35
+ if (!agentSessionsEnabledIn(viewer.organizationId, env) || !viewer.member || viewer.oxygenRole === "client")
36
+ return false;
37
+ return agentSessionsAudience(env) === "all" || guidedActivationEnabled(env) || isStaffEmail(viewer.email, env);
26
38
  }
27
39
  /** Reviewed native adapters. Unknown tools (including generic dispatch and agent management) fail closed. */
28
40
  export const AGENT_SESSION_ADAPTERS = {
@@ -132,7 +144,7 @@ export function validateAgentSessionConfig(config, requireResources = true) {
132
144
  if (config.guided_activation !== undefined && (!config.teammate_routine
133
145
  || !config.guided_activation || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(config.guided_activation.activation_id)
134
146
  || !["lead_scan", "morning_briefing"].includes(config.guided_activation.purpose))) {
135
- fail("Guided routines require an activation, purpose and private Oxygent chat.");
147
+ fail("Guided routines require an activation, purpose and bound Oxygent chat.");
136
148
  }
137
149
  if (config.guided_activation?.purpose === "lead_scan") {
138
150
  const policy = config.guided_activation.scan_policy;