@oxygen-agent/cli 1.1014.779 → 1.1014.951

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 (62) hide show
  1. package/README.md +1 -1
  2. package/dist/command-manifest.js +10 -1
  3. package/dist/commands/admin.js +1 -1
  4. package/dist/commands/billing.js +13 -9
  5. package/dist/commands/columns.js +3 -3
  6. package/dist/commands/crm.js +2 -2
  7. package/dist/commands/deliverability.js +18 -10
  8. package/dist/commands/domains.js +15 -3
  9. package/dist/commands/mailboxes.js +22 -20
  10. package/dist/commands/managed-inboxes.js +1 -1
  11. package/dist/commands/sequences.js +34 -8
  12. package/dist/commands/voice.d.ts +21 -0
  13. package/dist/commands/voice.js +121 -6
  14. package/dist/customer-operation-commands.js +12 -2
  15. package/dist/index.js +6 -0
  16. package/dist/run-wait.js +10 -2
  17. package/node_modules/@oxygen/formula/package.json +8 -0
  18. package/node_modules/@oxygen/shared/dist/agent-sessions.d.ts +3 -0
  19. package/node_modules/@oxygen/shared/dist/agent-sessions.js +22 -1
  20. package/node_modules/@oxygen/shared/dist/billing-money.d.ts +31 -0
  21. package/node_modules/@oxygen/shared/dist/billing-money.js +60 -0
  22. package/node_modules/@oxygen/shared/dist/billing.d.ts +3 -0
  23. package/node_modules/@oxygen/shared/dist/billing.js +9 -1
  24. package/node_modules/@oxygen/shared/dist/capability-discovery.js +2 -2
  25. package/node_modules/@oxygen/shared/dist/contracts/agent-sessions.d.ts +76 -0
  26. package/node_modules/@oxygen/shared/dist/contracts/agent-sessions.js +4 -2
  27. package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +2 -2
  28. package/node_modules/@oxygen/shared/dist/contracts/guided-activation.d.ts +177 -0
  29. package/node_modules/@oxygen/shared/dist/contracts/guided-activation.js +59 -0
  30. package/node_modules/@oxygen/shared/dist/contracts/operations.js +4 -3
  31. package/node_modules/@oxygen/shared/dist/copilot-access.js +2 -1
  32. package/node_modules/@oxygen/shared/dist/credit-guidance.js +8 -15
  33. package/node_modules/@oxygen/shared/dist/domain-authentication.d.ts +65 -0
  34. package/node_modules/@oxygen/shared/dist/domain-authentication.js +54 -0
  35. package/node_modules/@oxygen/shared/dist/email-infrastructure-policy.d.ts +5 -0
  36. package/node_modules/@oxygen/shared/dist/email-infrastructure-policy.js +13 -0
  37. package/node_modules/@oxygen/shared/dist/guided-activation.d.ts +155 -0
  38. package/node_modules/@oxygen/shared/dist/guided-activation.fixture.d.ts +15 -0
  39. package/node_modules/@oxygen/shared/dist/guided-activation.fixture.js +21 -0
  40. package/node_modules/@oxygen/shared/dist/guided-activation.js +38 -0
  41. package/node_modules/@oxygen/shared/dist/linkedin-connection-identity.d.ts +9 -0
  42. package/node_modules/@oxygen/shared/dist/linkedin-connection-identity.js +41 -0
  43. package/node_modules/@oxygen/shared/dist/managed-email-branding.js +9 -5
  44. package/node_modules/@oxygen/shared/dist/placement-history.d.ts +3 -0
  45. package/node_modules/@oxygen/shared/dist/placement-history.js +12 -0
  46. package/node_modules/@oxygen/shared/dist/placement-report.d.ts +7 -0
  47. package/node_modules/@oxygen/shared/dist/placement-report.js +34 -0
  48. package/node_modules/@oxygen/shared/dist/plan-limits-view.d.ts +1 -0
  49. package/node_modules/@oxygen/shared/dist/plan-limits-view.js +1 -0
  50. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +1 -0
  51. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +1 -0
  52. package/node_modules/@oxygen/shared/dist/sending-limits.d.ts +1 -0
  53. package/node_modules/@oxygen/shared/dist/sending-limits.js +2 -10
  54. package/node_modules/@oxygen/shared/dist/teammates.js +2 -1
  55. package/node_modules/@oxygen/shared/dist/user-capability-routing.js +100 -2
  56. package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
  57. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  58. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  59. package/node_modules/@oxygen/shared/dist/version.js +1 -1
  60. package/node_modules/@oxygen/shared/dist/voice-countries.d.ts +66 -0
  61. package/node_modules/@oxygen/shared/dist/voice-countries.js +75 -0
  62. package/package.json +1 -1
