@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
@@ -1,3 +1,4 @@
1
+ import { isRecord as isObject } from "./type-guards.js";
1
2
  const MAX_RESEARCH_SECTIONS = 20;
2
3
  const OUTPUT_SECTIONS_MARKER = /\b(?:return|provide|include|produce|output)\b.{0,80}\b(?:labeled|named|required|requested)?\s*(?:output\s+)?sections?\s*:/i;
3
4
  const LABELED_SECTION = /^(?:[-*+]\s+|\d+[.)]\s+)?([A-Za-z][A-Za-z0-9_. /&()'’+\-]{1,79})\s*:\s*.*$/;
@@ -88,6 +89,64 @@ export function buildResearchOutputSchema(contract) {
88
89
  required: ["answer", "found", "confidence", "citations"],
89
90
  };
90
91
  }
92
+ /**
93
+ * Whether a found answer MUST cite evidence. Shared by the write-path normalizer
94
+ * (which bakes it into the derived research contract), the runner (which
95
+ * validates the model output against it) and the output-field reader, so none
96
+ * of them can disagree.
97
+ *
98
+ * Explicit "strict" always requires it. A FETCH column requires it by default —
99
+ * the evidence is the one page the customer pointed at, so an answer that cites
100
+ * nothing answered from memory — unless the author chose "estimate".
101
+ */
102
+ export function researchEvidenceRequired(webSearch) {
103
+ if (!isObject(webSearch))
104
+ return false;
105
+ if (webSearch.evidenceMode === "strict")
106
+ return true;
107
+ if (webSearch.evidenceMode === "estimate")
108
+ return false;
109
+ return webSearch.source === "fetch";
110
+ }
111
+ /**
112
+ * True for a model-facing schema in the research envelope shape — the derived
113
+ * contract, the legacy broad schema, or a catalog template's typed answer
114
+ * envelope — as opposed to a hand-authored schema that replaces the envelope
115
+ * entirely. The output-field reader rewrites only the former into the stored
116
+ * cell shape (`sources` in, `citations` out); the latter is used verbatim.
117
+ */
118
+ export function isResearchEnvelopeSchema(schema) {
119
+ if (!isObject(schema) || schema.type !== "object")
120
+ return false;
121
+ const properties = isObject(schema.properties) ? schema.properties : null;
122
+ if (!properties)
123
+ return false;
124
+ return ["answer", "found", "confidence", "citations"].every((key) => isObject(properties[key]));
125
+ }
126
+ /**
127
+ * The model-facing schema for a research column whose ANSWER is a typed object
128
+ * (a catalog template such as a pricing summary): the same
129
+ * `{answer, found, confidence, citations}` envelope the derived contract uses,
130
+ * with `answer` replaced by the template's own object schema. The runner still
131
+ * resolves `citations` to real sources on the way to the cell, so a typed
132
+ * extraction cannot cite a page that was never fetched either.
133
+ */
134
+ export function buildResearchOutputSchemaForAnswer(answerSchema) {
135
+ const envelope = buildResearchOutputSchema(null);
136
+ const properties = isObject(envelope.properties) ? { ...envelope.properties } : {};
137
+ return {
138
+ ...envelope,
139
+ properties: {
140
+ ...properties,
141
+ answer: {
142
+ ...answerSchema,
143
+ description: typeof answerSchema.description === "string"
144
+ ? answerSchema.description
145
+ : "The typed answer read from the evidence; unsupported fields are null or empty.",
146
+ },
147
+ },
148
+ };
149
+ }
91
150
  /**
92
151
  * The schema of the research cell as it is actually STORED — which is not the
93
152
  * schema the model answers in.
@@ -101,8 +160,12 @@ export function buildResearchOutputSchema(contract) {
101
160
  * `buildResearchOutputSchema` would offer `citations`, which no cell has, and
102
161
  * hide `sources`, which every cell does.
103
162
  */
104
- export function buildResearchCellSchema(contract) {
105
- const modelSchema = buildResearchOutputSchema(contract);
163
+ export function buildResearchCellSchema(contract,
164
+ /** A column's own model-facing schema (a catalog template's typed answer
165
+ * envelope). When given it replaces the derived one, so the cell shape of a
166
+ * typed extraction exposes `answer.plan_count`, not a string answer. */
167
+ modelSchemaOverride) {
168
+ const modelSchema = modelSchemaOverride ?? buildResearchOutputSchema(contract);
106
169
  const properties = isObject(modelSchema.properties) ? { ...modelSchema.properties } : {};
107
170
  delete properties.citations;
108
171
  return {
@@ -191,6 +254,3 @@ function hasExactMembers(value, expected) {
191
254
  && value.length === expected.length
192
255
  && expected.every((member) => value.includes(member));
193
256
  }
194
- function isObject(value) {
195
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
196
- }
@@ -1,11 +1,10 @@
1
1
  // Shared search-planner vocabulary + stateless text helpers.
2
2
  //
3
3
  // Single source of truth for the constraint vocabulary and pure text helpers
4
- // consumed by BOTH the legacy web search planners
5
- // (apps/web/src/lib/company-search-plan.ts, people-search-plan.ts) and the
6
- // @oxygen/routing classifier/compiler (via packages/routing/src/vocab.ts,
7
- // which layers its named tuning deltas on top of these bases). Keeping one copy
8
- // here is what stops the two surfaces from silently drifting (OXY-414).
4
+ // consumed by the web search planners (apps/web/src/lib/company-search-plan.ts,
5
+ // people-search-plan.ts). Any other search classifier or compiler layers its
6
+ // tuning deltas on top of these bases rather than re-listing vocabulary, so the
7
+ // surfaces cannot silently drift (OXY-414).
9
8
  //
10
9
  // Everything here is a pure function or a static table: no Date.now, no
11
10
  // randomness, no I/O — safe to import from any layer.
@@ -75,7 +75,12 @@ export const IMAGE_COLUMN_SEMANTIC = "image";
75
75
  export function isImageColumnSemantic(semanticType) {
76
76
  return semanticType === "image" || semanticType === "avatar" || semanticType === "photo";
77
77
  }
78
- const MAX_OPTIONS = 200;
78
+ // A sanity ceiling, not a correctness one: it stops a malformed write from
79
+ // turning one column into an unbounded list. Raised from 200 to 300 for the
80
+ // CRM's country column, whose vocabulary is the 250-entry LinkedIn country set
81
+ // (see @oxygen/shared/linkedin-countries) — at 200 the tail of the alphabet was
82
+ // silently truncated away, which reads as "Oxygen has never heard of Vietnam".
83
+ const MAX_OPTIONS = 300;
79
84
  // Normalize a raw options array (from a column `definition.options`, CLI/MCP, or
80
85
  // the editor) into canonical SelectOptions: dedupe by value, default colors,
81
86
  // canonical 0..n ordering. Accepts strings or {value,label,color,order} objects.
@@ -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;
@@ -1,5 +1,6 @@
1
1
  import { redactCustomerFacingDetails, redactCustomerFacingError, } from "./error-redaction.js";
2
2
  import { isProviderFundingErrorCode, isProviderRateLimitErrorCode, } from "./provider-funding-errors.js";
3
+ import { asRecordOrNull as asRecord } from "./type-guards.js";
3
4
  export const SEQUENCE_FAILURE_SCHEMA_VERSION = 1;
4
5
  export const SEQUENCE_FAILURE_RETRY_AFTER_MAX_MS = 24 * 60 * 60 * 1000;
5
6
  export const SEQUENCE_FAILURE_CLASSES = [
@@ -318,8 +319,3 @@ function firstBoolean(records, keys) {
318
319
  }
319
320
  return null;
320
321
  }
321
- function asRecord(value) {
322
- return value !== null && typeof value === "object" && !Array.isArray(value)
323
- ? value
324
- : null;
325
- }
@@ -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>;
@@ -919,9 +919,28 @@ export type SequenceLintIssue = {
919
919
  path: string;
920
920
  message: string;
921
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
+ };
922
935
  export type ValidateSequenceOptions = {
923
936
  /** When provided, every channel-bearing step must use one of these channels. */
924
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[];
925
944
  /**
926
945
  * Whether this sequence has NATIVE open/click tracking enabled (a verified
927
946
  * tracking domain + EMAIL_TRACKING_SECRET, so the dispatcher injects a pixel +
@@ -980,6 +999,13 @@ export declare function sequenceStepsMissingCopy(steps: readonly SequenceStep[])
980
999
  */
981
1000
  export declare function validateSequenceDefinition(input: unknown, options?: ValidateSequenceOptions): SequenceDefinition;
982
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[];
983
1009
  export declare function lintSequenceDefinition(input: unknown, options?: ValidateSequenceOptions): SequenceLintIssue[];
984
1010
  /** Total base delay in milliseconds a wait step introduces (no jitter). */
985
1011
  export declare function sequenceWaitStepDelayMs(step: SequenceWaitStep): number;
@@ -1150,4 +1176,27 @@ export declare function secondsUntilSendWindowEnd(window: SequenceSendWindow, no
1150
1176
  * describes this window's sends.
1151
1177
  */
1152
1178
  export declare function secondsSinceSendWindowStart(window: SequenceSendWindow, now: Date, timezone?: string): number | null;
1179
+ /**
1180
+ * The next instant at which `isWithinSendWindow` becomes true, or null when the
1181
+ * window is already open, never opens, or cannot be evaluated.
1182
+ *
1183
+ * This is what a deferral should wait for. Before it existed, the two
1184
+ * native-email send-window gates deferred a flat +30 minutes, so every pending
1185
+ * action was re-claimed, re-planned and re-written twice an hour for as long as
1186
+ * the window stayed shut — ~128 times each across a closed weekend, which is
1187
+ * where 24,261 of one fortnight's deferral log lines came from.
1188
+ *
1189
+ * Null on exactly the cases its three siblings decline: an unresolvable
1190
+ * timezone, a malformed `start`/`end`, and a zero-width window. The caller keeps
1191
+ * its own fallback delay for those, so a bad window can never park an action
1192
+ * forever — the same fail-open posture isWithinSendWindow takes.
1193
+ *
1194
+ * Computed by stepping to the next allowed local `start` and then correcting
1195
+ * against the real clock, because minutes-of-day arithmetic alone is wrong
1196
+ * across a DST boundary (the offset moves between `now` and the candidate). The
1197
+ * correction re-reads the candidate's local time and closes the gap; the result
1198
+ * is only returned once isWithinSendWindow itself agrees, so this function can
1199
+ * never hand back an instant the gate would reject.
1200
+ */
1201
+ export declare function nextSendWindowOpenAt(window: SequenceSendWindow, now: Date, timezone?: string): Date | null;
1153
1202
  export {};
@@ -1,5 +1,6 @@
1
1
  import { OxygenError } from "./cli-result.js";
2
2
  import { hashVariantKey, renderTemplate, spintaxSyntaxIssues, templateColumnKeys, } from "./sequence-template.js";
3
+ import { isRecord } from "./type-guards.js";
3
4
  /**
4
5
  * Multichannel sequence DSL — the shared contract validated identically by CLI,
5
6
  * MCP, API, and web. A sequence is an ordered list of steps applied to each
@@ -1176,6 +1177,22 @@ export function validateSequenceDefinition(input, options = {}) {
1176
1177
  return { steps: normalized };
1177
1178
  }
1178
1179
  /** Non-throwing variant for lint surfaces. */
1180
+ /**
1181
+ * The non-blocking notices normalizing this definition would raise, for a
1182
+ * response `warnings` array. Never throws: an invalid definition yields whatever
1183
+ * notices were collected before validation gave up (the caller's own validate
1184
+ * call reports the failure), so this is safe to run beside a persist.
1185
+ */
1186
+ export function sequenceDefinitionNotices(input, options = {}) {
1187
+ const notices = [];
1188
+ try {
1189
+ validateSequenceDefinition(input, { ...options, notices });
1190
+ }
1191
+ catch {
1192
+ // Reported by the caller's own validation; the notices gathered so far stand.
1193
+ }
1194
+ return notices;
1195
+ }
1179
1196
  export function lintSequenceDefinition(input, options = {}) {
1180
1197
  try {
1181
1198
  validateSequenceDefinition(input, options);
@@ -1378,6 +1395,14 @@ raw, index, options, issues) {
1378
1395
  const cc = optionalEmailList(raw.cc, `${path}.cc`, issues);
1379
1396
  const bcc = optionalEmailList(raw.bcc, `${path}.bcc`, issues);
1380
1397
  const sendWindow = normalizeSendWindow(raw.send_window, `${path}.send_window`, issues);
1398
+ // Say so. The rewrite is deliberate compatibility, but a definition read back
1399
+ // with a different `kind` than was written, and nothing in the response to
1400
+ // explain it, reads as data loss to the author.
1401
+ options.notices?.push({
1402
+ path: `${path}.kind`,
1403
+ code: "email_reply_rewritten",
1404
+ message: `step '${id}': kind 'email_reply' is retired and was stored as 'email_send' with no subject_template — the threaded follow-up. It goes out as "Re: <the previous email's subject>" inside the lead's previous thread; give it a subject_template of its own to start a new email instead.`,
1405
+ });
1381
1406
  return {
1382
1407
  id, channel: "email", kind: "email_send", body_template: body ?? "",
1383
1408
  ...(bodyHtml !== undefined ? { body_html_template: bodyHtml } : {}),
@@ -2539,6 +2564,36 @@ function hhmmToMinutes(value) {
2539
2564
  return hours * 60 + minutes;
2540
2565
  }
2541
2566
  const ISO_WEEKDAY = { Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6, Sun: 7 };
2567
+ /**
2568
+ * Does this window admit `isoWeekday`? Shared by every gate so they cannot
2569
+ * disagree about what a day list means.
2570
+ *
2571
+ * `days` is contractually ISO numbers (normalizeWeekdays rejects anything else),
2572
+ * but `settings.email_send_window` is stored without passing through it, and
2573
+ * production holds windows whose days are `["mon","tue",...]`. A raw
2574
+ * `days.includes(1)` is false for every day of the week, so such a window is
2575
+ * never open and its Sequence can never send — a silent, permanent stall that
2576
+ * also contradicts this module's fail-open posture for a malformed window.
2577
+ * Names are therefore accepted, and a list with no recognisable entry at all is
2578
+ * treated as absent (every day), which is exactly what normalizeWeekdays
2579
+ * produces for the same input.
2580
+ */
2581
+ function sendWindowAllowsWeekday(days, isoWeekday) {
2582
+ let recognised = 0;
2583
+ for (const entry of days) {
2584
+ const day = typeof entry === "number"
2585
+ ? entry
2586
+ : typeof entry === "string"
2587
+ ? ISO_WEEKDAY[`${entry.trim().slice(0, 1).toUpperCase()}${entry.trim().slice(1, 3).toLowerCase()}`] ?? Number(entry)
2588
+ : Number.NaN;
2589
+ if (!Number.isInteger(day) || day < 1 || day > 7)
2590
+ continue;
2591
+ recognised += 1;
2592
+ if (day === isoWeekday)
2593
+ return true;
2594
+ }
2595
+ return recognised === 0;
2596
+ }
2542
2597
  /**
2543
2598
  * Resolve which IANA timezone a send window evaluates in for a given lead row.
2544
2599
  * "recipient" mode reads the lead's tz from `recipient_timezone_column`, falling
@@ -2569,7 +2624,7 @@ export function isWithinSendWindow(window, now, timezone) {
2569
2624
  const endM = hhmmToMinutes(window.end);
2570
2625
  if (startM === null || endM === null)
2571
2626
  return true; // malformed window → fail open
2572
- if (window.days && window.days.length > 0 && !window.days.includes(parts.isoWeekday))
2627
+ if (window.days && window.days.length > 0 && !sendWindowAllowsWeekday(window.days, parts.isoWeekday))
2573
2628
  return false;
2574
2629
  if (startM === endM)
2575
2630
  return false; // empty window
@@ -2630,6 +2685,84 @@ export function secondsSinceSendWindowStart(window, now, timezone) {
2630
2685
  : minutes + 24 * 60 - startM; // wrapped past midnight: opened yesterday evening
2631
2686
  return elapsedMinutes >= 0 ? elapsedMinutes * 60 : null;
2632
2687
  }
2688
+ /**
2689
+ * How far ahead nextSendWindowOpenAt will look for an opening. A window with a
2690
+ * `days` list opens at least weekly, so 8 days covers every configurable gap
2691
+ * with a day of slack; anything that finds nothing inside it is a window that
2692
+ * never opens (zero-width, or a malformed tz that fails the parts read).
2693
+ */
2694
+ const SEND_WINDOW_LOOKAHEAD_DAYS = 8;
2695
+ /**
2696
+ * The next instant at which `isWithinSendWindow` becomes true, or null when the
2697
+ * window is already open, never opens, or cannot be evaluated.
2698
+ *
2699
+ * This is what a deferral should wait for. Before it existed, the two
2700
+ * native-email send-window gates deferred a flat +30 minutes, so every pending
2701
+ * action was re-claimed, re-planned and re-written twice an hour for as long as
2702
+ * the window stayed shut — ~128 times each across a closed weekend, which is
2703
+ * where 24,261 of one fortnight's deferral log lines came from.
2704
+ *
2705
+ * Null on exactly the cases its three siblings decline: an unresolvable
2706
+ * timezone, a malformed `start`/`end`, and a zero-width window. The caller keeps
2707
+ * its own fallback delay for those, so a bad window can never park an action
2708
+ * forever — the same fail-open posture isWithinSendWindow takes.
2709
+ *
2710
+ * Computed by stepping to the next allowed local `start` and then correcting
2711
+ * against the real clock, because minutes-of-day arithmetic alone is wrong
2712
+ * across a DST boundary (the offset moves between `now` and the candidate). The
2713
+ * correction re-reads the candidate's local time and closes the gap; the result
2714
+ * is only returned once isWithinSendWindow itself agrees, so this function can
2715
+ * never hand back an instant the gate would reject.
2716
+ */
2717
+ export function nextSendWindowOpenAt(window, now, timezone) {
2718
+ const tz = timezone?.trim() ? timezone.trim() : window.timezone;
2719
+ const parts = tzParts(now, tz) ?? tzParts(now, window.timezone) ?? tzParts(now, "UTC");
2720
+ if (!parts)
2721
+ return null;
2722
+ const startM = hhmmToMinutes(window.start);
2723
+ const endM = hhmmToMinutes(window.end);
2724
+ if (startM === null || endM === null || startM === endM)
2725
+ return null;
2726
+ if (isWithinSendWindow(window, now, timezone))
2727
+ return null;
2728
+ const dayAllowed = (isoWeekday) => !window.days || window.days.length === 0 || sendWindowAllowsWeekday(window.days, isoWeekday);
2729
+ const minutes = parts.hour * 60 + parts.minute;
2730
+ // Days to skip before the window's local `start` is both in the future and on
2731
+ // an allowed weekday. 0 means "later today".
2732
+ let dayOffset = minutes < startM && dayAllowed(parts.isoWeekday) ? 0 : 1;
2733
+ while (dayOffset <= SEND_WINDOW_LOOKAHEAD_DAYS && !dayAllowed(((parts.isoWeekday - 1 + dayOffset) % 7) + 1)) {
2734
+ dayOffset += 1;
2735
+ }
2736
+ if (dayOffset > SEND_WINDOW_LOOKAHEAD_DAYS)
2737
+ return null;
2738
+ let candidate = new Date(now.getTime() + ((dayOffset * 24 * 60) + startM - minutes) * 60_000);
2739
+ // DST correction: the offset that applied at `now` may not apply at the
2740
+ // candidate, so the naive arithmetic can land an hour early or late. Re-read
2741
+ // the candidate's own local time and close the gap. Two rounds are enough for
2742
+ // any real transition (the second only runs if the first lands on the shifted
2743
+ // side of the boundary); a zone with no transition converges on the first.
2744
+ for (let round = 0; round < 2; round += 1) {
2745
+ const at = tzParts(candidate, tz);
2746
+ if (!at)
2747
+ break;
2748
+ const drift = (at.hour * 60 + at.minute) - startM;
2749
+ if (drift === 0)
2750
+ break;
2751
+ // Choose the nearer direction so a ±23h reading across midnight (the window
2752
+ // opens just after a backward transition) corrects by minutes, not a day.
2753
+ const shift = drift > 12 * 60 ? drift - 24 * 60 : drift < -12 * 60 ? drift + 24 * 60 : drift;
2754
+ candidate = new Date(candidate.getTime() - shift * 60_000);
2755
+ }
2756
+ // A local `start` can be skipped entirely by a forward DST jump, and a
2757
+ // corrected candidate can land a minute on the wrong side. Walk forward to the
2758
+ // first minute the gate actually accepts rather than trusting the arithmetic.
2759
+ for (let nudge = 0; nudge <= 120; nudge += 1) {
2760
+ const at = new Date(candidate.getTime() + nudge * 60_000);
2761
+ if (at.getTime() > now.getTime() && isWithinSendWindow(window, at, timezone))
2762
+ return at;
2763
+ }
2764
+ return null;
2765
+ }
2633
2766
  function tzParts(now, tz) {
2634
2767
  try {
2635
2768
  const formatted = new Intl.DateTimeFormat("en-US", {
@@ -2828,6 +2961,3 @@ function normalizeWeekdays(value, path, issues) {
2828
2961
  }
2829
2962
  return out.length > 0 ? out.sort((a, b) => a - b) : undefined;
2830
2963
  }
2831
- function isRecord(value) {
2832
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
2833
- }
@@ -20,19 +20,19 @@ export declare const AUTONOMOUS_WORKFLOW_TRIGGER_TYPES: ReadonlySet<string>;
20
20
  * Per-run managed-credit ceiling for a LIVE workflow run fired by an autonomous
21
21
  * trigger when neither run metadata nor the manifest declares `max_credits`.
22
22
  */
23
- export declare const DEFAULT_TRIGGER_RUN_CREDIT_CEILING: Record<PlanTier, number | null>;
23
+ export declare const DEFAULT_TRIGGER_RUN_CREDIT_CEILING: Record<SpendSafetyPlanTier, number | null>;
24
24
  /**
25
25
  * Per-delivery credit ceiling stamped onto a standing/webhook table auto-run
26
26
  * batch when the armed configuration carries no explicit `max_credits`.
27
27
  */
28
- export declare const DEFAULT_AUTO_RUN_BATCH_CREDIT_CEILING: Record<PlanTier, number | null>;
28
+ export declare const DEFAULT_AUTO_RUN_BATCH_CREDIT_CEILING: Record<SpendSafetyPlanTier, number | null>;
29
29
  /**
30
30
  * Row ceiling for a BYOK AI-column run whose caller gave no explicit row bound
31
31
  * (no limit, no row_ids, selection "all"). BYOK bills the customer's own
32
32
  * provider account, so a credit ceiling is meaningless — row count is the
33
33
  * enforceable unit. An explicit limit (up to the 500k platform row cap) wins.
34
34
  */
35
- export declare const DEFAULT_BYOK_COLUMN_RUN_MAX_ROWS: Record<PlanTier, number | null>;
35
+ export declare const DEFAULT_BYOK_COLUMN_RUN_MAX_ROWS: Record<SpendSafetyPlanTier, number | null>;
36
36
  /**
37
37
  * Default per-provider DAILY call cap for BYOK provider traffic (the customer's
38
38
  * own key). Short-window pacing defaults already exist in provider-fetch; this
@@ -40,7 +40,7 @@ export declare const DEFAULT_BYOK_COLUMN_RUN_MAX_ROWS: Record<PlanTier, number |
40
40
  * with the self-serve override surface live. A workspace
41
41
  * provider_rate_limit_policies row with windowSeconds=86400 overrides it.
42
42
  */
43
- export declare const DEFAULT_BYOK_PROVIDER_DAILY_CALL_CAP: Record<PlanTier, number | null>;
43
+ export declare const DEFAULT_BYOK_PROVIDER_DAILY_CALL_CAP: Record<SpendSafetyPlanTier, number | null>;
44
44
  export declare const BYOK_PROVIDER_DAILY_WINDOW_SECONDS: number;
45
45
  export declare const BYOK_PROVIDER_DAILY_CAP_ENFORCEMENT_ENV = "OXYGEN_BYOK_DAILY_CAPS";
46
46
  export type ByokProviderDailyCapEnforcementMode = "observe_only" | "enforced";
@@ -105,10 +105,14 @@ export type OrgDailySpendGuard = {
105
105
  * decision is made, so enforcement and the read surfaces cannot drift apart.
106
106
  */
107
107
  export declare function resolveOrgDailySpendGuard(monthlyCredits: number | null | undefined): OrgDailySpendGuard | null;
108
- export declare function resolveDefaultTriggerRunCreditCeiling(tier: PlanTier): number | null;
109
- export declare function resolveDefaultAutoRunBatchCreditCeiling(tier: PlanTier): number | null;
110
- export declare function resolveDefaultByokColumnRunMaxRows(tier: PlanTier): number | null;
111
- export declare function resolveDefaultByokProviderDailyCallCap(tier: PlanTier): number | null;
108
+ /** Credits carry 3 decimals everywhere (control-DB numeric(18,3)). */
109
+ export declare function roundCredits(value: number): number;
110
+ /** Credit estimates that present to 2 decimals (cent-grained customer copy). */
111
+ export declare function roundCreditsToCents(value: number): number;
112
+ export declare function resolveDefaultTriggerRunCreditCeiling(tier: SpendSafetyPlanTier): number | null;
113
+ export declare function resolveDefaultAutoRunBatchCreditCeiling(tier: SpendSafetyPlanTier): number | null;
114
+ export declare function resolveDefaultByokColumnRunMaxRows(tier: SpendSafetyPlanTier): number | null;
115
+ export declare function resolveDefaultByokProviderDailyCallCap(tier: SpendSafetyPlanTier): number | null;
112
116
  /**
113
117
  * The plan tiers the spend-safety default tables above are keyed by. `limitsTier`
114
118
  * collapses "enterprise" onto the "scale" enforcement rung, but these tables carry
@@ -119,5 +123,13 @@ export declare function resolveDefaultByokProviderDailyCallCap(tier: PlanTier):
119
123
  * `oxygen limits` report, the org-facing provider-limits API, and the BYOK
120
124
  * daily-cap enforcement resolver — they must agree on which column an org reads.
121
125
  */
122
- export declare const SPEND_SAFETY_PLAN_TIERS: readonly PlanTier[];
123
- export declare function resolveSpendSafetyPlanTier(planTier: string | null, limitsTier: LimitsTier): PlanTier;
126
+ /**
127
+ * The tiers these tables are keyed by. `oxygen` is deliberately ABSENT: its six
128
+ * rungs span four enforcement rungs, so an Oxygen plan must fall through to its
129
+ * limitsTier (the price band) rather than read one shared column. Adding an
130
+ * "oxygen" column here would silently give a $1,999 workspace the same ceiling
131
+ * as a $49 one.
132
+ */
133
+ export type SpendSafetyPlanTier = Exclude<PlanTier, "oxygen">;
134
+ export declare const SPEND_SAFETY_PLAN_TIERS: readonly SpendSafetyPlanTier[];
135
+ export declare function resolveSpendSafetyPlanTier(planTier: string | null, limitsTier: LimitsTier): SpendSafetyPlanTier;
@@ -9,11 +9,11 @@ export const AUTONOMOUS_WORKFLOW_TRIGGER_TYPES = new Set([
9
9
  * trigger when neither run metadata nor the manifest declares `max_credits`.
10
10
  */
11
11
  export const DEFAULT_TRIGGER_RUN_CREDIT_CEILING = {
12
- free: 500,
13
- starter: 5_000,
14
- pro: 10_000,
15
- team: 25_000,
16
- scale: 250_000,
12
+ free: 50,
13
+ starter: 500,
14
+ pro: 1_000,
15
+ team: 2_500,
16
+ scale: 25_000,
17
17
  enterprise: null,
18
18
  };
19
19
  /**
@@ -21,11 +21,11 @@ export const DEFAULT_TRIGGER_RUN_CREDIT_CEILING = {
21
21
  * batch when the armed configuration carries no explicit `max_credits`.
22
22
  */
23
23
  export const DEFAULT_AUTO_RUN_BATCH_CREDIT_CEILING = {
24
- free: 500,
25
- starter: 5_000,
26
- pro: 10_000,
27
- team: 25_000,
28
- scale: 250_000,
24
+ free: 50,
25
+ starter: 500,
26
+ pro: 1_000,
27
+ team: 2_500,
28
+ scale: 25_000,
29
29
  enterprise: null,
30
30
  };
31
31
  /**
@@ -129,9 +129,13 @@ export function resolveOrgDailySpendGuard(monthlyCredits) {
129
129
  };
130
130
  }
131
131
  /** Credits carry 3 decimals everywhere (control-DB numeric(18,3)). */
132
- function roundCredits(value) {
132
+ export function roundCredits(value) {
133
133
  return Math.round(value * 1000) / 1000;
134
134
  }
135
+ /** Credit estimates that present to 2 decimals (cent-grained customer copy). */
136
+ export function roundCreditsToCents(value) {
137
+ return Math.round((value + Number.EPSILON) * 100) / 100;
138
+ }
135
139
  export function resolveDefaultTriggerRunCreditCeiling(tier) {
136
140
  return DEFAULT_TRIGGER_RUN_CREDIT_CEILING[tier];
137
141
  }
@@ -144,16 +148,6 @@ export function resolveDefaultByokColumnRunMaxRows(tier) {
144
148
  export function resolveDefaultByokProviderDailyCallCap(tier) {
145
149
  return DEFAULT_BYOK_PROVIDER_DAILY_CALL_CAP[tier];
146
150
  }
147
- /**
148
- * The plan tiers the spend-safety default tables above are keyed by. `limitsTier`
149
- * collapses "enterprise" onto the "scale" enforcement rung, but these tables carry
150
- * a distinct "enterprise" column (null = runs on custom limits) — so prefer the
151
- * raw plan_tier whenever it is itself a table key, and fall back to the limits rung
152
- * (always a valid PlanTier key) when plan_tier is absent or an unknown/legacy
153
- * string (e.g. a Stripe price nickname). Single source of truth for the
154
- * `oxygen limits` report, the org-facing provider-limits API, and the BYOK
155
- * daily-cap enforcement resolver — they must agree on which column an org reads.
156
- */
157
151
  export const SPEND_SAFETY_PLAN_TIERS = [
158
152
  "free",
159
153
  "starter",
@@ -0,0 +1 @@
1
+ export declare function executeRows<T>(value: unknown): T[];
@@ -0,0 +1,3 @@
1
+ export function executeRows(value) {
2
+ return (value.rows ?? value);
3
+ }
@@ -1,4 +1,5 @@
1
1
  import { SpanStatusCode, metrics, trace, } from "@opentelemetry/api";
2
+ import { deployEnvTelemetryEnvironment, deployPlatformAttribute } from "./deploy-env.js";
2
3
  import { errorId } from "./log.js";
3
4
  import { normalizeTelemetryAttributes } from "./redaction.js";
4
5
  import { redactSqlParameters, sqlErrorTelemetryAttributes } from "./sql-error.js";
@@ -73,7 +74,14 @@ export function commonTelemetryAttributes(attributes) {
73
74
  const pick = (workerKeys, webKeys) => pickEnvBySurface(workerProcess, workerKeys, webKeys);
74
75
  return {
75
76
  "oxygen.version": OXYGEN_VERSION,
76
- "deployment.environment": pick(["FLY_ENVIRONMENT", "NODE_ENV"], ["VERCEL_ENV", "NODE_ENV"]),
77
+ // Which infrastructure this process runs on, so a self-hosted span is
78
+ // never mistaken for a managed one while both targets are live.
79
+ "oxygen.platform": deployPlatformAttribute(),
80
+ // OXYGEN_DEPLOY_ENV is consulted first and is the only signal a self-hosted
81
+ // box has: it carries neither FLY_ENVIRONMENT nor VERCEL_ENV, so without
82
+ // this the whole fleet would report its NODE_ENV and collapse prod, shadow
83
+ // and dev into one bucket. Unset leaves the managed chain untouched.
84
+ "deployment.environment": deployEnvTelemetryEnvironment() ?? pick(["FLY_ENVIRONMENT", "NODE_ENV"], ["VERCEL_ENV", "NODE_ENV"]),
77
85
  // deployment.sha stays the deploy artifact id: the Fly registry image ref on
78
86
  // the worker (release cross-reference), VERCEL_GIT_COMMIT_SHA on the web. The
79
87
  // worker's image ref is NOT a git commit, so deployment.git_sha carries the
@@ -13,3 +13,25 @@
13
13
  * `Record<string, unknown>`. Rejects `null`, arrays, and every primitive.
14
14
  */
15
15
  export declare function isRecord(value: unknown): value is Record<string, unknown>;
16
+ /**
17
+ * `value` as a `Record<string, unknown>` when it is a non-null, non-array
18
+ * object, else null.
19
+ */
20
+ export declare function asRecordOrNull(value: unknown): Record<string, unknown> | null;
21
+ /** Alias of {@link asRecordOrNull} for call sites that spell it `asRecord`. */
22
+ export declare const asRecord: typeof asRecordOrNull;
23
+ /**
24
+ * `value` as a `Record<string, unknown>`, falling back to an empty object.
25
+ * Deliberately masks "absent" and "present but wrong shape" the same way its
26
+ * call sites always have — use {@link asRecordOrNull} when that matters.
27
+ */
28
+ export declare function asRecordOrEmpty(value: unknown): Record<string, unknown>;
29
+ /** Finite number, else null. Does not parse numeric strings. */
30
+ export declare function readFiniteNumber(value: unknown): number | null;
31
+ /** Trimmed non-empty string, else null. */
32
+ export declare function readTrimmedString(value: unknown): string | null;
33
+ /**
34
+ * Trimmed non-empty string, else null — additionally coercing a number to its
35
+ * string form. Distinct from {@link readTrimmedString}, which rejects numbers.
36
+ */
37
+ export declare function readCoercedString(value: unknown): string | null;