@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.
- package/README.md +1 -1
- package/dist/admin-primary-providers-render.js +25 -5
- package/dist/column-run-notices.d.ts +16 -0
- package/dist/column-run-notices.js +57 -0
- package/dist/command-manifest.js +10 -1
- package/dist/connectors-commands.d.ts +11 -0
- package/dist/connectors-commands.js +63 -0
- package/dist/help.d.ts +27 -1
- package/dist/help.js +112 -29
- package/dist/http-client.js +8 -0
- package/dist/index.js +362 -130
- package/node_modules/@oxygen/cli-ugc/dist/commands.js +20 -69
- package/node_modules/@oxygen/shared/dist/byok-connect.d.ts +6 -4
- package/node_modules/@oxygen/shared/dist/byok-connect.js +11 -4
- package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +131 -1
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +325 -30
- package/node_modules/@oxygen/shared/dist/collab.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/collab.js +12 -0
- package/node_modules/@oxygen/shared/dist/column-decision.js +7 -3
- package/node_modules/@oxygen/shared/dist/column-output-fields.js +45 -3
- package/node_modules/@oxygen/shared/dist/company-enrichment-fields.d.ts +20 -13
- package/node_modules/@oxygen/shared/dist/company-enrichment-fields.js +29 -13
- package/node_modules/@oxygen/shared/dist/contracts/connectors.d.ts +23 -0
- package/node_modules/@oxygen/shared/dist/contracts/connectors.js +34 -0
- package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +0 -6
- package/node_modules/@oxygen/shared/dist/contracts/operations.js +8 -1
- package/node_modules/@oxygen/shared/dist/contracts/sender-limits.js +1 -1
- package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +2 -2
- package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +2 -2
- package/node_modules/@oxygen/shared/dist/credit-guidance.js +12 -1
- package/node_modules/@oxygen/shared/dist/enrichment-intents.d.ts +37 -0
- package/node_modules/@oxygen/shared/dist/enrichment-intents.js +79 -44
- package/node_modules/@oxygen/shared/dist/feature-gates.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/feature-gates.js +6 -0
- package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.d.ts +57 -8
- package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.js +74 -13
- package/node_modules/@oxygen/shared/dist/linkedin-url.d.ts +7 -0
- package/node_modules/@oxygen/shared/dist/linkedin-url.js +9 -0
- package/node_modules/@oxygen/shared/dist/log.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/log.js +15 -0
- package/node_modules/@oxygen/shared/dist/mcp-event-contract.d.ts +597 -0
- package/node_modules/@oxygen/shared/dist/mcp-event-contract.js +713 -0
- package/node_modules/@oxygen/shared/dist/mcp-events-enabled.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/mcp-events-enabled.js +14 -0
- package/node_modules/@oxygen/shared/dist/operational-telemetry.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/operational-telemetry.js +6 -2
- package/node_modules/@oxygen/shared/dist/otlp-log-sink.d.ts +7 -3
- package/node_modules/@oxygen/shared/dist/otlp-log-sink.js +22 -1
- package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +3 -7
- package/node_modules/@oxygen/shared/dist/plan-limits.js +2 -14
- package/node_modules/@oxygen/shared/dist/select-options.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/select-options.js +6 -1
- package/node_modules/@oxygen/shared/dist/sending-schedule.d.ts +39 -0
- package/node_modules/@oxygen/shared/dist/sending-schedule.js +41 -0
- package/node_modules/@oxygen/shared/dist/sequence-lifecycle.d.ts +19 -0
- package/node_modules/@oxygen/shared/dist/sequence-lifecycle.js +20 -0
- package/node_modules/@oxygen/shared/dist/sequence-sender-variables.d.ts +35 -0
- package/node_modules/@oxygen/shared/dist/sequence-sender-variables.js +63 -0
- package/node_modules/@oxygen/shared/dist/sequences.d.ts +13 -7
- package/node_modules/@oxygen/shared/dist/sequences.js +26 -0
- package/node_modules/@oxygen/shared/dist/signup-lead-payload.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/signup-lead-payload.js +5 -0
- package/node_modules/@oxygen/shared/dist/social-post-metrics-series.js +1 -15
- package/node_modules/@oxygen/shared/dist/standard-webhooks.d.ts +51 -0
- package/node_modules/@oxygen/shared/dist/standard-webhooks.js +124 -0
- package/node_modules/@oxygen/shared/dist/tags.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/tags.js +13 -1
- package/node_modules/@oxygen/shared/dist/ugc.d.ts +11 -7
- package/node_modules/@oxygen/shared/dist/ugc.js +14 -6
- package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
- package/node_modules/@oxygen/shared/dist/zoned-time.d.ts +39 -0
- package/node_modules/@oxygen/shared/dist/zoned-time.js +81 -0
- 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("
|
|
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("
|
|
56
|
-
f("
|
|
57
|
-
f("
|
|
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
|
-
|
|
77
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
115
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
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: {
|
|
186
|
-
status: "
|
|
187
|
-
statusReason: "
|
|
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
|
|
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
|
-
|
|
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
|
|
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("
|
|
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)
|
|
279
|
-
|
|
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
|
|
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
|
-
|
|
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, "
|
|
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
|
-
|
|
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("
|
|
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`),
|
|
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
|
|
29
|
-
*
|
|
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
|
-
/**
|
|
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
|
|
226
|
-
*
|
|
227
|
-
* has
|
|
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`),
|
|
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
|
|
29
|
-
*
|
|
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
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
|
207
|
-
*
|
|
208
|
-
* has
|
|
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;
|