@oxygen-agent/cli 1.1011.97 → 1.1011.288

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 (58) hide show
  1. package/README.md +1 -1
  2. package/dist/admin-primary-providers-render.js +25 -5
  3. package/dist/column-run-notices.d.ts +16 -0
  4. package/dist/column-run-notices.js +57 -0
  5. package/dist/command-manifest.js +10 -1
  6. package/dist/help.d.ts +27 -1
  7. package/dist/help.js +112 -29
  8. package/dist/http-client.js +8 -0
  9. package/dist/index.js +348 -127
  10. package/node_modules/@oxygen/cli-ugc/dist/commands.js +20 -69
  11. package/node_modules/@oxygen/shared/dist/byok-connect.d.ts +6 -4
  12. package/node_modules/@oxygen/shared/dist/byok-connect.js +11 -4
  13. package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +127 -1
  14. package/node_modules/@oxygen/shared/dist/capability-discovery.js +286 -24
  15. package/node_modules/@oxygen/shared/dist/collab.d.ts +1 -1
  16. package/node_modules/@oxygen/shared/dist/collab.js +12 -0
  17. package/node_modules/@oxygen/shared/dist/column-decision.js +7 -3
  18. package/node_modules/@oxygen/shared/dist/column-output-fields.js +45 -3
  19. package/node_modules/@oxygen/shared/dist/company-enrichment-fields.d.ts +20 -13
  20. package/node_modules/@oxygen/shared/dist/company-enrichment-fields.js +29 -13
  21. package/node_modules/@oxygen/shared/dist/contracts/sender-limits.js +1 -1
  22. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +2 -2
  23. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +2 -2
  24. package/node_modules/@oxygen/shared/dist/credit-guidance.js +12 -1
  25. package/node_modules/@oxygen/shared/dist/enrichment-intents.d.ts +37 -0
  26. package/node_modules/@oxygen/shared/dist/enrichment-intents.js +79 -44
  27. package/node_modules/@oxygen/shared/dist/linkedin-url.d.ts +7 -0
  28. package/node_modules/@oxygen/shared/dist/linkedin-url.js +9 -0
  29. package/node_modules/@oxygen/shared/dist/log.d.ts +1 -0
  30. package/node_modules/@oxygen/shared/dist/log.js +15 -0
  31. package/node_modules/@oxygen/shared/dist/operational-telemetry.d.ts +1 -0
  32. package/node_modules/@oxygen/shared/dist/operational-telemetry.js +6 -2
  33. package/node_modules/@oxygen/shared/dist/otlp-log-sink.d.ts +7 -3
  34. package/node_modules/@oxygen/shared/dist/otlp-log-sink.js +22 -1
  35. package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +3 -7
  36. package/node_modules/@oxygen/shared/dist/plan-limits.js +2 -14
  37. package/node_modules/@oxygen/shared/dist/sending-schedule.d.ts +39 -0
  38. package/node_modules/@oxygen/shared/dist/sending-schedule.js +41 -0
  39. package/node_modules/@oxygen/shared/dist/sequence-lifecycle.d.ts +19 -0
  40. package/node_modules/@oxygen/shared/dist/sequence-lifecycle.js +20 -0
  41. package/node_modules/@oxygen/shared/dist/sequence-sender-variables.d.ts +35 -0
  42. package/node_modules/@oxygen/shared/dist/sequence-sender-variables.js +63 -0
  43. package/node_modules/@oxygen/shared/dist/sequences.d.ts +13 -7
  44. package/node_modules/@oxygen/shared/dist/sequences.js +26 -0
  45. package/node_modules/@oxygen/shared/dist/signup-lead-payload.d.ts +2 -0
  46. package/node_modules/@oxygen/shared/dist/signup-lead-payload.js +5 -0
  47. package/node_modules/@oxygen/shared/dist/social-post-metrics-series.js +1 -15
  48. package/node_modules/@oxygen/shared/dist/standard-webhooks.d.ts +51 -0
  49. package/node_modules/@oxygen/shared/dist/standard-webhooks.js +124 -0
  50. package/node_modules/@oxygen/shared/dist/tags.d.ts +1 -1
  51. package/node_modules/@oxygen/shared/dist/tags.js +13 -1
  52. package/node_modules/@oxygen/shared/dist/ugc.d.ts +11 -7
  53. package/node_modules/@oxygen/shared/dist/ugc.js +14 -6
  54. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  55. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  56. package/node_modules/@oxygen/shared/dist/zoned-time.d.ts +39 -0
  57. package/node_modules/@oxygen/shared/dist/zoned-time.js +81 -0
  58. package/package.json +1 -1
@@ -31,6 +31,8 @@ export const ENRICHMENT_INTENT_GROUP_LABELS = {
31
31
  signal: "Signals",
32
32
  };
33
33
  const f = (key, label, semanticType, dataType = "text", derived) => ({ key, label, semanticType, dataType, ...(derived ? { derived: true } : {}) });
