@oxygen-agent/cli 1.1011.98 → 1.1011.322

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 (74) 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/connectors-commands.d.ts +11 -0
  7. package/dist/connectors-commands.js +63 -0
  8. package/dist/help.d.ts +27 -1
  9. package/dist/help.js +112 -29
  10. package/dist/http-client.js +8 -0
  11. package/dist/index.js +362 -130
  12. package/node_modules/@oxygen/cli-ugc/dist/commands.js +20 -69
  13. package/node_modules/@oxygen/shared/dist/byok-connect.d.ts +6 -4
  14. package/node_modules/@oxygen/shared/dist/byok-connect.js +11 -4
  15. package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +131 -1
  16. package/node_modules/@oxygen/shared/dist/capability-discovery.js +325 -30
  17. package/node_modules/@oxygen/shared/dist/collab.d.ts +1 -1
  18. package/node_modules/@oxygen/shared/dist/collab.js +12 -0
  19. package/node_modules/@oxygen/shared/dist/column-decision.js +7 -3
  20. package/node_modules/@oxygen/shared/dist/column-output-fields.js +45 -3
  21. package/node_modules/@oxygen/shared/dist/company-enrichment-fields.d.ts +20 -13
  22. package/node_modules/@oxygen/shared/dist/company-enrichment-fields.js +29 -13
  23. package/node_modules/@oxygen/shared/dist/contracts/connectors.d.ts +23 -0
  24. package/node_modules/@oxygen/shared/dist/contracts/connectors.js +34 -0
  25. package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +0 -6
  26. package/node_modules/@oxygen/shared/dist/contracts/operations.js +8 -1
  27. package/node_modules/@oxygen/shared/dist/contracts/sender-limits.js +1 -1
  28. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +2 -2
  29. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +2 -2
  30. package/node_modules/@oxygen/shared/dist/credit-guidance.js +12 -1
  31. package/node_modules/@oxygen/shared/dist/enrichment-intents.d.ts +37 -0
  32. package/node_modules/@oxygen/shared/dist/enrichment-intents.js +79 -44
  33. package/node_modules/@oxygen/shared/dist/feature-gates.d.ts +5 -0
  34. package/node_modules/@oxygen/shared/dist/feature-gates.js +6 -0
  35. package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.d.ts +57 -8
  36. package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.js +74 -13
  37. package/node_modules/@oxygen/shared/dist/linkedin-url.d.ts +7 -0
  38. package/node_modules/@oxygen/shared/dist/linkedin-url.js +9 -0
  39. package/node_modules/@oxygen/shared/dist/log.d.ts +1 -0
  40. package/node_modules/@oxygen/shared/dist/log.js +15 -0
  41. package/node_modules/@oxygen/shared/dist/mcp-event-contract.d.ts +597 -0
  42. package/node_modules/@oxygen/shared/dist/mcp-event-contract.js +713 -0
  43. package/node_modules/@oxygen/shared/dist/mcp-events-enabled.d.ts +2 -0
  44. package/node_modules/@oxygen/shared/dist/mcp-events-enabled.js +14 -0
  45. package/node_modules/@oxygen/shared/dist/operational-telemetry.d.ts +1 -0
  46. package/node_modules/@oxygen/shared/dist/operational-telemetry.js +6 -2
  47. package/node_modules/@oxygen/shared/dist/otlp-log-sink.d.ts +7 -3
  48. package/node_modules/@oxygen/shared/dist/otlp-log-sink.js +22 -1
  49. package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +3 -7
  50. package/node_modules/@oxygen/shared/dist/plan-limits.js +2 -14
  51. package/node_modules/@oxygen/shared/dist/select-options.d.ts +6 -0
  52. package/node_modules/@oxygen/shared/dist/select-options.js +6 -1
  53. package/node_modules/@oxygen/shared/dist/sending-schedule.d.ts +39 -0
  54. package/node_modules/@oxygen/shared/dist/sending-schedule.js +41 -0
  55. package/node_modules/@oxygen/shared/dist/sequence-lifecycle.d.ts +19 -0
  56. package/node_modules/@oxygen/shared/dist/sequence-lifecycle.js +20 -0
  57. package/node_modules/@oxygen/shared/dist/sequence-sender-variables.d.ts +35 -0
  58. package/node_modules/@oxygen/shared/dist/sequence-sender-variables.js +63 -0
  59. package/node_modules/@oxygen/shared/dist/sequences.d.ts +13 -7
  60. package/node_modules/@oxygen/shared/dist/sequences.js +26 -0
  61. package/node_modules/@oxygen/shared/dist/signup-lead-payload.d.ts +2 -0
  62. package/node_modules/@oxygen/shared/dist/signup-lead-payload.js +5 -0
  63. package/node_modules/@oxygen/shared/dist/social-post-metrics-series.js +1 -15
  64. package/node_modules/@oxygen/shared/dist/standard-webhooks.d.ts +51 -0
  65. package/node_modules/@oxygen/shared/dist/standard-webhooks.js +124 -0
  66. package/node_modules/@oxygen/shared/dist/tags.d.ts +1 -1
  67. package/node_modules/@oxygen/shared/dist/tags.js +13 -1
  68. package/node_modules/@oxygen/shared/dist/ugc.d.ts +11 -7
  69. package/node_modules/@oxygen/shared/dist/ugc.js +14 -6
  70. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  71. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  72. package/node_modules/@oxygen/shared/dist/zoned-time.d.ts +39 -0
  73. package/node_modules/@oxygen/shared/dist/zoned-time.js +81 -0
  74. 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
  },