@@ -0,0 +1,155 @@
1
+ export declare const GUIDED_ACTIVATION_PURPOSES: readonly ["lead_scan", "morning_briefing"];
2
+ export type GuidedActivationPurpose = typeof GUIDED_ACTIVATION_PURPOSES[number];
3
+ export type GuidedActivationRoutine = {
4
+ activation_id: string;
5
+ purpose: GuidedActivationPurpose;
6
+ scan_policy?: GuidedActivationScanPolicy;
7
+ };
8
+ export declare const GUIDED_ACTIVATION_POINTERS: readonly ["knowledge", "first_leads_table", "sequence", "sender", "oxygent", "direct_chat", "scan_routine", "briefing_routine"];
9
+ export type GuidedActivationPointer = typeof GUIDED_ACTIVATION_POINTERS[number];
10
+ export type GuidedActivationResources = Partial<Record<GuidedActivationPointer, string>>;
11
+ export declare const GUIDED_ACTIVATION_REVISIONS: readonly ["configuration", "sequence", "audience", "recipients", "sender", "copy", "knowledge", "schedule", "authority"];
12
+ export type GuidedActivationRevisions = Record<typeof GUIDED_ACTIVATION_REVISIONS[number], string>;
13
+ export type GuidedActivationBounds = {
14
+ recipients: number;
15
+ actions: number;
16
+ inference_credits: number;
17
+ provider_credits: number;
18
+ };
19
+ export type GuidedActivationReview = {
20
+ id: string;
21
+ purpose: "launch" | GuidedActivationPurpose;
22
+ activation_revision: number;
23
+ revisions: GuidedActivationRevisions;
24
+ bounds: GuidedActivationBounds;
25
+ input_digest: string;
26
+ expires_at: string;
27
+ actor_id: string;
28
+ authority: {
29
+ kind: "explicit" | "auto_approve";
30
+ revision: string;
31
+ };
32
+ };
33
+ export type GuidedActivation = {
34
+ id: string;
35
+ organization_id: string;
36
+ creator_id: string;
37
+ revision: number;
38
+ operating_path: "own_agent" | "oxygent";
39
+ resources: GuidedActivationResources;
40
+ outcomes: Partial<Record<"first_leads" | "sequence" | "launch" | GuidedActivationPurpose, "proposed" | "armed" | "launched" | "confirmed_sent" | "paused">>;
41
+ reviews: Partial<Record<GuidedActivationReview["purpose"], GuidedActivationReview>>;
42
+ onboarding_start: {
43
+ state: "claimed" | "posted";
44
+ chat_id: string;
45
+ turn_id?: string;
46
+ event_seq?: number;
47
+ } | null;
48
+ last_fault: {
49
+ code: string;
50
+ at: string;
51
+ } | null;
52
+ };
53
+ /** Unknown tiers and production-shaped data are always denied, independent of NODE_ENV. */
54
+ export declare function guidedActivationEnabled(env?: Record<string, string | undefined>): boolean;
55
+ export declare function guidedActivationWorkspaceAllowed(organizationId: string, env?: Record<string, string | undefined>): boolean;
56
+ export declare function guidedActivationAvailableTo(env: Record<string, string | undefined>, viewer: {
57
+ organizationId: string;
58
+ email: string | null | undefined;
59
+ member: boolean;
60
+ oxygenRole?: string | null;
61
+ }): boolean;
62
+ export declare function assertGuidedActivationBounds(bounds: GuidedActivationBounds, purpose: GuidedActivationReview["purpose"]): void;
63
+ /** Identity survives replacement sessions/config edits and the DST repeated hour. */
64
+ export declare function guidedActivationOccurrenceKey(input: {
65
+ activationId: string;
66
+ creatorId: string;
67
+ purpose: GuidedActivationPurpose;
68
+ localDate: string;
69
+ }): string;
70
+ /** Narrow reviewed source policy. Provider adapters must refuse unsupported incremental modes. */
71
+ export type GuidedActivationScanPolicy = {
72
+ version: 1;
73
+ table_id: string;
74
+ chat_id: string;
75
+ policy_digest: string;
76
+ revisions: GuidedActivationRevisions;
77
+ sources: Array<{
78
+ source_id: string;
79
+ config_digest: string;
80
+ incremental_mode: "cursor" | "watermark";
81
+ person_key_field: string;
82
+ }>;
83
+ exclude_dnc: true;
84
+ exclude_contacted: true;
85
+ review_only: true;
86
+ max_examined: number;
87
+ max_new_leads: number;
88
+ max_provider_pages: number;
89
+ max_provider_credits: number;
90
+ max_inference_credits: number;
91
+ };
92
+ export type GuidedActivationScanSnapshot = {
93
+ version: 1;
94
+ activation_id: string;
95
+ creator_id: string;
96
+ occurrence_id: string;
97
+ run_id: string;
98
+ routine_id: string;
99
+ period: {
100
+ local_date: string;
101
+ timezone: string;
102
+ started_at: string;
103
+ settled_at: string | null;
104
+ };
105
+ policy_digest: string;
106
+ table_id: string;
107
+ status: "running" | "completed" | "partial" | "failed" | "effect_unknown" | "budget_exhausted";
108
+ reason: string | null;
109
+ counts: {
110
+ examined: number;
111
+ new: number;
112
+ deduped: number;
113
+ qualified: number;
114
+ excluded: number;
115
+ };
116
+ candidate_ids: string[];
117
+ sources: Array<{
118
+ source_id: string;
119
+ config_digest: string;
120
+ ingestion_run_ids: string[];
121
+ cursor_before: string | null;
122
+ cursor_after: string | null;
123
+ status: string;
124
+ }>;
125
+ credits: {
126
+ captured: number;
127
+ held: number;
128
+ };
129
+ };
130
+ /** C's existing native launch_review is the only launch producer. Its digest binds all material inputs server-side. */
131
+ export type GuidedActivationLaunchReview = {
132
+ digest: string;
133
+ expires_at: string;
134
+ max_live_sends: number;
135
+ max_credits: number;
136
+ };
137
+ export type GuidedActivationSequenceMarker = {
138
+ activation_id: string;
139
+ creator_id: string;
140
+ };
141
+ export type GuidedActivationManualRunPreview = {
142
+ id: string;
143
+ kind: "run_now";
144
+ activation_id: string;
145
+ purpose: GuidedActivationPurpose;
146
+ actor_id: string;
147
+ session_id: string;
148
+ session_revision: number;
149
+ config_digest: string;
150
+ per_run_credits: number;
151
+ expires_at: string;
152
+ preview_token: string;
153
+ approved_at?: string;
154
+ applied_run_id?: string;
155
+ };
@@ -0,0 +1,15 @@
1
+ import type { GuidedActivationRevisions, GuidedActivationScanSnapshot } from "./guided-activation.js";
2
+ /** Deterministic non-provider fixtures for A–G; no connection/outreach authority. */
3
+ export declare const GUIDED_ACTIVATION_FIXTURE: {
4
+ readonly organizationId: "10000000-0000-4000-8000-000000000001";
5
+ readonly actorId: "10000000-0000-4000-8000-000000000002";
6
+ readonly activationId: "10000000-0000-4000-8000-000000000003";
7
+ readonly tableId: "10000000-0000-4000-8000-000000000004";
8
+ readonly chatId: "10000000-0000-4000-8000-000000000005";
9
+ readonly routineId: "10000000-0000-4000-8000-000000000006";
10
+ readonly runId: "10000000-0000-4000-8000-000000000007";
11
+ readonly now: "2026-10-25T00:30:00.000Z";
12
+ readonly timezone: "Europe/Berlin";
13
+ };
14
+ export declare const GUIDED_ACTIVATION_REVISION_FIXTURE: GuidedActivationRevisions;
15
+ export declare const GUIDED_ACTIVATION_SCAN_FIXTURE: GuidedActivationScanSnapshot;
@@ -0,0 +1,21 @@
1
+ /** Deterministic non-provider fixtures for A–G; no connection/outreach authority. */
2
+ export const GUIDED_ACTIVATION_FIXTURE = {
3
+ organizationId: "10000000-0000-4000-8000-000000000001", actorId: "10000000-0000-4000-8000-000000000002",
4
+ activationId: "10000000-0000-4000-8000-000000000003", tableId: "10000000-0000-4000-8000-000000000004",
5
+ chatId: "10000000-0000-4000-8000-000000000005", routineId: "10000000-0000-4000-8000-000000000006",
6
+ runId: "10000000-0000-4000-8000-000000000007", now: "2026-10-25T00:30:00.000Z", timezone: "Europe/Berlin",
7
+ };
8
+ export const GUIDED_ACTIVATION_REVISION_FIXTURE = {
9
+ configuration: "config-1", sequence: "sequence-1", audience: "audience-1", recipients: "recipients-1", sender: "sender-1",
10
+ copy: "copy-1", knowledge: "knowledge-1", schedule: "schedule-1", authority: "authority-1",
11
+ };
12
+ export const GUIDED_ACTIVATION_SCAN_FIXTURE = {
13
+ version: 1, activation_id: GUIDED_ACTIVATION_FIXTURE.activationId, creator_id: GUIDED_ACTIVATION_FIXTURE.actorId,
14
+ occurrence_id: JSON.stringify([GUIDED_ACTIVATION_FIXTURE.activationId, GUIDED_ACTIVATION_FIXTURE.actorId, "lead_scan", "2026-10-25"]),
15
+ run_id: GUIDED_ACTIVATION_FIXTURE.runId, routine_id: GUIDED_ACTIVATION_FIXTURE.routineId,
16
+ period: { local_date: "2026-10-25", timezone: "Europe/Berlin", started_at: "2026-10-25T00:00:00.000Z", settled_at: "2026-10-25T00:05:00.000Z" },
17
+ policy_digest: "fixture-policy-1", table_id: GUIDED_ACTIVATION_FIXTURE.tableId, status: "completed", reason: null,
18
+ counts: { examined: 10, new: 3, deduped: 2, qualified: 3, excluded: 5 }, candidate_ids: ["lead-1", "lead-2", "lead-3"],
19
+ sources: [{ source_id: "fixture-incremental-source", config_digest: "fixture-source-1", ingestion_run_ids: [], cursor_before: "page-1", cursor_after: "page-2", status: "completed" }],
20
+ credits: { captured: 0, held: 0 },
21
+ };
@@ -0,0 +1,38 @@
1
+ import { OxygenError } from "./cli-result.js";
2
+ import { isProductionData } from "./deploy-env.js";
3
+ export const GUIDED_ACTIVATION_PURPOSES = ["lead_scan", "morning_briefing"];
4
+ export const GUIDED_ACTIVATION_POINTERS = ["knowledge", "first_leads_table", "sequence", "sender", "oxygent", "direct_chat", "scan_routine", "briefing_routine"];
5
+ export const GUIDED_ACTIVATION_REVISIONS = ["configuration", "sequence", "audience", "recipients", "sender", "copy", "knowledge", "schedule", "authority"];
6
+ /** Unknown tiers and production-shaped data are always denied, independent of NODE_ENV. */
7
+ export function guidedActivationEnabled(env = process.env) {
8
+ return env.OXYGEN_DEPLOY_ENV === "selfhost-dev" && !isProductionData(env)
9
+ && env.OXYGEN_GUIDED_ACTIVATION_ENABLED === "staff";
10
+ }
11
+ export function guidedActivationWorkspaceAllowed(organizationId, env = process.env) {
12
+ // Philipp, 2026-10-15: every genuine DEV workspace, including future IDs.
13
+ // The old WORKSPACE_IDS inventory no longer grants or restricts admission.
14
+ return guidedActivationEnabled(env) && organizationId.trim().length > 0;
15
+ }
16
+ export function guidedActivationAvailableTo(env, viewer) {
17
+ return guidedActivationWorkspaceAllowed(viewer.organizationId, env) && viewer.member
18
+ && viewer.oxygenRole !== "client";
19
+ }
20
+ export function assertGuidedActivationBounds(bounds, purpose) {
21
+ if (!bounds || Object.values(bounds).length !== 4 || ![bounds.recipients, bounds.actions].every(value => Number.isSafeInteger(value) && value >= 0)
22
+ || ![bounds.inference_credits, bounds.provider_credits].every(value => Number.isFinite(value) && value >= 0)
23
+ || (purpose === "launch" && (bounds.actions <= 0 || bounds.recipients <= 0))
24
+ || (purpose === "lead_scan" && (bounds.recipients <= 0 || bounds.actions !== 0))
25
+ || (purpose === "morning_briefing" && (bounds.recipients !== 0 || bounds.actions !== 0 || bounds.provider_credits !== 0))) {
26
+ throw new OxygenError("activation_invalid_bounds", "Set exact action, recipient and credit ceilings for this operation.", { exitCode: 2 });
27
+ }
28
+ }
29
+ /** Identity survives replacement sessions/config edits and the DST repeated hour. */
30
+ export function guidedActivationOccurrenceKey(input) {
31
+ const { activationId, creatorId, purpose, localDate } = input;
32
+ const date = new Date(`${localDate}T00:00:00Z`);
33
+ if (!activationId || !creatorId || !GUIDED_ACTIVATION_PURPOSES.includes(purpose)
34
+ || !/^\d{4}-\d{2}-\d{2}$/.test(localDate) || !Number.isFinite(date.getTime()) || date.toISOString().slice(0, 10) !== localDate) {
35
+ throw new OxygenError("invalid_request", "A routine occurrence needs its activation, creator, purpose and local date.");
36
+ }
37
+ return JSON.stringify([activationId, creatorId, purpose, localDate]);
38
+ }
@@ -0,0 +1,9 @@
1
+ /** Aliases must come from the same provider person; never translate ACw to ACo. */
2
+ export type LinkedInConnectionIdentity = {
3
+ providerId: string;
4
+ profileUrl?: string | null | undefined;
5
+ publicIdentifier?: string | null | undefined;
6
+ };
7
+ export declare function linkedinConnectionKeys(identity: LinkedInConnectionIdentity): string[];
8
+ /** Return the original waiting IDs so frozen enrollment identity guards remain authoritative. */
9
+ export declare function connectedLinkedInLeadIds(targets: LinkedInConnectionIdentity[], connections: LinkedInConnectionIdentity[]): string[];
@@ -0,0 +1,41 @@
1
+ import { normalizeLinkedinProfileUrl, isLinkedinMemberIdHandle } from "./linkedin-url.js";
2
+ export function linkedinConnectionKeys(identity) {
3
+ const id = identity.providerId.trim();
4
+ const keys = id ? [`id:${id.replace(/^urn:li:(?:fs_miniProfile|fsd_profile|member):/, "")}`] : [];
5
+ const publicIdentifier = identity.publicIdentifier?.trim();
6
+ const publicUrl = publicIdentifier && !/[\s/?#]/.test(publicIdentifier)
7
+ ? `https://www.linkedin.com/in/${publicIdentifier}` : null;
8
+ for (const value of [identity.profileUrl, publicUrl, identity.providerId]) {
9
+ if (!value)
10
+ continue;
11
+ let path;
12
+ try {
13
+ path = new URL(value.startsWith("http") ? value : `https://${value}`).pathname;
14
+ }
15
+ catch {
16
+ continue;
17
+ }
18
+ if (!/^\/(?:in|pub)\/[^/]+\/*$/i.test(path))
19
+ continue;
20
+ const profile = normalizeLinkedinProfileUrl(value);
21
+ if (profile && !/[\s/?#]/.test(profile.dispatchHandle) && !isLinkedinMemberIdHandle(profile.dispatchHandle))
22
+ keys.push(`profile:${profile.handle}`);
23
+ }
24
+ return [...new Set(keys)];
25
+ }
26
+ /** Return the original waiting IDs so frozen enrollment identity guards remain authoritative. */
27
+ export function connectedLinkedInLeadIds(targets, connections) {
28
+ const owners = new Map();
29
+ for (const connection of connections) {
30
+ for (const key of linkedinConnectionKeys(connection)) {
31
+ const ids = owners.get(key) ?? new Set();
32
+ ids.add(connection.providerId.trim().replace(/^urn:li:(?:fs_miniProfile|fsd_profile|member):/, ""));
33
+ owners.set(key, ids);
34
+ }
35
+ }
36
+ return targets.filter(target => linkedinConnectionKeys(target).some(key => {
37
+ const ids = owners.get(key);
38
+ // Conflicting aliases are not proof of one member. Exact IDs remain usable.
39
+ return ids?.size === 1;
40
+ })).map(target => target.providerId);
41
+ }
@@ -1,6 +1,8 @@
1
1
  export const OXYGEN_WARMUP_PROVIDER = "oxygen_warmup";
2
2
  export const OXYGEN_PLACEMENT_PROVIDER = "oxygen_placement";
3
3
  export function managedEmailProviderInput(value, product) {
4
+ if (product === "placement" && typeof value === "string" && value.trim().toLowerCase() === "oxygen_deliverability")
5
+ return "emailguard";
4
6
  const publicId = product === "warmup" ? OXYGEN_WARMUP_PROVIDER : OXYGEN_PLACEMENT_PROVIDER;
5
7
  return typeof value === "string" && value.trim().toLowerCase() === publicId ? "sendkit" : value;
6
8
  }
@@ -12,7 +14,7 @@ export function isManagedPlacementProvider(value) {
12
14
  }
13
15
  // These are service-owned fields, never arbitrary customer content or opaque receipts.
14
16
  const IDENTIFIER_FIELDS = new Set([
15
- "provider", "active_provider", "warmup_provider", "warmupProvider", "warmupRail",
17
+ "provider", "health_provider", "healthProvider", "active_provider", "warmup_provider", "warmupProvider", "warmupRail",
16
18
  "action", "code", "failure_code", "error_code", "credential_source", "credentialSource",
17
19
  "method", "handoff", "reason_code", "kind", "type", "source",
18
20
  ]);
@@ -20,14 +22,14 @@ const COPY_FIELDS = new Set([
20
22
  "message", "note", "reason", "description", "hint", "transport_hint", "next_step",
21
23
  "next_action", "nextAction", "label", "title", "instruction", "instructions", "warnings",
22
24
  "recommendation", "recommendations", "limitations", "next_external_write", "price_note",
23
- "error", "errors", "summary", "blocker", "blockers", "detail", "details_text", "provider_label",
25
+ "pricing_basis", "billing", "capability", "error", "errors", "summary", "blocker", "blockers", "detail", "details_text", "provider_label", "command", "next_command", "next_commands", "needs",
24
26
  ]);
25
27
  export function managedEmailMessage(value, product = "warmup") {
26
28
  const label = product === "warmup" ? "OXYGEN Warm-up" : "OXYGEN placement evaluation";
27
29
  if (product === "placement") {
28
30
  value = value.replace(/(?:SendKit|OXYGEN placement evaluation) does not document whether inbox, spam, and missing summary values are counts or percentages; Oxygen preserves the values without conversion\./gi, "Summary units are unverified (counts or percentages); values are shown as recorded, without conversion.");
29
31
  }
30
- return value.replace(/SendKit(?:'s)?\s+warm-?up/gi, label).replace(/SendKit/gi, label);
32
+ return value.replace(/\bEMAILGUARD_[A-Z0-9_]+\b/g, "managed deliverability configuration").replace(/EmailGuard/gi, "OXYGEN deliverability").replace(/SendKit(?:'s)?\s+warm-?up/gi, label).replace(/SendKit/gi, label);
31
33
  }
32
34
  /**
33
35
  * Project only service identifiers and copy. Preserve URLs, ids, hashes, dates,
@@ -45,9 +47,11 @@ export function publicManagedEmail(value, product = "warmup") {
45
47
  return entry.replace(/^warmup:sendkit:/, "warmup:oxygen_warmup:");
46
48
  }
47
49
  if (IDENTIFIER_FIELDS.has(field)) {
50
+ if (entry.toLowerCase() === "emailguard")
51
+ return "oxygen_deliverability";
48
52
  if (entry.toLowerCase() === "sendkit")
49
53
  return publicId;
50
- return entry.replace(/sendkit_/gi, `${publicId}_`).replace(/_sendkit/gi, `_${publicId}`);
54
+ return entry.replace(/emailguard/gi, "deliverability").replace(/sendkit_/gi, `${publicId}_`).replace(/_sendkit/gi, `_${publicId}`);
51
55
  }
52
56
  return COPY_FIELDS.has(field) ? managedEmailMessage(entry, context) : entry;
53
57
  }
@@ -62,7 +66,7 @@ export function publicManagedEmail(value, product = "warmup") {
62
66
  const privateUsage = (record.provider === "sendkit" || groupKey?.provider === "sendkit") &&
63
67
  (record.type === "byok_usage" || (typeof record.byok_usage_events === "number" && record.byok_usage_events > 0));
64
68
  return Object.fromEntries(Object.entries(record).map(([key, item]) => [
65
- { sendkit_google: "oxygen_warmup_google", sendkit_microsoft: "oxygen_warmup_microsoft", sendkit_binding: "oxygen_placement_binding", sendkitBinding: "oxygen_placement_binding" }[key] ?? key,
69
+ { sendkit_google: "oxygen_warmup_google", sendkit_microsoft: "oxygen_warmup_microsoft", sendkit_binding: "oxygen_placement_binding", sendkitBinding: "oxygen_placement_binding" }[key] ?? key.replace(/emailguard/gi, "deliverability"),
66
70
  privateUsage && key === "provider" ? "private_integration"
67
71
  : privateUsage && key === "key" ? project({ ...groupKey, provider: "private_integration" }, key, context)
68
72
  : privateUsage && key === "tool_id" ? null
@@ -0,0 +1,3 @@
1
+ /** Bind a continuation to its filter. Tenant/RLS still authorizes the anchor. */
2
+ export declare function placementHistoryCursor(id: string, status?: string | null): string;
3
+ export declare function placementHistoryCursorAnchor(cursor: string, status?: string | null): string | null;
@@ -0,0 +1,12 @@
1
+ const PLACEMENT_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2
+ /** Bind a continuation to its filter. Tenant/RLS still authorizes the anchor. */
3
+ export function placementHistoryCursor(id, status) {
4
+ if (!PLACEMENT_ID.test(id))
5
+ throw new Error("Invalid placement-test cursor anchor.");
6
+ return `placement:v1:${status ?? "all"}:${id}`;
7
+ }
8
+ export function placementHistoryCursorAnchor(cursor, status) {
9
+ const [kind, version, filter, id, extra] = cursor.split(":");
10
+ return kind === "placement" && version === "v1" && filter === (status ?? "all")
11
+ && id !== undefined && PLACEMENT_ID.test(id) && extra === undefined ? id : null;
12
+ }
@@ -0,0 +1,7 @@
1
+ /** Shared placement-report policy. No provider calls, sends or billing effects. */
2
+ export declare const PLACEMENT_REPORT_WAIT_MS: number;
3
+ export declare const INCOMPLETE_PLACEMENT_MESSAGE = "This test is incomplete. Available seed results are shown.";
4
+ export declare function placementReportDeadline(provider: string, results: unknown): string | null;
5
+ export declare function placementReportExpired(provider: string, results: unknown, now?: number): boolean;
6
+ /** Customer copy follows the persisted settlement receipt, never an inferred balance. */
7
+ export declare function placementRefundMessage(value: unknown): string;
@@ -0,0 +1,34 @@
1
+ /** Shared placement-report policy. No provider calls, sends or billing effects. */
2
+ export const PLACEMENT_REPORT_WAIT_MS = 2 * 60 * 60 * 1000;
3
+ export const INCOMPLETE_PLACEMENT_MESSAGE = "This test is incomplete. Available seed results are shown.";
4
+ export function placementReportDeadline(provider, results) {
5
+ if (provider !== "emailguard" || !results || typeof results !== "object")
6
+ return null;
7
+ const receipt = results.seed_delivery;
8
+ if (!receipt || typeof receipt !== "object")
9
+ return null;
10
+ const send = receipt;
11
+ if (send.status !== "sent" || typeof send.sent_at !== "string")
12
+ return null;
13
+ const sent = Date.parse(send.sent_at);
14
+ return Number.isFinite(sent) ? new Date(sent + PLACEMENT_REPORT_WAIT_MS).toISOString() : null;
15
+ }
16
+ export function placementReportExpired(provider, results, now = Date.now()) {
17
+ const deadline = placementReportDeadline(provider, results);
18
+ return deadline !== null && now >= Date.parse(deadline);
19
+ }
20
+ /** Customer copy follows the persisted settlement receipt, never an inferred balance. */
21
+ export function placementRefundMessage(value) {
22
+ if (!value || typeof value !== "object")
23
+ return "";
24
+ const receipt = value;
25
+ if (receipt.status === "pending")
26
+ return "Your test-credit refund is pending.";
27
+ if (receipt.status === "refunded")
28
+ return "The test charge was refunded.";
29
+ if (receipt.status === "allowance_restored")
30
+ return "The included test allowance has been restored.";
31
+ if (receipt.status === "not_applicable")
32
+ return "Own-key tests do not receive an OXYGEN test-credit refund.";
33
+ return "";
34
+ }
@@ -109,6 +109,7 @@ type ChannelLimit = {
109
109
  };
110
110
  export type PlanLimitsSendingLimits = {
111
111
  email_per_mailbox?: ChannelLimit;
112
+ entra_per_mailbox?: ChannelLimit;
112
113
  whatsapp_per_number?: ChannelLimit;
113
114
  voice_per_number?: ChannelLimit;
114
115
  linkedin_per_account?: {
@@ -239,6 +239,7 @@ export function planLimitsSendingRows(sending) {
239
239
  return [];
240
240
  const maximum = (limit) => limit?.max_per_day === null ? "no maximum" : formatLimitCount(limit?.max_per_day);
241
241
  return [
242
+ ...(sending.entra_per_mailbox ? [{ label: "Entra per mailbox", defaultPerDay: formatLimitCount(sending.entra_per_mailbox.default_per_day), maximumPerDay: maximum(sending.entra_per_mailbox) }] : []),
242
243
  { label: "Email per mailbox", defaultPerDay: formatLimitCount(sending.email_per_mailbox?.default_per_day), maximumPerDay: maximum(sending.email_per_mailbox) },
243
244
  { label: "WhatsApp per number", defaultPerDay: formatLimitCount(sending.whatsapp_per_number?.default_per_day), maximumPerDay: maximum(sending.whatsapp_per_number) },
244
245
  {
@@ -19,6 +19,7 @@ export declare const WHATSAPP_ACCOUNT_CREDITS_PER_MONTH = 3400;
19
19
  export declare const X_ACCOUNT_CREDITS_PER_MONTH = 1000;
20
20
  export declare const EGRESS_IP_CREDITS_PER_MONTH = 2500;
21
21
  export declare const SENDING_MAILBOX_CREDITS_PER_MONTH = 100;
22
+ export declare const SENDING_ENTRA_MAILBOX_CREDITS_PER_MONTH = 25;
22
23
  export declare const MANAGED_INBOX_CREDITS_PER_MONTH = 300;
23
24
  export declare const MAILBOX_WARMUP_CREDITS_PER_MONTH = 0;
24
25
  export declare const DELIVERABILITY_SEAT_CREDITS_PER_MONTH = 100;
@@ -41,6 +41,7 @@ export const WHATSAPP_ACCOUNT_CREDITS_PER_MONTH = 3_400;
41
41
  export const X_ACCOUNT_CREDITS_PER_MONTH = 1_000;
42
42
  export const EGRESS_IP_CREDITS_PER_MONTH = 2_500;
43
43
  export const SENDING_MAILBOX_CREDITS_PER_MONTH = 100;
44
+ export const SENDING_ENTRA_MAILBOX_CREDITS_PER_MONTH = 25;
44
45
  export const MANAGED_INBOX_CREDITS_PER_MONTH = 300;
45
46
  export const MAILBOX_WARMUP_CREDITS_PER_MONTH = 0;
46
47
  export const DELIVERABILITY_SEAT_CREDITS_PER_MONTH = 100;
@@ -23,6 +23,7 @@ export declare function emailMailboxDailyCapMaximum(_options?: RepricingOptions)
23
23
  export declare function emailMailboxDailyCapInForce(input: {
24
24
  storedCap: number;
25
25
  source: EmailMailboxDailyCapSource | null;
26
+ metadata?: unknown;
26
27
  }, options?: RepricingOptions): number;
27
28
  /** A new voice number may dial this many times a day once it has warmed up. */
28
29
  export declare const VOICE_NUMBER_DEFAULT_DAILY_CAP = 100;
@@ -1,12 +1,4 @@
1
- // Per-channel sending limits that are the same on every plan (2026-09 repricing,
2
- // decision L3; specification slice S52). Limits guard, credits meter: none of
3
- // these is sold, and none charges for sending.
4
- //
5
- // Email campaign ramp: 5 / 10 / 20 based on accepted-send days, then the
6
- // configured cap (25 by default, maximum 50), independent of billing activation.
7
- // The versioned migration changes old defaults once; later choices are retained.
8
- //
9
- // Voice per number: 100 dials a day by default, adjustable up to 300.
1
+ import { mailboxInfrastructureDailyCap } from "./email-infrastructure-policy.js";
10
2
  import { EMAIL_DELIVERABILITY_DEFAULTS } from "./email-deliverability-policy.js";
11
3
  /**
12
4
  * The daily cap a new mailbox gets when nobody names one. New mailboxes were
@@ -35,7 +27,7 @@ export function emailMailboxDailyCapMaximum(_options = {}) {
35
27
  export function emailMailboxDailyCapInForce(input, options = {}) {
36
28
  const stored = Number.isFinite(input.storedCap) ? Math.max(0, Math.floor(input.storedCap)) : 0;
37
29
  const maximum = emailMailboxDailyCapMaximum(options);
38
- return maximum === null ? stored : Math.min(stored, maximum);
30
+ return Math.min(stored, maximum ?? Infinity, mailboxInfrastructureDailyCap(input.metadata) ?? Infinity);
39
31
  }
40
32
  /** A new voice number may dial this many times a day once it has warmed up. */
41
33
  export const VOICE_NUMBER_DEFAULT_DAILY_CAP = 100;
@@ -1,3 +1,4 @@
1
+ import { guidedActivationEnabled } from "./guided-activation.js";
1
2
  import { isStaffEmail } from "./staff.js";
2
3
  /**
3
4
  * AI teammates (ADR 0032): Agents people talk to, each person in one ongoing
@@ -20,7 +21,7 @@ export function teammatesEnabled(env = process.env) {
20
21
  return env.OXYGEN_AGENT_SESSIONS_ENABLED === "staff";
21
22
  }
22
23
  export function teammatesAvailableTo(env, viewer) {
23
- return teammatesEnabled(env) && viewer.member && viewer.oxygenRole !== "client" && isStaffEmail(viewer.email, env);
24
+ return teammatesEnabled(env) && viewer.member && viewer.oxygenRole !== "client" && (guidedActivationEnabled(env) || isStaffEmail(viewer.email, env));
24
25
  }
25
26
  /**
26
27
  * The Workspace Agent's slug. Every workspace's first Oxygent is that Agent: an
@@ -14,6 +14,19 @@ export function capabilityDiscoveryRecovery(query, surface) {
14
14
  // connection routes.
15
15
  export function inferUserCapabilityRoute(query) {
16
16
  const normalized = query.toLowerCase().replace(/[_-]+/g, " ");
17
+ // Exact native session activation must outrank the generic Agent gateways.
18
+ // Discovery supplies no approval, preview token or invented session binding.
19
+ if (/\bagent session activate\b/.test(normalized)
20
+ && !/\b(?:invite|members?|membership|permissions?|roles?|human|colleague|coworker)\b/.test(normalized)) {
21
+ const route = getCapabilityRouteMatch("agents");
22
+ if (route)
23
+ return {
24
+ ...route,
25
+ card: { ...route.card, execution: "Approve the exact preview revision and activate scoped standing authority. Starts bounded inference. A cap above default_max_credits requires separate approve_above_default=true. Later settings changes require a fresh preview." },
26
+ recommendedTools: ["oxygen_agent_session_activate", "oxygen_agent_session_preview", "oxygen_agent_session_get"],
27
+ recommendedCommands: ["agent session activate", "agent session preview", "agent session get"],
28
+ };
29
+ }
17
30
  if (isUnconfirmedLookupReviewIntent(normalized)) {
18
31
  const route = getCapabilityRouteMatch("observability");
19
32
  if (route)
@@ -38,6 +51,66 @@ export function inferUserCapabilityRoute(query) {
38
51
  exactMcpCall: { tool: "oxygen_knowledge_bootstrap", arguments: { status: true } },
39
52
  };
40
53
  }
54
+ // A compound onboarding request describes the journey's saved context,
55
+ // rather than a request to install skills or build a hosted Workflow.
56
+ if (isCompanyOnboardingContextIntent(normalized)) {
57
+ const route = getCapabilityRouteMatch("knowledge-graph");
58
+ if (route)
59
+ return { ...route, card: { ...route.card, posture: "read_only", execution: "Read saved company context and onboarding resources first. Inspect current research status; this discovery starts no research, skill installation, Workflow or outreach." },
60
+ recommendedTools: ["oxygen_context_profile_get", "oxygen_knowledge_bootstrap", "oxygen_onboarding_activation_status"],
61
+ recommendedCommands: ["context profile get", "knowledge bootstrap status", "onboarding activation status"],
62
+ exactCommand: "oxygen context profile get --json", exactMcpCall: { tool: "oxygen_context_profile_get", arguments: {} } };
63
+ }
64
+ if ((/\b(?:linkedin|sequence|campaign)\b/.test(normalized) && /\b(?:preview|review|readiness)\b/.test(normalized)
65
+ && /\b(?:launch|start)\b/.test(normalized) && /\b(?:approval|send limits?|caps?)\b/.test(normalized))
66
+ || (isLinkedInCampaignIntent(normalized) && /\breadiness\b/.test(normalized) && /\bpreview\b/.test(normalized))) {
67
+ const route = getCapabilityRouteMatch("sequences");
68
+ if (route)
69
+ return { ...route, card: { ...route.card, posture: "read_only", execution: "Read the native Sequence launch preview with approved=false. Review readiness, recipients, sender, copy and positive send limits before approving the exact native review." },
70
+ recommendedTools: ["oxygen_sequences_start", "oxygen_sequences_get", "oxygen_sequences_list"], recommendedCommands: ["sequences start", "sequences get", "sequences list"],
71
+ exactCommand: "oxygen sequences start '<sequence>' --json", exactMcpCall: { tool: "oxygen_sequences_start", arguments: { sequence: "<sequence>", approved: false } } };
72
+ }
73
+ if (isLinkedInCampaignIntent(normalized) && /\b(?:draft|copy)\b/.test(normalized) && /\breview\w*\b/.test(normalized)) {
74
+ const route = getCapabilityRouteMatch("sequences");
75
+ if (route)
76
+ return {
77
+ ...route,
78
+ card: { ...route.card, posture: "read_only", execution: "Read existing Sequence drafts and rendered copy. These reads do not draft, launch or send; AI drafting remains a separately quoted operation." },
79
+ recommendedTools: ["oxygen_sequences_list", "oxygen_sequences_get", "oxygen_sequences_render"],
80
+ recommendedCommands: ["sequences list", "sequences get", "sequences render"],
81
+ exactCommand: "oxygen sequences list --json",
82
+ exactMcpCall: { tool: "oxygen_sequences_list", arguments: {} },
83
+ };
84
+ }
85
+ // An explicit authored Sequence create must precede generic draft/LinkedIn
86
+ // scoring. Copy-producing columns keep the existing router and cost metadata.
87
+ if (isLinkedInCampaignIntent(normalized) && /\bsequences?\b/.test(normalized)
88
+ && /\bcreate\b/.test(normalized) && /\bdraft\b/.test(normalized)
89
+ && !/\b(?:columns?|formulas?|waterfalls?)\b/.test(normalized)) {
90
+ const route = getCapabilityRouteMatch("sequences");
91
+ if (route)
92
+ return {
93
+ ...route,
94
+ card: { ...route.card, execution: "Create an authored inert Sequence using the native schema. Creation does not generate copy with AI or start outreach; sender readiness and exact launch approval remain separate." },
95
+ recommendedTools: ["oxygen_sequences_create", "oxygen_sequences_get", "oxygen_sequences_list"],
96
+ recommendedCommands: ["sequences create", "sequences get", "sequences list"],
97
+ };
98
+ }
99
+ // Requested daily GTM work belongs to Oxygent routines. Discovery only reads
100
+ // existing sessions/templates; it never invents schedules or authorizes work.
101
+ if (/\b(?:morning briefing|nightly (?:lead )?scan|daily oxygent|daily (?:gtm )?(?:briefing|rhythm))\b/.test(normalized)
102
+ || isPrivateRecurringReportIntent(normalized)) {
103
+ const route = getCapabilityRouteMatch("agents");
104
+ if (route)
105
+ return {
106
+ ...route,
107
+ card: { ...route.card, posture: "read_only", execution: "Inspect existing Oxygent routines and the native session templates. Confirm timezone, exact resources and caps before previewing and activating; scans add review-only candidates and briefings stay in the creator's Oxygent chat." },
108
+ recommendedTools: ["oxygen_agent_session_list", "oxygen_agent_session_templates", "oxygen_agent_session_preview"],
109
+ recommendedCommands: ["agent session list", "agent session templates", "agent session preview"],
110
+ exactCommand: "oxygen agent session list --slug workspace --json",
111
+ exactMcpCall: { tool: "oxygen_agent_session_list", arguments: { slug: "workspace" } },
112
+ };
113
+ }
41
114
  const oxygents = oxygentsRoute(normalized);
42
115
  if (oxygents)
43
116
  return oxygents;
@@ -86,6 +159,28 @@ export function inferUserCapabilityRoute(query) {
86
159
  // intent is exactly what this wrapper exists to answer.
87
160
  return getCapabilityRouteMatch("connected-integrations") ?? route;
88
161
  }
162
+ function isCompanyOnboardingContextIntent(query) {
163
+ return /\bonboard\w*\b/.test(query)
164
+ && /\bcompany\b/.test(query)
165
+ && /\b(?:context|research|understand\w*|knowledge|website|icp|offer|positioning|voice)\b/.test(query)
166
+ && /\b(?:sequence|table|teammate|oxygent|icp|offer|activation|campaign)\b/.test(query);
167
+ }
168
+ /** Campaign copy belongs to Sequences; an existing conversation keeps its owner. */
169
+ function isLinkedInCampaignIntent(query) {
170
+ return /\blinkedin\b/.test(query)
171
+ && /\b(?:campaign|sequences?)\b/.test(query)
172
+ && !/\b(?:inbox|unibox|conversation|threads?|repl(?:y|ies)|publishing|posts?|comments?)\b/.test(query);
173
+ }
174
+ /** Private Oxygent/teammate phrasing may put the teammate between private and chat. */
175
+ function isPrivateRecurringReportIntent(query) {
176
+ const privateChat = /\bprivate(?:\s+(?:(?:oxygen|ai)\s+)?(?:oxygents?|teammates?))?\s+chat\b/.test(query);
177
+ const nativeTeammate = /\b(?:oxygents?|(?:oxygen|ai) teammates?)\b/.test(query)
178
+ || (/\bteammates?\b/.test(query) && !/\b(?:invite|membership|permissions?|roles?|human|colleague|coworker|slack|teams|whatsapp)\b/.test(query));
179
+ return /\b(?:nightly|daily|every night)\b/.test(query)
180
+ && /\b(?:search|scan)\b/.test(query)
181
+ && /\bmorning\b/.test(query)
182
+ && privateChat && nativeTeammate;
183
+ }
89
184
  function isSignupCompanyResearchReadIntent(query) {
90
185
  return /\bcompany research\b|\bresearch (?:the |my |our )?company\b/.test(query)
91
186
  && /\bsign\s?up\b|\bonboard\w*\b|\bworkspace setup\b|\bnew workspace\b/.test(query)
@@ -155,12 +250,15 @@ const OXYGENT_GROUP_VERBS = [
155
250
  * `commands search`, no `oxygents` command at all (blind CLI evals, 2026-10-13
156
251
  * and 2026-10-14). Only an explicit Oxygent ask moves, or one about a group
157
252
  * chat that names no other channel's (WhatsApp, Teams): a plain "agent" ask,
158
- * and a person's human "teammate", keep their owners.
253
+ * and explicit human teammate membership or channel asks, keep their owners. Bare teammates and private scheduled teammate work use the Oxygent owner.
159
254
  */
160
255
  function oxygentsRoute(normalized) {
161
256
  const groupChat = /\bgroup chats?\b/.test(normalized)
162
257
  && !/\b(?:whatsapp|teams|slack|telegram|linkedin|discord|signal|sms|e ?mail|inbox|unibox)\b/.test(normalized);
163
- if (!groupChat && !/\b(?:oxygents?|ai teammates?)\b/.test(normalized))
258
+ const nativeTeammate = /^\s*teammates?\s*$/.test(normalized)
259
+ || (/\bteammates?\b/.test(normalized) && /\b(?:routines?|schedul\w*|private chat)\b/.test(normalized)
260
+ && !/\b(?:invite|membership|permissions?|roles?|human|colleague|coworker|slack|teams|whatsapp)\b/.test(normalized));
261
+ if (!groupChat && !nativeTeammate && !/\b(?:oxygents?|ai teammates?)\b/.test(normalized))
164
262
  return null;
165
263
  const route = getCapabilityRouteMatch("agents");
166
264
  if (!route)
@@ -1,3 +1,3 @@
1
1
  export declare const OXYGEN_VERSION: string;
2
- export declare const OXYGEN_MINIMUM_CLI_VERSION = "1.948.1";
2
+ export declare const OXYGEN_MINIMUM_CLI_VERSION = "1.982.3";
3
3
  export declare const GEO_EXPORT_MINIMUM_CLI_VERSION = "1.1013.0";