@oxygen-agent/cli 1.982.3 → 1.1003.12

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 (127) hide show
  1. package/README.md +1 -1
  2. package/dist/admin-primary-providers-render.d.ts +0 -2
  3. package/dist/admin-primary-providers-render.js +1 -1
  4. package/dist/browser-login.js +1 -4
  5. package/dist/command-manifest.d.ts +3 -2
  6. package/dist/command-manifest.js +10 -0
  7. package/dist/credentials.d.ts +1 -1
  8. package/dist/functions-commands.js +27 -7
  9. package/dist/help.d.ts +29 -0
  10. package/dist/help.js +139 -0
  11. package/dist/index.js +1875 -164
  12. package/dist/knowledge-mirror.d.ts +2 -2
  13. package/dist/runtime.d.ts +0 -15
  14. package/dist/runtime.js +1 -1
  15. package/dist/session.d.ts +4 -3
  16. package/dist/skills.d.ts +8 -7
  17. package/dist/skills.js +24 -10
  18. package/dist/transcript.d.ts +2 -1
  19. package/dist/ugc-commands.d.ts +3 -6
  20. package/dist/ugc-commands.js +2 -1200
  21. package/dist/util.d.ts +1 -1
  22. package/dist/util.js +1 -3
  23. package/node_modules/@oxygen/cli-ugc/dist/commands.d.ts +3 -0
  24. package/node_modules/@oxygen/cli-ugc/dist/commands.js +1178 -0
  25. package/node_modules/@oxygen/cli-ugc/dist/field-parser.d.ts +7 -0
  26. package/node_modules/@oxygen/cli-ugc/dist/field-parser.js +25 -0
  27. package/node_modules/@oxygen/cli-ugc/dist/index.d.ts +14 -0
  28. package/node_modules/@oxygen/cli-ugc/dist/index.js +5 -0
  29. package/node_modules/@oxygen/cli-ugc/package.json +15 -0
  30. package/node_modules/@oxygen/formula/dist/coerce.d.ts +10 -0
  31. package/node_modules/@oxygen/formula/dist/coerce.js +10 -0
  32. package/node_modules/@oxygen/formula/dist/expression.js +14 -1
  33. package/node_modules/@oxygen/formula/dist/formula-functions.js +136 -1
  34. package/node_modules/@oxygen/formula/dist/hash.d.ts +19 -0
  35. package/node_modules/@oxygen/formula/dist/hash.js +199 -0
  36. package/node_modules/@oxygen/formula/dist/index.d.ts +1 -0
  37. package/node_modules/@oxygen/formula/dist/index.js +1 -0
  38. package/node_modules/@oxygen/formula/dist/value-cleaners.d.ts +74 -0
  39. package/node_modules/@oxygen/formula/dist/value-cleaners.js +358 -0
  40. package/node_modules/@oxygen/shared/dist/array-utils.d.ts +5 -0
  41. package/node_modules/@oxygen/shared/dist/array-utils.js +11 -0
  42. package/node_modules/@oxygen/shared/dist/billing.d.ts +103 -47
  43. package/node_modules/@oxygen/shared/dist/billing.js +150 -40
  44. package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +17 -0
  45. package/node_modules/@oxygen/shared/dist/capability-discovery.js +114 -16
  46. package/node_modules/@oxygen/shared/dist/column-autofill.d.ts +52 -0
  47. package/node_modules/@oxygen/shared/dist/column-autofill.js +80 -0
  48. package/node_modules/@oxygen/shared/dist/column-output-fields.js +14 -10
  49. package/node_modules/@oxygen/shared/dist/company-enrichment-fields.d.ts +108 -0
  50. package/node_modules/@oxygen/shared/dist/company-enrichment-fields.js +545 -0
  51. package/node_modules/@oxygen/shared/dist/copilot-playbooks.d.ts +18 -0
  52. package/node_modules/@oxygen/shared/dist/copilot-playbooks.js +43 -0
  53. package/node_modules/@oxygen/shared/dist/copilot-skills.d.ts +15 -0
  54. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +31 -0
  55. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +41 -0
  56. package/node_modules/@oxygen/shared/dist/copilot-skills.js +6 -0
  57. package/node_modules/@oxygen/shared/dist/deploy-env.d.ts +74 -0
  58. package/node_modules/@oxygen/shared/dist/deploy-env.js +82 -0
  59. package/node_modules/@oxygen/shared/dist/dnc-rules.d.ts +130 -0
  60. package/node_modules/@oxygen/shared/dist/dnc-rules.js +221 -0
  61. package/node_modules/@oxygen/shared/dist/enrichment-intents.d.ts +103 -0
  62. package/node_modules/@oxygen/shared/dist/enrichment-intents.js +819 -0
  63. package/node_modules/@oxygen/shared/dist/error-message.d.ts +1 -0
  64. package/node_modules/@oxygen/shared/dist/error-message.js +3 -0
  65. package/node_modules/@oxygen/shared/dist/error-redaction.js +1 -3
  66. package/node_modules/@oxygen/shared/dist/external-write-policy.d.ts +33 -0
  67. package/node_modules/@oxygen/shared/dist/external-write-policy.js +68 -0
  68. package/node_modules/@oxygen/shared/dist/format-percent.d.ts +8 -0
  69. package/node_modules/@oxygen/shared/dist/format-percent.js +13 -0
  70. package/node_modules/@oxygen/shared/dist/freemail-domains.d.ts +81 -0
  71. package/node_modules/@oxygen/shared/dist/freemail-domains.js +157 -0
  72. package/node_modules/@oxygen/shared/dist/future-signup-lifecycle-projection.d.ts +1 -0
  73. package/node_modules/@oxygen/shared/dist/future-signup-lifecycle-projection.js +1 -1
  74. package/node_modules/@oxygen/shared/dist/index.d.ts +14 -0
  75. package/node_modules/@oxygen/shared/dist/index.js +14 -0
  76. package/node_modules/@oxygen/shared/dist/json-path.js +1 -3
  77. package/node_modules/@oxygen/shared/dist/knowledge-bases.js +1 -3
  78. package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.d.ts +17 -2
  79. package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.js +28 -6
  80. package/node_modules/@oxygen/shared/dist/langfuse.d.ts +12 -1
  81. package/node_modules/@oxygen/shared/dist/langfuse.js +57 -8
  82. package/node_modules/@oxygen/shared/dist/linkedin-countries.d.ts +32 -0
  83. package/node_modules/@oxygen/shared/dist/linkedin-countries.js +359 -0
  84. package/node_modules/@oxygen/shared/dist/log-sink-selector.d.ts +39 -0
  85. package/node_modules/@oxygen/shared/dist/log-sink-selector.js +56 -0
  86. package/node_modules/@oxygen/shared/dist/log.d.ts +1 -0
  87. package/node_modules/@oxygen/shared/dist/log.js +6 -1
  88. package/node_modules/@oxygen/shared/dist/object-storage.d.ts +17 -0
  89. package/node_modules/@oxygen/shared/dist/object-storage.js +21 -0
  90. package/node_modules/@oxygen/shared/dist/otlp-log-sink.d.ts +54 -0
  91. package/node_modules/@oxygen/shared/dist/otlp-log-sink.js +213 -0
  92. package/node_modules/@oxygen/shared/dist/plan-capabilities.js +1 -0
  93. package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +23 -22
  94. package/node_modules/@oxygen/shared/dist/plan-limits.js +45 -18
  95. package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +48 -41
  96. package/node_modules/@oxygen/shared/dist/pricing-sheet.js +36 -25
  97. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +22 -22
  98. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +40 -34
  99. package/node_modules/@oxygen/shared/dist/product-analytics-environment.js +9 -0
  100. package/node_modules/@oxygen/shared/dist/product-analytics-events.d.ts +15 -0
  101. package/node_modules/@oxygen/shared/dist/product-analytics-events.js +15 -0
  102. package/node_modules/@oxygen/shared/dist/rate-window.d.ts +5 -0
  103. package/node_modules/@oxygen/shared/dist/rate-window.js +8 -0
  104. package/node_modules/@oxygen/shared/dist/research-output-contract.d.ts +33 -1
  105. package/node_modules/@oxygen/shared/dist/research-output-contract.js +65 -5
  106. package/node_modules/@oxygen/shared/dist/search-vocab.js +4 -5
  107. package/node_modules/@oxygen/shared/dist/select-options.js +6 -1
  108. package/node_modules/@oxygen/shared/dist/sequence-crm-events.d.ts +1 -1
  109. package/node_modules/@oxygen/shared/dist/sequence-failures.js +1 -5
  110. package/node_modules/@oxygen/shared/dist/sequence-hubspot-sync.d.ts +1 -1
  111. package/node_modules/@oxygen/shared/dist/sequences.d.ts +49 -0
  112. package/node_modules/@oxygen/shared/dist/sequences.js +134 -4
  113. package/node_modules/@oxygen/shared/dist/spend-safety.d.ts +22 -10
  114. package/node_modules/@oxygen/shared/dist/spend-safety.js +15 -21
  115. package/node_modules/@oxygen/shared/dist/sql-rows.d.ts +1 -0
  116. package/node_modules/@oxygen/shared/dist/sql-rows.js +3 -0
  117. package/node_modules/@oxygen/shared/dist/telemetry.js +9 -1
  118. package/node_modules/@oxygen/shared/dist/type-guards.d.ts +22 -0
  119. package/node_modules/@oxygen/shared/dist/type-guards.js +35 -0
  120. package/node_modules/@oxygen/shared/dist/value-readers.d.ts +21 -0
  121. package/node_modules/@oxygen/shared/dist/value-readers.js +59 -0
  122. package/node_modules/@oxygen/shared/dist/version.js +1 -1
  123. package/node_modules/@oxygen/shared/package.json +60 -0
  124. package/node_modules/@oxygen/workflows/dist/graph/expression.js +2 -5
  125. package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.d.ts +15 -15
  126. package/node_modules/@oxygen/workflows/dist/graph/params.js +1 -1
  127. package/package.json +6 -3