34
+ /** A field read out of one column's cell, e.g. the email in `work_email_enrichment.value`. */
35
+ const cell = (key, path, label, semanticType, dataType = "text") => ({ key, path, label, semanticType, dataType });
34
36
  export const ENRICHMENT_INTENT_TAXONOMY = [
35
37
  // ── Person ────────────────────────────────────────────────────────────────
36
38
  {
@@ -48,13 +50,13 @@ export const ENRICHMENT_INTENT_TAXONOMY = [
48
50
  f("first_name", "First name", "person.first_name"),
49
51
  f("last_name", "Last name", "person.last_name"),
50
52
  f("job_title", "Job title", "person.job_title"),
51
- f("headline", "Headline", "person.headline"),
53
+ f("person_headline", "Headline", "person.headline"),
52
54
  f("current_company", "Current company", null),
53
55
  f("current_company_linkedin_url", "Current company LinkedIn URL", "company.linkedin_url"),
54
56
  f("job_start_date", "Job start date", null),
55
- f("location", "Location", "person.location"),
56
- f("bio", "Bio", "person.bio"),
57
- f("linkedin_followers", "LinkedIn followers", "person.linkedin_followers", "numeric"),
57
+ f("person_location", "Location", "person.location"),
58
+ f("person_bio", "Bio", "person.bio"),
59
+ f("person_linkedin_followers", "LinkedIn followers", "person.linkedin_followers", "numeric"),
58
60
  f("education", "Education", null, "jsonb"),
59
61
  f("school_name", "School", null),
60
62
  ],
@@ -73,8 +75,9 @@ export const ENRICHMENT_INTENT_TAXONOMY = [
73
75
  aliases: ["linkedin url", "linkedin urls", "find linkedin", "profile url", "resolve linkedin", "linkedin for each"],
74
76
  inputShapes: ["name_company"],
75
77
  outputFields: [
76
- f("linkedin_url", "LinkedIn URL", "person.linkedin_url"),
77
- f("linkedin_url_confidence", "Match confidence", null),
78
+ cell("linkedin_url_enrichment", "value", "LinkedIn URL", "person.linkedin_url"),
79
+ // The identity check's verdict on the hit (D46): a URL that fails it is a miss.
80
+ cell("linkedin_url_enrichment", "confidence", "Match confidence", null),
78
81
  ],
79
82
  surfaces: { capability: "linkedin_url", find: "find linkedin", managedFunction: "person_linkedin_url", catalogEntry: "capability:linkedin_url", registryFunction: "enrichment", registryIntent: "find_linkedin_profile" },
80
83
  status: "default",
@@ -83,7 +86,7 @@ export const ENRICHMENT_INTENT_TAXONOMY = [
83
86
  {
84
87
  id: "person_company",
85
88
  label: "Person's company",
86
- description: "Adds the person's current company, then fills that company's domain, industry and headcount from the same profile.",
89
+ description: "Adds the person's current company from their profile, then fills that company's domain, industry and headcount.",
87
90
  entity: "person",
88
91
  group: "person",
89
92
  kind: "template",
@@ -94,8 +97,10 @@ export const ENRICHMENT_INTENT_TAXONOMY = [
94
97
  f("current_company_linkedin_url", "Company LinkedIn URL", "company.linkedin_url"),
95
98
  f("company_domain", "Company domain", "company.domain"),
96
99
  f("company_industry", "Industry", "company.industry"),
97
- f("company_headcount", "Headcount", "company.employee_count", "numeric"),
100
+ // Text: the exact count when the lookup has one, otherwise the size band.
101
+ f("company_headcount", "Headcount", "company.employee_count"),
98
102
  ],
103
+ // The recipe's two preset commands are this row's add path.
99
104
  surfaces: { catalogEntry: "recipe:person_company" },
100
105
  status: "default",
101
106
  },
@@ -111,11 +116,22 @@ export const ENRICHMENT_INTENT_TAXONOMY = [
111
116
  aliases: ["work email", "business email", "email address", "find email", "emails for", "email finder", "with emails"],
112
117
  inputShapes: ["linkedin_url", "name_domain", "first_last_domain"],
113
118
  outputFields: [
114
- f("email", "Work email", "person.work_email"),
115
- f("email_status", "Deliverability", "person.email_status"),
119
+ cell("work_email_enrichment", "value", "Work email", "person.work_email"),
120
+ // verified | catch_all | guessed | unverified, the grade the hit landed with.
121
+ cell("work_email_enrichment", "confidence", "Deliverability", "person.email_status"),
116
122
  ],
117
123
  surfaces: { capability: "work_email", find: "find email", managedFunction: "work_email", catalogEntry: "capability:work_email", registryFunction: "enrichment", registryIntent: "find_work_email" },
118
124
  status: "default",
125
+ // Founder, 2026-09-29: one email column; personal email is its option.
126
+ options: [
127
+ {
128
+ id: "include_personal_email",
129
+ intent: "find_personal_email",
130
+ label: "Personal email",
131
+ cliFlag: "--include-personal-email",
132
+ outputFields: [cell("work_email_enrichment", "personal_email.value", "Personal email", "person.personal_email")],
133
+ },
134
+ ],
119
135
  },
120
136
  {
121
137
  id: "find_personal_email",
@@ -127,10 +143,17 @@ export const ENRICHMENT_INTENT_TAXONOMY = [
127
143
  rank: 50,
128
144
  aliases: ["personal email", "gmail", "home email", "private email", "consumer email"],
129
145
  inputShapes: ["linkedin_url"],
130
- outputFields: [f("personal_email", "Personal email", "person.personal_email")],
131
- surfaces: { capability: "personal_email", find: "find personal-email", managedFunction: "personal_email", catalogEntry: "capability:personal_email", registryFunction: "enrichment", registryIntent: "find_personal_email" },
132
- status: "default",
133
- decision: "D43",
146
+ // The address lands in the Work email column's cell: the option is this
147
+ // row's add path.
148
+ outputFields: [cell("work_email_enrichment", "personal_email.value", "Personal email", "person.personal_email")],
149
+ // No column of its own (founder, 2026-09-29): the Work email column's
150
+ // `include_personal_email` option is its add path. `--capability
151
+ // personal_email` still seeds one for scripts that used it.
152
+ surfaces: { find: "find personal-email", managedFunction: "personal_email", catalogEntry: "capability:personal_email", registryFunction: "enrichment", registryIntent: "find_personal_email" },
153
+ status: "selectable",
154
+ statusReason: "An option of the Work email column rather than a column of its own: switch on Personal email when you add or edit Work email.",
155
+ decision: "D59",
156
+ optionOf: "find_work_email",
134
157
  },
135
158
  {
136
159
  id: "find_phone",
@@ -142,12 +165,10 @@ export const ENRICHMENT_INTENT_TAXONOMY = [
142
165
  rank: 60,
143
166
  aliases: ["mobile number", "mobile phone", "cell phone", "direct dial", "work phone", "phone number", "phone numbers", "this person's mobile", "find phone", "find mobile", "a mobile"],
144
167
  inputShapes: ["linkedin_url", "email", "name_domain"],
145
- outputFields: [
146
- f("mobile_phone", "Mobile phone", "person.mobile_phone"),
147
- f("work_phone", "Work / direct-dial phone", "person.phone"),
148
- f("phone_line_type", "Line type", null),
149
- f("phone_carrier", "Carrier", null),
150
- ],
168
+ // One number per row. Line type and carrier are the optional verification
169
+ // step's (`enrich-column run --verify-phone`), written as companion columns
170
+ // that run creates; the column this row's add path creates never holds them.
171
+ outputFields: [cell("mobile_phone_enrichment", "value", "Mobile phone", "person.mobile_phone")],
151
172
  surfaces: { capability: "mobile_phone", find: "find phone", managedFunction: "mobile_phone", catalogEntry: "capability:mobile_phone", registryFunction: "enrichment", registryIntent: "find_mobile_phone" },
152
173
  status: "default",
153
174
  decision: "D44",
@@ -162,7 +183,8 @@ export const ENRICHMENT_INTENT_TAXONOMY = [
162
183
  rank: 70,
163
184
  aliases: ["verify email", "verify these emails", "validate email", "email verification", "deliverability", "bounce check", "catch-all"],
164
185
  inputShapes: ["email"],
165
- outputFields: [f("email_verdict", "Verdict", "person.email_status")],
186
+ // The verdict is the cell's value: valid, invalid or catch_all (unknown writes none).
187
+ outputFields: [cell("email_verification", "value", "Verdict", "person.email_status")],
166
188
  surfaces: { capability: "verify_email", find: "verify email", catalogEntry: "capability:verify_email", registryFunction: "enrichment", registryIntent: "verify_email" },
167
189
  status: "default",
168
190
  decision: "D45",
@@ -177,14 +199,17 @@ export const ENRICHMENT_INTENT_TAXONOMY = [
177
199
  rank: 80,
178
200
  aliases: ["verify phone", "verify these phone numbers", "validate phone", "line type", "is this a mobile"],
179
201
  inputShapes: ["phone"],
202
+ // The companions the Phone column's verification step writes; the verdict
203
+ // only gates whether a found number is kept, and nothing stores it.
180
204
  outputFields: [
181
205
  f("phone_line_type", "Line type", null),
182
206
  f("phone_carrier", "Carrier", null),
183
- f("phone_verdict", "Verdict", null),
184
207
  ],
185
- surfaces: { registryFunction: "enrichment", registryIntent: "verify_phone" },
186
- status: "selectable",
187
- statusReason: "Runs today as the optional verification step of the Phone column; a standalone phone-verification column is a follow-up.",
208
+ surfaces: {},
209
+ status: "coming_soon",
210
+ statusReason: "Coming soon: checking numbers you already hold is not a column yet. The Phone column's optional verification step adds line type and carrier to the numbers it finds.",
211
+ decision: "D59",
212
+ candidateProviders: ["clearoutphone"],
188
213
  },
189
214
  // ── Company ───────────────────────────────────────────────────────────────
190
215
  {
@@ -205,14 +230,15 @@ export const ENRICHMENT_INTENT_TAXONOMY = [
205
230
  {
206
231
  id: "enrich_company:headcount",
207
232
  label: "Headcount",
208
- description: "Current employee count and size band.",
233
+ description: "Current employee count, or the size band when no exact count is published.",
209
234
  entity: "company",
210
235
  group: "company",
211
236
  kind: "enrichment",
212
237
  rank: 110,
213
238
  aliases: ["headcount", "employee count", "how many employees", "company size", "employees"],
214
239
  inputShapes: ["domain", "linkedin_url"],
215
- outputFields: [f("company_headcount", "Headcount", "company.employee_count", "numeric"), f("company_employee_range", "Size band", "company.employee_range", "text", true)],
240
+ // One text column: the exact count when the lookup has one, otherwise the band.
241
+ outputFields: [f("company_headcount", "Headcount", "company.employee_count")],
216
242
  surfaces: { preset: "company_enrich", companyFields: ["headcount"], managedFunction: "company_employee_count", catalogEntry: "preset:company_enrich", registryFunction: "enrichment", registryIntent: "enrich_company", registryInputShapes: ["field:headcount"] },
217
243
  status: "default",
218
244
  decision: "D48",
@@ -235,22 +261,21 @@ export const ENRICHMENT_INTENT_TAXONOMY = [
235
261
  {
236
262
  id: "enrich_company:company_profile",
237
263
  label: "Company profile",
238
- description: "Description, founded year, headquarters, company type, specialties, size band and logo from one profile lookup.",
264
+ description: "Description, founded year and headquarters country from one profile lookup, with the whole profile kept alongside.",
239
265
  entity: "company",
240
266
  group: "company",
241
267
  kind: "profile",
242
268
  rank: 130,
243
269
  aliases: ["company profile", "company description", "founded year", "headquarters", "company type", "specialties", "company logo", "firmographics"],
244
270
  inputShapes: ["domain", "linkedin_url"],
271
+ // The company bundle's own columns (D56): its payload column holds the
272
+ // whole profile; HQ address, type, specialties and logo have no column of
273
+ // their own since the bundle stopped taking field choices.
245
274
  outputFields: [
246
- f("company_profile", "Company profile", null, "jsonb"),
275
+ f("company_enrich", "Company profile", null, "jsonb"),
247
276
  f("company_description", "Description", "company.description", "text", true),
248
277
  f("company_founded_year", "Founded year", "company.founded_year", "numeric", true),
249
- f("company_hq_address", "HQ address", "company.hq_location", "text", true),
250
278
  f("company_hq_country", "HQ country", "company.hq_country", "text", true),
251
- f("company_type", "Company type", null, "text", true),
252
- f("company_specialties", "Specialties", "company.specialties", "jsonb", true),
253
- f("company_logo_url", "Logo", "company.logo_url", "text", true),
254
279
  ],
255
280
  surfaces: { preset: "company_enrich", companyFields: ["company_profile", "description", "founded_year", "hq_address", "hq_country", "company_type", "specialties", "logo_url"], managedFunction: "enrich_company", catalogEntry: "preset:company_enrich", registryFunction: "enrichment", registryIntent: "enrich_company", registryInputShapes: ["field:company_profile"] },
256
281
  status: "default",
@@ -275,22 +300,26 @@ export const ENRICHMENT_INTENT_TAXONOMY = [
275
300
  f("company_investors", "Investors", null, "jsonb", true),
276
301
  ],
277
302
  // Not in the company bundle (founder, 2026-09-24: one profile lookup, no
278
- // opt-in fields); a caller names the fields on `find company`.
279
- surfaces: { find: "find company --domain <domain> --fields funding,funding_stage,latest_funding_round,total_funding,funding_rounds_count,investors", companyFields: ["funding", "funding_stage", "latest_funding_round", "total_funding", "funding_rounds_count", "investors"], managedFunction: "company_total_funding", registryFunction: "enrichment", registryIntent: "enrich_company", registryInputShapes: ["field:funding"] },
303
+ // opt-in fields). Its own bundle since 2026-09-29, the Tech stack shape:
304
+ // one waterfall column over these fields; `find company` answers one company.
305
+ surfaces: { preset: "company_funding", find: "find company --domain <domain> --fields funding,funding_stage,latest_funding_round,total_funding,funding_rounds_count,investors", companyFields: ["funding", "funding_stage", "latest_funding_round", "total_funding", "funding_rounds_count", "investors"], managedFunction: "company_total_funding", catalogEntry: "preset:company_funding", registryFunction: "enrichment", registryIntent: "enrich_company", registryInputShapes: ["field:funding"] },
280
306
  status: "default",
281
307
  decision: "D49",
282
308
  },
283
309
  {
284
310
  id: "enrich_company:technologies",
285
311
  label: "Tech stack",
286
- description: "Technologies the company runs: from its job postings with confidence and first/last-seen dates, or from its website; plus a yes/no check for technologies you name.",
312
+ description: "Technologies the company runs: from its job postings with confidence and first/last-seen dates, or from its website.",
287
313
  entity: "company",
288
314
  group: "company",
289
315
  kind: "enrichment",
290
316
  rank: 150,
291
317
  aliases: ["tech stack", "technologies they use", "technographics", "tools they use", "software they use", "built with"],
292
318
  inputShapes: ["domain"],
293
- outputFields: [f("company_technologies", "Technologies", "company.technologies", "jsonb"), f("company_technology_check", "Uses named technology", null, "boolean", true)],
319
+ // The yes/no check for named technologies is `find company --fields
320
+ // technology_check --check-technologies <names>`, one company at a time: no
321
+ // preset takes a technology list since 2026-09-24, so the column never has it.
322
+ outputFields: [f("company_technologies", "Technologies", "company.technologies", "jsonb")],
294
323
  surfaces: { preset: "company_tech_stack", companyFields: ["technologies", "technology_check"], managedFunction: "company_tech_stack", catalogEntry: "preset:company_tech_stack", registryFunction: "enrichment", registryIntent: "enrich_company", registryInputShapes: ["field:technologies"] },
295
324
  status: "default",
296
325
  decision: "D49",
@@ -305,9 +334,9 @@ export const ENRICHMENT_INTENT_TAXONOMY = [
305
334
  rank: 160,
306
335
  aliases: ["open roles", "job openings", "are they hiring", "is hiring", "job postings", "hiring signals"],
307
336
  inputShapes: ["domain", "linkedin_url"],
308
- outputFields: [f("company_hiring_signals", "Hiring signals", null, "jsonb"), f("company_job_openings", "Open roles", null, "numeric", true)],
309
- // Not in the company bundle (founder, 2026-09-24).
310
- surfaces: { find: "find company --domain <domain> --fields hiring_signals,job_openings", companyFields: ["hiring_signals", "job_openings"], managedFunction: "company_job_openings", registryFunction: "enrichment", registryIntent: "enrich_company", registryInputShapes: ["field:hiring_signals"] },
337
+ outputFields: [f("company_hiring_signals", "Hiring signals", null, "jsonb"), f("company_job_openings", "Open roles", null, "jsonb", true)],
338
+ // Not in the company bundle (founder, 2026-09-24); its own bundle since 2026-09-29.
339
+ surfaces: { preset: "company_hiring", find: "find company --domain <domain> --fields hiring_signals,job_openings", companyFields: ["hiring_signals", "job_openings"], managedFunction: "company_job_openings", catalogEntry: "preset:company_hiring", registryFunction: "enrichment", registryIntent: "enrich_company", registryInputShapes: ["field:hiring_signals"] },
311
340
  status: "default",
312
341
  },
313
342
  {
@@ -432,8 +461,8 @@ export const ENRICHMENT_INTENT_TAXONOMY = [
432
461
  aliases: ["revenue", "annual revenue", "revenue estimate", "how much do they make", "arr", "reported revenue", "exact revenue", "financials", "company revenue"],
433
462
  inputShapes: ["domain"],
434
463
  outputFields: [f("company_revenue", "Revenue (estimate)", "company.revenue")],
435
- // Not in the company bundle (founder, 2026-09-24).
436
- surfaces: { find: "find company --domain <domain> --fields revenue", companyFields: ["revenue"], managedFunction: "company_revenue_estimate", registryFunction: "enrichment", registryIntent: "enrich_company", registryInputShapes: ["field:revenue"] },
464
+ // Not in the company bundle (founder, 2026-09-24); its own bundle since 2026-09-29.
465
+ surfaces: { preset: "company_revenue", find: "find company --domain <domain> --fields revenue", companyFields: ["revenue"], managedFunction: "company_revenue_estimate", catalogEntry: "preset:company_revenue", registryFunction: "enrichment", registryIntent: "enrich_company", registryInputShapes: ["field:revenue"] },
437
466
  status: "default",
438
467
  decision: "D50",
439
468
  },
@@ -498,7 +527,13 @@ export const ENRICHMENT_INTENT_TAXONOMY = [
498
527
  rank: 300,
499
528
  aliases: ["decision maker", "decision makers", "head of sales at", "people at these companies", "contacts at each account", "who should i contact", "find people at company"],
500
529
  inputShapes: ["company_domain", "company_linkedin_url"],
501
- outputFields: [f("full_name", "Full name", "person.full_name"), f("job_title", "Job title", "person.job_title"), f("linkedin_url", "LinkedIn URL", "person.linkedin_url")],
530
+ // The people table a table-sourced run creates, each row linked back to its company.
531
+ outputFields: [
532
+ f("first_name", "First name", "person.first_name"),
533
+ f("last_name", "Last name", "person.last_name"),
534
+ f("title", "Job title", "person.job_title"),
535
+ f("linkedin_url", "LinkedIn URL", "person.linkedin_url"),
536
+ ],
502
537
  surfaces: { managedFunction: "find_people_at_company", registryFunction: "people_search", registryIntent: "account_contacts" },
503
538
  status: "default",
504
539
  },
@@ -512,7 +547,7 @@ export const ENRICHMENT_INTENT_TAXONOMY = [
512
547
  rank: 310,
513
548
  aliases: ["find people matching", "people search", "persona list", "prospect list", "leads matching"],
514
549
  inputShapes: ["persona_filters"],
515
- outputFields: [f("full_name", "Full name", "person.full_name"), f("job_title", "Job title", "person.job_title"), f("linkedin_url", "LinkedIn URL", "person.linkedin_url")],
550
+ outputFields: [f("full_name", "Full name", "person.full_name"), f("title", "Job title", "person.job_title"), f("linkedin_url", "LinkedIn URL", "person.linkedin_url")],
516
551
  surfaces: { registryFunction: "people_search", registryIntent: "persona_search" },
517
552
  status: "default",
518
553
  },
@@ -25,6 +25,13 @@ export type LinkedinUrlNormalization = {
25
25
  dispatchUrl: string;
26
26
  } | null;
27
27
  export declare function normalizeLinkedinProfileUrl(raw: string | null | undefined): LinkedinUrlNormalization;
28
+ /**
29
+ * True when a profile handle is a member PROVIDER id (`ACoAA…` / `ACwAA…`), not a
30
+ * public identifier. People search answers some members with `/in/<id>` instead
31
+ * of their public URL; such a URL never equals the member's public profile URL
32
+ * from any other source, so it cannot serve as a LinkedIn URL identity key.
33
+ */
34
+ export declare function isLinkedinMemberIdHandle(handle: string | null | undefined): boolean;
28
35
  /**
29
36
  * Canonicalize any LinkedIn PROFILE REFERENCE a customer might put in a cell:
30
37
  * a full `/in/` URL, a scheme-less `linkedin.com/in/…`, or a BARE public handle
@@ -75,6 +75,15 @@ export function normalizeLinkedinProfileUrl(raw) {
75
75
  // too (`ACOAA…`, the shape a spreadsheet UPPER() or a CRM export produces), so
76
76
  // such an id is named as unrecognized at enrollment instead of dying at send.
77
77
  const LINKEDIN_MEMBER_ID_PREFIX_RE = /^AC[OoWw]/;
78
+ /**
79
+ * True when a profile handle is a member PROVIDER id (`ACoAA…` / `ACwAA…`), not a
80
+ * public identifier. People search answers some members with `/in/<id>` instead
81
+ * of their public URL; such a URL never equals the member's public profile URL
82
+ * from any other source, so it cannot serve as a LinkedIn URL identity key.
83
+ */
84
+ export function isLinkedinMemberIdHandle(handle) {
85
+ return typeof handle === "string" && LINKEDIN_MEMBER_ID_PREFIX_RE.test(handle.trim());
86
+ }
78
87
  // LinkedIn's public identifier ("vanity slug") charset: letters, digits and
79
88
  // hyphens only, 3–100 characters, never leading with a hyphen. Deliberately
80
89
  // NARROWER than "any non-URL string" — no `.`, `@`, `_`, `:`, `/` or whitespace
@@ -18,6 +18,7 @@ export declare function withLogContext<T>(ctx: LogContext, fn: () => T): T;
18
18
  export declare function enterLogContext(ctx: LogContext): void;
19
19
  export declare function logLevelForHttpStatus(status: number): LogLevel;
20
20
  export type LogSink = (record: Record<string, unknown>) => void;
21
+ export declare function takeWebLogVolumeCount(): number;
21
22
  export declare function setLogSink(sink: LogSink | null): void;
22
23
  export declare function getLogSink(): LogSink | null;
23
24
  export declare function log(level: LogLevel, msg: string, fields?: Record<string, unknown>): void;
@@ -115,6 +115,17 @@ export function logLevelForHttpStatus(status) {
115
115
  // object all bundle copies share; Symbol.for gives every copy the same slot.
116
116
  // Same trap + same fix as linkedin-quota's enforcer registration.
117
117
  const LOG_SINK_SLOT = Symbol.for("oxygen.log.sink");
118
+ const WEB_LOG_VOLUME_SLOT = Symbol.for("oxygen.log.web-volume");
119
+ // Count the same web records eligible for the log sink before the OTLP WARN
120
+ // floor discards INFO. The slot must be shared across Next.js route bundles and
121
+ // instrumentation, just like LOG_SINK_SLOT. A one-minute summary lets SigNoz
122
+ // detect a fall in total logging without storing every INFO record.
123
+ export function takeWebLogVolumeCount() {
124
+ const carrier = globalThis;
125
+ const count = carrier[WEB_LOG_VOLUME_SLOT] ?? 0;
126
+ carrier[WEB_LOG_VOLUME_SLOT] = 0;
127
+ return count;
128
+ }
118
129
  export function setLogSink(sink) {
119
130
  globalThis[LOG_SINK_SLOT] = sink;
120
131
  }
@@ -188,6 +199,10 @@ export function log(level, msg, fields) {
188
199
  const logSink = getLogSink();
189
200
  if (!logSink)
190
201
  return;
202
+ if (record.service_name === "oxygen-web" && msg !== "web.log_volume") {
203
+ const carrier = globalThis;
204
+ carrier[WEB_LOG_VOLUME_SLOT] = (carrier[WEB_LOG_VOLUME_SLOT] ?? 0) + 1;
205
+ }
191
206
  try {
192
207
  logSink(record);
193
208
  }
@@ -2,6 +2,7 @@ type AttributeContainer = {
2
2
  attributes?: Record<string, unknown>;
3
3
  name?: string;
4
4
  };
5
+ export declare function maskEmails(value: string): string;
5
6
  /** Sink-local copy; the managed logger retains its original record. */
6
7
  export declare function operationalLogSnapshot(record: Record<string, unknown>): Record<string, unknown>;
7
8
  /** Export a sanitized copy; never mutate the span another destination reads. */
@@ -3,18 +3,22 @@ import { forwardExporterMethods } from "./telemetry-export-observer.js";
3
3
  // Namespaced semantic-convention fields evade the generic exact-key log policy.
4
4
  // Numeric usage remains operational data, subject to the shared secret policy.
5
5
  const PAYLOAD_KEY = /(?:^|[._-])(?:prompts?|completions?|inputs?|outputs?|payloads?|rows?|messages?|content|body|customer_data)(?:$|[._-])|^(?:request|response)$/i;
6
+ // Error text, not a message body: ADR 0023 logs the detail withheld from a
7
+ // customer as `error_message` (errorFields) or `<scope>_error_message`, and
8
+ // SigNoz is the only dev log store. Sanitized like `error` (decided 2026-09-29).
9
+ const ERROR_TEXT_KEY = /(?:^|_)error_message$/i;
6
10
  const EXCEPTION_CONTENT = /^exception\.(?:message|stacktrace)$/i;
7
11
  const QUERY_CONTENT = /^(?:db\.(?:statement|query\.text)(?:[._].*)?|(?:http|url)\.(?:query|query_string|request\.query)|process\.command(?:_args|_line)?)$/i;
8
12
  // Addresses keep their domain (the operational signal) and drop the person.
9
13
  const EMAIL_ADDRESS = /[A-Za-z0-9._%+-]+@((?:[A-Za-z0-9-]+\.)+[A-Za-z]{2,})/g;
10
- function maskEmails(value) {
14
+ export function maskEmails(value) {
11
15
  return value.replace(EMAIL_ADDRESS, "***@$1");
12
16
  }
13
17
  function operationalText(value) {
14
18
  return maskEmails(redactSecretsInString(value).replace(/(?:https?:\/\/|\/)[^\s"'<>]+/gi, stripOperationalUrl));
15
19
  }
16
20
  function operationalValue(value, key, depth = 0) {
17
- if (PAYLOAD_KEY.test(key) && typeof value !== "number" && typeof value !== "boolean")
21
+ if (PAYLOAD_KEY.test(key) && !ERROR_TEXT_KEY.test(key) && typeof value !== "number" && typeof value !== "boolean")
18
22
  return "[omitted]";
19
23
  if (typeof value === "string")
20
24
  return /(?:^|[._-])(?:url|path|target|href)(?:$|[._-])/i.test(key)
@@ -46,13 +46,17 @@ export type OtlpLogSinkOptions = {
46
46
  /** Clock for the report throttle; tests only. */
47
47
  now?: (() => number) | undefined;
48
48
  /**
49
- * Lowest level this sink ships; records below it never enter the queue. Null
50
- * or absent ships every level. Sinks composed beside this one (Axiom) and
51
- * stdout are unaffected; see `resolveOtlpLogMinLevel`.
49
+ * Lowest level this sink ships, except exact messages listed in
50
+ * includeMessagesBelowMinLevel. Null or absent ships every level. Sinks
51
+ * composed beside this one (Axiom) and stdout are unaffected.
52
52
  */
53
53
  minLevel?: LogLevel | null | undefined;
54
+ /** Exact, low-volume monitor events that must reach OTLP below minLevel. */
55
+ includeMessagesBelowMinLevel?: readonly string[] | undefined;
54
56
  fetch?: typeof globalThis.fetch | undefined;
55
57
  };
58
+ /** Source alerts #13, #16, #17, #19, #43, #45, #51, #71, #73, #75–77, #87, #90, #92, #95 and #97 need these INFO events. */
59
+ export declare const OTLP_MONITOR_INFO_EVENTS: readonly ["tenant.migrations.cron.done", "outbound_send.sent", "worker_workflow_cron_overlap_skipped", "worker_workflow_cron_enqueued", "copilot.turn.finished", "worker.queue.heartbeat", "worker.queue.ledger", "worker.queue.redis", "worker.queue.unregistered_kind", "automation_usage.reconcile_completed", "provider_health.cron_completed", "provider_balances.cron_completed", "cost_snapshot.cron_completed", "worker.egress_order_reconcile_sweep", "posthog_canary.round_trip_ok", "web.log_volume"];
56
60
  export type OtlpLogSinkHandle = {
57
61
  sink: LogSink;
58
62
  flush: () => Promise<void>;
@@ -6,6 +6,25 @@ import { sanitizeLogFields } from "./redaction.js";
6
6
  import { trace } from "@opentelemetry/api";
7
7
  import { operationalLogSnapshot } from "./operational-telemetry.js";
8
8
  import { telemetryEndpointForLog } from "./telemetry-export-observer.js";
9
+ /** Source alerts #13, #16, #17, #19, #43, #45, #51, #71, #73, #75–77, #87, #90, #92, #95 and #97 need these INFO events. */
10
+ export const OTLP_MONITOR_INFO_EVENTS = [
11
+ "tenant.migrations.cron.done",
12
+ "outbound_send.sent",
13
+ "worker_workflow_cron_overlap_skipped",
14
+ "worker_workflow_cron_enqueued",
15
+ "copilot.turn.finished",
16
+ "worker.queue.heartbeat",
17
+ "worker.queue.ledger",
18
+ "worker.queue.redis",
19
+ "worker.queue.unregistered_kind",
20
+ "automation_usage.reconcile_completed",
21
+ "provider_health.cron_completed",
22
+ "provider_balances.cron_completed",
23
+ "cost_snapshot.cron_completed",
24
+ "worker.egress_order_reconcile_sweep",
25
+ "posthog_canary.round_trip_ok",
26
+ "web.log_volume",
27
+ ];
9
28
  const DEFAULT_BATCH_SIZE = 500;
10
29
  const DEFAULT_FLUSH_INTERVAL_MS = 2_000;
11
30
  const DEFAULT_MAX_QUEUE = 10_000;
@@ -189,6 +208,7 @@ export function createOtlpLogSink(options) {
189
208
  const doFetch = options.fetch ?? globalThis.fetch;
190
209
  const now = options.now ?? Date.now;
191
210
  const minSeverity = options.minLevel ? SEVERITY_NUMBERS[options.minLevel] : null;
211
+ const includedMessages = new Set(options.includeMessagesBelowMinLevel ?? []);
192
212
  // A null/undefined override is dropped rather than applied, so a caller that
193
213
  // resolves its own environment and comes back empty-handed falls back to the
194
214
  // default here instead of shipping records with no `deployment.environment`.
@@ -275,7 +295,8 @@ export function createOtlpLogSink(options) {
275
295
  if (stopped || emittingInternal)
276
296
  return;
277
297
  // An unknown level ships as INFO (see toLogRecord), so it filters as one.
278
- if (minSeverity !== null && SEVERITY_NUMBERS[recordLevel(record)] < minSeverity)
298
+ if (minSeverity !== null && SEVERITY_NUMBERS[recordLevel(record)] < minSeverity
299
+ && (typeof record.msg !== "string" || !includedMessages.has(record.msg)))
279
300
  return;
280
301
  // Drop the OLDEST record and keep this one: under sustained backpressure the
281
302
  // newest lines are the ones describing the incident being debugged.
@@ -807,21 +807,17 @@ export declare function resolvePlanAgentCreditLimits(tier: LimitsTier, options?:
807
807
  */
808
808
  export declare const AGENT_RUN_APPROVED_CEILING_MAX_CREDITS = 25000;
809
809
  /** Where an org's default Agent run ceiling came from. */
810
- export type AgentRunDefaultCeilingSource = "plan" | "enterprise_contract";
810
+ export type AgentRunDefaultCeilingSource = "plan";
811
811
  /** The Agent run ceilings an org's runs are held to without a further approval. */
812
812
  export type AgentRunDefaultCeilings = PlanAgentCreditLimits & {
813
813
  source: AgentRunDefaultCeilingSource;
814
814
  };
815
815
  /**
816
- * The default Agent run ceilings for an org: its plan rung's, unless an
817
- * enterprise contract names its own default (decision F.4). The contract value
818
- * replaces the rung's total and model calls take half of it, as on every paid
819
- * rung. A contract value is clamped to the platform backstop; an unusable one
820
- * (absent, non-numeric, not positive) is ignored.
816
+ * The default Agent run ceilings for an org come from its plan rung. Enterprise
817
+ * uses the $1,999 rung; any higher run ceiling needs explicit approval.
821
818
  */
822
819
  export declare function resolveAgentRunDefaultCeilings(input: {
823
820
  tier: LimitsTier;
824
- enterpriseDefaultCredits?: number | null;
825
821
  options?: RepricingOptions;
826
822
  }): AgentRunDefaultCeilings;
827
823
  /** `details.approval_kind` on the refusal below, so every surface can render its own approval card. */
@@ -368,22 +368,10 @@ export function resolvePlanAgentCreditLimits(tier, options = {}) {
368
368
  */
369
369
  export const AGENT_RUN_APPROVED_CEILING_MAX_CREDITS = 25_000;
370
370
  /**
371
- * The default Agent run ceilings for an org: its plan rung's, unless an
372
- * enterprise contract names its own default (decision F.4). The contract value
373
- * replaces the rung's total and model calls take half of it, as on every paid
374
- * rung. A contract value is clamped to the platform backstop; an unusable one
375
- * (absent, non-numeric, not positive) is ignored.
371
+ * The default Agent run ceilings for an org come from its plan rung. Enterprise
372
+ * uses the $1,999 rung; any higher run ceiling needs explicit approval.
376
373
  */
377
374
  export function resolveAgentRunDefaultCeilings(input) {
378
- const contract = input.enterpriseDefaultCredits;
379
- if (typeof contract === "number" && Number.isFinite(contract) && contract >= 1) {
380
- const total = Math.min(Math.floor(contract), AGENT_RUN_APPROVED_CEILING_MAX_CREDITS);
381
- return {
382
- maxTotalCredits: total,
383
- maxInferenceCredits: Math.max(1, Math.floor(total / 2)),
384
- source: "enterprise_contract",
385
- };
386
- }
387
375
  return { ...resolvePlanAgentCreditLimits(input.tier, input.options), source: "plan" };
388
376
  }
389
377
  /** `details.approval_kind` on the refusal below, so every surface can render its own approval card. */
@@ -0,0 +1,39 @@
1
+ import type { SequenceSendWindow } from "./sequences.js";
2
+ /**
3
+ * The one sending schedule Launch shows (web editor, CLI `--schedule-*`, MCP
4
+ * `sending_schedule`). Email and social dispatch keep separate stored windows,
5
+ * `settings.email_send_window` and `settings.schedule`, so one schedule is
6
+ * written to both.
7
+ *
8
+ * A leaf of its own, importing only types: the Launch editor's day and time
9
+ * pickers are also rendered on Connections, and a value import of the whole
10
+ * sequences module added 13.8 KB gzip to that page (measured 2026-09-29).
11
+ */
12
+ export type SendingSchedule = {
13
+ timezone: string;
14
+ days: number[];
15
+ start: string;
16
+ end: string;
17
+ };
18
+ /**
19
+ * The settings fragment that applies one schedule to every channel. The email
20
+ * window keeps its recipient-timezone fields; the social window takes the same
21
+ * zone, days and hours.
22
+ */
23
+ export declare function sendingScheduleSettings(emailWindow: SequenceSendWindow | null | undefined, schedule: SendingSchedule): {
24
+ email_send_window: SequenceSendWindow;
25
+ schedule: SendingSchedule;
26
+ };
27
+ /**
28
+ * Stable comparison key for a window: zone, sorted ISO days and hours. Days are
29
+ * read the way dispatch reads them (sendWindowAllowsWeekday in ./sequences):
30
+ * day names count, and a missing, malformed or unrecognisable list means every
31
+ * day.
32
+ */
33
+ export declare function sendingScheduleKey(window: Pick<SequenceSendWindow, "timezone" | "days" | "start" | "end">): string;
34
+ /**
35
+ * Whether a mixed journey's two stored windows disagree: the social window is
36
+ * unset, or its zone, days or hours differ from the effective email window.
37
+ * Launch marks this state and the launch check warns about it.
38
+ */
39
+ export declare function sendingWindowsDiverge(effectiveEmailWindow: SequenceSendWindow, socialWindow: Pick<SequenceSendWindow, "timezone" | "days" | "start" | "end"> | null | undefined): boolean;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The settings fragment that applies one schedule to every channel. The email
3
+ * window keeps its recipient-timezone fields; the social window takes the same
4
+ * zone, days and hours.
5
+ */
6
+ export function sendingScheduleSettings(emailWindow, schedule) {
7
+ return { email_send_window: { ...emailWindow, ...schedule }, schedule: { ...schedule } };
8
+ }
9
+ /**
10
+ * Stable comparison key for a window: zone, sorted ISO days and hours. Days are
11
+ * read the way dispatch reads them (sendWindowAllowsWeekday in ./sequences):
12
+ * day names count, and a missing, malformed or unrecognisable list means every
13
+ * day.
14
+ */
15
+ export function sendingScheduleKey(window) {
16
+ const listed = Array.isArray(window.days) ? window.days : [];
17
+ const days = [...new Set(listed.map(isoWeekdayOf).filter((day) => day !== null))].sort((a, b) => a - b);
18
+ return JSON.stringify({
19
+ timezone: window.timezone,
20
+ days: days.length > 0 ? days : [1, 2, 3, 4, 5, 6, 7],
21
+ start: window.start,
22
+ end: window.end,
23
+ });
24
+ }
25
+ /**
26
+ * Whether a mixed journey's two stored windows disagree: the social window is
27
+ * unset, or its zone, days or hours differ from the effective email window.
28
+ * Launch marks this state and the launch check warns about it.
29
+ */
30
+ export function sendingWindowsDiverge(effectiveEmailWindow, socialWindow) {
31
+ return !socialWindow || sendingScheduleKey(socialWindow) !== sendingScheduleKey(effectiveEmailWindow);
32
+ }
33
+ const WEEKDAY_BY_NAME = { Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6, Sun: 7 };
34
+ function isoWeekdayOf(entry) {
35
+ const day = typeof entry === "number"
36
+ ? entry
37
+ : typeof entry === "string"
38
+ ? WEEKDAY_BY_NAME[`${entry.trim().slice(0, 1).toUpperCase()}${entry.trim().slice(1, 3).toLowerCase()}`] ?? Number(entry)
39
+ : Number.NaN;
40
+ return Number.isInteger(day) && day >= 1 && day <= 7 ? day : null;
41
+ }
@@ -0,0 +1,19 @@
1
+ /** Every lifecycle a person reads: the stored statuses plus the derived `completed`. */
2
+ export declare const SEQUENCE_LIFECYCLES: readonly ["draft", "active", "completed", "paused", "archived"];
3
+ export type SequenceLifecycle = (typeof SEQUENCE_LIFECYCLES)[number];
4
+ /**
5
+ * The lifecycle word a person reads for a Sequence.
6
+ *
7
+ * The stored status records launch intent only (draft, active, paused,
8
+ * archived), and nothing moves it when the last lead finishes, so a campaign
9
+ * that contacted everyone kept reading "Active" forever. Whether work remains
10
+ * is the derived operational state; `drained` means every enrolled lead has
11
+ * reached an end. An active Sequence in that state reads as completed.
12
+ *
13
+ * Derived on read and never stored: operational health is one derived
14
+ * contract, not another lifecycle (docs/primitives/sequences.md, 2026-08-26).
15
+ * Enrolling new leads makes the same Sequence read as active again with no
16
+ * transition to undo. A paused Sequence stays paused, because someone decided
17
+ * that and the decision still holds.
18
+ */
19
+ export declare function sequenceDisplayStatus<Status extends string>(status: Status, operationalState: string | null | undefined): Status | "completed";