publishport-opencli 1.8.5-pp.40 → 1.8.5-pp.42

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/cli-manifest.json CHANGED
@@ -23652,7 +23652,7 @@
23652
23652
  {
23653
23653
  "site": "linkedin",
23654
23654
  "name": "job-detail",
23655
- "description": "Read one LinkedIn job page with description, apply URL, workplace type, applicants, and company metadata",
23655
+ "description": "Read one LinkedIn job via the Voyager API: description, apply URL, workplace type, applicants, and company metadata",
23656
23656
  "access": "read",
23657
23657
  "domain": "www.linkedin.com",
23658
23658
  "strategy": "cookie",
@@ -23742,7 +23742,7 @@
23742
23742
  {
23743
23743
  "site": "linkedin",
23744
23744
  "name": "people-search",
23745
- "description": "Search standard LinkedIn (not Sales Navigator) for people by keyword. Each invocation consumes against LinkedIn's monthly Commercial Use Limit on people search; throttle accordingly.",
23745
+ "description": "Search standard LinkedIn (not Sales Navigator) for people by keyword via the Voyager search API. Each invocation consumes against LinkedIn's monthly Commercial Use Limit on people search; throttle accordingly.",
23746
23746
  "access": "read",
23747
23747
  "domain": "www.linkedin.com",
23748
23748
  "strategy": "cookie",
@@ -23808,7 +23808,7 @@
23808
23808
  "name": "current-company-id",
23809
23809
  "type": "string",
23810
23810
  "required": false,
23811
- "help": "Comma-separated LinkedIn company IDs for CURRENT COMPANY filter (e.g. \"1441\" for Google). To find an ID: visit the company's LinkedIn URL — `/company/<slug>/` — then open Network tools, or click the filter chip on a search results page and read the URL's `currentCompany` param. A full URN like `urn:li:fs_company:1441` is also accepted."
23811
+ "help": "Comma-separated LinkedIn company IDs for CURRENT COMPANY filter (e.g. \"1441\" for Google). A full URN like `urn:li:fs_company:1441` is also accepted."
23812
23812
  },
23813
23813
  {
23814
23814
  "name": "past-company-id",
@@ -23820,13 +23820,13 @@
23820
23820
  "name": "industry-id",
23821
23821
  "type": "string",
23822
23822
  "required": false,
23823
- "help": "Comma-separated LinkedIn industry IDs (e.g. \"4\" for Computer Software). Apply the filter once via the LinkedIn UI and read the URL's `industry` param to discover IDs."
23823
+ "help": "Comma-separated LinkedIn industry IDs (e.g. \"4\" for Computer Software)."
23824
23824
  },
23825
23825
  {
23826
23826
  "name": "location-id",
23827
23827
  "type": "string",
23828
23828
  "required": false,
23829
- "help": "Comma-separated LinkedIn location IDs (geoUrn numeric tail; e.g. \"105214831\" for Bengaluru area). Apply the location filter once via UI and read the URL's `geoUrn` param."
23829
+ "help": "Comma-separated LinkedIn location IDs (geoUrn numeric tail; e.g. \"105214831\" for Bengaluru area)."
23830
23830
  },
23831
23831
  {
23832
23832
  "name": "school-id",
@@ -23935,7 +23935,7 @@
23935
23935
  {
23936
23936
  "site": "linkedin",
23937
23937
  "name": "profile-analytics",
23938
- "description": "Read visible LinkedIn profile dashboard metrics such as profile views, post impressions, and search appearances",
23938
+ "description": "Read your own LinkedIn analytics via the Voyager API: profile views, search appearances, followers, and connections (self only)",
23939
23939
  "access": "read",
23940
23940
  "domain": "www.linkedin.com",
23941
23941
  "strategy": "cookie",
@@ -23945,7 +23945,7 @@
23945
23945
  "name": "profile-url",
23946
23946
  "type": "string",
23947
23947
  "required": false,
23948
- "help": "LinkedIn /in/<handle>/ profile URL. Defaults to /in/me/."
23948
+ "help": "Ignored beyond validation analytics are only visible for the signed-in account."
23949
23949
  }
23950
23950
  ],
23951
23951
  "columns": [
@@ -23954,8 +23954,7 @@
23954
23954
  "post_impressions",
23955
23955
  "search_appearances",
23956
23956
  "followers",
23957
- "connections",
23958
- "raw_analytics"
23957
+ "connections"
23959
23958
  ],
23960
23959
  "type": "js",
23961
23960
  "modulePath": "linkedin/profile-analytics.js",
@@ -23965,7 +23964,7 @@
23965
23964
  {
23966
23965
  "site": "linkedin",
23967
23966
  "name": "profile-experience",
23968
- "description": "Read visible LinkedIn profile experience entries with titles, dates, locations, skills, media, and URLs",
23967
+ "description": "Read LinkedIn profile experience entries via the Voyager API: titles, companies, dates, locations, and descriptions",
23969
23968
  "access": "read",
23970
23969
  "domain": "www.linkedin.com",
23971
23970
  "strategy": "cookie",
@@ -24006,7 +24005,7 @@
24006
24005
  {
24007
24006
  "site": "linkedin",
24008
24007
  "name": "profile-projects",
24009
- "description": "Read visible LinkedIn profile projects with descriptions, dates, skills, media, and URLs",
24008
+ "description": "Read LinkedIn profile projects via the Voyager API: titles, dates, and descriptions",
24010
24009
  "access": "read",
24011
24010
  "domain": "www.linkedin.com",
24012
24011
  "strategy": "cookie",
@@ -24039,7 +24038,7 @@
24039
24038
  {
24040
24039
  "site": "linkedin",
24041
24040
  "name": "profile-read",
24042
- "description": "Read visible LinkedIn profile sections: headline, About, experience, education, services, and featured sections",
24041
+ "description": "Read LinkedIn profile sections via the Voyager API: name, headline, location, About, experience, and education",
24043
24042
  "access": "read",
24044
24043
  "domain": "www.linkedin.com",
24045
24044
  "strategy": "cookie",
@@ -24535,7 +24534,7 @@
24535
24534
  {
24536
24535
  "site": "linkedin",
24537
24536
  "name": "timeline",
24538
- "description": "Read LinkedIn home timeline posts",
24537
+ "description": "Read LinkedIn home timeline posts via the Voyager feed API",
24539
24538
  "access": "read",
24540
24539
  "domain": "www.linkedin.com",
24541
24540
  "strategy": "cookie",
@@ -24546,7 +24545,7 @@
24546
24545
  "type": "int",
24547
24546
  "default": 20,
24548
24547
  "required": false,
24549
- "help": "Number of posts to return (max 100)"
24548
+ "help": "Number of posts to return (max 50)"
24550
24549
  }
24551
24550
  ],
24552
24551
  "columns": [
@@ -1,113 +1,40 @@
1
- import{cli as y,Strategy as R}from"@jackwener/opencli/registry";import{ArgumentError as _,CommandExecutionError as W}from"@jackwener/opencli/errors";import{assertLinkedInAuthenticated as z,assertSafeLinkedinUrl as $,normalizeHttpUrl as T,normalizeWhitespace as K,unwrapEvaluateResult as f}from"./shared.js";function j(q){const B=$(q,"job-url"),C=new URL(B),G=C.pathname.match(/^\/jobs\/view\/(\d+)/)||C.search.match(/[?&]currentJobId=(\d+)/);if(!G)throw new _("job-url must be a https://www.linkedin.com/jobs/view/<id> URL");return`https://www.linkedin.com/jobs/search/?currentJobId=${G[1]}`}function S(q){const B=$(q,"job-url"),C=new URL(B),G=C.pathname.match(/^\/jobs\/view\/(?:[^/]+-)?(\d+)/)||C.search.match(/[?&]currentJobId=(\d+)/);if(!G)throw new _("job-url must be a https://www.linkedin.com/jobs/view/<id> URL");return`https://www.linkedin.com/jobs/view/${G[1]}`}function A(q){if(!q)return"";try{const B=new URL(q);if(B.pathname==="/redir/redirect/")return T(B.searchParams.get("url")||"")}catch{}return T(q)}function E(q){return K(String(q||"").replace(/<script[\s\S]*?<\/script>/gi," ").replace(/<style[\s\S]*?<\/style>/gi," ").replace(/<br\s*\/?>/gi,`
2
- `).replace(/<\/(?:p|li|div|section|article|h[1-6])>/gi,`
3
- `).replace(/<[^>]+>/g," ").replace(/&nbsp;/gi," ").replace(/&amp;/gi,"&").replace(/&lt;/gi,"<").replace(/&gt;/gi,">").replace(/&quot;/gi,'"').replace(/&#39;/gi,"'"))}function F(q){const B=K(q),C=B.search(/\b(?:We're looking|We are looking|About the role|About The Role|Job description|Job Description|Responsibilities|Qualifications)\b/);if(C>0&&/use ai|sign in|tailor my resume|email or phone/i.test(B.slice(0,C)))return K(B.slice(C));return B}function I(q){const B=String(q||"");for(const J of B.matchAll(/<script[^>]+type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi))try{const V=JSON.parse(J[1]||"{}"),P=Array.isArray(V)?V:[V];for(const Y of P){const Z=F(E(Y?.description||Y?.jobDescription||""));if(Z.length>40)return Z}}catch{}const C=E(B),G=C.search(/\bReport this job\b/i),N=G>=0?G+C.match(/\bReport this job\b/i)[0].length:C.search(/\b(?:We're looking|About the role|Job description|Responsibilities)\b/i);if(N<0)return"";const Q=C.slice(N),O=Q.search(/\b(?:Show more|Show less|Seniority level|Employment type|Job function|Industries|Similar jobs|People also viewed)\b/i),X=F(Q.slice(0,O>0?O:5000));return X.length>40?X:""}async function H(q){try{const B=await fetch(q,{headers:{"User-Agent":"Mozilla/5.0",Accept:"text/html,application/xhtml+xml"}});if(!B.ok)return"";return I(await B.text())}catch{return""}}function L(){return String.raw`(() => {
4
- const clean = (s) => String(s || '').replace(/[\u00a0\u202f]+/g, ' ').replace(/\s+/g, ' ').trim();
5
- const readRenderedDescription = () => {
6
- const readJsonLd = () => {
7
- const scripts = Array.from(document.querySelectorAll('script[type="application/ld+json"]'));
8
- for (const script of scripts) {
9
- let payload;
10
- try { payload = JSON.parse(script.textContent || '{}'); } catch { continue; }
11
- const items = Array.isArray(payload) ? payload : [payload];
12
- for (const item of items) {
13
- const value = clean(item?.description || item?.jobDescription || '');
14
- if (value.length > 40) return value.replace(/<[^>]+>/g, ' ');
15
- }
16
- }
17
- return '';
18
- };
19
- const jsonLd = readJsonLd();
20
- if (jsonLd) return clean(jsonLd);
21
- const expanders = Array.from(document.querySelectorAll('button, a'))
22
- .filter((el) => /\b(show more|see more|more)\b/i.test(clean(el.innerText || el.textContent || el.getAttribute('aria-label') || '')));
23
- for (const expander of expanders.slice(0, 3)) {
24
- try { expander.click(); } catch {}
25
- }
26
- const aboutHeading = Array.from(document.querySelectorAll('h1,h2,h3,h4'))
27
- .find((el) => /^about the job$/i.test(clean(el.innerText || el.textContent || '')));
28
- const aboutRoot = aboutHeading?.closest('section, article, div');
29
- if (aboutRoot) {
30
- const value = clean((aboutRoot.innerText || aboutRoot.textContent || '').replace(/^About the job\s*/i, ''));
31
- if (value && !/^show more$/i.test(value)) return value;
32
- }
33
- const candidates = Array.from(document.querySelectorAll(
34
- '.jobs-description-content__text, .jobs-box__html-content, .jobs-description__content, [class*="jobs-description"], [class*="description-content"]'
35
- ));
36
- for (const candidate of candidates) {
37
- const value = clean(candidate.innerText || candidate.textContent || '');
38
- if (value && value.length > 40 && !/^show more$/i.test(value)) return value.replace(/^About the job\s*/i, '');
39
- }
40
- const lines = (document.body?.innerText || '').split(/\n+/).map(clean).filter(Boolean);
41
- const reportIndex = lines.findIndex((line) => /^report this job$/i.test(line));
42
- const startIndex = reportIndex >= 0 ? reportIndex + 1 : lines.findIndex((line) => /^we'?re looking|^about the role|^job description|^responsibilities/i.test(line));
43
- if (startIndex >= 0) {
44
- const endIndex = lines.findIndex((line, index) => index > startIndex && /^(show more|show less|seniority level|employment type|job function|industries|similar jobs|people also viewed)$/i.test(line));
45
- const chunk = lines.slice(startIndex, endIndex > startIndex ? endIndex : startIndex + 80).join(' ');
46
- if (chunk.length > 40) return chunk;
47
- }
48
- return '';
49
- };
50
- const parseInlineJobData = () => {
51
- const codes = Array.from(document.querySelectorAll('code[id^="bpr-guid-"]'));
52
- for (const code of codes) {
53
- let payload;
54
- try { payload = JSON.parse(code.textContent || '{}'); } catch { continue; }
55
- const included = Array.isArray(payload.included) ? payload.included : [];
56
- const topCard = included.find((item) => item && (item.jobPostingTitle || item.primaryDescription || item.tertiaryDescription));
57
- if (!topCard) continue;
58
- const apply = included.find((item) => item && item.companyApplyUrl);
59
- const workplace = included.find((item) => item && (item.workplaceTypeEnum || item.localizedName));
60
- const company = included.find((item) => item && item.name && /company/i.test(String(item.entityUrn || item.$type || '')));
61
- return {
62
- url: location.href,
63
- title: clean(topCard.jobPostingTitle || topCard.title?.text || topCard.title?.accessibilityText || ''),
64
- company: clean(topCard.primaryDescription?.text || company?.name || ''),
65
- company_url: clean(topCard.primaryDescription?.attributesV2?.[0]?.detailData?.hyperlink || topCard.logo?.actionTarget || ''),
66
- location: clean(topCard.navigationBarSubtitle || topCard.secondaryDescription?.text || ''),
67
- workplace_type: clean(workplace?.localizedName || workplace?.workplaceTypeEnum || ''),
68
- job_type: clean((topCard.jobInsightsV2ResolutionResults || []).flatMap((x) => x?.jobInsightViewModel?.description || []).map((x) => x?.text?.text || '').find((x) => /full-time|part-time|contract|internship/i.test(x)) || ''),
69
- applicants: clean((topCard.tertiaryDescription?.text || '').match(/Over\s+\d+|\d[\d,]*\s+people clicked apply|\d[\d,]*\s+applicants?/i)?.[0] || ''),
70
- listed: clean((topCard.tertiaryDescription?.text || '').match(/\d+\s+(?:hour|hours|day|days|week|weeks|month|months)\s+ago/i)?.[0] || ''),
71
- apply_url: clean(apply?.companyApplyUrl || ''),
72
- description: '',
73
- };
74
- }
75
- return null;
76
- };
77
- const inline = parseInlineJobData();
78
- const renderedDescription = readRenderedDescription();
79
- if (inline && inline.title) return { ...inline, description: clean(inline.description || renderedDescription) };
80
- const text = document.body ? document.body.innerText || '' : '';
81
- const lines = text.split(/\n+/).map(clean).filter(Boolean);
82
- const h1 = clean(document.querySelector('h1')?.innerText || document.querySelector('h1')?.textContent || document.querySelector('.job-details-jobs-unified-top-card__job-title, [class*="job-title"]')?.textContent || '');
83
- const companyLink = document.querySelector('a[href*="/company/"]');
84
- const company = clean(companyLink?.innerText || companyLink?.textContent || '');
85
- const company_url = companyLink?.href ? new URL(companyLink.href, location.origin).toString().replace(/[?#].*$/, '') : '';
86
- const description = renderedDescription;
87
- const applyLink = Array.from(document.querySelectorAll('a[href], button')).find((el) => {
88
- const label = clean(el.innerText || el.textContent || el.getAttribute('aria-label') || '');
89
- return /\b(apply|easy apply)\b/i.test(label);
90
- });
91
- const apply_url = applyLink?.href ? new URL(applyLink.href, location.origin).toString() : '';
92
- const fullText = lines.join(' ');
93
- const workplaceMatch = fullText.match(/\b(Remote|Hybrid|On-site|Onsite)\b/i);
94
- const jobTypeMatch = fullText.match(/\b(Full-time|Part-time|Contract|Temporary|Internship|Volunteer)\b/i);
95
- const applicantsMatch = fullText.match(/(\d[\d,]*)\s+applicants?/i);
96
- const listedMatch = fullText.match(/(?:Reposted|Posted|Listed)\s+(\d+\s+(?:hour|hours|day|days|week|weeks|month|months)\s+ago)/i);
97
- const locationLine = lines.find((line) => /\b(Remote|Hybrid|On-site|Onsite)\b/i.test(line) && line.length < 180)
98
- || lines.find((line) => /,\s*[A-Z][A-Za-z\s]+/.test(line) && line.length < 120)
99
- || '';
1
+ import{cli as V,Strategy as X}from"@jackwener/opencli/registry";import{AuthRequiredError as Y,ArgumentError as Z,CommandExecutionError as P,EmptyResultError as $}from"@jackwener/opencli/errors";import{assertSafeLinkedinUrl as f,normalizeWhitespace as G,unwrapEvaluateResult as q}from"./shared.js";const B="com.linkedin.voyager.deco.jobs.web.shared.WebFullJobPosting-65";function Q(H){const M=f(H,"job-url"),K=new URL(M),F=K.pathname.match(/^\/jobs\/view\/(?:[^/]+-)?(\d+)/)||K.search.match(/[?&]currentJobId=(\d+)/);if(!F)throw new Z("job-url must be a https://www.linkedin.com/jobs/view/<id> URL");return F[1]}function D(H,M){return`(async () => {
2
+ const norm = (s) => String(s == null ? '' : s).replace(/[ ]+/g, ' ').replace(/\\s+/g, ' ').trim();
3
+ const csrf = ((document.cookie.split('; ').find((c) => c.startsWith('JSESSIONID=')) || '').split('=')[1] || '').replace(/^"|"$/g, '');
4
+ if (!csrf) return { ok: false, auth: true };
5
+ const res = await fetch('/voyager/api/jobs/jobPostings/' + ${JSON.stringify(H)} + '?decorationId=' + ${JSON.stringify(M)}, { credentials: 'include', headers: {
6
+ 'csrf-token': csrf,
7
+ 'accept': 'application/vnd.linkedin.normalized+json+2.1',
8
+ 'x-restli-protocol-version': '2.0.0',
9
+ 'x-li-lang': 'en_US',
10
+ } });
11
+ if (res.status === 401 || res.status === 403) return { ok: false, auth: true, status: res.status };
12
+ if (!res.ok) return { ok: false, status: res.status };
13
+ const json = await res.json();
14
+ const d = json.data || {};
15
+ const byUrn = {};
16
+ for (const e of (json.included || [])) { if (e && e.entityUrn) byUrn[e.entityUrn] = e; }
17
+ const textOf = (o) => (o && typeof o === 'object') ? norm(o.text) : norm(o);
18
+ // workplace type:resolutionResults 是 {urn:urn} 映射,值 urn → WorkplaceType.localizedName
19
+ let workplace = '';
20
+ const wpVals = Object.values(d.workplaceTypesResolutionResults || {});
21
+ if (wpVals.length && byUrn[wpVals[0]]) workplace = norm(byUrn[wpVals[0]].localizedName);
22
+ const emp = d['*employmentStatusResolutionResult'] ? byUrn[d['*employmentStatusResolutionResult']] : null;
23
+ const jobType = emp ? norm(emp.localizedName || emp.name) : '';
24
+ const company = Object.values(byUrn).find((e) => String(e.$type || '').endsWith('organization.Company')) || {};
25
+ const apply = d.applyMethod || {};
26
+ const applyUrl = norm(apply.companyApplyUrl || apply.easyApplyUrl || (apply.offsiteApply && apply.offsiteApply.companyApplyUrl));
100
27
  return {
101
- url: location.href,
102
- title: h1,
103
- company,
104
- company_url,
105
- location: locationLine,
106
- workplace_type: workplaceMatch ? workplaceMatch[1] : '',
107
- job_type: jobTypeMatch ? jobTypeMatch[1] : '',
108
- applicants: applicantsMatch ? applicantsMatch[1] : '',
109
- listed: listedMatch ? listedMatch[1] : '',
110
- apply_url,
111
- description,
28
+ ok: true,
29
+ title: textOf(d.title) || norm(d.title),
30
+ company: norm(company.name),
31
+ company_url: norm(company.url),
32
+ location: norm(d.formattedLocation),
33
+ workplace_type: workplace,
34
+ job_type: jobType,
35
+ applicants: Number(d.applies) || 0,
36
+ listed_at: Number(d.listedAt) || 0,
37
+ apply_url: applyUrl,
38
+ description: textOf(d.description),
112
39
  };
113
- })()`}function M(q){if(!q||typeof q!=="object")throw new W("LinkedIn job detail returned malformed extraction payload");const B=K(q.title);if(!B)throw new W("LinkedIn job detail could not find a job title");return{title:B,company:K(q.company),location:K(q.location),workplace_type:K(q.workplace_type),job_type:K(q.job_type),applicants:K(q.applicants),listed:K(q.listed),apply_url:A(K(q.apply_url)),company_url:T(q.company_url),url:T(q.url),description:K(q.description)}}y({site:"linkedin",name:"job-detail",access:"read",description:"Read one LinkedIn job page with description, apply URL, workplace type, applicants, and company metadata",domain:"www.linkedin.com",strategy:R.COOKIE,browser:!0,args:[{name:"job-url",type:"string",required:!0,positional:!0,help:"Exact LinkedIn job URL, e.g. https://www.linkedin.com/jobs/view/123/"}],columns:["title","company","location","workplace_type","job_type","applicants","listed","apply_url","company_url","url","description"],func:async(q,B)=>{if(!q)throw new W("Browser session required for linkedin job-detail");const C=S(B["job-url"]);await q.goto(C);await q.wait(4);let G=f(await q.evaluate(L())),N="";if(!K(G?.description)){N=await H(C);if(N)G={...G,description:N}}if(!K(G?.title)||!K(G?.description)){const Q=j(B["job-url"]);await q.goto(Q);await q.wait(4);await z(q,"LinkedIn job-detail");const O=f(await q.evaluate(L()));G={...O,description:K(O?.description)||K(G?.description)||N}}return[M(G)]}});export const __test__={normalizeJobUrl:j,normalizePublicJobUrl:S,decodeLinkedinRedirect:A,extractPublicDescriptionFromHtml:I,normalizeDetail:M};
40
+ })()`}V({site:"linkedin",name:"job-detail",access:"read",description:"Read one LinkedIn job via the Voyager API: description, apply URL, workplace type, applicants, and company metadata",domain:"www.linkedin.com",strategy:X.COOKIE,browser:!0,args:[{name:"job-url",type:"string",required:!0,positional:!0,help:"Exact LinkedIn job URL, e.g. https://www.linkedin.com/jobs/view/123/"}],columns:["title","company","location","workplace_type","job_type","applicants","listed","apply_url","company_url","url","description"],func:async(H,M)=>{if(!H)throw new P("Browser session required for linkedin job-detail");const K=Q(M["job-url"]);await H.goto("https://www.linkedin.com/jobs/");await H.wait(2);const F=q(await H.evaluate(D(K,B)));if(F?.auth)throw new Y("linkedin.com","LinkedIn job-detail requires an active signed-in browser session");if(!F?.ok)throw new P(`LinkedIn job-detail: jobPostings API returned HTTP ${F?.status||"?"}`);if(!G(F.title))throw new $("linkedin job-detail",`No job posting found for id ${K} (it may be closed or removed).`);const T=F.listed_at?new Date(F.listed_at).toISOString().slice(0,10):"";return[{title:G(F.title),company:G(F.company),location:G(F.location),workplace_type:G(F.workplace_type),job_type:G(F.job_type),applicants:F.applicants||0,listed:T,apply_url:G(F.apply_url),company_url:G(F.company_url),url:`https://www.linkedin.com/jobs/view/${K}`,description:G(F.description)}]}});export const __test__={extractJobId:Q};
@@ -1,81 +1 @@
1
- import{cli as I,Strategy as C}from"@jackwener/opencli/registry";import{ArgumentError as j,AuthRequiredError as D,CommandExecutionError as Z,EmptyResultError as w}from"@jackwener/opencli/errors";const T="www.linkedin.com",k="https://www.linkedin.com/search/results/people/",b=10;function H(F){return String(F??"").replace(/[ ]/g," ").replace(/\s+/g," ").trim()}function y(F,G,J=G){const Q=H(F[G]);if(!Q)throw new j(`${J} is required`);return Q}function U(F){if(F===void 0||F===null||F==="")return 5;const G=Number(F);if(!Number.isInteger(G)||G<1||G>b)throw new j(`--limit must be an integer between 1 and ${b}`);return G}function A(F){if(F&&typeof F==="object"&&"data"in F&&"session"in F)return F.data;return F}const E={"1":"F","2":"S","3":"O"},c=new Set(["proBono","boardMember"]);function X(F){if(!F)return[];return String(F).split(",").map((G)=>G.trim()).filter(Boolean)}function M(F){if(!F)return[];const G=X(F),J=[];for(const Q of G){const V=E[Q];if(!V)throw new j(`--network values must be one or more of 1,2,3 (got "${Q}")`);if(!J.includes(V))J.push(V)}return J}function R(F){if(!F)return[];const G=X(F).map((J)=>J.toLowerCase());for(const J of G)if(!/^[a-z]{2}$/.test(J))throw new j(`--profile-language values must be ISO 639-1 2-letter codes (got "${J}")`);return G}function O(F){if(!F)return[];const G=X(F);for(const J of G)if(!c.has(J))throw new j(`--open-to values must be one or more of proBono,boardMember (got "${J}")`);return G}function q(F,G){if(!F)return[];const J=X(F).map((Q)=>{return Q.replace(/^urn:li:[a-z_]+:/,"")});for(const Q of J)if(!/^\d+$/.test(Q))throw new j(`${G} values must be numeric IDs (got "${Q}")`);return J}function _(F){if(typeof F==="string")F={keywords:F};const G=["keywords="+encodeURIComponent(F.keywords)],J=(V,B)=>{const f=H(B);if(!f)return;G.push(V+"="+encodeURIComponent(JSON.stringify(f)))},Q=(V,B)=>{if(!Array.isArray(B)||B.length===0)return;G.push(V+"="+encodeURIComponent(JSON.stringify(B)))};J("firstName",F.firstName);J("lastName",F.lastName);J("title",F.title);J("schoolFreetext",F.schoolKeyword);Q("network",F.network);Q("profileLanguage",F.profileLanguage);Q("serviceCategory",F.openTo);Q("currentCompany",F.currentCompanyIds);Q("pastCompany",F.pastCompanyIds);Q("industry",F.industryIds);Q("geoUrn",F.geoIds);Q("schoolFilter",F.schoolIds);return k+"?"+G.join("&")}function z(F){const G=H(F).toLowerCase();if(!G)return!1;return/linkedin\.com\/(?:login|checkpoint|authwall|uas)/i.test(G)||/\b(sign in|log in|join linkedin|captcha|verification required)\b/i.test(G)||/(请登录|登录领英|安全验证)/.test(G)}function S(F){const G=H(F);if(!G)return"";try{const J=new URL(G),Q=J.hostname.toLowerCase();if(J.protocol!=="https:"||J.username||J.password||J.port)return"";if(Q!=="linkedin.com"&&Q!=="www.linkedin.com")return"";const V=J.pathname.match(/^\/in\/([^/?#]+)\/?$/);if(!V||!V[1])return"";return`https://www.linkedin.com/in/${V[1]}/`}catch{return""}}function W(F){if(!Array.isArray(F))throw new Z("LinkedIn people search returned malformed extraction payload: missing rows array");return F.map((G,J)=>{if(!G||typeof G!=="object")throw new Z(`LinkedIn people search returned malformed row at index ${J}`);const Q=H(G.name),V=S(G.profile_url);if(!Q||!V)throw new Z(`LinkedIn people search returned row without stable profile identity at index ${J}`);return{name:Q,headline:H(G.headline),location:H(G.location),profile_url:V}})}function K(F,G){const J=Number(F);if(!Number.isInteger(J)||J<0)throw new Z(`LinkedIn people search returned malformed extraction payload: invalid ${G}`);return J}function h(){return String.raw`(() => {
2
- if (!/search\/results\/people/.test(window.location.href)) {
3
- return { error: 'not on people search page', url: window.location.href };
4
- }
5
- const main = document.querySelector('main') || document.body;
6
- const normalize = (s) => String(s || '').replace(/[\s\u00a0\u202f]+/g, ' ').trim();
7
- const skip = (l) => !l
8
- || /^Status is/.test(l)
9
- || /^(Message|Connect|Follow|View profile|Pending|Remove)$/i.test(l)
10
- || /^[•·]\s*(?:1st|2nd|3rd\+?|degree)/i.test(l)
11
- || /^[•·]/.test(l)
12
- || l.includes('mutual connection')
13
- || l.includes('shared connection')
14
- || /^Summary:/i.test(l)
15
- || /^About this profile/i.test(l);
16
-
17
- const anchors = Array.from(main.querySelectorAll('a[href*="/in/"]'));
18
- const personEntries = [];
19
- const seenHandles = new Set();
20
- for (const a of anchors) {
21
- const m = (a.getAttribute('href') || '').match(/\/in\/([^/?#]+)/);
22
- if (!m || !m[1]) continue;
23
- const profileHandle = m[1];
24
- if (seenHandles.has(profileHandle)) continue;
25
- const aria = a.querySelector('span[aria-hidden="true"]');
26
- let name = normalize(aria ? aria.textContent : a.textContent);
27
- name = name.replace(/^Status is (online|offline)\.?\s*/i, '')
28
- .replace(/'?s profile$/i, '')
29
- .replace(/\s*[•·].*$/, '').trim();
30
- if (!name) continue;
31
- seenHandles.add(profileHandle);
32
- personEntries.push({ profileHandle, displayName: name });
33
- }
34
-
35
- const lines = (main.innerText || '').split(/\n+/).map(normalize).filter(Boolean);
36
-
37
- // skip() rejects mutual-connection lines, so candidates that only
38
- // appear as mutual-connection links inside another card's row
39
- // never resolve a name index and get filtered out below.
40
- const nameToIndex = new Map();
41
- for (const { displayName } of personEntries) {
42
- if (nameToIndex.has(displayName)) continue;
43
- const match = lines.findIndex((l) =>
44
- !skip(l) && (
45
- l === displayName
46
- || l.startsWith(displayName + ' ')
47
- || l.startsWith(displayName + ',')
48
- || l.startsWith(displayName + "'")
49
- )
50
- );
51
- if (match >= 0) nameToIndex.set(displayName, match);
52
- }
53
-
54
- const resolved = personEntries.filter((p) => nameToIndex.has(p.displayName));
55
- const rows = [];
56
- for (let i = 0; i < resolved.length; i++) {
57
- const { profileHandle, displayName } = resolved[i];
58
- const startIdx = nameToIndex.get(displayName);
59
- let stopIdx = lines.length;
60
- for (let j = i + 1; j < resolved.length; j++) {
61
- const otherStart = nameToIndex.get(resolved[j].displayName);
62
- if (otherStart != null && otherStart > startIdx) {
63
- stopIdx = otherStart;
64
- break;
65
- }
66
- }
67
- const slice = lines.slice(startIdx + 1, stopIdx).filter((l) => l !== displayName && !skip(l));
68
- rows.push({
69
- name: displayName,
70
- headline: slice[0] || '',
71
- location: slice[1] || '',
72
- profile_url: 'https://www.linkedin.com/in/' + profileHandle + '/',
73
- });
74
- }
75
- return {
76
- rows,
77
- candidate_count: personEntries.length,
78
- person_entries_count: personEntries.length,
79
- resolved_count: resolved.length,
80
- };
81
- })()`}I({site:"linkedin",name:"people-search",access:"read",description:"Search standard LinkedIn (not Sales Navigator) for people by keyword. Each invocation consumes against LinkedIn's monthly Commercial Use Limit on people search; throttle accordingly.",domain:T,strategy:C.COOKIE,browser:!0,args:[{name:"keywords",type:"string",required:!0,positional:!0,help:'People search keywords, e.g. "site reliability engineer berlin"'},{name:"limit",type:"int",default:5,help:`Maximum people to return (1-${b}); each query counts toward LinkedIn's monthly CUL`},{name:"first-name",type:"string",required:!1,help:"Filter to people whose first name matches this text (LinkedIn KEYWORDS / FIRST NAME filter)."},{name:"last-name",type:"string",required:!1,help:"Filter to people whose last name matches this text (LinkedIn KEYWORDS / LAST NAME filter)."},{name:"title",type:"string",required:!1,help:"Filter to people whose current title contains this text (LinkedIn KEYWORDS / TITLE filter)."},{name:"school-keyword",type:"string",required:!1,help:"Free-text school filter (LinkedIn SCHOOL filter, name match)."},{name:"network",type:"string",required:!1,help:'Comma-separated connection degrees: 1, 2, 3 (where 3 means 3rd+). E.g. "1,2" for 1st and 2nd-degree only.'},{name:"profile-language",type:"string",required:!1,help:'Comma-separated ISO 639-1 codes for profile language, e.g. "en,fr".'},{name:"open-to",type:"string",required:!1,help:"Comma-separated open-to values: proBono, boardMember."},{name:"current-company-id",type:"string",required:!1,help:"Comma-separated LinkedIn company IDs for CURRENT COMPANY filter (e.g. \"1441\" for Google). To find an ID: visit the company's LinkedIn URL — `/company/<slug>/` — then open Network tools, or click the filter chip on a search results page and read the URL's `currentCompany` param. A full URN like `urn:li:fs_company:1441` is also accepted."},{name:"past-company-id",type:"string",required:!1,help:"Comma-separated LinkedIn company IDs for PAST COMPANY filter. Same lookup pattern as --current-company-id."},{name:"industry-id",type:"string",required:!1,help:'Comma-separated LinkedIn industry IDs (e.g. "4" for Computer Software). Apply the filter once via the LinkedIn UI and read the URL\'s `industry` param to discover IDs.'},{name:"location-id",type:"string",required:!1,help:'Comma-separated LinkedIn location IDs (geoUrn numeric tail; e.g. "105214831" for Bengaluru area). Apply the location filter once via UI and read the URL\'s `geoUrn` param.'},{name:"school-id",type:"string",required:!1,help:"Comma-separated LinkedIn school IDs (numeric tail of urn:li:fs_school:<id>). For free-text school name matching, use --school-keyword instead."}],columns:["rank","name","headline","location","profile_url"],func:async(F,G)=>{if(!F)throw new Z("Browser session required for linkedin people-search");const J=y(G,"keywords","--keywords"),Q=U(G.limit),V={keywords:J,firstName:G["first-name"],lastName:G["last-name"],title:G.title,schoolKeyword:G["school-keyword"],network:M(G.network),profileLanguage:R(G["profile-language"]),openTo:O(G["open-to"]),currentCompanyIds:q(G["current-company-id"],"--current-company-id"),pastCompanyIds:q(G["past-company-id"],"--past-company-id"),industryIds:q(G["industry-id"],"--industry-id"),geoIds:q(G["location-id"],"--location-id"),schoolIds:q(G["school-id"],"--school-id")};try{await F.goto(_(V));await F.wait(6)}catch(Y){throw new Z(`LinkedIn people search navigation failed: ${Y?.message||Y}`)}let B;try{B=await F.getCookies({url:"https://www.linkedin.com"})}catch(Y){throw new Z(`LinkedIn cookie lookup failed: ${Y?.message||Y}`)}if(!Array.isArray(B))throw new Z("LinkedIn cookie lookup returned malformed payload");if(!B.find((Y)=>Y.name==="JSESSIONID")?.value)throw new D(T,"LinkedIn JSESSIONID cookie not found. Please sign in to LinkedIn in the browser.");let $;try{$=A(await F.evaluate(h()))}catch(Y){throw new Z(`LinkedIn people search extraction failed: ${Y?.message||Y}`)}if($?.error){if(z(`${$.url||""} ${$.error||""}`))throw new D(T,"LinkedIn people search requires an active signed-in browser session.");throw new Z(`LinkedIn redirected away from the search page (${$.error}). Likely Commercial Use Limit reached - the limit resets on the 1st of next month.`)}if(!$||typeof $!=="object")throw new Z("LinkedIn people search returned malformed extraction payload");const N=K($.candidate_count,"candidate_count");K($.person_entries_count,"person_entries_count");const x=K($.resolved_count,"resolved_count"),P=W($.rows);if(P.length===0&&(N>0||x>0))throw new Z("LinkedIn people search found profile candidates but could not parse stable result rows");if(P.length===0)throw new w(`No people found on the rendered page for "${J}". The search may have returned zero results, or the DOM markup may have changed.`);return P.slice(0,Q).map((Y,L)=>({rank:L+1,...Y}))}});export const __test__={normalizeWhitespace:H,parseLimit:U,buildSearchUrl:_,looksLinkedInAuthWall:z,normalizeProfileUrl:S,normalizePeopleRows:W,parseNonNegativeCount:K,extractionScript:h,parseNetworkArg:M,parseProfileLanguageArg:R,parseOpenToArg:O,parseIdListArg:q};
1
+ import{cli as z,Strategy as C}from"@jackwener/opencli/registry";import{ArgumentError as $,CommandExecutionError as M,EmptyResultError as P}from"@jackwener/opencli/errors";import{fetchSearchClusters as W}from"./search-api.js";const N="www.linkedin.com",R=10;function F(B){return String(B??"").replace(/[ ]/g," ").replace(/\s+/g," ").trim()}function _(B,G,H=G){const J=F(B[G]);if(!J)throw new $(`${H} is required`);return J}function S(B){if(B===void 0||B===null||B==="")return 5;const G=Number(B);if(!Number.isInteger(G)||G<1||G>R)throw new $(`--limit must be an integer between 1 and ${R}`);return G}const f={"1":"F","2":"S","3":"O"},O=new Set(["proBono","boardMember"]);function K(B){if(!B)return[];return String(B).split(",").map((G)=>G.trim()).filter(Boolean)}function X(B){if(!B)return[];const G=K(B),H=[];for(const J of G){const Q=f[J];if(!Q)throw new $(`--network values must be one or more of 1,2,3 (got "${J}")`);if(!H.includes(Q))H.push(Q)}return H}function D(B){if(!B)return[];const G=K(B).map((H)=>H.toLowerCase());for(const H of G)if(!/^[a-z]{2}$/.test(H))throw new $(`--profile-language values must be ISO 639-1 2-letter codes (got "${H}")`);return G}function x(B){if(!B)return[];const G=K(B);for(const H of G)if(!O.has(H))throw new $(`--open-to values must be one or more of proBono,boardMember (got "${H}")`);return G}function q(B,G){if(!B)return[];const H=K(B).map((J)=>J.replace(/^urn:li:[a-z_]+:/,""));for(const J of H)if(!/^\d+$/.test(J))throw new $(`${G} values must be numeric IDs (got "${J}")`);return H}function U(B){const G=["(key:resultType,value:List(PEOPLE))"],H=(Q,V)=>{if(Array.isArray(V)&&V.length)G.push(`(key:${Q},value:List(${V.join(",")}))`)},J=(Q,V)=>{const Z=F(V);if(Z)G.push(`(key:${Q},value:List(${encodeURIComponent(Z)}))`)};J("firstName",B.firstName);J("lastName",B.lastName);J("title",B.title);J("school",B.schoolKeyword);H("network",B.network);H("profileLanguage",B.profileLanguage);H("serviceCategory",B.openTo);H("currentCompany",B.currentCompanyIds);H("pastCompany",B.pastCompanyIds);H("industry",B.industryIds);H("geoUrn",B.geoIds);H("schools",B.schoolIds);return G}function b(B){const G=F(B);if(!G)return"";try{const H=new URL(G,"https://www.linkedin.com"),J=H.hostname.toLowerCase();if(H.protocol!=="https:"||H.username||H.password||H.port)return"";if(J!=="linkedin.com"&&J!=="www.linkedin.com")return"";const Q=H.pathname.match(/^\/in\/([^/?#]+)\/?$/);if(!Q||!Q[1])return"";return`https://www.linkedin.com/in/${Q[1]}/`}catch{return""}}z({site:"linkedin",name:"people-search",access:"read",description:"Search standard LinkedIn (not Sales Navigator) for people by keyword via the Voyager search API. Each invocation consumes against LinkedIn's monthly Commercial Use Limit on people search; throttle accordingly.",domain:N,strategy:C.COOKIE,browser:!0,args:[{name:"keywords",type:"string",required:!0,positional:!0,help:'People search keywords, e.g. "site reliability engineer berlin"'},{name:"limit",type:"int",default:5,help:`Maximum people to return (1-${R}); each query counts toward LinkedIn's monthly CUL`},{name:"first-name",type:"string",required:!1,help:"Filter to people whose first name matches this text (LinkedIn KEYWORDS / FIRST NAME filter)."},{name:"last-name",type:"string",required:!1,help:"Filter to people whose last name matches this text (LinkedIn KEYWORDS / LAST NAME filter)."},{name:"title",type:"string",required:!1,help:"Filter to people whose current title contains this text (LinkedIn KEYWORDS / TITLE filter)."},{name:"school-keyword",type:"string",required:!1,help:"Free-text school filter (LinkedIn SCHOOL filter, name match)."},{name:"network",type:"string",required:!1,help:'Comma-separated connection degrees: 1, 2, 3 (where 3 means 3rd+). E.g. "1,2" for 1st and 2nd-degree only.'},{name:"profile-language",type:"string",required:!1,help:'Comma-separated ISO 639-1 codes for profile language, e.g. "en,fr".'},{name:"open-to",type:"string",required:!1,help:"Comma-separated open-to values: proBono, boardMember."},{name:"current-company-id",type:"string",required:!1,help:'Comma-separated LinkedIn company IDs for CURRENT COMPANY filter (e.g. "1441" for Google). A full URN like `urn:li:fs_company:1441` is also accepted.'},{name:"past-company-id",type:"string",required:!1,help:"Comma-separated LinkedIn company IDs for PAST COMPANY filter. Same lookup pattern as --current-company-id."},{name:"industry-id",type:"string",required:!1,help:'Comma-separated LinkedIn industry IDs (e.g. "4" for Computer Software).'},{name:"location-id",type:"string",required:!1,help:'Comma-separated LinkedIn location IDs (geoUrn numeric tail; e.g. "105214831" for Bengaluru area).'},{name:"school-id",type:"string",required:!1,help:"Comma-separated LinkedIn school IDs (numeric tail of urn:li:fs_school:<id>). For free-text school name matching, use --school-keyword instead."}],columns:["rank","name","headline","location","profile_url"],func:async(B,G)=>{if(!B)throw new M("Browser session required for linkedin people-search");const H=_(G,"keywords","--keywords"),J=S(G.limit),Q=U({firstName:G["first-name"],lastName:G["last-name"],title:G.title,schoolKeyword:G["school-keyword"],network:X(G.network),profileLanguage:D(G["profile-language"]),openTo:x(G["open-to"]),currentCompanyIds:q(G["current-company-id"],"--current-company-id"),pastCompanyIds:q(G["past-company-id"],"--past-company-id"),industryIds:q(G["industry-id"],"--industry-id"),geoIds:q(G["location-id"],"--location-id"),schoolIds:q(G["school-id"],"--school-id")});await B.goto("https://www.linkedin.com/feed/");await B.wait(2);const{rows:V}=await W(B,{keywords:H,filters:Q,start:0,count:Math.max(J,10)},"people-search"),Z=[];for(const Y of V){const j=b(Y.nav);if(!Y.name||!j)continue;Z.push({name:Y.name,headline:Y.headline,location:Y.location,profile_url:j})}if(Z.length===0)throw new P(`No people found for "${H}". The search may have returned zero results, or your monthly Commercial Use Limit may be reached (resets on the 1st).`);return Z.slice(0,J).map((Y,j)=>({rank:j+1,...Y}))}});export const __test__={normalizeWhitespace:F,parseLimit:S,buildPeopleFilters:U,normalizeProfileUrl:b,parseNetworkArg:X,parseProfileLanguageArg:D,parseOpenToArg:x,parseIdListArg:q};
@@ -1,11 +1 @@
1
- import{cli as d,Strategy as f}from"@jackwener/opencli/registry";import{CommandExecutionError as a,EmptyResultError as m}from"@jackwener/opencli/errors";import{assertLinkedInAuthenticated as u,assertSafeLinkedinUrl as w,normalizeWhitespace as t,unwrapEvaluateResult as h}from"./shared.js";function o(e){const r=w(e||"https://www.linkedin.com/in/me/","profile-url","/in/me/"),n=new URL(r);if(!/^\/in\/[^/?#]+\/?$/.test(n.pathname))throw new a("LinkedIn profile-analytics requires a /in/<handle>/ profile URL");return n.toString()}function c(e){const n=t(e).toLowerCase().replace(/,/g,"").match(/(\d+(?:\.\d+)?)(k|m)?/i);if(!n)return"";const i=Number(n[1]);if(n[2]?.toLowerCase()==="k")return String(Math.round(i*1000));if(n[2]?.toLowerCase()==="m")return String(Math.round(i*1e6));return String(Math.round(i))}function s(e,r){for(const n of r){const i=t(e).match(n);if(i)return c(i[1])}return""}function l(e){const r=t(e);return{profile_views:s(r,[/(\d[\d,.]*\s*(?:k|m)?)\s+profile views?/i,/profile views?\s+(\d[\d,.]*\s*(?:k|m)?)/i]),post_impressions:s(r,[/(\d[\d,.]*\s*(?:k|m)?)\s+post impressions?/i,/post impressions?\s+(\d[\d,.]*\s*(?:k|m)?)/i]),search_appearances:s(r,[/(\d[\d,.]*\s*(?:k|m)?)\s+search appearances?/i,/search appearances?\s+(\d[\d,.]*\s*(?:k|m)?)/i]),followers:s(r,[/(\d[\d,.]*\s*(?:k|m)?)\s+followers?/i]),connections:s(r,[/(\d[\d,.]*\s*(?:k|m)?)\s+connections?/i])}}function k(){return String.raw`(() => {
2
- const clean = (s) => String(s || '').replace(/[\u00a0\u202f]+/g, ' ').replace(/\s+/g, ' ').trim();
3
- const text = clean(document.body?.innerText || document.body?.textContent || '');
4
- const matches = [
5
- ...text.matchAll(/\d[\d,.]*\s*(?:k|m)?\s+(?:profile views?|post impressions?|search appearances?|followers?|connections?)/gi),
6
- ].map((match) => clean(match[0]));
7
- return {
8
- profile_url: window.location.href,
9
- raw_analytics: matches.join(' | '),
10
- };
11
- })()`}function p(e){if(!e||typeof e!=="object")throw new a("LinkedIn profile-analytics returned malformed extraction payload");const r=l(e.raw_analytics);if(!Object.values(r).some(Boolean))throw new m("linkedin profile-analytics","No visible LinkedIn profile analytics counters were found.");return{profile_url:t(e.profile_url),...r,raw_analytics:t(e.raw_analytics)}}d({site:"linkedin",name:"profile-analytics",access:"read",description:"Read visible LinkedIn profile dashboard metrics such as profile views, post impressions, and search appearances",domain:"www.linkedin.com",strategy:f.COOKIE,browser:!0,args:[{name:"profile-url",type:"string",required:!1,help:"LinkedIn /in/<handle>/ profile URL. Defaults to /in/me/."}],columns:["profile_url","profile_views","post_impressions","search_appearances","followers","connections","raw_analytics"],func:async(e,r)=>{if(!e)throw new a("Browser session required for linkedin profile-analytics");await e.goto(o(r["profile-url"]));await e.wait(5);await u(e,"LinkedIn profile-analytics");const n=h(await e.evaluate(k()));return[p(n)]}});export const __test__={normalizeProfileAnalyticsUrl:o,parseMetric:c,parseDashboardMetrics:l,normalizeAnalytics:p};
1
+ import{cli as F,Strategy as G}from"@jackwener/opencli/registry";import{CommandExecutionError as H,EmptyResultError as J}from"@jackwener/opencli/errors";import{normalizeWhitespace as v}from"./shared.js";import{fetchSelfAnalytics as K,normalizeProfileInputUrl as L}from"./profile-api.js";F({site:"linkedin",name:"profile-analytics",access:"read",description:"Read your own LinkedIn analytics via the Voyager API: profile views, search appearances, followers, and connections (self only)",domain:"www.linkedin.com",strategy:G.COOKIE,browser:!0,args:[{name:"profile-url",type:"string",required:!1,help:"Ignored beyond validation — analytics are only visible for the signed-in account."}],columns:["profile_url","profile_views","post_impressions","search_appearances","followers","connections"],func:async(B,D)=>{if(!B)throw new H("Browser session required for linkedin profile-analytics");if(D["profile-url"])L(D["profile-url"],"profile-analytics");const q=await K(B,"profile-analytics"),b={profile_url:"https://www.linkedin.com/in/me/",profile_views:v(q.profile_views),post_impressions:"",search_appearances:v(q.search_appearances),followers:v(q.followers),connections:v(q.connections)};if(!b.profile_views&&!b.search_appearances&&!b.followers&&!b.connections)throw new J("linkedin profile-analytics","No LinkedIn analytics counters were returned by the Voyager API.");return[b]}});export const __test__={};