@oxygen-agent/cli 1.377.3 → 1.591.1

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/column-run-notices.d.ts +11 -0
  3. package/dist/column-run-notices.js +37 -0
  4. package/dist/command-manifest.js +13 -8
  5. package/dist/help.js +79 -16
  6. package/dist/index.js +3812 -460
  7. package/dist/skills.js +106 -1
  8. package/node_modules/@oxygen/formula/dist/coerce.d.ts +8 -0
  9. package/node_modules/@oxygen/formula/dist/coerce.js +10 -0
  10. package/node_modules/@oxygen/formula/dist/evaluate.d.ts +31 -0
  11. package/node_modules/@oxygen/formula/dist/evaluate.js +248 -0
  12. package/node_modules/@oxygen/formula/dist/expression.d.ts +64 -0
  13. package/node_modules/@oxygen/formula/dist/expression.js +428 -0
  14. package/node_modules/@oxygen/formula/dist/formula-functions.d.ts +71 -0
  15. package/node_modules/@oxygen/formula/dist/formula-functions.js +1100 -0
  16. package/node_modules/@oxygen/formula/dist/index.d.ts +17 -0
  17. package/node_modules/@oxygen/formula/dist/index.js +17 -0
  18. package/node_modules/@oxygen/formula/dist/value-normalizers.d.ts +30 -0
  19. package/node_modules/@oxygen/formula/dist/value-normalizers.js +80 -0
  20. package/node_modules/@oxygen/formula/package.json +26 -0
  21. package/node_modules/@oxygen/recipe-sdk/dist/index.d.ts +30 -0
  22. package/node_modules/@oxygen/recipe-sdk/dist/index.js +2 -2
  23. package/node_modules/@oxygen/shared/dist/billing-anchors.d.ts +60 -0
  24. package/node_modules/@oxygen/shared/dist/billing-anchors.js +135 -0
  25. package/node_modules/@oxygen/shared/dist/billing.d.ts +99 -5
  26. package/node_modules/@oxygen/shared/dist/billing.js +185 -8
  27. package/node_modules/@oxygen/shared/dist/call-outcomes.d.ts +59 -0
  28. package/node_modules/@oxygen/shared/dist/call-outcomes.js +73 -0
  29. package/node_modules/@oxygen/shared/dist/cli-result.js +1 -0
  30. package/node_modules/@oxygen/shared/dist/credit-guidance.js +3 -1
  31. package/node_modules/@oxygen/shared/dist/crm-reply-events.d.ts +35 -0
  32. package/node_modules/@oxygen/shared/dist/crm-reply-events.js +31 -0
  33. package/node_modules/@oxygen/shared/dist/dial-guardrail-overrides.d.ts +50 -0
  34. package/node_modules/@oxygen/shared/dist/dial-guardrail-overrides.js +65 -0
  35. package/node_modules/@oxygen/shared/dist/directory.d.ts +1 -1
  36. package/node_modules/@oxygen/shared/dist/directory.js +1 -0
  37. package/node_modules/@oxygen/shared/dist/file-import.js +58 -11
  38. package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +15 -0
  39. package/node_modules/@oxygen/shared/dist/hosted-ai.js +19 -0
  40. package/node_modules/@oxygen/shared/dist/index.d.ts +9 -0
  41. package/node_modules/@oxygen/shared/dist/index.js +9 -0
  42. package/node_modules/@oxygen/shared/dist/linkedin-quota-denial.d.ts +31 -0
  43. package/node_modules/@oxygen/shared/dist/linkedin-quota-denial.js +56 -0
  44. package/node_modules/@oxygen/shared/dist/linkedin-sequences.d.ts +5 -4
  45. package/node_modules/@oxygen/shared/dist/linkedin-sequences.js +5 -4
  46. package/node_modules/@oxygen/shared/dist/linkedin-url.d.ts +22 -0
  47. package/node_modules/@oxygen/shared/dist/linkedin-url.js +7 -4
  48. package/node_modules/@oxygen/shared/dist/log.js +41 -2
  49. package/node_modules/@oxygen/shared/dist/microsoft-consent-url.d.ts +7 -0
  50. package/node_modules/@oxygen/shared/dist/microsoft-consent-url.js +29 -0
  51. package/node_modules/@oxygen/shared/dist/object-storage.d.ts +31 -0
  52. package/node_modules/@oxygen/shared/dist/object-storage.js +61 -0
  53. package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +636 -0
  54. package/node_modules/@oxygen/shared/dist/plan-limits.js +199 -0
  55. package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +89 -23
  56. package/node_modules/@oxygen/shared/dist/pricing-sheet.js +88 -24
  57. package/node_modules/@oxygen/shared/dist/sequence-crm-events.d.ts +291 -0
  58. package/node_modules/@oxygen/shared/dist/sequence-crm-events.js +224 -0
  59. package/node_modules/@oxygen/shared/dist/sequence-template.d.ts +42 -1
  60. package/node_modules/@oxygen/shared/dist/sequence-template.js +0 -0
  61. package/node_modules/@oxygen/shared/dist/sequences.d.ts +287 -24
  62. package/node_modules/@oxygen/shared/dist/sequences.js +940 -60
  63. package/node_modules/@oxygen/shared/dist/spend-safety.d.ts +70 -0
  64. package/node_modules/@oxygen/shared/dist/spend-safety.js +106 -0
  65. package/node_modules/@oxygen/shared/dist/tags.d.ts +90 -1
  66. package/node_modules/@oxygen/shared/dist/tags.js +122 -6
  67. package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
  68. package/node_modules/@oxygen/shared/dist/version.js +1 -1
  69. package/node_modules/@oxygen/shared/dist/workflow-trigger-metadata.d.ts +1 -1
  70. package/node_modules/@oxygen/shared/dist/workflow-trigger-metadata.js +4 -0
  71. package/node_modules/@oxygen/shared/package.json +95 -0
  72. package/node_modules/@oxygen/workflows/dist/event-dispatch.d.ts +126 -0
  73. package/node_modules/@oxygen/workflows/dist/event-dispatch.js +173 -0
  74. package/node_modules/@oxygen/workflows/dist/graph/expression.d.ts +78 -0
  75. package/node_modules/@oxygen/workflows/dist/graph/expression.js +700 -0
  76. package/node_modules/@oxygen/workflows/dist/graph/index.d.ts +20 -0
  77. package/node_modules/@oxygen/workflows/dist/graph/index.js +20 -0
  78. package/node_modules/@oxygen/workflows/dist/graph/lint.d.ts +4 -0
  79. package/node_modules/@oxygen/workflows/dist/graph/lint.js +812 -0
  80. package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.d.ts +501 -0
  81. package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.js +200 -0
  82. package/node_modules/@oxygen/workflows/dist/graph/params.d.ts +86 -0
  83. package/node_modules/@oxygen/workflows/dist/graph/params.js +173 -0
  84. package/node_modules/@oxygen/workflows/dist/graph/remap.d.ts +48 -0
  85. package/node_modules/@oxygen/workflows/dist/graph/remap.js +213 -0
  86. package/node_modules/@oxygen/workflows/dist/graph/topology.d.ts +46 -0
  87. package/node_modules/@oxygen/workflows/dist/graph/topology.js +280 -0
  88. package/node_modules/@oxygen/workflows/dist/graph/types.d.ts +270 -0
  89. package/node_modules/@oxygen/workflows/dist/graph/types.js +93 -0
  90. package/node_modules/@oxygen/workflows/dist/index.d.ts +113 -1
  91. package/node_modules/@oxygen/workflows/dist/index.js +179 -13
  92. package/node_modules/@oxygen/workflows/dist/tool-effects.d.ts +1 -0
  93. package/node_modules/@oxygen/workflows/dist/tool-effects.js +19 -0
  94. package/node_modules/@oxygen/workflows/dist/usage-estimate.js +135 -4
  95. package/node_modules/@oxygen/workflows/package.json +4 -0
  96. package/package.json +7 -5
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @oxygen/formula — the OXYGEN expression language, host-free.
3
+ *
4
+ * Tokenizer, parser, static analysis, function registry, and a scope-driven
5
+ * evaluator with no node builtins and no dependency beyond the dependency-free
6
+ * `@oxygen/shared/cli-result` leaf (for `OxygenError`) — so the exact semantics
7
+ * that run a formula column in the worker also run in the browser (formula
8
+ * editor previews, the visual workflow editor).
9
+ *
10
+ * Hosts bind identifiers by implementing `FormulaScope.resolve` —
11
+ * `@oxygen/tenant-db` binds them to a workspace table row.
12
+ */
13
+ export { isRecord } from "./coerce.js";
14
+ export * from "./evaluate.js";
15
+ export * from "./expression.js";
16
+ export * from "./formula-functions.js";
17
+ export * from "./value-normalizers.js";
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @oxygen/formula — the OXYGEN expression language, host-free.
3
+ *
4
+ * Tokenizer, parser, static analysis, function registry, and a scope-driven
5
+ * evaluator with no node builtins and no dependency beyond the dependency-free
6
+ * `@oxygen/shared/cli-result` leaf (for `OxygenError`) — so the exact semantics
7
+ * that run a formula column in the worker also run in the browser (formula
8
+ * editor previews, the visual workflow editor).
9
+ *
10
+ * Hosts bind identifiers by implementing `FormulaScope.resolve` —
11
+ * `@oxygen/tenant-db` binds them to a workspace table row.
12
+ */
13
+ export { isRecord } from "./coerce.js";
14
+ export * from "./evaluate.js";
15
+ export * from "./expression.js";
16
+ export * from "./formula-functions.js";
17
+ export * from "./value-normalizers.js";
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Canonical value normalizers shared by every subsystem that compares
3
+ * identity-shaped values: CRM identity resolution, the formula function
4
+ * registry, row dedupe, and Records↔Tables binding. One source of truth so
5
+ * "does foo@Bar.com match FOO@bar.com" answers the same everywhere — including
6
+ * in the browser. Pure string functions — no I/O, no tenant context.
7
+ */
8
+ export declare const VALUE_NORMALIZATIONS: readonly ["exact_text_v1", "lower_trim_v1", "email_v1", "domain_v1", "linkedin_url_v1", "uuid_v1"];
9
+ export type ValueNormalization = (typeof VALUE_NORMALIZATIONS)[number];
10
+ export declare function isValueNormalization(value: unknown): value is ValueNormalization;
11
+ /** `email_v1`: case-insensitive mailbox match. */
12
+ export declare function normalizeEmail(value: string): string;
13
+ /**
14
+ * `domain_v1`: reduce a URL, hostname, or bare domain to its canonical
15
+ * registrable form — lowercased hostname without `www.` or trailing dots.
16
+ */
17
+ export declare function normalizeDomain(value: string): string;
18
+ /** `linkedin_url_v1`: trailing-slash-insensitive, case-insensitive profile URL match. */
19
+ export declare function normalizeLinkedinUrl(value: string): string;
20
+ /** `uuid_v1`: case-insensitive UUID match. */
21
+ export declare function normalizeUuid(value: string): string;
22
+ /** `exact_text_v1`: whitespace-trimmed verbatim match. */
23
+ export declare function normalizeExactText(value: string): string;
24
+ /** `lower_trim_v1`: the default loose text match (trimmed, case-insensitive). */
25
+ export declare function normalizeLowerTrim(value: string): string;
26
+ /**
27
+ * Apply a named normalization. Unknown names fall back to `exact_text_v1`
28
+ * semantics, matching the historical CRM identity behavior.
29
+ */
30
+ export declare function normalizeValue(normalization: string, value: string): string;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Canonical value normalizers shared by every subsystem that compares
3
+ * identity-shaped values: CRM identity resolution, the formula function
4
+ * registry, row dedupe, and Records↔Tables binding. One source of truth so
5
+ * "does foo@Bar.com match FOO@bar.com" answers the same everywhere — including
6
+ * in the browser. Pure string functions — no I/O, no tenant context.
7
+ */
8
+ export const VALUE_NORMALIZATIONS = [
9
+ "exact_text_v1",
10
+ "lower_trim_v1",
11
+ "email_v1",
12
+ "domain_v1",
13
+ "linkedin_url_v1",
14
+ "uuid_v1",
15
+ ];
16
+ export function isValueNormalization(value) {
17
+ return typeof value === "string" && VALUE_NORMALIZATIONS.includes(value);
18
+ }
19
+ /** `email_v1`: case-insensitive mailbox match. */
20
+ export function normalizeEmail(value) {
21
+ return value.trim().toLowerCase();
22
+ }
23
+ /**
24
+ * `domain_v1`: reduce a URL, hostname, or bare domain to its canonical
25
+ * registrable form — lowercased hostname without `www.` or trailing dots.
26
+ */
27
+ export function normalizeDomain(value) {
28
+ const trimmed = value.trim().toLowerCase();
29
+ if (!trimmed)
30
+ return "";
31
+ try {
32
+ const parsed = new URL(trimmed.includes("://") ? trimmed : `https://${trimmed}`);
33
+ return parsed.hostname.replace(/^www\./, "").replace(/\.+$/, "");
34
+ }
35
+ catch {
36
+ return trimmed
37
+ .replace(/^https?:\/\//, "")
38
+ .split("/")[0]
39
+ ?.replace(/^www\./, "")
40
+ .replace(/\.+$/, "")
41
+ ?? "";
42
+ }
43
+ }
44
+ /** `linkedin_url_v1`: trailing-slash-insensitive, case-insensitive profile URL match. */
45
+ export function normalizeLinkedinUrl(value) {
46
+ return value.trim().replace(/\/+$/, "").toLowerCase();
47
+ }
48
+ /** `uuid_v1`: case-insensitive UUID match. */
49
+ export function normalizeUuid(value) {
50
+ return value.trim().toLowerCase();
51
+ }
52
+ /** `exact_text_v1`: whitespace-trimmed verbatim match. */
53
+ export function normalizeExactText(value) {
54
+ return value.trim();
55
+ }
56
+ /** `lower_trim_v1`: the default loose text match (trimmed, case-insensitive). */
57
+ export function normalizeLowerTrim(value) {
58
+ return value.trim().toLowerCase();
59
+ }
60
+ /**
61
+ * Apply a named normalization. Unknown names fall back to `exact_text_v1`
62
+ * semantics, matching the historical CRM identity behavior.
63
+ */
64
+ export function normalizeValue(normalization, value) {
65
+ switch (normalization) {
66
+ case "email_v1":
67
+ return normalizeEmail(value);
68
+ case "domain_v1":
69
+ return normalizeDomain(value);
70
+ case "linkedin_url_v1":
71
+ return normalizeLinkedinUrl(value);
72
+ case "uuid_v1":
73
+ return normalizeUuid(value);
74
+ case "lower_trim_v1":
75
+ return normalizeLowerTrim(value);
76
+ case "exact_text_v1":
77
+ default:
78
+ return normalizeExactText(value);
79
+ }
80
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@oxygen/formula",
3
+ "version": "0.0.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
12
+ "default": "./dist/index.js"
13
+ },
14
+ "./functions": {
15
+ "types": "./dist/formula-functions.d.ts",
16
+ "import": "./dist/formula-functions.js",
17
+ "default": "./dist/formula-functions.js"
18
+ },
19
+ "./value-normalizers": {
20
+ "types": "./dist/value-normalizers.d.ts",
21
+ "import": "./dist/value-normalizers.js",
22
+ "default": "./dist/value-normalizers.js"
23
+ }
24
+ },
25
+ "dependencies": {}
26
+ }
@@ -130,6 +130,19 @@ export type RecipeStepOptions<T> = {
130
130
  effect?: WorkflowStepEffect;
131
131
  run: () => T | Promise<T>;
132
132
  };
