ilml-plugin-linkedin 1.20.1 → 1.20.2
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/CHANGELOG.md +3 -0
- package/README.md +2 -1
- package/dist/connect.mjs +11 -11
- package/dist/enrichProfile.mjs +14 -14
- package/dist/funnelRunner.mjs +1 -1
- package/dist/run.mjs +1 -1
- package/dist/syncAll.mjs +29 -29
- package/dist/verifyAbout.mjs +3 -3
- package/dist/version.mjs +1 -1
- package/ilml-plugin.json +15 -2
- package/package.json +1 -1
package/dist/verifyAbout.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var Ve=Object.defineProperty;var
|
|
1
|
+
var Ve=Object.defineProperty;var K=(e,t)=>()=>(e&&(t=e(e=0)),t);var We=(e,t)=>{for(var o in t)Ve(e,o,{get:t[o],enumerable:!0})};async function f(e){let t=Math.floor(Math.random()*2001),o=e+t;return new Promise(n=>setTimeout(n,o))}async function we(e){return await e.evaluate(()=>{let t=document.querySelectorAll("div.artdeco-modal--is-open");for(let n of t){let i=n.innerText||"",d=i.includes("Premium")||i.includes("premium")||i.includes("Upgrade")||i.includes("upgrade"),l=i.includes("Try for")||i.includes("free trial")||i.includes("/month")||i.includes("per month")||i.includes("Start my")||i.includes("Subscribe"),g=i.includes("Get hired faster")||i.includes("Get hired 2x");if(d||l||g){let L=n.querySelector('button[aria-label="Dismiss"]')||n.querySelector("button.artdeco-modal__dismiss")||n.querySelector("button[data-test-modal-close-btn]");if(L)return L.click(),{dismissed:!0,matched:i.substring(0,80)};let I=n.querySelectorAll("button");for(let a of I){let S=a.innerText?.trim().toLowerCase()||"",p=a.getAttribute("aria-label")?.toLowerCase()||"";if(S==="not now"||S==="no thanks"||S==="dismiss"||p.includes("dismiss"))return a.click(),{dismissed:!0,matched:i.substring(0,80)}}}}let o=document.querySelector(".premium-upsell-link")||document.querySelector("[data-test-premium-upsell]")||document.querySelector(".premium-hub-upsell");if(o){let n=o.closest('[class*="modal"]')?.querySelector('button[aria-label="Dismiss"]')||o.closest('[class*="banner"]')?.querySelector('button[aria-label="Close"]');if(n)return n.click(),{dismissed:!0,matched:"premium-banner"}}return!1})}var H=K(()=>{});import{getPluginConfig as Je}from"@ilivemylife/graph-sdk";var Ye,Ke,j,kt,Ge,X=K(async()=>{Ye=await Je();for(let[e,t]of Object.entries(Ye))process.env[e]===void 0&&(process.env[e]=t);Ke=process.env.LINKEDIN_NAME,j=Object.freeze({DRAFT:"draft",CONFIRMED:"confirmed",REJECTED:"rejected",NEEDS_REVIEW:"needs_review",SENT:"sent"}),kt=Object.freeze(new Set([j.DRAFT,j.CONFIRMED,j.NEEDS_REVIEW])),Ge=Object.freeze(new Set([j.DRAFT,j.CONFIRMED,j.REJECTED,j.NEEDS_REVIEW]))});import"dotenv/config";import*as w from"fs";import*as b from"path";import{fileURLToPath as ot}from"url";function lt(){w.existsSync(A)||w.mkdirSync(A,{recursive:!0})}function at(e,t){if(!w.existsSync(e))return null;try{let o=JSON.parse(w.readFileSync(e,"utf-8"));try{w.copyFileSync(e,e+".backup")}catch{}return o}catch(o){console.error(`[!] ${t} is corrupted: ${o.message}`),console.error(` File: ${b.resolve(e)}`);let n=e+".backup";if(w.existsSync(n))try{let i=JSON.parse(w.readFileSync(n,"utf-8"));console.error(` Restored from ${n}`);try{w.copyFileSync(n,e)}catch{}return i}catch{console.error(" Backup also corrupted!")}console.error(` Starting with empty data. Old file preserved as ${e}.corrupted`);try{w.copyFileSync(e,e+".corrupted")}catch{}return null}}function ee(){return G||(lt(),G=at(st,"people.json")||{},G)}var it,rt,A,st,Wt,Jt,Yt,Kt,Gt,zt,Te,Qt,Zt,G,te=K(async()=>{await X();it=ot(import.meta.url),rt=b.dirname(b.dirname(it)),A=process.env.DATA_DIR?b.resolve(process.env.DATA_DIR,"collected-profiles"):b.join(rt,"collected-profiles"),st=b.join(A,"people.json"),Wt=b.join(A,"jobs.json"),Jt=b.join(A,"quota.json"),Yt=b.join(A,"conversations.json"),Kt=b.join(A,"sync-state.json"),Gt=b.join(A,"application-questions.json"),zt=b.join(A,"profile-history"),Te=(e,t)=>{let o=parseInt(e,10);return Number.isInteger(o)&&o>=0?o:t},Qt=Te(process.env.CONNECT_MAX_DAILY,20),Zt=Te(process.env.CONNECT_MAX_WEEKLY,100),G=null});var Le={};We(Le,{extractCertificationsFromDetails:()=>mt,extractEducationFromDetails:()=>dt,extractExperienceFromDetails:()=>ct,extractFeaturedFromDetails:()=>ut,extractRecommendationsFromDetails:()=>pt,extractSkillsFromDetails:()=>ft});async function ct(e,t){let o=t.replace(/\/+$/,"")+"/details/experience/";try{await e.goto(o,{waitUntil:"domcontentloaded",timeout:6e4}),await f(3e3);let n=e.url();if(/\/(login|authwall|checkpoint)/i.test(n))throw new Error(`Session expired \u2014 bounced to ${n} when fetching /details/experience/`);for(let d=0;d<5;d++)await e.evaluate(()=>window.scrollTo(0,document.body.scrollHeight)),await f(900);await f(1e3);let i=0;for(;i<5&&await e.evaluate(()=>{for(let l of document.querySelectorAll("button"))if(/^Load more$/i.test((l.textContent||"").trim()))return l.click(),!0;return!1});)i++,await f(1500),await e.evaluate(()=>window.scrollTo(0,document.body.scrollHeight)),await f(700);return await e.evaluate(()=>{let d=document.querySelector("main");if(!d)return[];function l(a){let S=[];for(let p of a.querySelectorAll("div")){let h=Array.from(p.children).filter(x=>x.tagName==="DIV");if(h.length<3)continue;let y=h.filter(x=>(x.textContent||"").trim().length>50);y.length<3||(p.textContent||"").length<500||S.push({el:p,count:y.length,totalText:(p.textContent||"").length})}return S.sort((p,h)=>h.count-p.count||p.totalText-h.totalText),S[0]?.el}let g=l(d);if(!g)return[];let L=Array.from(g.children).filter(a=>a.tagName==="DIV").filter(a=>(a.textContent||"").trim().length>50);function I(a){let S=a[0]||null,p=null,h=null;if(a[1]){let u=a[1].split("\xB7").map(v=>v.trim());p=u[0]||null,u[1]&&(h=u[1])}let y=a.findIndex(u=>/(\d{4}|present)/i.test(u)&&/(-|–|—)/i.test(u)&&u.length<80),x=null,M=null;if(y>=0){let u=a[y].split("\xB7").map(v=>v.trim());x=u[0]||null,u[1]&&(M=u[1])}let q=null,R=-1;if(y>=0&&a[y+1]){let u=a[y+1],v=/^[>*\-•]|^>/.test(u),N=/https?:\/\//.test(u),O=/\d{4}/.test(u),C=/,/.test(u)||/\bArea\b|\bRegion\b|\bProvince\b|\bCountry\b|, [A-Z]{2}\b/.test(u);!v&&!N&&!O&&u.length<100&&C&&(q=u,R=y+1)}let z=null,m=-1,F=a[a.length-1];if(F&&/and\s+\+\d+\s+skills?$/i.test(F)){m=a.length-1;let u=F.match(/^(.+?)\s+and\s+\+(\d+)\s+skills?$/i);u&&(z={listed:u[1].split(",").map(v=>v.trim()),additional:parseInt(u[2],10)})}let P=new Set([0,1,y,R,m].filter(u=>u>=0)),$=a.filter((u,v)=>!P.has(v)),k=$.length>0?$.join(`
|
|
2
2
|
|
|
3
3
|
`):null;return{title:S,company:p,employmentType:h,dates:x,duration:M,location:q,description:k,skills:z,_raw:a}}return L.map(a=>{let p=(a.innerText||"").trim().split(`
|
|
4
4
|
`).map(h=>h.trim()).filter(h=>h&&h.length<5e3);return I(p)}).filter(a=>a.title)})}catch(n){if(/session expired|bounced to|authwall|checkpoint/i.test(n.message||""))throw n;return console.error(`extractExperienceFromDetails failed for ${t}: ${n.message?.slice(0,120)}`),[]}}async function U(e,t,o){let n=t.replace(/\/+$/,"")+`/details/${o}/`;await e.goto(n,{waitUntil:"domcontentloaded",timeout:6e4}),await f(3e3);let i=e.url();if(/\/(login|authwall|checkpoint)/i.test(i))throw new Error(`Session expired \u2014 bounced to ${i} when fetching /details/${o}/`);if(!i.toLowerCase().includes(`/details/${o.toLowerCase()}/`))throw new Error(`Section /details/${o}/ not available on profile (landed on ${i})`);for(let l=0;l<4;l++)await e.evaluate(()=>window.scrollTo(0,document.body.scrollHeight)),await f(700);await f(800);let d=0;for(;d<5&&await e.evaluate(()=>{for(let g of document.querySelectorAll("button"))if(/^Load more$/i.test((g.textContent||"").trim()))return g.click(),!0;return!1});)d++,await f(1300),await e.evaluate(()=>window.scrollTo(0,document.body.scrollHeight)),await f(600)}async function W(e,t,o,n){try{return await n()}catch(i){let d=i.message||"";if(/session expired|bounced to|authwall|checkpoint/i.test(d))throw i;return/not available on profile/i.test(d)?(console.log(` (${t}: section not on profile \u2014 skipped)`),[]):(console.error(`${o} failed for ${e}: ${d.slice(0,120)}`),[])}}async function ut(e,t){return W(t,"featured","extractFeaturedFromDetails",async()=>(await U(e,t,"featured"),await e.evaluate(`(() => {
|
|
@@ -172,7 +172,7 @@ var Ve=Object.defineProperty;var Y=(e,t)=>()=>(e&&(t=e(e=0)),t);var We=(e,t)=>{f
|
|
|
172
172
|
if (e.recommenderTitle && /^\xB7\\s*\\d+(?:st|nd|rd|th)\\+?$/i.test(e.recommenderTitle.trim())) return false;
|
|
173
173
|
return true;
|
|
174
174
|
});
|
|
175
|
-
})()`)))}var V,Me=
|
|
175
|
+
})()`)))}var V,Me=K(()=>{H();V=`
|
|
176
176
|
function findEntryContainer(root) {
|
|
177
177
|
const candidates = [];
|
|
178
178
|
for (const div of root.querySelectorAll('div')) {
|
|
@@ -236,7 +236,7 @@ function findEntryContainer(root) {
|
|
|
236
236
|
return candidates[0]?.el;
|
|
237
237
|
}
|
|
238
238
|
`});import et from"puppeteer";import{addExtra as tt}from"puppeteer-extra";import nt from"puppeteer-extra-plugin-stealth";H();await X();import _ from"fs";import T from"path";var ze="cookies.json";function ye(){let e=process.env.ILML_SCOPE_DIR;return e?T.join(e,"plugins-state","linkedin","cookies.json"):ze}function be(){let e=ye();if(!_.existsSync(e))throw new Error(`Cookies not found at ${e}. Run: ilml linkedin login`);let t;try{t=_.readFileSync(e,"utf-8")}catch(n){throw new Error(`Failed to read cookies at ${e}: ${n.message}. Run: ilml linkedin login`)}let o;try{o=JSON.parse(t)}catch{throw new Error(`Cookies file at ${e} is corrupted (invalid JSON). Run: ilml linkedin login`)}if(!Array.isArray(o)||o.length===0)throw new Error(`Cookies file at ${e} is empty or not an array. Run: ilml linkedin login`);return o}function Se(){return _.existsSync(ye())}function Qe(){let e=process.env.ILML_SCOPE_DIR;return e?T.join(e,"plugins-state","linkedin"):"."}function ve(e){let t=String(e||"").replace(/[^a-zA-Z0-9_.-]/g,"_").slice(0,80);return/[a-zA-Z0-9]/.test(t)?t:"default"}function Ze(){let e=process.env.MY_LINKEDIN_URL,t=e&&e.match(/\/in\/([^/?#]+)/);return t&&t[1]?ve(t[1]):"default"}function Ce(e){let t=process.env.LINKEDIN_CHROME_PROFILE_DIR;if(t)return _.existsSync(t)||_.mkdirSync(t,{recursive:!0}),T.resolve(t);let o=ve(e||Ze()),n=T.join(Qe(),"chrome-profile",o);return _.existsSync(n)||_.mkdirSync(n,{recursive:!0}),T.resolve(n)}var ke=".ilml-seeded";function xe(e){return!_.existsSync(T.join(e,ke))}function _e(e){try{_.writeFileSync(T.join(e,ke),new Date().toISOString())}catch{}}function Ee(e){let t=0;for(let o of["SingletonLock","SingletonCookie","SingletonSocket"])try{_.rmSync(T.join(e,o),{force:!0}),t++}catch{}return t}var Xe={headless:!1,defaultViewport:null,protocolTimeout:3e5};async function Ae(e,t={}){let o=Ce(),n={...Xe,...t,userDataDir:o},i;try{i=await e.launch(n)}catch{Ee(o);try{i=await e.launch(n)}catch(l){throw new Error(`Could not open the LinkedIn automation Chrome profile at ${o}. If a LinkedIn automation window is already open, close it and retry. (Chrome: ${l.message})`)}}return{browser:i,profileDir:o,needsSeed:xe(o)}}async function Ie(e,t){if(!t||!t.needsSeed)return!1;if(!Se())return console.warn(" \u26A0 No saved LinkedIn session found. If this command comes back empty or redirects to login, run: ilml linkedin login"),!1;try{return await e.setCookie(...be()),_e(t.profileDir),console.log(" (seeded persistent profile from saved cookies \u2014 one-time)"),!0}catch{return!1}}var $e=tt(et);$e.use(nt());async function De(){let{browser:e,profileDir:t,needsSeed:o}=await Ae($e),n=async()=>{console.log(`
|
|
239
|
-
Closing browser...`);try{await e.close()}catch{}process.exit(0)};process.on("SIGINT",n),process.on("SIGTERM",n);let i=await e.newPage();return await i.evaluateOnNewDocument(()=>{Object.defineProperty(navigator,"webdriver",{get:()=>{}})}),await Ie(i,{profileDir:t,needsSeed:o}),console.log(" Browser launched (persistent profile)."),{browser:e,page:i}}H();await te();async function ht(e,t){let o=e.url();if(o.includes("/in/"))return!1;if(o.includes("/login")||o.includes("/authwall")||o.includes("/checkpoint"))throw new Error("Session expired \u2014 redirected to login. Run: ilml linkedin login");console.log(` Off-track: ${o.slice(0,80)}`),console.log(" Navigating back to profile..."),await e.goto(t,{waitUntil:"domcontentloaded",timeout:6e4}),await f(3e3);let n=e.url();return n.includes("/in/")||console.log(` Recovery failed \u2014 still on: ${n.slice(0,80)}`),!0}async function qe(e,t,o={}){let{extractExperience:n=!1,extractFeatured:i=!1,extractEducation:d=!1,extractSkills:l=!1,extractRecommendations:g=!1,extractCertifications:L=!1}=o;try{await e.goto(t,{waitUntil:"domcontentloaded",timeout:6e4}),await f(3e3),await we(e)&&(console.log(" Dismissed Premium overlay"),await f(1500),await ht(e,t));let a=0;for(let m=0;m<2;m++){let F=await e.evaluate(()=>document.body.scrollHeight);for(let $=1;$<=6;$++)await e.evaluate(k=>window.scrollTo(0,k),F*$/6),await f(900);let P=await e.evaluate(()=>document.body.scrollHeight);if(P===a)break;a=P}await f(800);let S=null;try{let m=e.url();m&&/\/in\/[^/?#]/.test(m)&&(S=m.split("?")[0].replace(/\/+$/,""))}catch{}let p=await e.evaluate(()=>{let m=new Set(["Highlights","About","Activity","Experience","Education","Skills","Recommendations","Languages","Interests","Volunteer experience","Featured","Honors & awards","Honors and awards","Projects","Publications","Patents","Certifications","Licenses & certifications","Courses","Test scores","Organizations"]),F=new Set(["Ad Options","Don\u2019t want to see this","0 notifications","Explore Premium profiles","You might like","People you may know","Suggested for you","More profiles for you","Promoted"]),P=r=>{let s=(r||"").trim().replace(/^[·•]\s*/,"");return/^(1st|2nd|3rd|she\/her|he\/him|they\/them|their\/them)$/i.test(s)},$={name:null,title:null,location:null,currentCompany:null,currentSchool:null,connectionDegree:null,hasVerifiedBadge:!1,photoUrl:null,connectionsLevel:null,mutualConnectionsCount:null,followers:null,openToWork:!1,hiring:!1,about:null,topSkills:null,availableSections:[]},k=document.querySelector("main");if(!k)return $;let u=[...Array.from(k.querySelectorAll("h1")),...Array.from(k.querySelectorAll("h2"))],v=Array.from(k.querySelectorAll("h2")),
|
|
239
|
+
Closing browser...`);try{await e.close()}catch{}process.exit(0)};process.on("SIGINT",n),process.on("SIGTERM",n);let i=await e.newPage();return await i.evaluateOnNewDocument(()=>{Object.defineProperty(navigator,"webdriver",{get:()=>{}})}),await Ie(i,{profileDir:t,needsSeed:o}),console.log(" Browser launched (persistent profile)."),{browser:e,page:i}}H();await te();async function ht(e,t){let o=e.url();if(o.includes("/in/"))return!1;if(o.includes("/login")||o.includes("/authwall")||o.includes("/checkpoint"))throw new Error("Session expired \u2014 redirected to login. Run: ilml linkedin login");console.log(` Off-track: ${o.slice(0,80)}`),console.log(" Navigating back to profile..."),await e.goto(t,{waitUntil:"domcontentloaded",timeout:6e4}),await f(3e3);let n=e.url();return n.includes("/in/")||console.log(` Recovery failed \u2014 still on: ${n.slice(0,80)}`),!0}async function qe(e,t,o={}){let{extractExperience:n=!1,extractFeatured:i=!1,extractEducation:d=!1,extractSkills:l=!1,extractRecommendations:g=!1,extractCertifications:L=!1}=o;try{await e.goto(t,{waitUntil:"domcontentloaded",timeout:6e4}),await f(3e3),await we(e)&&(console.log(" Dismissed Premium overlay"),await f(1500),await ht(e,t));let a=0;for(let m=0;m<2;m++){let F=await e.evaluate(()=>document.body.scrollHeight);for(let $=1;$<=6;$++)await e.evaluate(k=>window.scrollTo(0,k),F*$/6),await f(900);let P=await e.evaluate(()=>document.body.scrollHeight);if(P===a)break;a=P}await f(800);let S=null;try{let m=e.url();m&&/\/in\/[^/?#]/.test(m)&&(S=m.split("?")[0].replace(/\/+$/,""))}catch{}let p=await e.evaluate(()=>{let m=new Set(["Highlights","About","Activity","Experience","Education","Skills","Recommendations","Languages","Interests","Volunteer experience","Featured","Honors & awards","Honors and awards","Projects","Publications","Patents","Certifications","Licenses & certifications","Courses","Test scores","Organizations"]),F=new Set(["Ad Options","Don\u2019t want to see this","0 notifications","Explore Premium profiles","You might like","People you may know","Suggested for you","More profiles for you","Promoted"]),P=r=>{let s=(r||"").trim().replace(/^[·•]\s*/,"");return/^(1st|2nd|3rd|she\/her|he\/him|they\/them|their\/them)$/i.test(s)},$={name:null,title:null,location:null,currentCompany:null,currentSchool:null,connectionDegree:null,hasVerifiedBadge:!1,photoUrl:null,connectionsLevel:null,mutualConnectionsCount:null,followers:null,openToWork:!1,hiring:!1,about:null,topSkills:null,availableSections:[]},k=document.querySelector("main");if(!k)return $;let u=[...Array.from(k.querySelectorAll("h1")),...Array.from(k.querySelectorAll("h2"))],v=Array.from(k.querySelectorAll("h2")),N=u.find(r=>{let s=r.textContent&&r.textContent.trim(),c=(s||"").replace(/\s*\(\d+\)$/,"").trim();return s&&s.length>1&&s.length<100&&!m.has(c)&&!F.has(c)}),O=N?N.textContent.trim():null,C=N?N.parentElement:null;for(;C&&C!==k&&!Array.from(C.children).filter(s=>s.tagName==="P").some(s=>{let c=s.textContent&&s.textContent.trim();return c&&c.length>5&&!P(c)});)C=C.parentElement;let oe=null,ie=null,re=null,Q=null;if(C&&C!==k){let r=Array.from(C.children).filter(c=>c.tagName==="P").filter(c=>{let D=c.textContent&&c.textContent.trim();return D&&D.length>5&&!P(D)});if(r[0]&&(oe=r[0].textContent.trim()),r[1]){let c=r[1].textContent.trim().split(/\s*[·•]\s*/);ie=c[0]||null,c.length>1&&(re=c.slice(1).join(" \xB7 "))}let s=Array.from(C.children).filter(c=>c.tagName==="DIV");for(let c=s.length-1;c>=0;c--){let D=s[c].textContent&&s[c].textContent.trim();if(D&&!(O&&D.startsWith(O))&&(Q=D.split(/[·•]/)[0].trim(),Q))break}}let se=null;if(C){let r=C.querySelectorAll("p, span, div");for(let s of r){let c=(s.textContent||"").trim().replace(/^[·•]\s*/,"");if(c==="1st"||c==="2nd"||c==="3rd"){se=c;break}}}let Pe=!!k.querySelector('svg[data-test-icon="verified"], [aria-label*="Verified" i]'),le=document.querySelector('main img[width="200"], button img[alt*="photo" i]'),je=le&&le.src||null,J=k.textContent||"",ae=null,ce=J.match(/(\d+\+?)\s+connections\b/i);ce&&(ae=ce[1]+" connections");let Y=null,ue=J.match(/(\d+)\s+mutual connection/i),de=J.match(/and\s+(\d+)\s+other mutual/i),Ne=J.match(/([A-Z][\w'-]+(?:\s+[A-Z][\w'-]+)?)\s+and\s+([A-Z][\w'-]+(?:\s+[A-Z][\w'-]+)?)\s+are\s+mutual/);ue?Y=parseInt(ue[1],10):de?Y=2+parseInt(de[1],10):Ne&&(Y=2);let fe=null,me=null,pe=v.find(r=>r.textContent&&r.textContent.trim()==="About");if(pe){let r=pe.parentElement;for(;r&&r.tagName!=="SECTION";)r=r.parentElement;if(r){let s=(r.textContent||"").trim();s=s.replace(/^About\s*/,"").trim();let c=s.match(/Top skills\s*(.+?)$/i);if(c){let Z=c[1].split(/\b(?:see (?:more|less)|show (?:more|less))\b/i)[0].trim().split(/\s*[·•]\s*/).map(Ue=>Ue.trim()).filter(Boolean);Z.length>0&&Z.length<10&&(me=Z)}s=s.split(/\n?…\s*more/i)[0].trim(),s=s.split(/\n?\.\.\.\s*more/i)[0].trim(),s=s.split(/\bTop skills\b/i)[0].trim(),s=s.split(/\b(?:see (?:more|less)|show (?:more|less))\b/i)[0].trim(),fe=s||null}}let he=null,ge=v.find(r=>r.textContent&&r.textContent.trim()==="Activity");if(ge){let r=ge.parentElement;for(;r&&r.tagName!=="SECTION";)r=r.parentElement;if(r){let s=(r.textContent||"").slice(0,200).match(/([\d,]+)\s+followers/i);s&&(he=parseInt(s[1].replace(/,/g,""),10))}}let Oe=[...new Set(v.map(r=>(r.textContent||"").trim().replace(/\s*\(\d+\)$/,"").trim()).filter(r=>r&&r.length>1&&r.length<60&&r!==O).filter(r=>m.has(r)))],B=document.body.textContent||"",Be=/#OpenToWork\b/i.test(B)||/\bOpen to work\b/.test(B),He=/#Hiring\b/i.test(B)||/\bI'?m hiring\b/i.test(B)||/\bWe are hiring\b/i.test(B);return{name:O,title:oe,location:Q,currentCompany:ie,currentSchool:re,connectionDegree:se,hasVerifiedBadge:Pe,photoUrl:je,connectionsLevel:ae,mutualConnectionsCount:Y,followers:he,openToWork:Be,hiring:He,about:fe,topSkills:me,availableSections:Oe}});console.log(`Viewed: ${p.name} - ${p.title}`);let h=null,y=null,x=null,M=null,q=null,R=null;if(n||i||d||l||g||L){let m=await Promise.resolve().then(()=>(Me(),Le));n&&(h=await m.extractExperienceFromDetails(e,t),console.log(` + Experience: ${h.length} entries`)),i&&(y=await m.extractFeaturedFromDetails(e,t),console.log(` + Featured: ${y.length} entries`)),d&&(x=await m.extractEducationFromDetails(e,t),console.log(` + Education: ${x.length} entries`)),l&&(M=await m.extractSkillsFromDetails(e,t),console.log(` + Skills: ${M.length} entries`)),g&&(q=await m.extractRecommendationsFromDetails(e,t),console.log(` + Recommendations: ${q.length} entries`)),L&&(R=await m.extractCertificationsFromDetails(e,t),console.log(` + Certifications: ${R.length} entries`))}return{...p,profileUrl:t,resolvedUrl:S,...h!==null&&{experience:h},...y!==null&&{featured:y},...x!==null&&{education:x},...M!==null&&{skillsFull:M},...q!==null&&{recommendations:q},...R!==null&&{certifications:R}}}catch(I){console.error(`Error viewing profile ${t}:`,I.message);try{await e.goto("about:blank",{timeout:1e4}),await f(1e3)}catch{}if(/session expired|bounced to|authwall|checkpoint|redirected to login/i.test(I.message||""))throw I;return null}}await te();H();var ne=process.argv.slice(2),Re=ne.filter(e=>e.startsWith("http")),Fe=parseInt(ne.find(e=>e.startsWith("--pick="))?.split("=")[1]||"0"),gt=ne.includes("--has-about");function wt(e,t){return[...e].sort(()=>Math.random()-.5).slice(0,t)}function yt(){if(Re.length>0)return Re.map(t=>({url:t,name:"(arg)"}));if(Fe>0){let t=ee(),o=Object.entries(t).filter(([n])=>n.startsWith("http")).filter(([,n])=>gt?!!(n.about&&n.about.trim()):!n.about);return wt(o,Fe).map(([n,i])=>({url:n,name:i.name}))}let e=process.env.MY_LINKEDIN_URL;return e||(console.error("Pass a URL, use --pick=N, or set MY_LINKEDIN_URL in .env"),process.exit(1)),[{url:e,name:"(self)"}]}function E(e,t,o=80){if(t==null){console.log(` ${e.padEnd(20)} (null)`);return}let n=String(t),i=n.length>o?n.slice(0,o)+`\u2026 (+${n.length-o} chars)`:n;console.log(` ${e.padEnd(20)} ${i} [len=${n.length}]`)}async function bt(){let e=yt();console.log(`
|
|
240
240
|
About-selector verification \u2014 ${e.length} profile(s)
|
|
241
241
|
`);let{browser:t,page:o}=await De();try{for(let n=0;n<e.length;n++){let{url:i,name:d}=e[n];console.log("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"),console.log(` [${n+1}/${e.length}] ${d}`),console.log(` ${i}`),console.log("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");let l=await qe(o,i);if(!l){console.log(` \u26A0 viewProfile returned null \u2014 selector failed entirely
|
|
242
242
|
`);continue}E("name",l.name),E("title",l.title),E("location",l.location),E("company",l.company),E("connectionDegree",l.connectionDegree),E("connectionsCount",l.connectionsCount),E("mutualConnections",l.mutualConnections),E("hasVerifiedBadge",l.hasVerifiedBadge),E("photoUrl",l.photoUrl,60),console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 about \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"),E("about",l.about,200);let g=[];l.name||g.push("NAME MISSING"),l.title||g.push("TITLE MISSING"),l.location||g.push("LOCATION MISSING"),l.about||g.push("ABOUT MISSING"),console.log(`
|
package/dist/version.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{readFileSync as e}from"fs";import{fileURLToPath as r}from"url";function i(){return"1.20.
|
|
1
|
+
import{readFileSync as e}from"fs";import{fileURLToPath as r}from"url";function i(){return"1.20.2";try{let n=new URL("./ilml-plugin.json",import.meta.url);return JSON.parse(e(r(n),"utf-8")).version}catch{return"unknown"}}console.log(`ilml-plugin-linkedin ${i()}`);
|
package/ilml-plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "linkedin",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.2",
|
|
4
4
|
"description": "Reference ilml plugin: keep a local mirror of your LinkedIn data with AI-assisted workflows",
|
|
5
5
|
"commands": {
|
|
6
6
|
"help": { "run": "node dist/help.mjs", "description": "List all commands with descriptions; or `help <cmd>` for detailed info on one command (usage, flags, ban-risk, when-to-use). Reads ilml-plugin.json directly.", "usage": "ilml linkedin help [<cmd>]", "useWhen": "You forgot what a command does, or you're an AI agent picking the right tool." },
|
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
"flags": {
|
|
69
69
|
"add <file.json>": "Add profiles from a JSON array [{url, note?, name?}] to the queue. Dedupes against the queue and skips people already connected/pending. Combine with --tag=X and --priority=high|normal|low.",
|
|
70
70
|
"fill --category=...": "Fill the queue from people already categorised in your contacts DB (not yet connected). e.g. --category=founder,investor,hiring_manager (or --valuable for the standard high-value set). Each entry is tagged with its category so `quota` shows acceptance per category. By default it VALIDATES that each person's stored title actually confirms the category (drops people whose title contradicts the tag — the category field is sometimes mis-set by enrichment; people with no stored title are held rather than visited, to avoid ban risk). --no-validate keeps the old trust-the-tag behaviour. --min-posts=N keeps only people who appeared in at least N of your job posts (the repeat-recruiter signal, useful with --category=recruiter). --max=N keeps the best N (ranked by priority, mutual connections, repeat count). Respects your excluded-locations setting. No browser.",
|
|
71
|
+
"fill --from-discovered": "Top up the queue from the DISCOVERY POOL — people the plugin harvested from LinkedIn's 'People also viewed / More profiles for you' side-panels while connecting (enabled by the DISCOVERY_ENABLED setting). Ranked by relevance to you (personalised rank × sector-fit), best first — use it when your hand-curated list runs out. --dry-run previews the ranked picks without queuing; --max=N caps (default 20); --min-fit=0..1 and --min-rank=N set thresholds; --category / --sector filter; honours your excluded-locations; skips anyone already connected or queued. Only STAGES the queue — `approve` + `run` still do the sending. No browser.",
|
|
71
72
|
"list": "Show the queue (status, priority, note flag, name/title joined from your contacts). No browser. Optional --status=queued|approved|sent|skipped|failed.",
|
|
72
73
|
"approve <url>": "Move an entry from queued to approved. Or approve a whole batch with --all or --tag=X. Only approved entries are sent.",
|
|
73
74
|
"note <url> \"text\"": "Save a personal note on an entry (max 300 chars). It is sent ONLY when you run `connect run --with-notes` — by default connect sends plain requests, because LinkedIn caps custom notes to a handful per month. Shown with 📝 in `connect list`.",
|
|
@@ -77,7 +78,13 @@
|
|
|
77
78
|
"run": "Send approved entries, highest priority first, under the daily/weekly quota (browser). By default sends WITHOUT notes; add --with-notes to send the saved notes (spends your scarce monthly LinkedIn notes). --max=N caps this run, --dry-run previews without sending, --yes skips the confirmation prompt (for schedulers). Stops itself after 3 failures in a row and saves what LinkedIn showed to connect-diagnostics/.",
|
|
78
79
|
"--with-notes": "With `run`: actually send the per-entry saved notes (opt-in; default is no notes).",
|
|
79
80
|
"--tag=X": "Label a batch (used by add/approve). Shows up as the source in `ilml linkedin quota` acceptance stats.",
|
|
80
|
-
"--priority=LEVEL": "high|normal|low (used by add)."
|
|
81
|
+
"--priority=LEVEL": "high|normal|low (used by add).",
|
|
82
|
+
"--from-discovered": "With `fill`: source from the discovery pool instead of your contacts DB (see DISCOVERY_ENABLED).",
|
|
83
|
+
"--dry-run": "With `fill --from-discovered` or `run`: preview what would be added/sent without writing anything.",
|
|
84
|
+
"--min-fit=0..1": "With `fill --from-discovered`: minimum sector-fit (relevance to your interests) to include.",
|
|
85
|
+
"--min-rank=N": "With `fill --from-discovered`: minimum personalised rank to include.",
|
|
86
|
+
"--sector=a,b": "With `fill --from-discovered`: keep only these sectors (ai, robotics, deep-tech, fintech, …).",
|
|
87
|
+
"--max=N": "With `fill`/`run`: cap how many entries to add / send this run."
|
|
81
88
|
},
|
|
82
89
|
"banRisk": "Medium for `run` (sends real connection requests, browser) — but every send goes through the same daily/weekly quota gate as the funnel, so you can't exceed the limit. All other subcommands are local, no browser, no risk.",
|
|
83
90
|
"useWhen": "You have a curated list of people worth connecting to (investors from a post, event speakers, company leaders) and want to review, prioritise, and send requests under quota instead of blindly connecting recruiters."
|
|
@@ -320,6 +327,12 @@
|
|
|
320
327
|
{ "key": "CONNECT_MAX_DAILY", "label": "Max connection requests per day", "type": "number", "required": false, "default": 20, "hint": "Your OWN conservative daily cap across every connection path. LinkedIn's real limit is dynamic and unknown — keep this below it. Raise it deliberately only to surface LinkedIn's own limit: connect stops itself after 3 failures in a row and saves what LinkedIn showed to connect-diagnostics/." },
|
|
321
328
|
{ "key": "CONNECT_MAX_WEEKLY", "label": "Max connection requests per week", "type": "number", "required": false, "default": 100, "hint": "Rolling 7-day cap. LinkedIn free accounts are often limited around 100-200/week and adjust it based on your acceptance rate." }
|
|
322
329
|
]
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
"title": "Discovery (experimental)",
|
|
333
|
+
"fields": [
|
|
334
|
+
{ "key": "DISCOVERY_ENABLED", "label": "Build a discovery pool while connecting", "type": "boolean", "required": false, "default": false, "hint": "OFF by default. When ON, `connect run` also reads LinkedIn's \"People also viewed / More profiles for you\" side-panel on each profile it already opens, and saves those people to a local discovery pool — no extra page loads, no effect on the connect itself. `connect fill --from-discovered` then tops up your queue from that pool, ranked by relevance to you. Adds ~2-3s per profile (a scroll to load the panel), and only works on English-locale LinkedIn for now. Leave it OFF and nothing changes." }
|
|
335
|
+
]
|
|
323
336
|
}
|
|
324
337
|
]
|
|
325
338
|
}
|
package/package.json
CHANGED