@oxygen-agent/cli 1.936.1 → 1.982.3

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 (71) hide show
  1. package/README.md +1 -1
  2. package/dist/admin-primary-providers-render.js +9 -1
  3. package/dist/cli-values.d.ts +14 -0
  4. package/dist/cli-values.js +26 -0
  5. package/dist/command-manifest.js +30 -2
  6. package/dist/functions-commands.js +13 -5
  7. package/dist/help.js +2 -0
  8. package/dist/index.js +1509 -290
  9. package/dist/knowledge-repository-commands.d.ts +6 -0
  10. package/dist/knowledge-repository-commands.js +198 -0
  11. package/dist/skills.js +20 -0
  12. package/dist/ugc-commands.js +470 -15
  13. package/node_modules/@oxygen/recipe-sdk/dist/index.d.ts +2 -0
  14. package/node_modules/@oxygen/shared/dist/byok-connect.d.ts +11 -6
  15. package/node_modules/@oxygen/shared/dist/byok-connect.js +14 -6
  16. package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +8 -0
  17. package/node_modules/@oxygen/shared/dist/capability-discovery.js +152 -20
  18. package/node_modules/@oxygen/shared/dist/copilot-errors.js +3 -0
  19. package/node_modules/@oxygen/shared/dist/copilot-journeys.d.ts +19 -1
  20. package/node_modules/@oxygen/shared/dist/copilot-journeys.generated.d.ts +19 -0
  21. package/node_modules/@oxygen/shared/dist/copilot-journeys.generated.js +26 -0
  22. package/node_modules/@oxygen/shared/dist/copilot-journeys.js +8 -41
  23. package/node_modules/@oxygen/shared/dist/email-dsn.d.ts +60 -0
  24. package/node_modules/@oxygen/shared/dist/email-dsn.js +120 -0
  25. package/node_modules/@oxygen/shared/dist/email-warmup-readiness.d.ts +64 -0
  26. package/node_modules/@oxygen/shared/dist/email-warmup-readiness.js +90 -0
  27. package/node_modules/@oxygen/shared/dist/inbox-avatar-url.d.ts +28 -0
  28. package/node_modules/@oxygen/shared/dist/inbox-avatar-url.js +57 -0
  29. package/node_modules/@oxygen/shared/dist/index.d.ts +10 -0
  30. package/node_modules/@oxygen/shared/dist/index.js +10 -0
  31. package/node_modules/@oxygen/shared/dist/knowledge-bases.d.ts +74 -0
  32. package/node_modules/@oxygen/shared/dist/knowledge-bases.js +456 -0
  33. package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.d.ts +56 -48
  34. package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.js +50 -49
  35. package/node_modules/@oxygen/shared/dist/knowledge-repository.d.ts +22 -0
  36. package/node_modules/@oxygen/shared/dist/knowledge-repository.js +121 -0
  37. package/node_modules/@oxygen/shared/dist/knowledge-vault-markdown.d.ts +20 -0
  38. package/node_modules/@oxygen/shared/dist/knowledge-vault-markdown.js +155 -0
  39. package/node_modules/@oxygen/shared/dist/langfuse.d.ts +8 -3
  40. package/node_modules/@oxygen/shared/dist/langfuse.js +177 -130
  41. package/node_modules/@oxygen/shared/dist/llm-payload.d.ts +10 -0
  42. package/node_modules/@oxygen/shared/dist/llm-payload.js +54 -0
  43. package/node_modules/@oxygen/shared/dist/llm-usage.d.ts +11 -0
  44. package/node_modules/@oxygen/shared/dist/llm-usage.js +30 -0
  45. package/node_modules/@oxygen/shared/dist/mailbox-import.d.ts +10 -0
  46. package/node_modules/@oxygen/shared/dist/mailbox-import.js +53 -0
  47. package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +8 -0
  48. package/node_modules/@oxygen/shared/dist/plan-limits.js +8 -0
  49. package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +1 -1
  50. package/node_modules/@oxygen/shared/dist/pricing-sheet.js +1 -1
  51. package/node_modules/@oxygen/shared/dist/product-analytics-core.d.ts +98 -0
  52. package/node_modules/@oxygen/shared/dist/product-analytics-core.js +159 -0
  53. package/node_modules/@oxygen/shared/dist/product-analytics-environment.d.ts +18 -0
  54. package/node_modules/@oxygen/shared/dist/product-analytics-environment.js +46 -0
  55. package/node_modules/@oxygen/shared/dist/product-analytics-events.d.ts +116 -0
  56. package/node_modules/@oxygen/shared/dist/product-analytics-events.js +120 -0
  57. package/node_modules/@oxygen/shared/dist/recipes.d.ts +6 -0
  58. package/node_modules/@oxygen/shared/dist/recipes.js +23 -0
  59. package/node_modules/@oxygen/shared/dist/sequences.d.ts +126 -2
  60. package/node_modules/@oxygen/shared/dist/sequences.js +280 -4
  61. package/node_modules/@oxygen/shared/dist/ugc-amplification-identity.d.ts +2 -0
  62. package/node_modules/@oxygen/shared/dist/ugc-amplification-identity.js +24 -0
  63. package/node_modules/@oxygen/shared/dist/ugc.d.ts +29 -1
  64. package/node_modules/@oxygen/shared/dist/user-capability-routing.js +8 -1
  65. package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
  66. package/node_modules/@oxygen/shared/dist/version.js +3 -1
  67. package/node_modules/@oxygen/shared/dist/workspace-file-storage.d.ts +6 -2
  68. package/node_modules/@oxygen/shared/dist/workspace-file-storage.js +15 -4
  69. package/node_modules/@oxygen/shared/package.json +15 -0
  70. package/node_modules/@oxygen/workflows/dist/graph/lint.js +22 -0
  71. package/package.json +2 -1
