@oxygen-agent/cli 1.948.1 → 1.987.20

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 (84) 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 +6 -0
  6. package/dist/functions-commands.js +33 -9
  7. package/dist/help.d.ts +21 -0
  8. package/dist/help.js +94 -0
  9. package/dist/index.js +1493 -297
  10. package/dist/knowledge-repository-commands.d.ts +6 -0
  11. package/dist/knowledge-repository-commands.js +198 -0
  12. package/dist/skills.js +20 -0
  13. package/dist/ugc-commands.d.ts +3 -6
  14. package/dist/ugc-commands.js +2 -1086
  15. package/node_modules/@oxygen/cli-ugc/dist/commands.d.ts +3 -0
  16. package/node_modules/@oxygen/cli-ugc/dist/commands.js +1178 -0
  17. package/node_modules/@oxygen/cli-ugc/dist/field-parser.d.ts +7 -0
  18. package/node_modules/@oxygen/cli-ugc/dist/field-parser.js +25 -0
  19. package/node_modules/@oxygen/cli-ugc/dist/index.d.ts +14 -0
  20. package/node_modules/@oxygen/cli-ugc/dist/index.js +5 -0
  21. package/node_modules/@oxygen/cli-ugc/package.json +15 -0
  22. package/node_modules/@oxygen/formula/dist/expression.js +14 -1
  23. package/node_modules/@oxygen/formula/dist/formula-functions.js +71 -1
  24. package/node_modules/@oxygen/formula/dist/index.d.ts +1 -0
  25. package/node_modules/@oxygen/formula/dist/index.js +1 -0
  26. package/node_modules/@oxygen/formula/dist/value-cleaners.d.ts +69 -0
  27. package/node_modules/@oxygen/formula/dist/value-cleaners.js +374 -0
  28. package/node_modules/@oxygen/recipe-sdk/dist/index.d.ts +2 -0
  29. package/node_modules/@oxygen/shared/dist/billing.d.ts +27 -27
  30. package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +8 -0
  31. package/node_modules/@oxygen/shared/dist/capability-discovery.js +127 -18
  32. package/node_modules/@oxygen/shared/dist/column-output-fields.js +12 -4
  33. package/node_modules/@oxygen/shared/dist/copilot-errors.js +3 -0
  34. package/node_modules/@oxygen/shared/dist/copilot-journeys.d.ts +19 -1
  35. package/node_modules/@oxygen/shared/dist/copilot-journeys.generated.d.ts +19 -0
  36. package/node_modules/@oxygen/shared/dist/copilot-journeys.generated.js +26 -0
  37. package/node_modules/@oxygen/shared/dist/copilot-journeys.js +8 -41
  38. package/node_modules/@oxygen/shared/dist/copilot-playbooks.d.ts +18 -0
  39. package/node_modules/@oxygen/shared/dist/copilot-playbooks.js +43 -0
  40. package/node_modules/@oxygen/shared/dist/copilot-skills.d.ts +15 -0
  41. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +31 -0
  42. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +41 -0
  43. package/node_modules/@oxygen/shared/dist/copilot-skills.js +6 -0
  44. package/node_modules/@oxygen/shared/dist/inbox-avatar-url.d.ts +28 -0
  45. package/node_modules/@oxygen/shared/dist/inbox-avatar-url.js +57 -0
  46. package/node_modules/@oxygen/shared/dist/index.d.ts +5 -0
  47. package/node_modules/@oxygen/shared/dist/index.js +5 -0
  48. package/node_modules/@oxygen/shared/dist/knowledge-bases.d.ts +74 -0
  49. package/node_modules/@oxygen/shared/dist/knowledge-bases.js +456 -0
  50. package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.d.ts +32 -38
  51. package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.js +38 -41
  52. package/node_modules/@oxygen/shared/dist/knowledge-repository.d.ts +22 -0
  53. package/node_modules/@oxygen/shared/dist/knowledge-repository.js +121 -0
  54. package/node_modules/@oxygen/shared/dist/knowledge-vault-markdown.d.ts +20 -0
  55. package/node_modules/@oxygen/shared/dist/knowledge-vault-markdown.js +155 -0
  56. package/node_modules/@oxygen/shared/dist/langfuse.d.ts +12 -1
  57. package/node_modules/@oxygen/shared/dist/langfuse.js +48 -8
  58. package/node_modules/@oxygen/shared/dist/mailbox-import.d.ts +10 -0
  59. package/node_modules/@oxygen/shared/dist/mailbox-import.js +53 -0
  60. package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +8 -0
  61. package/node_modules/@oxygen/shared/dist/plan-limits.js +8 -0
  62. package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +1 -1
  63. package/node_modules/@oxygen/shared/dist/pricing-sheet.js +1 -1
  64. package/node_modules/@oxygen/shared/dist/product-analytics-events.d.ts +24 -0
  65. package/node_modules/@oxygen/shared/dist/product-analytics-events.js +24 -0
  66. package/node_modules/@oxygen/shared/dist/recipes.d.ts +6 -0
  67. package/node_modules/@oxygen/shared/dist/recipes.js +23 -0
  68. package/node_modules/@oxygen/shared/dist/research-output-contract.d.ts +33 -1
  69. package/node_modules/@oxygen/shared/dist/research-output-contract.js +64 -2
  70. package/node_modules/@oxygen/shared/dist/sequence-crm-events.d.ts +1 -1
  71. package/node_modules/@oxygen/shared/dist/sequence-hubspot-sync.d.ts +1 -1
  72. package/node_modules/@oxygen/shared/dist/sequences.d.ts +152 -2
  73. package/node_modules/@oxygen/shared/dist/sequences.js +304 -4
  74. package/node_modules/@oxygen/shared/dist/ugc-amplification-identity.d.ts +2 -0
  75. package/node_modules/@oxygen/shared/dist/ugc-amplification-identity.js +24 -0
  76. package/node_modules/@oxygen/shared/dist/ugc.d.ts +8 -0
  77. package/node_modules/@oxygen/shared/dist/user-capability-routing.js +8 -1
  78. package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
  79. package/node_modules/@oxygen/shared/dist/version.js +3 -1
  80. package/node_modules/@oxygen/shared/dist/workspace-file-storage.d.ts +6 -2
  81. package/node_modules/@oxygen/shared/dist/workspace-file-storage.js +15 -4
  82. package/node_modules/@oxygen/shared/package.json +25 -0
  83. package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.d.ts +15 -15
  84. package/package.json +6 -2
