ilml-plugin-linkedin 1.13.1 → 1.14.0
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 +22 -0
- package/README.md +7 -4
- package/dist/connect.mjs +325 -0
- package/dist/enrichProfile.mjs +3 -3
- package/dist/enrichProfiles.mjs +7 -7
- package/dist/funnelRunner.mjs +32 -32
- package/dist/messageBot.mjs +8 -8
- package/dist/quota.mjs +8 -0
- package/dist/run.mjs +48 -48
- package/dist/src/saveLoginState.mjs +3 -3
- package/dist/syncAll.mjs +38 -38
- package/dist/verifyAbout.mjs +7 -7
- package/dist/version.mjs +1 -0
- package/dist/viewers.mjs +9 -9
- package/dist/warmScan.mjs +3 -5
- package/ilml-plugin.json +37 -3
- package/package.json +4 -3
- package/target-companies.json +0 -58
package/dist/verifyAbout.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
var Ie=Object.defineProperty;var de=(e,t)=>()=>(e&&(t=e(e=0)),t);var De=(e,t)=>{for(var o in t)Ie(e,o,{get:t[o],enumerable:!0})};async function d(e){let t=Math.floor(Math.random()*2001),o=e+t;return new Promise(n=>setTimeout(n,o))}async function me(e){return await e.evaluate(()=>{let t=document.querySelectorAll("div.artdeco-modal--is-open");for(let n of t){let s=n.innerText||"",m=s.includes("Premium")||s.includes("premium")||s.includes("Upgrade")||s.includes("upgrade"),u=s.includes("Try for")||s.includes("free trial")||s.includes("/month")||s.includes("per month")||s.includes("Start my")||s.includes("Subscribe"),h=s.includes("Get hired faster")||s.includes("Get hired 2x");if(m||u||h){let D=n.querySelector('button[aria-label="Dismiss"]')||n.querySelector("button.artdeco-modal__dismiss")||n.querySelector("button[data-test-modal-close-btn]");if(D)return D.click(),{dismissed:!0,matched:s.substring(0,80)};let $=n.querySelectorAll("button");for(let l of $){let S=l.innerText?.trim().toLowerCase()||"",f=l.getAttribute("aria-label")?.toLowerCase()||"";if(S==="not now"||S==="no thanks"||S==="dismiss"||f.includes("dismiss"))return l.click(),{dismissed:!0,matched:s.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 j=de(()=>{});var we={};De(we,{extractCertificationsFromDetails:()=>
|
|
1
|
+
var Ie=Object.defineProperty;var de=(e,t)=>()=>(e&&(t=e(e=0)),t);var De=(e,t)=>{for(var o in t)Ie(e,o,{get:t[o],enumerable:!0})};async function d(e){let t=Math.floor(Math.random()*2001),o=e+t;return new Promise(n=>setTimeout(n,o))}async function me(e){return await e.evaluate(()=>{let t=document.querySelectorAll("div.artdeco-modal--is-open");for(let n of t){let s=n.innerText||"",m=s.includes("Premium")||s.includes("premium")||s.includes("Upgrade")||s.includes("upgrade"),u=s.includes("Try for")||s.includes("free trial")||s.includes("/month")||s.includes("per month")||s.includes("Start my")||s.includes("Subscribe"),h=s.includes("Get hired faster")||s.includes("Get hired 2x");if(m||u||h){let D=n.querySelector('button[aria-label="Dismiss"]')||n.querySelector("button.artdeco-modal__dismiss")||n.querySelector("button[data-test-modal-close-btn]");if(D)return D.click(),{dismissed:!0,matched:s.substring(0,80)};let $=n.querySelectorAll("button");for(let l of $){let S=l.innerText?.trim().toLowerCase()||"",f=l.getAttribute("aria-label")?.toLowerCase()||"";if(S==="not now"||S==="no thanks"||S==="dismiss"||f.includes("dismiss"))return l.click(),{dismissed:!0,matched:s.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 j=de(()=>{});var we={};De(we,{extractCertificationsFromDetails:()=>ze,extractEducationFromDetails:()=>Ke,extractExperienceFromDetails:()=>Ye,extractFeaturedFromDetails:()=>Ge,extractRecommendationsFromDetails:()=>Ze,extractSkillsFromDetails:()=>Qe});async function Ye(e,t){let o=t.replace(/\/+$/,"")+"/details/experience/";try{await e.goto(o,{waitUntil:"domcontentloaded",timeout:6e4}),await d(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 m=0;m<5;m++)await e.evaluate(()=>window.scrollTo(0,document.body.scrollHeight)),await d(900);await d(1e3);let s=0;for(;s<5&&await e.evaluate(()=>{for(let u of document.querySelectorAll("button"))if(/^Load more$/i.test((u.textContent||"").trim()))return u.click(),!0;return!1});)s++,await d(1500),await e.evaluate(()=>window.scrollTo(0,document.body.scrollHeight)),await d(700);return await e.evaluate(()=>{let m=document.querySelector("main");if(!m)return[];function u(l){let S=[];for(let f of l.querySelectorAll("div")){let p=Array.from(f.children).filter(x=>x.tagName==="DIV");if(p.length<3)continue;let w=p.filter(x=>(x.textContent||"").trim().length>50);w.length<3||(f.textContent||"").length<500||S.push({el:f,count:w.length,totalText:(f.textContent||"").length})}return S.sort((f,p)=>p.count-f.count||f.totalText-p.totalText),S[0]?.el}let h=u(m);if(!h)return[];let D=Array.from(h.children).filter(l=>l.tagName==="DIV").filter(l=>(l.textContent||"").trim().length>50);function $(l){let S=l[0]||null,f=null,p=null;if(l[1]){let a=l[1].split("\xB7").map(C=>C.trim());f=a[0]||null,a[1]&&(p=a[1])}let w=l.findIndex(a=>/(\d{4}|present)/i.test(a)&&/(-|–|—)/i.test(a)&&a.length<80),x=null,T=null;if(w>=0){let a=l[w].split("\xB7").map(C=>C.trim());x=a[0]||null,a[1]&&(T=a[1])}let M=null,J=-1;if(w>=0&&l[w+1]){let a=l[w+1],C=/^[>*\-•]|^>/.test(a),R=/https?:\/\//.test(a),v=/\d{4}/.test(a),H=/,/.test(a)||/\bArea\b|\bRegion\b|\bProvince\b|\bCountry\b|, [A-Z]{2}\b/.test(a);!C&&!R&&!v&&a.length<100&&H&&(M=a,J=w+1)}let y=null,q=-1,E=l[l.length-1];if(E&&/and\s+\+\d+\s+skills?$/i.test(E)){q=l.length-1;let a=E.match(/^(.+?)\s+and\s+\+(\d+)\s+skills?$/i);a&&(y={listed:a[1].split(",").map(C=>C.trim()),additional:parseInt(a[2],10)})}let L=new Set([0,1,w,J,q].filter(a=>a>=0)),k=l.filter((a,C)=>!L.has(C)),Y=k.length>0?k.join(`
|
|
2
2
|
|
|
3
3
|
`):null;return{title:S,company:f,employmentType:p,dates:x,duration:T,location:M,description:Y,skills:y,_raw:l}}return D.map(l=>{let f=(l.innerText||"").trim().split(`
|
|
4
|
-
`).map(p=>p.trim()).filter(p=>p&&p.length<5e3);return $(f)}).filter(l=>l.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 O(e,t,o){let n=t.replace(/\/+$/,"")+`/details/${o}/`;await e.goto(n,{waitUntil:"domcontentloaded",timeout:6e4}),await d(3e3);let s=e.url();if(/\/(login|authwall|checkpoint)/i.test(s))throw new Error(`Session expired \u2014 bounced to ${s} when fetching /details/${o}/`);if(!s.toLowerCase().includes(`/details/${o.toLowerCase()}/`))throw new Error(`Section /details/${o}/ not available on profile (landed on ${s})`);for(let u=0;u<4;u++)await e.evaluate(()=>window.scrollTo(0,document.body.scrollHeight)),await d(700);await d(800);let m=0;for(;m<5&&await e.evaluate(()=>{for(let h of document.querySelectorAll("button"))if(/^Load more$/i.test((h.textContent||"").trim()))return h.click(),!0;return!1});)m++,await d(1300),await e.evaluate(()=>window.scrollTo(0,document.body.scrollHeight)),await d(600)}async function B(e,t,o,n){try{return await n()}catch(s){let m=s.message||"";if(/session expired|bounced to|authwall|checkpoint/i.test(m))throw s;return/not available on profile/i.test(m)?(console.log(` (${t}: section not on profile \u2014 skipped)`),[]):(console.error(`${o} failed for ${e}: ${m.slice(0,120)}`),[])}}async function
|
|
4
|
+
`).map(p=>p.trim()).filter(p=>p&&p.length<5e3);return $(f)}).filter(l=>l.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 O(e,t,o){let n=t.replace(/\/+$/,"")+`/details/${o}/`;await e.goto(n,{waitUntil:"domcontentloaded",timeout:6e4}),await d(3e3);let s=e.url();if(/\/(login|authwall|checkpoint)/i.test(s))throw new Error(`Session expired \u2014 bounced to ${s} when fetching /details/${o}/`);if(!s.toLowerCase().includes(`/details/${o.toLowerCase()}/`))throw new Error(`Section /details/${o}/ not available on profile (landed on ${s})`);for(let u=0;u<4;u++)await e.evaluate(()=>window.scrollTo(0,document.body.scrollHeight)),await d(700);await d(800);let m=0;for(;m<5&&await e.evaluate(()=>{for(let h of document.querySelectorAll("button"))if(/^Load more$/i.test((h.textContent||"").trim()))return h.click(),!0;return!1});)m++,await d(1300),await e.evaluate(()=>window.scrollTo(0,document.body.scrollHeight)),await d(600)}async function B(e,t,o,n){try{return await n()}catch(s){let m=s.message||"";if(/session expired|bounced to|authwall|checkpoint/i.test(m))throw s;return/not available on profile/i.test(m)?(console.log(` (${t}: section not on profile \u2014 skipped)`),[]):(console.error(`${o} failed for ${e}: ${m.slice(0,120)}`),[])}}async function Ge(e,t){return B(t,"featured","extractFeaturedFromDetails",async()=>(await O(e,t,"featured"),await e.evaluate(`(() => {
|
|
5
5
|
${N}
|
|
6
6
|
|
|
7
7
|
const main = document.querySelector('main');
|
|
@@ -42,7 +42,7 @@ var Ie=Object.defineProperty;var de=(e,t)=>()=>(e&&(t=e(e=0)),t);var De=(e,t)=>{
|
|
|
42
42
|
if (e.caption && /^\xB7\\s*\\d+(?:st|nd|rd|th)\\+?$/i.test(e.caption.trim())) return false;
|
|
43
43
|
return true;
|
|
44
44
|
});
|
|
45
|
-
})()`)))}async function
|
|
45
|
+
})()`)))}async function Ke(e,t){return B(t,"education","extractEducationFromDetails",async()=>(await O(e,t,"education"),await e.evaluate(`(() => {
|
|
46
46
|
${N}
|
|
47
47
|
|
|
48
48
|
const main = document.querySelector('main');
|
|
@@ -79,7 +79,7 @@ var Ie=Object.defineProperty;var de=(e,t)=>()=>(e&&(t=e(e=0)),t);var De=(e,t)=>{
|
|
|
79
79
|
if (e.degree && /^\xB7\\s*\\d+(?:st|nd|rd|th)\\+?$/i.test(e.degree.trim())) return false;
|
|
80
80
|
return true;
|
|
81
81
|
});
|
|
82
|
-
})()`)))}async function
|
|
82
|
+
})()`)))}async function Qe(e,t){return B(t,"skills","extractSkillsFromDetails",async()=>(await O(e,t,"skills"),await e.evaluate(`(() => {
|
|
83
83
|
${N}
|
|
84
84
|
|
|
85
85
|
const main = document.querySelector('main');
|
|
@@ -111,7 +111,7 @@ var Ie=Object.defineProperty;var de=(e,t)=>()=>(e&&(t=e(e=0)),t);var De=(e,t)=>{
|
|
|
111
111
|
skills.push({ name, endorsements, _raw: lines });
|
|
112
112
|
}
|
|
113
113
|
return skills;
|
|
114
|
-
})()`)))}async function
|
|
114
|
+
})()`)))}async function ze(e,t){return B(t,"certifications","extractCertificationsFromDetails",async()=>(await O(e,t,"certifications"),await e.evaluate(`(() => {
|
|
115
115
|
${N}
|
|
116
116
|
|
|
117
117
|
const main = document.querySelector('main');
|
|
@@ -235,10 +235,10 @@ function findEntryContainer(root) {
|
|
|
235
235
|
candidates.sort((a, b) => b.count - a.count || b.totalText - a.totalText);
|
|
236
236
|
return candidates[0]?.el;
|
|
237
237
|
}
|
|
238
|
-
`});import je from"puppeteer";import{addExtra as Oe}from"puppeteer-extra";import Ne from"puppeteer-extra-plugin-stealth";j();import{getPluginConfig as Te}from"@ilivemylife/graph-sdk";var Me=await Te();for(let[e,t]of Object.entries(Me))process.env[e]===void 0&&(process.env[e]=t);var Le=process.env.LINKEDIN_NAME,F=Object.freeze({DRAFT:"draft",CONFIRMED:"confirmed",REJECTED:"rejected",NEEDS_REVIEW:"needs_review",SENT:"sent"}),lt=Object.freeze(new Set([F.DRAFT,F.CONFIRMED,F.NEEDS_REVIEW])),Fe=Object.freeze(new Set([F.DRAFT,F.CONFIRMED,F.REJECTED,F.NEEDS_REVIEW]));import fe from"fs";import qe from"path";var
|
|
238
|
+
`});import je from"puppeteer";import{addExtra as Oe}from"puppeteer-extra";import Ne from"puppeteer-extra-plugin-stealth";j();import{getPluginConfig as Te}from"@ilivemylife/graph-sdk";var Me=await Te();for(let[e,t]of Object.entries(Me))process.env[e]===void 0&&(process.env[e]=t);var Le=process.env.LINKEDIN_NAME,F=Object.freeze({DRAFT:"draft",CONFIRMED:"confirmed",REJECTED:"rejected",NEEDS_REVIEW:"needs_review",SENT:"sent"}),lt=Object.freeze(new Set([F.DRAFT,F.CONFIRMED,F.NEEDS_REVIEW])),Fe=Object.freeze(new Set([F.DRAFT,F.CONFIRMED,F.REJECTED,F.NEEDS_REVIEW]));import fe from"fs";import qe from"path";var Re="cookies.json";function Pe(){let e=process.env.ILML_SCOPE_DIR;return e?qe.join(e,"plugins-state","linkedin","cookies.json"):Re}function pe(){let e=Pe();if(!fe.existsSync(e))throw new Error(`Cookies not found at ${e}. Run: ilml linkedin login`);let t;try{t=fe.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}var he=Oe(je);he.use(Ne());async function ge(){let e=await he.launch({headless:!1,defaultViewport:null,protocolTimeout:3e5}),t=async()=>{console.log(`
|
|
239
239
|
Closing browser...`);try{await e.close()}catch{}process.exit(0)};process.on("SIGINT",t),process.on("SIGTERM",t);let o=await e.newPage();await o.evaluateOnNewDocument(()=>{Object.defineProperty(navigator,"webdriver",{get:()=>{}})});try{let n=pe();await o.setCookie(...n),console.log(" Browser launched, cookies loaded.")}catch(n){console.error(`
|
|
240
240
|
[!] ${n.message}
|
|
241
|
-
`),await e.close(),process.exit(1)}return{browser:e,page:o}}j();import"dotenv/config";import*as g from"fs";import*as b from"path";import{fileURLToPath as Be}from"url";var He=Be(import.meta.url),
|
|
241
|
+
`),await e.close(),process.exit(1)}return{browser:e,page:o}}j();import"dotenv/config";import*as g from"fs";import*as b from"path";import{fileURLToPath as Be}from"url";var He=Be(import.meta.url),Ve=b.dirname(b.dirname(He)),A=process.env.DATA_DIR?b.resolve(process.env.DATA_DIR,"collected-profiles"):b.join(Ve,"collected-profiles"),Ue=b.join(A,"people.json"),_t=b.join(A,"jobs.json"),Et=b.join(A,"quota.json"),At=b.join(A,"conversations.json"),$t=b.join(A,"sync-state.json"),It=b.join(A,"application-questions.json"),Dt=b.join(A,"profile-history");var W=null;function We(){g.existsSync(A)||g.mkdirSync(A,{recursive:!0})}function Je(e,t){if(!g.existsSync(e))return null;try{let o=JSON.parse(g.readFileSync(e,"utf-8"));try{g.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(g.existsSync(n))try{let s=JSON.parse(g.readFileSync(n,"utf-8"));console.error(` Restored from ${n}`);try{g.copyFileSync(n,e)}catch{}return s}catch{console.error(" Backup also corrupted!")}console.error(` Starting with empty data. Old file preserved as ${e}.corrupted`);try{g.copyFileSync(e,e+".corrupted")}catch{}return null}}function Q(){return W||(We(),W=Je(Ue,"people.json")||{},W)}async function Xe(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: npm run login");console.log(` Off-track: ${o.slice(0,80)}`),console.log(" Navigating back to profile..."),await e.goto(t,{waitUntil:"domcontentloaded",timeout:6e4}),await d(3e3);let n=e.url();return n.includes("/in/")||console.log(` Recovery failed \u2014 still on: ${n.slice(0,80)}`),!0}async function be(e,t,o={}){let{extractExperience:n=!1,extractFeatured:s=!1,extractEducation:m=!1,extractSkills:u=!1,extractRecommendations:h=!1,extractCertifications:D=!1}=o;try{await e.goto(t,{waitUntil:"domcontentloaded",timeout:6e4}),await d(3e3),await me(e)&&(console.log(" Dismissed Premium overlay"),await d(1500),await Xe(e,t));let l=0;for(let y=0;y<2;y++){let q=await e.evaluate(()=>document.body.scrollHeight);for(let L=1;L<=6;L++)await e.evaluate(k=>window.scrollTo(0,k),q*L/6),await d(900);let E=await e.evaluate(()=>document.body.scrollHeight);if(E===l)break;l=E}await d(800);let S=await e.evaluate(()=>{let y=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"]),q=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"]),E=i=>{let r=(i||"").trim().replace(/^[·•]\s*/,"");return/^(1st|2nd|3rd|she\/her|he\/him|they\/them|their\/them)$/i.test(r)},L={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 L;let Y=[...Array.from(k.querySelectorAll("h1")),...Array.from(k.querySelectorAll("h2"))],a=Array.from(k.querySelectorAll("h2")),C=Y.find(i=>{let r=i.textContent&&i.textContent.trim(),c=(r||"").replace(/\s*\(\d+\)$/,"").trim();return r&&r.length>1&&r.length<100&&!y.has(c)&&!q.has(c)}),R=C?C.textContent.trim():null,v=C?C.parentElement:null;for(;v&&v!==k&&!Array.from(v.children).filter(r=>r.tagName==="P").some(r=>{let c=r.textContent&&r.textContent.trim();return c&&c.length>5&&!E(c)});)v=v.parentElement;let H=null,Z=null,X=null,G=null;if(v&&v!==k){let i=Array.from(v.children).filter(c=>c.tagName==="P").filter(c=>{let I=c.textContent&&c.textContent.trim();return I&&I.length>5&&!E(I)});if(i[0]&&(H=i[0].textContent.trim()),i[1]){let c=i[1].textContent.trim().split(/\s*[·•]\s*/);Z=c[0]||null,c.length>1&&(X=c.slice(1).join(" \xB7 "))}let r=Array.from(v.children).filter(c=>c.tagName==="DIV");for(let c=r.length-1;c>=0;c--){let I=r[c].textContent&&r[c].textContent.trim();if(I&&!(R&&I.startsWith(R))&&(G=I.split(/[·•]/)[0].trim(),G))break}}let ee=null;if(v){let i=v.querySelectorAll("p, span, div");for(let r of i){let c=(r.textContent||"").trim().replace(/^[·•]\s*/,"");if(c==="1st"||c==="2nd"||c==="3rd"){ee=c;break}}}let ke=!!k.querySelector('svg[data-test-icon="verified"], [aria-label*="Verified" i]'),te=document.querySelector('main img[width="200"], button img[alt*="photo" i]'),ve=te&&te.src||null,V=k.textContent||"",ne=null,oe=V.match(/(\d+\+?)\s+connections\b/i);oe&&(ne=oe[1]+" connections");let U=null,ie=V.match(/(\d+)\s+mutual connection/i),se=V.match(/and\s+(\d+)\s+other mutual/i),xe=V.match(/([A-Z][\w'-]+(?:\s+[A-Z][\w'-]+)?)\s+and\s+([A-Z][\w'-]+(?:\s+[A-Z][\w'-]+)?)\s+are\s+mutual/);ie?U=parseInt(ie[1],10):se?U=2+parseInt(se[1],10):xe&&(U=2);let re=null,le=null,ae=a.find(i=>i.textContent&&i.textContent.trim()==="About");if(ae){let i=ae.parentElement;for(;i&&i.tagName!=="SECTION";)i=i.parentElement;if(i){let r=(i.textContent||"").trim();r=r.replace(/^About\s*/,"").trim();let c=r.match(/Top skills\s*(.+?)$/i);if(c){let K=c[1].split(/\b(?:see (?:more|less)|show (?:more|less))\b/i)[0].trim().split(/\s*[·•]\s*/).map($e=>$e.trim()).filter(Boolean);K.length>0&&K.length<10&&(le=K)}r=r.split(/\n?…\s*more/i)[0].trim(),r=r.split(/\n?\.\.\.\s*more/i)[0].trim(),r=r.split(/\bTop skills\b/i)[0].trim(),r=r.split(/\b(?:see (?:more|less)|show (?:more|less))\b/i)[0].trim(),re=r||null}}let ce=null,ue=a.find(i=>i.textContent&&i.textContent.trim()==="Activity");if(ue){let i=ue.parentElement;for(;i&&i.tagName!=="SECTION";)i=i.parentElement;if(i){let r=(i.textContent||"").slice(0,200).match(/([\d,]+)\s+followers/i);r&&(ce=parseInt(r[1].replace(/,/g,""),10))}}let _e=[...new Set(a.map(i=>(i.textContent||"").trim().replace(/\s*\(\d+\)$/,"").trim()).filter(i=>i&&i.length>1&&i.length<60&&i!==R).filter(i=>y.has(i)))],P=document.body.textContent||"",Ee=/#OpenToWork\b/i.test(P)||/\bOpen to work\b/.test(P),Ae=/#Hiring\b/i.test(P)||/\bI'?m hiring\b/i.test(P)||/\bWe are hiring\b/i.test(P);return{name:R,title:H,location:G,currentCompany:Z,currentSchool:X,connectionDegree:ee,hasVerifiedBadge:ke,photoUrl:ve,connectionsLevel:ne,mutualConnectionsCount:U,followers:ce,openToWork:Ee,hiring:Ae,about:re,topSkills:le,availableSections:_e}});console.log(`Viewed: ${S.name} - ${S.title}`);let f=null,p=null,w=null,x=null,T=null,M=null;if(n||s||m||u||h||D){let y=await Promise.resolve().then(()=>(ye(),we));n&&(f=await y.extractExperienceFromDetails(e,t),console.log(` + Experience: ${f.length} entries`)),s&&(p=await y.extractFeaturedFromDetails(e,t),console.log(` + Featured: ${p.length} entries`)),m&&(w=await y.extractEducationFromDetails(e,t),console.log(` + Education: ${w.length} entries`)),u&&(x=await y.extractSkillsFromDetails(e,t),console.log(` + Skills: ${x.length} entries`)),h&&(T=await y.extractRecommendationsFromDetails(e,t),console.log(` + Recommendations: ${T.length} entries`)),D&&(M=await y.extractCertificationsFromDetails(e,t),console.log(` + Certifications: ${M.length} entries`))}return{...S,profileUrl:t,...f!==null&&{experience:f},...p!==null&&{featured:p},...w!==null&&{education:w},...x!==null&&{skillsFull:x},...T!==null&&{recommendations:T},...M!==null&&{certifications:M}}}catch($){console.error(`Error viewing profile ${t}:`,$.message);try{await e.goto("about:blank",{timeout:1e4}),await d(1e3)}catch{}if(/session expired|bounced to|authwall|checkpoint|redirected to login/i.test($.message||""))throw $;return null}}j();var z=process.argv.slice(2),Se=z.filter(e=>e.startsWith("http")),Ce=parseInt(z.find(e=>e.startsWith("--pick="))?.split("=")[1]||"0"),et=z.includes("--has-about");function tt(e,t){return[...e].sort(()=>Math.random()-.5).slice(0,t)}function nt(){if(Se.length>0)return Se.map(t=>({url:t,name:"(arg)"}));if(Ce>0){let t=Q(),o=Object.entries(t).filter(([n])=>n.startsWith("http")).filter(([,n])=>et?!!(n.about&&n.about.trim()):!n.about);return tt(o,Ce).map(([n,s])=>({url:n,name:s.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,t,o=80){if(t==null){console.log(` ${e.padEnd(20)} (null)`);return}let n=String(t),s=n.length>o?n.slice(0,o)+`\u2026 (+${n.length-o} chars)`:n;console.log(` ${e.padEnd(20)} ${s} [len=${n.length}]`)}async function ot(){let e=nt();console.log(`
|
|
242
242
|
About-selector verification \u2014 ${e.length} profile(s)
|
|
243
243
|
`);let{browser:t,page:o}=await ge();try{for(let n=0;n<e.length;n++){let{url:s,name:m}=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}] ${m}`),console.log(` ${s}`),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 u=await be(o,s);if(!u){console.log(` \u26A0 viewProfile returned null \u2014 selector failed entirely
|
|
244
244
|
`);continue}_("name",u.name),_("title",u.title),_("location",u.location),_("company",u.company),_("connectionDegree",u.connectionDegree),_("connectionsCount",u.connectionsCount),_("mutualConnections",u.mutualConnections),_("hasVerifiedBadge",u.hasVerifiedBadge),_("photoUrl",u.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"),_("about",u.about,200);let h=[];u.name||h.push("NAME MISSING"),u.title||h.push("TITLE MISSING"),u.location||h.push("LOCATION MISSING"),u.about||h.push("ABOUT MISSING"),console.log(`
|
package/dist/version.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{readFileSync as e}from"fs";import{fileURLToPath as r}from"url";function i(){return"1.14.0";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/dist/viewers.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
var Ne=Object.defineProperty;var G=(e,o)=>{for(var n in o)Ne(e,n,{get:o[n],enumerable:!0})};import"dotenv/config";import*as E from"fs";import*as L from"path";import{fileURLToPath as to}from"url";import Ve from"puppeteer";import{addExtra as We}from"puppeteer-extra";import He from"puppeteer-extra-plugin-stealth";async function V(e){let o=Math.floor(Math.random()*2001),n=e+o;return new Promise(t=>setTimeout(t,n))}import{getPluginConfig as Le}from"@ilivemylife/graph-sdk";var Pe=await Le();for(let[e,o]of Object.entries(Pe))process.env[e]===void 0&&(process.env[e]=o);var Me=process.env.LINKEDIN_NAME,
|
|
1
|
+
var Ne=Object.defineProperty;var G=(e,o)=>{for(var n in o)Ne(e,n,{get:o[n],enumerable:!0})};import"dotenv/config";import*as E from"fs";import*as L from"path";import{fileURLToPath as to}from"url";import Ve from"puppeteer";import{addExtra as We}from"puppeteer-extra";import He from"puppeteer-extra-plugin-stealth";async function V(e){let o=Math.floor(Math.random()*2001),n=e+o;return new Promise(t=>setTimeout(t,n))}import{getPluginConfig as Le}from"@ilivemylife/graph-sdk";var Pe=await Le();for(let[e,o]of Object.entries(Pe))process.env[e]===void 0&&(process.env[e]=o);var Me=process.env.LINKEDIN_NAME,R=Object.freeze({DRAFT:"draft",CONFIRMED:"confirmed",REJECTED:"rejected",NEEDS_REVIEW:"needs_review",SENT:"sent"}),uo=Object.freeze(new Set([R.DRAFT,R.CONFIRMED,R.NEEDS_REVIEW])),qe=Object.freeze(new Set([R.DRAFT,R.CONFIRMED,R.REJECTED,R.NEEDS_REVIEW]));import ie from"fs";import Ue from"path";var Be="cookies.json";function Je(){let e=process.env.ILML_SCOPE_DIR;return e?Ue.join(e,"plugins-state","linkedin","cookies.json"):Be}function ce(){let e=Je();if(!ie.existsSync(e))throw new Error(`Cookies not found at ${e}. Run: ilml linkedin login`);let o;try{o=ie.readFileSync(e,"utf-8")}catch(t){throw new Error(`Failed to read cookies at ${e}: ${t.message}. Run: ilml linkedin login`)}let n;try{n=JSON.parse(o)}catch{throw new Error(`Cookies file at ${e} is corrupted (invalid JSON). Run: ilml linkedin login`)}if(!Array.isArray(n)||n.length===0)throw new Error(`Cookies file at ${e} is empty or not an array. Run: ilml linkedin login`);return n}var le=We(Ve);le.use(He());async function ae(){let e=await le.launch({headless:!1,defaultViewport:null,protocolTimeout:3e5}),o=async()=>{console.log(`
|
|
2
2
|
Closing browser...`);try{await e.close()}catch{}process.exit(0)};process.on("SIGINT",o),process.on("SIGTERM",o);let n=await e.newPage();await n.evaluateOnNewDocument(()=>{Object.defineProperty(navigator,"webdriver",{get:()=>{}})});try{let t=ce();await n.setCookie(...t),console.log(" Browser launched, cookies loaded.")}catch(t){console.error(`
|
|
3
3
|
[!] ${t.message}
|
|
4
|
-
`),await e.close(),process.exit(1)}return{browser:e,page:n}}import"dotenv/config";import*as h from"fs";import*as g from"path";import{fileURLToPath as Ge}from"url";var Ye=Ge(import.meta.url),ze=g.dirname(g.dirname(Ye)),C=process.env.DATA_DIR?g.resolve(process.env.DATA_DIR,"collected-profiles"):g.join(ze,"collected-profiles"),Ke=g.join(C,"people.json"),Io=g.join(C,"jobs.json"),
|
|
4
|
+
`),await e.close(),process.exit(1)}return{browser:e,page:n}}import"dotenv/config";import*as h from"fs";import*as g from"path";import{fileURLToPath as Ge}from"url";var Ye=Ge(import.meta.url),ze=g.dirname(g.dirname(Ye)),C=process.env.DATA_DIR?g.resolve(process.env.DATA_DIR,"collected-profiles"):g.join(ze,"collected-profiles"),Ke=g.join(C,"people.json"),Io=g.join(C,"jobs.json"),xo=g.join(C,"quota.json"),Ro=g.join(C,"conversations.json"),Oo=g.join(C,"sync-state.json"),_o=g.join(C,"application-questions.json"),jo=g.join(C,"profile-history");var U=null,Qe=null,Xe=null,Ze=null,et=null,tt=null;function ue(){U=null,Qe=null,Xe=null,Ze=null,et=null,tt=null}function ot(){h.existsSync(C)||h.mkdirSync(C,{recursive:!0})}function nt(e,o){if(!h.existsSync(e))return null;try{let n=JSON.parse(h.readFileSync(e,"utf-8"));try{h.copyFileSync(e,e+".backup")}catch{}return n}catch(n){console.error(`[!] ${o} is corrupted: ${n.message}`),console.error(` File: ${g.resolve(e)}`);let t=e+".backup";if(h.existsSync(t))try{let s=JSON.parse(h.readFileSync(t,"utf-8"));console.error(` Restored from ${t}`);try{h.copyFileSync(t,e)}catch{}return s}catch{console.error(" Backup also corrupted!")}console.error(` Starting with empty data. Old file preserved as ${e}.corrupted`);try{h.copyFileSync(e,e+".corrupted")}catch{}return null}}function fe(){return U||(ot(),U=nt(Ke,"people.json")||{},U)}import"dotenv/config";import*as _ from"fs";import*as S from"path";import{fileURLToPath as st}from"url";var rt=st(import.meta.url),it=S.dirname(S.dirname(rt)),J=process.env.DATA_DIR?S.resolve(process.env.DATA_DIR,"market-research"):S.join(it,"market-research"),Po=S.join(J,"scouted-jobs.json"),ct=S.join(J,"discovered-people.json"),lt=S.join(J,"visit-log.json"),Mo=S.join(J,"companies.json"),qo=S.join(J,"job-decisions.json");function pe(e){try{if(_.existsSync(e))return JSON.parse(_.readFileSync(e,"utf-8"))}catch{}return null}function de(){at=null,ut=null,B=null,ft=null,O=null}var at=null;var ut=null;var B=null;function me(){return B||(B=pe(ct)||{},B)}var ft=null;var O=null;function he(){return O||(O=pe(lt)||{visits:[]},Array.isArray(O.visits)||(O.visits=[]),O)}async function ge(e){await e.goto("https://www.linkedin.com/analytics/profile-views/",{waitUntil:"domcontentloaded",timeout:6e4}),await V(5e3);for(let n=0;n<5;n++)await e.evaluate(()=>window.scrollBy(0,600)),await V(1e3);return await e.evaluate(()=>{let n=[],t=document.querySelectorAll('a[href*="/in/"]'),s=new Set;for(let r of t){let i=r.getAttribute("href")?.split("?")[0];if(!i||s.has(i))continue;s.add(i);let c=r;for(let p=0;p<8&&!(!c.parentElement||(c=c.parentElement,c.tagName==="LI"||c.classList?.contains("pvs-list__paged-list-wrapper")));p++);let l=c?.innerText?.trim()||"",u=l.split(`
|
|
5
5
|
`).map(p=>p.trim()).filter(Boolean),f="";for(let p of u)if(p.length>2&&p.length<60&&!p.includes("View")&&!p.includes("profile")&&!p.includes("\xB7")&&!p.startsWith("1st")&&!p.startsWith("2nd")&&!p.startsWith("3rd")){f=p;break}f||(f=r.textContent?.trim()?.split(`
|
|
6
6
|
`)[0]?.trim()||""),f=f.replace(/View .+?'s profile$/i,"").trim();let m=l.match(/·\s*(1st|2nd|3rd)/)?.[1]||null,v="",P=u.indexOf(f);if(P>=0&&P+1<u.length){let p=u[P+1];!p.includes("\xB7")&&!p.startsWith("1st")&&!p.startsWith("2nd")&&(v=p)}f&&f.length>1&&n.push({profileUrl:i.startsWith("/")?"https://www.linkedin.com"+i:i,name:f,title:v||null,connectionDegree:m})}return n})}function ye(e){let o=fe(),n=he(),t=me(),s=new Map;for(let r of n.visits||[]){let i=s.get(r.profileUrl);(!i||r.date>i)&&s.set(r.profileUrl,r.date)}return e.map(r=>{let i=o[r.profileUrl],c=t[r.profileUrl],l=s.get(r.profileUrl);return{...r,isConnected:i?.connectionStatus==="connected",isPending:i?.connectionStatus==="pending"||i?.connectionStatus==="sent_without_note",isInPeopleDb:!!i,isDiscovered:!!c,weVisitedThem:!!l,ourVisitDate:l||null,category:i?.category||c?.role||null}})}import"dotenv/config";import*as b from"fs";import*as I from"path";import{fileURLToPath as yt}from"url";import"dotenv/config";import{createGraphClient as pt,resolveToken as dt}from"@ilivemylife/graph-sdk";import{config as mt}from"dotenv";mt();var Se=dt()||process.env.TOKEN;Se||(console.error(`
|
|
7
7
|
[FATAL] No iLiveMyLife token found.`),console.error(" The bot uses Lifebot AI to decide which jobs to apply for"),console.error(` and how to fill application forms. It cannot work without it.
|
|
@@ -10,7 +10,7 @@ var Ne=Object.defineProperty;var G=(e,o)=>{for(var n in o)Ne(e,n,{get:o[n],enume
|
|
|
10
10
|
`),process.exit(1));var we=pt({token:Se});function ht(e){if(!e||e<1e3)return`${e||0}ms`;let o=Math.round(e/1e3);if(o<60)return`${o}s`;let n=Math.floor(o/60),t=o%60;return t?`${n}m ${t}s`:`${n}m`}function gt({scriptName:e,mode:o,status:n,error:t,duration:s}){let r=n==="error"?"\u2717":n==="quota-reached"?"\u26A0":"\u2713",i=new Date().toISOString().slice(0,16).replace("T"," "),c=s?` ${ht(s)}`:"",l=n==="error"?`error${t?`: ${String(t).slice(0,80)}`:""}`:n==="quota-reached"?"daily quota reached":"done",u=o&&o!=="default"?` (${o})`:"";return`${r} ${e}${u} \u2014 ${l} \u2014 ${i}${c}`}async function ve({scriptName:e,mode:o,status:n,summaryLines:t,error:s,duration:r}){let i=process.env.NODE_RUN_REPORTS;if(!i||!Array.isArray(t)||t.length===0)return;let l=`**${gt({scriptName:e,mode:o,status:n,error:s,duration:r})}**
|
|
11
11
|
|
|
12
12
|
${t.join(`
|
|
13
|
-
`)}`;try{await we.addMessage(i,l)}catch(u){console.error(`[runReport] Failed to post end-of-session message: ${u.message}`)}}var St=yt(import.meta.url),wt=I.dirname(I.dirname(St)),Y=process.env.DATA_DIR?I.resolve(process.env.DATA_DIR,"collected-profiles"):I.join(wt,"collected-profiles"),z=I.join(Y,"run-log.json");function vt(e,o){let n=JSON.stringify(o,null,2),t=e+".tmp";b.writeFileSync(t,n);for(let s=0;s<3;s++)try{b.renameSync(t,e);return}catch(r){if(s<2&&(r.code==="EPERM"||r.code==="EBUSY")){let i=Date.now();for(;Date.now()-i<200;);continue}throw r}}function be(){try{if(!b.existsSync(z))return{runs:[]};let e=JSON.parse(b.readFileSync(z,"utf-8"));return!e.runs||!Array.isArray(e.runs)?{runs:[]}:e}catch{return{runs:[]}}}function ke(e){b.existsSync(Y)||b.mkdirSync(Y,{recursive:!0});try{vt(z,e)}catch(o){console.error(`[!] Failed to save run-log.json: ${o.message}`)}}function $e(e,o="default"){let n=be();for(let i of n.runs)i.status==="running"&&(i.status="interrupted",i.result="interrupted",i.timestamp&&(i.duration=Date.now()-new Date(i.timestamp).getTime()));let t=new Date,s={timestamp:t.toISOString(),script:e,mode:o,status:"running",result:null,duration:null,stats:null,error:null};n.runs.push(s);let r=Date.now()-30*24*60*60*1e3;return n.runs=n.runs.filter(i=>new Date(i.timestamp).getTime()>r),ke(n),{script:e,mode:o,startTime:t.getTime(),runIndex:n.runs.length-1}}function K(e,o,n=null,t=null){if(!e)return;let s=null;try{let i=be(),c=i.runs[e.runIndex];if(c&&c.status==="running"&&c.script===e.script)c.status=o,c.result=o,c.duration=Date.now()-e.startTime,c.stats=n,c.error=t,s=c.duration;else{let l=i.runs.find(u=>u.script===e.script&&u.status==="running"&&u.timestamp===new Date(e.startTime).toISOString());l&&(l.status=o,l.result=o,l.duration=Date.now()-e.startTime,l.stats=n,l.error=t,s=l.duration)}ke(i)}catch(i){console.error(`[runLog] Failed to end run: ${i.message}`)}let r=n&&Array.isArray(n.summaryLines)?n.summaryLines:null;if(r&&r.length>0&&process.env.NODE_RUN_REPORTS){let i=o==="error"?"error":n&&n.dailyLimitHit?"quota-reached":"done";ve({scriptName:e.script,mode:e.mode,status:i,summaryLines:r,error:t,duration:s??Date.now()-e.startTime}).catch(c=>console.error(`[runLog] postRunReport failed: ${c.message}`))}}import"dotenv/config";import*as a from"fs";import*as ne from"os";import*as d from"path";import{fileURLToPath as Pt}from"url";var Z={};G(Z,{NAME:()=>kt,VERSION:()=>bt,up:()=>Et});import*as k from"fs";import*as W from"path";var bt=1,kt="normalize-jobs-and-questions",$t=["description","techStack","jobTags","seniority","employmentType","industries","jobFunction","postedTime","applicantCount","companySize","workplaceType","contractType","isEasyApply","applyType","companyUrl","scoutedAt","lastSeen"],At=50;function Q(e){return k.existsSync(e)?JSON.parse(k.readFileSync(e,"utf-8")):null}function X(e,o){let n=JSON.stringify(o,null,2),t=e+".tmp";try{k.writeFileSync(t,n)}catch(s){try{k.unlinkSync(t)}catch{}throw s}try{k.renameSync(t,e)}catch(s){try{k.unlinkSync(t)}catch{}if(s.code==="EPERM"||s.code==="EBUSY"){let r=new Error(`File locked: ${e} (${s.code}). On Windows usually antivirus / file explorer / open editor. Close it and re-run.`);throw r.code=s.code,r}throw s}}async function Et(e,o){let n=W.join(e,"jobs.json"),t=W.join(o,"scouted-jobs.json"),s=W.join(e,"application-questions.json"),r=Q(n)||{},i=Q(t)||{},c=0,l=[];for(let f of Object.keys(r)){if(!i[f])continue;let w=i[f],m=r[f];for(let v of $t)w[v]!=null&&m[v]==null&&(m[v]=w[v]);l.push(f),c++}c>0?(X(n,r),console.log(` Phase 1: Merged ${c} scouted jobs into jobs.json`)):console.log(" Phase 1: No overlapping jobs to merge");let u=Q(s);if(u&&Array.isArray(u.applications)){let f={},w=0;for(let m of u.applications){let v=m.jobId||"unknown";f[v]||(f[v]={applications:[]});let P=(m.questions||[]).map(p=>{let M={type:p.type||"input",question:p.question||"",answer:p.answer||""};return p.options&&(p.options.length<=At?M.options=p.options:(M.optionsOmitted=!0,w++)),p.page&&(M.page=p.page),p.wasPreFilled&&(M.wasPreFilled=!0),p.wasRetry&&(M.wasRetry=!0),M});f[v].applications.push({applicationId:m.applicationId,date:m.date,result:m.result||"submitted",formPages:m.formPages||null,duration:m.duration||null,retries:m.retries||0,failedField:m.failedField||null,questionCount:m.questionCount||P.length,questions:P})}X(s,f),console.log(` Phase 2: Rekeyed ${u.applications.length} applications by jobId (${Object.keys(f).length} unique jobs)`),w>0&&console.log(` Phase 2: Stripped bloated options from ${w} questions`)}else u&&!u.applications?console.log(" Phase 2: application-questions.json already in keyed format"):console.log(" Phase 2: No application-questions.json found");if(l.length>0){for(let f of l)delete i[f];X(t,i),console.log(` Phase 3: Removed ${l.length} merged entries from scouted-jobs.json (${Object.keys(i).length} remaining)`)}else console.log(" Phase 3: No entries to remove from scouted-jobs.json")}var ee={};G(ee,{NAME:()=>Ct,VERSION:()=>Tt,up:()=>
|
|
13
|
+
`)}`;try{await we.addMessage(i,l)}catch(u){console.error(`[runReport] Failed to post end-of-session message: ${u.message}`)}}var St=yt(import.meta.url),wt=I.dirname(I.dirname(St)),Y=process.env.DATA_DIR?I.resolve(process.env.DATA_DIR,"collected-profiles"):I.join(wt,"collected-profiles"),z=I.join(Y,"run-log.json");function vt(e,o){let n=JSON.stringify(o,null,2),t=e+".tmp";b.writeFileSync(t,n);for(let s=0;s<3;s++)try{b.renameSync(t,e);return}catch(r){if(s<2&&(r.code==="EPERM"||r.code==="EBUSY")){let i=Date.now();for(;Date.now()-i<200;);continue}throw r}}function be(){try{if(!b.existsSync(z))return{runs:[]};let e=JSON.parse(b.readFileSync(z,"utf-8"));return!e.runs||!Array.isArray(e.runs)?{runs:[]}:e}catch{return{runs:[]}}}function ke(e){b.existsSync(Y)||b.mkdirSync(Y,{recursive:!0});try{vt(z,e)}catch(o){console.error(`[!] Failed to save run-log.json: ${o.message}`)}}function $e(e,o="default"){let n=be();for(let i of n.runs)i.status==="running"&&(i.status="interrupted",i.result="interrupted",i.timestamp&&(i.duration=Date.now()-new Date(i.timestamp).getTime()));let t=new Date,s={timestamp:t.toISOString(),script:e,mode:o,status:"running",result:null,duration:null,stats:null,error:null};n.runs.push(s);let r=Date.now()-30*24*60*60*1e3;return n.runs=n.runs.filter(i=>new Date(i.timestamp).getTime()>r),ke(n),{script:e,mode:o,startTime:t.getTime(),runIndex:n.runs.length-1}}function K(e,o,n=null,t=null){if(!e)return;let s=null;try{let i=be(),c=i.runs[e.runIndex];if(c&&c.status==="running"&&c.script===e.script)c.status=o,c.result=o,c.duration=Date.now()-e.startTime,c.stats=n,c.error=t,s=c.duration;else{let l=i.runs.find(u=>u.script===e.script&&u.status==="running"&&u.timestamp===new Date(e.startTime).toISOString());l&&(l.status=o,l.result=o,l.duration=Date.now()-e.startTime,l.stats=n,l.error=t,s=l.duration)}ke(i)}catch(i){console.error(`[runLog] Failed to end run: ${i.message}`)}let r=n&&Array.isArray(n.summaryLines)?n.summaryLines:null;if(r&&r.length>0&&process.env.NODE_RUN_REPORTS){let i=o==="error"?"error":n&&n.dailyLimitHit?"quota-reached":"done";ve({scriptName:e.script,mode:e.mode,status:i,summaryLines:r,error:t,duration:s??Date.now()-e.startTime}).catch(c=>console.error(`[runLog] postRunReport failed: ${c.message}`))}}import"dotenv/config";import*as a from"fs";import*as ne from"os";import*as d from"path";import{fileURLToPath as Pt}from"url";var Z={};G(Z,{NAME:()=>kt,VERSION:()=>bt,up:()=>Et});import*as k from"fs";import*as W from"path";var bt=1,kt="normalize-jobs-and-questions",$t=["description","techStack","jobTags","seniority","employmentType","industries","jobFunction","postedTime","applicantCount","companySize","workplaceType","contractType","isEasyApply","applyType","companyUrl","scoutedAt","lastSeen"],At=50;function Q(e){return k.existsSync(e)?JSON.parse(k.readFileSync(e,"utf-8")):null}function X(e,o){let n=JSON.stringify(o,null,2),t=e+".tmp";try{k.writeFileSync(t,n)}catch(s){try{k.unlinkSync(t)}catch{}throw s}try{k.renameSync(t,e)}catch(s){try{k.unlinkSync(t)}catch{}if(s.code==="EPERM"||s.code==="EBUSY"){let r=new Error(`File locked: ${e} (${s.code}). On Windows usually antivirus / file explorer / open editor. Close it and re-run.`);throw r.code=s.code,r}throw s}}async function Et(e,o){let n=W.join(e,"jobs.json"),t=W.join(o,"scouted-jobs.json"),s=W.join(e,"application-questions.json"),r=Q(n)||{},i=Q(t)||{},c=0,l=[];for(let f of Object.keys(r)){if(!i[f])continue;let w=i[f],m=r[f];for(let v of $t)w[v]!=null&&m[v]==null&&(m[v]=w[v]);l.push(f),c++}c>0?(X(n,r),console.log(` Phase 1: Merged ${c} scouted jobs into jobs.json`)):console.log(" Phase 1: No overlapping jobs to merge");let u=Q(s);if(u&&Array.isArray(u.applications)){let f={},w=0;for(let m of u.applications){let v=m.jobId||"unknown";f[v]||(f[v]={applications:[]});let P=(m.questions||[]).map(p=>{let M={type:p.type||"input",question:p.question||"",answer:p.answer||""};return p.options&&(p.options.length<=At?M.options=p.options:(M.optionsOmitted=!0,w++)),p.page&&(M.page=p.page),p.wasPreFilled&&(M.wasPreFilled=!0),p.wasRetry&&(M.wasRetry=!0),M});f[v].applications.push({applicationId:m.applicationId,date:m.date,result:m.result||"submitted",formPages:m.formPages||null,duration:m.duration||null,retries:m.retries||0,failedField:m.failedField||null,questionCount:m.questionCount||P.length,questions:P})}X(s,f),console.log(` Phase 2: Rekeyed ${u.applications.length} applications by jobId (${Object.keys(f).length} unique jobs)`),w>0&&console.log(` Phase 2: Stripped bloated options from ${w} questions`)}else u&&!u.applications?console.log(" Phase 2: application-questions.json already in keyed format"):console.log(" Phase 2: No application-questions.json found");if(l.length>0){for(let f of l)delete i[f];X(t,i),console.log(` Phase 3: Removed ${l.length} merged entries from scouted-jobs.json (${Object.keys(i).length} remaining)`)}else console.log(" Phase 3: No entries to remove from scouted-jobs.json")}var ee={};G(ee,{NAME:()=>Ct,VERSION:()=>Tt,up:()=>xt});import*as $ from"fs";import*as Ae from"path";var Tt=2,Ct="reset-unread-flags";function Dt(e){return $.existsSync(e)?JSON.parse($.readFileSync(e,"utf-8")):null}function It(e,o){let n=JSON.stringify(o,null,2),t=e+".tmp";try{$.writeFileSync(t,n)}catch(s){try{$.unlinkSync(t)}catch{}throw s}try{$.renameSync(t,e)}catch(s){try{$.unlinkSync(t)}catch{}if(s.code==="EPERM"||s.code==="EBUSY"){let r=new Error(`File locked: ${e} (${s.code}). On Windows usually antivirus / file explorer / open editor. Close it and re-run.`);throw r.code=s.code,r}throw s}}async function xt(e){let o=Ae.join(e,"conversations.json"),n=Dt(o);if(!n){console.log(" conversations.json not found \u2014 nothing to reset.");return}let t=0;for(let s of Object.keys(n))n[s].unread===!0&&(n[s].unread=!1,t++);if(t===0){console.log(" No stale unread flags found \u2014 nothing to do.");return}It(o,n),console.log(` Reset unread=false on ${t} conversations. Run 'ilml linkedin sync-all' to re-establish real unread state from LinkedIn.`)}var te={};G(te,{NAME:()=>Ot,VERSION:()=>Rt,up:()=>Lt});import*as A from"fs";import*as Ee from"path";var Rt=3,Ot="recompute-last-message-by",_t=new Set(["draft","confirmed","rejected","needs_review"]);function jt(e){return A.existsSync(e)?JSON.parse(A.readFileSync(e,"utf-8")):null}function Ft(e,o){let n=JSON.stringify(o,null,2),t=e+".tmp";try{A.writeFileSync(t,n)}catch(s){try{A.unlinkSync(t)}catch{}throw s}try{A.renameSync(t,e)}catch(s){try{A.unlinkSync(t)}catch{}if(s.code==="EPERM"||s.code==="EBUSY"){let r=new Error(`File locked: ${e} (${s.code}). On Windows usually antivirus / file explorer / open editor. Close it and re-run.`);throw r.code=s.code,r}throw s}}function Nt(e,o){if(!e||e.length===0)return"them";let n=e.filter(r=>!_t.has(r.status));if(n.length===0)return"them";let t=n.map(r=>({m:r,t:Date.parse(r.time)})).filter(r=>Number.isFinite(r.t)).sort((r,i)=>i.t-r.t).map(r=>r.m),s=t.length>0?t:[...n].reverse();for(let r of s){let i=r.sender;if(i===o)return"me";if(i!=="Unknown"&&i!=="")return"them"}return"them"}async function Lt(e){let o=Ee.join(e,"conversations.json"),n=jt(o);if(!n){console.log(" conversations.json not found \u2014 nothing to recompute.");return}let t=process.env.LINKEDIN_NAME;if(!t||typeof t!="string"||!t.trim())throw new Error(`LINKEDIN_NAME env var is not set. Migration v003 needs your display name to determine which messages are yours vs theirs. Set LINKEDIN_NAME in your .env (or run "ilml plugin config linkedin set LINKEDIN_NAME 'Your Name'") and retry.`);let s=0,r=0,i=0;for(let c of Object.keys(n)){let l=n[c];if(!l||typeof l!="object")continue;Object.prototype.hasOwnProperty.call(l,"_cardSynced")&&(delete l._cardSynced,r++);let u=Array.isArray(l.messages)?l.messages:[],f=Nt(u,t),w=f==="me"?"awaiting_reply":"unanswered_by_us";l.lastMessageBy!==f&&i++,l.lastMessageBy=f,l.conversationStatus=w,s++}if(s===0){console.log(" No conversations to recompute.");return}Ft(o,n),console.log(` Recomputed lastMessageBy + conversationStatus on ${s} conversations.`),console.log(` ${i} had stale lastMessageBy (now corrected).`),r>0&&console.log(` Cleaned ${r} leftover _cardSynced flags.`),console.log(" Re-run 'ilml linkedin enrich' if you want actionNeeded / summary / autoTags to also update.")}var Mt=Pt(import.meta.url),qt=d.dirname(Mt),Ut=d.dirname(d.dirname(qt));function Bt(e){return e&&(e==="~"?ne.homedir():e.startsWith("~/")||e.startsWith("~\\")?d.join(ne.homedir(),e.slice(2)):e)}var T=process.env.DATA_DIR?d.resolve(Bt(process.env.DATA_DIR)):Ut,F=d.join(T,"collected-profiles"),N=d.join(T,"market-research"),y=d.join(T,".schema-version"),Te=[d.join(F,".schema-version"),d.join(N,".schema-version")],x=3,D=[{version:1,module:Z},{version:2,module:ee},{version:3,module:te}];(function(){D.sort((t,s)=>t.version-s.version);let o=new Set;for(let t of D){if(!Number.isInteger(t.version)||t.version<1)throw new Error(`MIGRATION_REGISTRY: invalid version ${JSON.stringify(t.version)} (must be positive integer)`);if(o.has(t.version))throw new Error(`MIGRATION_REGISTRY: duplicate version v${t.version}`);o.add(t.version);let s=t.module;if(!s||typeof s.up!="function")throw new Error(`MIGRATION_REGISTRY: v${t.version} module is missing up()`);if(s.VERSION!==void 0&&s.VERSION!==t.version)throw new Error(`MIGRATION_REGISTRY: v${t.version} entry does not match module VERSION (${s.VERSION}). Drift between registry and module \u2014 pick one source of truth.`)}let n=D.length?D[D.length-1].version:0;if(n!==x)throw new Error(`MIGRATION_REGISTRY: highest version v${n} but CURRENT_SCHEMA_VERSION=${x}. Forgot to bump CURRENT_SCHEMA_VERSION when adding the new migration?`);for(let t=0;t<D.length;t++){let s=t+1;if(D[t].version!==s)throw new Error(`MIGRATION_REGISTRY: gap detected \u2014 expected v${s} at position ${t} but found v${D[t].version}`)}})();function xe(){process.env.DATA_DIR!==void 0&&process.env.DATA_DIR.trim()===""&&process.env.DATA_DIR.length>0&&(console.error(`
|
|
14
14
|
[FATAL] DATA_DIR is set to whitespace ("${process.env.DATA_DIR}"). Either unset it or set a real path.
|
|
15
15
|
`),process.exit(1));try{a.mkdirSync(T,{recursive:!0})}catch(o){o.code!=="EEXIST"&&(console.error(`
|
|
16
16
|
[FATAL] Cannot create DATA_DIR (${T}): ${o.message}`),console.error(` Fix the path or permissions and try again.
|
|
@@ -32,11 +32,11 @@ ${t.join(`
|
|
|
32
32
|
[FATAL] ${y} is not valid JSON: ${n.message}`),console.error(" Refusing to proceed \u2014 silently treating it as 'fresh install' would re-apply"),console.error(" every migration on top of already-migrated data and corrupt it."),console.error(' Repair the file (set it to {"version":N,"migrations":[...]} matching your real state)'),console.error(` or restore from the *.pre-vNNN.backup files if you have them.
|
|
33
33
|
`),process.exit(1)}let o=Ht(e);return o===null&&(console.error(`
|
|
34
34
|
[FATAL] ${y} has invalid structure:`),console.error(` ${JSON.stringify(e)?.slice(0,200)||"(unparseable)"}`),console.error(' Expected shape: { "version": <integer>, "migrations": [...] }'),console.error(` Repair the file or restore from a backup.
|
|
35
|
-
`),process.exit(1)),o}function
|
|
36
|
-
[FATAL] Your data schema is at v${o}, but this plugin build only understands up to v${
|
|
37
|
-
`),console.error(" Options:"),console.error(" A) Re-install the newer plugin version that produced this schema, then"),console.error(" (if you really want to downgrade) run 'rollback-data --confirm' first,"),console.error(" then re-install the older plugin version."),console.error(` B) Manually restore your data from the *.pre-vNNN.backup files matching v${
|
|
35
|
+
`),process.exit(1)),o}function Re(e){try{a.chmodSync(e,384)}catch{}}function Gt(e){if(xe(),a.existsSync(y))try{if(a.lstatSync(y).isSymbolicLink())throw new Error(`SCHEMA_FILE is a symbolic link: ${y}. Refusing to write through it (potential symlink attack). Remove the link and re-run.`)}catch(n){if(n.message?.includes("symbolic link"))throw n}let o=y+".tmp";try{a.writeFileSync(o,JSON.stringify(e,null,2)),Re(o),a.renameSync(o,y)}catch(n){try{a.unlinkSync(o)}catch{}throw n}}function Ie(e,o){if(!a.existsSync(e))return;let n=`.pre-v${String(o).padStart(3,"0")}.backup`,t=a.readdirSync(e).filter(c=>{let l=c.toLowerCase();if(l.includes(".backup")||l===".schema-version"||l.endsWith(".tmp"))return!1;try{return a.statSync(d.join(e,c)).isFile()}catch{return!1}}),s=0,r=0,i=[];for(let c of t){let l=d.join(e,c),u=d.join(e,c+n);if(a.existsSync(u)){r++;continue}try{_e(l,u),s++}catch(f){i.push({file:c,err:f.message})}}if(i.length>0){let c=new Error(`Failed to back up ${i.length} file(s) in ${d.basename(e)}/: `+i.map(l=>`${l.file} (${l.err})`).join(", "));throw c.code="BACKUP_INCOMPLETE",c}if(s>0||r>0){let c=r>0?` (${r} pre-existing backup(s) preserved)`:"";console.log(` Backed up ${s} files in ${d.basename(e)}/ (${n})${c}`)}}var oe=!1;async function Oe(){if(oe)return;xe(),Xt();let o=De().version;if(o===x){oe=!0;return}Vt(),Jt(),process.on("exit",se),o>x&&(console.error(`
|
|
36
|
+
[FATAL] Your data schema is at v${o}, but this plugin build only understands up to v${x}.`),console.error(" This usually means you downgraded the plugin without rolling back data."),console.error(` Operating on the newer-format data with this build would risk corrupting it.
|
|
37
|
+
`),console.error(" Options:"),console.error(" A) Re-install the newer plugin version that produced this schema, then"),console.error(" (if you really want to downgrade) run 'rollback-data --confirm' first,"),console.error(" then re-install the older plugin version."),console.error(` B) Manually restore your data from the *.pre-vNNN.backup files matching v${x}.
|
|
38
38
|
`),process.exit(1)),console.log(`
|
|
39
|
-
\u{1F4E6} Schema migration: v${o} \u2192 v${
|
|
39
|
+
\u{1F4E6} Schema migration: v${o} \u2192 v${x}`);let n=Zt();if(!n.ok){console.error(`
|
|
40
40
|
[FATAL] Cannot migrate \u2014 current data is corrupted:`);for(let s of n.broken)console.error(` ${s.dir}/${s.file}: ${s.err}`);console.error(`
|
|
41
41
|
Migration ABORTED. No backups taken, no files modified.`),console.error(" Repair the file(s) above (restore from your own backup, or fix manually)"),console.error(` and try again. To inspect: 'ilml linkedin migrate-status'.
|
|
42
42
|
`),process.exit(1)}let t=D.filter(s=>s.version>o);for(let s of t){console.log(`
|
|
@@ -49,8 +49,8 @@ ${t.join(`
|
|
|
49
49
|
`)}process.exit(1)}try{let l=new Date().toISOString(),u={version:s.version,name:c.NAME||r,appliedAt:l},f=De();f.version=s.version,f.migratedAt=l,f.migrations=f.migrations||[],f.migrations.push(u),Gt(f)}catch(l){console.error(`
|
|
50
50
|
[FATAL] Migration ${r} applied successfully but schema version file could NOT be updated: ${l.message}`),console.error(` Your data is in the v${s.version} state but .schema-version still shows v${o}.`),console.error(` On next plugin command the runner will try to re-apply ${r}. If that migration is`),console.error(` idempotent (most are), it will succeed harmlessly. If you're unsure, run 'data-cleanup' and contact support.
|
|
51
51
|
`),process.exit(1)}console.log(` \u2713 Migration ${r} complete`)}try{ue()}catch{}try{de()}catch{}oe=!0,se(),console.log(`
|
|
52
|
-
\u2713 Schema is now at v${
|
|
53
|
-
`)}var Yt=[/\.json\.tmp$/,/\.schema-version\.tmp$/,/\.backup\.tmp$/];function zt(e){let o=e.toLowerCase();return Yt.some(n=>n.test(o))}var Kt=6e4;function Qt(){let e=Date.now(),o=[];for(let n of[T,F,N]){if(!a.existsSync(n))continue;let t;try{t=a.readdirSync(n)}catch{continue}for(let s of t){if(!zt(s))continue;let r=d.join(n,s);try{let i=a.statSync(r);if(!i.isFile()||e-i.mtimeMs<Kt)continue;o.push({dir:n,file:s,fullPath:r})}catch{}}}return o}function Xt({verbose:e=!1}={}){let o=Qt();if(o.length===0)return e&&console.log(" No orphan .tmp files found."),0;let n=0;for(let t of o)try{a.unlinkSync(t.fullPath),n++,e&&console.log(` Removed orphan: ${d.basename(t.dir)}/${t.file}`)}catch(s){console.warn(` [!] Could not remove orphan ${t.fullPath}: ${s.message}`)}return!e&&n>0&&console.log(` Cleaned up ${n} orphan .tmp file(s) from previous run.`),n}function Zt(){let e=[];for(let o of[F,N]){if(!a.existsSync(o))continue;let n=a.readdirSync(o).filter(t=>{let s=t.toLowerCase();return s.endsWith(".json")&&!s.includes(".backup")});for(let t of n){let s=d.join(o,t);try{H(s)}catch(r){e.push({dir:d.basename(o),file:t,err:r.message})}}}return e.length===0?{ok:!0}:{ok:!1,broken:e}}function eo(e){let o=[],n=[];for(let r of[F,N]){if(!a.existsSync(r))continue;let i=a.readdirSync(r).filter(c=>c.endsWith(e));for(let c of i){let l=d.join(r,c),u=c.slice(0,-e.length),f=d.join(r,u);try{u.toLowerCase().endsWith(".json")?H(l):a.accessSync(l,a.constants.R_OK),o.push({backupPath:l,originalPath:f,fileName:c,dir:r})}catch(w){n.push(`${d.basename(r)}/${c}: ${w.message}`)}}}if(n.length>0)return{ok:!1,error:n.join("; "),restored:0};let t=0,s=[];for(let{backupPath:r,originalPath:i,fileName:c,dir:l}of o)try{_e(r,i),t++}catch(u){s.push(`${d.basename(l)}/${c}: ${u.message}`)}return s.length>0?{ok:!1,error:s.join("; "),restored:t}:{ok:!0,restored:t}}function _e(e,o){let n=o+".tmp";try{a.copyFileSync(e,n),
|
|
52
|
+
\u2713 Schema is now at v${x}
|
|
53
|
+
`)}var Yt=[/\.json\.tmp$/,/\.schema-version\.tmp$/,/\.backup\.tmp$/];function zt(e){let o=e.toLowerCase();return Yt.some(n=>n.test(o))}var Kt=6e4;function Qt(){let e=Date.now(),o=[];for(let n of[T,F,N]){if(!a.existsSync(n))continue;let t;try{t=a.readdirSync(n)}catch{continue}for(let s of t){if(!zt(s))continue;let r=d.join(n,s);try{let i=a.statSync(r);if(!i.isFile()||e-i.mtimeMs<Kt)continue;o.push({dir:n,file:s,fullPath:r})}catch{}}}return o}function Xt({verbose:e=!1}={}){let o=Qt();if(o.length===0)return e&&console.log(" No orphan .tmp files found."),0;let n=0;for(let t of o)try{a.unlinkSync(t.fullPath),n++,e&&console.log(` Removed orphan: ${d.basename(t.dir)}/${t.file}`)}catch(s){console.warn(` [!] Could not remove orphan ${t.fullPath}: ${s.message}`)}return!e&&n>0&&console.log(` Cleaned up ${n} orphan .tmp file(s) from previous run.`),n}function Zt(){let e=[];for(let o of[F,N]){if(!a.existsSync(o))continue;let n=a.readdirSync(o).filter(t=>{let s=t.toLowerCase();return s.endsWith(".json")&&!s.includes(".backup")});for(let t of n){let s=d.join(o,t);try{H(s)}catch(r){e.push({dir:d.basename(o),file:t,err:r.message})}}}return e.length===0?{ok:!0}:{ok:!1,broken:e}}function eo(e){let o=[],n=[];for(let r of[F,N]){if(!a.existsSync(r))continue;let i=a.readdirSync(r).filter(c=>c.endsWith(e));for(let c of i){let l=d.join(r,c),u=c.slice(0,-e.length),f=d.join(r,u);try{u.toLowerCase().endsWith(".json")?H(l):a.accessSync(l,a.constants.R_OK),o.push({backupPath:l,originalPath:f,fileName:c,dir:r})}catch(w){n.push(`${d.basename(r)}/${c}: ${w.message}`)}}}if(n.length>0)return{ok:!1,error:n.join("; "),restored:0};let t=0,s=[];for(let{backupPath:r,originalPath:i,fileName:c,dir:l}of o)try{_e(r,i),t++}catch(u){s.push(`${d.basename(l)}/${c}: ${u.message}`)}return s.length>0?{ok:!1,error:s.join("; "),restored:t}:{ok:!0,restored:t}}function _e(e,o){let n=o+".tmp";try{a.copyFileSync(e,n),Re(n)}catch(t){try{a.unlinkSync(n)}catch{}throw t}try{a.renameSync(n,o)}catch(t){try{a.unlinkSync(n)}catch{}if(t.code==="EPERM"||t.code==="EBUSY"){let s=new Error(`File locked: ${o} (${t.code}). On Windows this is usually antivirus / Search Indexer / the file open in another app. Close anything that might be holding it and re-run.`);throw s.code=t.code,s}throw t}}var oo=to(import.meta.url),no=L.dirname(oo),re=process.env.DATA_DIR?L.resolve(process.env.DATA_DIR,"market-research"):L.join(no,"market-research"),q=L.join(re,"profile-viewers.json");await Oe();var Fe=process.argv.slice(2),je=Fe.includes("--save"),so=Fe.includes("--list");function ro(){if(!E.existsSync(q)){console.log(`
|
|
54
54
|
No viewer data yet. Run: npm run viewers -- --save
|
|
55
55
|
`);return}let o=JSON.parse(E.readFileSync(q,"utf-8")).scans||[],n=o.flatMap(i=>i.viewers||[]),t=new Map;for(let i of n)(!t.has(i.profileUrl)||i.scanDate>t.get(i.profileUrl).scanDate)&&t.set(i.profileUrl,i);console.log(`
|
|
56
56
|
\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(" PROFILE VIEWERS"),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(` Total scans: ${o.length}`),console.log(` Unique viewers: ${t.size}`);let s=[...t.values()].filter(i=>i.isConnected).length,r=[...t.values()].filter(i=>i.weVisitedThem).length;if(console.log(` Already connected: ${s}`),console.log(` We visited first: ${r} (\u2192 they responded by viewing us)`),t.size>0){console.log(`
|
package/dist/warmScan.mjs
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import"dotenv/config";import*as
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
\u2550\u2550 Warm-scan: ${h}${_} \u2014 ${u.length} target companies \u2550\u2550
|
|
5
|
-
`);var I=Object.entries(p).sort((n,e)=>e[1].connected.length-n[1].connected.length),w=0;for(let[n,e]of I){let r=e.connected.length+e.notConnected.length+e.unknown.length;w+=e.connected.length;let i=`${n.padEnd(18)} connected: ${e.connected.length}, other: ${e.notConnected.length+e.unknown.length}, total seen: ${r}`;if(console.log(i),e.connected.length>0)for(let t of e.connected){let c=(t.title||"").slice(0,55),f=t.convStatus?` [${t.convStatus}]`:"",D=t.lastMsg?` (${t.lastMsg.slice(0,10)})`:"";console.log(` \xB7 ${t.name.padEnd(28)} ${c}${f}${D}`)}let o=[...e.notConnected,...e.unknown];if(o.length>0&&(v||o.length<=5)){console.log(" seen but not 1st-degree:");for(let t of o.slice(0,v?o.length:5))console.log(` ${t.name.padEnd(28)} ${(t.title||"").slice(0,55)}`)}else o.length>5&&console.log(` seen but not 1st-degree: ${o.length} \u2014 add --show-all or --companies="${n}" to expand`);console.log("")}console.log(`Total 1st-degree at target companies: ${w}`);
|
|
1
|
+
import"dotenv/config";import*as j from"fs";import*as a from"path";import{fileURLToPath as N}from"url";import{getPluginConfig as T}from"@ilivemylife/graph-sdk";var _=await T();for(let[e,n]of Object.entries(_))process.env[e]===void 0&&(process.env[e]=n);var z=process.env.LINKEDIN_NAME,p=Object.freeze({DRAFT:"draft",CONFIRMED:"confirmed",REJECTED:"rejected",NEEDS_REVIEW:"needs_review",SENT:"sent"}),Y=Object.freeze(new Set([p.DRAFT,p.CONFIRMED,p.NEEDS_REVIEW])),G=Object.freeze(new Set([p.DRAFT,p.CONFIRMED,p.REJECTED,p.NEEDS_REVIEW]));import"dotenv/config";import*as s from"fs";import*as t from"path";import{fileURLToPath as k}from"url";var F=k(import.meta.url),R=t.dirname(t.dirname(F)),f=process.env.DATA_DIR?t.resolve(process.env.DATA_DIR,"collected-profiles"):t.join(R,"collected-profiles"),ee=t.join(f,"people.json"),ne=t.join(f,"jobs.json"),te=t.join(f,"quota.json"),oe=t.join(f,"conversations.json"),se=t.join(f,"sync-state.json"),re=t.join(f,"application-questions.json"),ie=t.join(f,"profile-history");function b(e,n){if(!s.existsSync(e))return null;try{let c=JSON.parse(s.readFileSync(e,"utf-8"));try{s.copyFileSync(e,e+".backup")}catch{}return c}catch(c){console.error(`[!] ${n} is corrupted: ${c.message}`),console.error(` File: ${t.resolve(e)}`);let i=e+".backup";if(s.existsSync(i))try{let o=JSON.parse(s.readFileSync(i,"utf-8"));console.error(` Restored from ${i}`);try{s.copyFileSync(i,e)}catch{}return o}catch{console.error(" Backup also corrupted!")}console.error(` Starting with empty data. Old file preserved as ${e}.corrupted`);try{s.copyFileSync(e,e+".corrupted")}catch{}return null}}var L=a.dirname(N(import.meta.url)),I=process.env.DATA_DIR?a.resolve(process.env.DATA_DIR,"collected-profiles"):a.join(L,"collected-profiles"),g={};for(let e of process.argv.slice(2)){let n=e.match(/^--([^=]+)(?:=(.*))?$/);n&&(g[n[1]]=n[2]??!0)}var d=g.group||null,C=typeof g["min-seniority"]=="string"?g["min-seniority"]:null,D=!!g["show-all"],u={},h=process.env.DATA_DIR&&a.resolve(process.env.DATA_DIR,"target-companies.json");if(h&&j.existsSync(h))try{u=JSON.parse(j.readFileSync(h,"utf8"))}catch(e){console.error(`Ignoring malformed ${h}: ${e.message}`)}(!u||typeof u!="object"||Array.isArray(u))&&(u={});var A=typeof g.companies=="string"?g.companies.split(","):d?u[d]?.companies:null,y=Array.isArray(A)?A.filter(e=>typeof e=="string"&&e.trim()).map(e=>e.trim()):null;if(!y||!y.length){console.error("Specify which companies to scan:"),console.error(' --companies="Google,Meta,Stripe"');let e=Object.keys(u);d&&!u[d]&&console.error(` (group "${d}" not found)`),e.length?console.error(` --group=<name> your groups: ${e.join(", ")}`):console.error(` or define named groups in ${h||"<DATA_DIR>/target-companies.json"}`),process.exit(1)}var v=b(a.join(I,"people.json"),"people.json")||{},E=b(a.join(I,"conversations.json"),"conversations.json")||{};Object.keys(v).length||(console.error("No contacts found yet \u2014 run `ilml linkedin sync` first to build your network, then warm-scan."),process.exit(1));var w=new Map;for(let[e,n]of Object.entries(E))typeof n.isConnection=="boolean"&&w.set(e,{isConnection:n.isConnection,conv:n});function M(e){if(e.name&&w.has(e.name)){let n=w.get(e.name);return{is:n.isConnection,src:"A.convInbox",conv:n.conv}}return e.connectionDegree==="1st"?{is:!0,src:"B.profileScrape"}:e.connectionDegree==="2nd"||e.connectionDegree==="3rd"?{is:!1,src:"B.profileScrape"}:e.connectionStatus==="connected"?{is:!0,src:"C.botStatus"}:["sent_without_note","sent_with_note","withdrawn","rejected"].includes(e.connectionStatus)?{is:!1,src:"C.botStatus"}:{is:null,src:null}}var O={intern:0,junior:1,mid:2,senior:3,lead:4,staff:4,principal:5,director:5,vp:6};function P(e,n){if(!n)return!0;if(!e)return!1;let c=e.toLowerCase(),i=O[n.toLowerCase()];if(i==null)return!0;for(let[o,r]of Object.entries(O))if(c.includes(o)&&r>=i)return!0;return!1}function J(e,n){return[e.company,e.title,n?.personContext?.title,n?.personContext?.company].filter(Boolean).join(" | ").toLowerCase()}function B(e){let n=e.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g,"\\$&");return new RegExp(`\\b${n}\\b`,"i")}var U=y.map(e=>({name:e,re:B(e)})),S={};for(let e of y)S[e]={connected:[],notConnected:[],unknown:[]};for(let[e,n]of Object.entries(v)){let c=n.name?E[n.name]:null,i=J(n,c);if(i)for(let{name:o,re:r}of U){if(!r.test(i))continue;if(!P(n.title||c?.personContext?.title,C))break;let l=M(n),m={name:n.name||e.slice(-40),title:n.title||c?.personContext?.title||"",company:n.company,degree:n.connectionDegree,via:l.src,convStatus:l.conv?.conversationStatus,lastMsg:l.conv?.lastMessageTime,lastBy:l.conv?.lastMessageBy,category:l.conv?.category||n.category};l.is===!0?S[o].connected.push(m):l.is===!1?S[o].notConnected.push(m):S[o].unknown.push(m);break}}var q=C?` (min-seniority=${C})`:"",V=d?`group "${d}"`:`${y.length} target companies`;console.log(`
|
|
2
|
+
\u2550\u2550 Warm-scan: ${V}${q} \u2550\u2550
|
|
3
|
+
`);var Q=Object.entries(S).sort((e,n)=>n[1].connected.length-e[1].connected.length),$=0;for(let[e,n]of Q){let c=n.connected.length+n.notConnected.length+n.unknown.length;$+=n.connected.length;let i=`${e.padEnd(18)} connected: ${n.connected.length}, other: ${n.notConnected.length+n.unknown.length}, total seen: ${c}`;if(console.log(i),n.connected.length>0)for(let r of n.connected){let l=(r.title||"").slice(0,55),m=r.convStatus?` [${r.convStatus}]`:"",x=r.lastMsg?` (${r.lastMsg.slice(0,10)})`:"";console.log(` \xB7 ${r.name.padEnd(28)} ${l}${m}${x}`)}let o=[...n.notConnected,...n.unknown];if(o.length>0&&(D||o.length<=5)){console.log(" seen but not 1st-degree:");for(let r of o.slice(0,D?o.length:5))console.log(` ${r.name.padEnd(28)} ${(r.title||"").slice(0,55)}`)}else o.length>5&&console.log(` seen but not 1st-degree: ${o.length} \u2014 add --show-all or --companies="${e}" to expand`);console.log("")}console.log(`Total 1st-degree at target companies: ${$}`);
|
package/ilml-plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "linkedin",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.0",
|
|
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." },
|
|
@@ -36,6 +36,37 @@
|
|
|
36
36
|
"today": { "run": "node dist/syncAll.mjs --today", "description": "No-browser daily action plan from already-synced data: who to reply to, what to do, prioritised by category and last activity.", "usage": "ilml linkedin today", "useWhen": "First thing each morning — what should I touch today?" },
|
|
37
37
|
"enrich": { "run": "node dist/syncAll.mjs --enrich", "description": "Re-classify every saved conversation (recruiter / HM / founder / coach / etc.) and rebuild summaries, priorities, suggested actions. Offline, no browser. Idempotent.", "usage": "ilml linkedin enrich", "useWhen": "After upgrading the plugin and enrichment rules changed, or to refresh stale auto-tags." },
|
|
38
38
|
"report": { "run": "node dist/syncAll.mjs --report", "description": "Print the summary of the latest sync session and current inbox stats. No browser, no writes.", "usage": "ilml linkedin report", "useWhen": "Quick health check on the local DB and recent sync results." },
|
|
39
|
+
"version": { "run": "node dist/version.mjs", "description": "Print the installed plugin version. Use this to confirm an update actually landed.", "usage": "ilml linkedin version" },
|
|
40
|
+
"quota": {
|
|
41
|
+
"run": "node dist/quota.mjs",
|
|
42
|
+
"description": "Connection-quota monitor and acceptance analytics. No browser. Shows how much of today's/this week's connection quota is left and how it was spent (by source), joins the send log with your contacts to show acceptance rate per source (which outreach actually converts), and lists high-value people (founder / investor / hiring_manager / peer) you haven't connected to yet.",
|
|
43
|
+
"usage": "ilml linkedin quota [--json]",
|
|
44
|
+
"flags": {
|
|
45
|
+
"--json": "Machine-readable JSON instead of the formatted report."
|
|
46
|
+
},
|
|
47
|
+
"banRisk": "None. Reads local data only, no browser.",
|
|
48
|
+
"useWhen": "You want to see if your daily connection quota is being used, and whether the people you connect with actually accept — before deciding who to target next."
|
|
49
|
+
},
|
|
50
|
+
"connect": {
|
|
51
|
+
"run": "node dist/connect.mjs",
|
|
52
|
+
"description": "Connect queue: a reviewable, prioritised list of people to send connection requests to. Drop a list of profiles, review and approve them, choose who gets a personal note (LinkedIn limits those), then send the approved ones in one browser session under the daily/weekly quota. The queue is one file with a status on each entry (queued → approved → sent) — approving, reordering and sending all update it in place. Rich profile data stays in your contacts DB; the queue just references people by URL.",
|
|
53
|
+
"usage": "ilml linkedin connect <add|fill|list|approve|note|no-note|skip|priority|run> [...]",
|
|
54
|
+
"flags": {
|
|
55
|
+
"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.",
|
|
56
|
+
"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. --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.",
|
|
57
|
+
"list": "Show the queue (status, priority, note flag, name/title joined from your contacts). No browser. Optional --status=queued|approved|sent|skipped|failed.",
|
|
58
|
+
"approve <url>": "Move an entry from queued to approved. Or approve a whole batch with --all or --tag=X. Only approved entries are sent.",
|
|
59
|
+
"note <url> \"text\"": "Attach a personal note to an entry (it will send WITH a note). LinkedIn caps notes at 300 characters and limits how many you can send — allocate deliberately.",
|
|
60
|
+
"no-note <url>": "Remove the note from an entry (sends without a note, frees your note budget).",
|
|
61
|
+
"skip <url>": "Drop an entry from sending (queued/approved → skipped).",
|
|
62
|
+
"priority <url> <lvl>": "Set priority high|normal|low. Higher priority is sent first.",
|
|
63
|
+
"run": "Send approved entries, highest priority first, under the daily/weekly quota (browser). --max=N caps this run, --dry-run previews without sending, --yes skips the confirmation prompt (for schedulers).",
|
|
64
|
+
"--tag=X": "Label a batch (used by add/approve). Shows up as the source in `ilml linkedin quota` acceptance stats.",
|
|
65
|
+
"--priority=LEVEL": "high|normal|low (used by add)."
|
|
66
|
+
},
|
|
67
|
+
"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.",
|
|
68
|
+
"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."
|
|
69
|
+
},
|
|
39
70
|
"scout": {
|
|
40
71
|
"run": "node dist/scout.mjs",
|
|
41
72
|
"description": "Scan jobs from LINKEDIN_SCOUT_URLS (multiple search URLs supported), score them, and save to market-research/ — WITHOUT applying. Builds a ranked queue for apply-queue to consume.",
|
|
@@ -120,8 +151,10 @@
|
|
|
120
151
|
"description": "Find your 1st-degree connections at target companies. Offline read of people.json. Use to identify mutual paths into a company before cold outreach.",
|
|
121
152
|
"usage": "ilml linkedin warm-scan [--companies=\"X,Y,Z\"] [--group=<named-group>]",
|
|
122
153
|
"flags": {
|
|
123
|
-
"--companies=\"X,Y,Z\"": "
|
|
124
|
-
"--group=<name>": "Use a named group from your
|
|
154
|
+
"--companies=\"X,Y,Z\"": "The companies to scan for (comma-separated). Primary input — no file needed.",
|
|
155
|
+
"--group=<name>": "Use a named group from your own <DATA_DIR>/target-companies.json.",
|
|
156
|
+
"--min-seniority=<lvl>": "Only show titles at or above this level (intern|junior|mid|senior|lead|staff|principal|director|vp).",
|
|
157
|
+
"--show-all": "Expand the 'seen but not 1st-degree' lists."
|
|
125
158
|
},
|
|
126
159
|
"banRisk": "None — offline.",
|
|
127
160
|
"useWhen": "Before drafting an outreach to a company — see who you already know there."
|
|
@@ -259,6 +292,7 @@
|
|
|
259
292
|
{
|
|
260
293
|
"title": "Recruiter funnel (optional)",
|
|
261
294
|
"fields": [
|
|
295
|
+
{ "key": "FUNNEL_CONNECT_RECRUITERS", "label": "Auto-connect recruiters found while applying", "type": "boolean", "required": false, "default": false, "hint": "Off by default. Cold recruiters who post job listings rarely convert and dilute your feed relevance. Turn on only if you want apply to also send recruiter connection requests." },
|
|
262
296
|
{ "key": "FUNNEL_USE_CUSTOM_MESSAGE", "label": "Send custom message with connection requests", "type": "boolean", "required": false, "default": false },
|
|
263
297
|
{ "key": "FUNNEL_MAX_CONNECTIONS", "label": "Max connection requests per run", "type": "number", "required": false, "default": 20 },
|
|
264
298
|
{ "key": "FUNNEL_MESSAGE", "label": "Connection request message", "type": "text", "required": false, "hint": "Use {name} placeholder, e.g., 'Hi {name}, I just applied for a role at your company'" },
|
package/package.json
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ilml-plugin-linkedin",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.0",
|
|
4
4
|
"description": "Reference ilml plugin: keep a local mirror of your LinkedIn data and run AI-assisted workflows under your direction.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
7
7
|
"dist",
|
|
8
8
|
"ilml-plugin.json",
|
|
9
|
-
"target-companies.json",
|
|
10
9
|
"CHANGELOG.md"
|
|
11
10
|
],
|
|
12
11
|
"scripts": {
|
|
@@ -56,7 +55,9 @@
|
|
|
56
55
|
"warm-scan": "node warmScan.mjs",
|
|
57
56
|
"enrich": "node syncAll.mjs --enrich",
|
|
58
57
|
"today": "node syncAll.mjs --today",
|
|
59
|
-
"
|
|
58
|
+
"quota": "node quota.mjs",
|
|
59
|
+
"connect": "node connect.mjs",
|
|
60
|
+
"plugin-version": "node version.mjs",
|
|
60
61
|
"migrate:status": "node migrationCli.mjs status",
|
|
61
62
|
"rollback:data": "node migrationCli.mjs rollback-data",
|
|
62
63
|
"data:cleanup": "node migrationCli.mjs data-cleanup",
|
package/target-companies.json
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"apiLeaders": {
|
|
3
|
-
"description": "API/AI industry leaders (original focus). Substring match, word-boundary.",
|
|
4
|
-
"companies": [
|
|
5
|
-
"Anthropic",
|
|
6
|
-
"OpenAI",
|
|
7
|
-
"Stripe",
|
|
8
|
-
"Plaid",
|
|
9
|
-
"Twilio",
|
|
10
|
-
"Mastercard",
|
|
11
|
-
"Visa",
|
|
12
|
-
"Square",
|
|
13
|
-
"Block",
|
|
14
|
-
"Ramp",
|
|
15
|
-
"Retool",
|
|
16
|
-
"Linear",
|
|
17
|
-
"Cohere",
|
|
18
|
-
"Hugging Face",
|
|
19
|
-
"Scale AI"
|
|
20
|
-
]
|
|
21
|
-
},
|
|
22
|
-
"topPaying": {
|
|
23
|
-
"description": "FAANG + tier-1 tech + top AI + high-comp fintech/SaaS — senior engineer total comp typically $300K+. Broader target when user isn't picky about domain, only about pay.",
|
|
24
|
-
"companies": [
|
|
25
|
-
"Meta",
|
|
26
|
-
"Google",
|
|
27
|
-
"Amazon",
|
|
28
|
-
"Apple",
|
|
29
|
-
"Netflix",
|
|
30
|
-
"Microsoft",
|
|
31
|
-
"Uber",
|
|
32
|
-
"Airbnb",
|
|
33
|
-
"LinkedIn",
|
|
34
|
-
"Pinterest",
|
|
35
|
-
"Anthropic",
|
|
36
|
-
"OpenAI",
|
|
37
|
-
"Cohere",
|
|
38
|
-
"Databricks",
|
|
39
|
-
"Snowflake",
|
|
40
|
-
"Hugging Face",
|
|
41
|
-
"Scale AI",
|
|
42
|
-
"Stripe",
|
|
43
|
-
"Plaid",
|
|
44
|
-
"Mastercard",
|
|
45
|
-
"Visa",
|
|
46
|
-
"Square",
|
|
47
|
-
"Block",
|
|
48
|
-
"Ramp",
|
|
49
|
-
"Datadog",
|
|
50
|
-
"MongoDB",
|
|
51
|
-
"Figma",
|
|
52
|
-
"Notion",
|
|
53
|
-
"HashiCorp",
|
|
54
|
-
"Retool",
|
|
55
|
-
"Linear"
|
|
56
|
-
]
|
|
57
|
-
}
|
|
58
|
-
}
|