@@ -80,6 +80,11 @@ export type FeatureGateResolution = {
80
80
  * instead of a sending seat (`packages/shared/src/credit-gate.ts`). A wrong
81
81
  * "on" while commitments do not bill every connectable kind would admit
82
82
  * accounts nothing ever charges.
83
+ * - OXYGEN_MCP_EVENTS_ENABLED — (b). Lets workspace events leave OXYGEN as
84
+ * signed POSTs to third-party callback URLs and lets those events start
85
+ * unattended ChatGPT runs (`packages/shared/src/mcp-events-enabled.ts`, ADR
86
+ * 0030 §11). It stays off in production until the per-connection credit
87
+ * ceiling exists; a remote "on" would bypass that decision.
83
88
  *
84
89
  * The list is a guard, not a to-do: none of these is migrated, and
85
90
  * `findFeatureGateRegistryViolations` fails the build's tests if one ever is.
@@ -71,6 +71,11 @@
71
71
  * instead of a sending seat (`packages/shared/src/credit-gate.ts`). A wrong
72
72
  * "on" while commitments do not bill every connectable kind would admit
73
73
  * accounts nothing ever charges.
74
+ * - OXYGEN_MCP_EVENTS_ENABLED — (b). Lets workspace events leave OXYGEN as
75
+ * signed POSTs to third-party callback URLs and lets those events start
76
+ * unattended ChatGPT runs (`packages/shared/src/mcp-events-enabled.ts`, ADR
77
+ * 0030 §11). It stays off in production until the per-connection credit
78
+ * ceiling exists; a remote "on" would bypass that decision.
74
79
  *
75
80
  * The list is a guard, not a to-do: none of these is migrated, and
76
81
  * `findFeatureGateRegistryViolations` fails the build's tests if one ever is.
@@ -87,6 +92,7 @@ export const NEVER_FLAGGABLE = [
87
92
  "OXYGEN_BYOK_DAILY_CAPS",
88
93
  "OXYGEN_REPRICING_2026_09_EFFECTIVE_AT",
89
94
  "OXYGEN_CREDIT_GATE_ENABLED",
95
+ "OXYGEN_MCP_EVENTS_ENABLED",
90
96
  ];
91
97
  /**
92
98
  * Every server-side gate resolved through PostHog.
@@ -3,7 +3,8 @@
3
3
  *
4
4
  * WHAT IT IS. Exactly once in a workspace's life, OXYGEN researches the customer's
5
5
  * OWN company from the domain the creator typed when the workspace was created
6
- * (control-DB `organizations.iconDomain`), then fills the typed company profile
6
+ * (control-DB `organizations.iconDomain`), or from the company LinkedIn page they
7
+ * typed instead when the company has no website, then fills the typed company profile
7
8
  * (`ox_context.company_profile`) and writes one cited wiki page. A founder who
8
9
  * signs up should not face an empty Knowledge Graph and have to type their own
9
10
  * positioning back at us; every grounded AI action downstream (message drafts, AI
@@ -25,8 +26,9 @@
25
26
  * sub-ceiling on the provider (external-money) half. The cap bounds
26
27
  * OXYGEN's own money now, and it is the same number.
27
28
  * 2. AUTHORIZED — it runs only for a workspace whose creator typed the company
28
- * website into the REQUIRED field at workspace creation; that
29
- * entry is recorded as `knowledge_bootstrap_consent` (source
29
+ * website, or its LinkedIn company page when it has no website
30
+ * (Philipp, 2026-09-30), into the REQUIRED field at workspace
31
+ * creation; that entry is recorded as `knowledge_bootstrap_consent` (source
30
32
  * `workspace_creation`) and read FAIL-CLOSED, so a workspace that
31
33
  * predates the field is never researched and a personal-email
32
34
  * signup with no website never reaches the worker. We never
@@ -86,6 +88,17 @@ export declare const KNOWLEDGE_BOOTSTRAP_MAX_PAGES_READ = 5;
86
88
  * it. This bounds latency only; the credit ceilings above remain the money guard.
87
89
  */
88
90
  export declare const KNOWLEDGE_BOOTSTRAP_PROVIDER_TIMEOUT_MS = 45000;
91
+ /**
92
+ * The company-page lookup's budget when a LinkedIn company page, not a domain, names
93
+ * the company. That lookup is then the pass's only company evidence, and there is no
94
+ * website to fall back on. The managed lane measured successful company lookups at
95
+ * p90 37.1 s, p99 62.2 s and max 68.6 s over 3,138 calls to 2026-09-20
96
+ * (packages/providers/src/scraper/managed.ts), so the 45 s leg budget would abandon
97
+ * real answers. 90 s matches that lane's own abort budget. A pass with a website
98
+ * behind the page can overrun the step deadline in the worst case. The abort check
99
+ * between legs then ends it as `step_aborted`, and the credit ceilings are unchanged.
100
+ */
101
+ export declare const KNOWLEDGE_BOOTSTRAP_COMPANY_PAGE_LOOKUP_TIMEOUT_MS = 90000;
89
102
  /** Fleet-wide kill switch, matching OXYGEN_KNOWLEDGE_RETRIEVAL/AUTO_SOURCES_DISABLED. */
90
103
  export declare const KNOWLEDGE_BOOTSTRAP_DISABLED_ENV_VAR = "OXYGEN_KNOWLEDGE_BOOTSTRAP_DISABLED";
91
104
  /**
@@ -140,7 +153,11 @@ export type KnowledgeBootstrapMarker = {
140
153
  run_id: string | null;
141
154
  /** Managed credits actually spent, across every phase. Never exceeds the cap. */
142
155
  credits_used: number;
143
- /** The workspace domain researched (from organizations.iconDomain). */
156
+ /**
157
+ * The workspace domain researched (from organizations.iconDomain). Null (stored
158
+ * as "", the key the execution and onboarding checks compare on) for a company
159
+ * named by its LinkedIn page, which `linkedin_url` then carries.
160
+ */
144
161
  domain: string | null;
145
162
  /** Why a pass was skipped/partial/failed — a short machine-ish token, not prose. */
146
163
  reason: string | null;
@@ -163,7 +180,8 @@ export type KnowledgeBootstrapMarker = {
163
180
  * Canonical company LinkedIn URL the requester supplied, when they supplied one.
164
181
  * The same hint `/setup` stores at KNOWLEDGE_BOOTSTRAP_LINKEDIN_METADATA_KEY, but
165
182
  * carried per-request so a one-off `--linkedin` never mutates the workspace's
166
- * recorded answer.
183
+ * recorded answer. For a company named by its LinkedIn page it is the company
184
+ * itself: the page the automatic pass was claimed for.
167
185
  */
168
186
  linkedin_url?: string | null;
169
187
  };
@@ -180,11 +198,32 @@ export type KnowledgeBootstrapMarker = {
180
198
  * nobody recorded.
181
199
  */
182
200
  export declare const KNOWLEDGE_BOOTSTRAP_CONSENT_METADATA_KEY = "knowledge_bootstrap_consent";
201
+ /**
202
+ * Control-DB `organizations.metadata` key for the consent of a workspace named by its
203
+ * LinkedIn company page because the company has no website (Philipp, 2026-09-30).
204
+ *
205
+ * A key of its own, not a field on the consent above, on purpose. A worker built before
206
+ * it existed reads only that key, and a consent there with a page and no domain would
207
+ * send it to its icon fallback (a domain derived from the creator's email address, so
208
+ * possibly another company) or to a durable `no_company_domain` skip. Under this key the
209
+ * same worker finds no consent at all: a transient `not_consented` that writes nothing,
210
+ * until a worker that reads pages takes the pass. A rolling deploy serves new web code
211
+ * well before the worker roles move, so this is a real window, not a hypothetical one.
212
+ * Writers set exactly one of the two keys.
213
+ */
214
+ export declare const KNOWLEDGE_BOOTSTRAP_COMPANY_PAGE_CONSENT_METADATA_KEY = "knowledge_bootstrap_company_page_consent";
183
215
  /** The recorded consent, exactly as stored under the key above. */
184
216
  export type KnowledgeBootstrapConsent = {
185
217
  /** New workspace creation may authorize bounded creator-profile research. */
186
218
  scope?: "company_and_creator";
187
219
  domain?: string;
220
+ /**
221
+ * The company LinkedIn page the creator named INSTEAD of a website, canonical
222
+ * `/company/` form. Present only on a company-page consent
223
+ * (`KNOWLEDGE_BOOTSTRAP_COMPANY_PAGE_CONSENT_METADATA_KEY`), which never carries a
224
+ * domain; it is then the company the research pass starts from.
225
+ */
226
+ company_linkedin_url?: string;
188
227
  /** Canonical `/in/` profile supplied by this exact creator; never company evidence. */
189
228
  creator_linkedin_url?: string;
190
229
  /** ISO timestamp consent was recorded. Must parse; a marker that does not is not consent. */
@@ -222,11 +261,21 @@ export declare function readKnowledgeBootstrapConsent(metadata: unknown): Knowle
222
261
  * personal profile as if it were the company is exactly the confidently-wrong write
223
262
  * the bootstrap's domain gate exists to prevent.
224
263
  *
225
- * Optional by design. Its absence is normal and costs nothing: the research pass
226
- * resolves the company from the domain and pays for a LinkedIn lookup only when it
227
- * has a page worth looking up.
264
+ * Optional when the workspace named a domain: the research pass then resolves the
265
+ * company from the domain. For a company with no website it is the only company
266
+ * identity the workspace has, and the consent carries the same page as the anchor
267
+ * the research starts from.
228
268
  */
229
269
  export declare const KNOWLEDGE_BOOTSTRAP_LINKEDIN_METADATA_KEY = "company_linkedin_url";
270
+ /**
271
+ * The company LinkedIn page a workspace names instead of a website, as canonical
272
+ * `https://www.linkedin.com/company/<slug>`, or null. A pasted sub-page
273
+ * (`/company/acme/about/`) resolves to its company; `/school/` pages and personal
274
+ * `/in/` profiles do not, because the research provider looks a company up by
275
+ * exactly this form, and researching a person as if they were the company is the
276
+ * confidently-wrong write the authorization exists to prevent.
277
+ */
278
+ export declare function normalizeBootstrapCompanyLinkedinUrl(value: unknown): string | null;
230
279
  /**
231
280
  * How long a `running` marker may sit before the workspace is reported as STUCK.
232
281
  *
@@ -3,7 +3,8 @@
3
3
  *
4
4
  * WHAT IT IS. Exactly once in a workspace's life, OXYGEN researches the customer's
5
5
  * OWN company from the domain the creator typed when the workspace was created
6
- * (control-DB `organizations.iconDomain`), then fills the typed company profile
6
+ * (control-DB `organizations.iconDomain`), or from the company LinkedIn page they
7
+ * typed instead when the company has no website, then fills the typed company profile
7
8
  * (`ox_context.company_profile`) and writes one cited wiki page. A founder who
8
9
  * signs up should not face an empty Knowledge Graph and have to type their own
9
10
  * positioning back at us; every grounded AI action downstream (message drafts, AI
@@ -25,8 +26,9 @@
25
26
  * sub-ceiling on the provider (external-money) half. The cap bounds
26
27
  * OXYGEN's own money now, and it is the same number.
27
28
  * 2. AUTHORIZED — it runs only for a workspace whose creator typed the company
28
- * website into the REQUIRED field at workspace creation; that
29
- * entry is recorded as `knowledge_bootstrap_consent` (source
29
+ * website, or its LinkedIn company page when it has no website
30
+ * (Philipp, 2026-09-30), into the REQUIRED field at workspace
31
+ * creation; that entry is recorded as `knowledge_bootstrap_consent` (source
30
32
  * `workspace_creation`) and read FAIL-CLOSED, so a workspace that
31
33
  * predates the field is never researched and a personal-email
32
34
  * signup with no website never reaches the worker. We never
@@ -52,6 +54,7 @@
52
54
  * Professional person evidence remains actor-scoped; ICP/offers are hypotheses.
53
55
  * The provider sub-cap covers every external research leg, including fallbacks.
54
56
  */
57
+ import { normalizeLinkedinCompanyUrl } from "./dnc-identities.js";
55
58
  import { normalizeLinkedinProfileUrl } from "./linkedin-url.js";
56
59
  /** Hard ceiling on managed credits ONE workspace bootstrap may spend, all phases. */
57
60
  export const KNOWLEDGE_BOOTSTRAP_MAX_CREDITS = 30;
@@ -86,6 +89,17 @@ export const KNOWLEDGE_BOOTSTRAP_MAX_PAGES_READ = 5;
86
89
  * it. This bounds latency only; the credit ceilings above remain the money guard.
87
90
  */
88
91
  export const KNOWLEDGE_BOOTSTRAP_PROVIDER_TIMEOUT_MS = 45_000;
92
+ /**
93
+ * The company-page lookup's budget when a LinkedIn company page, not a domain, names
94
+ * the company. That lookup is then the pass's only company evidence, and there is no
95
+ * website to fall back on. The managed lane measured successful company lookups at
96
+ * p90 37.1 s, p99 62.2 s and max 68.6 s over 3,138 calls to 2026-09-20
97
+ * (packages/providers/src/scraper/managed.ts), so the 45 s leg budget would abandon
98
+ * real answers. 90 s matches that lane's own abort budget. A pass with a website
99
+ * behind the page can overrun the step deadline in the worst case. The abort check
100
+ * between legs then ends it as `step_aborted`, and the credit ceilings are unchanged.
101
+ */
102
+ export const KNOWLEDGE_BOOTSTRAP_COMPANY_PAGE_LOOKUP_TIMEOUT_MS = 90_000;
89
103
  /** Fleet-wide kill switch, matching OXYGEN_KNOWLEDGE_RETRIEVAL/AUTO_SOURCES_DISABLED. */
90
104
  export const KNOWLEDGE_BOOTSTRAP_DISABLED_ENV_VAR = "OXYGEN_KNOWLEDGE_BOOTSTRAP_DISABLED";
91
105
  /**
@@ -149,6 +163,20 @@ export function isKnowledgeBootstrapStatus(value) {
149
163
  * nobody recorded.
150
164
  */
151
165
  export const KNOWLEDGE_BOOTSTRAP_CONSENT_METADATA_KEY = "knowledge_bootstrap_consent";
166
+ /**
167
+ * Control-DB `organizations.metadata` key for the consent of a workspace named by its
168
+ * LinkedIn company page because the company has no website (Philipp, 2026-09-30).
169
+ *
170
+ * A key of its own, not a field on the consent above, on purpose. A worker built before
171
+ * it existed reads only that key, and a consent there with a page and no domain would
172
+ * send it to its icon fallback (a domain derived from the creator's email address, so
173
+ * possibly another company) or to a durable `no_company_domain` skip. Under this key the
174
+ * same worker finds no consent at all: a transient `not_consented` that writes nothing,
175
+ * until a worker that reads pages takes the pass. A rolling deploy serves new web code
176
+ * well before the worker roles move, so this is a real window, not a hypothetical one.
177
+ * Writers set exactly one of the two keys.
178
+ */
179
+ export const KNOWLEDGE_BOOTSTRAP_COMPANY_PAGE_CONSENT_METADATA_KEY = "knowledge_bootstrap_company_page_consent";
152
180
  /**
153
181
  * Read consent out of an organization's control-DB metadata blob, FAIL-CLOSED.
154
182
  *
@@ -163,7 +191,22 @@ export const KNOWLEDGE_BOOTSTRAP_CONSENT_METADATA_KEY = "knowledge_bootstrap_con
163
191
  export function readKnowledgeBootstrapConsent(metadata) {
164
192
  if (!metadata || typeof metadata !== "object" || Array.isArray(metadata))
165
193
  return null;
166
- const raw = metadata[KNOWLEDGE_BOOTSTRAP_CONSENT_METADATA_KEY];
194
+ const record = metadata;
195
+ const domainConsent = readGrant(record[KNOWLEDGE_BOOTSTRAP_CONSENT_METADATA_KEY]);
196
+ if (domainConsent) {
197
+ const domain = domainConsent.record.domain;
198
+ return { ...domainConsent.grant, ...(typeof domain === "string" ? { domain } : {}) };
199
+ }
200
+ // Fail closed here too: a page consent counts only as the creator-scoped grant it is
201
+ // written as, naming a page that still resolves to a company. Anything less is no
202
+ // consent, never a reason to fall back to a domain nobody named.
203
+ const pageConsent = readGrant(record[KNOWLEDGE_BOOTSTRAP_COMPANY_PAGE_CONSENT_METADATA_KEY]);
204
+ const page = normalizeBootstrapCompanyLinkedinUrl(pageConsent?.record.company_linkedin_url);
205
+ if (!pageConsent || pageConsent.grant.scope !== "company_and_creator" || !page)
206
+ return null;
207
+ return { ...pageConsent.grant, company_linkedin_url: page };
208
+ }
209
+ function readGrant(raw) {
167
210
  if (!raw || typeof raw !== "object" || Array.isArray(raw))
168
211
  return null;
169
212
  const record = raw;
@@ -182,12 +225,14 @@ export function readKnowledgeBootstrapConsent(metadata) {
182
225
  ? normalizeLinkedinProfileUrl(record.creator_linkedin_url)?.dispatchUrl ?? null
183
226
  : null;
184
227
  return {
185
- granted_at: grantedAt,
186
- ...(scope ? { scope } : {}),
187
- ...(typeof record.domain === "string" ? { domain: record.domain } : {}),
188
- ...(creatorLinkedinUrl ? { creator_linkedin_url: creatorLinkedinUrl } : {}),
189
- source: typeof record.source === "string" && record.source.length > 0 ? record.source : null,
190
- granted_by_clerk_user_id: grantedBy,
228
+ record,
229
+ grant: {
230
+ granted_at: grantedAt,
231
+ ...(scope ? { scope } : {}),
232
+ ...(creatorLinkedinUrl ? { creator_linkedin_url: creatorLinkedinUrl } : {}),
233
+ source: typeof record.source === "string" && record.source.length > 0 ? record.source : null,
234
+ granted_by_clerk_user_id: grantedBy,
235
+ },
191
236
  };
192
237
  }
193
238
  /**
@@ -203,11 +248,27 @@ export function readKnowledgeBootstrapConsent(metadata) {
203
248
  * personal profile as if it were the company is exactly the confidently-wrong write
204
249
  * the bootstrap's domain gate exists to prevent.
205
250
  *
206
- * Optional by design. Its absence is normal and costs nothing: the research pass
207
- * resolves the company from the domain and pays for a LinkedIn lookup only when it
208
- * has a page worth looking up.
251
+ * Optional when the workspace named a domain: the research pass then resolves the
252
+ * company from the domain. For a company with no website it is the only company
253
+ * identity the workspace has, and the consent carries the same page as the anchor
254
+ * the research starts from.
209
255
  */
210
256
  export const KNOWLEDGE_BOOTSTRAP_LINKEDIN_METADATA_KEY = "company_linkedin_url";
257
+ /**
258
+ * The company LinkedIn page a workspace names instead of a website, as canonical
259
+ * `https://www.linkedin.com/company/<slug>`, or null. A pasted sub-page
260
+ * (`/company/acme/about/`) resolves to its company; `/school/` pages and personal
261
+ * `/in/` profiles do not, because the research provider looks a company up by
262
+ * exactly this form, and researching a person as if they were the company is the
263
+ * confidently-wrong write the authorization exists to prevent.
264
+ */
265
+ export function normalizeBootstrapCompanyLinkedinUrl(value) {
266
+ if (typeof value !== "string")
267
+ return null;
268
+ const companyRoot = value.trim().replace(/(\/company\/[^/?#]+)\/[^?#]*/i, "$1");
269
+ const canonical = normalizeLinkedinCompanyUrl(companyRoot);
270
+ return canonical?.startsWith("https://www.linkedin.com/company/") ? canonical : null;
271
+ }
211
272
  /**
212
273
  * How long a `running` marker may sit before the workspace is reported as STUCK.
213
274
  *
@@ -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;