@@ -0,0 +1,60 @@
1
+ /**
2
+ * PURE classifier for delivery-status notifications (bounce NDRs / DSNs) that
3
+ * land back in a sending mailbox's own inbox.
4
+ *
5
+ * WHY this exists: a mailbox-provider reputation block is the loudest
6
+ * deliverability signal a workspace ever receives, and it arrives as an ordinary
7
+ * inbound email — "Message rejected", "support.google.com/mail/answer/69585" —
8
+ * that no OXYGEN surface reads. A warm-up vendor reporting 96% inbox placement
9
+ * cannot contradict it, because the vendor only measures its own seed network.
10
+ * Classifying the DSN turns the workspace's own inbox into first-party
11
+ * deliverability evidence: OXYGEN's send log said we sent, the DSN says the
12
+ * receiving provider refused it, and only the second one is the truth.
13
+ *
14
+ * Pure by design — no clock, no I/O, no provider call — so the same
15
+ * classification runs in the tenant rollup, a worker sweep, and a unit test.
16
+ * Output is stable machine data (kinds and codes), never prose.
17
+ */
18
+ /** What the notification actually says happened. */
19
+ export type DeliveryStatusNotificationKind =
20
+ /** The receiving provider refused the message on reputation/policy grounds. */
21
+ "provider_rejected"
22
+ /** The address does not exist (list hygiene, not reputation). */
23
+ | "no_such_user"
24
+ /** The recipient mailbox is over quota. */
25
+ | "mailbox_full"
26
+ /** Transient: retried, not dead. */
27
+ | "delayed"
28
+ /**
29
+ * The envelope proves it is a DSN, but nothing classifiable was stored — the
30
+ * body was never synced, so only the subject survives. Kept distinct from
31
+ * "other" because counting these as "other" reads as "we looked and found
32
+ * nothing wrong", when the truth is that we could not look.
33
+ */
34
+ | "unclassified"
35
+ /** A DSN with real content we can recognize but not attribute. */
36
+ | "other";
37
+ /** Every kind, in one place, so callers can build a complete counts map. */
38
+ export declare const DELIVERY_STATUS_NOTIFICATION_KINDS: readonly ["provider_rejected", "no_such_user", "mailbox_full", "delayed", "unclassified", "other"];
39
+ /** Which mailbox provider generated the notification, when it is knowable. */
40
+ export type DeliveryStatusNotificationProvider = "google" | "microsoft" | "unknown";
41
+ export type DeliveryStatusNotification = {
42
+ kind: DeliveryStatusNotificationKind;
43
+ provider: DeliveryStatusNotificationProvider;
44
+ /** The address the original message was addressed to, when the DSN names it. */
45
+ recipient: string | null;
46
+ /** Enhanced status code (5.7.1) when present, else the bare SMTP reply (550). */
47
+ smtpCode: string | null;
48
+ };
49
+ /**
50
+ * Classify one stored inbound message as a delivery-status notification.
51
+ *
52
+ * Returns null when the message is not a DSN at all — the caller's SQL prefilter
53
+ * is intentionally wide, and a false positive here would invent a deliverability
54
+ * incident out of a customer reply.
55
+ */
56
+ export declare function classifyDeliveryStatusNotification(input: {
57
+ fromAddress?: string | null;
58
+ subject?: string | null;
59
+ bodyText?: string | null;
60
+ }): DeliveryStatusNotification | null;
@@ -0,0 +1,120 @@
1
+ /**
2
+ * PURE classifier for delivery-status notifications (bounce NDRs / DSNs) that
3
+ * land back in a sending mailbox's own inbox.
4
+ *
5
+ * WHY this exists: a mailbox-provider reputation block is the loudest
6
+ * deliverability signal a workspace ever receives, and it arrives as an ordinary
7
+ * inbound email — "Message rejected", "support.google.com/mail/answer/69585" —
8
+ * that no OXYGEN surface reads. A warm-up vendor reporting 96% inbox placement
9
+ * cannot contradict it, because the vendor only measures its own seed network.
10
+ * Classifying the DSN turns the workspace's own inbox into first-party
11
+ * deliverability evidence: OXYGEN's send log said we sent, the DSN says the
12
+ * receiving provider refused it, and only the second one is the truth.
13
+ *
14
+ * Pure by design — no clock, no I/O, no provider call — so the same
15
+ * classification runs in the tenant rollup, a worker sweep, and a unit test.
16
+ * Output is stable machine data (kinds and codes), never prose.
17
+ */
18
+ /** Every kind, in one place, so callers can build a complete counts map. */
19
+ export const DELIVERY_STATUS_NOTIFICATION_KINDS = [
20
+ "provider_rejected",
21
+ "no_such_user",
22
+ "mailbox_full",
23
+ "delayed",
24
+ "unclassified",
25
+ "other",
26
+ ];
27
+ // A message only enters classification when its envelope looks like a DSN. The
28
+ // SQL prefilter that feeds this is deliberately loose (ILIKE), so this gate is
29
+ // what keeps an ordinary customer reply that happens to contain the word
30
+ // "blocked" out of the deliverability evidence.
31
+ const DSN_FROM_PATTERN = /(mailer-daemon|mail-daemon|postmaster|microsoftexchange)/i;
32
+ const DSN_SUBJECT_PATTERN = /(undeliver|delivery status notification|mail delivery fail|delivery has failed|returned mail)/i;
33
+ const GOOGLE_BLOCK_URL = /support\.google\.com\/mail\/answer\/69585/i;
34
+ const GOOGLE_FROM = /@(?:[a-z0-9-]+\.)*(?:googlemail|google)\.com\b/i;
35
+ const MICROSOFT_FROM = /(?:@(?:[a-z0-9-]+\.)*(?:outlook|office365|hotmail|microsoft)\.com\b|microsoftexchange|\bexchange\b)/i;
36
+ const MICROSOFT_BODY = /(microsoft exchange|exchange server|office\s?365)/i;
37
+ // Enhanced status code (RFC 3463): class.subject.detail, where subject/detail can
38
+ // be multi-digit (Google emits 5.7.708). Kept separate from the bare 3-digit SMTP
39
+ // reply so a caller can tell "550" from "5.7.1".
40
+ const ENHANCED_CODE = /\b([2-5]\.\d{1,3}\.\d{1,3})\b/;
41
+ const SMTP_REPLY_CODE = /\b([2-5]\d{2})\b/;
42
+ const REPUTATION_BLOCK = /\b5\.7\.\d{1,3}\b/;
43
+ const MAILBOX_FULL_CODE = /\b[45]\.2\.2\b/;
44
+ const NO_SUCH_USER_CODE = /\b5\.1\.\d{1,3}\b/;
45
+ const TRANSIENT_CODE = /(\b4\.\d{1,3}\.\d{1,3}\b|\b4\d{2}\b)/;
46
+ const MAILBOX_FULL_TEXT = /(mailbox (?:is )?full|over quota|quota exceeded|insufficient storage)/i;
47
+ const NO_SUCH_USER_TEXT = /(does ?n[o']?t exist|nosuchuser|no such user|user unknown|unknown user|address not found|recipient (?:address )?(?:not found|rejected)|invalid recipient)/i;
48
+ const PROVIDER_REJECTED_TEXT = /(message rejected|has been blocked|\bblocked\b|\bbanned\b|\bspam\b|local policy violation|unsolicited|reputation)/i;
49
+ const TRANSIENT_TEXT = /(temporar|will retry|try again later|rate ?limit|throttl)/i;
50
+ // Greedy address capture: the character class already stops at whitespace and
51
+ // angle brackets, so trailing sentence punctuation is stripped afterwards. A lazy
52
+ // capture would stop at the dot inside "gmail.com".
53
+ const ADDRESS = "([^\\s<>,;()]+@[^\\s<>,;()]+)";
54
+ const RECIPIENT_PATTERNS = [
55
+ new RegExp(`your message to\\s+<?${ADDRESS}>?`, "i"),
56
+ new RegExp(`\\bto\\s+<?${ADDRESS}>?\\s+(?:was|were|could not|couldn't|has been|had been)`, "i"),
57
+ // Machine-readable half of a real RFC 3464 report, when the provider sends one.
58
+ new RegExp(`(?:final|original)-recipient:\\s*rfc822;\\s*<?${ADDRESS}>?`, "i"),
59
+ ];
60
+ function readRecipient(haystack) {
61
+ for (const pattern of RECIPIENT_PATTERNS) {
62
+ const match = pattern.exec(haystack);
63
+ const value = match?.[1]?.replace(/[.,;:>]+$/, "").trim().toLowerCase();
64
+ if (value && value.includes("@"))
65
+ return value;
66
+ }
67
+ return null;
68
+ }
69
+ function readProvider(from, haystack) {
70
+ // The 69585 URL is decisive: only Google emits it, and it is the exact article
71
+ // a Gmail reputation block cites.
72
+ if (GOOGLE_BLOCK_URL.test(haystack) || GOOGLE_FROM.test(from))
73
+ return "google";
74
+ if (MICROSOFT_FROM.test(from) || MICROSOFT_BODY.test(haystack))
75
+ return "microsoft";
76
+ return "unknown";
77
+ }
78
+ function readKind(subject, haystack, hasBody) {
79
+ // Order is load-bearing. A Google "(Delay)" report can quote block-sounding
80
+ // prose while the message is still queued, so the transient marker wins first;
81
+ // an explicit enhanced code then beats keyword matching, because "blocked" and
82
+ // "spam" appear in boilerplate that a 5.1.1 no-such-user report also carries.
83
+ if (/\(delay\)/i.test(subject))
84
+ return "delayed";
85
+ if (REPUTATION_BLOCK.test(haystack))
86
+ return "provider_rejected";
87
+ if (MAILBOX_FULL_CODE.test(haystack) || MAILBOX_FULL_TEXT.test(haystack))
88
+ return "mailbox_full";
89
+ if (NO_SUCH_USER_CODE.test(haystack) || NO_SUCH_USER_TEXT.test(haystack))
90
+ return "no_such_user";
91
+ if (PROVIDER_REJECTED_TEXT.test(haystack))
92
+ return "provider_rejected";
93
+ if (TRANSIENT_CODE.test(haystack) || TRANSIENT_TEXT.test(haystack))
94
+ return "delayed";
95
+ // Nothing matched AND there was no body to match against: the sync stored the
96
+ // envelope only. Reporting that as "other" would be a false clean.
97
+ return hasBody ? "other" : "unclassified";
98
+ }
99
+ /**
100
+ * Classify one stored inbound message as a delivery-status notification.
101
+ *
102
+ * Returns null when the message is not a DSN at all — the caller's SQL prefilter
103
+ * is intentionally wide, and a false positive here would invent a deliverability
104
+ * incident out of a customer reply.
105
+ */
106
+ export function classifyDeliveryStatusNotification(input) {
107
+ const from = typeof input.fromAddress === "string" ? input.fromAddress : "";
108
+ const subject = typeof input.subject === "string" ? input.subject : "";
109
+ const body = typeof input.bodyText === "string" ? input.bodyText : "";
110
+ if (!DSN_FROM_PATTERN.test(from) && !DSN_SUBJECT_PATTERN.test(subject))
111
+ return null;
112
+ const haystack = `${subject}\n${body}`;
113
+ const smtpCode = ENHANCED_CODE.exec(haystack)?.[1] ?? SMTP_REPLY_CODE.exec(haystack)?.[1] ?? null;
114
+ return {
115
+ kind: readKind(subject, haystack, body.trim().length > 0),
116
+ provider: readProvider(from, haystack),
117
+ recipient: readRecipient(haystack),
118
+ smtpCode,
119
+ };
120
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * PURE warm-up readiness rule: what daily cold volume a sending mailbox can
3
+ * actually carry, given its warm-up age rather than a provider's opinion of it.
4
+ *
5
+ * WHY it lives in @oxygen/shared and not next to the other email-health rules in
6
+ * @oxygen/integrations: the tenant rollup computes this per mailbox, and
7
+ * @oxygen/tenant-db must never import @oxygen/integrations (integrations depends
8
+ * on tenant-db). Duplicating the thresholds so each package could own a copy is
9
+ * exactly how two surfaces start recommending different numbers, so the rule is
10
+ * defined once here and re-exported from
11
+ * packages/integrations/src/email-health/health-state.ts, which stays the
12
+ * email-health surface every caller reads.
13
+ *
14
+ * DIRECTIONAL, like everything else in the deliverability cluster: it produces a
15
+ * recommendation and a launch-preview warning. It never clamps a cap, never
16
+ * pauses a mailbox, and never changes what a sequence sends.
17
+ */
18
+ /** Below this warm-up day a mailbox should carry only the starter cold volume. */
19
+ export declare const WARMUP_EARLY_DAY_LIMIT = 14;
20
+ /** Below this warm-up day a mailbox should stay at the reduced cold volume. */
21
+ export declare const WARMUP_ESTABLISHED_DAY_LIMIT = 28;
22
+ /** Recommended cold sends/day before day 14 (and for an un-warmed young mailbox). */
23
+ export declare const WARMUP_EARLY_DAILY_CAP = 5;
24
+ /** Recommended cold sends/day between day 14 and day 28. */
25
+ export declare const WARMUP_ESTABLISHED_DAILY_CAP = 10;
26
+ /** A warm-up health score under this is treated as "not ready for volume". */
27
+ export declare const WARMUP_HEALTH_SCORE_FLOOR = 60;
28
+ /** Hard-bounce rate above which a fleet is burning its domains, not just its list. */
29
+ export declare const HARD_BOUNCE_RATE_CEILING = 0.03;
30
+ /** Below this send volume a bounce rate is noise, not a signal. */
31
+ export declare const HARD_BOUNCE_RATE_MIN_SENDS = 20;
32
+ /**
33
+ * The DIRECTIONAL per-mailbox daily cold-send ceiling warm-up readiness supports,
34
+ * or null when nothing in the evidence argues for holding volume back.
35
+ *
36
+ * Pure — the caller resolves `mailboxAgeDays` from created_at, so this stays
37
+ * clock-free and identical on every surface.
38
+ *
39
+ * - warm-up day < 14 -> 5/day
40
+ * - mailbox younger than 28 days, with either no active
41
+ * warm-up or no warm-up day reported at all -> 5/day
42
+ * - warm-up day < 28 -> 10/day
43
+ * - warm-up health score < 60 -> at most 5/day
44
+ * - otherwise -> null (no advice)
45
+ *
46
+ * The second rule is the one that matters for a freshly provisioned fleet: a
47
+ * mailbox whose warm-up never started (state "unknown") reports no day at all,
48
+ * and a rule keyed only on the day number would have said nothing about the exact
49
+ * mailboxes most likely to get blocked.
50
+ */
51
+ export declare function recommendedDailyCapForWarmup(input: {
52
+ warmupState?: string | null;
53
+ warmupDay?: number | null;
54
+ warmupHealthScore?: number | null;
55
+ mailboxAgeDays?: number | null;
56
+ }): number | null;
57
+ /**
58
+ * True when a hard-bounce count is high enough, over enough sends, to be a real
59
+ * reputation problem rather than list noise.
60
+ */
61
+ export declare function hardBounceRateIsHigh(input: {
62
+ hardBounces: number;
63
+ coldSends: number;
64
+ }): boolean;
@@ -0,0 +1,90 @@
1
+ /**
2
+ * PURE warm-up readiness rule: what daily cold volume a sending mailbox can
3
+ * actually carry, given its warm-up age rather than a provider's opinion of it.
4
+ *
5
+ * WHY it lives in @oxygen/shared and not next to the other email-health rules in
6
+ * @oxygen/integrations: the tenant rollup computes this per mailbox, and
7
+ * @oxygen/tenant-db must never import @oxygen/integrations (integrations depends
8
+ * on tenant-db). Duplicating the thresholds so each package could own a copy is
9
+ * exactly how two surfaces start recommending different numbers, so the rule is
10
+ * defined once here and re-exported from
11
+ * packages/integrations/src/email-health/health-state.ts, which stays the
12
+ * email-health surface every caller reads.
13
+ *
14
+ * DIRECTIONAL, like everything else in the deliverability cluster: it produces a
15
+ * recommendation and a launch-preview warning. It never clamps a cap, never
16
+ * pauses a mailbox, and never changes what a sequence sends.
17
+ */
18
+ /** Below this warm-up day a mailbox should carry only the starter cold volume. */
19
+ export const WARMUP_EARLY_DAY_LIMIT = 14;
20
+ /** Below this warm-up day a mailbox should stay at the reduced cold volume. */
21
+ export const WARMUP_ESTABLISHED_DAY_LIMIT = 28;
22
+ /** Recommended cold sends/day before day 14 (and for an un-warmed young mailbox). */
23
+ export const WARMUP_EARLY_DAILY_CAP = 5;
24
+ /** Recommended cold sends/day between day 14 and day 28. */
25
+ export const WARMUP_ESTABLISHED_DAILY_CAP = 10;
26
+ /** A warm-up health score under this is treated as "not ready for volume". */
27
+ export const WARMUP_HEALTH_SCORE_FLOOR = 60;
28
+ /** Hard-bounce rate above which a fleet is burning its domains, not just its list. */
29
+ export const HARD_BOUNCE_RATE_CEILING = 0.03;
30
+ /** Below this send volume a bounce rate is noise, not a signal. */
31
+ export const HARD_BOUNCE_RATE_MIN_SENDS = 20;
32
+ /** Warm-up states in which a vendor is actively conditioning the mailbox. */
33
+ const ACTIVE_WARMUP_STATES = new Set(["warming", "active"]);
34
+ function finiteOrNull(value) {
35
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
36
+ }
37
+ /**
38
+ * The DIRECTIONAL per-mailbox daily cold-send ceiling warm-up readiness supports,
39
+ * or null when nothing in the evidence argues for holding volume back.
40
+ *
41
+ * Pure — the caller resolves `mailboxAgeDays` from created_at, so this stays
42
+ * clock-free and identical on every surface.
43
+ *
44
+ * - warm-up day < 14 -> 5/day
45
+ * - mailbox younger than 28 days, with either no active
46
+ * warm-up or no warm-up day reported at all -> 5/day
47
+ * - warm-up day < 28 -> 10/day
48
+ * - warm-up health score < 60 -> at most 5/day
49
+ * - otherwise -> null (no advice)
50
+ *
51
+ * The second rule is the one that matters for a freshly provisioned fleet: a
52
+ * mailbox whose warm-up never started (state "unknown") reports no day at all,
53
+ * and a rule keyed only on the day number would have said nothing about the exact
54
+ * mailboxes most likely to get blocked.
55
+ */
56
+ export function recommendedDailyCapForWarmup(input) {
57
+ const day = finiteOrNull(input.warmupDay);
58
+ const ageDays = finiteOrNull(input.mailboxAgeDays);
59
+ const healthScore = finiteOrNull(input.warmupHealthScore);
60
+ const warmingNow = ACTIVE_WARMUP_STATES.has((input.warmupState ?? "").trim().toLowerCase());
61
+ let cap = null;
62
+ if (day !== null && day < WARMUP_EARLY_DAY_LIMIT) {
63
+ cap = WARMUP_EARLY_DAILY_CAP;
64
+ }
65
+ else if (ageDays !== null &&
66
+ ageDays < WARMUP_ESTABLISHED_DAY_LIMIT &&
67
+ (day === null || !warmingNow)) {
68
+ // A rail that reports "warming" but no day number proves nothing about how
69
+ // far the ramp got, so age still governs. Gating this on !warmingNow alone
70
+ // left exactly that mailbox — enrolled, young, no telemetry — with no advice.
71
+ cap = WARMUP_EARLY_DAILY_CAP;
72
+ }
73
+ else if (day !== null && day < WARMUP_ESTABLISHED_DAY_LIMIT) {
74
+ cap = WARMUP_ESTABLISHED_DAILY_CAP;
75
+ }
76
+ if (healthScore !== null && healthScore < WARMUP_HEALTH_SCORE_FLOOR) {
77
+ cap = cap === null ? WARMUP_EARLY_DAILY_CAP : Math.min(cap, WARMUP_EARLY_DAILY_CAP);
78
+ }
79
+ return cap;
80
+ }
81
+ /**
82
+ * True when a hard-bounce count is high enough, over enough sends, to be a real
83
+ * reputation problem rather than list noise.
84
+ */
85
+ export function hardBounceRateIsHigh(input) {
86
+ if (!Number.isFinite(input.coldSends) || input.coldSends < HARD_BOUNCE_RATE_MIN_SENDS) {
87
+ return false;
88
+ }
89
+ return input.hardBounces / input.coldSends > HARD_BOUNCE_RATE_CEILING;
90
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Where a hosted inbox-avatar URL may be minted, and how.
3
+ *
4
+ * An avatar URL is handed to an external inbox vendor that fetches it server-side,
5
+ * later, possibly more than once — and Oxygen stores nothing about that hand-off
6
+ * afterwards, so a bad URL cannot be found again or reissued. A URL minted on a
7
+ * preview deployment would be baked into a vendor's mailbox record and die when
8
+ * that deployment does; one minted on localhost never resolves at all. So the
9
+ * origin is an allowlist, not "whatever host served this request", and the SAME
10
+ * allowlist binds every minter: the web request path (which prefers the inbound
11
+ * host) and the worker's LinkedIn-photo backfill (which only has the configured
12
+ * app URL). Two allowlists would drift, and the drift would ship as a mailbox
13
+ * photo that resolves from one deployment and 404s from the other.
14
+ */
15
+ export declare const INBOX_AVATAR_ORIGIN_HOSTS: ReadonlySet<string>;
16
+ /**
17
+ * The first candidate that is an https URL on an allowed host, normalized to
18
+ * `protocol//host`, or null when none qualifies. Returning null rather than a
19
+ * best-effort URL is the point: on localhost the only honest answer is "not from
20
+ * here", and an `http://localhost` URL would sail through this function only to
21
+ * be rejected by an https-only check one step later and much harder to read.
22
+ */
23
+ export declare function resolveInboxAvatarOrigin(candidates: readonly (string | null | undefined)[]): string | null;
24
+ /**
25
+ * The public, permanent URL for a stored inbox avatar on an already-resolved
26
+ * origin — or null when there is no origin, or the key is not an inbox avatar.
27
+ */
28
+ export declare function inboxAvatarPublicUrlForOrigin(origin: string | null, storageKey: string): string | null;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Where a hosted inbox-avatar URL may be minted, and how.
3
+ *
4
+ * An avatar URL is handed to an external inbox vendor that fetches it server-side,
5
+ * later, possibly more than once — and Oxygen stores nothing about that hand-off
6
+ * afterwards, so a bad URL cannot be found again or reissued. A URL minted on a
7
+ * preview deployment would be baked into a vendor's mailbox record and die when
8
+ * that deployment does; one minted on localhost never resolves at all. So the
9
+ * origin is an allowlist, not "whatever host served this request", and the SAME
10
+ * allowlist binds every minter: the web request path (which prefers the inbound
11
+ * host) and the worker's LinkedIn-photo backfill (which only has the configured
12
+ * app URL). Two allowlists would drift, and the drift would ship as a mailbox
13
+ * photo that resolves from one deployment and 404s from the other.
14
+ */
15
+ export const INBOX_AVATAR_ORIGIN_HOSTS = new Set([
16
+ "oxygen-agent.com",
17
+ "www.oxygen-agent.com",
18
+ "dev.oxygen-agent.com",
19
+ ]);
20
+ const INBOX_AVATAR_KEY_PREFIX = "inbox-avatars/";
21
+ /**
22
+ * The first candidate that is an https URL on an allowed host, normalized to
23
+ * `protocol//host`, or null when none qualifies. Returning null rather than a
24
+ * best-effort URL is the point: on localhost the only honest answer is "not from
25
+ * here", and an `http://localhost` URL would sail through this function only to
26
+ * be rejected by an https-only check one step later and much harder to read.
27
+ */
28
+ export function resolveInboxAvatarOrigin(candidates) {
29
+ for (const candidate of candidates) {
30
+ if (!candidate)
31
+ continue;
32
+ try {
33
+ const url = new URL(candidate);
34
+ if (url.protocol === "https:" && INBOX_AVATAR_ORIGIN_HOSTS.has(url.host)) {
35
+ return `${url.protocol}//${url.host}`;
36
+ }
37
+ }
38
+ catch {
39
+ // Not a URL at all — try the next candidate.
40
+ }
41
+ }
42
+ return null;
43
+ }
44
+ /**
45
+ * The public, permanent URL for a stored inbox avatar on an already-resolved
46
+ * origin — or null when there is no origin, or the key is not an inbox avatar.
47
+ */
48
+ export function inboxAvatarPublicUrlForOrigin(origin, storageKey) {
49
+ if (!origin || !storageKey.startsWith(INBOX_AVATAR_KEY_PREFIX))
50
+ return null;
51
+ const path = storageKey
52
+ .slice(INBOX_AVATAR_KEY_PREFIX.length)
53
+ .split("/")
54
+ .map((segment) => encodeURIComponent(segment))
55
+ .join("/");
56
+ return `${origin}/api/inbox-avatars/${path}`;
57
+ }
@@ -30,20 +30,27 @@ export * from "./copilot-journeys.js";
30
30
  export * from "./copilot-plan.js";
31
31
  export * from "./credit-guidance.js";
32
32
  export * from "./directory.js";
33
+ export * from "./email-dsn.js";
34
+ export * from "./email-warmup-readiness.js";
33
35
  export * from "./email-tracking-token.js";
34
36
  export * from "./email-unsubscribe-token.js";
35
37
  export * from "./error-redaction.js";
36
38
  export * from "./future-signup-events.js";
37
39
  export * from "./future-signup-lifecycle-projection.js";
38
40
  export * from "./feature-gates.js";
41
+ export * from "./product-analytics-core.js";
42
+ export * from "./product-analytics-environment.js";
43
+ export * from "./product-analytics-events.js";
39
44
  export * from "./hosted-ai.js";
40
45
  export * from "./identifiers.js";
41
46
  export * from "./knowledge-constants.js";
42
47
  export * from "./knowledge-bootstrap.js";
43
48
  export * from "./knowledge-links.js";
44
49
  export * from "./knowledge-markdown.js";
50
+ export * from "./knowledge-vault-markdown.js";
45
51
  export * from "./knowledge-seed-content.js";
46
52
  export * from "./langfuse.js";
53
+ export * from "./llm-usage.js";
47
54
  export * from "./linkedin-mentions.js";
48
55
  export * from "./linkedin-post-url.js";
49
56
  export * from "./linkedin-quota-denial.js";
@@ -113,3 +120,6 @@ export declare function isVersionGreater(a: string, b: string): boolean;
113
120
  /** True when `a` is a strictly lesser semantic version than `b`. */
114
121
  export declare function isVersionLess(a: string, b: string): boolean;
115
122
  export * from "./ugc.js";
123
+ export * from "./ugc-amplification-identity.js";
124
+ export * from "./knowledge-repository.js";
125
+ export * from "./knowledge-bases.js";
@@ -30,20 +30,27 @@ export * from "./copilot-journeys.js";
30
30
  export * from "./copilot-plan.js";
31
31
  export * from "./credit-guidance.js";
32
32
  export * from "./directory.js";
33
+ export * from "./email-dsn.js";
34
+ export * from "./email-warmup-readiness.js";
33
35
  export * from "./email-tracking-token.js";
34
36
  export * from "./email-unsubscribe-token.js";
35
37
  export * from "./error-redaction.js";
36
38
  export * from "./future-signup-events.js";
37
39
  export * from "./future-signup-lifecycle-projection.js";
38
40
  export * from "./feature-gates.js";
41
+ export * from "./product-analytics-core.js";
42
+ export * from "./product-analytics-environment.js";
43
+ export * from "./product-analytics-events.js";
39
44
  export * from "./hosted-ai.js";
40
45
  export * from "./identifiers.js";
41
46
  export * from "./knowledge-constants.js";
42
47
  export * from "./knowledge-bootstrap.js";
43
48
  export * from "./knowledge-links.js";
44
49
  export * from "./knowledge-markdown.js";
50
+ export * from "./knowledge-vault-markdown.js";
45
51
  export * from "./knowledge-seed-content.js";
46
52
  export * from "./langfuse.js";
53
+ export * from "./llm-usage.js";
47
54
  export * from "./linkedin-mentions.js";
48
55
  export * from "./linkedin-post-url.js";
49
56
  export * from "./linkedin-quota-denial.js";
@@ -149,3 +156,6 @@ export function isVersionLess(a, b) {
149
156
  return compareSemver(a, b) < 0;
150
157
  }
151
158
  export * from "./ugc.js";
159
+ export * from "./ugc-amplification-identity.js";
160
+ export * from "./knowledge-repository.js";
161
+ export * from "./knowledge-bases.js";
@@ -0,0 +1,74 @@
1
+ export type KnowledgeBaseViewType = "table" | "cards" | "kanban";
2
+ export type KnowledgeBaseFilter = string | {
3
+ and?: KnowledgeBaseFilter[];
4
+ or?: KnowledgeBaseFilter[];
5
+ not?: KnowledgeBaseFilter[];
6
+ };
7
+ export type KnowledgeBaseIssue = {
8
+ code: "invalid_base" | "unsupported_filter" | "unsupported_view" | "invalid_view";
9
+ message: string;
10
+ expression?: string;
11
+ view?: string;
12
+ };
13
+ export type KnowledgeBaseProperty = {
14
+ displayName?: string;
15
+ raw: Record<string, unknown>;
16
+ };
17
+ export type KnowledgeBaseView = {
18
+ type: string;
19
+ name: string;
20
+ groupBy?: {
21
+ property: string;
22
+ direction: "ASC" | "DESC";
23
+ };
24
+ order: string[];
25
+ boardColumns: string[];
26
+ cardTitleProperty?: string;
27
+ sort: Array<{
28
+ property: string;
29
+ direction: "ASC" | "DESC";
30
+ }>;
31
+ filters?: KnowledgeBaseFilter;
32
+ columnColors: Record<string, string>;
33
+ wipLimits: Record<string, number>;
34
+ raw: Record<string, unknown>;
35
+ };
36
+ export type KnowledgeBaseDefinition = {
37
+ path: string;
38
+ name: string;
39
+ filters?: KnowledgeBaseFilter;
40
+ properties: Record<string, KnowledgeBaseProperty>;
41
+ views: KnowledgeBaseView[];
42
+ raw: Record<string, unknown>;
43
+ };
44
+ export type KnowledgeBaseFile = {
45
+ id: string;
46
+ path: string;
47
+ body?: string;
48
+ properties: Record<string, unknown>;
49
+ revision: number;
50
+ };
51
+ export type KnowledgeBaseParseResult = {
52
+ definition: KnowledgeBaseDefinition | null;
53
+ issues: KnowledgeBaseIssue[];
54
+ };
55
+ export type KnowledgeBaseEvaluation = {
56
+ files: KnowledgeBaseFile[];
57
+ issues: KnowledgeBaseIssue[];
58
+ };
59
+ export type KnowledgeBaseGroup = {
60
+ key: string;
61
+ title: string;
62
+ files: KnowledgeBaseFile[];
63
+ };
64
+ export declare function parseKnowledgeBase(path: string, source: string): KnowledgeBaseParseResult;
65
+ export declare function knowledgeBasePropertyKey(property: string): string;
66
+ export declare function knowledgeBasePropertyValue(file: KnowledgeBaseFile, property: string): unknown;
67
+ export declare function knowledgeBasePropertyText(file: KnowledgeBaseFile, property: string): string;
68
+ export declare function evaluateKnowledgeBaseView(definition: KnowledgeBaseDefinition, view: KnowledgeBaseView, files: readonly KnowledgeBaseFile[]): KnowledgeBaseEvaluation;
69
+ export declare function knowledgeBaseGroups(view: KnowledgeBaseView, files: readonly KnowledgeBaseFile[], configuredColumns?: readonly string[]): KnowledgeBaseGroup[];
70
+ export declare function knowledgeBaseDisplayName(definition: KnowledgeBaseDefinition, property: string): string;
71
+ export declare function knowledgeBaseCardTitle(file: KnowledgeBaseFile, view: KnowledgeBaseView): string;
72
+ export declare function parseBaseBoardColumnOrder(value: unknown): Record<string, string[]>;
73
+ /** Resolve Base Board's `<folder>::<view>::<property>` lane order, with view-name shorthand for API callers. */
74
+ export declare function resolveBaseBoardColumnOrder(definition: KnowledgeBaseDefinition, view: KnowledgeBaseView, configured: Readonly<Record<string, readonly string[]>>): string[];