@oxygen-agent/cli 1.1014.1600 → 1.1014.1763
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-families/program.js +4 -0
- package/dist/command-manifest.js +18 -3
- package/dist/command-support.js +1 -1
- package/dist/commands/admin.js +82 -1
- package/dist/commands/billing.js +1 -1
- package/dist/commands/copilot.js +29 -7
- package/dist/commands/custom-integrations.js +13 -0
- package/dist/commands/domains.js +7 -0
- package/dist/commands/email-assets.d.ts +3 -0
- package/dist/commands/email-assets.js +78 -0
- package/dist/commands/email-templates.d.ts +3 -0
- package/dist/commands/email-templates.js +101 -0
- package/dist/commands/inbox.js +1 -1
- package/dist/commands/integrations.js +5 -1
- package/dist/commands/mailboxes.js +26 -6
- package/dist/commands/managed-inboxes.js +2 -2
- package/dist/commands/senders.js +4 -4
- package/dist/commands/sequences.js +47 -12
- package/dist/commands/subscriptions.d.ts +2 -4
- package/dist/commands/subscriptions.js +297 -51
- package/dist/credential-storage/store.d.ts +10 -0
- package/dist/credential-storage/store.js +282 -0
- package/dist/credentials.d.ts +1 -3
- package/dist/credentials.js +3 -275
- package/dist/mailbox-fleet-output.js +11 -2
- package/dist/sequence-start-at.d.ts +7 -0
- package/dist/sequence-start-at.js +21 -0
- package/dist/sequences-brand-kit-command.d.ts +28 -0
- package/dist/sequences-brand-kit-command.js +83 -0
- package/dist/sequences-content-commands.d.ts +28 -0
- package/dist/sequences-content-commands.js +176 -0
- package/dist/sequences-newsletter-commands.d.ts +6 -0
- package/dist/sequences-newsletter-commands.js +108 -0
- package/dist/sequences-test-send-command.d.ts +3 -0
- package/dist/sequences-test-send-command.js +47 -0
- package/dist/workspace-placement-commands.js +3 -2
- package/node_modules/@oxygen/formula/dist/evaluate.js +2 -4
- package/node_modules/@oxygen/formula/dist/expression.d.ts +7 -5
- package/node_modules/@oxygen/formula/dist/expression.js +26 -4
- package/node_modules/@oxygen/formula/dist/formula-functions.d.ts +6 -0
- package/node_modules/@oxygen/formula/dist/formula-functions.js +55 -0
- package/node_modules/@oxygen/shared/dist/billing.d.ts +2 -2
- package/node_modules/@oxygen/shared/dist/billing.js +5 -7
- package/node_modules/@oxygen/shared/dist/bulk-email-pacing.d.ts +105 -0
- package/node_modules/@oxygen/shared/dist/bulk-email-pacing.js +125 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +29 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +236 -18
- package/node_modules/@oxygen/shared/dist/clay-migration-auth-state.d.ts +7 -1
- package/node_modules/@oxygen/shared/dist/clay-migration-auth-state.js +6 -5
- package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +1 -0
- package/node_modules/@oxygen/shared/dist/contracts/clay-migration.d.ts +40 -1
- package/node_modules/@oxygen/shared/dist/contracts/clay-migration.js +60 -6
- package/node_modules/@oxygen/shared/dist/contracts/copilot-attachment-read.d.ts +24 -0
- package/node_modules/@oxygen/shared/dist/contracts/copilot-attachment-read.js +7 -0
- package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +2 -1
- package/node_modules/@oxygen/shared/dist/contracts/teammates.js +6 -1
- package/node_modules/@oxygen/shared/dist/copilot-journeys.generated.js +1 -1
- package/node_modules/@oxygen/shared/dist/copilot-playbooks.generated.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/copilot-playbooks.generated.js +2 -2
- package/node_modules/@oxygen/shared/dist/email-brand-kit.d.ts +108 -0
- package/node_modules/@oxygen/shared/dist/email-brand-kit.js +214 -0
- package/node_modules/@oxygen/shared/dist/email-classification-scope.d.ts +35 -0
- package/node_modules/@oxygen/shared/dist/email-classification-scope.js +40 -0
- package/node_modules/@oxygen/shared/dist/email-delivery.d.ts +10 -0
- package/node_modules/@oxygen/shared/dist/email-delivery.js +14 -0
- package/node_modules/@oxygen/shared/dist/email-documents.d.ts +61 -0
- package/node_modules/@oxygen/shared/dist/email-documents.js +143 -0
- package/node_modules/@oxygen/shared/dist/email-dsn.js +52 -10
- package/node_modules/@oxygen/shared/dist/email-html-sanitize.d.ts +23 -0
- package/node_modules/@oxygen/shared/dist/email-html-sanitize.js +96 -0
- package/node_modules/@oxygen/shared/dist/email-subscriptions.d.ts +63 -1
- package/node_modules/@oxygen/shared/dist/email-subscriptions.js +149 -1
- package/node_modules/@oxygen/shared/dist/email-unsubscribe-token.d.ts +41 -2
- package/node_modules/@oxygen/shared/dist/email-unsubscribe-token.js +50 -1
- package/node_modules/@oxygen/shared/dist/native-chat.d.ts +9 -0
- package/node_modules/@oxygen/shared/dist/native-chat.js +12 -1
- package/node_modules/@oxygen/shared/dist/newsletter-analytics.d.ts +90 -0
- package/node_modules/@oxygen/shared/dist/newsletter-analytics.js +28 -0
- package/node_modules/@oxygen/shared/dist/newsletter-test-send.d.ts +20 -0
- package/node_modules/@oxygen/shared/dist/newsletter-test-send.js +46 -0
- package/node_modules/@oxygen/shared/dist/object-storage.d.ts +11 -0
- package/node_modules/@oxygen/shared/dist/object-storage.js +12 -0
- package/node_modules/@oxygen/shared/dist/onboarding-research.d.ts +9 -2
- package/node_modules/@oxygen/shared/dist/onboarding-research.js +9 -2
- package/node_modules/@oxygen/shared/dist/plan-band.d.ts +9 -0
- package/node_modules/@oxygen/shared/dist/plan-band.js +17 -0
- package/node_modules/@oxygen/shared/dist/plan-capabilities.d.ts +4 -2
- package/node_modules/@oxygen/shared/dist/plan-capabilities.js +12 -1
- package/node_modules/@oxygen/shared/dist/plan-limits-view.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/plan-limits-view.js +3 -0
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +5 -0
- package/node_modules/@oxygen/shared/dist/product-analytics-events.d.ts +29 -0
- package/node_modules/@oxygen/shared/dist/product-analytics-events.js +29 -0
- package/node_modules/@oxygen/shared/dist/recipes.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/recipes.js +13 -0
- package/node_modules/@oxygen/shared/dist/sending-domain-faults.d.ts +21 -0
- package/node_modules/@oxygen/shared/dist/sending-domain-faults.js +53 -0
- package/node_modules/@oxygen/shared/dist/sending-domain-rollout.d.ts +30 -0
- package/node_modules/@oxygen/shared/dist/sending-domain-rollout.js +75 -0
- package/node_modules/@oxygen/shared/dist/sequence-failures.d.ts +10 -1
- package/node_modules/@oxygen/shared/dist/sequence-failures.js +68 -6
- package/node_modules/@oxygen/shared/dist/sequence-lifecycle.d.ts +34 -3
- package/node_modules/@oxygen/shared/dist/sequence-lifecycle.js +57 -4
- package/node_modules/@oxygen/shared/dist/sequence-template.d.ts +17 -0
- package/node_modules/@oxygen/shared/dist/sequence-template.js +0 -0
- package/node_modules/@oxygen/shared/dist/sequences.d.ts +41 -0
- package/node_modules/@oxygen/shared/dist/sequences.js +86 -4
- package/node_modules/@oxygen/shared/dist/ses-identity-policy.d.ts +19 -0
- package/node_modules/@oxygen/shared/dist/ses-identity-policy.js +54 -0
- package/node_modules/@oxygen/shared/dist/skill-play-routing.d.ts +22 -0
- package/node_modules/@oxygen/shared/dist/skill-play-routing.js +75 -0
- package/node_modules/@oxygen/shared/dist/skill-plays.generated.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/skill-plays.generated.js +269 -0
- package/node_modules/@oxygen/shared/dist/spend-safety.d.ts +7 -0
- package/node_modules/@oxygen/shared/dist/spend-safety.js +7 -0
- package/node_modules/@oxygen/shared/dist/teammates.js +2 -1
- package/node_modules/@oxygen/shared/dist/tool-call-arguments.d.ts +24 -0
- package/node_modules/@oxygen/shared/dist/tool-call-arguments.js +68 -0
- package/node_modules/@oxygen/shared/dist/user-capability-routing.js +15 -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/workspace-event-catalog.js +2 -2
- package/node_modules/@oxygen/shared/dist/workspace-placement.d.ts +3 -0
- package/node_modules/@oxygen/shared/dist/workspace-placement.js +11 -0
- package/package.json +2 -2
|
@@ -1,17 +1,35 @@
|
|
|
1
|
-
import { arr, enumOf, int, obj, str } from "./schema.js";
|
|
1
|
+
import { arr, bool, enumOf, int, obj, str } from "./schema.js";
|
|
2
2
|
export const CLAY_MIGRATION_ACTIONS = ["connect", "status", "discover", "read", "transfer", "checkpoint", "report", "disconnect"];
|
|
3
3
|
export const CLAY_RESOURCE_TYPES = ["workspace", "folder", "workbook", "table", "audience", "workflow", "function", "claygent", "knowledge", "schedule", "campaign"];
|
|
4
4
|
export const CLAY_RESOURCE_DISPOSITIONS = ["pending", "copied", "rebuilt", "needs_decision", "unsupported", "inaccessible", "excluded", "failed"];
|
|
5
|
+
const FULL_READ_SECTIONS = {
|
|
6
|
+
table: ["metadata", "columns", "rows", "row"], audience: ["metadata", "fields", "record_ids", "records"],
|
|
7
|
+
workflow: ["metadata", "graph"], campaign: ["metadata", "sequence"],
|
|
8
|
+
};
|
|
9
|
+
export const CLAY_GLOBAL_AUDIENCE_SCOPES = {
|
|
10
|
+
"global-people": { entity_type: "people", global: true },
|
|
11
|
+
"global-companies": { entity_type: "companies", global: true },
|
|
12
|
+
"global-deals": { entity_type: "deals", global: true },
|
|
13
|
+
"global-archived-people": { entity_type: "people", global: true, archived: true },
|
|
14
|
+
"global-archived-companies": { entity_type: "companies", global: true, archived: true },
|
|
15
|
+
"global-archived-deals": { entity_type: "deals", global: true, archived: true },
|
|
16
|
+
};
|
|
17
|
+
export function clayGlobalAudienceScope(resourceId) {
|
|
18
|
+
return Object.hasOwn(CLAY_GLOBAL_AUDIENCE_SCOPES, resourceId) ? CLAY_GLOBAL_AUDIENCE_SCOPES[resourceId] : null;
|
|
19
|
+
}
|
|
5
20
|
export const CLAY_MIGRATION_CONTRACT = obj({
|
|
6
21
|
action: enumOf(CLAY_MIGRATION_ACTIONS),
|
|
7
22
|
migration_id: str({ minLength: 1, maxLength: 200 }),
|
|
8
23
|
source_workspace_id: str({ minLength: 1, maxLength: 200 }),
|
|
9
24
|
resource_type: enumOf(CLAY_RESOURCE_TYPES),
|
|
10
|
-
resource_id: str({ minLength: 1, maxLength:
|
|
11
|
-
|
|
12
|
-
|
|
25
|
+
resource_id: str({ minLength: 1, maxLength: 256 }),
|
|
26
|
+
row_id: str({ minLength: 1, maxLength: 200, pattern: "^[a-zA-Z0-9][a-zA-Z0-9_-]*$", description: "Source row ID for table read section=row; retains structured cell fields separately from displayed values." }),
|
|
27
|
+
section: enumOf(["metadata", "columns", "rows", "row", "graph", "fields", "record_ids", "records", "sequence"]),
|
|
28
|
+
full: bool({ description: "Export one entire adapter response (8 MiB data + 1 MiB envelope); config credential redaction applies. No path/text projection. Discover omits derived resources; use report for dispositions/targets." }),
|
|
29
|
+
path: str({ minLength: 1, maxLength: 1000, description: "Dotted path or JSON pointer into a source response, e.g. cells.f_1.fields." }),
|
|
13
30
|
text_offset: int({ minimum: 0, maximum: 8388608, description: "Continue a large scalar read at next_text_offset." }),
|
|
14
31
|
entity_type: enumOf(["people", "companies", "deals"]),
|
|
32
|
+
archived: bool({ description: "Read archived Audience IDs instead of the default scope; only read section=record_ids. Transfer scope is pinned by resource_id." }),
|
|
15
33
|
record_ids: arr(int({ minimum: 1 }), { minItems: 1, maxItems: 100 }),
|
|
16
34
|
cursor: str({ maxLength: 4000 }),
|
|
17
35
|
limit: int({ minimum: 1, maximum: 100 }),
|
|
@@ -44,17 +62,53 @@ export function clayMigrationInputIssues(args) {
|
|
|
44
62
|
}
|
|
45
63
|
if (args.action === "checkpoint")
|
|
46
64
|
requireField("disposition");
|
|
65
|
+
if (args.full !== undefined && !["read", "discover"].includes(String(args.action)))
|
|
66
|
+
issues.push({ path: "full", message: "full is only available for read or discover." });
|
|
67
|
+
if (args.full === true) {
|
|
68
|
+
if (args.action === "discover") {
|
|
69
|
+
requireField("resource_type");
|
|
70
|
+
for (const key of ["section", "resource_id", "record_ids"]) {
|
|
71
|
+
if (args[key] !== undefined)
|
|
72
|
+
issues.push({ path: key, message: `${key} is unavailable for a complete catalog response; use read for an individual resource section.` });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const sections = Object.hasOwn(FULL_READ_SECTIONS, String(args.resource_type)) ? FULL_READ_SECTIONS[args.resource_type] : ["metadata"];
|
|
76
|
+
if (args.action === "read" && !sections.includes(String(args.section ?? "metadata"))) {
|
|
77
|
+
issues.push({ path: "section", message: "This resource does not expose that section for complete export." });
|
|
78
|
+
}
|
|
79
|
+
if (args.entity_type !== undefined && args.resource_type !== "audience")
|
|
80
|
+
issues.push({ path: "entity_type", message: "entity_type only identifies Audience source responses." });
|
|
81
|
+
for (const key of ["path", "text_offset"]) {
|
|
82
|
+
if (args[key] !== undefined)
|
|
83
|
+
issues.push({ path: key, message: `${key} cannot be combined with full; export the entire response or inspect a projection.` });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
47
86
|
if (args.section === "records") {
|
|
48
87
|
requireField("entity_type");
|
|
49
88
|
requireField("record_ids");
|
|
50
89
|
}
|
|
90
|
+
if (args.archived !== undefined && !(args.action === "read" && args.resource_type === "audience" && args.section === "record_ids")) {
|
|
91
|
+
issues.push({ path: "archived", message: "archived is only available for Audience read section=record_ids; transfer scope is bound to its discovered resource." });
|
|
92
|
+
}
|
|
93
|
+
const rowRead = args.action === "read" && args.resource_type === "table" && args.section === "row";
|
|
94
|
+
if (args.section === "row" && !rowRead)
|
|
95
|
+
issues.push({ path: "section", message: "section=row requires action=read and resource_type=table." });
|
|
96
|
+
if (rowRead) {
|
|
97
|
+
requireField("row_id");
|
|
98
|
+
for (const key of ["cursor", "limit", "entity_type", "record_ids", "target_table", "mapping", "source_cells_column", "source_row_id_column", "disposition", "target_kind", "target_id", "notes"]) {
|
|
99
|
+
if (args[key] !== undefined)
|
|
100
|
+
issues.push({ path: key, message: `${key} is unavailable for a single row read.` });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else if (args.row_id !== undefined)
|
|
104
|
+
issues.push({ path: "row_id", message: "row_id is only available for table read section=row." });
|
|
51
105
|
return issues;
|
|
52
106
|
}
|
|
53
107
|
export const CLAY_MIGRATION_OPERATION = {
|
|
54
108
|
id: "clay.migration", route: "/api/cli/migrations/clay", method: "POST",
|
|
55
109
|
contract: CLAY_MIGRATION_CONTRACT, inputCheck: clayMigrationInputIssues,
|
|
56
110
|
generated: { cli: false, mcp: false },
|
|
57
|
-
cli: { path: ["clay", "migration"], summary: "Migrate a Clay workspace into Oxygen: connect, discover
|
|
58
|
-
mcp: { name: "oxygen_clay_migration", title: "Oxygen: Migrate from Clay", description: "Connect to Clay with browser sign-in, discover resources,
|
|
111
|
+
cli: { path: ["clay", "migration"], summary: "Migrate a Clay workspace into Oxygen: connect, discover configuration, inspect samples or export complete responses, transfer table and Audience rows, checkpoint native rebuilds and reconcile coverage. Creates no paid runs or active schedules. Complete exports (--full): 8 MiB data + 1 MiB envelope. Full discovery omits derived resources; report retains dispositions and targets." },
|
|
112
|
+
mcp: { name: "oxygen_clay_migration", title: "Oxygen: Migrate from Clay", description: "Connect to Clay with browser sign-in, discover resources, inspect or export complete source responses, transfer table and Audience rows to existing Oxygen tables, checkpoint rebuilds and report coverage. Use native Oxygen tools to create targets and rebuild behavior. Follow cursors; report unsupported or inaccessible resources. Never activates schedules or paid runs. Full exports: 8 MiB data + 1 MiB envelope; discover omits derived resources, report retains dispositions and targets.", pack: "integrations", annotations: { openWorldHint: true, destructiveHint: false, idempotentHint: false } },
|
|
59
113
|
mutates: true, spends: false,
|
|
60
114
|
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare const COPILOT_ATTACHMENT_READ_CONTRACT: import("./schema.js").ContractObject<{
|
|
2
|
+
readonly offset: {
|
|
3
|
+
type: "integer";
|
|
4
|
+
description?: string;
|
|
5
|
+
minimum?: number;
|
|
6
|
+
maximum?: number;
|
|
7
|
+
"x-clamp"?: true;
|
|
8
|
+
};
|
|
9
|
+
readonly limit: {
|
|
10
|
+
type: "integer";
|
|
11
|
+
description?: string;
|
|
12
|
+
minimum?: number;
|
|
13
|
+
maximum?: number;
|
|
14
|
+
"x-clamp"?: true;
|
|
15
|
+
};
|
|
16
|
+
readonly version: import("./schema.js").StringProperty;
|
|
17
|
+
readonly max_chars: {
|
|
18
|
+
type: "integer";
|
|
19
|
+
description?: string;
|
|
20
|
+
minimum?: number;
|
|
21
|
+
maximum?: number;
|
|
22
|
+
"x-clamp"?: true;
|
|
23
|
+
};
|
|
24
|
+
}, never>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { int, obj, str } from "./schema.js";
|
|
2
|
+
export const COPILOT_ATTACHMENT_READ_CONTRACT = obj({
|
|
3
|
+
offset: int({ minimum: 0, maximum: 512 * 1024 * 1024, description: "Byte offset; default 0. Resume at next_offset." }),
|
|
4
|
+
limit: int({ minimum: 1, maximum: 2048, description: "Original bytes requested; default 2048. Context bounds may return fewer." }),
|
|
5
|
+
version: str({ minLength: 64, maxLength: 64, description: "Opaque file revision returned by the first read; required when resuming." }),
|
|
6
|
+
max_chars: int({ minimum: 1024, maximum: 40_000, description: "Complete serialized byte-window size." }),
|
|
7
|
+
});
|
|
@@ -36,8 +36,9 @@ export const CUSTOMER_OPERATIONS = [
|
|
|
36
36
|
operation("sequences.standby.get", "/api/cli/sequences/{sequence}/standby", "GET", "sequences standby get", "oxygen_sequences_standby_get", "sequences", "Read automatic mailbox replacement policy, ready reserve and replacements for a campaign."),
|
|
37
37
|
operation("sequences.standby.set", "/api/cli/sequences/{sequence}/standby", "PATCH", "sequences standby set", "oxygen_sequences_standby_set", "sequences", "Enable or disable automatic replacement after bounce protection pauses a mailbox. Preview first; apply with approved and returned plan. Shared reserves may change sender identity for uncontacted leads. Contacted leads keep their original inbox and hold follow-ups. 0 credits.", { enabled: bool(), approved: bool(), plan: text() }, ["enabled"]),
|
|
38
38
|
operation("chat.channel", "/api/cli/chat", "GET", "chat channel", "oxygen_chat_channel", "workspace", "Dev only: read or initialize this workspace's native support channel, persistent threads, unread counts and members; optional search matches messages.", {}, [], { contract: CHAT_CHANNEL_READ_CONTRACT, mutates: true }),
|
|
39
|
+
operation("chat.threads.list", "/api/cli/chat/threads", "GET", "chat threads list", "oxygen_chat_threads_list", "workspace", "Dev only: list up to 100 recently updated support threads in this workspace. Search retained subjects and visible messages; initialized=false means no channel. Never creates a channel or marks messages read.", {}, [], { contract: CHAT_CHANNEL_READ_CONTRACT }),
|
|
39
40
|
operation("chat.threads.create", "/api/cli/chat/threads", "POST", "chat threads create", "oxygen_chat_thread_create", "workspace", "Dev only: create a persistent native support thread; subject and body are customer-visible. No Plain or Slack transport.", {}, [], { contract: CHAT_THREAD_CREATE_CONTRACT }),
|
|
40
|
-
operation("chat.threads.get", "/api/cli/chat/threads/{thread_id}", "GET", "chat threads get", "oxygen_chat_thread_get", "workspace", "Dev only: read a native support thread, messages, members and release progress. Continue message history with the returned cursor.", {}, [], { contract: CHAT_THREAD_READ_CONTRACT }),
|
|
41
|
+
operation("chat.threads.get", "/api/cli/chat/threads/{thread_id}", "GET", "chat threads get", "oxygen_chat_thread_get", "workspace", "Dev only: read a native support thread, messages, members and release progress (none: no verified deployment, dev: verified on dev, prod: verified in production; stale means recheck). Continue message history with the returned cursor.", {}, [], { contract: CHAT_THREAD_READ_CONTRACT }),
|
|
41
42
|
operation("chat.threads.reply", "/api/cli/chat/threads/{thread_id}/messages", "POST", "chat threads reply", "oxygen_chat_thread_reply", "workspace", "Dev only: reply in a native support thread with uploaded attachment IDs and member mentions. Public by default; internal notes require staff.", {}, [], { contract: CHAT_MESSAGE_CONTRACT }),
|
|
42
43
|
operation("chat.threads.update", "/api/cli/chat/threads/{thread_id}", "PATCH", "chat threads update", "oxygen_chat_thread_update", "workspace", "Staff-only. Dev only: update native support status, assignment, urgency, fixing-agent progress or fix evidence; release status is verified separately.", {}, [], { contract: CHAT_THREAD_PATCH_CONTRACT }),
|
|
43
44
|
operation("chat.threads.read", "/api/cli/chat/threads/{thread_id}/read", "POST", "chat threads read", "oxygen_chat_thread_read", "workspace", "Dev only: mark a native support thread read through the viewed cursor; omit cursor to mark all currently visible messages.", { cursor: text() }, [], { bodyRequired: true }),
|
|
@@ -136,7 +136,12 @@ export const TEAMMATE_OPERATIONS = [
|
|
|
136
136
|
make_admin: GROUP_PEOPLE("People in it to make admins, by workspace email."),
|
|
137
137
|
remove_admin: GROUP_PEOPLE("Admins to make members again, by workspace email; a group keeps at least one admin."),
|
|
138
138
|
}),
|
|
139
|
-
teammateGroupOperation("teammate_groups.message", "group_message", "POST", "/{group_id}/messages", "Send a message in a group chat.
|
|
139
|
+
teammateGroupOperation("teammate_groups.message", "group_message", "POST", "/{group_id}/messages", "Send a message and uploaded files in a group chat. group_id is its Copilot session id: upload with oxygen copilot attach <group_id> <path>, inspect files with copilot attachments (oxygen_copilot_attachments), then bind attachment_ids here; read messages with copilot get (oxygen_copilot_get). The Oxygents it @mentions answer in order; otherwise those it is for answer, or whoever answered last in a group of only you. A mention of only people goes to them. Answers wait for earlier ones and are billed from workspace credits; paid or external actions ask the sender.", {
|
|
140
|
+
message: str({ minLength: 1, maxLength: 16000 }),
|
|
141
|
+
attachment_ids: arr(str({ pattern: "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$" }), {
|
|
142
|
+
description: "File UUIDs uploaded by you to this group session and ready to send. Omit for text only.",
|
|
143
|
+
}),
|
|
144
|
+
}, ["message"], true),
|
|
140
145
|
teammateGroupOperation("teammate_groups.read", "group_read", "POST", "/{group_id}/read", "Mark a group chat read up to its newest turn, or up to turn_index.", { turn_index: int({ minimum: 0 }) }),
|
|
141
146
|
teammateGroupOperation("teammate_groups.leave", "group_leave", "POST", "/{group_id}/leave", "Leave a group chat: it disappears for you and its Oxygents stop answering your waiting messages. If you were its last admin, whoever has been in it longest becomes one; if you were the last person, it ends."),
|
|
142
147
|
teammateGroupOperation("teammate_groups.end", "group_end", "POST", "/{group_id}/end", "End a group chat for everyone in it, as one of its admins: nobody answers in it again, and its messages stay readable."),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// GENERATED by apps/web/scripts/generate-copilot-journeys.ts from the recipe
|
|
2
|
-
// catalog (`journey:` blocks in apps/web/src/lib/
|
|
2
|
+
// catalog (`journey:` blocks in apps/web/src/lib/agent-skill-plays.ts).
|
|
3
3
|
// Do not edit by hand — run `npm run skills:metadata -w @oxygen/web`.
|
|
4
4
|
export const COPILOT_JOURNEYS_GENERATED = [
|
|
5
5
|
{
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export declare const COPILOT_PLAYBOOKS_GENERATED: readonly [{
|
|
2
2
|
readonly slug: "icp-to-account-map";
|
|
3
3
|
readonly title: "TAM sourcing";
|
|
4
|
-
readonly prompt: "Find about 25 companies that
|
|
4
|
+
readonly prompt: "Find about 25 companies that fit who we sell to, grade how well each one fits and why, and show me which kinds fit best. If I share a list, portfolio page or event list, grade that instead.";
|
|
5
5
|
}, {
|
|
6
6
|
readonly slug: "founder-posting-system";
|
|
7
7
|
readonly title: "LinkedIn content strategy";
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// GENERATED by apps/web/scripts/generate-copilot-playbooks.ts from the play
|
|
2
2
|
// catalog (featured plays: `featured_order`, `label`, `prompt` in
|
|
3
|
-
// apps/web/src/lib/
|
|
3
|
+
// apps/web/src/lib/agent-skill-plays.ts).
|
|
4
4
|
// Do not edit by hand — run `npm run skills:metadata -w @oxygen/web`.
|
|
5
5
|
export const COPILOT_PLAYBOOKS_GENERATED = [
|
|
6
6
|
{
|
|
7
7
|
slug: "icp-to-account-map",
|
|
8
8
|
title: "TAM sourcing",
|
|
9
|
-
prompt: "Find about 25 companies that
|
|
9
|
+
prompt: "Find about 25 companies that fit who we sell to, grade how well each one fits and why, and show me which kinds fit best. If I share a list, portfolio page or event list, grade that instead.",
|
|
10
10
|
},
|
|
11
11
|
{
|
|
12
12
|
slug: "founder-posting-system",
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The workspace's newsletter brand kit (tenant table ox_sequencer.email_brand_kit,
|
|
3
|
+
* migration 0559): the logo, colours, width and font newsletters are designed
|
|
4
|
+
* with, the company name and postal address every bulk email carries in its
|
|
5
|
+
* footer, social links and the preference page's title.
|
|
6
|
+
*
|
|
7
|
+
* One parser serves the `/api/cli/sequencer/brand-kit` route (web, CLI, MCP):
|
|
8
|
+
* a patch names only the fields it changes, `null` clears one (a colour, width
|
|
9
|
+
* or font falls back to its default). Pure and browser-safe.
|
|
10
|
+
*/
|
|
11
|
+
export declare const EMAIL_BRAND_FONT_FAMILIES: readonly ["system", "georgia", "arial", "helvetica", "verdana"];
|
|
12
|
+
export type EmailBrandFontFamily = typeof EMAIL_BRAND_FONT_FAMILIES[number];
|
|
13
|
+
export declare const EMAIL_BRAND_FONT_LABELS: Record<EmailBrandFontFamily, string>;
|
|
14
|
+
/** The CSS font stack each allowlisted family renders with in an email. */
|
|
15
|
+
export declare const EMAIL_BRAND_FONT_STACKS: Record<EmailBrandFontFamily, string>;
|
|
16
|
+
export declare const EMAIL_BRAND_SOCIAL_NETWORKS: readonly ["website", "linkedin", "x", "facebook", "instagram", "youtube", "tiktok", "github"];
|
|
17
|
+
export type EmailBrandSocialNetwork = typeof EMAIL_BRAND_SOCIAL_NETWORKS[number];
|
|
18
|
+
export declare const EMAIL_BRAND_COLOR_FIELDS: readonly ["background_color", "content_background_color", "text_color", "link_color", "button_color", "button_text_color"];
|
|
19
|
+
export type EmailBrandColorField = typeof EMAIL_BRAND_COLOR_FIELDS[number];
|
|
20
|
+
export declare const EMAIL_BRAND_LIMITS: {
|
|
21
|
+
readonly containerWidthMin: 480;
|
|
22
|
+
readonly containerWidthMax: 800;
|
|
23
|
+
readonly logoWidthMin: 40;
|
|
24
|
+
readonly logoWidthMax: 600;
|
|
25
|
+
readonly urlMax: 2048;
|
|
26
|
+
readonly companyNameMax: 120;
|
|
27
|
+
readonly postalAddressMax: 500;
|
|
28
|
+
readonly socialLinksMax: 10;
|
|
29
|
+
readonly preferencePageTitleMax: 120;
|
|
30
|
+
readonly preferencePageDescriptionMax: 500;
|
|
31
|
+
};
|
|
32
|
+
export type EmailBrandSocialLink = {
|
|
33
|
+
network: EmailBrandSocialNetwork;
|
|
34
|
+
url: string;
|
|
35
|
+
};
|
|
36
|
+
/** The kit as every surface reads it (snake_case wire shape), defaults applied. */
|
|
37
|
+
export type EmailBrandKit = {
|
|
38
|
+
logo_url: string | null;
|
|
39
|
+
logo_width: number | null;
|
|
40
|
+
background_color: string;
|
|
41
|
+
content_background_color: string;
|
|
42
|
+
text_color: string;
|
|
43
|
+
link_color: string;
|
|
44
|
+
button_color: string;
|
|
45
|
+
button_text_color: string;
|
|
46
|
+
container_width: number;
|
|
47
|
+
font_family: EmailBrandFontFamily;
|
|
48
|
+
company_name: string | null;
|
|
49
|
+
postal_address: string | null;
|
|
50
|
+
social_links: EmailBrandSocialLink[];
|
|
51
|
+
preference_page_title: string | null;
|
|
52
|
+
preference_page_description: string | null;
|
|
53
|
+
updated_by: string | null;
|
|
54
|
+
updated_at: string | null;
|
|
55
|
+
};
|
|
56
|
+
export declare const DEFAULT_EMAIL_BRAND_COLORS: Record<EmailBrandColorField, string>;
|
|
57
|
+
export declare const DEFAULT_EMAIL_BRAND_KIT: EmailBrandKit;
|
|
58
|
+
/** Every editable field; a patch names a subset. */
|
|
59
|
+
export declare const EMAIL_BRAND_KIT_FIELDS: readonly ["logo_url", "logo_width", "background_color", "content_background_color", "text_color", "link_color", "button_color", "button_text_color", "container_width", "font_family", "company_name", "postal_address", "social_links", "preference_page_title", "preference_page_description"];
|
|
60
|
+
export type EmailBrandKitField = typeof EMAIL_BRAND_KIT_FIELDS[number];
|
|
61
|
+
/** A validated change: a present key is written, `null` clears it. */
|
|
62
|
+
export type EmailBrandKitPatch = {
|
|
63
|
+
logo_url?: string | null;
|
|
64
|
+
logo_width?: number | null;
|
|
65
|
+
background_color?: string | null;
|
|
66
|
+
content_background_color?: string | null;
|
|
67
|
+
text_color?: string | null;
|
|
68
|
+
link_color?: string | null;
|
|
69
|
+
button_color?: string | null;
|
|
70
|
+
button_text_color?: string | null;
|
|
71
|
+
container_width?: number | null;
|
|
72
|
+
font_family?: EmailBrandFontFamily | null;
|
|
73
|
+
company_name?: string | null;
|
|
74
|
+
postal_address?: string | null;
|
|
75
|
+
social_links?: EmailBrandSocialLink[] | null;
|
|
76
|
+
preference_page_title?: string | null;
|
|
77
|
+
preference_page_description?: string | null;
|
|
78
|
+
};
|
|
79
|
+
export declare class EmailBrandKitInputError extends Error {
|
|
80
|
+
readonly field: string;
|
|
81
|
+
readonly code = "invalid_brand_kit";
|
|
82
|
+
constructor(field: string, message: string);
|
|
83
|
+
}
|
|
84
|
+
/** `#rrggbb` lowercase from `#rrggbb`, `rrggbb` or `#rgb`; throws on anything else. */
|
|
85
|
+
export declare function normalizeBrandColor(value: unknown, field: string): string;
|
|
86
|
+
/** An absolute https URL; email clients block or warn on plain http images and links. */
|
|
87
|
+
export declare function normalizeBrandUrl(value: unknown, field: string): string;
|
|
88
|
+
export declare function isEmailBrandFontFamily(value: unknown): value is EmailBrandFontFamily;
|
|
89
|
+
/**
|
|
90
|
+
* Validate a brand kit change. Only the named fields are returned; `null` (or
|
|
91
|
+
* an empty string for text) clears one. Throws `EmailBrandKitInputError` naming
|
|
92
|
+
* the field, and refuses a body that names no field.
|
|
93
|
+
*/
|
|
94
|
+
export declare function parseEmailBrandKitPatch(body: Record<string, unknown>): EmailBrandKitPatch;
|
|
95
|
+
/** A newsletter may go live only once the kit carries a postal address. */
|
|
96
|
+
export declare function brandKitHasPostalAddress(kit: Pick<EmailBrandKit, "postal_address"> | null | undefined): boolean;
|
|
97
|
+
/** The email editor's global styles, as the composer and its preview consume them. */
|
|
98
|
+
export type EmailBrandGlobalStyles = {
|
|
99
|
+
background: string;
|
|
100
|
+
contentBackground: string;
|
|
101
|
+
text: string;
|
|
102
|
+
link: string;
|
|
103
|
+
button: string;
|
|
104
|
+
buttonText: string;
|
|
105
|
+
containerWidth: number;
|
|
106
|
+
fontFamily: string;
|
|
107
|
+
};
|
|
108
|
+
export declare function brandKitGlobalStyles(kit: Partial<EmailBrandKit> | null | undefined): EmailBrandGlobalStyles;
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The workspace's newsletter brand kit (tenant table ox_sequencer.email_brand_kit,
|
|
3
|
+
* migration 0559): the logo, colours, width and font newsletters are designed
|
|
4
|
+
* with, the company name and postal address every bulk email carries in its
|
|
5
|
+
* footer, social links and the preference page's title.
|
|
6
|
+
*
|
|
7
|
+
* One parser serves the `/api/cli/sequencer/brand-kit` route (web, CLI, MCP):
|
|
8
|
+
* a patch names only the fields it changes, `null` clears one (a colour, width
|
|
9
|
+
* or font falls back to its default). Pure and browser-safe.
|
|
10
|
+
*/
|
|
11
|
+
export const EMAIL_BRAND_FONT_FAMILIES = ["system", "georgia", "arial", "helvetica", "verdana"];
|
|
12
|
+
export const EMAIL_BRAND_FONT_LABELS = {
|
|
13
|
+
system: "System sans",
|
|
14
|
+
georgia: "Georgia",
|
|
15
|
+
arial: "Arial",
|
|
16
|
+
helvetica: "Helvetica",
|
|
17
|
+
verdana: "Verdana",
|
|
18
|
+
};
|
|
19
|
+
/** The CSS font stack each allowlisted family renders with in an email. */
|
|
20
|
+
export const EMAIL_BRAND_FONT_STACKS = {
|
|
21
|
+
system: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif",
|
|
22
|
+
georgia: "Georgia, 'Times New Roman', Times, serif",
|
|
23
|
+
arial: "Arial, Helvetica, sans-serif",
|
|
24
|
+
helvetica: "Helvetica, Arial, sans-serif",
|
|
25
|
+
verdana: "Verdana, Geneva, sans-serif",
|
|
26
|
+
};
|
|
27
|
+
export const EMAIL_BRAND_SOCIAL_NETWORKS = [
|
|
28
|
+
"website", "linkedin", "x", "facebook", "instagram", "youtube", "tiktok", "github",
|
|
29
|
+
];
|
|
30
|
+
export const EMAIL_BRAND_COLOR_FIELDS = [
|
|
31
|
+
"background_color", "content_background_color", "text_color", "link_color", "button_color", "button_text_color",
|
|
32
|
+
];
|
|
33
|
+
export const EMAIL_BRAND_LIMITS = {
|
|
34
|
+
containerWidthMin: 480,
|
|
35
|
+
containerWidthMax: 800,
|
|
36
|
+
logoWidthMin: 40,
|
|
37
|
+
logoWidthMax: 600,
|
|
38
|
+
urlMax: 2048,
|
|
39
|
+
companyNameMax: 120,
|
|
40
|
+
postalAddressMax: 500,
|
|
41
|
+
socialLinksMax: 10,
|
|
42
|
+
preferencePageTitleMax: 120,
|
|
43
|
+
preferencePageDescriptionMax: 500,
|
|
44
|
+
};
|
|
45
|
+
export const DEFAULT_EMAIL_BRAND_COLORS = {
|
|
46
|
+
background_color: "#f4f4f5",
|
|
47
|
+
content_background_color: "#ffffff",
|
|
48
|
+
text_color: "#18181b",
|
|
49
|
+
link_color: "#2563eb",
|
|
50
|
+
button_color: "#18181b",
|
|
51
|
+
button_text_color: "#ffffff",
|
|
52
|
+
};
|
|
53
|
+
export const DEFAULT_EMAIL_BRAND_KIT = {
|
|
54
|
+
logo_url: null,
|
|
55
|
+
logo_width: null,
|
|
56
|
+
...DEFAULT_EMAIL_BRAND_COLORS,
|
|
57
|
+
container_width: 600,
|
|
58
|
+
font_family: "system",
|
|
59
|
+
company_name: null,
|
|
60
|
+
postal_address: null,
|
|
61
|
+
social_links: [],
|
|
62
|
+
preference_page_title: null,
|
|
63
|
+
preference_page_description: null,
|
|
64
|
+
updated_by: null,
|
|
65
|
+
updated_at: null,
|
|
66
|
+
};
|
|
67
|
+
/** Every editable field; a patch names a subset. */
|
|
68
|
+
export const EMAIL_BRAND_KIT_FIELDS = [
|
|
69
|
+
"logo_url", "logo_width", ...EMAIL_BRAND_COLOR_FIELDS, "container_width", "font_family", "company_name",
|
|
70
|
+
"postal_address", "social_links", "preference_page_title", "preference_page_description",
|
|
71
|
+
];
|
|
72
|
+
export class EmailBrandKitInputError extends Error {
|
|
73
|
+
field;
|
|
74
|
+
code = "invalid_brand_kit";
|
|
75
|
+
constructor(field, message) {
|
|
76
|
+
super(message);
|
|
77
|
+
this.field = field;
|
|
78
|
+
this.name = "EmailBrandKitInputError";
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const HEX_RE = /^#?([0-9a-f]{6}|[0-9a-f]{3})$/i;
|
|
82
|
+
/** `#rrggbb` lowercase from `#rrggbb`, `rrggbb` or `#rgb`; throws on anything else. */
|
|
83
|
+
export function normalizeBrandColor(value, field) {
|
|
84
|
+
const raw = typeof value === "string" ? value.trim() : "";
|
|
85
|
+
const match = HEX_RE.exec(raw);
|
|
86
|
+
if (!match)
|
|
87
|
+
throw new EmailBrandKitInputError(field, `${field} must be a hex colour such as #1a2b3c.`);
|
|
88
|
+
const hex = match[1].toLowerCase();
|
|
89
|
+
return `#${hex.length === 3 ? hex.split("").map((char) => char + char).join("") : hex}`;
|
|
90
|
+
}
|
|
91
|
+
/** An absolute https URL; email clients block or warn on plain http images and links. */
|
|
92
|
+
export function normalizeBrandUrl(value, field) {
|
|
93
|
+
const raw = typeof value === "string" ? value.trim() : "";
|
|
94
|
+
let parsed = null;
|
|
95
|
+
try {
|
|
96
|
+
parsed = raw ? new URL(raw) : null;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
parsed = null;
|
|
100
|
+
}
|
|
101
|
+
if (!parsed || parsed.protocol !== "https:" || !parsed.hostname || raw.length > EMAIL_BRAND_LIMITS.urlMax) {
|
|
102
|
+
throw new EmailBrandKitInputError(field, `${field} must be an https:// URL.`);
|
|
103
|
+
}
|
|
104
|
+
return parsed.toString();
|
|
105
|
+
}
|
|
106
|
+
function boundedInteger(value, field, min, max) {
|
|
107
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < min || value > max) {
|
|
108
|
+
throw new EmailBrandKitInputError(field, `${field} must be a whole number from ${min} to ${max}.`);
|
|
109
|
+
}
|
|
110
|
+
return value;
|
|
111
|
+
}
|
|
112
|
+
function boundedText(value, field, max, options = {}) {
|
|
113
|
+
if (typeof value !== "string")
|
|
114
|
+
throw new EmailBrandKitInputError(field, `${field} must be text.`);
|
|
115
|
+
const lines = value.replace(/\r\n?/g, "\n").split("\n").map((line) => line.replace(/\s+/g, " ").trim());
|
|
116
|
+
const text = options.multiline ? lines.filter(Boolean).join("\n") : lines.filter(Boolean).join(" ");
|
|
117
|
+
if (!text)
|
|
118
|
+
return null;
|
|
119
|
+
if (text.length > max)
|
|
120
|
+
throw new EmailBrandKitInputError(field, `${field} must be at most ${max} characters.`);
|
|
121
|
+
return text;
|
|
122
|
+
}
|
|
123
|
+
export function isEmailBrandFontFamily(value) {
|
|
124
|
+
return typeof value === "string" && EMAIL_BRAND_FONT_FAMILIES.includes(value);
|
|
125
|
+
}
|
|
126
|
+
function parseSocialLinks(value) {
|
|
127
|
+
if (!Array.isArray(value))
|
|
128
|
+
throw new EmailBrandKitInputError("social_links", "social_links must be a list of { network, url }.");
|
|
129
|
+
if (value.length > EMAIL_BRAND_LIMITS.socialLinksMax) {
|
|
130
|
+
throw new EmailBrandKitInputError("social_links", `At most ${EMAIL_BRAND_LIMITS.socialLinksMax} social links.`);
|
|
131
|
+
}
|
|
132
|
+
return value.map((entry, index) => {
|
|
133
|
+
const record = entry && typeof entry === "object" && !Array.isArray(entry) ? entry : {};
|
|
134
|
+
const network = typeof record.network === "string" ? record.network.trim().toLowerCase() : "";
|
|
135
|
+
if (!EMAIL_BRAND_SOCIAL_NETWORKS.includes(network)) {
|
|
136
|
+
throw new EmailBrandKitInputError(`social_links[${index}].network`, `network must be one of ${EMAIL_BRAND_SOCIAL_NETWORKS.join(", ")}.`);
|
|
137
|
+
}
|
|
138
|
+
return { network: network, url: normalizeBrandUrl(record.url, `social_links[${index}].url`) };
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Validate a brand kit change. Only the named fields are returned; `null` (or
|
|
143
|
+
* an empty string for text) clears one. Throws `EmailBrandKitInputError` naming
|
|
144
|
+
* the field, and refuses a body that names no field.
|
|
145
|
+
*/
|
|
146
|
+
export function parseEmailBrandKitPatch(body) {
|
|
147
|
+
const patch = {};
|
|
148
|
+
const has = (field) => Object.hasOwn(body, field) && body[field] !== undefined;
|
|
149
|
+
const cleared = (field) => body[field] === null || body[field] === "";
|
|
150
|
+
if (has("logo_url"))
|
|
151
|
+
patch.logo_url = cleared("logo_url") ? null : normalizeBrandUrl(body.logo_url, "logo_url");
|
|
152
|
+
if (has("logo_width")) {
|
|
153
|
+
patch.logo_width = cleared("logo_width") ? null
|
|
154
|
+
: boundedInteger(body.logo_width, "logo_width", EMAIL_BRAND_LIMITS.logoWidthMin, EMAIL_BRAND_LIMITS.logoWidthMax);
|
|
155
|
+
}
|
|
156
|
+
for (const field of EMAIL_BRAND_COLOR_FIELDS) {
|
|
157
|
+
if (has(field))
|
|
158
|
+
patch[field] = cleared(field) ? null : normalizeBrandColor(body[field], field);
|
|
159
|
+
}
|
|
160
|
+
if (has("container_width")) {
|
|
161
|
+
patch.container_width = cleared("container_width") ? null
|
|
162
|
+
: boundedInteger(body.container_width, "container_width", EMAIL_BRAND_LIMITS.containerWidthMin, EMAIL_BRAND_LIMITS.containerWidthMax);
|
|
163
|
+
}
|
|
164
|
+
if (has("font_family")) {
|
|
165
|
+
const font = typeof body.font_family === "string" ? body.font_family.trim().toLowerCase() : body.font_family;
|
|
166
|
+
if (cleared("font_family"))
|
|
167
|
+
patch.font_family = null;
|
|
168
|
+
else if (isEmailBrandFontFamily(font))
|
|
169
|
+
patch.font_family = font;
|
|
170
|
+
else
|
|
171
|
+
throw new EmailBrandKitInputError("font_family", `font_family must be one of ${EMAIL_BRAND_FONT_FAMILIES.join(", ")}.`);
|
|
172
|
+
}
|
|
173
|
+
if (has("company_name")) {
|
|
174
|
+
patch.company_name = body.company_name === null ? null : boundedText(body.company_name, "company_name", EMAIL_BRAND_LIMITS.companyNameMax);
|
|
175
|
+
}
|
|
176
|
+
if (has("postal_address")) {
|
|
177
|
+
patch.postal_address = body.postal_address === null ? null
|
|
178
|
+
: boundedText(body.postal_address, "postal_address", EMAIL_BRAND_LIMITS.postalAddressMax, { multiline: true });
|
|
179
|
+
}
|
|
180
|
+
if (has("social_links"))
|
|
181
|
+
patch.social_links = body.social_links === null ? null : parseSocialLinks(body.social_links);
|
|
182
|
+
if (has("preference_page_title")) {
|
|
183
|
+
patch.preference_page_title = body.preference_page_title === null ? null
|
|
184
|
+
: boundedText(body.preference_page_title, "preference_page_title", EMAIL_BRAND_LIMITS.preferencePageTitleMax);
|
|
185
|
+
}
|
|
186
|
+
if (has("preference_page_description")) {
|
|
187
|
+
patch.preference_page_description = body.preference_page_description === null ? null
|
|
188
|
+
: boundedText(body.preference_page_description, "preference_page_description", EMAIL_BRAND_LIMITS.preferencePageDescriptionMax, { multiline: true });
|
|
189
|
+
}
|
|
190
|
+
if (Object.keys(patch).length === 0) {
|
|
191
|
+
throw new EmailBrandKitInputError("brand_kit", `Pass at least one of ${EMAIL_BRAND_KIT_FIELDS.join(", ")}.`);
|
|
192
|
+
}
|
|
193
|
+
return patch;
|
|
194
|
+
}
|
|
195
|
+
/** A newsletter may go live only once the kit carries a postal address. */
|
|
196
|
+
export function brandKitHasPostalAddress(kit) {
|
|
197
|
+
return typeof kit?.postal_address === "string" && kit.postal_address.trim().length > 0;
|
|
198
|
+
}
|
|
199
|
+
export function brandKitGlobalStyles(kit) {
|
|
200
|
+
const merged = { ...DEFAULT_EMAIL_BRAND_KIT, ...stripNullish(kit ?? {}) };
|
|
201
|
+
return {
|
|
202
|
+
background: merged.background_color,
|
|
203
|
+
contentBackground: merged.content_background_color,
|
|
204
|
+
text: merged.text_color,
|
|
205
|
+
link: merged.link_color,
|
|
206
|
+
button: merged.button_color,
|
|
207
|
+
buttonText: merged.button_text_color,
|
|
208
|
+
containerWidth: merged.container_width,
|
|
209
|
+
fontFamily: EMAIL_BRAND_FONT_STACKS[merged.font_family] ?? EMAIL_BRAND_FONT_STACKS.system,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function stripNullish(value) {
|
|
213
|
+
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== null && entry !== undefined));
|
|
214
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which inbound email conversations of a mailbox the paid inbox classifier
|
|
3
|
+
* triages (Tim, 2026-10-21, T-316). `replies`: Sequence threads and threads the
|
|
4
|
+
* workspace wrote in; every other conversation (newsletters, notifications,
|
|
5
|
+
* cold pitches) stays in Unibox unclassified and costs nothing. `all`: every
|
|
6
|
+
* inbound conversation.
|
|
7
|
+
*
|
|
8
|
+
* Without a choice, a Google mailbox the customer added themselves defaults to
|
|
9
|
+
* `replies`: most of its mail is not outreach, and its poll reads Sent mail, so
|
|
10
|
+
* a thread they wrote in is recognisable. Every other mailbox defaults to `all`:
|
|
11
|
+
* Microsoft's poll reads only the Inbox, and a reply to a campaign sent from
|
|
12
|
+
* another platform has no outbound message here to be recognised by.
|
|
13
|
+
* tenant-db's `emailClassificationInScopeSql` is the SQL twin of this default.
|
|
14
|
+
*/
|
|
15
|
+
export declare const EMAIL_CLASSIFICATION_SCOPES: readonly ["replies", "all"];
|
|
16
|
+
export type EmailClassificationScope = (typeof EMAIL_CLASSIFICATION_SCOPES)[number];
|
|
17
|
+
/** Mailbox metadata key holding an explicit choice. */
|
|
18
|
+
export declare const EMAIL_CLASSIFICATION_SCOPE_KEY = "classification_scope";
|
|
19
|
+
/** The `mailbox_origin` stamp of a mailbox the customer added themselves. */
|
|
20
|
+
export declare const SELF_ADDED_MAILBOX_ORIGIN = "manual";
|
|
21
|
+
/** The provider whose self-added mailboxes default to `replies`. */
|
|
22
|
+
export declare const SELF_ADDED_REPLIES_PROVIDER = "google";
|
|
23
|
+
export type EmailClassificationScopeView = {
|
|
24
|
+
scope: EmailClassificationScope;
|
|
25
|
+
/** `setting` when someone chose it; `default` when it follows the mailbox kind. */
|
|
26
|
+
source: "setting" | "default";
|
|
27
|
+
};
|
|
28
|
+
export declare function isEmailClassificationScope(value: unknown): value is EmailClassificationScope;
|
|
29
|
+
/** True for a connected full-mail inbox: its analysis is included in the monthly connection, so no scope applies. */
|
|
30
|
+
export declare function isMailboxSyncMetadata(metadata: Record<string, unknown> | null | undefined): boolean;
|
|
31
|
+
/** The scope a mailbox's inbound mail is classified under, or null for a connected full-mail inbox. */
|
|
32
|
+
export declare function resolveEmailClassificationScope(mailbox: {
|
|
33
|
+
provider: string;
|
|
34
|
+
metadata: Record<string, unknown> | null | undefined;
|
|
35
|
+
}): EmailClassificationScopeView | null;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which inbound email conversations of a mailbox the paid inbox classifier
|
|
3
|
+
* triages (Tim, 2026-10-21, T-316). `replies`: Sequence threads and threads the
|
|
4
|
+
* workspace wrote in; every other conversation (newsletters, notifications,
|
|
5
|
+
* cold pitches) stays in Unibox unclassified and costs nothing. `all`: every
|
|
6
|
+
* inbound conversation.
|
|
7
|
+
*
|
|
8
|
+
* Without a choice, a Google mailbox the customer added themselves defaults to
|
|
9
|
+
* `replies`: most of its mail is not outreach, and its poll reads Sent mail, so
|
|
10
|
+
* a thread they wrote in is recognisable. Every other mailbox defaults to `all`:
|
|
11
|
+
* Microsoft's poll reads only the Inbox, and a reply to a campaign sent from
|
|
12
|
+
* another platform has no outbound message here to be recognised by.
|
|
13
|
+
* tenant-db's `emailClassificationInScopeSql` is the SQL twin of this default.
|
|
14
|
+
*/
|
|
15
|
+
export const EMAIL_CLASSIFICATION_SCOPES = ["replies", "all"];
|
|
16
|
+
/** Mailbox metadata key holding an explicit choice. */
|
|
17
|
+
export const EMAIL_CLASSIFICATION_SCOPE_KEY = "classification_scope";
|
|
18
|
+
/** The `mailbox_origin` stamp of a mailbox the customer added themselves. */
|
|
19
|
+
export const SELF_ADDED_MAILBOX_ORIGIN = "manual";
|
|
20
|
+
/** The provider whose self-added mailboxes default to `replies`. */
|
|
21
|
+
export const SELF_ADDED_REPLIES_PROVIDER = "google";
|
|
22
|
+
export function isEmailClassificationScope(value) {
|
|
23
|
+
return typeof value === "string" && EMAIL_CLASSIFICATION_SCOPES.includes(value);
|
|
24
|
+
}
|
|
25
|
+
/** True for a connected full-mail inbox: its analysis is included in the monthly connection, so no scope applies. */
|
|
26
|
+
export function isMailboxSyncMetadata(metadata) {
|
|
27
|
+
return metadata != null && Object.hasOwn(metadata, "mailbox_sync");
|
|
28
|
+
}
|
|
29
|
+
/** The scope a mailbox's inbound mail is classified under, or null for a connected full-mail inbox. */
|
|
30
|
+
export function resolveEmailClassificationScope(mailbox) {
|
|
31
|
+
const { metadata } = mailbox;
|
|
32
|
+
if (isMailboxSyncMetadata(metadata))
|
|
33
|
+
return null;
|
|
34
|
+
const stored = metadata?.[EMAIL_CLASSIFICATION_SCOPE_KEY];
|
|
35
|
+
if (isEmailClassificationScope(stored))
|
|
36
|
+
return { scope: stored, source: "setting" };
|
|
37
|
+
const selfAddedGoogle = metadata?.mailbox_origin === SELF_ADDED_MAILBOX_ORIGIN
|
|
38
|
+
&& mailbox.provider === SELF_ADDED_REPLIES_PROVIDER;
|
|
39
|
+
return { scope: selfAddedGoogle ? "replies" : "all", source: "default" };
|
|
40
|
+
}
|
|
@@ -25,6 +25,14 @@ export type SendingDomainEmailDelivery = {
|
|
|
25
25
|
export type SequenceEmailDelivery = {
|
|
26
26
|
mode: "inboxes";
|
|
27
27
|
} | SendingDomainEmailDelivery;
|
|
28
|
+
/**
|
|
29
|
+
* The opt-out line of a cold email sent as plain text, when its Sequence turned the
|
|
30
|
+
* visible unsubscribe on without writing its own `reply_to_stop_text`. Plain text
|
|
31
|
+
* shows a line, never the signed URL (Plain T-317). Any reply stops the lead's
|
|
32
|
+
* enrollment in that Sequence, and opt-out wording ("not interested") puts the
|
|
33
|
+
* address on do-not-contact unless the workspace lowered `email_optout_phrase`.
|
|
34
|
+
*/
|
|
35
|
+
export declare const DEFAULT_PLAIN_TEXT_OPT_OUT = "Not interested? Just reply and I won't follow up.";
|
|
28
36
|
export declare class EmailDeliverySettingError extends Error {
|
|
29
37
|
readonly field: string;
|
|
30
38
|
constructor(field: string, message: string);
|
|
@@ -120,6 +128,8 @@ export declare function sendingDomainRemainingCapacity(input: {
|
|
|
120
128
|
export declare function bulkSendIntervalMs(maxSendRate: number): number;
|
|
121
129
|
/** Estimated AWS cost of a send count, USD rounded up to the cent. */
|
|
122
130
|
export declare function estimateSesCostUsd(sends: number): number;
|
|
131
|
+
/** Credits for newsletter emails sent through a sending domain (`send.bulk_email`, 5x AWS). */
|
|
132
|
+
export declare function bulkEmailCredits(emails: number): number;
|
|
123
133
|
export declare const RELEASE_SLICE_FRACTION = 0.05;
|
|
124
134
|
export declare const RELEASE_SLICE_MINIMUM = 50;
|
|
125
135
|
export declare const RELEASE_SLICE_HOLD_MS: number;
|