@@ -0,0 +1 @@
1
+ export declare function errorMessage(error: unknown): string;
@@ -0,0 +1,3 @@
1
+ export function errorMessage(error) {
2
+ return error instanceof Error ? error.message : String(error);
3
+ }
@@ -20,6 +20,7 @@
20
20
  // idempotent, never throws, and never widens a message.
21
21
  import { redactSecretsInString } from "./redaction.js";
22
22
  import { isRetryableConcurrencyError, redactSqlParameters } from "./sql-error.js";
23
+ import { isRecord } from "./type-guards.js";
23
24
  // Whole fields whose NAME marks them secret. Unchanged from the operation-event
24
25
  // writer this was extracted from; substring credentials (Bearer/sk-/DB URLs) are
25
26
  // scrubbed separately by redactSecretsInString.
@@ -306,6 +307,3 @@ export function summarizeProviderBody(body) {
306
307
  function truncate(value, maxLength) {
307
308
  return value.length > maxLength ? `${value.slice(0, maxLength - 3)}...` : value;
308
309
  }
309
- function isRecord(value) {
310
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
311
- }
@@ -0,0 +1,33 @@
1
+ export type ExternalWritePolicy = "allow" | "deny";
2
+ /** The error code every refusal carries, on every surface. */
3
+ export declare const EXTERNAL_WRITES_DENIED_CODE = "external_writes_denied";
4
+ export declare const EXTERNAL_WRITES_ENV_VAR = "OXYGEN_EXTERNAL_WRITES";
5
+ type PolicyEnv = {
6
+ [key: string]: string | undefined;
7
+ };
8
+ /**
9
+ * `deny` when `OXYGEN_EXTERNAL_WRITES` is exactly `deny` (trimmed,
10
+ * case-insensitive), `allow` for anything else including unset.
11
+ *
12
+ * Read per call rather than memoized at module load: the value is read on paths
13
+ * a test needs to drive both ways, and a process-static snapshot would make the
14
+ * denial untestable in exactly the suite that has to prove it.
15
+ */
16
+ export declare function resolveExternalWritePolicy(env?: PolicyEnv): ExternalWritePolicy;
17
+ /** Convenience predicate for callers that only branch. */
18
+ export declare function externalWritesDenied(env?: PolicyEnv): boolean;
19
+ /**
20
+ * Refuse `action` when this environment denies external writes.
21
+ *
22
+ * Throws `OxygenError` with code `external_writes_denied` so the refusal travels
23
+ * the CLI/MCP envelope, the run record and the worker's job failure the same way
24
+ * every other refusal does — a shadow that swallows the denial into a generic
25
+ * failure teaches its operator nothing.
26
+ *
27
+ * Logged at `warn`, not `error`: a refusal here is the control WORKING, and the
28
+ * shadow's soak is expected to produce a stream of them. The message names both
29
+ * the action and the variable, because the person reading it in Axiom is usually
30
+ * asking "why did nothing send" and the answer is a configuration they set.
31
+ */
32
+ export declare function assertExternalWritesAllowed(action: string, env?: PolicyEnv): void;
33
+ export {};
@@ -0,0 +1,68 @@
1
+ // The environment-level external-write denial, and the one place that reads
2
+ // `OXYGEN_EXTERNAL_WRITES`.
3
+ //
4
+ // WHY THIS EXISTS AND WHY IT IS NOT A CLASSIFIER. A production *shadow* holds a
5
+ // restored copy of production: real mailbox OAuth tokens, real provider
6
+ // credentials, real customer records — while managed production is still live
7
+ // and doing the same work. The restore script scrubs those columns, but a scrub
8
+ // is data hygiene, not a guarantee: one un-nulled column is one real send, to a
9
+ // real prospect, twice. So the shadow also fails closed at the platform
10
+ // boundary (`.agents/skills/oxygen-self-host-cutover`, Part 1 Control 3).
11
+ //
12
+ // THE ORDERING IS THE SAFETY PROPERTY, and it is the same lesson
13
+ // `EXTERNAL_REACH_CAPABILITY_NAMES` (packages/copilot/src/policy.ts) was written
14
+ // to record: `capabilityLooksPaid` read ARGUMENT NAMES, so a write-capable tool
15
+ // declaring `max_credits` classified `paid` and could never reach the
16
+ // `external_write` branch — which made a real outbound launch waivable by an
17
+ // approval-free spend budget on a declared ceiling of one credit. A denial that
18
+ // runs anywhere except FIRST inherits exactly that defect. Every caller here
19
+ // asks this before it classifies, before it evaluates an allowance, an approval
20
+ // or a policy rule, and before it constructs a provider client.
21
+ //
22
+ // UNSET MEANS ALLOW, deliberately and permanently. Managed production must be
23
+ // byte-identical to the day before this module landed; only a shadow sets the
24
+ // variable. `deny` is the only recognised denial value — a typo (`denied`,
25
+ // `true`, `1`) allows, which is the fail-OPEN direction and is the right one
26
+ // here: a variable nobody set must never take production off the air, and the
27
+ // shadow's operator proves the denial by ATTEMPTING a send and observing the
28
+ // refusal rather than by trusting the configuration (same skill, "Proving it,
29
+ // rather than believing it").
30
+ import { OxygenError } from "./cli-result.js";
31
+ import { log } from "./log.js";
32
+ /** The error code every refusal carries, on every surface. */
33
+ export const EXTERNAL_WRITES_DENIED_CODE = "external_writes_denied";
34
+ export const EXTERNAL_WRITES_ENV_VAR = "OXYGEN_EXTERNAL_WRITES";
35
+ /**
36
+ * `deny` when `OXYGEN_EXTERNAL_WRITES` is exactly `deny` (trimmed,
37
+ * case-insensitive), `allow` for anything else including unset.
38
+ *
39
+ * Read per call rather than memoized at module load: the value is read on paths
40
+ * a test needs to drive both ways, and a process-static snapshot would make the
41
+ * denial untestable in exactly the suite that has to prove it.
42
+ */
43
+ export function resolveExternalWritePolicy(env = process.env) {
44
+ return env[EXTERNAL_WRITES_ENV_VAR]?.trim().toLowerCase() === "deny" ? "deny" : "allow";
45
+ }
46
+ /** Convenience predicate for callers that only branch. */
47
+ export function externalWritesDenied(env = process.env) {
48
+ return resolveExternalWritePolicy(env) === "deny";
49
+ }
50
+ /**
51
+ * Refuse `action` when this environment denies external writes.
52
+ *
53
+ * Throws `OxygenError` with code `external_writes_denied` so the refusal travels
54
+ * the CLI/MCP envelope, the run record and the worker's job failure the same way
55
+ * every other refusal does — a shadow that swallows the denial into a generic
56
+ * failure teaches its operator nothing.
57
+ *
58
+ * Logged at `warn`, not `error`: a refusal here is the control WORKING, and the
59
+ * shadow's soak is expected to produce a stream of them. The message names both
60
+ * the action and the variable, because the person reading it in Axiom is usually
61
+ * asking "why did nothing send" and the answer is a configuration they set.
62
+ */
63
+ export function assertExternalWritesAllowed(action, env = process.env) {
64
+ if (resolveExternalWritePolicy(env) !== "deny")
65
+ return;
66
+ log("warn", "external_writes.denied", { action });
67
+ throw new OxygenError(EXTERNAL_WRITES_DENIED_CODE, `External writes are denied in this environment (${EXTERNAL_WRITES_ENV_VAR}=deny), so "${action}" cannot run.`, { details: { action, env_var: EXTERNAL_WRITES_ENV_VAR } });
68
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Canonical percentage formatting shared by the web app and MCP surfaces.
3
+ */
4
+ /**
5
+ * Format a 0..1 ratio as an en-US percentage with at most one decimal; `null`
6
+ * renders as an em dash. The locale is pinned so web and MCP cannot drift.
7
+ */
8
+ export declare function formatPercent(value: number | null): string;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Canonical percentage formatting shared by the web app and MCP surfaces.
3
+ */
4
+ /**
5
+ * Format a 0..1 ratio as an en-US percentage with at most one decimal; `null`
6
+ * renders as an em dash. The locale is pinned so web and MCP cannot drift.
7
+ */
8
+ export function formatPercent(value) {
9
+ return value === null ? "—" : new Intl.NumberFormat("en-US", {
10
+ style: "percent",
11
+ maximumFractionDigits: 1,
12
+ }).format(value);
13
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Consumer / freemail mail hosts — the ONE list, in two deliberate tiers.
3
+ *
4
+ * EIGHT call sites used to carry their own hand-typed copy of this concept — 101, 34, 34,
5
+ * 25, 22, 18, 13 and 12 entries — none importing another, none covered by a test
6
+ * asserting they agreed, and two of them byte-identical forks that nothing kept in sync.
7
+ * The same address resolved differently depending on which copy a code path happened to
8
+ * reach. This module is the single source of truth; `packages/shared` is the only leaf
9
+ * package all eight can reach without inverting a dependency edge.
10
+ *
11
+ * WHY TWO TIERS, and why they must not be collapsed into one:
12
+ *
13
+ * The callers are not asking the same question, and the cost of a wrong answer is
14
+ * asymmetric in opposite directions.
15
+ *
16
+ * - `FREEMAIL_DOMAINS` (core) answers "may I treat this domain as a shared EMPLOYER?"
17
+ * Two consumers ride on that answer, and they fail in OPPOSITE directions, which is
18
+ * why this tier is conservative rather than generous:
19
+ *
20
+ * 1. The company-scope reply stop (`settings.stop_on_reply_scope = "company_domain"`)
21
+ * terminalizes an enrollment's colleagues by matching domain. A host wrongly IN
22
+ * this tier is harmless here; a host wrongly OUT of it stops strangers who merely
23
+ * share a mail host — real, customer-visible damage.
24
+ * 2. Company derivation from a person's email domain
25
+ * (`deriveCompanyDomainFromEmail`, `packages/tenant-db/src/table-link-plan.ts`)
26
+ * drops freemail before offering a domain as a company join key. Here the
27
+ * polarity REVERSES: a host wrongly OUT of this tier makes OXYGEN mint a company
28
+ * record for a mail provider, so every @<host> person collapses onto one fake
29
+ * account. Where that derivation is wired into record creation, removing a host
30
+ * from this tier silently starts creating accounts for it.
31
+ *
32
+ * So a host must be in this tier to be safe for BOTH, and the safe direction for an
33
+ * edit is to ADD, never to remove or relocate to the broad tier. Extend it only with
34
+ * hosts that are unambiguously consumer mailboxes — and note the ORDER caveat in
35
+ * `packages/tenant-db/src/freemail-domains.ts`, because a core-tier edit also changes
36
+ * a formula string already persisted in every tenant's column definitions.
37
+ *
38
+ * - `PERSONAL_EMAIL_DOMAINS` (broad) answers "is this domain a poor stand-in for a
39
+ * COMPANY identity?" — used to skip fetching a workspace icon, to reject a personal
40
+ * host typed into the marketing hero form, to avoid picking a personal address as a
41
+ * person's work email, and to avoid treating a meeting participant's mail host as their
42
+ * employer. Here a false positive costs nothing (we simply decline to guess a company)
43
+ * while a miss produces a junk company/icon. So this tier is the superset, including
44
+ * regional and privacy-focused providers.
45
+ *
46
+ * The broad tier is a strict superset of the core tier by construction below, so a host
47
+ * can never be "freemail but not personal" — exactly the incoherence the forks had
48
+ * developed (the old core list carried `t-online.de`; several broad lists did not).
49
+ *
50
+ * The broad tier is the union of all eight former lists. Most of its bulk comes from the
51
+ * old `packages/formula` copy, which was by far the best maintained and is the only one
52
+ * that covered country-specific variants (`yahoo.co.uk`, `hotmail.fr`, `orange.fr`,
53
+ * `wp.pl`, `uol.com.br` …) and ISP mailboxes (`comcast.net`, `btinternet.com`). Every
54
+ * other caller was silently missing those.
55
+ */
56
+ /** Tier 1 — see the module note. Small on purpose. */
57
+ export declare const FREEMAIL_DOMAINS: ReadonlySet<string>;
58
+ /** Tier 2 — a strict superset of {@link FREEMAIL_DOMAINS}. */
59
+ export declare const PERSONAL_EMAIL_DOMAINS: ReadonlySet<string>;
60
+ /**
61
+ * Reduce an address or a bare domain to a comparable mail host: lowercased, trimmed,
62
+ * everything before the last '@' dropped, and a trailing FQDN dot removed. Returns null
63
+ * for junk/empty/nullish input so callers can distinguish "not a host" from "not in the
64
+ * set". Previously each fork normalized differently — one did full address handling, two
65
+ * only lowercased a bare domain — so the same input could resolve differently depending
66
+ * on which copy you happened to call.
67
+ */
68
+ export declare function normalizeMailHost(value: string | null | undefined): string | null;
69
+ /**
70
+ * True when the host is a tier-1 consumer mailbox, i.e. it NEVER identifies a shared
71
+ * employer. Accepts a bare domain or a full address. Unknown input returns false: an
72
+ * unrecognized domain is treated as a company domain, which keeps the company-scope
73
+ * stop's safe default identical to lead-scope.
74
+ */
75
+ export declare function isFreemailDomain(domain: string | null | undefined): boolean;
76
+ /**
77
+ * True when the host is a personal mailbox of any kind (tier 2). Use this for "should I
78
+ * infer a company from this domain?"; use {@link isFreemailDomain} when a false positive
79
+ * would wrongly group unrelated people as colleagues.
80
+ */
81
+ export declare function isPersonalEmailDomain(domain: string | null | undefined): boolean;
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Consumer / freemail mail hosts — the ONE list, in two deliberate tiers.
3
+ *
4
+ * EIGHT call sites used to carry their own hand-typed copy of this concept — 101, 34, 34,
5
+ * 25, 22, 18, 13 and 12 entries — none importing another, none covered by a test
6
+ * asserting they agreed, and two of them byte-identical forks that nothing kept in sync.
7
+ * The same address resolved differently depending on which copy a code path happened to
8
+ * reach. This module is the single source of truth; `packages/shared` is the only leaf
9
+ * package all eight can reach without inverting a dependency edge.
10
+ *
11
+ * WHY TWO TIERS, and why they must not be collapsed into one:
12
+ *
13
+ * The callers are not asking the same question, and the cost of a wrong answer is
14
+ * asymmetric in opposite directions.
15
+ *
16
+ * - `FREEMAIL_DOMAINS` (core) answers "may I treat this domain as a shared EMPLOYER?"
17
+ * Two consumers ride on that answer, and they fail in OPPOSITE directions, which is
18
+ * why this tier is conservative rather than generous:
19
+ *
20
+ * 1. The company-scope reply stop (`settings.stop_on_reply_scope = "company_domain"`)
21
+ * terminalizes an enrollment's colleagues by matching domain. A host wrongly IN
22
+ * this tier is harmless here; a host wrongly OUT of it stops strangers who merely
23
+ * share a mail host — real, customer-visible damage.
24
+ * 2. Company derivation from a person's email domain
25
+ * (`deriveCompanyDomainFromEmail`, `packages/tenant-db/src/table-link-plan.ts`)
26
+ * drops freemail before offering a domain as a company join key. Here the
27
+ * polarity REVERSES: a host wrongly OUT of this tier makes OXYGEN mint a company
28
+ * record for a mail provider, so every @<host> person collapses onto one fake
29
+ * account. Where that derivation is wired into record creation, removing a host
30
+ * from this tier silently starts creating accounts for it.
31
+ *
32
+ * So a host must be in this tier to be safe for BOTH, and the safe direction for an
33
+ * edit is to ADD, never to remove or relocate to the broad tier. Extend it only with
34
+ * hosts that are unambiguously consumer mailboxes — and note the ORDER caveat in
35
+ * `packages/tenant-db/src/freemail-domains.ts`, because a core-tier edit also changes
36
+ * a formula string already persisted in every tenant's column definitions.
37
+ *
38
+ * - `PERSONAL_EMAIL_DOMAINS` (broad) answers "is this domain a poor stand-in for a
39
+ * COMPANY identity?" — used to skip fetching a workspace icon, to reject a personal
40
+ * host typed into the marketing hero form, to avoid picking a personal address as a
41
+ * person's work email, and to avoid treating a meeting participant's mail host as their
42
+ * employer. Here a false positive costs nothing (we simply decline to guess a company)
43
+ * while a miss produces a junk company/icon. So this tier is the superset, including
44
+ * regional and privacy-focused providers.
45
+ *
46
+ * The broad tier is a strict superset of the core tier by construction below, so a host
47
+ * can never be "freemail but not personal" — exactly the incoherence the forks had
48
+ * developed (the old core list carried `t-online.de`; several broad lists did not).
49
+ *
50
+ * The broad tier is the union of all eight former lists. Most of its bulk comes from the
51
+ * old `packages/formula` copy, which was by far the best maintained and is the only one
52
+ * that covered country-specific variants (`yahoo.co.uk`, `hotmail.fr`, `orange.fr`,
53
+ * `wp.pl`, `uol.com.br` …) and ISP mailboxes (`comcast.net`, `btinternet.com`). Every
54
+ * other caller was silently missing those.
55
+ */
56
+ // Tier 1. Unambiguous global consumer mailboxes. Load-bearing for the company-scope
57
+ // reply stop — read the note above before adding anything here.
58
+ const CORE_FREEMAIL_DOMAINS = [
59
+ "gmail.com",
60
+ "googlemail.com",
61
+ "outlook.com",
62
+ "hotmail.com",
63
+ "live.com",
64
+ "msn.com",
65
+ "yahoo.com",
66
+ "ymail.com",
67
+ "icloud.com",
68
+ "me.com",
69
+ "aol.com",
70
+ "gmx.com",
71
+ "gmx.net",
72
+ "gmx.de",
73
+ "web.de",
74
+ "proton.me",
75
+ "protonmail.com",
76
+ "mail.com",
77
+ "zoho.com",
78
+ "yandex.com",
79
+ "yandex.ru",
80
+ "t-online.de",
81
+ ];
82
+ // Tier 2 additions: country variants of the global providers, regional hosts (qq.com,
83
+ // naver.com, wp.pl, libero.it, uol.com.br), ISP mailboxes (comcast.net, btinternet.com,
84
+ // telenet.be), privacy hosts (pm.me, tutanota.com, duck.com) and prosumer ones
85
+ // (fastmail, hey.com, mac.com). Confident enough to refuse to call any of them a
86
+ // company; NOT confident enough to let them drive a reply-stop decision — an ISP or
87
+ // country-TLD host is exactly where a "shared employer" guess goes wrong.
88
+ // Sorted, because this list is long enough that grouping by theme would rot.
89
+ const ADDITIONAL_PERSONAL_EMAIL_DOMAINS = [
90
+ "126.com", "163.com", "att.net", "bellsouth.net",
91
+ "bigpond.com", "bluewin.ch", "blueyonder.co.uk", "bol.com.br",
92
+ "btinternet.com", "charter.net", "comcast.net", "cox.net",
93
+ "daum.net", "duck.com", "earthlink.net", "fastmail.com",
94
+ "fastmail.fm", "free.fr", "freenet.de", "gmx.at",
95
+ "gmx.ch", "hanmail.net", "hey.com", "home.nl",
96
+ "hotmail.co.uk", "hotmail.de", "hotmail.es", "hotmail.fr",
97
+ "hotmail.it", "interia.pl", "kpnmail.nl", "laposte.net",
98
+ "libero.it", "live.co.uk", "live.de", "live.fr",
99
+ "mac.com", "mail.ru", "naver.com", "o2.pl",
100
+ "onet.pl", "optusnet.com.au", "orange.fr", "outlook.co.uk",
101
+ "outlook.de", "outlook.es", "outlook.fr", "pm.me",
102
+ "qq.com", "rediffmail.com", "rocketmail.com", "rogers.com",
103
+ "sbcglobal.net", "seznam.cz", "sfr.fr", "shaw.ca",
104
+ "sina.com", "sky.com", "skynet.be", "sympatico.ca",
105
+ "talktalk.net", "telenet.be", "telus.net", "terra.com.br",
106
+ "tuta.io", "tutanota.com", "uol.com.br", "verizon.net",
107
+ "virgilio.it", "virginmedia.com", "wanadoo.fr", "wp.pl",
108
+ "yahoo.ca", "yahoo.co.jp", "yahoo.co.uk", "yahoo.com.au",
109
+ "yahoo.de", "yahoo.es", "yahoo.fr", "yahoo.it",
110
+ "ziggo.nl",
111
+ ];
112
+ /** Tier 1 — see the module note. Small on purpose. */
113
+ export const FREEMAIL_DOMAINS = new Set(CORE_FREEMAIL_DOMAINS);
114
+ /** Tier 2 — a strict superset of {@link FREEMAIL_DOMAINS}. */
115
+ export const PERSONAL_EMAIL_DOMAINS = new Set([
116
+ ...CORE_FREEMAIL_DOMAINS,
117
+ ...ADDITIONAL_PERSONAL_EMAIL_DOMAINS,
118
+ ]);
119
+ /**
120
+ * Reduce an address or a bare domain to a comparable mail host: lowercased, trimmed,
121
+ * everything before the last '@' dropped, and a trailing FQDN dot removed. Returns null
122
+ * for junk/empty/nullish input so callers can distinguish "not a host" from "not in the
123
+ * set". Previously each fork normalized differently — one did full address handling, two
124
+ * only lowercased a bare domain — so the same input could resolve differently depending
125
+ * on which copy you happened to call.
126
+ */
127
+ export function normalizeMailHost(value) {
128
+ if (typeof value !== "string")
129
+ return null;
130
+ let host = value.trim().toLowerCase();
131
+ if (!host)
132
+ return null;
133
+ const at = host.lastIndexOf("@");
134
+ if (at >= 0)
135
+ host = host.slice(at + 1);
136
+ host = host.replace(/\.+$/, "");
137
+ return host || null;
138
+ }
139
+ /**
140
+ * True when the host is a tier-1 consumer mailbox, i.e. it NEVER identifies a shared
141
+ * employer. Accepts a bare domain or a full address. Unknown input returns false: an
142
+ * unrecognized domain is treated as a company domain, which keeps the company-scope
143
+ * stop's safe default identical to lead-scope.
144
+ */
145
+ export function isFreemailDomain(domain) {
146
+ const host = normalizeMailHost(domain);
147
+ return host !== null && FREEMAIL_DOMAINS.has(host);
148
+ }
149
+ /**
150
+ * True when the host is a personal mailbox of any kind (tier 2). Use this for "should I
151
+ * infer a company from this domain?"; use {@link isFreemailDomain} when a false positive
152
+ * would wrongly group unrelated people as colleagues.
153
+ */
154
+ export function isPersonalEmailDomain(domain) {
155
+ const host = normalizeMailHost(domain);
156
+ return host !== null && PERSONAL_EMAIL_DOMAINS.has(host);
157
+ }
@@ -423,3 +423,4 @@ export declare function normalizeFutureSignupLifecycleProjectionEnvelope(value:
423
423
  export declare function normalizeFutureSignupLifecycleProjectionV2Envelope(value: unknown): NormalizedFutureSignupLifecycleProjectionV2Envelope;
424
424
  export declare function normalizeFutureSignupLifecycleProjectionDestinationApplyOptions(value: unknown): NormalizedFutureSignupLifecycleProjectionDestinationApplyOptions;
425
425
  export declare function normalizeFutureSignupLifecycleProjectionV2DestinationApplyOptions(value: unknown): NormalizedFutureSignupLifecycleProjectionV2DestinationApplyOptions;
426
+ export declare function codeUnitCompare(left: string, right: string): number;
@@ -1336,7 +1336,7 @@ function canonicalize(value) {
1336
1336
  }
1337
1337
  return value;
1338
1338
  }
1339
- function codeUnitCompare(left, right) {
1339
+ export function codeUnitCompare(left, right) {
1340
1340
  return left < right ? -1 : left > right ? 1 : 0;
1341
1341
  }
1342
1342
  function invalid(path, rule) {
@@ -17,6 +17,10 @@ export * from "./provider-balance-signal.js";
17
17
  export * from "./provider-funding-errors.js";
18
18
  export * from "./publishing-limits.js";
19
19
  export * from "./spend-safety.js";
20
+ export * from "./company-enrichment-fields.js";
21
+ export * from "./enrichment-intents.js";
22
+ export * from "./error-message.js";
23
+ export * from "./sql-rows.js";
20
24
  export * from "./cell-format.js";
21
25
  export * from "./cli-envelope.js";
22
26
  export * from "./cli-login-code.js";
@@ -27,8 +31,10 @@ export * from "./crm-activity-events.js";
27
31
  export * from "./column-types.js";
28
32
  export * from "./copilot-errors.js";
29
33
  export * from "./copilot-journeys.js";
34
+ export * from "./copilot-playbooks.js";
30
35
  export * from "./copilot-plan.js";
31
36
  export * from "./credit-guidance.js";
37
+ export * from "./deploy-env.js";
32
38
  export * from "./directory.js";
33
39
  export * from "./email-dsn.js";
34
40
  export * from "./email-warmup-readiness.js";
@@ -72,6 +78,7 @@ export * from "./dial-guardrail-overrides.js";
72
78
  export * from "./sequences.js";
73
79
  export * from "./suppression-entries.js";
74
80
  export * from "./dnc-identities.js";
81
+ export * from "./dnc-rules.js";
75
82
  export * from "./table-limits.js";
76
83
  export * from "./table-capacity.js";
77
84
  export * from "./log.js";
@@ -88,8 +95,12 @@ export * from "./tags.js";
88
95
  export * from "./telemetry.js";
89
96
  export * from "./tenant-database-secret.js";
90
97
  export * from "./egress-transport-readiness.js";
98
+ export * from "./rate-window.js";
91
99
  export * from "./timing.js";
92
100
  export * from "./type-guards.js";
101
+ export * from "./value-readers.js";
102
+ export * from "./array-utils.js";
103
+ export * from "./format-percent.js";
93
104
  export * from "./worker-failures-queue.js";
94
105
  export * from "./workflow-mcp-tools.js";
95
106
  export * from "./webhook-headers.js";
@@ -123,3 +134,6 @@ export * from "./ugc.js";
123
134
  export * from "./ugc-amplification-identity.js";
124
135
  export * from "./knowledge-repository.js";
125
136
  export * from "./knowledge-bases.js";
137
+ export * from "./external-write-policy.js";
138
+ export * from "./log-sink-selector.js";
139
+ export * from "./otlp-log-sink.js";
@@ -17,6 +17,10 @@ export * from "./provider-balance-signal.js";
17
17
  export * from "./provider-funding-errors.js";
18
18
  export * from "./publishing-limits.js";
19
19
  export * from "./spend-safety.js";
20
+ export * from "./company-enrichment-fields.js";
21
+ export * from "./enrichment-intents.js";
22
+ export * from "./error-message.js";
23
+ export * from "./sql-rows.js";
20
24
  export * from "./cell-format.js";
21
25
  export * from "./cli-envelope.js";
22
26
  export * from "./cli-login-code.js";
@@ -27,8 +31,10 @@ export * from "./crm-activity-events.js";
27
31
  export * from "./column-types.js";
28
32
  export * from "./copilot-errors.js";
29
33
  export * from "./copilot-journeys.js";
34
+ export * from "./copilot-playbooks.js";
30
35
  export * from "./copilot-plan.js";
31
36
  export * from "./credit-guidance.js";
37
+ export * from "./deploy-env.js";
32
38
  export * from "./directory.js";
33
39
  export * from "./email-dsn.js";
34
40
  export * from "./email-warmup-readiness.js";
@@ -72,6 +78,7 @@ export * from "./dial-guardrail-overrides.js";
72
78
  export * from "./sequences.js";
73
79
  export * from "./suppression-entries.js";
74
80
  export * from "./dnc-identities.js";
81
+ export * from "./dnc-rules.js";
75
82
  export * from "./table-limits.js";
76
83
  export * from "./table-capacity.js";
77
84
  export * from "./log.js";
@@ -99,8 +106,12 @@ export * from "./tags.js";
99
106
  export * from "./telemetry.js";
100
107
  export * from "./tenant-database-secret.js";
101
108
  export * from "./egress-transport-readiness.js";
109
+ export * from "./rate-window.js";
102
110
  export * from "./timing.js";
103
111
  export * from "./type-guards.js";
112
+ export * from "./value-readers.js";
113
+ export * from "./array-utils.js";
114
+ export * from "./format-percent.js";
104
115
  export * from "./worker-failures-queue.js";
105
116
  export * from "./workflow-mcp-tools.js";
106
117
  export * from "./webhook-headers.js";
@@ -159,3 +170,6 @@ export * from "./ugc.js";
159
170
  export * from "./ugc-amplification-identity.js";
160
171
  export * from "./knowledge-repository.js";
161
172
  export * from "./knowledge-bases.js";
173
+ export * from "./external-write-policy.js";
174
+ export * from "./log-sink-selector.js";
175
+ export * from "./otlp-log-sink.js";
@@ -36,15 +36,13 @@
36
36
  * That difference is observable, so it is a choice the caller makes, not one
37
37
  * this module makes for it.
38
38
  */
39
+ import { isRecord } from "./type-guards.js";
39
40
  const NUMERIC_SEGMENT = /^\d+$/;
40
41
  const TEMPLATE_SAFE_PATH = /^[a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*$/;
41
42
  // A head of one-or-more non-`[` characters, optionally followed by one or more
42
43
  // `[123]` index groups. Anything else (a stray bracket, an unclosed group) is
43
44
  // left as a single literal segment rather than silently reinterpreted.
44
45
  const BRACKET_SEGMENT = /^([^[]+)((?:\[\d+\])+)?$/;
45
- function isRecord(value) {
46
- return typeof value === "object" && value !== null && !Array.isArray(value);
47
- }
48
46
  /**
49
47
  * A string that stores a serialized JSON object/array is treated as its parsed
50
48
  * container; anything else is returned untouched — including strings that fail
@@ -1,12 +1,10 @@
1
1
  import { parseDocument } from "yaml";
2
+ import { readTrimmedString as stringValue } from "./type-guards.js";
2
3
  function record(value) {
3
4
  return value !== null && typeof value === "object" && !Array.isArray(value)
4
5
  ? value
5
6
  : null;
6
7
  }
7
- function stringValue(value) {
8
- return typeof value === "string" && value.trim() ? value.trim() : null;
9
- }
10
8
  function strings(value) {
11
9
  return Array.isArray(value)
12
10
  ? value
@@ -54,13 +54,13 @@
54
54
  */
55
55
  import type { HostedAiLevel } from "./hosted-ai.js";
56
56
  /** Hard ceiling on managed credits ONE workspace bootstrap may spend, all phases. */
57
- export declare const KNOWLEDGE_BOOTSTRAP_MAX_CREDITS = 300;
57
+ export declare const KNOWLEDGE_BOOTSTRAP_MAX_CREDITS = 30;
58
58
  /**
59
59
  * Tighter sub-ceiling on the provider/enrichment half (search + page fetch), i.e.
60
60
  * the external-money phase. Reaching it degrades the pass to `partial` — synthesize
61
61
  * from what was read — rather than failing it.
62
62
  */
63
- export declare const KNOWLEDGE_BOOTSTRAP_ENRICHMENT_MAX_CREDITS = 60;
63
+ export declare const KNOWLEDGE_BOOTSTRAP_ENRICHMENT_MAX_CREDITS = 6;
64
64
  /**
65
65
  * Reasoning tier the synthesis pass runs at. `medium` ("Oxygen Balanced") is the
66
66
  * deliberate middle: `low` cannot hold a company profile's structure together, and
@@ -73,6 +73,19 @@ export declare const KNOWLEDGE_BOOTSTRAP_SYNTHESIS_TIER: HostedAiLevel;
73
73
  * it is a quality/latency knob, and the credit ceilings above remain the money guard.
74
74
  */
75
75
  export declare const KNOWLEDGE_BOOTSTRAP_MAX_PAGES_READ = 5;
76
+ /**
77
+ * Per-call provider abort budget for every paid leg of the pass (LinkedIn company,
78
+ * person search/profile, page search, page read), in milliseconds.
79
+ *
80
+ * The platform default is 120 s, sized for a durable column run. This pass runs
81
+ * inside the worker's tenant-maintenance tick under a 300 s step deadline, so two
82
+ * hung LinkedIn calls at the default (measured 2026-09-15/16: the managed vendor
83
+ * accepted connections and never answered) consumed the whole budget before the
84
+ * page reads and the synthesis got a turn. 45 s keeps the worst case — five legs,
85
+ * every one hanging — inside the step, and a healthy vendor answers in well under
86
+ * it. This bounds latency only; the credit ceilings above remain the money guard.
87
+ */
88
+ export declare const KNOWLEDGE_BOOTSTRAP_PROVIDER_TIMEOUT_MS = 45000;
76
89
  /** Fleet-wide kill switch, matching OXYGEN_KNOWLEDGE_RETRIEVAL/AUTO_SOURCES_DISABLED. */
77
90
  export declare const KNOWLEDGE_BOOTSTRAP_DISABLED_ENV_VAR = "OXYGEN_KNOWLEDGE_BOOTSTRAP_DISABLED";
78
91
  /**
@@ -172,6 +185,8 @@ export type KnowledgeBootstrapConsent = {
172
185
  /** New workspace creation may authorize bounded creator-profile research. */
173
186
  scope?: "company_and_creator";
174
187
  domain?: string;
188
+ /** Canonical `/in/` profile supplied by this exact creator; never company evidence. */
189
+ creator_linkedin_url?: string;
175
190
  /** ISO timestamp consent was recorded. Must parse; a marker that does not is not consent. */
176
191
  granted_at: string;
177
192
  /**
@@ -52,14 +52,15 @@
52
52
  * Professional person evidence remains actor-scoped; ICP/offers are hypotheses.
53
53
  * The provider sub-cap covers every external research leg, including fallbacks.
54
54
  */
55
+ import { normalizeLinkedinProfileUrl } from "./linkedin-url.js";
55
56
  /** Hard ceiling on managed credits ONE workspace bootstrap may spend, all phases. */
56
- export const KNOWLEDGE_BOOTSTRAP_MAX_CREDITS = 300;
57
+ export const KNOWLEDGE_BOOTSTRAP_MAX_CREDITS = 30;
57
58
  /**
58
59
  * Tighter sub-ceiling on the provider/enrichment half (search + page fetch), i.e.
59
60
  * the external-money phase. Reaching it degrades the pass to `partial` — synthesize
60
61
  * from what was read — rather than failing it.
61
62
  */
62
- export const KNOWLEDGE_BOOTSTRAP_ENRICHMENT_MAX_CREDITS = 60;
63
+ export const KNOWLEDGE_BOOTSTRAP_ENRICHMENT_MAX_CREDITS = 6;
63
64
  /**
64
65
  * Reasoning tier the synthesis pass runs at. `medium` ("Oxygen Balanced") is the
65
66
  * deliberate middle: `low` cannot hold a company profile's structure together, and
@@ -72,6 +73,19 @@ export const KNOWLEDGE_BOOTSTRAP_SYNTHESIS_TIER = "medium";
72
73
  * it is a quality/latency knob, and the credit ceilings above remain the money guard.
73
74
  */
74
75
  export const KNOWLEDGE_BOOTSTRAP_MAX_PAGES_READ = 5;
76
+ /**
77
+ * Per-call provider abort budget for every paid leg of the pass (LinkedIn company,
78
+ * person search/profile, page search, page read), in milliseconds.
79
+ *
80
+ * The platform default is 120 s, sized for a durable column run. This pass runs
81
+ * inside the worker's tenant-maintenance tick under a 300 s step deadline, so two
82
+ * hung LinkedIn calls at the default (measured 2026-09-15/16: the managed vendor
83
+ * accepted connections and never answered) consumed the whole budget before the
84
+ * page reads and the synthesis got a turn. 45 s keeps the worst case — five legs,
85
+ * every one hanging — inside the step, and a healthy vendor answers in well under
86
+ * it. This bounds latency only; the credit ceilings above remain the money guard.
87
+ */
88
+ export const KNOWLEDGE_BOOTSTRAP_PROVIDER_TIMEOUT_MS = 45_000;
75
89
  /** Fleet-wide kill switch, matching OXYGEN_KNOWLEDGE_RETRIEVAL/AUTO_SOURCES_DISABLED. */
76
90
  export const KNOWLEDGE_BOOTSTRAP_DISABLED_ENV_VAR = "OXYGEN_KNOWLEDGE_BOOTSTRAP_DISABLED";
77
91
  /**
@@ -158,14 +172,22 @@ export function readKnowledgeBootstrapConsent(metadata) {
158
172
  return null;
159
173
  if (!Number.isFinite(Date.parse(grantedAt)))
160
174
  return null;
175
+ const scope = record.scope === "company_and_creator" ? "company_and_creator" : undefined;
176
+ const grantedBy = typeof record.granted_by_clerk_user_id === "string" && record.granted_by_clerk_user_id.length > 0
177
+ ? record.granted_by_clerk_user_id
178
+ : null;
179
+ // A person hint is valid only as part of the creator-scoped grant. Otherwise a
180
+ // stale metadata value could be mistaken for an organization-wide profile hint.
181
+ const creatorLinkedinUrl = scope && grantedBy && typeof record.creator_linkedin_url === "string"
182
+ ? normalizeLinkedinProfileUrl(record.creator_linkedin_url)?.dispatchUrl ?? null
183
+ : null;
161
184
  return {
162
185
  granted_at: grantedAt,
163
- ...(record.scope === "company_and_creator" ? { scope: "company_and_creator" } : {}),
186
+ ...(scope ? { scope } : {}),
164
187
  ...(typeof record.domain === "string" ? { domain: record.domain } : {}),
188
+ ...(creatorLinkedinUrl ? { creator_linkedin_url: creatorLinkedinUrl } : {}),
165
189
  source: typeof record.source === "string" && record.source.length > 0 ? record.source : null,
166
- granted_by_clerk_user_id: typeof record.granted_by_clerk_user_id === "string" && record.granted_by_clerk_user_id.length > 0
167
- ? record.granted_by_clerk_user_id
168
- : null,
190
+ granted_by_clerk_user_id: grantedBy,
169
191
  };
170
192
  }
171
193
  /**