@oxygen-agent/cli 1.1014.1696 → 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.
Files changed (96) hide show
  1. package/README.md +1 -1
  2. package/dist/command-families/program.js +4 -0
  3. package/dist/command-manifest.js +14 -2
  4. package/dist/command-support.js +1 -1
  5. package/dist/commands/admin.js +82 -1
  6. package/dist/commands/billing.js +1 -1
  7. package/dist/commands/copilot.js +29 -7
  8. package/dist/commands/domains.js +7 -0
  9. package/dist/commands/email-assets.d.ts +3 -0
  10. package/dist/commands/email-assets.js +78 -0
  11. package/dist/commands/email-templates.d.ts +3 -0
  12. package/dist/commands/email-templates.js +101 -0
  13. package/dist/commands/sequences.js +47 -12
  14. package/dist/commands/subscriptions.d.ts +2 -4
  15. package/dist/commands/subscriptions.js +297 -51
  16. package/dist/sequence-start-at.d.ts +7 -0
  17. package/dist/sequence-start-at.js +21 -0
  18. package/dist/sequences-brand-kit-command.d.ts +28 -0
  19. package/dist/sequences-brand-kit-command.js +83 -0
  20. package/dist/sequences-content-commands.d.ts +28 -0
  21. package/dist/sequences-content-commands.js +176 -0
  22. package/dist/sequences-newsletter-commands.d.ts +6 -0
  23. package/dist/sequences-newsletter-commands.js +108 -0
  24. package/dist/sequences-test-send-command.d.ts +3 -0
  25. package/dist/sequences-test-send-command.js +47 -0
  26. package/node_modules/@oxygen/formula/dist/evaluate.js +2 -4
  27. package/node_modules/@oxygen/formula/dist/expression.d.ts +7 -5
  28. package/node_modules/@oxygen/formula/dist/expression.js +26 -4
  29. package/node_modules/@oxygen/formula/dist/formula-functions.d.ts +6 -0
  30. package/node_modules/@oxygen/formula/dist/formula-functions.js +55 -0
  31. package/node_modules/@oxygen/shared/dist/billing.d.ts +2 -2
  32. package/node_modules/@oxygen/shared/dist/billing.js +5 -7
  33. package/node_modules/@oxygen/shared/dist/bulk-email-pacing.d.ts +105 -0
  34. package/node_modules/@oxygen/shared/dist/bulk-email-pacing.js +125 -0
  35. package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +29 -0
  36. package/node_modules/@oxygen/shared/dist/capability-discovery.js +174 -10
  37. package/node_modules/@oxygen/shared/dist/contracts/clay-migration.d.ts +1 -0
  38. package/node_modules/@oxygen/shared/dist/contracts/clay-migration.js +28 -2
  39. package/node_modules/@oxygen/shared/dist/contracts/copilot-attachment-read.d.ts +24 -0
  40. package/node_modules/@oxygen/shared/dist/contracts/copilot-attachment-read.js +7 -0
  41. package/node_modules/@oxygen/shared/dist/contracts/teammates.js +6 -1
  42. package/node_modules/@oxygen/shared/dist/email-brand-kit.d.ts +108 -0
  43. package/node_modules/@oxygen/shared/dist/email-brand-kit.js +214 -0
  44. package/node_modules/@oxygen/shared/dist/email-delivery.d.ts +10 -0
  45. package/node_modules/@oxygen/shared/dist/email-delivery.js +14 -0
  46. package/node_modules/@oxygen/shared/dist/email-documents.d.ts +61 -0
  47. package/node_modules/@oxygen/shared/dist/email-documents.js +143 -0
  48. package/node_modules/@oxygen/shared/dist/email-dsn.js +52 -10
  49. package/node_modules/@oxygen/shared/dist/email-html-sanitize.d.ts +23 -0
  50. package/node_modules/@oxygen/shared/dist/email-html-sanitize.js +96 -0
  51. package/node_modules/@oxygen/shared/dist/email-subscriptions.d.ts +63 -1
  52. package/node_modules/@oxygen/shared/dist/email-subscriptions.js +149 -1
  53. package/node_modules/@oxygen/shared/dist/email-unsubscribe-token.d.ts +41 -2
  54. package/node_modules/@oxygen/shared/dist/email-unsubscribe-token.js +50 -1
  55. package/node_modules/@oxygen/shared/dist/newsletter-analytics.d.ts +90 -0
  56. package/node_modules/@oxygen/shared/dist/newsletter-analytics.js +28 -0
  57. package/node_modules/@oxygen/shared/dist/newsletter-test-send.d.ts +20 -0
  58. package/node_modules/@oxygen/shared/dist/newsletter-test-send.js +46 -0
  59. package/node_modules/@oxygen/shared/dist/object-storage.d.ts +11 -0
  60. package/node_modules/@oxygen/shared/dist/object-storage.js +12 -0
  61. package/node_modules/@oxygen/shared/dist/plan-band.d.ts +9 -0
  62. package/node_modules/@oxygen/shared/dist/plan-band.js +17 -0
  63. package/node_modules/@oxygen/shared/dist/plan-capabilities.d.ts +4 -2
  64. package/node_modules/@oxygen/shared/dist/plan-capabilities.js +12 -1
  65. package/node_modules/@oxygen/shared/dist/plan-limits-view.d.ts +2 -0
  66. package/node_modules/@oxygen/shared/dist/plan-limits-view.js +3 -0
  67. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +5 -0
  68. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +5 -0
  69. package/node_modules/@oxygen/shared/dist/product-analytics-events.d.ts +29 -0
  70. package/node_modules/@oxygen/shared/dist/product-analytics-events.js +29 -0
  71. package/node_modules/@oxygen/shared/dist/sending-domain-faults.d.ts +21 -0
  72. package/node_modules/@oxygen/shared/dist/sending-domain-faults.js +53 -0
  73. package/node_modules/@oxygen/shared/dist/sending-domain-rollout.d.ts +24 -1
  74. package/node_modules/@oxygen/shared/dist/sending-domain-rollout.js +51 -4
  75. package/node_modules/@oxygen/shared/dist/sequence-failures.d.ts +10 -1
  76. package/node_modules/@oxygen/shared/dist/sequence-failures.js +68 -6
  77. package/node_modules/@oxygen/shared/dist/sequence-lifecycle.d.ts +34 -3
  78. package/node_modules/@oxygen/shared/dist/sequence-lifecycle.js +57 -4
  79. package/node_modules/@oxygen/shared/dist/sequence-template.d.ts +17 -0
  80. package/node_modules/@oxygen/shared/dist/sequence-template.js +0 -0
  81. package/node_modules/@oxygen/shared/dist/sequences.d.ts +41 -0
  82. package/node_modules/@oxygen/shared/dist/sequences.js +86 -4
  83. package/node_modules/@oxygen/shared/dist/ses-identity-policy.d.ts +19 -0
  84. package/node_modules/@oxygen/shared/dist/ses-identity-policy.js +54 -0
  85. package/node_modules/@oxygen/shared/dist/skill-play-routing.d.ts +22 -0
  86. package/node_modules/@oxygen/shared/dist/skill-play-routing.js +75 -0
  87. package/node_modules/@oxygen/shared/dist/skill-plays.generated.d.ts +5 -0
  88. package/node_modules/@oxygen/shared/dist/skill-plays.generated.js +269 -0
  89. package/node_modules/@oxygen/shared/dist/spend-safety.d.ts +7 -0
  90. package/node_modules/@oxygen/shared/dist/spend-safety.js +7 -0
  91. package/node_modules/@oxygen/shared/dist/teammates.js +2 -1
  92. package/node_modules/@oxygen/shared/dist/user-capability-routing.js +15 -1
  93. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  94. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  95. package/node_modules/@oxygen/shared/dist/workspace-event-catalog.js +2 -2
  96. package/package.json +2 -2
