ilml-plugin-linkedin 1.5.0 → 1.8.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 CHANGED
@@ -1,5 +1,36 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.8.0
4
+ - **Profile scrape works again on the 2026 LinkedIn DOM.** LinkedIn rebuilt the profile page with obfuscated class hashes and dropped `<h1>` and `section[id]` anchors, which silently broke `viewProfile()` and `sendConnectionRequest()`'s profile-info extraction — every visit had been returning all-null since the redesign. The new extractor uses structural anchors only: the name lives in the first `<h2>` inside `<main>` that isn't a section header; the top card is the nearest ancestor whose direct children include a contentful `<p>` (excluding connection-degree badges and pronoun chips); the About section is found by `<h2>About</h2>` walking to its `<section>` ancestor. Tested across recruiter, founder, and engineer profiles. `<h1>` is still tried first as a fallback in case LinkedIn ever reverts.
5
+ - **India / Pakistan location filter no longer leaks.** Three connection requests went to India-based recruiters in the last two weeks despite `FUNNEL_EXCLUDE_LOCATIONS=India,Pakistan`. Root cause: the live page-evaluate occasionally returned `null` for the location element (DOM variation, A/B render, or Premium-overlay timing), so `shouldExcludeLocation(null, ['India',...])` returned `false` and the connect proceeded — even though `people.json` already had the location stored from a prior visit. Fix is defense-in-depth: (1) early-skip *before* navigating, using the stored people.json location when known; (2) inside `sendConnectionRequest`, fall back to the stored location when the live scrape returned null; logs distinguish `(live)` vs `(stored)` so the trail is debuggable.
6
+ - **Profile data now accumulates organically across normal flows.** Every existing place we open a profile — `apply` (funnel connect path), `outreach` (Lifebot qualification), `funnel` (view-only mode), `visit` — now persists the scraped profile via `upsertPerson` so people.json fills in over time without dedicated mass-visit batches. New fields captured per visit (when LinkedIn renders them): `currentCompany`, `currentSchool`, `connectionsLevel`, `mutualConnectionsCount`, `followers`, `openToWork`, `hiring`, `topSkills` (top 3), `availableSections` (which h2 sections were on the page), in addition to the existing name/title/location/about. All fields gracefully `null` when LinkedIn hides them, and `MERGE_FIELDS` keeps the previously-known value rather than overwriting with null — so a render that happens to skip a section never erases what we already knew about that person.
7
+ - **Profile-history versioning — see how a contact's profile changed over time.** When a tracked content field changes between visits (e.g. someone's `title` goes from "Software Engineer" → "Engineering Manager", or `openToWork` flips on, or `currentCompany` changes), the previous full state is snapshotted to `<DATA_DIR>/collected-profiles/profile-history/<slug>.json` *before* people.json is overwritten with the new state. Snapshot is taken only when both old and new have real observations and they differ — null-or-empty transitions ("we just learned this" or "LinkedIn hid the field this render") are NOT treated as changes. Tracked fields that trigger a snapshot: `name`, `title`, `location`, `currentCompany`, `currentSchool`, `connectionDegree`, `hasVerifiedBadge`, `openToWork`, `hiring`, `about`, `topSkills`. Noisy fields (`followers`, `connectionsLevel`, `mutualConnectionsCount`, `availableSections`, `photoUrl`) are captured but never trigger.
8
+ - **New `ilml linkedin profile-history` command** — read-only CLI over the snapshots, no browser. Modes: `profile-history <slug-or-url>` shows one person's full history with OLD/NEW diffs per field; `--all` lists everyone with any history sorted by recency; `--since=YYYY-MM-DD` shows all change events in a window; `--field=title --since=...` filters to changes of a single field (e.g. `--field=currentCompany --since=2026-04-01` answers "who changed jobs this month"). Works fully offline against local files.
9
+ - **New `ilml linkedin verify-about` command** — diagnostic that visits one or more profiles and prints what was scraped, with a verdict per profile (`OK` or `ABOUT MISSING`, etc.). Useful when LinkedIn changes the DOM again — run it to see whether the parser still works before the next mass operation. Modes: `<url>` (single), `--pick=N` (random N from people.json), `--pick=N --has-about` (regression check on already-enriched).
10
+ - `enrich-profiles` (mass-enrichment script) is shipped but **not exposed in the manifest** and ships with a tiny default cap (10 visits/session) and explicit ban-risk warning in the help text. Idle profile visits with no follow-up action are exactly the pattern LinkedIn previously flagged with an account warning, so the safer path is the organic accumulation above. The script remains available as `node dist/enrichProfiles.mjs` for users who understand the trade-off.
11
+ - **enrich-profiles is now configurable per audience** — replaced the hard-coded `--recruiters` flag (regex `recruiter|sourcer|talent|staffing`) with the generic `--match-title=<regex>`. Pass any case-insensitive regex over LinkedIn's headline text: `--match-title="founder|ceo"`, `--match-title="investor|VC|venture"`, `--match-title="engineering manager|director of"`. Works on top of `--connections`. Default: no title filter (full queue).
12
+ - **enrich-profiles makes monotonic progress instead of random sampling.** Within each priority tier the queue is now sorted by `lastFullVisitAt` ascending — never-fully-visited profiles first, then oldest-visited next — so two consecutive runs never re-visit the same person until the entire long tail has been touched at least once. Previously the within-tier order was random, which meant after a couple of runs we'd start picking already-visited people again and the long tail could take months to converge. The marker is set automatically inside `upsertPerson` when incoming data carries `availableSections` (the field is only ever populated by `viewProfile()` — list-scrape upserts from `sync` / `syncConnections` don't update it, so `lastFullVisitAt` truly tracks "real profile page visit, not a list-card sighting").
13
+ - No schema migration. The new fields on people.json are additive — old entries simply lack them (read as `null`/`undefined`) and `upsertPerson` populates them on the next visit. The new `profile-history/` subdirectory is created on first snapshot. `CURRENT_SCHEMA_VERSION` unchanged.
14
+
15
+ ## 1.7.0
16
+ - **`apply` no longer waits for Enter to close the browser at end of session.** The script now closes the browser and exits immediately after the final summary (success, error, or quota reached). Previously a `Press Enter to close browser...` prompt blocked the process on stdin — closing the browser window manually left a hung Node process that kept the run flagged `status: running` in `run-log.json` until someone came back to the terminal and pressed Enter.
17
+ - **Fixes a Windows-only race that could post a stale end-of-session report to `NODE_RUN_REPORTS`.** When the stdin wait above was interrupted with Ctrl+C, the `^C` byte could resolve the `process.stdin.once('data')` await *before* the SIGINT handler reached `process.exit(0)`. Execution then fell through to `endRun` with whatever `sessionStats.dailyLimitHit` was set at that moment — sending a misleading `quota-reached` push for an interrupted, not actually-completed, run. Removing the wait removes the race.
18
+ - Internal: removed the post-funnel `process.stdin.once('data')` block in `run.mjs` (was gated by `!jobUrlsFile && !--no-wait`); dropped the now-redundant `--no-wait` argument that `daily.mjs` was passing to `run.mjs`. No schema change — runtime behavior only.
19
+
20
+ ## 1.6.0
21
+ - **`sync-all` now preserves your LinkedIn unread badges by default.** Until now, opening the local sync would silently mark every unread thread as read on LinkedIn — burning your main attention signal in the LinkedIn UI itself. Starting in 1.6.0 the default behavior is the opposite: unread threads are scanned (preview, sender, time read off the sidebar card) but never *opened*, so they stay bold in linkedin.com until you read them yourself. The previous opt-in `--skip-unread` flag is no longer needed and is silently accepted as a no-op for back-compat with existing scripts.
22
+ - **New section in the sync report — `STILL UNREAD ON LINKEDIN`.** When the sync finishes, the report lists every conversation that's still unread on LinkedIn, enriched with the local DB context (category, summary, person title/company) when we already know the contact. This is the daily "what landed in my inbox while I wasn't looking" view, all from the terminal, with zero impact on the bold markers. New conversations show as `(no DB context)` so you immediately spot strangers vs. people you've talked to before.
23
+ - **Unread state now persists to disk, so `today` and `report` see it offline.** Previously, unread flags only lived in memory during a sync run — between runs, the local DB had no record of which threads were bold on LinkedIn. Now Phase 1 saves the unread state at the end of the inbox scan: existing conversations get their `unread` flag updated, and brand-new unread threads (people you've never talked to before) get a stub record (name, sidebar preview, time — no messages yet). Result: `npm run today` (no browser) opens with an `UNREAD ON LINKEDIN` block at the top showing what's waiting for you, even if you ran the sync hours ago. Stub records become full conversations the next time the thread is read (manually in LinkedIn, or via `--read-unread`).
24
+ - **Opt-in to the old behavior with `--read-unread`.** If you want the sync to vacuum unread threads into the local DB (e.g. you're deliberately archiving the inbox state and don't care about the LinkedIn badges), pass `--read-unread`. Threads get opened, messages stored, badges go away — same as the pre-1.6.0 default. In this mode the `STILL UNREAD ON LINKEDIN` block is suppressed (they're not unread anymore — they show up in the normal `REPLY NEEDED` list).
25
+ - Removed the `npm run sync-all:safe` script — it was an alias for `--skip-unread` which is now the default. `npm run sync-all` does the same thing. One less command to memorize.
26
+ - **Schema migration v002 runs automatically on first launch after upgrade** — resets `unread=false` on all existing conversations so the new "unread = currently bold in LinkedIn UI" semantic starts from a clean slate. Backups of `conversations.json` etc. are written to `.pre-v002.backup` next to the originals before the migration runs. The next `sync-all` re-establishes real unread state from the LinkedIn DOM. No data loss — only the flag is reset; messages, notes, drafts, profileUrls, enrichment fields are untouched.
27
+ - **New manual migration commands.** `ilml linkedin migrate-status` shows the current schema version, applied migrations with timestamps, available backups, pending upgrades, and a JSON-parse integrity check on all data files. `ilml linkedin rollback-data` previews what a data rollback would do (read-only); `ilml linkedin rollback-data --confirm` actually performs it. The command name is deliberately explicit: it touches DATA only, not plugin code — the help text walks the user through the required two-step procedure (roll back data → reinstall older plugin version) and warns that running anything else between the two steps will silently undo the rollback.
28
+ - **Single source of truth for schema version.** Pre-1.6.0 stored `.schema-version` in both `collected-profiles/` and `market-research/` — two files for one DB, with a permanent risk of mismatch if a migration crashed between writes. Now it's a single `<DATA_ROOT>/.schema-version` written atomically (`.tmp` + rename). On first run after upgrade, the legacy files are auto-relocated and removed; if they happen to disagree (a previous mid-migration crash) the runner aborts cleanly so the user can reconcile manually. Corrupted legacy files are surfaced as a fatal error rather than silently treated as version 0.
29
+ - **DATA_DIR stays clean: orphan `.tmp` files are auto-removed on every plugin start** — leftovers from a crashed atomic-write or backup never accumulate. Pre-rollback snapshots (`.pre-rollback-vNNN.backup`, kept as a safety net after rollback) are visible in `migrate-status` and removable on demand via the new `ilml linkedin data-cleanup` command. `.pre-vNNN.backup` files (needed for `rollback-data`) are NEVER auto-removed.
30
+ - **Migration / rollback hardening (additional fixes):** (1) Backups never overwrite an existing same-version backup — historical pre-migration state is preserved across restart-after-crash. (2) Rollback writes the lowered schema version BEFORE restoring data, so a mid-rollback crash never leaves the plugin running new code on old-format data. (3) The `_migrationDone` singleton flag is now set only after success, so a non-fatal failure path can't silently skip migration on a re-entrant call.
31
+ - **Hardened migration safety pipeline:** (1) Pre-migration JSON integrity check — if any data file is corrupted *before* migration starts, abort cleanly without touching anything. (2) Backups are full snapshots of every regular file in the data dirs (not just `.json`), copied via `.tmp`+rename so a crash mid-backup never produces a half-written backup. (3) Migration registry uses static imports so esbuild reliably bundles every migration into the published `dist/`. (4) Rollback validates ALL backups upfront and aborts if any is corrupted, before touching any original — no partial-restore inconsistency. (5) Both data directories must be on the same schema version for rollback to proceed; mismatch aborts with a clear error.
32
+ - Heads-up for end-users on `ilml linkedin sync-all`: existing scheduled jobs and `daily` pipelines keep working unchanged. The functional difference is that LinkedIn's unread bold indicators now survive a sync — most users will read this as a fix, not a behavior change.
33
+
3
34
  ## 1.5.0
4
35
  - **Easy Apply now fills optional fields too, not just required ones.** Until now we only answered fields that LinkedIn flagged with a validation error — i.e. required ones. Optional fields (cover letter, salary expectation, LinkedIn URL, portfolio, notice period, etc.) silently went unanswered, which is exactly the kind of small signal that makes a candidate look less interested. Each step of the form is now scanned proactively: any field that's not required, not pre-filled by LinkedIn from your profile, and still empty gets routed through Lifebot for an answer. If Lifebot returns nothing or an answer that doesn't match the available options, the field stays empty — no breakage, just no answer logged.
5
36
  - **Pre-filled values stay untouched.** When LinkedIn pulls phone / email / name from your profile we leave it alone, even if the field is technically optional. The pre-filled answer goes into the stats as-is (`wasPreFilled: true`), counted as your answer because it came from your profile.
package/README.md CHANGED
@@ -13,7 +13,7 @@ It also automates LinkedIn from the terminal — applying to jobs, drafting outr
13
13
  - **A full local mirror of your LinkedIn graph.** Connections, conversation threads, message history, profile metadata, your private notes about people — synced into your iLiveMyLife graph and a local database. Nothing stays trapped in linkedin.com.
14
14
  - **AI-assisted messaging — drafts go through you.** [Lifebot](https://ilivemylife.io) reads the full context of each thread (history, your notes, tags) and drafts replies in your voice. You review the batch in one pass and push approved drafts as a group. On push, the bot re-scans every thread first — if a new incoming message arrived since you reviewed, it pauses that draft and flags it for re-review instead of sending blind. Never autopilot.
15
15
  - **An auto-triaged inbox.** Every conversation gets classified (recruiter / hiring manager / founder / investor / spam / event), tagged, prioritized, and stamped with a suggested next action. `ilml linkedin today` prints a no-browser daily plan: who to reply to, what to do, in priority order — before you've even opened LinkedIn.
16
- - **Inbox sync that doesn't break your workflow.** A `--skip-unread` mode pulls the conversation list and metadata without "opening" unread threads — LinkedIn keeps showing the unread badges until *you* read them in the UI. Use the bot as an analyst without losing your own attention markers.
16
+ - **Inbox sync that doesn't break your workflow.** By default `sync-all` pulls the conversation list and metadata without "opening" unread threads — LinkedIn keeps showing the unread badges until *you* read them in the UI. The sync report ends with a list of what's still unread so you can scan it from the terminal first. Use the bot as an analyst without losing your own attention markers. (Pass `--read-unread` if you want to vacuum unread threads into the local DB.)
17
17
  - **Terminal-first automation.** Batch operations — apply to dozens of jobs at once, draft 50 outreach messages then review them all in one pass, run a connection campaign on a schedule. Scriptable. Re-runnable. Logged.
18
18
  - **Easy Apply that actually thinks.** Auto-fills LinkedIn job applications, with Lifebot answering custom questions ("Why are you interested in this role?") in your voice using context from your graph — not boilerplate.
19
19
  - **Your data outlives the plugin.** The connection database, conversation history, and your notes live in a directory **you** choose (`DATA_DIR`). Plugin updates don't wipe them. Logging out of ilml doesn't wipe them. Uninstalling the plugin doesn't wipe them. Only **you** decide when they go.
@@ -161,7 +161,7 @@ Sessions live in your active ilml scope (`<scope>/.ilivemylife/plugins-state/lin
161
161
  | Command | What it does |
162
162
  |---|---|
163
163
  | `ilml linkedin apply` | Auto-apply to LinkedIn Easy Apply jobs from `LINKEDIN_SEARCH_URL`. Stops after `MAX_APPLY_FOR_RUN`. |
164
- | `ilml linkedin sync-all` | Pull the LinkedIn inbox into the local DB and the graph. Add `--skip-unread` to scan without opening unread threads (preserves LinkedIn's unread badges). `--full` for a full re-scan, `--report` for stats only with no browser. |
164
+ | `ilml linkedin sync-all` | Pull the LinkedIn inbox into the local DB and the graph. By default it preserves LinkedIn's unread badges (unread threads aren't opened) and lists what's still unread at the end of the report. Pass `--read-unread` to vacuum unread threads into the DB too (loses the badges). `--full` for a full re-scan, `--report` for stats only with no browser. |
165
165
  | `ilml linkedin today` | No-browser daily plan: who to reply to, what to do, in priority order. Reads already-synced data — doesn't open LinkedIn. |
166
166
  | `ilml linkedin enrich` | Re-classify every conversation (recruiter / hiring manager / founder / investor / spam / event) and rebuild priorities, summaries, and suggested actions. Offline, no browser. |
167
167
  | `ilml linkedin report` | Print the summary of the latest session. |
@@ -183,8 +183,8 @@ A few patterns that string the daily commands together. Outgoing messages always
183
183
 
184
184
  **Morning triage** — see what needs attention before opening LinkedIn:
185
185
  ```bash
186
- ilml linkedin sync-all --skip-unread # pull new messages without losing unread badges
187
- ilml linkedin today # print prioritized day plan
186
+ ilml linkedin sync-all # pull new messages; unread threads stay bold and are listed at the end
187
+ ilml linkedin today # print prioritized day plan
188
188
  ```
189
189
 
190
190
  **Active job search** — apply, scout, network on autopilot; you stay in the loop on outgoing messages:
@@ -196,7 +196,7 @@ ilml linkedin messages --push-drafts # push the ones you approved
196
196
 
197
197
  **Passive pipeline** — keep the inbox synced and triaged without touching outgoing channels:
198
198
  ```bash
199
- ilml linkedin sync-all --skip-unread
199
+ ilml linkedin sync-all
200
200
  ilml linkedin enrich
201
201
  ilml linkedin today
202
202
  ```
@@ -226,6 +226,43 @@ You'll see one of:
226
226
 
227
227
  **Your config and your data both survive updates.** Only the plugin code dir gets refreshed; everything user-tied lives elsewhere.
228
228
 
229
+ ### Schema migrations
230
+
231
+ Some updates change the on-disk format of your local data. The plugin runs needed migrations automatically the first time you invoke any command after an update — you don't have to do anything. Several safety nets are in place every time:
232
+
233
+ 1. **Pre-migration integrity check.** Before anything else, every `.json` file in `DATA_DIR/collected-profiles/` and `DATA_DIR/market-research/` is parsed as JSON. If any file is corrupted (truncated, hand-edited and broken, hit by disk failure, etc.) the migration aborts before touching anything — you get a list of which files are broken so you can repair them first. We don't back up corrupted state and we don't try to migrate it.
234
+ 2. **Pre-migration backup.** Once integrity is confirmed, every regular file in both data dirs is atomically copied to `<filename>.pre-vNNN.backup` (where `NNN` is the migration version about to run). Originals are left untouched.
235
+ 3. **Atomic write + crash-safe.** Migrations and backup copies use a `.tmp` file + rename, so a crash mid-write never leaves a half-written file. If a migration throws, the runner exits with an error pointing at the backups; your originals are intact.
236
+
237
+ Manual operations:
238
+
239
+ ```bash
240
+ ilml linkedin migrate-status # what schema version you're on, what's pending, what backups exist, what leftovers can be cleaned
241
+ ilml linkedin data-cleanup # remove orphan .tmp files + pre-rollback snapshots (does NOT touch pre-migration backups)
242
+ ilml linkedin rollback-data # preview what a DATA rollback would do (read-only)
243
+ ilml linkedin rollback-data --confirm # actually roll back DATA — does NOT touch plugin code
244
+ ```
245
+
246
+ `DATA_DIR` is kept tidy automatically — orphan `.tmp` files (leftovers from a crashed atomic write) are swept on every plugin start. Pre-migration backups (`.pre-vNNN.backup`) are kept indefinitely because `rollback-data` needs them; you can remove them manually if you're sure you'll never roll back. Pre-rollback snapshots (`.pre-rollback-vNNN.backup`) are kept as a "I changed my mind" safety net after a rollback and can be removed with `data-cleanup` once you're confident the rollback is good.
247
+
248
+ ### Full rollback procedure (two steps)
249
+
250
+ `rollback-data` deliberately separates DATA rollback from CODE rollback. To fully revert to a previous plugin version you must do BOTH, in this order:
251
+
252
+ ```bash
253
+ # Step 1 — roll back data (only run on the new plugin version; older versions don't have this command)
254
+ ilml linkedin rollback-data --confirm
255
+
256
+ # Step 2 — IMMEDIATELY downgrade the plugin code, BEFORE running any other ilml linkedin command
257
+ ilml plugin install linkedin@<previous-version>
258
+ ```
259
+
260
+ Between step 1 and step 2: **do not run any `ilml linkedin` command.** The current plugin will see the rolled-back data, detect a "missing" migration, and immediately re-apply it — silently undoing your rollback.
261
+
262
+ **`rollback-data` is destructive** — it overwrites your current files with the backup taken before the last migration. Any data added after that migration (new conversations synced, drafts written, notes added) will be lost. Rollback uses an all-or-nothing strategy: every backup file is JSON-validated upfront; if any one is corrupted, rollback aborts before touching any original (avoids "half restored, half new" inconsistency). Once validation passes, your current state is also snapshotted into `<filename>.pre-rollback-vNNN.backup` files as a safety net before the atomic restore.
263
+
264
+ **Just want to inspect old data?** Don't roll back. Copy your `DATA_DIR` somewhere else and run a separate `DATA_DIR=/copy/path` plugin instance against that copy — your live setup stays untouched.
265
+
229
266
  ## Where things live
230
267
 
231
268
  | Path | What's there | Wiped on plugin update? | Wiped on `ilml logout`? |
@@ -1,27 +1,58 @@
1
- var mt=Object.defineProperty;var Ie=(t,e)=>()=>(t&&(e=t(t=0)),e);var $e=(t,e)=>{for(var o in e)mt(t,o,{get:e[o],enumerable:!0})};var ze={};$e(ze,{addEncounter:()=>xe,canSendConnection:()=>Je,commitDataSnapshot:()=>Ge,default:()=>wt,getAllApplications:()=>Ve,getPersonStatus:()=>Le,getQuestionStats:()=>Qe,getQuotaStatus:()=>Ne,getRecentConnections:()=>ee,linkPersonToJob:()=>_e,loadApplicationQuestions:()=>de,loadConversations:()=>Re,loadJobs:()=>H,loadPeople:()=>O,loadQuota:()=>Z,loadSyncState:()=>Ue,logApplicationQuestions:()=>Be,migrateFromDailyFiles:()=>Me,recordConnectionSent:()=>ke,resetCaches:()=>yt,saveConversations:()=>Ae,saveJobs:()=>X,savePeople:()=>A,saveQuota:()=>fe,saveSyncState:()=>Pe,updateConnectionStatus:()=>Fe,upsertJob:()=>Ee,upsertPerson:()=>pe});import"dotenv/config";import*as d from"fs";import*as h from"path";import{fileURLToPath as ht}from"url";import{execSync as ie}from"child_process";function yt(){L=null,F=null,D=null,C=null,k=null,T=null}function y(){d.existsSync(S)||d.mkdirSync(S,{recursive:!0})}function R(t,e){let o=JSON.stringify(e,null,2),r=t+".tmp";d.writeFileSync(r,o);for(let n=0;n<3;n++)try{d.renameSync(r,t);return}catch(s){if(n<2&&(s.code==="EPERM"||s.code==="EBUSY")){let i=Date.now();for(;Date.now()-i<200;);continue}throw s}}function U(t,e){if(!d.existsSync(t))return null;try{let o=JSON.parse(d.readFileSync(t,"utf-8"));try{d.copyFileSync(t,t+".backup")}catch{}return o}catch(o){console.error(`[!] ${e} is corrupted: ${o.message}`),console.error(` File: ${h.resolve(t)}`);let r=t+".backup";if(d.existsSync(r))try{let n=JSON.parse(d.readFileSync(r,"utf-8"));console.error(` Restored from ${r}`);try{d.copyFileSync(r,t)}catch{}return n}catch{console.error(" Backup also corrupted!")}console.error(` Starting with empty data. Old file preserved as ${t}.corrupted`);try{d.copyFileSync(t,t+".corrupted")}catch{}return null}}function J(){return new Date().toISOString().split("T")[0]}function O(){return L||(y(),L=U(ce,"people.json")||{},L)}function A(t){y(),L=t;try{R(ce,t)}catch(e){console.error(`[!] Failed to save people.json: ${e.message}`),console.error(` Path: ${h.resolve(ce)}`)}}function H(){return F||(y(),F=U(le,"jobs.json")||{},F)}function X(t){y(),F=t;try{R(le,t)}catch(e){console.error(`[!] Failed to save jobs.json: ${e.message}`),console.error(` Path: ${h.resolve(le)}`)}}function Re(){return C||(y(),C=U(K,"conversations.json")||{},jt(C),C)}function jt(t){let e=0;for(let o of Object.values(t)){if(!(o.draftStatus!==void 0||o.draftPreparedAt!==void 0))continue;let n=(o.messages||[]).find(s=>s.status==="draft");n&&o.draftStatus&&o.draftStatus!=="pending"&&(n.status=o.draftStatus),delete o.draftStatus,delete o.draftPreparedAt,e++}if(e>0){console.log(` [migration] Moved draft state from conv to msg.status for ${e} conversations`);try{R(K,t)}catch{}}}function Ae(t){y(),C=t;try{R(K,t)}catch(e){console.error(`[!] Failed to save conversations.json: ${e.message}`),console.error(` Path: ${h.resolve(K)}`)}}function pe(t,e={}){if(!t)return null;let o=O(),r=J(),n=new Date().toISOString(),s=o[t];if(s){s.seenCount=(s.seenCount||1)+1,s.lastSeen=r,s.lastUpdated=n,e.source&&!s.sources?.includes(e.source)&&(s.sources=s.sources||[],s.sources.push(e.source));for(let i of ue)e[i]!=null&&(s[i]=e[i])}else o[t]={name:e.name||null,title:e.title||null,location:e.location||null,company:e.company||null,connectionDegree:e.connectionDegree||null,connectionsCount:e.connectionsCount||null,mutualConnections:e.mutualConnections||null,photoUrl:e.photoUrl||null,hasVerifiedBadge:e.hasVerifiedBadge||!1,firstSeen:r,lastSeen:r,seenCount:1,sources:e.source?[e.source]:[],connectionStatus:null,connectionDate:null,connectionMethod:null,messageSent:null,jobIds:[],lastUpdated:n};return A(o),o[t]}function Ee(t,e={}){if(!t)return null;let o=H(),r=J(),n=o[t];if(n){let s=["title","company","companyUrl","location","salary","url","applied","appliedDate","recruiterUrl","description","techStack","jobTags","seniority","employmentType","industries","jobFunction","postedTime","applicantCount","companySize","workplaceType","contractType","isEasyApply","applyType","scoutedAt","lastSeen"];for(let i of s)e[i]!=null&&(n[i]=e[i])}else o[t]={title:e.title||null,company:e.company||null,location:e.location||null,salary:e.salary||null,url:e.url||null,applied:e.applied||!1,appliedDate:e.appliedDate||r,recruiterUrl:e.recruiterUrl||null,firstSeen:r};return X(o),o[t]}function _e(t,e){if(!t||!e)return;let o=O(),r=o[t];r&&(r.jobIds||(r.jobIds=[]),r.jobIds.includes(e)||(r.jobIds.push(e),r.lastUpdated=new Date().toISOString(),A(o)))}function xe(t,e){if(!t||!e)return;let o=O(),r=o[t];if(!r)return;r.encounters||(r.encounters=[]);let s={date:J(),source:e.source||"unknown",action:e.action||"unknown"};e.jobId&&(s.jobId=String(e.jobId)),e.jobTitle&&(s.jobTitle=e.jobTitle),e.jobCompany&&(s.jobCompany=e.jobCompany),!r.encounters.some(a=>a.date===s.date&&a.source===s.source&&a.action===s.action&&(a.jobId||"")===(s.jobId||""))&&(r.encounters.push(s),r.lastUpdated=new Date().toISOString(),A(o))}function Le(t){return t&&O()[t]?.connectionStatus||null}function Fe(t,e,o={}){if(!t)return;let r=O(),n=r[t];if(n||(pe(t,o.profileInfo||{}),n=r[t]),!!n){if(n.connectionStatus=e,n.connectionDate=J(),n.connectionMethod=o.method||n.connectionMethod,n.messageSent=o.messageSent||n.messageSent,n.lastUpdated=new Date().toISOString(),o.profileInfo)for(let s of ue)o.profileInfo[s]!=null&&(n[s]=o.profileInfo[s]);A(r)}}function Z(){return D||(y(),D=U(Te,"quota.json"),(!D||!D.connections)&&(D={connections:[]}),D)}function fe(t){y(),D=t;try{R(Te,t)}catch(e){console.error(`[!] Failed to save quota.json: ${e.message}`)}}function ke(t,e="unknown"){let o=Z(),r=new Date;o.connections.push({date:r.toISOString().split("T")[0],time:r.toTimeString().split(" ")[0],profileUrl:t,source:e}),fe(o)}function ee(){let t=Z(),e=new Date;e.setDate(e.getDate()-7);let o=e.toISOString().split("T")[0];return t.connections.filter(r=>r.date>=o)}function Je(){let t=ee(),e=J(),o=t.filter(u=>u.date===e).length,r=t.length,n=o>=N,s=r>=M,i=!n&&!s,a={allowed:i,daily:{sent:o,max:N},weekly:{sent:r,max:M}};return i||(a.reason=n?`Daily limit reached (${o}/${N})`:`Weekly limit reached (${r}/${M})`),a}function Ne(){let t=ee(),e=J(),o=t.filter(n=>n.date===e).length,r=t.length;return{daily:{sent:o,max:N,remaining:N-o},weekly:{sent:r,max:M,remaining:M-r}}}function bt(t){if(!t)return null;let e=t.match(/jobs\/view\/(\d+)/);return e?e[1]:null}function Me(){y();let t=d.readdirSync(S).filter(s=>s.endsWith(".json")&&s!=="people.json"&&s!=="jobs.json"&&s!=="quota.json");if(t.length===0)return console.log("No daily files found to migrate."),{people:0,jobs:0};console.log(`
2
- Migrating from ${t.length} daily files...
3
- `);let e=O(),o=H(),r=0,n=0;t.sort();for(let s of t){let i=h.join(S,s),a;try{a=JSON.parse(d.readFileSync(i,"utf-8"))}catch(c){console.log(` Skipping ${s}: ${c.message}`);continue}let u=a.date||s.match(/\d{4}-\d{2}-\d{2}/)?.[0]||null,p=a.source||s.split("-").slice(0,-1).join("-").replace(/-\d{4}$/,"")||"unknown";if(!a.profiles||!Array.isArray(a.profiles)){console.log(` Skipping ${s}: no profiles array`);continue}console.log(` ${s}: ${a.profiles.length} profiles`);for(let c of a.profiles){let m=c.profileUrl;if(!m)continue;let l=e[m];if(l){l.seenCount=(l.seenCount||1)+1,u&&(!l.lastSeen||u>l.lastSeen)&&(l.lastSeen=u),u&&(!l.firstSeen||u<l.firstSeen)&&(l.firstSeen=u),p&&!l.sources?.includes(p)&&(l.sources=l.sources||[],l.sources.push(p));for(let f of ue)c[f]!=null&&(l[f]=c[f]);c.status&&c.processed&&(l.connectionStatus=c.status,l.connectionDate=u,c.funnelResult&&(l.connectionMethod=c.funnelResult.method||l.connectionMethod,l.messageSent=c.funnelResult.message||l.messageSent)),l.lastUpdated=new Date().toISOString()}else e[m]={name:c.name||null,title:c.title||null,location:c.location||null,company:c.company||null,connectionDegree:c.connectionDegree||null,connectionsCount:c.connectionsCount||null,mutualConnections:c.mutualConnections||null,photoUrl:c.photoUrl||null,hasVerifiedBadge:c.hasVerifiedBadge||!1,firstSeen:u,lastSeen:u,seenCount:1,sources:p?[p]:[],connectionStatus:c.processed&&c.status||null,connectionDate:c.processed?u:null,connectionMethod:c.funnelResult?.method||null,messageSent:c.funnelResult?.message||null,jobIds:[],lastUpdated:new Date().toISOString()},r++;if(c.fromJob?.url){let f=bt(c.fromJob.url);f&&(e[m].jobIds||(e[m].jobIds=[]),e[m].jobIds.includes(f)||e[m].jobIds.push(f),o[f]?o[f].recruiterUrl||(o[f].recruiterUrl=m):(o[f]={title:c.fromJob.title||null,company:c.fromJob.company||null,location:null,salary:null,url:c.fromJob.url,applied:!0,appliedDate:u,recruiterUrl:m,firstSeen:u},n++))}}}return A(e),X(o),console.log(`
4
- Migration complete!`),console.log(` People: ${Object.keys(e).length} total (${r} new)`),console.log(` Jobs: ${Object.keys(o).length} total (${n} new)`),{people:Object.keys(e).length,jobs:Object.keys(o).length}}function Ue(){return k||(y(),k=U(Ce,"sync-state.json")||{},k)}function Pe(t){y(),k=t;try{R(Ce,t)}catch(e){console.error(`[!] Failed to save sync-state.json: ${e.message}`)}}function de(){if(T)return T;y();try{d.existsSync(ae)&&(T=JSON.parse(d.readFileSync(ae,"utf-8")))}catch{}return T||(T={}),T}function qe(t){return Array.isArray(t.applications)}function Ve(){let t=de();if(qe(t))return t.applications;let e=[];for(let o of Object.values(t))o.applications&&e.push(...o.applications);return e}function Be(t){if(!t||!t.questions||t.questions.length===0)return;let e=de(),o=t.jobId||"unknown",n={applicationId:`${o}_${Date.now()}`,date:new Date().toISOString(),result:t.result||"submitted",formPages:t.formPages||null,duration:t.duration||null,retries:t.retries||0,failedField:t.failedField||null,questionCount:t.questions.length,questions:t.questions.map(s=>{let i={type:s.type||"input",question:s.labelText||s.question||"",answer:s.answer||""};return s.options&&(s.options.length<=vt?i.options=s.options:i.optionsOmitted=!0),s.page&&(i.page=s.page),s.wasPreFilled&&(i.wasPreFilled=!0),s.wasRetry&&(i.wasRetry=!0),typeof s.isRequired=="boolean"&&(i.isRequired=s.isRequired),s.wasOptional&&(i.wasOptional=!0),i})};qe(e)?(n.jobId=o,n.jobTitle=t.jobTitle||null,n.jobCompany=t.jobCompany||null,n.jobUrl=t.jobUrl||null,e.applications.push(n)):(e[o]||(e[o]={applications:[]}),e[o].applications.push(n));try{R(ae,e)}catch(s){console.error(`[!] Failed to save application-questions.json: ${s.message}`)}}function Qe(){let t=Ve(),e=new Map;for(let s of t)for(let i of s.questions||[]){let a=i.question.toLowerCase().trim();if(!a)continue;e.has(a)||e.set(a,{question:i.question,count:0,answers:new Map});let u=e.get(a);u.count++;let p=(i.answer||"").trim();u.answers.set(p,(u.answers.get(p)||0)+1)}let o=[...e.values()].sort((s,i)=>i.count-s.count),r=o.slice(0,15).map(s=>({question:s.question,count:s.count,topAnswer:[...s.answers.entries()].sort((i,a)=>a[1]-i[1])[0]?.[0]||""})),n=o.filter(s=>s.answers.size>1&&s.count>=3).map(s=>({question:s.question,count:s.count,answers:[...s.answers.entries()].sort((i,a)=>a[1]-i[1]).map(([i,a])=>`${i} (${a}x)`)}));return{totalApplications:t.length,uniqueQuestions:e.size,topQuestions:r,inconsistent:n}}function Ge(t){if(d.existsSync(h.join(S,".git")))try{let e={cwd:S,stdio:"pipe",timeout:1e4};ie("git add conversations.json people.json jobs.json quota.json sync-state.json run-log.json application-questions.json",e);try{ie("git diff --cached --quiet",e);return}catch{}let o=(t||`sync ${new Date().toISOString().slice(0,16)}`).replace(/"/g,'\\"');ie(`git commit -m "${o}"`,e)}catch(e){process.env.VERBOSE&&console.log(` [git] snapshot skipped: ${e.message?.slice(0,80)}`)}}var gt,St,S,ce,le,Te,K,Ce,ae,N,M,L,F,D,C,k,T,ue,vt,wt,me=Ie(()=>{gt=ht(import.meta.url),St=h.dirname(h.dirname(gt)),S=process.env.DATA_DIR?h.resolve(process.env.DATA_DIR,"collected-profiles"):h.join(St,"collected-profiles"),ce=h.join(S,"people.json"),le=h.join(S,"jobs.json"),Te=h.join(S,"quota.json"),K=h.join(S,"conversations.json"),Ce=h.join(S,"sync-state.json"),ae=h.join(S,"application-questions.json"),N=20,M=100,L=null,F=null,D=null,C=null,k=null,T=null;ue=["name","title","location","company","connectionDegree","connectionsCount","mutualConnections","photoUrl","hasVerifiedBadge"];vt=50;wt={loadPeople:O,savePeople:A,loadJobs:H,saveJobs:X,loadConversations:Re,saveConversations:Ae,upsertPerson:pe,upsertJob:Ee,linkPersonToJob:_e,addEncounter:xe,getPersonStatus:Le,updateConnectionStatus:Fe,migrateFromDailyFiles:Me,loadQuota:Z,saveQuota:fe,recordConnectionSent:ke,canSendConnection:Je,getRecentConnections:ee,getQuotaStatus:Ne,loadSyncState:Ue,saveSyncState:Pe,commitDataSnapshot:Ge,logApplicationQuestions:Be,getQuestionStats:Qe}});var et={};$e(et,{getJobDecision:()=>Ct,getLastVisit:()=>_t,getScoutStats:()=>xt,loadCompanies:()=>Ze,loadDiscoveredPeople:()=>ge,loadJobDecisions:()=>Y,loadScoutedJobs:()=>W,loadVisitLog:()=>te,recordVisit:()=>Et,resetCaches:()=>$t,saveCompany:()=>At,saveDiscoveredPerson:()=>Rt,saveScoutedJob:()=>he,setJobDecision:()=>Tt});import"dotenv/config";import*as v from"fs";import*as j from"path";import{fileURLToPath as Ot}from"url";function Q(){v.existsSync(_)||v.mkdirSync(_,{recursive:!0})}function G(t,e){let o=JSON.stringify(e,null,2),r=t+".tmp";v.writeFileSync(r,o);for(let n=0;n<3;n++)try{v.renameSync(r,t);return}catch(s){if(n<2&&(s.code==="EPERM"||s.code==="EBUSY")){let i=Date.now();for(;Date.now()-i<200;);continue}throw s}}function z(t){try{if(v.existsSync(t))return JSON.parse(v.readFileSync(t,"utf-8"))}catch{}return null}function $t(){P=null,q=null,V=null,B=null,E=null}function W(){return P||(P=z(We)||{},P)}function he(t,e){if(!t)return;Q();let o=W(),r=new Date().toISOString().slice(0,10);if(o[t]){for(let[n,s]of Object.entries(e))s!=null&&(o[t][n]=s);o[t].lastSeen=r}else o[t]={...e,scoutedAt:r,lastSeen:r};try{G(We,o)}catch(n){console.error(`[!] Failed to save scouted-jobs.json: ${n.message}`)}}function Y(){return q||(q=z(Xe)||{},q)}function Tt(t,e,o={}){if(!t||!e)return;Q();let r=Y(),n=new Date().toISOString().slice(0,10);r[t]?(r[t].status=e,r[t].updatedAt=n,o.reason&&(r[t].reason=o.reason),o.score!=null&&(r[t].score=o.score),o.source&&(r[t].source=o.source)):r[t]={status:e,decidedAt:n,updatedAt:n,reason:o.reason||null,score:o.score??null,source:o.source||null};try{G(Xe,r)}catch(s){console.error(`[!] Failed to save job-decisions.json: ${s.message}`)}}function Ct(t){return Y()[t]||null}function ge(){return V||(V=z(Ye)||{},V)}function Rt(t,e){if(!t)return;Q();let o=ge(),r=new Date().toISOString().slice(0,10);if(o[t]){let n=o[t];e.name&&(n.name=e.name),e.title&&(n.title=e.title),e.company&&(n.company=e.company),e.location&&(n.location=e.location),e.role&&(n.role=e.role),e.isConnected!=null&&(n.isConnected=e.isConnected),e.jobId&&(n.jobIds||(n.jobIds=[]),n.jobIds.includes(e.jobId)||n.jobIds.push(e.jobId)),n.lastSeen=r,n.seenCount=(n.seenCount||1)+1}else o[t]={name:e.name||null,title:e.title||null,company:e.company||null,location:e.location||null,role:e.role||null,source:e.source||"scout",isConnected:e.isConnected||!1,discoveredAt:r,lastSeen:r,seenCount:1,jobIds:e.jobId?[e.jobId]:[]};try{G(Ye,o)}catch(n){console.error(`[!] Failed to save discovered-people.json: ${n.message}`)}}function Ze(){return B||(B=z(He)||{},B)}function At(t,e){if(!t)return;Q();let o=Ze(),r=new Date().toISOString().slice(0,10);if(o[t]){let n=o[t];e.name&&(n.name=e.name),e.about&&(n.about=e.about),e.size&&(n.size=e.size),e.industries&&(n.industries=e.industries),e.jobId&&(n.jobIds||(n.jobIds=[]),n.jobIds.includes(e.jobId)||n.jobIds.push(e.jobId)),e.recruiterUrl&&(n.recruiterUrls||(n.recruiterUrls=[]),n.recruiterUrls.includes(e.recruiterUrl)||n.recruiterUrls.push(e.recruiterUrl)),n.lastSeen=r,n.jobCount=n.jobIds?.length||0}else o[t]={name:e.name||null,about:e.about||null,size:e.size||null,industries:e.industries||null,discoveredAt:r,lastSeen:r,jobIds:e.jobId?[e.jobId]:[],recruiterUrls:e.recruiterUrl?[e.recruiterUrl]:[],jobCount:e.jobId?1:0};try{G(He,o)}catch(n){console.error(`[!] Failed to save companies.json: ${n.message}`)}}function te(){return E||(E=z(Ke)||{visits:[]},Array.isArray(E.visits)||(E.visits=[]),E)}function Et(t,e={}){if(!t)return;Q();let o=te();o.visits.push({date:new Date().toISOString(),profileUrl:t,name:e.name||null,source:e.source||"manual"});let r=Date.now()-90*24*60*60*1e3;o.visits=o.visits.filter(n=>new Date(n.date).getTime()>r);try{G(Ke,o)}catch(n){console.error(`[!] Failed to save visit-log.json: ${n.message}`)}}function _t(t){let e=te();for(let o=e.visits.length-1;o>=0;o--)if(e.visits[o].profileUrl===t)return e.visits[o].date;return null}function xt(){let t=W(),e=ge(),o=Object.values(t),r=Object.values(e),n={};for(let l of o)l.company&&(n[l.company]=(n[l.company]||0)+1);let s=Object.entries(n).sort((l,f)=>f[1]-l[1]).slice(0,5).map(([l,f])=>`${l} (${f})`),i={};for(let l of o)for(let f of l.techStack||[])i[f]=(i[f]||0)+1;let a=Object.entries(i).sort((l,f)=>f[1]-l[1]).slice(0,8).map(([l,f])=>`${l} (${f})`),u=te(),p=new Date().toISOString().slice(0,10),c=u.visits.filter(l=>l.date.startsWith(p)).length,m=new Set(u.visits.map(l=>l.profileUrl)).size;return{totalJobs:o.length,totalRecruiters:r.length,topCompanies:s,topTech:a,totalVisited:m,todayVisited:c,lastScoutDate:o.length>0?o.sort((l,f)=>(f.scoutedAt||"").localeCompare(l.scoutedAt||""))[0]?.scoutedAt:null}}var Dt,It,_,We,Ye,Ke,He,Xe,P,q,V,B,E,oe=Ie(()=>{Dt=Ot(import.meta.url),It=j.dirname(j.dirname(Dt)),_=process.env.DATA_DIR?j.resolve(process.env.DATA_DIR,"market-research"):j.join(It,"market-research"),We=j.join(_,"scouted-jobs.json"),Ye=j.join(_,"discovered-people.json"),Ke=j.join(_,"visit-log.json"),He=j.join(_,"companies.json"),Xe=j.join(_,"job-decisions.json");P=null;q=null;V=null;B=null;E=null});import"dotenv/config";import*as $ from"fs";import*as x from"path";import{fileURLToPath as to}from"url";import{execFileSync as oo}from"child_process";me();oe();var Lt=new Set(["java","c#",".net","scala","python","typescript","javascript","react","node.js","aws","azure","docker","kubernetes","microservices","graphql","rest","sql","postgresql","mongodb","kafka","redis","ci/cd","terraform","spring"]),Ft=new Set(["iot","machine learning","ai","data engineering","spark","go","rust","kotlin","next.js","devops","sre"]),tt=["vancouver","british columbia"],kt=["canada","montreal","quebec","alberta","calgary","ottawa"],Jt=["toronto","ontario"],Nt=["india","pakistan","nigeria","philippines"];function Mt(t,e={}){let o=0,r=(t.techStack||[]).map(c=>c.toLowerCase()),n=0,s=0;for(let c of r)Lt.has(c)&&n++,Ft.has(c)&&s++;if(o+=Math.min(n*6,24),o+=Math.min(s*3,6),t.salary){let c=Ut(t.salary);c&&(c>=15e4?o+=20:c>=12e4?o+=15:c>=1e5?o+=10:c>=8e4?o+=5:c>=100&&c<1e3?o+=15:c>=70&&c<1e3&&(o+=10))}let i=(t.location||"").toLowerCase(),a=/remote/i.test(i)||t.workplaceType==="remote";Nt.some(c=>i.includes(c))?o-=30:a&&/united states|usa|\bus\b/i.test(i)?o+=25:a&&tt.some(c=>i.includes(c))?o+=22:a?o+=20:tt.some(c=>i.includes(c))?o+=18:kt.some(c=>i.includes(c))?o+=10:Jt.some(c=>i.includes(c))&&(o+=5),t.isEasyApply&&(o+=5);let u=(t.seniority||"").toLowerCase();u.includes("senior")||u.includes("lead")||u.includes("staff")?o+=10:u.includes("mid")?o+=5:(u.includes("entry")||u.includes("intern"))&&(o-=10),t.recruiterUrl&&(e.conversationRecruiters?.has(t.recruiterUrl)?o+=20:e.connectedRecruiters?.has(t.recruiterUrl)?o+=15:o+=3);let p=new Set(t.jobTags||[]);return p.has("managerial")?o+=8:p.has("senior-ic")&&(o+=5),p.has("junior")&&(o-=15),p.has("consultant")&&(o-=3),Math.max(0,Math.min(100,o))}function Se(t=10,e={}){let{easyApplyOnly:o=!0}=e,r=W(),n=O(),s=new Set,i=new Set;for(let[m,l]of Object.entries(n))l.connectionStatus==="connected"&&s.add(m);let a={connectedRecruiters:s,conversationRecruiters:i},u=Y(),p=new Set(["applied","skipped","expired","archived"]);return Object.entries(r).filter(([m,l])=>{if(typeof l!="object"||o&&!l.isEasyApply||l.applied)return!1;let f=u[m];return!(f&&p.has(f.status))}).map(([m,l])=>({jobId:m,score:Mt(l,a),job:l})).sort((m,l)=>{if(l.score!==m.score)return l.score-m.score;let f=m.job.scoutedAt||m.job.postedTime||"";return(l.job.scoutedAt||l.job.postedTime||"").localeCompare(f)}).slice(0,t)}function Ut(t){if(!t)return null;let e=t.match(/[\$£€CAD\s]*([\d,]+(?:\.\d+)?)\s*[kK]?/g);if(!e||e.length===0)return null;let o=e.map(r=>{let n=parseFloat(r.replace(/[^\d.]/g,""));return r.toLowerCase().includes("k")&&(n*=1e3),n}).filter(r=>r>0);return o.length>=2?(o[0]+o[1])/2:o.length===1?o[0]:null}oe();import"dotenv/config";import*as w from"fs";import*as I from"path";import{fileURLToPath as Gt}from"url";import"dotenv/config";import{createGraphClient as Pt,resolveToken as qt}from"@ilivemylife/graph-sdk";import{config as Vt}from"dotenv";Vt();var ot=qt()||process.env.TOKEN;ot||(console.error(`
1
+ var Ie=Object.defineProperty;var se=(e,n)=>{for(var o in n)Ie(e,o,{get:n[o],enumerable:!0})};import"dotenv/config";import*as E from"fs";import*as _ from"path";import{fileURLToPath as Jo}from"url";import{execFileSync as Uo}from"child_process";import"dotenv/config";import*as m from"fs";import*as g from"path";import{fileURLToPath as Ee}from"url";var De=Ee(import.meta.url),Ce=g.dirname(g.dirname(De)),O=process.env.DATA_DIR?g.resolve(process.env.DATA_DIR,"collected-profiles"):g.join(Ce,"collected-profiles"),Ne=g.join(O,"people.json"),Ko=g.join(O,"jobs.json"),zo=g.join(O,"quota.json"),Xo=g.join(O,"conversations.json"),Zo=g.join(O,"sync-state.json"),en=g.join(O,"application-questions.json"),on=g.join(O,"profile-history");var L=null,xe=null,_e=null,Fe=null,Le=null,Pe=null;function re(){L=null,xe=null,_e=null,Fe=null,Le=null,Pe=null}function Me(){m.existsSync(O)||m.mkdirSync(O,{recursive:!0})}function Je(e,n){if(!m.existsSync(e))return null;try{let o=JSON.parse(m.readFileSync(e,"utf-8"));try{m.copyFileSync(e,e+".backup")}catch{}return o}catch(o){console.error(`[!] ${n} is corrupted: ${o.message}`),console.error(` File: ${g.resolve(e)}`);let t=e+".backup";if(m.existsSync(t))try{let s=JSON.parse(m.readFileSync(t,"utf-8"));console.error(` Restored from ${t}`);try{m.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{m.copyFileSync(e,e+".corrupted")}catch{}return null}}function ie(){return L||(Me(),L=Je(Ne,"people.json")||{},L)}import"dotenv/config";import*as v from"fs";import*as b from"path";import{fileURLToPath as Ue}from"url";var qe=Ue(import.meta.url),Be=b.dirname(b.dirname(qe)),D=process.env.DATA_DIR?b.resolve(process.env.DATA_DIR,"market-research"):b.join(Be,"market-research"),ce=b.join(D,"scouted-jobs.json"),rn=b.join(D,"discovered-people.json"),cn=b.join(D,"visit-log.json"),ln=b.join(D,"companies.json"),Ve=b.join(D,"job-decisions.json");function Ge(){v.existsSync(D)||v.mkdirSync(D,{recursive:!0})}function We(e,n){let o=JSON.stringify(n,null,2),t=e+".tmp";v.writeFileSync(t,o);for(let s=0;s<3;s++)try{v.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 le(e){try{if(v.existsSync(e))return JSON.parse(v.readFileSync(e,"utf-8"))}catch{}return null}function ae(){P=null,M=null,Ye=null,He=null,Qe=null}var P=null;function V(){return P||(P=le(ce)||{},P)}function ue(e,n){if(!e)return;Ge();let o=V(),t=new Date().toISOString().slice(0,10);if(o[e]){for(let[s,r]of Object.entries(n))r!=null&&(o[e][s]=r);o[e].lastSeen=t}else o[e]={...n,scoutedAt:t,lastSeen:t};try{We(ce,o)}catch(s){console.error(`[!] Failed to save scouted-jobs.json: ${s.message}`)}}var M=null;function fe(){return M||(M=le(Ve)||{},M)}var Ye=null;var He=null;var Qe=null;var Ke=new Set(["java","c#",".net","scala","python","typescript","javascript","react","node.js","aws","azure","docker","kubernetes","microservices","graphql","rest","sql","postgresql","mongodb","kafka","redis","ci/cd","terraform","spring"]),ze=new Set(["iot","machine learning","ai","data engineering","spark","go","rust","kotlin","next.js","devops","sre"]),pe=["vancouver","british columbia"],Xe=["canada","montreal","quebec","alberta","calgary","ottawa"],Ze=["toronto","ontario"],eo=["india","pakistan","nigeria","philippines"];function oo(e,n={}){let o=0,t=(e.techStack||[]).map(a=>a.toLowerCase()),s=0,r=0;for(let a of t)Ke.has(a)&&s++,ze.has(a)&&r++;if(o+=Math.min(s*6,24),o+=Math.min(r*3,6),e.salary){let a=no(e.salary);a&&(a>=15e4?o+=20:a>=12e4?o+=15:a>=1e5?o+=10:a>=8e4?o+=5:a>=100&&a<1e3?o+=15:a>=70&&a<1e3&&(o+=10))}let i=(e.location||"").toLowerCase(),u=/remote/i.test(i)||e.workplaceType==="remote";eo.some(a=>i.includes(a))?o-=30:u&&/united states|usa|\bus\b/i.test(i)?o+=25:u&&pe.some(a=>i.includes(a))?o+=22:u?o+=20:pe.some(a=>i.includes(a))?o+=18:Xe.some(a=>i.includes(a))?o+=10:Ze.some(a=>i.includes(a))&&(o+=5),e.isEasyApply&&(o+=5);let l=(e.seniority||"").toLowerCase();l.includes("senior")||l.includes("lead")||l.includes("staff")?o+=10:l.includes("mid")?o+=5:(l.includes("entry")||l.includes("intern"))&&(o-=10),e.recruiterUrl&&(n.conversationRecruiters?.has(e.recruiterUrl)?o+=20:n.connectedRecruiters?.has(e.recruiterUrl)?o+=15:o+=3);let f=new Set(e.jobTags||[]);return f.has("managerial")?o+=8:f.has("senior-ic")&&(o+=5),f.has("junior")&&(o-=15),f.has("consultant")&&(o-=3),Math.max(0,Math.min(100,o))}function G(e=10,n={}){let{easyApplyOnly:o=!0}=n,t=V(),s=ie(),r=new Set,i=new Set;for(let[h,d]of Object.entries(s))d.connectionStatus==="connected"&&r.add(h);let u={connectedRecruiters:r,conversationRecruiters:i},l=fe(),f=new Set(["applied","skipped","expired","archived"]);return Object.entries(t).filter(([h,d])=>{if(typeof d!="object"||o&&!d.isEasyApply||d.applied)return!1;let y=l[h];return!(y&&f.has(y.status))}).map(([h,d])=>({jobId:h,score:oo(d,u),job:d})).sort((h,d)=>{if(d.score!==h.score)return d.score-h.score;let y=h.job.scoutedAt||h.job.postedTime||"";return(d.job.scoutedAt||d.job.postedTime||"").localeCompare(y)}).slice(0,e)}function no(e){if(!e)return null;let n=e.match(/[\$£€CAD\s]*([\d,]+(?:\.\d+)?)\s*[kK]?/g);if(!n||n.length===0)return null;let o=n.map(t=>{let s=parseFloat(t.replace(/[^\d.]/g,""));return t.toLowerCase().includes("k")&&(s*=1e3),s}).filter(t=>t>0);return o.length>=2?(o[0]+o[1])/2:o.length===1?o[0]:null}import"dotenv/config";import*as $ from"fs";import*as T from"path";import{fileURLToPath as lo}from"url";import"dotenv/config";import{createGraphClient as to,resolveToken as so}from"@ilivemylife/graph-sdk";import{config as ro}from"dotenv";ro();var de=so()||process.env.TOKEN;de||(console.error(`
5
2
  [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.
6
3
  `),console.error(" Easiest way:"),console.error(" 1. npm install -g @ilivemylife/graph-sdk"),console.error(` 2. ilml login your@email.com yourpassword
7
4
  `),console.error(` Or add ILML_TOKEN=... to .env
8
- `),process.exit(1));var nt=Pt({token:ot});function Bt(t){if(!t||t<1e3)return`${t||0}ms`;let e=Math.round(t/1e3);if(e<60)return`${e}s`;let o=Math.floor(e/60),r=e%60;return r?`${o}m ${r}s`:`${o}m`}function Qt({scriptName:t,mode:e,status:o,error:r,duration:n}){let s=o==="error"?"\u2717":o==="quota-reached"?"\u26A0":"\u2713",i=new Date().toISOString().slice(0,16).replace("T"," "),a=n?` ${Bt(n)}`:"",u=o==="error"?`error${r?`: ${String(r).slice(0,80)}`:""}`:o==="quota-reached"?"daily quota reached":"done",p=e&&e!=="default"?` (${e})`:"";return`${s} ${t}${p} \u2014 ${u} \u2014 ${i}${a}`}async function st({scriptName:t,mode:e,status:o,summaryLines:r,error:n,duration:s}){let i=process.env.NODE_RUN_REPORTS;if(!i||!Array.isArray(r)||r.length===0)return;let u=`**${Qt({scriptName:t,mode:e,status:o,error:n,duration:s})}**
5
+ `),process.exit(1));var he=to({token:de});function io(e){if(!e||e<1e3)return`${e||0}ms`;let n=Math.round(e/1e3);if(n<60)return`${n}s`;let o=Math.floor(n/60),t=n%60;return t?`${o}m ${t}s`:`${o}m`}function co({scriptName:e,mode:n,status:o,error:t,duration:s}){let r=o==="error"?"\u2717":o==="quota-reached"?"\u26A0":"\u2713",i=new Date().toISOString().slice(0,16).replace("T"," "),u=s?` ${io(s)}`:"",l=o==="error"?`error${t?`: ${String(t).slice(0,80)}`:""}`:o==="quota-reached"?"daily quota reached":"done",f=n&&n!=="default"?` (${n})`:"";return`${r} ${e}${f} \u2014 ${l} \u2014 ${i}${u}`}async function me({scriptName:e,mode:n,status:o,summaryLines:t,error:s,duration:r}){let i=process.env.NODE_RUN_REPORTS;if(!i||!Array.isArray(t)||t.length===0)return;let l=`**${co({scriptName:e,mode:n,status:o,error:s,duration:r})}**
9
6
 
10
- ${r.join(`
11
- `)}`;try{await nt.addMessage(i,u)}catch(p){console.error(`[runReport] Failed to post end-of-session message: ${p.message}`)}}var zt=Gt(import.meta.url),Wt=I.dirname(I.dirname(zt)),ye=process.env.DATA_DIR?I.resolve(process.env.DATA_DIR,"collected-profiles"):I.join(Wt,"collected-profiles"),je=I.join(ye,"run-log.json");function Yt(t,e){let o=JSON.stringify(e,null,2),r=t+".tmp";w.writeFileSync(r,o);for(let n=0;n<3;n++)try{w.renameSync(r,t);return}catch(s){if(n<2&&(s.code==="EPERM"||s.code==="EBUSY")){let i=Date.now();for(;Date.now()-i<200;);continue}throw s}}function rt(){try{if(!w.existsSync(je))return{runs:[]};let t=JSON.parse(w.readFileSync(je,"utf-8"));return!t.runs||!Array.isArray(t.runs)?{runs:[]}:t}catch{return{runs:[]}}}function it(t){w.existsSync(ye)||w.mkdirSync(ye,{recursive:!0});try{Yt(je,t)}catch(e){console.error(`[!] Failed to save run-log.json: ${e.message}`)}}function ct(t,e="default"){let o=rt();for(let i of o.runs)i.status==="running"&&(i.status="interrupted",i.result="interrupted",i.timestamp&&(i.duration=Date.now()-new Date(i.timestamp).getTime()));let r=new Date,n={timestamp:r.toISOString(),script:t,mode:e,status:"running",result:null,duration:null,stats:null,error:null};o.runs.push(n);let s=Date.now()-30*24*60*60*1e3;return o.runs=o.runs.filter(i=>new Date(i.timestamp).getTime()>s),it(o),{script:t,mode:e,startTime:r.getTime(),runIndex:o.runs.length-1}}function be(t,e,o=null,r=null){if(!t)return;let n=null;try{let i=rt(),a=i.runs[t.runIndex];if(a&&a.status==="running"&&a.script===t.script)a.status=e,a.result=e,a.duration=Date.now()-t.startTime,a.stats=o,a.error=r,n=a.duration;else{let u=i.runs.find(p=>p.script===t.script&&p.status==="running"&&p.timestamp===new Date(t.startTime).toISOString());u&&(u.status=e,u.result=e,u.duration=Date.now()-t.startTime,u.stats=o,u.error=r,n=u.duration)}it(i)}catch(i){console.error(`[runLog] Failed to end run: ${i.message}`)}let s=o&&Array.isArray(o.summaryLines)?o.summaryLines:null;if(s&&s.length>0&&process.env.NODE_RUN_REPORTS){let i=e==="error"?"error":o&&o.dailyLimitHit?"quota-reached":"done";st({scriptName:t.script,mode:t.mode,status:i,summaryLines:s,error:r,duration:n??Date.now()-t.startTime}).catch(a=>console.error(`[runLog] postRunReport failed: ${a.message}`))}}import"dotenv/config";import*as b from"fs";import*as g from"path";import{fileURLToPath as Kt}from"url";var Ht=Kt(import.meta.url),Xt=g.dirname(Ht),ut=g.dirname(g.dirname(Xt)),ne=process.env.DATA_DIR?g.resolve(process.env.DATA_DIR,"collected-profiles"):g.join(ut,"collected-profiles"),se=process.env.DATA_DIR?g.resolve(process.env.DATA_DIR,"market-research"):g.join(ut,"market-research"),ve=1,Zt=[{version:1,file:"./v001-normalize-jobs-and-questions.mjs"}];function pt(t){return g.join(t,".schema-version")}function we(t){let e=pt(t);if(!b.existsSync(e))return{version:0,migrations:[]};try{return JSON.parse(b.readFileSync(e,"utf-8"))}catch{return{version:0,migrations:[]}}}function eo(t,e){b.writeFileSync(pt(t),JSON.stringify(e,null,2))}function lt(t,e){if(!b.existsSync(t))return;let o=`.pre-v${String(e).padStart(3,"0")}.backup`,r=b.readdirSync(t).filter(n=>n.endsWith(".json")&&!n.includes(".backup"));for(let n of r){let s=g.join(t,n),i=g.join(t,n+o);try{b.copyFileSync(s,i)}catch(a){console.error(` [!] Failed to backup ${n}: ${a.message}`)}}r.length>0&&console.log(` Backed up ${r.length} files in ${g.basename(t)}/ (${o})`)}var at=!1;async function ft(){if(at)return;at=!0;let t=we(ne),e=we(se),o=Math.min(t.version,e.version);if(o>=ve)return;console.log(`
12
- \u{1F4E6} Schema migration: v${o} \u2192 v${ve}`);let r=Zt.filter(n=>n.version>o);for(let n of r){console.log(`
13
- Running migration v${String(n.version).padStart(3,"0")}...`),lt(ne,n.version),lt(se,n.version);let s=await import(n.file);try{await s.up(ne,se)}catch(u){console.error(`
14
- [FATAL] Migration v${n.version} failed: ${u.message}`),console.error(` Backups are in collected-profiles/ and market-research/ (.pre-v${String(n.version).padStart(3,"0")}.backup)`),console.error(` Fix the issue and restart.
15
- `),process.exit(1)}let i=new Date().toISOString(),a={version:n.version,name:s.NAME||`v${String(n.version).padStart(3,"0")}`,appliedAt:i};for(let u of[ne,se]){if(!b.existsSync(u))continue;let p=we(u);p.version=n.version,p.migratedAt=i,p.migrations.push(a),eo(u,p)}console.log(` \u2713 Migration v${String(n.version).padStart(3,"0")} complete`)}try{let{resetCaches:n}=await Promise.resolve().then(()=>(me(),ze));n()}catch{}try{let{resetCaches:n}=await Promise.resolve().then(()=>(oe(),et));n()}catch{}console.log(`
16
- \u2713 Schema is now at v${ve}
17
- `)}var no=to(import.meta.url),so=x.dirname(no),Oe=process.env.DATA_DIR?x.resolve(process.env.DATA_DIR,"collected-profiles"):x.join(so,"collected-profiles");await ft();var De=process.argv.slice(2),dt=parseInt(De.find(t=>t.startsWith("--max="))?.split("=")[1]||"50"),re=parseInt(De.find(t=>t.startsWith("--min-score="))?.split("=")[1]||"50"),ro=De.includes("--list");function io(){let e=Se(20,{easyApplyOnly:!0}).filter(o=>o.score>=re);if(console.log(`
18
- \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(` APPLY QUEUE (${e.length} jobs, min score ${re})`),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"),e.length===0)console.log(" No jobs in queue. Run: npm run scout");else for(let o=0;o<e.length;o++){let{score:r,job:n,jobId:s}=e[o],i=(n.techStack||[]).slice(0,4).join(", ");console.log(` ${String(o+1).padStart(2)}. [${r}] ${(n.title||"?").slice(0,35)} @ ${(n.company||"?").slice(0,20)}`),console.log(` ${n.salary||"no salary"} | ${n.location||"?"} | ${i||"no tech"}`),console.log(` ${n.url}`)}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
19
- `)}async function co(){if(console.log(`
7
+ ${t.join(`
8
+ `)}`;try{await he.addMessage(i,l)}catch(f){console.error(`[runReport] Failed to post end-of-session message: ${f.message}`)}}var ao=lo(import.meta.url),uo=T.dirname(T.dirname(ao)),W=process.env.DATA_DIR?T.resolve(process.env.DATA_DIR,"collected-profiles"):T.join(uo,"collected-profiles"),Y=T.join(W,"run-log.json");function fo(e,n){let o=JSON.stringify(n,null,2),t=e+".tmp";$.writeFileSync(t,o);for(let s=0;s<3;s++)try{$.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 ge(){try{if(!$.existsSync(Y))return{runs:[]};let e=JSON.parse($.readFileSync(Y,"utf-8"));return!e.runs||!Array.isArray(e.runs)?{runs:[]}:e}catch{return{runs:[]}}}function ye(e){$.existsSync(W)||$.mkdirSync(W,{recursive:!0});try{fo(Y,e)}catch(n){console.error(`[!] Failed to save run-log.json: ${n.message}`)}}function Se(e,n="default"){let o=ge();for(let i of o.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:n,status:"running",result:null,duration:null,stats:null,error:null};o.runs.push(s);let r=Date.now()-30*24*60*60*1e3;return o.runs=o.runs.filter(i=>new Date(i.timestamp).getTime()>r),ye(o),{script:e,mode:n,startTime:t.getTime(),runIndex:o.runs.length-1}}function H(e,n,o=null,t=null){if(!e)return;let s=null;try{let i=ge(),u=i.runs[e.runIndex];if(u&&u.status==="running"&&u.script===e.script)u.status=n,u.result=n,u.duration=Date.now()-e.startTime,u.stats=o,u.error=t,s=u.duration;else{let l=i.runs.find(f=>f.script===e.script&&f.status==="running"&&f.timestamp===new Date(e.startTime).toISOString());l&&(l.status=n,l.result=n,l.duration=Date.now()-e.startTime,l.stats=o,l.error=t,s=l.duration)}ye(i)}catch(i){console.error(`[runLog] Failed to end run: ${i.message}`)}let r=o&&Array.isArray(o.summaryLines)?o.summaryLines:null;if(r&&r.length>0&&process.env.NODE_RUN_REPORTS){let i=n==="error"?"error":o&&o.dailyLimitHit?"quota-reached":"done";me({scriptName:e.script,mode:e.mode,status:i,summaryLines:r,error:t,duration:s??Date.now()-e.startTime}).catch(u=>console.error(`[runLog] postRunReport failed: ${u.message}`))}}import"dotenv/config";import*as c from"fs";import*as ee from"os";import*as p from"path";import{fileURLToPath as ko}from"url";var z={};se(z,{NAME:()=>ho,VERSION:()=>po,up:()=>yo});import*as w from"fs";import*as J from"path";var po=1,ho="normalize-jobs-and-questions",mo=["description","techStack","jobTags","seniority","employmentType","industries","jobFunction","postedTime","applicantCount","companySize","workplaceType","contractType","isEasyApply","applyType","companyUrl","scoutedAt","lastSeen"],go=50;function Q(e){return w.existsSync(e)?JSON.parse(w.readFileSync(e,"utf-8")):null}function K(e,n){let o=JSON.stringify(n,null,2),t=e+".tmp";try{w.writeFileSync(t,o)}catch(s){try{w.unlinkSync(t)}catch{}throw s}try{w.renameSync(t,e)}catch(s){try{w.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 yo(e,n){let o=J.join(e,"jobs.json"),t=J.join(n,"scouted-jobs.json"),s=J.join(e,"application-questions.json"),r=Q(o)||{},i=Q(t)||{},u=0,l=[];for(let a of Object.keys(r)){if(!i[a])continue;let h=i[a],d=r[a];for(let y of mo)h[y]!=null&&d[y]==null&&(d[y]=h[y]);l.push(a),u++}u>0?(K(o,r),console.log(` Phase 1: Merged ${u} scouted jobs into jobs.json`)):console.log(" Phase 1: No overlapping jobs to merge");let f=Q(s);if(f&&Array.isArray(f.applications)){let a={},h=0;for(let d of f.applications){let y=d.jobId||"unknown";a[y]||(a[y]={applications:[]});let B=(d.questions||[]).map(j=>{let F={type:j.type||"input",question:j.question||"",answer:j.answer||""};return j.options&&(j.options.length<=go?F.options=j.options:(F.optionsOmitted=!0,h++)),j.page&&(F.page=j.page),j.wasPreFilled&&(F.wasPreFilled=!0),j.wasRetry&&(F.wasRetry=!0),F});a[y].applications.push({applicationId:d.applicationId,date:d.date,result:d.result||"submitted",formPages:d.formPages||null,duration:d.duration||null,retries:d.retries||0,failedField:d.failedField||null,questionCount:d.questionCount||B.length,questions:B})}K(s,a),console.log(` Phase 2: Rekeyed ${f.applications.length} applications by jobId (${Object.keys(a).length} unique jobs)`),h>0&&console.log(` Phase 2: Stripped bloated options from ${h} questions`)}else f&&!f.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 a of l)delete i[a];K(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 X={};se(X,{NAME:()=>bo,VERSION:()=>So,up:()=>wo});import*as k from"fs";import*as be from"path";var So=2,bo="reset-unread-flags";function vo(e){return k.existsSync(e)?JSON.parse(k.readFileSync(e,"utf-8")):null}function $o(e,n){let o=JSON.stringify(n,null,2),t=e+".tmp";try{k.writeFileSync(t,o)}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 wo(e){let n=be.join(e,"conversations.json"),o=vo(n);if(!o){console.log(" conversations.json not found \u2014 nothing to reset.");return}let t=0;for(let s of Object.keys(o))o[s].unread===!0&&(o[s].unread=!1,t++);if(t===0){console.log(" No stale unread flags found \u2014 nothing to do.");return}$o(n,o),console.log(` Reset unread=false on ${t} conversations. Run 'ilml linkedin sync-all' to re-establish real unread state from LinkedIn.`)}var jo=ko(import.meta.url),Ao=p.dirname(jo),Oo=p.dirname(p.dirname(Ao));function Ro(e){return e&&(e==="~"?ee.homedir():e.startsWith("~/")||e.startsWith("~\\")?p.join(ee.homedir(),e.slice(2)):e)}var A=process.env.DATA_DIR?p.resolve(Ro(process.env.DATA_DIR)):Oo,N=p.join(A,"collected-profiles"),x=p.join(A,"market-research"),S=p.join(A,".schema-version"),ve=[p.join(N,".schema-version"),p.join(x,".schema-version")],I=2,R=[{version:1,module:z},{version:2,module:X}];(function(){R.sort((t,s)=>t.version-s.version);let n=new Set;for(let t of R){if(!Number.isInteger(t.version)||t.version<1)throw new Error(`MIGRATION_REGISTRY: invalid version ${JSON.stringify(t.version)} (must be positive integer)`);if(n.has(t.version))throw new Error(`MIGRATION_REGISTRY: duplicate version v${t.version}`);n.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 o=R.length?R[R.length-1].version:0;if(o!==I)throw new Error(`MIGRATION_REGISTRY: highest version v${o} but CURRENT_SCHEMA_VERSION=${I}. Forgot to bump CURRENT_SCHEMA_VERSION when adding the new migration?`);for(let t=0;t<R.length;t++){let s=t+1;if(R[t].version!==s)throw new Error(`MIGRATION_REGISTRY: gap detected \u2014 expected v${s} at position ${t} but found v${R[t].version}`)}})();function je(){process.env.DATA_DIR!==void 0&&process.env.DATA_DIR.trim()===""&&process.env.DATA_DIR.length>0&&(console.error(`
9
+ [FATAL] DATA_DIR is set to whitespace ("${process.env.DATA_DIR}"). Either unset it or set a real path.
10
+ `),process.exit(1));try{c.mkdirSync(A,{recursive:!0})}catch(n){n.code!=="EEXIST"&&(console.error(`
11
+ [FATAL] Cannot create DATA_DIR (${A}): ${n.message}`),console.error(` Fix the path or permissions and try again.
12
+ `),process.exit(1))}let e;try{e=c.statSync(A)}catch(n){console.error(`
13
+ [FATAL] DATA_DIR (${A}) is not accessible: ${n.message}
14
+ `),process.exit(1)}e.isDirectory()||(console.error(`
15
+ [FATAL] DATA_DIR (${A}) exists but is NOT a directory.`),console.error(` Looks like a typo \u2014 your DATA_DIR points at a file. Fix the path and try again.
16
+ `),process.exit(1));for(let n of[N,x]){let o;try{o=c.lstatSync(n)}catch(t){if(t.code==="ENOENT")continue;throw t}o.isSymbolicLink()&&(console.error(`
17
+ [FATAL] Data subdirectory is a symbolic link: ${n}`),console.error(" Refusing to follow \u2014 backup code would copy whatever the link points at."),console.error(` If this is intentional, replace the symlink with the real directory.
18
+ `),process.exit(1))}}var C=p.join(A,".migration-lock");function To(){let e=JSON.stringify({pid:process.pid,startedAt:new Date().toISOString()},null,2);try{return c.writeFileSync(C,e,{flag:"wx"}),!0}catch(t){if(t.code!=="EEXIST")throw t}let n;try{n=JSON.parse(c.readFileSync(C,"utf-8"))}catch{n=null}if(n&&Number.isInteger(n.pid)){let t=!1;try{process.kill(n.pid,0),t=!0}catch(i){i.code==="EPERM"&&(t=!0)}let s=24*60*60*1e3,r=1/0;if(n.startedAt){let i=Date.parse(n.startedAt);Number.isNaN(i)||(r=Date.now()-i)}t&&r<s&&(console.error(`
19
+ [FATAL] Another plugin process is currently running migrations:`),console.error(` PID: ${n.pid}`),console.error(` Started at: ${n.startedAt||"(unknown)"}`),console.error(` Lock file: ${C}
20
+ `),console.error(" Wait for it to finish, or if you're sure it's not actually running,"),console.error(` delete the lock file manually and re-run.
21
+ `),process.exit(1)),t&&r>=s&&console.log(` Lock holder PID ${n.pid} is alive but lock is older than 24h \u2014 assuming PID reuse, taking over.`)}console.log(` Stale migration lock from PID ${n?.pid??"?"} found \u2014 taking over.`);let o=C+".tmp";try{return c.writeFileSync(o,e),c.renameSync(o,C),!0}catch(t){try{c.unlinkSync(o)}catch{}throw new Error(`Failed to take over stale migration lock: ${t.message}`)}}function oe(){try{if(JSON.parse(c.readFileSync(C,"utf-8")).pid!==process.pid)return;c.unlinkSync(C)}catch{}}var $e=!1;function Io(){if($e)return;$e=!0;let e=()=>{try{oe()}catch{}process.exit(1)};for(let n of["SIGINT","SIGTERM","SIGHUP"])try{process.on(n,e)}catch{}}function Eo(){if(c.existsSync(S))return;let e=[],n=[];for(let r of ve)if(c.existsSync(r))try{e.push({file:r,schema:U(r)})}catch(i){n.push({file:r,err:i.message})}if(n.length>0){console.error(`
22
+ [FATAL] Legacy .schema-version file(s) corrupted \u2014 cannot determine current schema version:`);for(let r of n)console.error(` ${r.file}: ${r.err}`);console.error(`
23
+ Repair the file(s) (likely just an integer 'version' field \u2014 see another working DATA_DIR`),console.error(` or default to {"version":1,"migrations":[]} if you're sure you're on schema v1) and try again.
24
+ `),process.exit(1)}if(e.length===0)return;let o=e.map(r=>r.schema.version);if(new Set(o).size>1){console.error(`
25
+ [FATAL] Legacy .schema-version files disagree:`);for(let r of e)console.error(` ${r.file}: v${r.schema.version}`);console.error(" This means a previous migration crashed between writing the two files."),console.error(` Reconcile manually (delete the wrong one, keep the right one), then re-run.
26
+ `),process.exit(1)}let t=e[0].schema,s=S+".tmp";try{c.writeFileSync(s,JSON.stringify(t,null,2)),c.renameSync(s,S)}catch(r){try{c.unlinkSync(s)}catch{}throw r}for(let r of ve)if(c.existsSync(r))try{c.unlinkSync(r)}catch{}console.log(` Consolidated legacy .schema-version files \u2192 ${S}`)}function Do(e){if(e===null||typeof e!="object"||Array.isArray(e))return null;let n=Number(e.version);return!Number.isFinite(n)||n<0||!Number.isInteger(n)?null:(e.version=n,Array.isArray(e.migrations)||(e.migrations=[]),e)}function U(e){let n=c.readFileSync(e,"utf-8");return n.charCodeAt(0)===65279&&(n=n.slice(1)),JSON.parse(n)}function we(){if(Eo(),!c.existsSync(S))return{version:0,migrations:[]};let e;try{e=U(S)}catch(o){if(o.code==="ENOENT")return{version:0,migrations:[]};console.error(`
27
+ [FATAL] ${S} is not valid JSON: ${o.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.
28
+ `),process.exit(1)}let n=Do(e);return n===null&&(console.error(`
29
+ [FATAL] ${S} 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.
30
+ `),process.exit(1)),n}function Ae(e){try{c.chmodSync(e,384)}catch{}}function Co(e){if(je(),c.existsSync(S))try{if(c.lstatSync(S).isSymbolicLink())throw new Error(`SCHEMA_FILE is a symbolic link: ${S}. Refusing to write through it (potential symlink attack). Remove the link and re-run.`)}catch(o){if(o.message?.includes("symbolic link"))throw o}let n=S+".tmp";try{c.writeFileSync(n,JSON.stringify(e,null,2)),Ae(n),c.renameSync(n,S)}catch(o){try{c.unlinkSync(n)}catch{}throw o}}function ke(e,n){if(!c.existsSync(e))return;let o=`.pre-v${String(n).padStart(3,"0")}.backup`,t=c.readdirSync(e).filter(u=>{let l=u.toLowerCase();if(l.includes(".backup")||l===".schema-version"||l.endsWith(".tmp"))return!1;try{return c.statSync(p.join(e,u)).isFile()}catch{return!1}}),s=0,r=0,i=[];for(let u of t){let l=p.join(e,u),f=p.join(e,u+o);if(c.existsSync(f)){r++;continue}try{Re(l,f),s++}catch(a){i.push({file:u,err:a.message})}}if(i.length>0){let u=new Error(`Failed to back up ${i.length} file(s) in ${p.basename(e)}/: `+i.map(l=>`${l.file} (${l.err})`).join(", "));throw u.code="BACKUP_INCOMPLETE",u}if(s>0||r>0){let u=r>0?` (${r} pre-existing backup(s) preserved)`:"";console.log(` Backed up ${s} files in ${p.basename(e)}/ (${o})${u}`)}}var Z=!1;async function Oe(){if(Z)return;je(),Lo();let n=we().version;if(n===I){Z=!0;return}Io(),To(),process.on("exit",oe),n>I&&(console.error(`
31
+ [FATAL] Your data schema is at v${n}, but this plugin build only understands up to v${I}.`),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.
32
+ `),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${I}.
33
+ `),process.exit(1)),console.log(`
34
+ \u{1F4E6} Schema migration: v${n} \u2192 v${I}`);let o=Po();if(!o.ok){console.error(`
35
+ [FATAL] Cannot migrate \u2014 current data is corrupted:`);for(let s of o.broken)console.error(` ${s.dir}/${s.file}: ${s.err}`);console.error(`
36
+ 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'.
37
+ `),process.exit(1)}let t=R.filter(s=>s.version>n);for(let s of t){console.log(`
38
+ Running migration v${String(s.version).padStart(3,"0")}...`);let r=`v${String(s.version).padStart(3,"0")}`,i=`.pre-${r}.backup`;try{ke(N,s.version),ke(x,s.version)}catch(l){console.error(`
39
+ [FATAL] Pre-migration backup failed for ${r}: ${l.message}`),console.error(" Migration NOT applied. Repair the underlying issue (disk space, permissions, locked files)"),console.error(` and re-run the plugin command.
40
+ `),process.exit(1)}let u=s.module;try{await u.up(N,x)}catch(l){console.error(`
41
+ [FATAL] Migration ${r} failed: ${l.message}`),console.error(` Attempting auto-rollback from ${i} files...`);try{let f=Mo(i);f.ok?(console.error(` \u2713 Auto-rollback restored ${f.restored} files. Schema stays at v${n}.`),console.error(` The migration will be retried on next plugin command. Fix the root cause first.
42
+ `)):(console.error(` [!] Auto-rollback INCOMPLETE: ${f.error}`),console.error(` Manual recovery: copy *${i} files back over the originals.
43
+ `))}catch(f){console.error(` [!] Auto-rollback CRASHED while restoring: ${f.message}`),console.error(` Manual recovery: copy *${i} files back over the originals.
44
+ `)}process.exit(1)}try{let l=new Date().toISOString(),f={version:s.version,name:u.NAME||r,appliedAt:l},a=we();a.version=s.version,a.migratedAt=l,a.migrations=a.migrations||[],a.migrations.push(f),Co(a)}catch(l){console.error(`
45
+ [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${n}.`),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.
46
+ `),process.exit(1)}console.log(` \u2713 Migration ${r} complete`)}try{re()}catch{}try{ae()}catch{}Z=!0,oe(),console.log(`
47
+ \u2713 Schema is now at v${I}
48
+ `)}var No=[/\.json\.tmp$/,/\.schema-version\.tmp$/,/\.backup\.tmp$/];function xo(e){let n=e.toLowerCase();return No.some(o=>o.test(n))}var _o=6e4;function Fo(){let e=Date.now(),n=[];for(let o of[A,N,x]){if(!c.existsSync(o))continue;let t;try{t=c.readdirSync(o)}catch{continue}for(let s of t){if(!xo(s))continue;let r=p.join(o,s);try{let i=c.statSync(r);if(!i.isFile()||e-i.mtimeMs<_o)continue;n.push({dir:o,file:s,fullPath:r})}catch{}}}return n}function Lo({verbose:e=!1}={}){let n=Fo();if(n.length===0)return e&&console.log(" No orphan .tmp files found."),0;let o=0;for(let t of n)try{c.unlinkSync(t.fullPath),o++,e&&console.log(` Removed orphan: ${p.basename(t.dir)}/${t.file}`)}catch(s){console.warn(` [!] Could not remove orphan ${t.fullPath}: ${s.message}`)}return!e&&o>0&&console.log(` Cleaned up ${o} orphan .tmp file(s) from previous run.`),o}function Po(){let e=[];for(let n of[N,x]){if(!c.existsSync(n))continue;let o=c.readdirSync(n).filter(t=>{let s=t.toLowerCase();return s.endsWith(".json")&&!s.includes(".backup")});for(let t of o){let s=p.join(n,t);try{U(s)}catch(r){e.push({dir:p.basename(n),file:t,err:r.message})}}}return e.length===0?{ok:!0}:{ok:!1,broken:e}}function Mo(e){let n=[],o=[];for(let r of[N,x]){if(!c.existsSync(r))continue;let i=c.readdirSync(r).filter(u=>u.endsWith(e));for(let u of i){let l=p.join(r,u),f=u.slice(0,-e.length),a=p.join(r,f);try{f.toLowerCase().endsWith(".json")?U(l):c.accessSync(l,c.constants.R_OK),n.push({backupPath:l,originalPath:a,fileName:u,dir:r})}catch(h){o.push(`${p.basename(r)}/${u}: ${h.message}`)}}}if(o.length>0)return{ok:!1,error:o.join("; "),restored:0};let t=0,s=[];for(let{backupPath:r,originalPath:i,fileName:u,dir:l}of n)try{Re(r,i),t++}catch(f){s.push(`${p.basename(l)}/${u}: ${f.message}`)}return s.length>0?{ok:!1,error:s.join("; "),restored:t}:{ok:!0,restored:t}}function Re(e,n){let o=n+".tmp";try{c.copyFileSync(e,o),Ae(o)}catch(t){try{c.unlinkSync(o)}catch{}throw t}try{c.renameSync(o,n)}catch(t){try{c.unlinkSync(o)}catch{}if(t.code==="EPERM"||t.code==="EBUSY"){let s=new Error(`File locked: ${n} (${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 qo=Jo(import.meta.url),Bo=_.dirname(qo),ne=process.env.DATA_DIR?_.resolve(process.env.DATA_DIR,"collected-profiles"):_.join(Bo,"collected-profiles");await Oe();var te=process.argv.slice(2),Te=parseInt(te.find(e=>e.startsWith("--max="))?.split("=")[1]||"50"),q=parseInt(te.find(e=>e.startsWith("--min-score="))?.split("=")[1]||"50"),Vo=te.includes("--list");function Go(){let n=G(20,{easyApplyOnly:!0}).filter(o=>o.score>=q);if(console.log(`
49
+ \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(` APPLY QUEUE (${n.length} jobs, min score ${q})`),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"),n.length===0)console.log(" No jobs in queue. Run: npm run scout");else for(let o=0;o<n.length;o++){let{score:t,job:s,jobId:r}=n[o],i=(s.techStack||[]).slice(0,4).join(", ");console.log(` ${String(o+1).padStart(2)}. [${t}] ${(s.title||"?").slice(0,35)} @ ${(s.company||"?").slice(0,20)}`),console.log(` ${s.salary||"no salary"} | ${s.location||"?"} | ${i||"no tech"}`),console.log(` ${s.url}`)}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
50
+ `)}async function Wo(){if(console.log(`
20
51
  LinkedIn Apply Queue
21
- `),ro){io();return}let e=Se(dt*3,{easyApplyOnly:!0}).filter(s=>s.score>=re).slice(0,dt);if(e.length===0){console.log(` No jobs to apply. Run: npm run scout
22
- `);return}console.log(` Applying to ${e.length} jobs (score >= ${re}):
23
- `);for(let{score:s,job:i}of e)console.log(` [${s}] ${(i.title||"?").slice(0,40)} @ ${(i.company||"?").slice(0,20)}`);$.existsSync(Oe)||$.mkdirSync(Oe,{recursive:!0});let o=x.join(Oe,"apply-queue-urls.json"),r=e.map(({job:s,jobId:i})=>({url:s.url,title:s.title,company:s.company,location:s.location,salary:s.salary}));$.writeFileSync(o,JSON.stringify(r,null,2)),console.log(`
24
- Generated ${o} with ${r.length} jobs`);for(let{jobId:s}of e)he(s,{queued:!0,queuedAt:new Date().toISOString().slice(0,10)});let n=ct("apply-queue",`${e.length} jobs`);console.log(`
52
+ `),Vo){Go();return}let n=G(Te*3,{easyApplyOnly:!0}).filter(r=>r.score>=q).slice(0,Te);if(n.length===0){console.log(` No jobs to apply. Run: npm run scout
53
+ `);return}console.log(` Applying to ${n.length} jobs (score >= ${q}):
54
+ `);for(let{score:r,job:i}of n)console.log(` [${r}] ${(i.title||"?").slice(0,40)} @ ${(i.company||"?").slice(0,20)}`);E.existsSync(ne)||E.mkdirSync(ne,{recursive:!0});let o=_.join(ne,"apply-queue-urls.json"),t=n.map(({job:r,jobId:i})=>({url:r.url,title:r.title,company:r.company,location:r.location,salary:r.salary}));E.writeFileSync(o,JSON.stringify(t,null,2)),console.log(`
55
+ Generated ${o} with ${t.length} jobs`);for(let{jobId:r}of n)ue(r,{queued:!0,queuedAt:new Date().toISOString().slice(0,10)});let s=Se("apply-queue",`${n.length} jobs`);console.log(`
25
56
  Launching run.mjs --job-urls...
26
- `);try{oo("node",["run.mjs",`--job-urls=${o}`],{stdio:"inherit",timeout:144e5}),be(n,"success",{applied:e.length})}catch(s){console.error(`
27
- Apply queue error: ${s.message?.slice(0,80)}`),be(n,"error",{applied:e.length},s.message)}try{$.unlinkSync(o)}catch{}}co().catch(t=>{console.error("Apply queue failed:",t),process.exit(1)});
57
+ `);try{Uo("node",["run.mjs",`--job-urls=${o}`],{stdio:"inherit",timeout:144e5}),H(s,"success",{applied:n.length})}catch(r){console.error(`
58
+ Apply queue error: ${r.message?.slice(0,80)}`),H(s,"error",{applied:n.length},r.message)}try{E.unlinkSync(o)}catch{}}Wo().catch(e=>{console.error("Apply queue failed:",e),process.exit(1)});