✅ Builds cleanly — npm run build succeeds with zero errors.
Load dist/ as an unpacked extension in Chrome: chrome://extensions → Load unpacked → select dist/.
The NVIDIA key shared in chat was exposed. Regenerate it immediately at build.nvidia.com → API Keys.
Paste the new key only into the extension: Options → AI Features tab. It stores in chrome.storage.local only — never in any file.
All project scaffolding in place.
| File | Purpose |
|---|---|
package.json |
All dependencies declared |
vite.config.ts |
Vite + CRXJS + Tailwind v4 |
tsconfig.json / tsconfig.node.json |
TypeScript strict mode |
src/manifest.ts |
MV3 manifest — named platform host permissions, optional *://*/* |
.env.example |
NVIDIA API key placeholder |
public/icons/ |
Green circle placeholder icons (16/48/128px) |
Heuristic-first extraction — zero API calls in MVP.
| File | Purpose |
|---|---|
src/constants/patterns.ts |
All regex: languages, visa, work auth, employment type, experience, salary, remote |
src/constants/scoring.ts |
Weights, thresholds, freshness modifiers |
src/constants/platforms.ts |
Per-platform URL patterns + CSS selectors |
src/constants/models.ts |
NVIDIA NIM model names + token budgets |
src/background/extraction/jsonld.extractor.ts |
Parse JobPosting schema |
src/background/extraction/regex.extractor.ts |
Regex extraction for all key fields |
src/background/extraction/dict.extractor.ts |
80+ skill dictionary matching |
src/background/extraction/confidence.ts |
Per-field confidence scoring |
src/background/extraction/pipeline.ts |
JSON-LD → regex → dict → LLM fallback orchestration |
Fully deterministic — no API key required.
| File | Purpose |
|---|---|
src/background/parsers/pdf.parser.ts |
pdfjs-dist text extraction |
src/background/parsers/docx.parser.ts |
mammoth DOCX text extraction |
src/background/parsers/resume.parser.ts |
Deterministic ResumeProfile parser with date range merging |
Key implementation: computeMergedExperienceYears() unions overlapping date intervals before summing — prevents inflated experience from concurrent roles.
| Store | Key | Purpose |
|---|---|---|
profile |
'main' |
Single UserProfile record |
jobs |
jobId |
Raw JobListing scraped data |
extractions |
jobId |
Cached ExtractionResult — revisit = instant |
matches |
jobId |
Cached MatchResult — revisit = instant |
applications |
id |
Full CRUD with status timeline |
companies |
normalizedName |
CompanyProfile schema (seeded post-MVP) |
rules |
id |
User automation rules |
metrics |
metric key | Incrementing usage counters |
| File | Purpose |
|---|---|
src/background/scoring/hard-filters.ts |
6 hard filters: language gap, visa, work auth, employment type, location, experience gap, excluded company |
src/background/scoring/engine.ts |
Deterministic 0–100 scorer — 7 weighted dimensions + freshness modifier |
src/background/scoring/freshness.ts |
Age-based score modifier (+5 fresh, -5 stale, -15 very stale) |
src/background/rules/engine.ts |
Evaluate UserRule conditions against match + extraction |
src/background/rules/defaults.ts |
Auto-generate default rules from workAuth status |
Score weights: skills(30%), experience(22%), language(20%), location(10%), visa(8%), salary(5%), employment type(5%).
Hard filter types: language_gap, visa_blocked, location_blocked, employment_type_mismatch, experience_gap, excluded_company. Any blocker severity forces score ≤ 34 and recommendation = red.
| File | Purpose |
|---|---|
src/background/handlers/job.handler.ts |
Fingerprint job, deduplicate, run extraction pipeline, cache |
src/background/handlers/match.handler.ts |
Cache-check, compute match, push result to tab, update metrics |
src/background/handlers/profile.handler.ts |
Save profile, upload + parse resume, seed default rules |
src/background/handlers/tracker.handler.ts |
Save application, update status, increment metrics |
src/background/index.ts |
Service worker message router — handles all BackgroundMessage types |
src/shared/utils/fingerprint.ts |
fingerprintJob() — sha256 of normalized company+title+location |
Job fingerprinting: normalizeTitle() strips (m/f/d), (w/m/d), senior, junior etc. before hashing — prevents duplicate jobs from different URLs counting separately.
| File | Purpose |
|---|---|
src/content/observer.ts |
History patch (pushState/replaceState) + MutationObserver — detects SPA navigation |
src/content/detectors/generic.detector.ts |
PRIMARY — JSON-LD + URL heuristic + DOM keyword scan |
src/content/detectors/linkedin.detector.ts |
LinkedIn-specific URL pattern |
src/content/extractors/generic.extractor.ts |
JSON-LD → microdata → largest text block |
src/content/extractors/linkedin.extractor.ts |
LinkedIn-specific CSS selectors (aria-stable attrs) |
src/content/shadow-host.ts |
Closed shadow DOM mount — CSP-safe, style isolation |
src/content/index.ts |
Entry — detect → extract → mount overlay → send JOB_DETECTED |
src/content/overlay-mount.tsx |
React root inside shadow DOM |
| Component | Purpose |
|---|---|
Overlay.tsx |
Main container — listens for greenapply:message events, state machine |
ScoreRing.tsx |
Animated SVG progress ring (0–100) |
RecommendationBadge.tsx |
🟢🟡🟠🔴 pill with label |
HardFilterAlert.tsx |
Red/orange alert cards for blockers and warnings |
SkillGapList.tsx |
Matched (green) / bonus (blue) / missing (red) skill chips |
JobFreshness.tsx |
"Posted 87 days ago" with warning for stale listings |
ConfidenceCaveat.tsx |
Low-confidence visa notice |
ActionButtons.tsx |
"Save Job" button → sends SAVE_APPLICATION to background |
Core UI moments implemented:
- 🔴 Skip with "Estimated time saved: ~15 minutes" badge
- 🟢 Strong Apply with score ring + skill breakdown + save button
| File | Purpose |
|---|---|
src/popup/Popup.tsx |
Dashboard (time saved, stats) + Jobs list with status badges |
src/options/Options.tsx |
4 sections: Resume upload, Profile, Preferences, API key (optional) |
| File | Purpose |
|---|---|
src/background/nim/client.ts |
nimComplete() (batch) + nimStream() (SSE) with 3-attempt exponential backoff |
src/background/nim/explainer.ts |
2–3 sentence score explanation — called async after match computed, result pushed to tab |
src/background/nim/generator.ts |
Cover letter streaming prompt — candidate profile + job desc |
src/background/handlers/generate.handler.ts |
Streaming via chrome.runtime.connect() port — keeps SW alive |
src/overlay/components/GeneratePanel.tsx |
Streaming cover letter panel with copy button |
src/overlay/components/TrackingDropdown.tsx |
Status picker (saved → applied → interview → offer → rejected) |
src/content/feed.ts |
Injects 🟢🔴 score badges into job list cards (LinkedIn, Indeed, StepStone) |
New platform extractors + detectors: Indeed, Greenhouse, Lever, Workday, Personio, StepStone — all wired into content/index.ts.
How AI works:
- Match score is computed deterministically first and returned immediately
- LLM explanation fires async in background; when done, pushes updated MatchResult to the tab (overlay re-renders with summary text)
- Cover letter streams token-by-token through a long-lived
chrome.runtime.connect()port - If no API key: score + hard filters still work; explanation and cover letter are hidden
- Extraction is now AI-first for testing: the full job text goes to NIM first, then JSON-LD / regex / dictionary fall back for missing fields.
Test on 100 real job pages. Targets:
- Language detection >90%
- Visa/sponsorship >85% accuracy, <5% false positives
- Employment type >90%
- Experience years >80%
src/content/feed.ts — inject 🟢🔴 score badges directly into job list cards on LinkedIn / Indeed before user clicks through.
src/background/nim/client.ts— fetch wrappersrc/background/nim/explainer.ts— 2–3 sentence score summarysrc/background/nim/generator.ts— streaming cover letter viachrome.runtime.connect()portsrc/overlay/components/GeneratePanel.tsx— streaming text output
Indeed, Glassdoor, StepStone, Workday, Personio, Greenhouse, Lever, Ashby.
- No API key required for MVP. All scoring and extraction is deterministic.
- Extraction cache:
extractionsstore keyed by jobId — revisiting same job skips all processing. - Match cache:
matchesstore keyed by jobId — re-opening same job tab = instant result. - Shadow DOM overlay: Closed shadow root prevents page CSS from leaking in. Tailwind styles injected via
adoptedStyleSheets— CSP-safe. - Chrome Store strategy: Named platform domains in
content_scripts.matches,*://*/*only inoptional_host_permissions. Generic career sites request origin permission at runtime. - Date range merging:
computeMergedExperienceYears()unions overlapping intervals — critical for accurate seniority inference on student profiles. - Feature updates: When adding or changing a feature, update
memory.mdwith the new behavior or convention.
npm install
npm run build
# Chrome → chrome://extensions → Enable Developer mode
# Load unpacked → select /Users/khaleel/Documents/greenapply/dist/Visit any LinkedIn job page — the overlay should appear bottom-right. Upload resume in the extension options page first for match scoring to work.