133
+ export type RecipeWaitOptions = {
134
+ /** Relative deadline. Exactly one of `seconds` or `until` is required. */
135
+ seconds?: number;
136
+ /** Absolute deadline as an ISO-8601 instant. */
137
+ until?: string;
138
+ };
139
+ export type RecipeWaitResult = {
140
+ waited: boolean;
141
+ /** The deadline this wait resolved against, ISO-8601. */
142
+ resume_at: string;
143
+ /** True when the mode short-circuited the wait instead of parking. */
144
+ simulated?: boolean;
145
+ };
133
146
  export type RecipeContext = {
134
147
  input: unknown;
135
148
  mode: WorkflowMode;
@@ -143,6 +156,23 @@ export type RecipeContext = {
143
156
  approvals: RecipeApprovalApi;
144
157
  log: (level: RecipeLogLevel, message: string, payload?: Record<string, unknown>) => void;
145
158
  step: <T = unknown>(key: string, options: RecipeStepOptions<T>) => Promise<T>;
159
+ /**
160
+ * Park the run until a deadline, durably.
161
+ *
162
+ * NOT a sleep: the run's lease is released and the worker moves on, so a wait
163
+ * of three days costs no compute and survives a deploy or a crash. When the
164
+ * deadline passes the recipe replays from its last checkpoint and this call
165
+ * returns — so everything before it keeps its recorded results and no tool is
166
+ * re-run.
167
+ *
168
+ * The `key` is the checkpoint identity, exactly like ctx.step(): it is what
169
+ * lets a replay recognise a wait it has already served rather than restarting
170
+ * the clock. Reusing a key within one execution is refused.
171
+ *
172
+ * Outside `live` the wait returns immediately — a dry run has no real clock to
173
+ * honour, and blocking a preview for three days would make previews unusable.
174
+ */
175
+ wait: (key: string, options: RecipeWaitOptions) => Promise<RecipeWaitResult>;
146
176
  now: () => Promise<string>;
147
177
  uuid: () => Promise<string>;
148
178
  };
@@ -14,8 +14,8 @@ export function defineRecipe(input) {
14
14
  if (input.runtime !== undefined && input.runtime !== "durable") {
15
15
  throw new Error("Recipe runtime must be durable.");
16
16
  }
17
- if (!Array.isArray(input.tools) || input.tools.length === 0) {
18
- throw new Error("Durable recipes must declare at least one allowed tool.");
17
+ if (!Array.isArray(input.tools)) {
18
+ throw new Error("Durable recipes must declare a tools array (use tools: [] for pure recipes).");
19
19
  }
20
20
  const tools = Array.from(new Set(input.tools.map((tool) => {
21
21
  if (typeof tool !== "string" || !tool.trim()) {
@@ -0,0 +1,60 @@
1
+ /**
2
+ * The immutable anchor day of a commitment — the UTC day-of-month it was
3
+ * created on. Stored once at creation and never recomputed, so the clamp in
4
+ * {@link nextAnchorDueAt} can always project from the original intent (the 31st)
5
+ * rather than from a previously clamped value (the 28th).
6
+ */
7
+ export declare function anchorDayOf(anchorAt: Date): number;
8
+ /**
9
+ * The next due instant for a commitment: exactly one calendar month after
10
+ * `periodStart`, on `anchorDay` clamped to the target month's length, preserving
11
+ * `periodStart`'s UTC time-of-day.
12
+ *
13
+ * Time-of-day is preserved so a mailbox connected at 09:14 bills at 09:14 — a
14
+ * commitment created minutes ago must not become due "today at 00:00" and get
15
+ * charged twice in one day by the next sweep tick.
16
+ *
17
+ * `anchorDay` is clamped into 1..31 defensively; a caller passing a value from a
18
+ * corrupted row gets a sane projection instead of an Invalid Date.
19
+ */
20
+ export declare function nextAnchorDueAt(periodStart: Date, anchorDay: number): Date;
21
+ /**
22
+ * The atomic-claim cursor for one commitment period. Replaces the calendar-month
23
+ * `YYYY-MM` key the other billers use; the shared billMonthlyCreditCycle engine
24
+ * never inspects the key's shape, so the swap is contained here.
25
+ *
26
+ * The period start is the identity (not the due date): a period is claimed once,
27
+ * and stamping the START means a claim written before the anchor advances still
28
+ * names the period that was actually billed.
29
+ */
30
+ export declare function commitmentCycleKey(commitmentId: string, periodStart: Date): string;
31
+ /**
32
+ * How many times a commitment on `anchorDay` falls due in the half-open window
33
+ * [from, to). Used to size the block for subscription periods longer than a
34
+ * month and to project "what will this cost me before my next renewal" on the
35
+ * commitments surface.
36
+ *
37
+ * Walks period by period rather than dividing elapsed days, because month
38
+ * lengths differ and the clamped anchor is not a fixed stride.
39
+ */
40
+ export declare function occurrencesInPeriod(from: Date, to: Date, anchorDay: number): number;
41
+ /**
42
+ * How many months of every fixed resource the block must hold, given the
43
+ * subscription period length in days.
44
+ *
45
+ * The block is a STANDING one-month-per-resource reserve (see
46
+ * credit-commitments.ts): because every anchor is at most one month out, holding
47
+ * one month per active resource always covers every fixed charge falling due
48
+ * inside any coming subscription period of <= 1 month. Longer periods (annual
49
+ * plans) need proportionally more held, or the block runs dry mid-term.
50
+ *
51
+ * Returns at least 1 — a missing/zero/garbage period length must never collapse
52
+ * the block to nothing, which would silently disable the whole feature.
53
+ */
54
+ export declare function blockPeriodsForSubscription(periodDays: number | null | undefined): number;
55
+ /**
56
+ * Days between two instants, floored. Small helper so callers deriving
57
+ * `blockPeriodsForSubscription` from a Stripe period pair do not each reimplement
58
+ * the millisecond math (and get it wrong across DST by using local dates).
59
+ */
60
+ export declare function periodLengthDays(start: Date | null | undefined, end: Date | null | undefined): number | null;
@@ -0,0 +1,135 @@
1
+ // Rolling per-resource monthly anchors for FIXED (committed) credit charges.
2
+ //
3
+ // Every other recurring biller in Oxygen (managed inboxes, warmup, placement,
4
+ // LinkedIn seats, egress addons) keys its cycle on the CALENDAR month —
5
+ // `YYYY-MM`. That is correct for a rail whose vendor invoices monthly, but it is
6
+ // wrong for a per-resource commitment: a mailbox connected on the 9th should
7
+ // bill on the 9th, and a LinkedIn account connected on the 22nd on the 22nd, so
8
+ // each individual resource rolls its own month independently of the calendar and
9
+ // independently of the subscription period.
10
+ //
11
+ // THE ANCHOR DAY IS STORED SEPARATELY FROM THE LAST DUE DATE, and that is the
12
+ // whole trick. If you derive the next due date from the previous one by "same
13
+ // day next month, clamped", a 31st anchor DECAYS: 31 Jan -> 28 Feb -> 28 Mar ->
14
+ // 28 Apr, and the customer silently drifts three days earlier every year. Keeping
15
+ // the immutable `anchorDay` and re-clamping from it each period gives the Stripe
16
+ // anchor rule instead: 31 Jan -> 28 Feb -> 31 Mar. The clamp is a per-period
17
+ // projection, never a mutation of the anchor.
18
+ //
19
+ // Pure module: no DB, no clock reads beyond the arguments handed in, no
20
+ // dependencies. All arithmetic is UTC — a rolling anchor must not shift when the
21
+ // worker host's local zone crosses DST.
22
+ /** Milliseconds in a day. Local to keep this module dependency-free. */
23
+ const DAY_MS = 24 * 60 * 60 * 1000;
24
+ /**
25
+ * Days in a given UTC (year, monthIndex). `Date.UTC(y, m + 1, 0)` is the last
26
+ * day of month `m`, which is exactly the count.
27
+ */
28
+ function daysInUtcMonth(year, monthIndex) {
29
+ return new Date(Date.UTC(year, monthIndex + 1, 0)).getUTCDate();
30
+ }
31
+ /**
32
+ * The immutable anchor day of a commitment — the UTC day-of-month it was
33
+ * created on. Stored once at creation and never recomputed, so the clamp in
34
+ * {@link nextAnchorDueAt} can always project from the original intent (the 31st)
35
+ * rather than from a previously clamped value (the 28th).
36
+ */
37
+ export function anchorDayOf(anchorAt) {
38
+ return anchorAt.getUTCDate();
39
+ }
40
+ /**
41
+ * The next due instant for a commitment: exactly one calendar month after
42
+ * `periodStart`, on `anchorDay` clamped to the target month's length, preserving
43
+ * `periodStart`'s UTC time-of-day.
44
+ *
45
+ * Time-of-day is preserved so a mailbox connected at 09:14 bills at 09:14 — a
46
+ * commitment created minutes ago must not become due "today at 00:00" and get
47
+ * charged twice in one day by the next sweep tick.
48
+ *
49
+ * `anchorDay` is clamped into 1..31 defensively; a caller passing a value from a
50
+ * corrupted row gets a sane projection instead of an Invalid Date.
51
+ */
52
+ export function nextAnchorDueAt(periodStart, anchorDay) {
53
+ const safeAnchorDay = Math.min(31, Math.max(1, Math.trunc(anchorDay)));
54
+ const year = periodStart.getUTCFullYear();
55
+ const monthIndex = periodStart.getUTCMonth();
56
+ // Normalize the +1 month ourselves rather than letting Date roll it: passing
57
+ // monthIndex 12 to Date.UTC is well-defined, but computing the target's day
58
+ // count needs the normalized (year, month) pair anyway.
59
+ const targetYear = monthIndex === 11 ? year + 1 : year;
60
+ const targetMonthIndex = monthIndex === 11 ? 0 : monthIndex + 1;
61
+ const day = Math.min(safeAnchorDay, daysInUtcMonth(targetYear, targetMonthIndex));
62
+ return new Date(Date.UTC(targetYear, targetMonthIndex, day, periodStart.getUTCHours(), periodStart.getUTCMinutes(), periodStart.getUTCSeconds(), periodStart.getUTCMilliseconds()));
63
+ }
64
+ /**
65
+ * The atomic-claim cursor for one commitment period. Replaces the calendar-month
66
+ * `YYYY-MM` key the other billers use; the shared billMonthlyCreditCycle engine
67
+ * never inspects the key's shape, so the swap is contained here.
68
+ *
69
+ * The period start is the identity (not the due date): a period is claimed once,
70
+ * and stamping the START means a claim written before the anchor advances still
71
+ * names the period that was actually billed.
72
+ */
73
+ export function commitmentCycleKey(commitmentId, periodStart) {
74
+ return `${commitmentId}:${periodStart.toISOString()}`;
75
+ }
76
+ /**
77
+ * How many times a commitment on `anchorDay` falls due in the half-open window
78
+ * [from, to). Used to size the block for subscription periods longer than a
79
+ * month and to project "what will this cost me before my next renewal" on the
80
+ * commitments surface.
81
+ *
82
+ * Walks period by period rather than dividing elapsed days, because month
83
+ * lengths differ and the clamped anchor is not a fixed stride.
84
+ */
85
+ export function occurrencesInPeriod(from, to, anchorDay) {
86
+ if (!(from instanceof Date) || !(to instanceof Date))
87
+ return 0;
88
+ if (Number.isNaN(from.getTime()) || Number.isNaN(to.getTime()))
89
+ return 0;
90
+ if (to <= from)
91
+ return 0;
92
+ let count = 0;
93
+ let cursor = from;
94
+ // Hard bound: a caller passing a decade-wide window should get a number, not a
95
+ // hang. 1200 periods is 100 years — far past any real subscription.
96
+ for (let guard = 0; guard < 1200; guard += 1) {
97
+ cursor = nextAnchorDueAt(cursor, anchorDay);
98
+ if (cursor >= to)
99
+ break;
100
+ count += 1;
101
+ }
102
+ return count;
103
+ }
104
+ /**
105
+ * How many months of every fixed resource the block must hold, given the
106
+ * subscription period length in days.
107
+ *
108
+ * The block is a STANDING one-month-per-resource reserve (see
109
+ * credit-commitments.ts): because every anchor is at most one month out, holding
110
+ * one month per active resource always covers every fixed charge falling due
111
+ * inside any coming subscription period of <= 1 month. Longer periods (annual
112
+ * plans) need proportionally more held, or the block runs dry mid-term.
113
+ *
114
+ * Returns at least 1 — a missing/zero/garbage period length must never collapse
115
+ * the block to nothing, which would silently disable the whole feature.
116
+ */
117
+ export function blockPeriodsForSubscription(periodDays) {
118
+ if (typeof periodDays !== "number" || !Number.isFinite(periodDays) || periodDays <= 0) {
119
+ return 1;
120
+ }
121
+ return Math.max(1, Math.ceil(periodDays / 30));
122
+ }
123
+ /**
124
+ * Days between two instants, floored. Small helper so callers deriving
125
+ * `blockPeriodsForSubscription` from a Stripe period pair do not each reimplement
126
+ * the millisecond math (and get it wrong across DST by using local dates).
127
+ */
128
+ export function periodLengthDays(start, end) {
129
+ if (!start || !end)
130
+ return null;
131
+ const ms = end.getTime() - start.getTime();
132
+ if (!Number.isFinite(ms) || ms <= 0)
133
+ return null;
134
+ return Math.floor(ms / DAY_MS);
135
+ }
@@ -35,6 +35,60 @@ export declare const CREDIT_TOPUP_DEFAULT_CREDITS = 20000;
35
35
  export declare function isValidCreditTopupCredits(credits: number): boolean;
36
36
  export declare function creditTopupUsdCents(credits: number): number | null;
37
37
  export declare const AUTOMATION_ACTION_CREDITS = 0.01;
38
+ /** Kinds of resource that carry a fixed monthly credit commitment. */
39
+ export declare const CREDIT_COMMITMENT_KINDS: readonly ["sending_mailbox", "managed_mailbox", "mailbox_warmup", "deliverability_unit", "linkedin_account", "whatsapp_account"];
40
+ export type CreditCommitmentKind = (typeof CREDIT_COMMITMENT_KINDS)[number];
41
+ /**
42
+ * Sequencer platform fee per CONNECTED SENDING MAILBOX per month, in credits
43
+ * ($1.00). Charged on EVERY mailbox wired to the sequencer — BYOK/self-connected
44
+ * Gmail and Microsoft inboxes included — and it STACKS on Oxygen-sold mailboxes,
45
+ * which additionally pay their own mailbox/warmup/placement lines.
46
+ */
47
+ export declare const SENDING_MAILBOX_MONTHLY_CREDITS = 1000;
48
+ /**
49
+ * Oxygen-sold managed mailbox per month, in credits ($3.00). Flat, superseding
50
+ * the dynamic vendor-COGS x 1.25 quote for the recurring mailbox line.
51
+ */
52
+ export declare const MANAGED_MAILBOX_MONTHLY_CREDITS = 3000;
53
+ /**
54
+ * Grace window after a commitment goes past_due before the owning subsystem may
55
+ * suspend the resource. Notify -> pause -> lapse; never auto-cancel. Aligned with
56
+ * the subscription entitlement grace so a customer never hits two different
57
+ * clocks for the same missed payment.
58
+ */
59
+ export declare const COMMITMENT_PAST_DUE_GRACE_DAYS = 14;
60
+ /**
61
+ * Reconnect window in which an ENDED commitment resumes instead of starting a new
62
+ * one. The single most important anti-double-charge rule: a Unipile re-auth, a
63
+ * mailbox re-import, or a reconciler blip must not re-charge a full month. Past
64
+ * this window a reconnect is treated as genuinely new and gets a fresh anchor,
65
+ * so a long-dead row can never resurrect and bill a stale period.
66
+ */
67
+ export declare const COMMITMENT_RESUME_WINDOW_MS: number;
68
+ /**
69
+ * Maximum periods one commitment may catch up in a single sweep tick. A worker
70
+ * outage (or a resurrected row) must never drain a wallet in one lump: past this
71
+ * many overdue periods the biller skips forward, stamps metadata.skipped_periods,
72
+ * and logs. Fail closed TOWARD the customer.
73
+ */
74
+ export declare const COMMITMENT_MAX_CATCHUP_PERIODS = 3;
75
+ /**
76
+ * Free month granted to resources that already existed when commitments went
77
+ * live. Combined with CREDIT_COMMITMENT_EPOCH_AT (which pins their anchor to
78
+ * go-live rather than their original created_at), this guarantees no existing
79
+ * customer is charged for infrastructure that was free when they connected it.
80
+ */
81
+ export declare const COMMITMENT_GRACE_DAYS = 30;
82
+ /**
83
+ * Flat ratified monthly price for a commitment kind, in credits — or null when
84
+ * the price is not owned HERE (warmup and deliverability are ratified in
85
+ * @oxygen/control-db; their callers use the integrations readers).
86
+ *
87
+ * Returning null is the fail-closed signal: an unpriced kind creates NO
88
+ * commitment row rather than a zero-credit one, matching the
89
+ * InboxPricingUnsignedError / WarmupPricingUnsignedError doctrine.
90
+ */
91
+ export declare function ratifiedCommitmentCredits(kind: CreditCommitmentKind): number | null;
38
92
  export declare const CREDIT_TOPUP_PACKS: readonly [{
39
93
  readonly id: "10";
40
94
  readonly usdCents: 1000;
@@ -64,16 +118,16 @@ export declare const BASE_PRICING_PLANS: {
64
118
  readonly tier: "free";
65
119
  readonly name: "Free";
66
120
  readonly monthlyPriceCents: 0;
67
- readonly monthlyCredits: 10000;
121
+ readonly monthlyCredits: 0;
68
122
  readonly weeklyCreditsLimit: null;
69
- readonly rolloverCap: 10000;
123
+ readonly rolloverCap: null;
70
124
  readonly monthlyAutomationActions: null;
71
125
  readonly automationOverageCentsPerMillion: null;
72
126
  readonly automationOverageEnabledDefault: false;
73
127
  readonly byokEnabled: false;
74
- readonly description: "Get $10 in credits on us every month — try OXYGEN with managed email, phone enrichment, and AI credits.";
75
- readonly ctaLabel: "Start free";
76
- readonly features: readonly ["$10 in credits every month", "All integrations", "Workflows", "No card required"];
128
+ readonly description: "No active plan. Start a 7-day Starter trial to use OXYGEN — existing credit balances remain spendable.";
129
+ readonly ctaLabel: "Start trial";
130
+ readonly features: readonly ["Existing credits stay spendable", "Read access to your workspace"];
77
131
  };
78
132
  readonly starter: {
79
133
  readonly tier: "starter";
@@ -185,6 +239,46 @@ export declare function isBillingCurrency(value: string): value is BillingCurren
185
239
  export declare function normalizeBillingCurrency(value: string | null | undefined): BillingCurrency;
186
240
  export declare function getCurrentFreeTierCycleKey(date?: Date): string;
187
241
  export declare function getCurrentBillingCycleKey(date?: Date): string;
242
+ /**
243
+ * A monthly Stripe period runs 28-31 days; 35 leaves slack for a proration or a
244
+ * billing-anchor shift without admitting a quarterly or annual period.
245
+ */
246
+ export declare const CREDIT_CYCLE_MAX_PERIOD_DAYS = 35;
247
+ export type CreditCycleSource = "subscription_period" | "calendar_month";
248
+ export type CreditCycleWindow = {
249
+ start: Date;
250
+ /** Exclusive; may be in the future (the cycle is usually still running). */
251
+ end: Date;
252
+ source: CreditCycleSource;
253
+ /** "July 2026" | "Jun 10 – Jul 10" */
254
+ label: string;
255
+ };
256
+ /**
257
+ * The window the credit-allowance bar measures: "this month's credits", resolved
258
+ * to the boundary at which the plan's grant actually renews.
259
+ *
260
+ * A Stripe-managed monthly subscription renews on its own period boundary, so
261
+ * that is the honest cycle for it. Everything else — off-Stripe custom plans,
262
+ * the free tier, and any non-monthly Stripe period — renews by CALENDAR month,
263
+ * which is the same boundary already used by getCurrentBillingCycleKey(),
264
+ * getCurrentFreeTierCycleKey(), the shared-billing workspace cap, and the
265
+ * automation-actions meter. The fallback is therefore consistent with every
266
+ * other monthly concept in the product rather than an invention.
267
+ *
268
+ * The length guard cuts BOTH ways deliberately. `now - start <= 35d` rejects a
269
+ * stale period a webhook never advanced; `end - start <= 35d` rejects a genuine
270
+ * annual subscription, which would otherwise pass the first check on day 3 and
271
+ * render a 365-day "month".
272
+ */
273
+ export declare function resolveCreditCycleWindow(input: {
274
+ subscription: {
275
+ currentPeriodStart: Date | null;
276
+ currentPeriodEnd: Date | null;
277
+ /** isOffStripeSubscription(subscription.metadata) — resolved by the caller. */
278
+ offStripe: boolean;
279
+ } | null;
280
+ now?: Date;
281
+ }): CreditCycleWindow;
188
282
  export declare function evaluateWeeklyQuota(usedCredits: number, requestedCredits: number, weeklyCreditsLimit: number): {
189
283
  usedCredits: number;
190
284
  requestedCredits: number;