@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.
- package/README.md +1 -1
- package/dist/command-manifest.js +10 -1
- package/dist/commands/admin.js +1 -1
- package/dist/commands/billing.js +13 -9
- package/dist/commands/columns.js +3 -3
- package/dist/commands/crm.js +2 -2
- package/dist/commands/deliverability.js +18 -10
- package/dist/commands/domains.js +15 -3
- package/dist/commands/mailboxes.js +22 -20
- package/dist/commands/managed-inboxes.js +1 -1
- package/dist/commands/sequences.js +34 -8
- package/dist/commands/voice.d.ts +21 -0
- package/dist/commands/voice.js +121 -6
- package/dist/customer-operation-commands.js +12 -2
- package/dist/index.js +6 -0
- package/dist/run-wait.js +10 -2
- package/node_modules/@oxygen/formula/package.json +8 -0
- package/node_modules/@oxygen/shared/dist/agent-sessions.d.ts +3 -0
- package/node_modules/@oxygen/shared/dist/agent-sessions.js +22 -1
- package/node_modules/@oxygen/shared/dist/billing-money.d.ts +31 -0
- package/node_modules/@oxygen/shared/dist/billing-money.js +60 -0
- package/node_modules/@oxygen/shared/dist/billing.d.ts +3 -0
- package/node_modules/@oxygen/shared/dist/billing.js +9 -1
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +2 -2
- package/node_modules/@oxygen/shared/dist/contracts/agent-sessions.d.ts +76 -0
- package/node_modules/@oxygen/shared/dist/contracts/agent-sessions.js +4 -2
- package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +2 -2
- package/node_modules/@oxygen/shared/dist/contracts/guided-activation.d.ts +177 -0
- package/node_modules/@oxygen/shared/dist/contracts/guided-activation.js +59 -0
- package/node_modules/@oxygen/shared/dist/contracts/operations.js +4 -3
- package/node_modules/@oxygen/shared/dist/copilot-access.js +2 -1
- package/node_modules/@oxygen/shared/dist/credit-guidance.js +8 -15
- package/node_modules/@oxygen/shared/dist/domain-authentication.d.ts +65 -0
- package/node_modules/@oxygen/shared/dist/domain-authentication.js +54 -0
- package/node_modules/@oxygen/shared/dist/email-infrastructure-policy.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/email-infrastructure-policy.js +13 -0
- package/node_modules/@oxygen/shared/dist/guided-activation.d.ts +155 -0
- package/node_modules/@oxygen/shared/dist/guided-activation.fixture.d.ts +15 -0
- package/node_modules/@oxygen/shared/dist/guided-activation.fixture.js +21 -0
- package/node_modules/@oxygen/shared/dist/guided-activation.js +38 -0
- package/node_modules/@oxygen/shared/dist/linkedin-connection-identity.d.ts +9 -0
- package/node_modules/@oxygen/shared/dist/linkedin-connection-identity.js +41 -0
- package/node_modules/@oxygen/shared/dist/managed-email-branding.js +9 -5
- package/node_modules/@oxygen/shared/dist/placement-history.d.ts +3 -0
- package/node_modules/@oxygen/shared/dist/placement-history.js +12 -0
- package/node_modules/@oxygen/shared/dist/placement-report.d.ts +7 -0
- package/node_modules/@oxygen/shared/dist/placement-report.js +34 -0
- package/node_modules/@oxygen/shared/dist/plan-limits-view.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/plan-limits-view.js +1 -0
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +1 -0
- package/node_modules/@oxygen/shared/dist/sending-limits.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/sending-limits.js +2 -10
- package/node_modules/@oxygen/shared/dist/teammates.js +2 -1
- package/node_modules/@oxygen/shared/dist/user-capability-routing.js +100 -2
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
- package/node_modules/@oxygen/shared/dist/version.js +1 -1
- package/node_modules/@oxygen/shared/dist/voice-countries.d.ts +66 -0
- package/node_modules/@oxygen/shared/dist/voice-countries.js +75 -0
- package/package.json +1 -1
|
@@ -45,6 +45,8 @@ function buildSequenceDraftBody(options) {
|
|
|
45
45
|
return {
|
|
46
46
|
goal,
|
|
47
47
|
max_credits: readPositiveNumber(options.maxCredits),
|
|
48
|
+
...(options.channel ? { channel: options.channel } : {}),
|
|
49
|
+
...(readOption(options.activationId) ? { activation_id: readOption(options.activationId) } : {}),
|
|
48
50
|
...(audience ? { audience } : {}),
|
|
49
51
|
...(table ? { source_table: table } : {}),
|
|
50
52
|
...(steps !== undefined ? { steps } : {}),
|
|
@@ -70,11 +72,21 @@ function writeSequenceDraftPreview(data) {
|
|
|
70
72
|
: [];
|
|
71
73
|
const sequence = record.sequence;
|
|
72
74
|
const lines = [];
|
|
73
|
-
|
|
75
|
+
const status = typeof sequence?.status === "string" ? sequence.status : "unknown";
|
|
76
|
+
if (record.reused === true && sequence) {
|
|
77
|
+
lines.push(`Recovered sequence "${String(sequence.name ?? "")}" (${String(sequence.slug ?? "")}), status: ${status}.`);
|
|
78
|
+
}
|
|
79
|
+
else if (record.created && sequence && status === "draft") {
|
|
74
80
|
lines.push(`Saved draft sequence "${String(sequence.name ?? "")}" (${String(sequence.slug ?? "")}).`);
|
|
75
81
|
}
|
|
82
|
+
else if (record.created && sequence) {
|
|
83
|
+
lines.push(`Saved sequence "${String(sequence.name ?? "")}" (${String(sequence.slug ?? "")}), status: ${status}.`);
|
|
84
|
+
}
|
|
76
85
|
else {
|
|
77
|
-
|
|
86
|
+
const channels = new Set(steps.flatMap(step => isRecord(step) && typeof step.channel === "string" ? [step.channel] : []));
|
|
87
|
+
const channel = channels.size === 1 ? [...channels][0] : null;
|
|
88
|
+
const label = channel === "linkedin" ? "LinkedIn " : channel === "email" ? "email " : "";
|
|
89
|
+
lines.push(`Drafted ${label}sequence (preview — not saved):`);
|
|
78
90
|
}
|
|
79
91
|
let emailNumber = 0;
|
|
80
92
|
// The subject the next threaded email continues under. A drafted follow-up
|
|
@@ -1580,6 +1592,7 @@ export function registerSequencesFamily(program, handleAsyncAction) {
|
|
|
1580
1592
|
.option("--phone-column-key <key>", "WhatsApp: row_values key holding each lead's phone number (else falls back to phone/phone_number/mobile).")
|
|
1581
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`).")
|
|
1582
1594
|
.option("--table <id>", "Source table id or slug whose rows supply {{column}} template values.")
|
|
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.")
|
|
1583
1596
|
.option("--from-table <id>", "Alias of --table: the source table id or slug this sequence draws its leads and {{column}} values from.")
|
|
1584
1597
|
.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.")
|
|
1585
1598
|
.option("--url-column <key>", "Column key holding each lead's LinkedIn URL/provider id.")
|
|
@@ -1649,6 +1662,7 @@ export function registerSequencesFamily(program, handleAsyncAction) {
|
|
|
1649
1662
|
...(senders.length > 0 ? { senders } : {}),
|
|
1650
1663
|
...(tags.length > 0 ? { tags } : {}),
|
|
1651
1664
|
...(sourceTable ? { source_table_id: sourceTable } : {}),
|
|
1665
|
+
...(readOption(options.activationId) ? { activation_id: readOption(options.activationId) } : {}),
|
|
1652
1666
|
...(options.enroll ? { enroll_from_table: true } : {}),
|
|
1653
1667
|
...(readOption(options.urlColumn) ? { linkedin_url_column_key: readOption(options.urlColumn) } : {}),
|
|
1654
1668
|
...(email ? { email } : {}),
|
|
@@ -1663,12 +1677,14 @@ export function registerSequencesFamily(program, handleAsyncAction) {
|
|
|
1663
1677
|
});
|
|
1664
1678
|
}))
|
|
1665
1679
|
.addCommand(new Command("draft")
|
|
1666
|
-
.description("Draft an email
|
|
1680
|
+
.description("Draft an email or LinkedIn sequence with AI, grounded in confirmed workspace Knowledge with cited provenance. PAID: --max-credits bounds generation. Email is the default; LinkedIn drafts invite/message actions and waits. --create with --name and --slug saves an inert draft; enrollment and launch remain separate.")
|
|
1667
1681
|
.requiredOption("--goal <goal>", "Plain-language goal for the sequence (e.g. \"book demos with RevOps leaders at Series A SaaS\"). The trusted brief the copy is written to.")
|
|
1682
|
+
.addOption(new Option("--channel <channel>", "Outreach channel; email default, linkedin for invite/message steps.").choices(["email", "linkedin"]))
|
|
1683
|
+
.option("--activation-id <id>", "Guided onboarding activation UUID; --create reuses its bound native Sequence and exact First leads Table. Creator is derived from identity.")
|
|
1668
1684
|
.option("--audience <audience>", "Who the sequence targets (e.g. \"RevOps leaders at 20-100 person B2B SaaS\"). Sharpens the Knowledge Graph retrieval.")
|
|
1669
1685
|
.option("--table <id>", "Source table id whose column keys become the {{column|fallback}} personalization tokens the copy may use.")
|
|
1670
|
-
.option("--steps <n>", "
|
|
1671
|
-
.option("--variants <n>", "Copy variants per
|
|
1686
|
+
.option("--steps <n>", "Outreach actions (1-7), excluding waits; default 4 email or 3 LinkedIn.")
|
|
1687
|
+
.option("--variants <n>", "Copy variants per outreach action (1-3); default 1.")
|
|
1672
1688
|
.requiredOption("--max-credits <n>", "Credit cap for the AI call (required — this is a paid action).")
|
|
1673
1689
|
.option("--create", "Save the draft as a DRAFT sequence (requires --name and --slug). Without this, the draft is only previewed.")
|
|
1674
1690
|
.option("--name <name>", "Human-readable sequence name (required with --create).")
|
|
@@ -1917,14 +1933,23 @@ export function registerSequencesFamily(program, handleAsyncAction) {
|
|
|
1917
1933
|
.description("Preview or start a sequence. Without --approved this is the launch check and changes nothing: it returns launch_readiness (blockers and warnings), sender_journey (each step's From, candidate inboxes and assignment; no leads needed), rendered pending-lead LinkedIn/WhatsApp copy, character counts/limits, copy blockers, per-kind scope, sender/mailbox capacity, CRM readiness and safety caps. Email copy is not rendered; inspect its saved templates and unsubscribe settings with sequences get. Live start requires --approved; --max-credits is optional. Email/WhatsApp/CRM-task journeys additionally require --max-live-sends because their external actions cost 0 Oxygen credits. --dry-run starts the sequence in simulated mode (it leaves draft), with no send, provider campaign, CRM task or credits. Live start enables the standing reply → CRM automation only if it has never been configured. Planned counts include only leads each step's channel can reach; skipped_actions_by_reason and channel_coverage show the rest.")
|
|
1918
1934
|
.argument("<sequence>", "Sequence id or slug.")
|
|
1919
1935
|
.option("--approved", "Approve and activate live. Without this flag, returns a preview only, including copy_preview rendered samples and blockers.")
|
|
1920
|
-
.option("--max-credits <n>", "Optional
|
|
1921
|
-
.option("--max-live-sends <n>", "Total live-send and CRM-write ceiling across channels, including LinkedIn invites/messages (positive integer). Required for email, WhatsApp, or crm_task steps;
|
|
1936
|
+
.option("--max-credits <n>", "Optional credit ceiling. Ordinary sequences require >0; guided LinkedIn permits 0 with a positive action ceiling and exact launch review.")
|
|
1937
|
+
.option("--max-live-sends <n>", "Total live-send and CRM-write ceiling across channels, including LinkedIn invites/messages (positive integer). Required for email, WhatsApp, or crm_task steps; guided LinkedIn binds its positive cap in launch_review.")
|
|
1938
|
+
.option("--launch-review-json <json>", "Guided onboarding: echo the exact preview.launch_review (digest, expiry, positive action ceiling and nonnegative credit cap). Changed inputs require a new preview.")
|
|
1939
|
+
.option("--launch-review-file <path>", "Read the exact launch_review object from a JSON file instead of --launch-review-json.")
|
|
1922
1940
|
.option("--dry-run", "Activate the sequence (status draft → active, startedAt set) in dry-run mode: every step advances with simulated actions — no sends, CRM writes, provider calls, or credits. This is NOT the preview: `sequences start` with no flags is the side-effect-free launch preview. A later live start (--approved) restarts those leads from the first step so they are contacted for real, except deleted source rows, a person another live campaign contacts under exclude_contacted, and older duplicates of a person.")
|
|
1923
1941
|
.option("--json", "Print a JSON envelope.")
|
|
1924
1942
|
.action(async (sequence, options) => {
|
|
1925
1943
|
await handleAsyncAction("sequences start", options, () => {
|
|
1926
|
-
const maxCredits =
|
|
1944
|
+
const maxCredits = readCreditCeilingOrZero(options.maxCredits);
|
|
1927
1945
|
const maxLiveSends = readPositiveInt(options.maxLiveSends);
|
|
1946
|
+
if (options.launchReviewJson !== undefined && options.launchReviewFile !== undefined)
|
|
1947
|
+
throw new OxygenError("invalid_request", "Choose --launch-review-json or --launch-review-file, not both.", { exitCode: 2 });
|
|
1948
|
+
const launchReview = options.launchReviewFile !== undefined
|
|
1949
|
+
? readJsonFileValue(resolve(options.launchReviewFile), "--launch-review-file")
|
|
1950
|
+
: options.launchReviewJson !== undefined ? parseJsonObject(options.launchReviewJson) : undefined;
|
|
1951
|
+
if (launchReview !== undefined && !isRecord(launchReview))
|
|
1952
|
+
throw new OxygenError("invalid_request", "The launch review must be the exact object returned by the preview.", { exitCode: 2 });
|
|
1928
1953
|
return requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}/start`, {
|
|
1929
1954
|
method: "POST",
|
|
1930
1955
|
body: {
|
|
@@ -1932,6 +1957,7 @@ export function registerSequencesFamily(program, handleAsyncAction) {
|
|
|
1932
1957
|
...(options.approved ? { approved: true } : {}),
|
|
1933
1958
|
...(maxCredits !== undefined ? { max_credits: maxCredits } : {}),
|
|
1934
1959
|
...(maxLiveSends !== undefined ? { max_live_sends: maxLiveSends } : {}),
|
|
1960
|
+
...(launchReview !== undefined ? { launch_review: launchReview } : {}),
|
|
1935
1961
|
},
|
|
1936
1962
|
});
|
|
1937
1963
|
});
|
package/dist/commands/voice.d.ts
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
import { JsonOptions } from "../cli-output.js";
|
|
1
2
|
import { ExtractedCommandActionHandler } from "../command-support.js";
|
|
2
3
|
import { Command } from "commander";
|
|
4
|
+
export type VoiceVerificationSubmitOptions = JsonOptions & {
|
|
5
|
+
country?: string;
|
|
6
|
+
email?: string;
|
|
7
|
+
field?: string[];
|
|
8
|
+
addressName?: string;
|
|
9
|
+
street?: string;
|
|
10
|
+
city?: string;
|
|
11
|
+
region?: string;
|
|
12
|
+
postalCode?: string;
|
|
13
|
+
file?: string[];
|
|
14
|
+
fileBase64?: string[];
|
|
15
|
+
documentType?: string[];
|
|
16
|
+
approved?: boolean;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* The `voice verification submit` request body. Documents are read here from
|
|
20
|
+
* local paths (or taken as base64 for a caller with no file system), so the
|
|
21
|
+
* route receives exactly the bytes the customer pointed at.
|
|
22
|
+
*/
|
|
23
|
+
export declare function buildVoiceVerificationBody(options: VoiceVerificationSubmitOptions): Promise<Record<string, unknown>>;
|
|
3
24
|
export declare function registerVoiceFamily(program: Command, handleAsyncAction: ExtractedCommandActionHandler): void;
|
package/dist/commands/voice.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { basename, extname } from "node:path";
|
|
3
|
+
import { OxygenError } from "@oxygen/shared/cli-result";
|
|
4
|
+
import { collectRepeatable, splitCommaList } from "../command-support.js";
|
|
2
5
|
import { requestOxygen } from "../http-client.js";
|
|
3
6
|
import { readOption } from "../util.js";
|
|
4
7
|
import { Command } from "commander";
|
|
@@ -13,9 +16,92 @@ function voiceNumberFilters(options) {
|
|
|
13
16
|
...(country ? { iso_country: country.toUpperCase() } : {}),
|
|
14
17
|
};
|
|
15
18
|
}
|
|
19
|
+
const DOCUMENT_CONTENT_TYPES = {
|
|
20
|
+
".pdf": "application/pdf",
|
|
21
|
+
".jpg": "image/jpeg",
|
|
22
|
+
".jpeg": "image/jpeg",
|
|
23
|
+
".png": "image/png",
|
|
24
|
+
};
|
|
25
|
+
/** `slot=value` pairs from a repeatable flag, refusing a malformed one by name. */
|
|
26
|
+
function slotPairs(values, flag) {
|
|
27
|
+
const pairs = new Map();
|
|
28
|
+
for (const raw of values ?? []) {
|
|
29
|
+
const index = raw.indexOf("=");
|
|
30
|
+
const key = index > 0 ? raw.slice(0, index).trim() : "";
|
|
31
|
+
if (!key) {
|
|
32
|
+
throw new OxygenError("invalid_request", `${flag} takes <slot>=<value>, e.g. ${flag} commercial_registrar_excerpt=...`, { exitCode: 2 });
|
|
33
|
+
}
|
|
34
|
+
pairs.set(key, raw.slice(index + 1));
|
|
35
|
+
}
|
|
36
|
+
return pairs;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The `voice verification submit` request body. Documents are read here from
|
|
40
|
+
* local paths (or taken as base64 for a caller with no file system), so the
|
|
41
|
+
* route receives exactly the bytes the customer pointed at.
|
|
42
|
+
*/
|
|
43
|
+
export async function buildVoiceVerificationBody(options) {
|
|
44
|
+
const country = readOption(options.country);
|
|
45
|
+
if (!country)
|
|
46
|
+
throw new OxygenError("invalid_request", "--country is required.", { exitCode: 2 });
|
|
47
|
+
const businessFields = Object.fromEntries(slotPairs(options.field, "--field"));
|
|
48
|
+
const files = slotPairs(options.file, "--file");
|
|
49
|
+
const inline = slotPairs(options.fileBase64, "--file-base64");
|
|
50
|
+
const types = slotPairs(options.documentType, "--document-type");
|
|
51
|
+
for (const slot of files.keys()) {
|
|
52
|
+
if (inline.has(slot)) {
|
|
53
|
+
throw new OxygenError("invalid_request", `Choose --file or --file-base64 for ${slot}, not both.`, { exitCode: 2 });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const documents = [];
|
|
57
|
+
for (const [slot, path] of files) {
|
|
58
|
+
documents.push({
|
|
59
|
+
slot,
|
|
60
|
+
file_name: basename(path),
|
|
61
|
+
content_type: DOCUMENT_CONTENT_TYPES[extname(path).toLowerCase()] ?? "application/pdf",
|
|
62
|
+
content_base64: (await readFile(path)).toString("base64"),
|
|
63
|
+
...(types.get(slot) ? { document_type: types.get(slot) } : {}),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
for (const [slot, content] of inline) {
|
|
67
|
+
documents.push({
|
|
68
|
+
slot,
|
|
69
|
+
file_name: `${slot}.pdf`,
|
|
70
|
+
content_type: "application/pdf",
|
|
71
|
+
content_base64: content,
|
|
72
|
+
...(types.get(slot) ? { document_type: types.get(slot) } : {}),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
const addressName = readOption(options.addressName);
|
|
76
|
+
const street = readOption(options.street);
|
|
77
|
+
const city = readOption(options.city);
|
|
78
|
+
const region = readOption(options.region);
|
|
79
|
+
const postalCode = readOption(options.postalCode);
|
|
80
|
+
const hasAddress = Boolean(addressName || street || city || region || postalCode);
|
|
81
|
+
const email = readOption(options.email);
|
|
82
|
+
return {
|
|
83
|
+
action: "submit",
|
|
84
|
+
iso_country: country.toUpperCase(),
|
|
85
|
+
...(email ? { email } : {}),
|
|
86
|
+
business_fields: businessFields,
|
|
87
|
+
...(hasAddress
|
|
88
|
+
? {
|
|
89
|
+
address: {
|
|
90
|
+
...(addressName ? { customer_name: addressName } : {}),
|
|
91
|
+
...(street ? { street } : {}),
|
|
92
|
+
...(city ? { city } : {}),
|
|
93
|
+
...(region ? { region } : {}),
|
|
94
|
+
...(postalCode ? { postal_code: postalCode } : {}),
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
: {}),
|
|
98
|
+
documents,
|
|
99
|
+
...(options.approved === true ? { approved: true } : {}),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
16
102
|
export function registerVoiceFamily(program, handleAsyncAction) {
|
|
17
103
|
program.addCommand(new Command("voice")
|
|
18
|
-
.description("The call channel. `tasks` works the call queue — leads waiting for a HUMAN to dial, queued by sequence call steps, CRM records, table rows, and replies. Claiming takes a lease so two reps never dial the same prospect. `numbers` manages the dialing pool: list, search, buy, tag, cap, release. Nothing here places a call: a call is live audio a rep holds in the web softphone at /sequencer/calls (a beta feature: Settings → Beta features; 5 credits per connected minute), so the terminal works everything around it. Only `numbers buy --approved` starts a charge.")
|
|
104
|
+
.description("The call channel. `tasks` works the call queue — leads waiting for a HUMAN to dial, queued by sequence call steps, CRM records, table rows, and replies. Claiming takes a lease so two reps never dial the same prospect. `numbers` manages the dialing pool: list, search, buy, tag, cap, release. `verification` is the business verification that unlocks numbers in the UK, Germany, Austria, Switzerland and France. Nothing here places a call: a call is live audio a rep holds in the web softphone at /sequencer/calls (a beta feature: Settings → Beta features; 5 credits per connected minute), so the terminal works everything around it. Only `numbers buy --approved` starts a charge.")
|
|
19
105
|
.addCommand(new Command("tasks")
|
|
20
106
|
.description("Work the call queue: queue | list | claim | complete | extend | skip | override.\n\nA call that nobody answers is dispositioned for you from the call itself, which\nalso resumes any sequence enrollment parked on it — so `complete` and `skip`\nreport already_recorded:true rather than failing when that has happened. You log\nonly what a person said.\n\nWhat blocks a dial, and what you can do about it:\n calling_window lead's local time outside 09:00-20:00 waivable per lead\n unknown_timezone no timezone on the lead, so the window\n cannot be checked (fails closed) waivable per lead\n daily_cap the chosen number hit today's dial cap waivable per lead\n suppressed on the workspace do-not-call list NEVER waivable\n destination_blocked your numbers cannot reach that country NEVER waivable\n\n`list` reports dialable, block_reason and block_detail per lead, plus\ndialable_count and blocked_by_reason for the page. Waive one with `override`.")
|
|
21
107
|
.addCommand(new Command("queue")
|
|
@@ -236,9 +322,9 @@ export function registerVoiceFamily(program, handleAsyncAction) {
|
|
|
236
322
|
}))
|
|
237
323
|
.addCommand(new Command("search")
|
|
238
324
|
.description("Find numbers you could buy, by area code or region. Free: searching buys nothing and reserves nothing, so a listed number can be gone by the time you buy it.")
|
|
239
|
-
.option("--area-code <code>", "Area code, e.g. 415
|
|
240
|
-
.option("--region <code>", "
|
|
241
|
-
.option("--country <iso>", "ISO country code (default US).
|
|
325
|
+
.option("--area-code <code>", "Area code, e.g. 415, or 30 for Berlin.")
|
|
326
|
+
.option("--region <code>", "US/CA state or province, e.g. CA.")
|
|
327
|
+
.option("--country <iso>", "ISO country code (default US). Outside US/CA, see `voice verification list`.")
|
|
242
328
|
.option("--limit <n>", "How many to return (1-30; default 10).")
|
|
243
329
|
.option("--json", "Print a JSON envelope.")
|
|
244
330
|
.action(async (options) => {
|
|
@@ -255,7 +341,7 @@ export function registerVoiceFamily(program, handleAsyncAction) {
|
|
|
255
341
|
});
|
|
256
342
|
}))
|
|
257
343
|
.addCommand(new Command("requirements")
|
|
258
|
-
.description("What a country demands before it sells a number (business details, documents
|
|
344
|
+
.description("What a country demands before it sells a number: access (instant, verification, unavailable), business details, documents, and the collapsed `plan` that `voice verification submit` takes. Free, and read live from the carrier.")
|
|
259
345
|
.option("--country <iso>", "ISO country code (default US).")
|
|
260
346
|
.option("--end-user <type>", "business (default) or individual.")
|
|
261
347
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -347,5 +433,34 @@ export function registerVoiceFamily(program, handleAsyncAction) {
|
|
|
347
433
|
},
|
|
348
434
|
});
|
|
349
435
|
});
|
|
436
|
+
})))
|
|
437
|
+
.addCommand(new Command("verification")
|
|
438
|
+
.description("Business verification (a Twilio regulatory bundle): the paperwork that unlocks local numbers in the UK, Germany, Austria, Switzerland and France. Read what a country needs with `voice numbers requirements --country <iso>` (its `plan`), submit once with `voice verification submit`, then wait about three business days for Twilio's review. Free: no credits.")
|
|
439
|
+
.addCommand(new Command("list")
|
|
440
|
+
.description("Each country's number access for this workspace. Instant countries sell now; verification countries show not_started, draft, pending-review, in-review, twilio-approved or twilio-rejected with the reviewer's reason; unavailable ones say why. Refreshes bundles in review from the carrier. Read-only.")
|
|
441
|
+
.option("--json", "Print a JSON envelope.")
|
|
442
|
+
.action(async (options) => {
|
|
443
|
+
await handleAsyncAction("voice verification list", options, () => requestOxygen("/api/cli/voice/verification"));
|
|
444
|
+
}))
|
|
445
|
+
.addCommand(new Command("submit")
|
|
446
|
+
.description("Send the workspace's business details, registered address and documents to Twilio for review. Previews by default: the preview validates everything and names what would be sent (one file can become several provider documents). Pass --approved to submit; a submitted verification cannot be edited, only resubmitted after a rejection. Costs no credits. Example: `oxygen voice verification submit --country DE --field business_name=\"Acme GmbH\" --field business_registration_number=\"HRB 123456\" ... --street \"Unter den Linden 1\" --city Berlin --region Berlin --postal-code 10117 --file commercial_registrar_excerpt=./hrb.pdf`.")
|
|
447
|
+
.requiredOption("--country <iso>", "GB, DE, AT, CH or FR.")
|
|
448
|
+
.option("--field <key=value>", "One business detail by the plan's field key (repeatable), e.g. business_name=Acme GmbH.", collectRepeatable, [])
|
|
449
|
+
.option("--email <email>", "Where Twilio sends review updates. Defaults to the representative's email field when the country has one.")
|
|
450
|
+
.option("--address-name <name>", "Name on the registered address. Defaults to business_name.")
|
|
451
|
+
.option("--street <street>", "Registered address: street and number.")
|
|
452
|
+
.option("--city <city>", "Registered address: city.")
|
|
453
|
+
.option("--region <region>", "Registered address: state or region.")
|
|
454
|
+
.option("--postal-code <code>", "Registered address: postal code.")
|
|
455
|
+
.option("--file <slot=path>", "A document for one upload slot from the plan, read from a local PDF, JPEG or PNG under 5 MB (repeatable).", collectRepeatable, [])
|
|
456
|
+
.option("--file-base64 <slot=base64>", "The same document as base64 instead of a path (repeatable). Choose --file or this per slot.", collectRepeatable, [])
|
|
457
|
+
.option("--document-type <slot=type>", "Which accepted document type the file is, when the slot accepts several (repeatable), e.g. commercial_registrar_excerpt=tax_document.", collectRepeatable, [])
|
|
458
|
+
.option("--approved", "Actually submit. Without this you get a validated preview and nothing is sent.")
|
|
459
|
+
.option("--json", "Print a JSON envelope.")
|
|
460
|
+
.action(async (options) => {
|
|
461
|
+
await handleAsyncAction("voice verification submit", options, async () => requestOxygen("/api/cli/voice/verification", {
|
|
462
|
+
method: "POST",
|
|
463
|
+
body: await buildVoiceVerificationBody(options),
|
|
464
|
+
}));
|
|
350
465
|
}))));
|
|
351
466
|
}
|
|
@@ -29,9 +29,19 @@ const GROUP_PURPOSES = {
|
|
|
29
29
|
"billing ownership": "Read or change a workspace's shared billing owner.",
|
|
30
30
|
"notetaker calendar": "Connect calendars, sync meetings and approve meeting recording.",
|
|
31
31
|
"notetaker notes": "Edit saved meeting notes.",
|
|
32
|
-
onboarding: "Save workspace discovery choices and
|
|
32
|
+
onboarding: "Save workspace discovery choices and resume guided onboarding.",
|
|
33
|
+
"onboarding activation": "Resume your dev-pilot activation and its native resources.",
|
|
33
34
|
};
|
|
34
35
|
const OPERATION_FLAGS = {
|
|
36
|
+
"agent session activate": {
|
|
37
|
+
revision: { help: "Current session revision returned by agent session preview; must match the reviewed configuration." },
|
|
38
|
+
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." },
|
|
39
|
+
},
|
|
40
|
+
"onboarding activation get": { activationId: { flag: "activation-id", help: "Exact activation ID returned by start or status." } },
|
|
41
|
+
"onboarding activation link": { activationId: { flag: "activation-id", help: "Exact activation ID returned by start or status." } },
|
|
42
|
+
"onboarding activation first-leads": { activationId: { flag: "activation-id", help: "Exact activation ID returned by start or status." } },
|
|
43
|
+
"onboarding activation routines-preview": { activationId: { flag: "activation-id", help: "Exact activation ID returned by start or status." } },
|
|
44
|
+
"onboarding activation routine-activate": { activationId: { flag: "activation-id", help: "Exact activation ID returned by start or status." } },
|
|
35
45
|
// Null and an omitted/empty label preserve an existing attachment label.
|
|
36
46
|
"crm files complete-upload": { label: { noClear: true } },
|
|
37
47
|
"tables rows remove-selection": {
|
|
@@ -78,7 +88,7 @@ export function registerCustomerOperationCommands(program, handle) {
|
|
|
78
88
|
command.option("--file <path>", "Upload bytes from this local file. Omit the base64 flag; file name and content type default from the path.");
|
|
79
89
|
for (const name of contract.required ?? []) {
|
|
80
90
|
const property = contract.properties[name];
|
|
81
|
-
const flag = `--${name.replace(/_/g, "-")}`;
|
|
91
|
+
const flag = `--${overlay[name]?.flag ?? name.replace(/_/g, "-")}`;
|
|
82
92
|
const option = command.options.find((candidate) => candidate.long === flag || candidate.long === `${flag}-json`);
|
|
83
93
|
const multipartAlternative = spec.multipart && [spec.multipart.contentField, spec.multipart.nameField, spec.multipart.typeField].includes(name);
|
|
84
94
|
if (multipartAlternative) {
|
package/dist/index.js
CHANGED
|
@@ -355,6 +355,12 @@ beginCliDiagnostics();
|
|
|
355
355
|
maybeScheduleBackgroundUpdate();
|
|
356
356
|
const program = createProgram();
|
|
357
357
|
installCommanderExitOverride(program);
|
|
358
|
+
// Compatibility dispatch stays out of help and the command catalog.
|
|
359
|
+
const legacyMailboxCommand = process.argv.indexOf("mailboxes");
|
|
360
|
+
if (legacyMailboxCommand >= 0 && process.argv[legacyMailboxCommand + 1] === "emailguard"
|
|
361
|
+
&& !process.argv.slice(2, legacyMailboxCommand).some(arg => program.commands.some(command => command.name() === arg))) {
|
|
362
|
+
process.argv[legacyMailboxCommand + 1] = "deliverability";
|
|
363
|
+
}
|
|
358
364
|
const argv = withoutHelpForUnknownCommand(program, process.argv.map((arg) => arg === "--help-full" ? "--help" : arg));
|
|
359
365
|
setCliDiagnosticOperation(commandNameForArgv(program, argv.slice(2)));
|
|
360
366
|
try {
|
package/dist/run-wait.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { OxygenError
|
|
1
|
+
import { OxygenError } from "@oxygen/shared/cli-result";
|
|
2
|
+
import { sleep } from "@oxygen/shared/timing";
|
|
2
3
|
import { readPositiveInt, readRecordString } from "./cli-values.js";
|
|
3
4
|
export async function waitForCliRun(config) {
|
|
4
5
|
const timeoutSeconds = readPositiveInt(config.requestedTimeoutSeconds)
|
|
@@ -42,6 +43,13 @@ export async function waitForCliRun(config) {
|
|
|
42
43
|
exitCode: 1,
|
|
43
44
|
});
|
|
44
45
|
}
|
|
45
|
-
|
|
46
|
+
// A timer can wake before its target. Preserve the poll interval even then,
|
|
47
|
+
// especially when the next permitted poll coincides with the deadline.
|
|
48
|
+
const nextPollAt = Math.min(deadline, Date.now() + intervalSeconds * 1000);
|
|
49
|
+
let sleepRemainingMs = nextPollAt - Date.now();
|
|
50
|
+
while (sleepRemainingMs > 0) {
|
|
51
|
+
await sleep(sleepRemainingMs);
|
|
52
|
+
sleepRemainingMs = nextPollAt - Date.now();
|
|
53
|
+
}
|
|
46
54
|
}
|
|
47
55
|
}
|
|
@@ -20,6 +20,14 @@
|
|
|
20
20
|
"types": "./dist/value-normalizers.d.ts",
|
|
21
21
|
"import": "./dist/value-normalizers.js",
|
|
22
22
|
"default": "./dist/value-normalizers.js"
|
|
23
|
+
},
|
|
24
|
+
"./evaluate": {
|
|
25
|
+
"types": "./dist/evaluate.d.ts",
|
|
26
|
+
"import": "./dist/evaluate.js"
|
|
27
|
+
},
|
|
28
|
+
"./expression": {
|
|
29
|
+
"types": "./dist/expression.d.ts",
|
|
30
|
+
"import": "./dist/expression.js"
|
|
23
31
|
}
|
|
24
32
|
},
|
|
25
33
|
"dependencies": {}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { GuidedActivationRoutine } from "./guided-activation.js";
|
|
1
2
|
import { type AgentSessionSchedule } from "./agent-session-schedule.js";
|
|
2
3
|
export type AgentSessionStatus = "draft" | "active" | "paused" | "budget_wait" | "completed" | "archived";
|
|
3
4
|
export type AgentSessionResources = {
|
|
@@ -30,10 +31,12 @@ export type AgentSessionConfig = {
|
|
|
30
31
|
* change or message it; anyone may pause it.
|
|
31
32
|
*/
|
|
32
33
|
teammate_routine?: AgentSessionTeammateRoutine | undefined;
|
|
34
|
+
guided_activation?: GuidedActivationRoutine | undefined;
|
|
33
35
|
};
|
|
34
36
|
export type AgentSessionTeammateRoutine = {
|
|
35
37
|
chat_id: string;
|
|
36
38
|
proposal_id?: string | undefined;
|
|
39
|
+
report_policy?: "changes_only" | "periodic";
|
|
37
40
|
};
|
|
38
41
|
/** The routine marker of a session's settings, or null for a general session. */
|
|
39
42
|
export declare function agentSessionTeammateRoutine(config: Pick<AgentSessionConfig, "teammate_routine">): AgentSessionTeammateRoutine | null;
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { guidedActivationEnabled } from "./guided-activation.js";
|
|
2
|
+
import { validateContract } from "./contracts/schema.js";
|
|
3
|
+
import { GUIDED_ACTIVATION_SCAN_POLICY_CONTRACT } from "./contracts/guided-activation.js";
|
|
1
4
|
import { isStaffEmail } from "./staff.js";
|
|
2
5
|
import { OxygenError } from "./cli-result.js";
|
|
3
6
|
import { WORKSPACE_EVENT_CATALOG } from "./workspace-event-catalog.js";
|
|
@@ -11,7 +14,7 @@ export function agentSessionsEnabled(env = process.env) {
|
|
|
11
14
|
return env.OXYGEN_DEPLOY_ENV === "selfhost-dev" && env.OXYGEN_AGENT_SESSIONS_ENABLED === "staff" && env.OXYGEN_AGENTS_ENABLED?.trim().toLowerCase() === "true";
|
|
12
15
|
}
|
|
13
16
|
export function agentSessionsAvailableTo(env, viewer) {
|
|
14
|
-
return agentSessionsEnabled(env) && viewer.member && viewer.oxygenRole !== "client" && isStaffEmail(viewer.email, env);
|
|
17
|
+
return agentSessionsEnabled(env) && viewer.member && viewer.oxygenRole !== "client" && (guidedActivationEnabled(env) || isStaffEmail(viewer.email, env));
|
|
15
18
|
}
|
|
16
19
|
/** Reviewed native adapters. Unknown tools (including generic dispatch and agent management) fail closed. */
|
|
17
20
|
export const AGENT_SESSION_ADAPTERS = {
|
|
@@ -118,6 +121,24 @@ export function validateAgentSessionConfig(config, requireResources = true) {
|
|
|
118
121
|
const scheduleProblem = agentSessionScheduleProblem(config.schedule);
|
|
119
122
|
if (scheduleProblem)
|
|
120
123
|
fail(scheduleProblem);
|
|
124
|
+
if (config.guided_activation !== undefined && (!config.teammate_routine
|
|
125
|
+
|| !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)
|
|
126
|
+
|| !["lead_scan", "morning_briefing"].includes(config.guided_activation.purpose))) {
|
|
127
|
+
fail("Guided routines require an activation, purpose and private Oxygent chat.");
|
|
128
|
+
}
|
|
129
|
+
if (config.guided_activation?.purpose === "lead_scan") {
|
|
130
|
+
const policy = config.guided_activation.scan_policy;
|
|
131
|
+
if (!policy || !validateContract(GUIDED_ACTIVATION_SCAN_POLICY_CONTRACT, policy, { unknown: "reject", overMax: "reject" }).ok
|
|
132
|
+
|| policy.exclude_dnc !== true || policy.exclude_contacted !== true || policy.review_only !== true
|
|
133
|
+
|| policy.max_new_leads > policy.max_examined || policy.max_provider_credits + policy.max_inference_credits > perRun) {
|
|
134
|
+
fail("Review the exact scan sources, review-only destination, exclusions and credit ceilings.");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else if (config.guided_activation?.scan_policy)
|
|
138
|
+
fail("Only a lead scan may have a sourcing policy.");
|
|
139
|
+
if (config.teammate_routine?.report_policy !== undefined && !["changes_only", "periodic"].includes(config.teammate_routine.report_policy)) {
|
|
140
|
+
fail("Choose changes_only or periodic for the routine report policy.");
|
|
141
|
+
}
|
|
121
142
|
const routine = config.teammate_routine;
|
|
122
143
|
if (routine !== undefined && (!routine || typeof routine.chat_id !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(routine.chat_id)
|
|
123
144
|
|| (routine.proposal_id !== undefined && (typeof routine.proposal_id !== "string" || !routine.proposal_id.trim() || routine.proposal_id.length > 64)))) {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Milli-credit amounts shared by quoting and ledger settlement. Rounding policies
|
|
2
|
+
* are deliberately distinct: a positive hold rounds up, a quote rounds nearest,
|
|
3
|
+
* and persisted usage keeps its historical decimal rounding. */
|
|
4
|
+
export declare const CREDIT_DECIMAL_SCALE = 1000;
|
|
5
|
+
export type SettleCreditReservationInput = {
|
|
6
|
+
description?: string | null;
|
|
7
|
+
metadata?: Record<string, unknown>;
|
|
8
|
+
};
|
|
9
|
+
export type CreditReservationSettlement = {
|
|
10
|
+
kind: "release";
|
|
11
|
+
} | {
|
|
12
|
+
kind: "capture";
|
|
13
|
+
} | {
|
|
14
|
+
kind: "partial";
|
|
15
|
+
captured: number;
|
|
16
|
+
released: number;
|
|
17
|
+
captureIdempotencyKey: string;
|
|
18
|
+
releaseIdempotencyKey: string;
|
|
19
|
+
captureDescription: string;
|
|
20
|
+
releaseDescription: string;
|
|
21
|
+
metadataJson: string;
|
|
22
|
+
};
|
|
23
|
+
export declare function resolveCreditReservationSettlement(reservation: {
|
|
24
|
+
id: string;
|
|
25
|
+
credits: number;
|
|
26
|
+
}, capturedCredits: number, input: SettleCreditReservationInput): CreditReservationSettlement;
|
|
27
|
+
export declare function normalizeCredits(value: number): number;
|
|
28
|
+
export declare function normalizeUsageCredits(value: number): number;
|
|
29
|
+
/** A free quote is zero; negative, missing and nonfinite prices remain unknown. */
|
|
30
|
+
export declare function normalizeQuotedCredits(value: number | null | undefined): number | null;
|
|
31
|
+
export declare function roundQuotedCredits(value: number): number;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { OxygenError } from "./cli-result.js";
|
|
2
|
+
/** Milli-credit amounts shared by quoting and ledger settlement. Rounding policies
|
|
3
|
+
* are deliberately distinct: a positive hold rounds up, a quote rounds nearest,
|
|
4
|
+
* and persisted usage keeps its historical decimal rounding. */
|
|
5
|
+
export const CREDIT_DECIMAL_SCALE = 1000;
|
|
6
|
+
export function resolveCreditReservationSettlement(reservation, capturedCredits, input) {
|
|
7
|
+
if (!Number.isFinite(capturedCredits) || capturedCredits < 0) {
|
|
8
|
+
throw new OxygenError("invalid_credit_amount", "Captured credit amount must be zero or positive.", {
|
|
9
|
+
details: { credits: capturedCredits },
|
|
10
|
+
exitCode: 1,
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
if (capturedCredits <= 0)
|
|
14
|
+
return { kind: "release" };
|
|
15
|
+
const captured = normalizeCredits(capturedCredits);
|
|
16
|
+
if (captured > reservation.credits) {
|
|
17
|
+
throw new OxygenError("credit_capture_exceeds_reservation", "Captured credits cannot exceed the reservation.", {
|
|
18
|
+
details: {
|
|
19
|
+
reservation_id: reservation.id,
|
|
20
|
+
reserved_credits: reservation.credits,
|
|
21
|
+
captured_credits: captured,
|
|
22
|
+
},
|
|
23
|
+
exitCode: 1,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
if (captured === reservation.credits)
|
|
27
|
+
return { kind: "capture" };
|
|
28
|
+
const released = normalizeUsageCredits(reservation.credits - captured);
|
|
29
|
+
return {
|
|
30
|
+
kind: "partial",
|
|
31
|
+
captured,
|
|
32
|
+
released,
|
|
33
|
+
captureIdempotencyKey: `reservation:${reservation.id}:settle:capture`,
|
|
34
|
+
releaseIdempotencyKey: `reservation:${reservation.id}:settle:release`,
|
|
35
|
+
captureDescription: input.description ?? "Managed credit reservation partially captured",
|
|
36
|
+
releaseDescription: input.description ?? "Managed credit reservation remainder released",
|
|
37
|
+
metadataJson: JSON.stringify(input.metadata ?? {}),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export function normalizeCredits(value) {
|
|
41
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
42
|
+
throw new OxygenError("invalid_credit_amount", "Credit amount must be positive.", {
|
|
43
|
+
details: { credits: value },
|
|
44
|
+
exitCode: 1,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return Number((Math.ceil(value * CREDIT_DECIMAL_SCALE) / CREDIT_DECIMAL_SCALE).toFixed(3));
|
|
48
|
+
}
|
|
49
|
+
export function normalizeUsageCredits(value) {
|
|
50
|
+
return Number(value.toFixed(3));
|
|
51
|
+
}
|
|
52
|
+
/** A free quote is zero; negative, missing and nonfinite prices remain unknown. */
|
|
53
|
+
export function normalizeQuotedCredits(value) {
|
|
54
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0)
|
|
55
|
+
return null;
|
|
56
|
+
return roundQuotedCredits(value);
|
|
57
|
+
}
|
|
58
|
+
export function roundQuotedCredits(value) {
|
|
59
|
+
return Math.round(value * CREDIT_DECIMAL_SCALE) / CREDIT_DECIMAL_SCALE;
|
|
60
|
+
}
|
|
@@ -159,6 +159,9 @@ export type CreditCommitmentKind = (typeof CREDIT_COMMITMENT_KINDS)[number];
|
|
|
159
159
|
* there is one definition of the price.
|
|
160
160
|
*/
|
|
161
161
|
export declare const SENDING_MAILBOX_MONTHLY_CREDITS = 100;
|
|
162
|
+
/** Sequencer + included warm-up; managed tenant hosting is billed separately. */
|
|
163
|
+
export declare function sendingMailboxMonthlyCredits(metadata?: unknown): number;
|
|
164
|
+
export declare function sendingMailboxChargeKey(metadata?: unknown): string;
|
|
162
165
|
/**
|
|
163
166
|
* Grace window after a commitment goes past_due before the account disconnects.
|
|
164
167
|
* Repricing 2026-09 decision 2.8: sending from the account pauses at once, and
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { CREDITS_PER_USD as SNAPSHOT_CREDITS_PER_USD, PRICING_PLAN_SNAPSHOT, MANAGED_INBOX_CREDITS_PER_MONTH as SNAPSHOT_MANAGED_MAILBOX_CREDITS_PER_MONTH, SENDING_MAILBOX_CREDITS_PER_MONTH as SNAPSHOT_SENDING_MAILBOX_CREDITS_PER_MONTH, } from "./pricing-snapshot.generated.js";
|
|
1
|
+
import { CREDITS_PER_USD as SNAPSHOT_CREDITS_PER_USD, PRICING_PLAN_SNAPSHOT, MANAGED_INBOX_CREDITS_PER_MONTH as SNAPSHOT_MANAGED_MAILBOX_CREDITS_PER_MONTH, SENDING_MAILBOX_CREDITS_PER_MONTH as SNAPSHOT_SENDING_MAILBOX_CREDITS_PER_MONTH, SENDING_ENTRA_MAILBOX_CREDITS_PER_MONTH, } from "./pricing-snapshot.generated.js";
|
|
2
|
+
import { isEntraMailbox } from "./email-infrastructure-policy.js";
|
|
2
3
|
import { currentSubscriptionGrantSlice, dueSubscriptionGrantSlices, } from "./billing-anchors.js";
|
|
3
4
|
import { freeTierEntitlementEnabled } from "./plan-capabilities.js";
|
|
4
5
|
import { AUTOMATION_STEP_CREDITS_REPRICING, AUTOMATION_STEPS_COUNT_TOWARD_RUN_CAP_REPRICING, isRepricingInForce, BYOK_PLATFORM_FEE_REPRICING, OXYGEN_49_BYOK_REPRICING, repricedValue, TOPUP_RATE_REPRICING, } from "./repricing.js";
|
|
@@ -222,6 +223,13 @@ export const CREDIT_COMMITMENT_KINDS = [
|
|
|
222
223
|
* there is one definition of the price.
|
|
223
224
|
*/
|
|
224
225
|
export const SENDING_MAILBOX_MONTHLY_CREDITS = SNAPSHOT_SENDING_MAILBOX_CREDITS_PER_MONTH;
|
|
226
|
+
/** Sequencer + included warm-up; managed tenant hosting is billed separately. */
|
|
227
|
+
export function sendingMailboxMonthlyCredits(metadata) {
|
|
228
|
+
return isEntraMailbox(metadata) ? SENDING_ENTRA_MAILBOX_CREDITS_PER_MONTH : SENDING_MAILBOX_MONTHLY_CREDITS;
|
|
229
|
+
}
|
|
230
|
+
export function sendingMailboxChargeKey(metadata) {
|
|
231
|
+
return isEntraMailbox(metadata) ? "commitment.sending_entra_mailbox" : "commitment.sending_mailbox";
|
|
232
|
+
}
|
|
225
233
|
/**
|
|
226
234
|
* Grace window after a commitment goes past_due before the account disconnects.
|
|
227
235
|
* Repricing 2026-09 decision 2.8: sending from the account pauses at once, and
|
|
@@ -3151,7 +3151,7 @@ function placementGuideRecommendation(query) {
|
|
|
3151
3151
|
commands: ["skills get"],
|
|
3152
3152
|
exactCommand: "oxygen skills get oxygen-deliverability --file references/placement.md --json",
|
|
3153
3153
|
exactMcpCall: { tool: "oxygen_skills_get", arguments: { name: "oxygen-deliverability", file: "references/placement.md" } },
|
|
3154
|
-
execution: "Read oxygen-deliverability, references/placement.md for placement-test guidance and references/
|
|
3154
|
+
execution: "Read oxygen-deliverability, references/placement.md for placement-test guidance and references/monitoring.md for inbox monitoring. Instruction reads do not run a test or enroll a mailbox. Keep the live preview, sender scope, credit ceiling and separate approvals for provider provisioning and test sends.",
|
|
3155
3155
|
};
|
|
3156
3156
|
}
|
|
3157
3157
|
/** OXP-43.72: pricing qualifies the lookup or scrape, rather than changing its owner. */
|
|
@@ -3336,7 +3336,7 @@ function isMailboxDeleteIntent(query) {
|
|
|
3336
3336
|
* as sending infrastructure, and a table/list/suppression context is excluded.
|
|
3337
3337
|
*/
|
|
3338
3338
|
function isDomainDnsIntent(query) {
|
|
3339
|
-
return /\
|
|
3339
|
+
return /\b(dns|spf|dkim|dmarc)\b/.test(query)
|
|
3340
3340
|
&& !/\b(tables?|columns?|rows?|crm|contacts?|leads?)\b/.test(query);
|
|
3341
3341
|
}
|
|
3342
3342
|
function isSendingDomainDeleteIntent(query) {
|