@@ -49,6 +49,27 @@ const MAILBOX_TENANT_HEADERS = new Set([
49
49
  "microsofttenantid",
50
50
  "tenantid",
51
51
  ]);
52
+ const MAILBOX_FIRST_NAME_HEADERS = new Set([
53
+ "first",
54
+ "firstname",
55
+ "givenname",
56
+ ]);
57
+ const MAILBOX_LAST_NAME_HEADERS = new Set([
58
+ "familyname",
59
+ "last",
60
+ "lastname",
61
+ "surname",
62
+ ]);
63
+ const MAILBOX_DISPLAY_NAME_HEADERS = new Set([
64
+ "displayname",
65
+ "fromname",
66
+ "name",
67
+ "sendername",
68
+ ]);
69
+ const MAILBOX_SENDER_PROFILE_HEADERS = new Set([
70
+ "senderprofile",
71
+ "senderprofileid",
72
+ ]);
52
73
  const MAILBOX_NON_TRANSFERABLE_SECRET_HEADERS = new Set([
53
74
  "accesstoken",
54
75
  "applicationsecret",
@@ -328,6 +349,17 @@ function normalizeMailboxExportRow(row, index, mode) {
328
349
  ? "microsoft_azure"
329
350
  : null);
330
351
  const infrastructurePlatform = normalizeMailboxInfrastructurePlatform(infrastructurePlatformRaw, provider, index);
352
+ // WHO the inbox sends as. Carried through because an inbox always belongs to a
353
+ // sender (v1.954.0): a file that already names the person is the difference
354
+ // between a real sender profile and one OXYGEN guesses from the address.
355
+ const firstName = readMailboxIdentityName(byHeader, MAILBOX_FIRST_NAME_HEADERS, index, "first_name");
356
+ const lastName = readMailboxIdentityName(byHeader, MAILBOX_LAST_NAME_HEADERS, index, "last_name");
357
+ const displayName = readMailboxIdentityName(byHeader, MAILBOX_DISPLAY_NAME_HEADERS, index, "display_name");
358
+ const senderProfileId = readUniqueMailboxExportString(byHeader, MAILBOX_SENDER_PROFILE_HEADERS, index, "sender_profile_id", (value) => value.trim().toLowerCase());
359
+ if (senderProfileId &&
360
+ !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(senderProfileId)) {
361
+ throw new OxygenError("invalid_request", `mailboxes[${index}].sender_profile_id must be a sender profile id (UUID). List them with \`oxygen senders profiles list\`, or drop the column and let the row's first/last name name the sender.`, { exitCode: 1 });
362
+ }
331
363
  return {
332
364
  email_address: email.trim().toLowerCase(),
333
365
  provider,
@@ -338,6 +370,10 @@ function normalizeMailboxExportRow(row, index, mode) {
338
370
  ? { infrastructure_platform: infrastructurePlatform }
339
371
  : {}),
340
372
  ...(tenantId ? { tenant_id: tenantId.trim().toLowerCase() } : {}),
373
+ ...(firstName ? { first_name: firstName } : {}),
374
+ ...(lastName ? { last_name: lastName } : {}),
375
+ ...(displayName ? { display_name: displayName } : {}),
376
+ ...(senderProfileId ? { sender_profile_id: senderProfileId } : {}),
341
377
  ...(mode === "credential" &&
342
378
  provider === "google" &&
343
379
  distinctPasswords[0] !== undefined
@@ -345,6 +381,23 @@ function normalizeMailboxExportRow(row, index, mode) {
345
381
  : {}),
346
382
  };
347
383
  }
384
+ /**
385
+ * One identity half off an export row. Header-bound (the From name rides an SMTP
386
+ * header), so a CR/LF is refused by field name rather than silently stripped — the
387
+ * caller is usually holding hundreds of rows and needs to know which one.
388
+ */
389
+ function readMailboxIdentityName(byHeader, headers, index, field) {
390
+ const value = readUniqueMailboxExportString(byHeader, headers, index, field, (raw) => raw.trim());
391
+ if (!value)
392
+ return null;
393
+ if (value.length > 200) {
394
+ throw new OxygenError("invalid_request", `mailboxes[${index}].${field} must be at most 200 characters.`, { exitCode: 1 });
395
+ }
396
+ if (/[\r\n]/.test(value)) {
397
+ throw new OxygenError("invalid_request", `mailboxes[${index}].${field} must be a single line.`, { exitCode: 1 });
398
+ }
399
+ return value;
400
+ }
348
401
  function normalizeMailboxExportHeader(value) {
349
402
  return value
350
403
  .replace(/^\uFEFF/, "")
@@ -78,6 +78,14 @@ export type PlanLimits = {
78
78
  */
79
79
  export declare const TABLE_IMPORT_ROW_LIMIT: 3000000;
80
80
  export { VERCEL_REQUEST_BODY_LIMIT_BYTES } from "./import-limits.js";
81
+ /**
82
+ * OXYGEN's own JSON-body ceiling for every `/api/cli/*` route (enforced by
83
+ * `assertCliJsonBodyWithinLimit` on the content-length header). Shared so the CLI
84
+ * pre-splits a row batch by measured bytes instead of learning the number from a
85
+ * 413. Must stay below VERCEL_REQUEST_BODY_LIMIT_BYTES, and must never be raised
86
+ * in the CLI alone — an older server would still 413 at the old number.
87
+ */
88
+ export declare const MAX_CLI_JSON_BODY_BYTES = 2000000;
81
89
  /**
82
90
  * The per-rung limit matrix. `ai_live` deliberately equals `tool_live` at every
83
91
  * rung — one "live actions" mental model; the per-call cost asymmetry between
@@ -21,6 +21,14 @@ export const TABLE_IMPORT_ROW_LIMIT = WORKSPACE_TABLE_CAPACITY.tableRowLimit;
21
21
  // ceilings below ("how big can an import be"), but it must stay importable from a
22
22
  // browser bundle, so it is defined in ./import-limits and re-exported here.
23
23
  export { VERCEL_REQUEST_BODY_LIMIT_BYTES } from "./import-limits.js";
24
+ /**
25
+ * OXYGEN's own JSON-body ceiling for every `/api/cli/*` route (enforced by
26
+ * `assertCliJsonBodyWithinLimit` on the content-length header). Shared so the CLI
27
+ * pre-splits a row batch by measured bytes instead of learning the number from a
28
+ * 413. Must stay below VERCEL_REQUEST_BODY_LIMIT_BYTES, and must never be raised
29
+ * in the CLI alone — an older server would still 413 at the old number.
30
+ */
31
+ export const MAX_CLI_JSON_BODY_BYTES = 2_000_000;
24
32
  /**
25
33
  * The per-rung limit matrix. `ai_live` deliberately equals `tool_live` at every
26
34
  * rung — one "live actions" mental model; the per-call cost asymmetry between
@@ -176,7 +176,7 @@ export declare const ENRICHMENT_CREDITS: {
176
176
  readonly company_enrich_typical: 99;
177
177
  readonly web_search: 5;
178
178
  readonly page_scrape: 40;
179
- readonly linkedin_profile_scrape: 25.5;
179
+ readonly linkedin_profile_scrape: 10;
180
180
  };
181
181
  /** Editable calculator default: blended credits per fully-enriched row. */
182
182
  export declare const ENRICHMENT_CREDITS_PER_ROW_TYPICAL = 100;
@@ -163,7 +163,7 @@ export const ENRICHMENT_CREDITS = {
163
163
  company_enrich_typical: 99,
164
164
  web_search: 5, // Serper
165
165
  page_scrape: 40, // Firecrawl (wedge price)
166
- linkedin_profile_scrape: 25.5, // HarvestAPI Basic full-profile job, exactly 5x COGS
166
+ linkedin_profile_scrape: 10, // Managed profile credit at $0.002 COGS, exactly 5x
167
167
  };
168
168
  /** Editable calculator default: blended credits per fully-enriched row. */
169
169
  export const ENRICHMENT_CREDITS_PER_ROW_TYPICAL = SNAPSHOT_ENRICHMENT_CREDITS_PER_ROW_TYPICAL;
@@ -77,6 +77,30 @@ export declare const PRODUCT_EVENTS: {
77
77
  readonly TRIAL_CANCEL_SCHEDULED: "trial_cancel_scheduled";
78
78
  readonly TRIAL_CANCEL_RESUMED: "trial_cancel_resumed";
79
79
  readonly TRIAL_ENDED_UNCONVERTED: "trial_ended_unconverted";
80
+ /**
81
+ * Marketing site. The public hero lookup on oxygen-agent.com: an ANONYMOUS
82
+ * visitor types their website and OXYGEN spends its own credits profiling it.
83
+ *
84
+ * Deliberately absent from the web's KPI_TYPE_TO_EVENT. These describe a
85
+ * pre-signup funnel step, not a lifecycle moment the company reports, and
86
+ * mapping one onto a `company_kpi_events` type would redefine a headline
87
+ * number without anyone editing that number's definition.
88
+ *
89
+ * Properties are enums, numbers and the domain HASH only — never the raw
90
+ * domain. The visitor has no account and never consented to their employer's
91
+ * name landing in an analytics warehouse (ADR 0021: PostHog stays sanitized).
92
+ * `hero_lookup_cached` is what says whether the lookup cache is paying for
93
+ * itself, which is the whole economics of the surface.
94
+ */
95
+ readonly HERO_LOOKUP_STARTED: "hero_lookup_started";
96
+ readonly HERO_PROFILE_COMPLETED: "hero_profile_completed";
97
+ readonly HERO_LEADS_COMPLETED: "hero_leads_completed";
98
+ readonly HERO_LOOKUP_CACHED: "hero_lookup_cached";
99
+ readonly HERO_LOOKUP_DEGRADED: "hero_lookup_degraded";
100
+ /** Browser-side hero events; props are enums, booleans and counts only. */
101
+ readonly HERO_LOOKUP_SUBMITTED: "hero_lookup_submitted";
102
+ readonly HERO_LOOKUP_RENDERED: "hero_lookup_rendered";
103
+ readonly HERO_SIGNUP_CLICKED: "hero_signup_clicked";
80
104
  /** Pipeline heartbeat: proves ingestion is alive when the product is quiet. */
81
105
  readonly POSTHOG_CANARY: "posthog_canary";
82
106
  };
@@ -77,6 +77,30 @@ export const PRODUCT_EVENTS = {
77
77
  TRIAL_CANCEL_SCHEDULED: "trial_cancel_scheduled",
78
78
  TRIAL_CANCEL_RESUMED: "trial_cancel_resumed",
79
79
  TRIAL_ENDED_UNCONVERTED: "trial_ended_unconverted",
80
+ /**
81
+ * Marketing site. The public hero lookup on oxygen-agent.com: an ANONYMOUS
82
+ * visitor types their website and OXYGEN spends its own credits profiling it.
83
+ *
84
+ * Deliberately absent from the web's KPI_TYPE_TO_EVENT. These describe a
85
+ * pre-signup funnel step, not a lifecycle moment the company reports, and
86
+ * mapping one onto a `company_kpi_events` type would redefine a headline
87
+ * number without anyone editing that number's definition.
88
+ *
89
+ * Properties are enums, numbers and the domain HASH only — never the raw
90
+ * domain. The visitor has no account and never consented to their employer's
91
+ * name landing in an analytics warehouse (ADR 0021: PostHog stays sanitized).
92
+ * `hero_lookup_cached` is what says whether the lookup cache is paying for
93
+ * itself, which is the whole economics of the surface.
94
+ */
95
+ HERO_LOOKUP_STARTED: "hero_lookup_started",
96
+ HERO_PROFILE_COMPLETED: "hero_profile_completed",
97
+ HERO_LEADS_COMPLETED: "hero_leads_completed",
98
+ HERO_LOOKUP_CACHED: "hero_lookup_cached",
99
+ HERO_LOOKUP_DEGRADED: "hero_lookup_degraded",
100
+ /** Browser-side hero events; props are enums, booleans and counts only. */
101
+ HERO_LOOKUP_SUBMITTED: "hero_lookup_submitted",
102
+ HERO_LOOKUP_RENDERED: "hero_lookup_rendered",
103
+ HERO_SIGNUP_CLICKED: "hero_signup_clicked",
80
104
  /** Pipeline heartbeat: proves ingestion is alive when the product is quiet. */
81
105
  POSTHOG_CANARY: "posthog_canary",
82
106
  };
@@ -16,4 +16,10 @@ export type RecipePrerequisiteKind = typeof RECIPE_PREREQUISITE_KINDS[number];
16
16
  export declare const RECIPE_BODY_SECTIONS: readonly ["The Play", "What You Get", "When To Run (And When Not To)", "Before You Start", "Steps", "Ready Assets", "Calibrate", "Troubleshooting", "Operator Notes", "Related"];
17
17
  export declare const RECIPE_SLUG_PATTERN: RegExp;
18
18
  export declare function recipeWikiSlug(recipeSlug: string): string;
19
+ export declare const RECIPE_KIT_MAX_STAGES = 6;
20
+ export declare const RECIPE_KIT_TABLE_REF_PATTERN: RegExp;
21
+ export declare const RECIPE_KIT_STAGE_STATUSES: readonly ["planned", "installed", "reused", "skipped_active", "blocked", "failed"];
22
+ export type RecipeKitStageStatus = typeof RECIPE_KIT_STAGE_STATUSES[number];
23
+ export type RecipeKitStatus = "not_applied" | "partial" | "applied";
24
+ export declare function recipeKitSourceRef(recipeSlug: string, version: number): string;
19
25
  export declare const RECIPE_TRIAL_SAFE_PILOT_CREDIT_CAP = 100;
@@ -87,6 +87,29 @@ export const RECIPE_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{0,119}$/;
87
87
  export function recipeWikiSlug(recipeSlug) {
88
88
  return `playbook-${recipeSlug}`;
89
89
  }
90
+ // ── Kits (ADR 0025) ─────────────────────────────────────────────────────────
91
+ // A recipe carries an ORDERED kit of Blueprint stages. `oxygen recipes apply`
92
+ // preflights every stage into one forecast and installs them all under one
93
+ // approval — 0 credits, no provider call, every installed Workflow disabled.
94
+ // A stage may bind a table created by an earlier stage through a
95
+ // `$stages.<index>.tables.<ref>` reference so later stages graft onto it
96
+ // instead of creating a second table.
97
+ export const RECIPE_KIT_MAX_STAGES = 6;
98
+ export const RECIPE_KIT_TABLE_REF_PATTERN = /^\$stages\.(\d)\.tables\.([a-z][a-z0-9_]*)$/;
99
+ export const RECIPE_KIT_STAGE_STATUSES = [
100
+ "planned",
101
+ "installed",
102
+ "reused",
103
+ "skipped_active",
104
+ "blocked",
105
+ "failed",
106
+ ];
107
+ // page_sources ref an apply writes on the recipe's playbook page — one per
108
+ // recipe version, ON CONFLICT DO NOTHING, so first-apply provenance survives
109
+ // reapplies.
110
+ export function recipeKitSourceRef(recipeSlug, version) {
111
+ return `https://oxygen-agent.com/recipes/${recipeSlug}@v${version}#kit`;
112
+ }
90
113
  // trial_safe recipes must keep their pilot path within 10% of the 7-day
91
114
  // card-required trial's 1,000 managed credits.
92
115
  export const RECIPE_TRIAL_SAFE_PILOT_CREDIT_CAP = 100;
@@ -14,6 +14,34 @@ export declare function deriveResearchOutputContract(prompt: string, options: {
14
14
  evidenceRequired: boolean;
15
15
  }): ResearchOutputContract | null;
16
16
  export declare function buildResearchOutputSchema(contract: ResearchOutputContract | null): Record<string, unknown>;
17
+ /**
18
+ * Whether a found answer MUST cite evidence. Shared by the write-path normalizer
19
+ * (which bakes it into the derived research contract), the runner (which
20
+ * validates the model output against it) and the output-field reader, so none
21
+ * of them can disagree.
22
+ *
23
+ * Explicit "strict" always requires it. A FETCH column requires it by default —
24
+ * the evidence is the one page the customer pointed at, so an answer that cites
25
+ * nothing answered from memory — unless the author chose "estimate".
26
+ */
27
+ export declare function researchEvidenceRequired(webSearch: unknown): boolean;
28
+ /**
29
+ * True for a model-facing schema in the research envelope shape — the derived
30
+ * contract, the legacy broad schema, or a catalog template's typed answer
31
+ * envelope — as opposed to a hand-authored schema that replaces the envelope
32
+ * entirely. The output-field reader rewrites only the former into the stored
33
+ * cell shape (`sources` in, `citations` out); the latter is used verbatim.
34
+ */
35
+ export declare function isResearchEnvelopeSchema(schema: unknown): boolean;
36
+ /**
37
+ * The model-facing schema for a research column whose ANSWER is a typed object
38
+ * (a catalog template such as a pricing summary): the same
39
+ * `{answer, found, confidence, citations}` envelope the derived contract uses,
40
+ * with `answer` replaced by the template's own object schema. The runner still
41
+ * resolves `citations` to real sources on the way to the cell, so a typed
42
+ * extraction cannot cite a page that was never fetched either.
43
+ */
44
+ export declare function buildResearchOutputSchemaForAnswer(answerSchema: Record<string, unknown>): Record<string, unknown>;
17
45
  /**
18
46
  * The schema of the research cell as it is actually STORED — which is not the
19
47
  * schema the model answers in.
@@ -27,7 +55,11 @@ export declare function buildResearchOutputSchema(contract: ResearchOutputContra
27
55
  * `buildResearchOutputSchema` would offer `citations`, which no cell has, and
28
56
  * hide `sources`, which every cell does.
29
57
  */
30
- export declare function buildResearchCellSchema(contract: ResearchOutputContract | null): Record<string, unknown>;
58
+ export declare function buildResearchCellSchema(contract: ResearchOutputContract | null,
59
+ /** A column's own model-facing schema (a catalog template's typed answer
60
+ * envelope). When given it replaces the derived one, so the cell shape of a
61
+ * typed extraction exposes `answer.plan_count`, not a string answer. */
62
+ modelSchemaOverride?: Record<string, unknown> | null): Record<string, unknown>;
31
63
  /**
32
64
  * Is this research column's output schema OWNED BY THE SERVER, i.e. re-derived
33
65
  * from the prompt rather than authored by hand?
@@ -88,6 +88,64 @@ export function buildResearchOutputSchema(contract) {
88
88
  required: ["answer", "found", "confidence", "citations"],
89
89
  };
90
90
  }
91
+ /**
92
+ * Whether a found answer MUST cite evidence. Shared by the write-path normalizer
93
+ * (which bakes it into the derived research contract), the runner (which
94
+ * validates the model output against it) and the output-field reader, so none
95
+ * of them can disagree.
96
+ *
97
+ * Explicit "strict" always requires it. A FETCH column requires it by default —
98
+ * the evidence is the one page the customer pointed at, so an answer that cites
99
+ * nothing answered from memory — unless the author chose "estimate".
100
+ */
101
+ export function researchEvidenceRequired(webSearch) {
102
+ if (!isObject(webSearch))
103
+ return false;
104
+ if (webSearch.evidenceMode === "strict")
105
+ return true;
106
+ if (webSearch.evidenceMode === "estimate")
107
+ return false;
108
+ return webSearch.source === "fetch";
109
+ }
110
+ /**
111
+ * True for a model-facing schema in the research envelope shape — the derived
112
+ * contract, the legacy broad schema, or a catalog template's typed answer
113
+ * envelope — as opposed to a hand-authored schema that replaces the envelope
114
+ * entirely. The output-field reader rewrites only the former into the stored
115
+ * cell shape (`sources` in, `citations` out); the latter is used verbatim.
116
+ */
117
+ export function isResearchEnvelopeSchema(schema) {
118
+ if (!isObject(schema) || schema.type !== "object")
119
+ return false;
120
+ const properties = isObject(schema.properties) ? schema.properties : null;
121
+ if (!properties)
122
+ return false;
123
+ return ["answer", "found", "confidence", "citations"].every((key) => isObject(properties[key]));
124
+ }
125
+ /**
126
+ * The model-facing schema for a research column whose ANSWER is a typed object
127
+ * (a catalog template such as a pricing summary): the same
128
+ * `{answer, found, confidence, citations}` envelope the derived contract uses,
129
+ * with `answer` replaced by the template's own object schema. The runner still
130
+ * resolves `citations` to real sources on the way to the cell, so a typed
131
+ * extraction cannot cite a page that was never fetched either.
132
+ */
133
+ export function buildResearchOutputSchemaForAnswer(answerSchema) {
134
+ const envelope = buildResearchOutputSchema(null);
135
+ const properties = isObject(envelope.properties) ? { ...envelope.properties } : {};
136
+ return {
137
+ ...envelope,
138
+ properties: {
139
+ ...properties,
140
+ answer: {
141
+ ...answerSchema,
142
+ description: typeof answerSchema.description === "string"
143
+ ? answerSchema.description
144
+ : "The typed answer read from the evidence; unsupported fields are null or empty.",
145
+ },
146
+ },
147
+ };
148
+ }
91
149
  /**
92
150
  * The schema of the research cell as it is actually STORED — which is not the
93
151
  * schema the model answers in.
@@ -101,8 +159,12 @@ export function buildResearchOutputSchema(contract) {
101
159
  * `buildResearchOutputSchema` would offer `citations`, which no cell has, and
102
160
  * hide `sources`, which every cell does.
103
161
  */
104
- export function buildResearchCellSchema(contract) {
105
- const modelSchema = buildResearchOutputSchema(contract);
162
+ export function buildResearchCellSchema(contract,
163
+ /** A column's own model-facing schema (a catalog template's typed answer
164
+ * envelope). When given it replaces the derived one, so the cell shape of a
165
+ * typed extraction exposes `answer.plan_count`, not a string answer. */
166
+ modelSchemaOverride) {
167
+ const modelSchema = modelSchemaOverride ?? buildResearchOutputSchema(contract);
106
168
  const properties = isObject(modelSchema.properties) ? { ...modelSchema.properties } : {};
107
169
  delete properties.citations;
108
170
  return {
@@ -287,5 +287,5 @@ export declare const SEQUENCE_HUBSPOT_EVENT_SYNC_TEMPLATE_ID = "sequencer-hubspo
287
287
  export declare const SEQUENCE_WORKFLOW_EVENT_SOURCE = "sequencer";
288
288
  export declare const SEQUENCE_CONTACT_ACTIVITY_EVENT = "contact_activity";
289
289
  export declare const SEQUENCE_HUBSPOT_LINKEDIN_IDENTITY_PROPERTY = "oxygen_linkedin_url";
290
- export declare const DEFAULT_SEQUENCE_HUBSPOT_EVENT_MAPPINGS: Readonly<Record<"email_sent" | "meeting_booked" | "email_bounced" | "linkedin_profile_visited" | "linkedin_connection_sent" | "linkedin_connection_accepted" | "linkedin_message_sent" | "linkedin_inmail_sent" | "linkedin_reply_received" | "linkedin_followed" | "linkedin_post_liked" | "linkedin_post_commented" | "linkedin_connection_withdrawn" | "email_campaign_enrolled" | "email_campaign_moved" | "email_campaign_stopped" | "email_opened" | "email_clicked" | "email_reply_received" | "email_unsubscribed" | "email_spam_complaint" | "whatsapp_message_sent" | "whatsapp_reply_received" | "call_task_created" | "call_connected" | "call_no_answer" | "call_voicemail" | "crm_task_created", string>>;
290
+ export declare const DEFAULT_SEQUENCE_HUBSPOT_EVENT_MAPPINGS: Readonly<Record<"call_connected" | "call_no_answer" | "call_task_created" | "call_voicemail" | "crm_task_created" | "email_bounced" | "email_campaign_enrolled" | "email_campaign_moved" | "email_campaign_stopped" | "email_clicked" | "email_opened" | "email_reply_received" | "email_sent" | "email_spam_complaint" | "email_unsubscribed" | "linkedin_connection_accepted" | "linkedin_connection_sent" | "linkedin_connection_withdrawn" | "linkedin_followed" | "linkedin_inmail_sent" | "linkedin_message_sent" | "linkedin_post_commented" | "linkedin_post_liked" | "linkedin_profile_visited" | "linkedin_reply_received" | "meeting_booked" | "whatsapp_message_sent" | "whatsapp_reply_received", string>>;
291
291
  export declare function isSequenceCrmEventKey(value: unknown): value is SequenceCrmEventKey;
@@ -17,7 +17,7 @@ export declare const HUBSPOT_DEFAULT_LEAD_STATUS_PROPERTY = "hs_lead_status";
17
17
  export declare const DEFAULT_HUBSPOT_SEQUENCE_TIMELINE_EVENTS: readonly ["email_sent", "email_reply_received", "email_bounced", "linkedin_message_sent", "linkedin_inmail_sent", "linkedin_reply_received"];
18
18
  /** Events for which the HubSpot adapter has a first-class activity object. */
19
19
  export declare const HUBSPOT_SEQUENCE_TIMELINE_EVENT_KEYS: readonly ["email_sent", "email_reply_received", "email_bounced", "linkedin_message_sent", "linkedin_inmail_sent", "linkedin_reply_received"];
20
- export declare const HUBSPOT_CLASSIFIED_REPLY_TIMELINE_EVENTS: Set<"email_sent" | "meeting_booked" | "email_bounced" | "linkedin_profile_visited" | "linkedin_connection_sent" | "linkedin_connection_accepted" | "linkedin_message_sent" | "linkedin_inmail_sent" | "linkedin_reply_received" | "linkedin_followed" | "linkedin_post_liked" | "linkedin_post_commented" | "linkedin_connection_withdrawn" | "email_campaign_enrolled" | "email_campaign_moved" | "email_campaign_stopped" | "email_opened" | "email_clicked" | "email_reply_received" | "email_unsubscribed" | "email_spam_complaint" | "whatsapp_message_sent" | "whatsapp_reply_received" | "call_task_created" | "call_connected" | "call_no_answer" | "call_voicemail" | "crm_task_created">;
20
+ export declare const HUBSPOT_CLASSIFIED_REPLY_TIMELINE_EVENTS: Set<"call_connected" | "call_no_answer" | "call_task_created" | "call_voicemail" | "crm_task_created" | "email_bounced" | "email_campaign_enrolled" | "email_campaign_moved" | "email_campaign_stopped" | "email_clicked" | "email_opened" | "email_reply_received" | "email_sent" | "email_spam_complaint" | "email_unsubscribed" | "linkedin_connection_accepted" | "linkedin_connection_sent" | "linkedin_connection_withdrawn" | "linkedin_followed" | "linkedin_inmail_sent" | "linkedin_message_sent" | "linkedin_post_commented" | "linkedin_post_liked" | "linkedin_profile_visited" | "linkedin_reply_received" | "meeting_booked" | "whatsapp_message_sent" | "whatsapp_reply_received">;
21
21
  export type HubSpotSequenceSyncConfig = {
22
22
  /** Optional latest-occurrence snapshots on HubSpot contact properties. */
23
23
  mappings: Record<SequenceCrmEventKey, string>;
@@ -108,8 +108,11 @@ export type EspMatchingMode = (typeof ESP_MATCHING_MODES)[number];
108
108
  export declare const DEFAULT_ESP_MATCHING_MODE: EspMatchingMode;
109
109
  export declare function isEspMatchingMode(value: unknown): value is EspMatchingMode;
110
110
  /**
111
- * Validate settings.esp_matching before persistence. Absent/null is valid (means
112
- * DEFAULT_ESP_MATCHING_MODE "off"); any other value must be one of the three
111
+ * Validate settings.esp_matching before persistence. Absent/null is valid and
112
+ * means DEFAULT_ESP_MATCHING_MODE, which is "prefer" — an unset sequence still
113
+ * biases rotation toward a same-provider sender, so a workspace whose senders on
114
+ * one provider are in bad standing concentrates that provider's recipients on
115
+ * them until an operator sets "off". Any other value must be one of the three
113
116
  * modes. Pure; throws OxygenError("invalid_sequence_settings") on a bad value so
114
117
  * the CLI / MCP / API report an identical error. Called from the tenant-db
115
118
  * settings validator alongside the budget/prioritization checks.
@@ -128,6 +131,127 @@ export declare function validateEspMatchingSetting(value: unknown): void;
128
131
  */
129
132
  export declare const RECIPIENT_ESPS: readonly ["google", "microsoft"];
130
133
  export type RecipientEsp = (typeof RECIPIENT_ESPS)[number];
134
+ /**
135
+ * WEIGHTED SENDER ROUTING — the object form of settings.esp_matching.
136
+ *
137
+ * The three scalar modes above answer one question ("bias toward the recipient's
138
+ * own provider, yes or no"), and they bake in the assumption that same-provider
139
+ * is always better. That assumption fails whenever one provider's senders are in
140
+ * bad standing: a workspace whose Google-hosted domains are refused by Gmail
141
+ * wants its Google-hosted RECIPIENTS served from Microsoft senders, which no
142
+ * scalar mode can express. "off" does not express it either, because free
143
+ * rotation still hands Google recipients a Google sender in proportion to the
144
+ * pool.
145
+ *
146
+ * So the object form states the routing directly, as SEND SHARES per recipient
147
+ * provider:
148
+ *
149
+ * { mode: "prefer",
150
+ * routes: { google: { microsoft: 100 }, microsoft: { microsoft: 100 } },
151
+ * exclude: [{ domain: "burned.example", from_recipients: ["google"] }] }
152
+ *
153
+ * `routes` is keyed by the RECIPIENT's resolved provider; the inner map is the
154
+ * SENDER provider. Weights are relative shares, so { google: 3, microsoft: 1 }
155
+ * and { google: 75, microsoft: 25 } are the same policy and nothing has to sum
156
+ * to 100. An omitted weight is zero. A bucket whose entry is explicitly present
157
+ * but carries no positive weight means "no opinion, rotate freely for this
158
+ * recipient class"; a bucket the operator never mentioned keeps the historical
159
+ * same-provider default, so editing Google routing cannot silently change how
160
+ * Microsoft recipients are served.
161
+ *
162
+ * `exclude` is a hard never, not a preference: a sender domain listed here is
163
+ * removed from the candidate pool for the named recipient buckets (all buckets
164
+ * when `from_recipients` is absent) in EVERY mode, including "off", and it
165
+ * survives the prefer-fallback. A deliverability guard that a matching toggle
166
+ * could disarm would not be a guard.
167
+ *
168
+ * The scalar modes remain valid values and are read as policies, so there is one
169
+ * routing code path rather than two: "prefer" becomes { mode: "prefer" } with no
170
+ * routes, which resolves to the same-provider default and behaves exactly as it
171
+ * did before.
172
+ */
173
+ export declare const ESP_ROUTE_BUCKETS: readonly ["google", "microsoft", "unknown"];
174
+ /** The RECIPIENT side of a route: a matchable provider, or "unknown" when resolution failed. */
175
+ export type EspRouteBucket = (typeof ESP_ROUTE_BUCKETS)[number];
176
+ /** The SENDER side of a route: relative send shares per mailbox provider. */
177
+ export type EspRouteWeights = Partial<Record<RecipientEsp, number>>;
178
+ export type EspSenderExclusion = {
179
+ domain: string;
180
+ from_recipients?: EspRouteBucket[];
181
+ };
182
+ export type EspRoutingPolicy = {
183
+ mode?: EspMatchingMode;
184
+ routes?: Partial<Record<EspRouteBucket, EspRouteWeights>>;
185
+ exclude?: EspSenderExclusion[];
186
+ };
187
+ /** settings.esp_matching accepts either the legacy scalar or the routing policy. */
188
+ export type EspMatchingSetting = EspMatchingMode | EspRoutingPolicy;
189
+ /**
190
+ * A policy with every field present, which is what the dispatcher and the launch
191
+ * preview consume. Produced only by espPolicyFromSetting so both surfaces answer
192
+ * identically from the same stored value.
193
+ */
194
+ export type ResolvedEspPolicy = {
195
+ mode: EspMatchingMode;
196
+ routes: Partial<Record<EspRouteBucket, EspRouteWeights>>;
197
+ exclude: Array<{
198
+ domain: string;
199
+ buckets: readonly EspRouteBucket[];
200
+ }>;
201
+ };
202
+ /** Cap on settings.esp_matching.exclude — a routing policy, not a suppression list. */
203
+ export declare const MAX_ESP_SENDER_EXCLUSIONS = 50;
204
+ /** Upper bound on a single route weight; relative shares never need more. */
205
+ export declare const MAX_ESP_ROUTE_WEIGHT = 1000000;
206
+ export declare function isEspRouteBucket(value: unknown): value is EspRouteBucket;
207
+ /**
208
+ * A sending domain as the mailbox table stores it: lowercase, no leading "@".
209
+ * Mirrors lower(split_part(email_address, '@', 2)) in pickEmailMailbox so an
210
+ * exclusion the operator typed as "@Burned.Example " still matches.
211
+ */
212
+ export declare function normalizeEspSenderDomain(value: string): string;
213
+ /**
214
+ * LENIENT read of a persisted settings.esp_matching (never throws — a read must
215
+ * not break a send). Absent, null, or malformed reads as the default policy, and
216
+ * a malformed FIELD bails the WHOLE object rather than half-applying a routing
217
+ * policy, which is the readMailboxRampConfig convention in tenant-db. The strict
218
+ * counterpart that guards the write path is validateEspMatchingSetting.
219
+ */
220
+ export declare function espPolicyFromSetting(value: unknown): ResolvedEspPolicy;
221
+ /**
222
+ * The effective send shares for one recipient bucket. An explicitly-present
223
+ * entry wins even when it is empty or all-zero (that is the operator saying
224
+ * "rotate freely here"); a bucket that was never mentioned keeps the historical
225
+ * same-provider default, and "unknown" has no same-provider to default to.
226
+ */
227
+ export declare function espRouteWeightsFor(policy: ResolvedEspPolicy, bucket: EspRouteBucket): EspRouteWeights;
228
+ /**
229
+ * The sender providers to try, best first, for one recipient bucket — a
230
+ * deterministic weighted shuffle WITHOUT replacement.
231
+ *
232
+ * Returning an ORDER rather than a single draw is what makes "prefer" correct:
233
+ * a 90/10 Google policy over a pool holding no Google mailbox must send 100%
234
+ * from Microsoft, not defer and not fall through to provider-blind rotation.
235
+ * An empty result means "no opinion" and the caller rotates freely.
236
+ *
237
+ * Deterministic by construction: the seed carries the enrollment and step, never
238
+ * a clock or Math.random, so a dry-run preview and the later live send choose
239
+ * the same provider, and a replan after a defer re-derives the same route.
240
+ * Candidates are enumerated in RECIPIENT_ESPS order rather than object-key
241
+ * order, because jsonb does not preserve key order and a policy that round-trips
242
+ * through the database must not reroute the fleet.
243
+ */
244
+ export declare function espRouteCandidates(input: {
245
+ policy: ResolvedEspPolicy;
246
+ recipient: EspRouteBucket;
247
+ seed: string;
248
+ }): RecipientEsp[];
249
+ /**
250
+ * Sending domains that must not serve this recipient bucket, normalized and
251
+ * de-duplicated for pickEmailMailbox. Applies in every mode and survives the
252
+ * prefer-fallback — see the exclude contract above.
253
+ */
254
+ export declare function espExcludedSenderDomains(policy: ResolvedEspPolicy, bucket: EspRouteBucket): string[];
131
255
  /**
132
256
  * The DISPLAY taxonomy: the mail-infrastructure family a recipient domain sits
133
257
  * on. Wider than RecipientEsp because "everything that isn't Google or Microsoft"
@@ -795,9 +919,28 @@ export type SequenceLintIssue = {
795
919
  path: string;
796
920
  message: string;
797
921
  };
922
+ /**
923
+ * A non-blocking fact about how a definition was normalized that its author must
924
+ * be told, as opposed to an issue that rejects it. Today exactly one:
925
+ * `email_reply_rewritten`. The retired kind is accepted and rewritten into a
926
+ * subject-less `email_send`, and a caller that only reads the stored definition
927
+ * back sees a different `kind` than it wrote with nothing in the response to
928
+ * explain it — reported as a silent coercion by a customer (Plain T-163).
929
+ */
930
+ export type SequenceDefinitionNotice = {
931
+ path: string;
932
+ code: "email_reply_rewritten";
933
+ message: string;
934
+ };
798
935
  export type ValidateSequenceOptions = {
799
936
  /** When provided, every channel-bearing step must use one of these channels. */
800
937
  allowedChannels?: SequenceChannel[];
938
+ /**
939
+ * Sink for non-blocking normalization notices (see SequenceDefinitionNotice).
940
+ * Validation never throws for these; a caller that wants to surface them to
941
+ * the author passes an array and reads it back after the call.
942
+ */
943
+ notices?: SequenceDefinitionNotice[];
801
944
  /**
802
945
  * Whether this sequence has NATIVE open/click tracking enabled (a verified
803
946
  * tracking domain + EMAIL_TRACKING_SECRET, so the dispatcher injects a pixel +
@@ -856,6 +999,13 @@ export declare function sequenceStepsMissingCopy(steps: readonly SequenceStep[])
856
999
  */
857
1000
  export declare function validateSequenceDefinition(input: unknown, options?: ValidateSequenceOptions): SequenceDefinition;
858
1001
  /** Non-throwing variant for lint surfaces. */
1002
+ /**
1003
+ * The non-blocking notices normalizing this definition would raise, for a
1004
+ * response `warnings` array. Never throws: an invalid definition yields whatever
1005
+ * notices were collected before validation gave up (the caller's own validate
1006
+ * call reports the failure), so this is safe to run beside a persist.
1007
+ */
1008
+ export declare function sequenceDefinitionNotices(input: unknown, options?: Omit<ValidateSequenceOptions, "notices">): SequenceDefinitionNotice[];
859
1009
  export declare function lintSequenceDefinition(input: unknown, options?: ValidateSequenceOptions): SequenceLintIssue[];
860
1010
  /** Total base delay in milliseconds a wait step introduces (no jitter). */
861
1011
  export declare function sequenceWaitStepDelayMs(step: SequenceWaitStep): number;