@@ -1204,6 +1204,61 @@ const SPECS = [
1204
1204
  }),
1205
1205
  ];
1206
1206
  export const FORMULA_FUNCTION_REGISTRY = new Map(SPECS.map((entry) => [entry.name, entry]));
1207
+ /**
1208
+ * Names authors bring from spreadsheets, SQL and JavaScript, mapped to the
1209
+ * function here that does that job. Keys are normalized (lowercase, no `_`).
1210
+ */
1211
+ const FORMULA_FUNCTION_ALIASES = {
1212
+ tonumber: "number", parseint: "number", parsefloat: "number", int: "number", float: "number", value: "number", cast: "number", numeric: "number",
1213
+ tostring: "text", str: "text", string: "text",
1214
+ regexpreplace: "regex_replace", regexreplace: "regex_replace", regexpextract: "regex_extract", regexpsubstr: "regex_extract",
1215
+ regexextract: "regex_extract", regexpmatch: "regex_match", regexplike: "regex_match", regexmatch: "regex_match", match: "regex_match",
1216
+ splitpart: "split", indexof: "contains", includes: "contains", instr: "contains", strpos: "contains", position: "contains", find: "contains", search: "contains",
1217
+ substr: "substring", mid: "substring", len: "length", charlength: "length", lengthb: "length",
1218
+ tolowercase: "lower", lcase: "lower", touppercase: "upper", ucase: "upper", proper: "title_case", initcap: "title_case",
1219
+ iif: "if", ifs: "switch", case: "switch", ifnull: "coalesce", nvl: "coalesce", isnull: "is_blank", isblank: "is_blank", isempty: "is_blank",
1220
+ datediff: "date_diff", dateadd: "date_add", datevalue: "date_parse", todate: "date_parse", formatdate: "date_format", textjoin: "join",
1221
+ startswith: "starts_with", endswith: "ends_with", average: "avg", mean: "avg", size: "count", domain: "email_domain",
1222
+ };
1223
+ /**
1224
+ * The refusal for a call to a function the formula language does not have:
1225
+ * the likely intended function (`did_you_mean`) and every supported name, so the
1226
+ * author corrects the call without another lookup.
1227
+ */
1228
+ export function unsupportedFormulaFunctionError(name) {
1229
+ const suggestions = suggestFormulaFunctions(name);
1230
+ const supported = [...FORMULA_FUNCTION_REGISTRY.keys()].sort();
1231
+ return formulaExpressionError(suggestions.length > 0
1232
+ ? `Unsupported formula function ${name}; did you mean ${suggestions.join(" or ")}?`
1233
+ : `Unsupported formula function ${name}; use one of details.supported_functions.`, {
1234
+ function: name,
1235
+ ...(suggestions.length > 0 ? { did_you_mean: suggestions } : {}),
1236
+ supported_functions: supported,
1237
+ next_step: "Read signatures and examples with `oxygen formulas functions` (MCP oxygen_formulas_list).",
1238
+ });
1239
+ }
1240
+ function suggestFormulaFunctions(name) {
1241
+ const key = name.toLowerCase().replace(/_/g, "");
1242
+ const alias = FORMULA_FUNCTION_ALIASES[key];
1243
+ if (alias)
1244
+ return [alias];
1245
+ const ranked = [...FORMULA_FUNCTION_REGISTRY.keys()]
1246
+ .map((candidate) => ({ candidate, distance: editDistance(key, candidate.replace(/_/g, "")) }))
1247
+ .filter((entry) => entry.distance <= Math.max(1, Math.floor(key.length / 4)))
1248
+ .sort((a, b) => a.distance - b.distance || a.candidate.localeCompare(b.candidate));
1249
+ return ranked.slice(0, 2).map((entry) => entry.candidate);
1250
+ }
1251
+ function editDistance(a, b) {
1252
+ let previous = Array.from({ length: b.length + 1 }, (_, index) => index);
1253
+ for (let i = 1; i <= a.length; i += 1) {
1254
+ const current = [i];
1255
+ for (let j = 1; j <= b.length; j += 1) {
1256
+ current[j] = Math.min(previous[j] + 1, current[j - 1] + 1, previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
1257
+ }
1258
+ previous = current;
1259
+ }
1260
+ return previous[b.length];
1261
+ }
1207
1262
  export const FORMULA_FUNCTION_CATEGORIES = [
1208
1263
  "logic",
1209
1264
  "string",
@@ -64,14 +64,14 @@ export declare const CREDITS_PER_USD = 100;
64
64
  */
65
65
  export declare function creditTopupUsdCentsPer100(options?: RepricingOptions): number;
66
66
  export declare const CREDIT_TOPUP_MIN_CREDITS = 800;
67
- export declare const CREDIT_TOPUP_MAX_CREDITS = 100000;
67
+ export declare const CREDIT_TOPUP_MAX_CREDITS = 769200;
68
68
  export declare const CREDIT_TOPUP_STEP_CREDITS = 100;
69
69
  export declare const CREDIT_TOPUP_DEFAULT_CREDITS = 2000;
70
70
  export declare function isValidCreditTopupCredits(credits: number): boolean;
71
71
  /**
72
72
  * A shortfall expressed as an amount `oxygen billing topup` will actually SELL:
73
73
  * rounded up onto CREDIT_TOPUP_STEP_CREDITS (100) and clamped into the purchasable
74
- * [CREDIT_TOPUP_MIN_CREDITS .. CREDIT_TOPUP_MAX_CREDITS] band (800 .. 100,000). A next_action (or an alert email) naming a number the
74
+ * [CREDIT_TOPUP_MIN_CREDITS .. CREDIT_TOPUP_MAX_CREDITS] band. A next_action (or an alert email) naming a number the
75
75
  * top-up route refuses with `invalid_topup_amount` is not a next action, it is a
76
76
  * second dead end during the incident it exists to end.
77
77
  *
@@ -63,13 +63,11 @@ export const CREDITS_PER_USD = SNAPSHOT_CREDITS_PER_USD;
63
63
  export function creditTopupUsdCentsPer100(options = {}) {
64
64
  return repricedValue(TOPUP_RATE_REPRICING, options);
65
65
  }
66
- // On-demand purchases run from 800 to 100,000 credits in any 100-credit
67
- // increment ($10.00 to $1,250 at $1.25 per 100; $10.40 to $1,300 at $1.30). The
68
- // ceiling is a single-checkout guard rail, not a spend cap: a workspace that
69
- // needs more tops up again (purchased credits never expire). The range and step
70
- // are unchanged by the 2026-09 repricing (proposal P-7).
66
+ // On-demand purchases run in 100-credit increments up to $10,000 before tax.
67
+ // At $1.30 per 100, the largest whole increment is 769,200 credits ($9,999.60).
68
+ // The ceiling is per checkout; purchased credits never expire.
71
69
  export const CREDIT_TOPUP_MIN_CREDITS = 800;
72
- export const CREDIT_TOPUP_MAX_CREDITS = 100_000;
70
+ export const CREDIT_TOPUP_MAX_CREDITS = 769_200;
73
71
  export const CREDIT_TOPUP_STEP_CREDITS = 100;
74
72
  export const CREDIT_TOPUP_DEFAULT_CREDITS = 2_000;
75
73
  export function isValidCreditTopupCredits(credits) {
@@ -81,7 +79,7 @@ export function isValidCreditTopupCredits(credits) {
81
79
  /**
82
80
  * A shortfall expressed as an amount `oxygen billing topup` will actually SELL:
83
81
  * rounded up onto CREDIT_TOPUP_STEP_CREDITS (100) and clamped into the purchasable
84
- * [CREDIT_TOPUP_MIN_CREDITS .. CREDIT_TOPUP_MAX_CREDITS] band (800 .. 100,000). A next_action (or an alert email) naming a number the
82
+ * [CREDIT_TOPUP_MIN_CREDITS .. CREDIT_TOPUP_MAX_CREDITS] band. A next_action (or an alert email) naming a number the
85
83
  * top-up route refuses with `invalid_topup_amount` is not a next action, it is a
86
84
  * second dead end during the incident it exists to end.
87
85
  *
@@ -0,0 +1,105 @@
1
+ export declare const BULK_EMAIL_PACING: {
2
+ /** A send this small always goes at once, even for a brand-new workspace. */
3
+ readonly quickEmails: 200;
4
+ /** Days of warm-up after the workspace's first newsletter send. */
5
+ readonly warmupDays: 30;
6
+ /** Look-back window for the proven volume. */
7
+ readonly provenDays: 60;
8
+ /** A send counts towards the proven volume this long after it went out, once its bounces have come in. */
9
+ readonly provenSettleHours: 24;
10
+ /** Sends still queued, and sends of this many past hours, count against a new send. */
11
+ readonly recentHours: 24;
12
+ /** Multiplier on the proven volume: `first` on warm-up day 1, rising linearly to `last` on the last day and after. */
13
+ readonly growth: {
14
+ readonly first: 2;
15
+ readonly last: 5;
16
+ };
17
+ /** A send over the allowance is spread over at least this many hours … */
18
+ readonly minSpreadHours: 4;
19
+ /** … plus this many for each doubling past the allowance … */
20
+ readonly hoursPerDoubling: 4;
21
+ /** … and never more than this. */
22
+ readonly maxSpreadHours: 72;
23
+ };
24
+ /**
25
+ * Workspace-level bounce and complaint limits, each with a minimum count so one
26
+ * complaint or a few bounces never stop a workspace. `pause` stops the workspace
27
+ * until someone resumes it; `block` is abuse and only staff unblock it. The rates
28
+ * match the release slice (`email-delivery.ts`): 0.3% is Gmail's spam line, 5%
29
+ * where SES opens a review.
30
+ */
31
+ export declare const BULK_EMAIL_WORKSPACE_LIMITS: {
32
+ readonly pause: {
33
+ readonly bounceRate: 0.02;
34
+ readonly minBounces: 5;
35
+ readonly complaintRate: 0.0008;
36
+ readonly minComplaints: 2;
37
+ };
38
+ readonly block: {
39
+ readonly bounceRate: 0.05;
40
+ readonly minBounces: 10;
41
+ readonly complaintRate: 0.003;
42
+ readonly minComplaints: 3;
43
+ };
44
+ };
45
+ export type BulkEmailWorkspaceVerdict = "ok" | "pause" | "block";
46
+ /** The workspace's verdict for a window of sends, hard bounces and complaints. */
47
+ export declare function bulkEmailWorkspaceVerdict(input: {
48
+ sends: number;
49
+ hardBounces: number;
50
+ complaints: number;
51
+ }): BulkEmailWorkspaceVerdict;
52
+ /** Warm-up day (1-based) on `now` for a workspace whose first send was at `firstSendAt`; day 1 before any send. */
53
+ export declare function bulkEmailWarmupDay(firstSendAt: Date | null, now: Date): number;
54
+ /** The proven-volume multiplier on a warm-up day. */
55
+ export declare function bulkEmailGrowth(warmupDay: number): number;
56
+ /** Emails that may go out at once: the larger of the quick allowance and the proven volume times the day's growth. */
57
+ export declare function bulkEmailImmediateAllowance(input: {
58
+ provenVolume: number;
59
+ warmupDay: number;
60
+ }): number;
61
+ /**
62
+ * Hours a send is spread over: 0 when the send, together with what the workspace
63
+ * still has queued and sent in the recent window, fits the allowance; otherwise
64
+ * the minimum spread plus one step per doubling past the allowance, capped.
65
+ */
66
+ export declare function bulkEmailSpreadHours(input: {
67
+ emails: number;
68
+ queued: number;
69
+ recentlySent: number;
70
+ allowance: number;
71
+ }): number;
72
+ /** When the `index`-th of `count` emails of a spread send may leave: the first at once, the last at the end, the rest evenly between. */
73
+ export declare function bulkEmailSpreadOffsetMs(index: number, count: number, spreadHours: number): number;
74
+ /**
75
+ * Share of the account's bulk 24-hour quota (what is left after the
76
+ * transactional reserve) that one workspace may reserve in a day, so no single
77
+ * workspace can drain the account for everyone else.
78
+ */
79
+ export declare const BULK_EMAIL_WORKSPACE_QUOTA_SHARE = 0.25;
80
+ /**
81
+ * Share of the account's bulk send rate one workspace may use in a second. A
82
+ * half leaves the other half to everyone else, so a second workspace always
83
+ * progresses while one large send runs, and a lone workspace still sends at
84
+ * half the account's speed (its spread schedule, not the rate, sets its pace).
85
+ */
86
+ export declare const BULK_EMAIL_WORKSPACE_RATE_SHARE = 0.5;
87
+ /**
88
+ * Newsletter emails a workspace may reserve today: the smallest of its plan's
89
+ * daily cap, the staff override (which only lowers) and its share of the
90
+ * account's bulk 24-hour quota. No account snapshot is zero (fails closed).
91
+ */
92
+ export declare function bulkEmailWorkspaceDailyAllowance(input: {
93
+ planDailyCap: number;
94
+ allowanceOverride: number | null;
95
+ accountMax24HourSend: number | null;
96
+ }): number;
97
+ /**
98
+ * Bulk sends a second, account-wide and per workspace: the account limit is
99
+ * 80% of MaxSendRate (the rest is for transactional mail), the workspace limit
100
+ * its share of that, both at least 1.
101
+ */
102
+ export declare function bulkEmailSendRateLimits(maxSendRate: number): {
103
+ account: number;
104
+ workspace: number;
105
+ };
@@ -0,0 +1,125 @@
1
+ // How fast one workspace's newsletter mail leaves the shared SES account, and when
2
+ // its bounces and complaints pause or block it. Volume is earned: a workspace sends
3
+ // at once only up to a multiple of the largest send it has already made cleanly,
4
+ // and a bigger send is spread over hours, so a bad list shows in the first hours
5
+ // instead of in every inbox. Clean-room after mailcheap.ai's documented behaviour
6
+ // (AGPL; design reference only, no code taken).
7
+ import { SES_BULK_RATE_FRACTION, SES_TRANSACTIONAL_RESERVE_FRACTION } from "./email-delivery.js";
8
+ export const BULK_EMAIL_PACING = {
9
+ /** A send this small always goes at once, even for a brand-new workspace. */
10
+ quickEmails: 200,
11
+ /** Days of warm-up after the workspace's first newsletter send. */
12
+ warmupDays: 30,
13
+ /** Look-back window for the proven volume. */
14
+ provenDays: 60,
15
+ /** A send counts towards the proven volume this long after it went out, once its bounces have come in. */
16
+ provenSettleHours: 24,
17
+ /** Sends still queued, and sends of this many past hours, count against a new send. */
18
+ recentHours: 24,
19
+ /** Multiplier on the proven volume: `first` on warm-up day 1, rising linearly to `last` on the last day and after. */
20
+ growth: { first: 2, last: 5 },
21
+ /** A send over the allowance is spread over at least this many hours … */
22
+ minSpreadHours: 4,
23
+ /** … plus this many for each doubling past the allowance … */
24
+ hoursPerDoubling: 4,
25
+ /** … and never more than this. */
26
+ maxSpreadHours: 72,
27
+ };
28
+ /**
29
+ * Workspace-level bounce and complaint limits, each with a minimum count so one
30
+ * complaint or a few bounces never stop a workspace. `pause` stops the workspace
31
+ * until someone resumes it; `block` is abuse and only staff unblock it. The rates
32
+ * match the release slice (`email-delivery.ts`): 0.3% is Gmail's spam line, 5%
33
+ * where SES opens a review.
34
+ */
35
+ export const BULK_EMAIL_WORKSPACE_LIMITS = {
36
+ pause: { bounceRate: 0.02, minBounces: 5, complaintRate: 0.0008, minComplaints: 2 },
37
+ block: { bounceRate: 0.05, minBounces: 10, complaintRate: 0.003, minComplaints: 3 },
38
+ };
39
+ /** The workspace's verdict for a window of sends, hard bounces and complaints. */
40
+ export function bulkEmailWorkspaceVerdict(input) {
41
+ const sends = Math.max(0, input.sends);
42
+ if (sends === 0)
43
+ return "ok";
44
+ const over = (level) => (input.hardBounces >= level.minBounces && input.hardBounces / sends >= level.bounceRate)
45
+ || (input.complaints >= level.minComplaints && input.complaints / sends >= level.complaintRate);
46
+ if (over(BULK_EMAIL_WORKSPACE_LIMITS.block))
47
+ return "block";
48
+ if (over(BULK_EMAIL_WORKSPACE_LIMITS.pause))
49
+ return "pause";
50
+ return "ok";
51
+ }
52
+ /** Warm-up day (1-based) on `now` for a workspace whose first send was at `firstSendAt`; day 1 before any send. */
53
+ export function bulkEmailWarmupDay(firstSendAt, now) {
54
+ if (!firstSendAt)
55
+ return 1;
56
+ return Math.max(1, Math.floor((now.getTime() - firstSendAt.getTime()) / 86_400_000) + 1);
57
+ }
58
+ /** The proven-volume multiplier on a warm-up day. */
59
+ export function bulkEmailGrowth(warmupDay) {
60
+ const { warmupDays, growth } = BULK_EMAIL_PACING;
61
+ const day = Math.min(Math.max(1, Math.floor(warmupDay)), warmupDays);
62
+ return growth.first + ((growth.last - growth.first) * (day - 1)) / (warmupDays - 1);
63
+ }
64
+ /** Emails that may go out at once: the larger of the quick allowance and the proven volume times the day's growth. */
65
+ export function bulkEmailImmediateAllowance(input) {
66
+ const proven = Math.max(0, Math.floor(input.provenVolume));
67
+ return Math.max(BULK_EMAIL_PACING.quickEmails, Math.floor(proven * bulkEmailGrowth(input.warmupDay)));
68
+ }
69
+ /**
70
+ * Hours a send is spread over: 0 when the send, together with what the workspace
71
+ * still has queued and sent in the recent window, fits the allowance; otherwise
72
+ * the minimum spread plus one step per doubling past the allowance, capped.
73
+ */
74
+ export function bulkEmailSpreadHours(input) {
75
+ const total = Math.max(0, input.emails) + Math.max(0, input.queued) + Math.max(0, input.recentlySent);
76
+ const allowance = Math.max(1, input.allowance);
77
+ if (total <= allowance)
78
+ return 0;
79
+ const { minSpreadHours, hoursPerDoubling, maxSpreadHours } = BULK_EMAIL_PACING;
80
+ const doublings = Math.floor(Math.log2(total / allowance));
81
+ return Math.min(maxSpreadHours, minSpreadHours + hoursPerDoubling * doublings);
82
+ }
83
+ /** When the `index`-th of `count` emails of a spread send may leave: the first at once, the last at the end, the rest evenly between. */
84
+ export function bulkEmailSpreadOffsetMs(index, count, spreadHours) {
85
+ if (spreadHours <= 0 || count <= 1)
86
+ return 0;
87
+ const clamped = Math.min(Math.max(0, index), count - 1);
88
+ return Math.round((clamped / (count - 1)) * spreadHours * 3_600_000);
89
+ }
90
+ /**
91
+ * Share of the account's bulk 24-hour quota (what is left after the
92
+ * transactional reserve) that one workspace may reserve in a day, so no single
93
+ * workspace can drain the account for everyone else.
94
+ */
95
+ export const BULK_EMAIL_WORKSPACE_QUOTA_SHARE = 0.25;
96
+ /**
97
+ * Share of the account's bulk send rate one workspace may use in a second. A
98
+ * half leaves the other half to everyone else, so a second workspace always
99
+ * progresses while one large send runs, and a lone workspace still sends at
100
+ * half the account's speed (its spread schedule, not the rate, sets its pace).
101
+ */
102
+ export const BULK_EMAIL_WORKSPACE_RATE_SHARE = 0.5;
103
+ /**
104
+ * Newsletter emails a workspace may reserve today: the smallest of its plan's
105
+ * daily cap, the staff override (which only lowers) and its share of the
106
+ * account's bulk 24-hour quota. No account snapshot is zero (fails closed).
107
+ */
108
+ export function bulkEmailWorkspaceDailyAllowance(input) {
109
+ if (input.accountMax24HourSend === null || !Number.isFinite(input.accountMax24HourSend))
110
+ return 0;
111
+ const accountShare = Math.floor(Math.max(0, input.accountMax24HourSend) * (1 - SES_TRANSACTIONAL_RESERVE_FRACTION) * BULK_EMAIL_WORKSPACE_QUOTA_SHARE);
112
+ const caps = [Math.max(0, Math.floor(input.planDailyCap)), accountShare];
113
+ if (input.allowanceOverride !== null && Number.isFinite(input.allowanceOverride))
114
+ caps.push(Math.max(0, Math.floor(input.allowanceOverride)));
115
+ return Math.min(...caps);
116
+ }
117
+ /**
118
+ * Bulk sends a second, account-wide and per workspace: the account limit is
119
+ * 80% of MaxSendRate (the rest is for transactional mail), the workspace limit
120
+ * its share of that, both at least 1.
121
+ */
122
+ export function bulkEmailSendRateLimits(maxSendRate) {
123
+ const account = Math.max(1, Math.floor((Number.isFinite(maxSendRate) ? Math.max(0, maxSendRate) : 0) * SES_BULK_RATE_FRACTION));
124
+ return { account, workspace: Math.max(1, Math.ceil(account * BULK_EMAIL_WORKSPACE_RATE_SHARE)) };
125
+ }
@@ -1,5 +1,7 @@
1
1
  import { type EnrichmentIntentTaxonomyEntry } from "./enrichment-intents.js";
2
2
  import { type RecipePrimitive } from "./recipes.js";
3
+ import { type SkillPlayMatch } from "./skill-play-routing.js";
4
+ export { matchSkillPlay, SKILL_PLAY_TRIGGERS, type SkillPlayMatch } from "./skill-play-routing.js";
3
5
  export type CapabilityLayer = "Control" | "Knowledge" | "Data" | "Action" | "External";
4
6
  export type CapabilityPosture = "read_only" | "workspace_write" | "paid_read" | "external_write" | "mixed";
5
7
  export type CapabilityRouteCard = {
@@ -55,6 +57,11 @@ export type CapabilityRouteMatch = {
55
57
  /** The same read as an MCP call, served on MCP in place of `exactCommand`. */
56
58
  exactMcpCall: CapabilityMcpCall;
57
59
  };
60
+ /**
61
+ * The feature-skill play the ask matches (skill-play-routing.ts), present only
62
+ * when the route's card lists that skill: the exact file to open before planning.
63
+ */
64
+ play?: SkillPlayMatch;
58
65
  };
59
66
  export type PrimitiveRouteMatch = CapabilityRouteMatch & {
60
67
  card: PrimitiveRouteCard;
@@ -202,6 +209,26 @@ export declare const CAPABILITY_ROUTE_MCP_CALLS: {
202
209
  product_url: string;
203
210
  };
204
211
  };
212
+ publishingPostDraft: () => {
213
+ tool: string;
214
+ arguments: {
215
+ content_text: string;
216
+ publish_at: string;
217
+ status: string;
218
+ };
219
+ };
220
+ signalCapture: () => {
221
+ tool: string;
222
+ arguments: {
223
+ object: string;
224
+ row_id: string;
225
+ activity_type: string;
226
+ direction: string;
227
+ summary: string;
228
+ occurred_at: string;
229
+ mode: string;
230
+ };
231
+ };
205
232
  publishingFirstComment: () => {
206
233
  tool: string;
207
234
  arguments: {
@@ -286,6 +313,8 @@ export declare const CAPABILITY_ROUTE_MCP_CALLS: {
286
313
  /** The catalogue row's `enrichment catalog --intent <id>` as an MCP call. */
287
314
  export declare function catalogueIntentMcpCall(intentId: string): CapabilityMcpCall;
288
315
  /** A post's first comment is set with the post; `publishing posts update` takes the same flag. */
316
+ export declare const POST_DRAFT_COMMAND = "oxygen publishing posts create --draft --publish-at <iso> --text \"<post>\" --json";
317
+ export declare const SIGNAL_CAPTURE_COMMAND = "oxygen crm activity log people <rowId or email> --type website_visit --direction inbound --summary \"<what they viewed>\" --live --json";
289
318
  export declare const PUBLISHING_FIRST_COMMENT_COMMAND = "oxygen publishing posts create --publish-at <iso> --sender <sender_id> --text \"<post>\" --first-comment \"<comment, e.g. the link>\" --json";
290
319
  export declare const COMPANY_PAGE_FOLLOWERS_COMMAND = "oxygen tables followers check --company <linkedin company page URL> --records <n> --json";
291
320
  /** The exact `companies search plan` invocation for the url_recovery intent. */