← All posts

בניתי מחדש את תוסף הכרום שעזר לי לחפש עבודה. הנה ההנחיה

תוסף כרום שממלא טפסי מועמדות, שומר כל משרה שהגשתם אליה ומראה כמה הגשות הפכו לראיון. במקום לינק לתוסף: ההנחיה ל-coding agent, כדי שתבנו גרסה משלכם.

לפני כמה שנים, באמצע חיפוש עבודה, נתקלתי בלינקדאין בפוסט על תוסף כרום שממלא טפסי מועמדות בלחיצה. הוא חסך לי שעות. במקום להקליד בפעם הארבעים שם, טלפון ולינקדאין ולצרף שוב קורות חיים, יכולתי להתמקד במשרות עצמן. ואז הוא הפסיק לעבוד.

השנה חזרתי לחפש עבודה, אז בניתי אותו מחדש, עם כמה דברים שתמיד היו חסרים לי. במקום לינק להורדה, אני משתף כאן את ההנחיה שבעזרתה תוכלו לבנות גרסה משלכם.

דלגו להנחיה ↓

מה התוסף עושה

  • ממלא טופס בלחיצה אחת: שם, טלפון, מייל, לינקדאין, וגם מצרף קורות חיים. עובד גם באתרי גיוס כמו Comeet, Greenhouse ו-SmartRecruiters.
  • שומר את המשרה כשלוחצים “הגש”: חברה, תפקיד, תאריך ותיאור המשרה. כשמגייסת מתקשרת אחרי שבועיים, יש לכם את ההקשר, גם אם המודעה כבר ירדה.
  • לוח מעקב: הוגש, בתהליך, ראיון, הצעה. גוררים כרטיס כדי לעדכן סטטוס.
  • ייבוא מ-LinkedIn: המשרות שהגשתם אליהן דרך LinkedIn נכנסות ללוח.
  • תזכורות ומספרים: תזכורת לעשות follow-up, ואחוז התגובות והראיונות לפי מקור.

למי זה מתאים

  • למי שמגיש להרבה משרות ונמאס לו להקליד את אותם פרטים שוב ושוב.
  • למי שכבר לא זוכר לאן הגיש, מתי, ולאיזה תפקיד.
  • למי שרוצה לדעת מה באמת עובד: לינקדאין, חבר מביא חבר, או הגשה ישירה באתר החברה.

למה הנחיה ולא לינק להורדה

התוסף הקודם הפסיק לעבוד כי הוא היה תלוי במישהו אחר שימשיך לתחזק אותו. כשאתם בונים בעצמכם, הקוד אצלכם, והנתונים נשמרים רק בדפדפן שלכם. אפשר להתאים את השפה, השדות ושלבי המעקב, וכשמשהו נשבר מבקשים מה-agent לתקן.

זה עדיין דורש התקנה, בדיקה ותיקון מדי פעם. זה לא קסם, אבל זה שלכם.

איך בונים

  1. פותחים coding agent, למשל Claude Code, Cursor או Codex, על תיקייה ריקה.
  2. מעתיקים את ההנחיה שלמטה ומדביקים אותה.
  3. עונים על השאלות שה-agent שואל: שפה, אילו שדות למלא, אילו קבצים לצרף ואילו פיצ’רים.
  4. טוענים את התוסף בכרום: נכנסים לעמוד התוספים, מפעילים את Developer mode, לוחצים על Load unpacked ובוחרים את התיקייה.
  5. ממלאים את הפרטים בדף ההגדרות שנפתח, ובודקים על משרה אמיתית לפני שלוחצים “הגש”.

נתקעתם? העתיקו ל-agent את הודעת השגיאה, או תארו מה לא עבד.

ההנחיה

ההנחיה87 שורות
# Build me a job-application Chrome extension

You are a coding agent. Build a Chrome extension (Manifest V3, plain JavaScript/HTML/CSS, no build step, no frameworks, no external servers) that fills job application forms in one click and tracks every application I submit. All data stays local in `chrome.storage`.

## Step 0: ask me first

Before writing code, ask me these questions in one message and wait for the answers:

1. Which UI language? (e.g. Hebrew RTL, English). All UI text must be in that language.
2. Which fields do I want to fill? Suggest a default list: first name, last name, full name, email, phone, city, LinkedIn URL, GitHub URL, portfolio URL, years of experience, salary expectation, notice period. For each, I will give a value and aliases.
3. Which files should it attach? (resume / CV, cover letter, portfolio).
4. Which application statuses do I want? Default: saved (to apply), applied, in process, interview, offer, rejected, withdrawn.
5. Do I want the optional features: LinkedIn import, follow-up reminders, Chrome sync? (Default: yes to all.)

Then build everything below, adapting labels and defaults to my answers.

## 1. Form filling (the core)

**Settings page** (`options.html`, opens in a tab, opens automatically on first install):
- A list of rows: field name + value. The name accepts several comma-separated aliases, e.g. `Phone, mobile, טלפון, נייד`. Add / remove rows.
- A list of file rows: aliases + uploaded file (resume etc.), up to 10MB each, stored as a data URL in `chrome.storage.local` (request `unlimitedStorage`).
- An "overwrite fields that already have a value" checkbox (default off).
- Save button with a success message.

**Trigger:** clicking the toolbar icon opens a small popup that fills immediately and lists what was filled. Keyboard shortcut `Alt+Shift+F` via `commands._execute_action`. The popup has "fill again" and "edit fields" buttons.

**Fill function:** one self-contained function (Chrome serializes its source, so no outer references) injected with `chrome.scripting.executeScript({ target: { tabId, allFrames: true } })` so it works inside iframes (Comeet, Greenhouse embeds). Needs `host_permissions: ["<all_urls>"]`, `scripting`, `activeTab`. It returns `{ filled, matched, files }` per frame; the popup sums them.

Matching rules, which are what make it work on real sites:
- Normalize text: lowercase, turn `* : _ - . ( ) [ ] / \ | " ' ? !` into spaces, collapse whitespace.
- For every `input, textarea, select`, collect candidate texts: `<label>` (via `el.labels`), `aria-label`, `aria-labelledby` targets, `placeholder`, `title`, `name`, `id`, `autocomplete`, plus a camelCase-split variant of name/id/autocomplete (`firstName` gives `first Name`). Also climb up to 6 ancestors and take the text of each ancestor that wraps only this one control (≤80 chars). Many forms use `<div><span>Label</span><input></div>` with no `<label>`.
- First pass: exact match of a candidate to an alias. Second pass: whole-word containment, and the longest alias wins.
- **Shadow DOM:** query recursively into open shadow roots (SmartRecruiters and design-system forms). When climbing parents, step from a shadow root to its host.
- Skip disabled, readonly, invisible (`getClientRects().length === 0`, `display:none`, `visibility:hidden`) and types hidden/submit/button/reset/image/password/checkbox/radio/range/color.
- **Set values with the native setter** (`Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value").set`, matching prototype for textarea/select), then dispatch `input`, `change`, `blur` with `bubbles: true`. Otherwise React/Angular/Vue forms ignore the value.
- `<select>`: match an option by value or text, exact first, then contains.
- File inputs: don't check visibility (they hide behind styled buttons). Build a `File` from the data URL, set `el.files` through a `DataTransfer`, dispatch `input` and `change`.
- Briefly outline filled fields (2px colored outline for 1.5s).
- Don't overwrite non-empty fields unless the overwrite option is on.

## 2. Application tracker

**Data model** (`chrome.storage.local`): `{ id, company, title, url, source, date "YYYY-MM-DD", status, notes, description, resumeFile, contact, contactInfo, salaryAsked, salaryOffered, followUp, history: [{ status, date }], linkedinJobId?, updatedAt }`. Put all reads/writes in one shared module (`applications-store.js`) used by every page. Every status change goes through one `updateApplication` function that appends to `history`.

**Save from the popup:** below the fill result, a "track application" section prefilled from the page: company, job title, source (detect from hostname: LinkedIn, Indeed, AllJobs, Drushim, Comeet, Greenhouse, Lever, etc.), status picker, save button. Extract job info with an injected function: JSON-LD `JobPosting` first, then `og:` meta tags, then `<h1>` and title. Also save the job description text (so I have it before the interview even if the posting is removed) and the attached resume file name. URLs are deduplicated after stripping tracking params (`utm_*`, `ref`, `trk`, etc.): if the URL exists, the button updates instead of duplicating.

**Auto-track on submit:** after the popup fills a tab, inject a submit watcher into all frames and keep the popup's job details (including my edits to them) per tab in `chrome.storage.session`. The watcher reports to the background service worker when:
- a native `submit` event fires (after browser validation), or
- a click on a button / `input[type=submit]` / `[role=button]` whose text matches submit words (`submit|apply|send` plus my language's equivalents) and does NOT match `next|continue|back|cancel|save|draft|search|login|sign up` (plus equivalents). Use `e.composedPath()` to find buttons inside shadow roots. Skip the click if a real form submit will fire anyway, or if `form.checkValidity()` fails.
The worker then saves the application as "applied" and shows a notification (click opens the tracker). An existing "saved" entry moves to "applied". An entry already in a later status isn't changed or duplicated. Report once per page.

**Tracker page** (`applications.html`, linked from popup and settings):
- Table view and board (Kanban) view. Drag a card between columns to change status.
- Search plus filters by status, source and date range (today / last day / week / month / all), with the visible count shown.
- Bulk actions in table view: checkbox per row and a select-all-visible checkbox in the header; a bar to change status or delete selected.
- "⋯" opens a details dialog: all fields, notes, job description, status timeline.
- Stats: response rate, interview rate, average days to response and to interview, breakdown by source.
- Find and remove duplicates: same LinkedIn job id, or same normalized URL, or same company + title. Keep the copy with the longest history or the most filled fields.

## 3. LinkedIn import (optional)

A button in a dialog on the tracker page with a range picker (7 days, month, 3 months, 6 months, year, all). It opens `https://www.linkedin.com/jobs-tracker/?stage=applied` in a background tab, injects a scraper that reads job cards (company, title, link, job id, location, "Applied Xd/mo ago"), clicks "show more" / next page, tries to sort by applied date and stops once past the range, then closes the tab. Show a progress bar in the LinkedIn tab with a "stop and save what was found" button. New jobs go in as "applied". Existing ones (same job id, or same company + title) aren't duplicated and keep their status; only empty fields get filled. Where possible fetch the exact applied date and the description; otherwise store the estimated date and say so in notes. Requires me to be logged in. LinkedIn's DOM changes often, so select by stable attributes / text patterns, not generated class names, and fail with a clear message.

## 4. Follow-ups, backup, sync (optional)

- Applied / in-process / interview entries get a follow-up date (default 7 days, configurable). A `chrome.alarms` check sets the badge count on the icon, highlights due rows, and sends at most one notification per day. A ✓ button dismisses the reminder.
- Export CSV (UTF-8 with BOM so Excel shows non-Latin text), full JSON backup, and import of both (CSV import skips existing entries).
- Optional sync across computers via `chrome.storage.sync`, chunked to respect quota. Don't sync descriptions; truncate very long notes.

## Engineering rules

- Manifest V3, `permissions: storage, unlimitedStorage, scripting, activeTab, alarms, notifications`, `host_permissions: <all_urls>`.
- Injected functions must be fully self-contained.
- Respect RTL if the UI language needs it (`dir="rtl"`), light and dark mode, clean minimal UI.
- Add a `[hidden] { display: none !important; }` global rule.
- Include `test/test.html`: a local form covering label-for, wrapped label, placeholder-only, aria-label, aria-labelledby, camelCase name, div+span without label, a select, a file input, an iframe, and a shadow-DOM input. It's for manual testing (enable "Allow access to file URLs").
- Write a README in my language: install (`chrome://extensions` → Developer mode → Load unpacked), usage, how matching works, limits (checkbox/radio not filled), and a warning to avoid short generic aliases like `name` or `url` (they also match "Company name" or "GitHub URL").

## Done means

1. It loads unpacked with no errors in `chrome://extensions` (check the service worker console too).
2. On `test/test.html`, every test field fills, including the iframe, shadow DOM and file.
3. Saving from the popup twice on the same URL updates instead of duplicating.
4. Submitting the test form after filling adds an "applied" entry and shows a notification.
5. The tracker's table, board drag-and-drop, filters, bulk actions, stats and CSV/JSON export → import round trip all work.

Walk me through loading it and testing on one real job application.

איפה זה היה מסובך

רוב העבודה לא הייתה בפיצ’רים, אלא בלגרום למילוי לעבוד באתרי גיוס אמיתיים. כמה דברים שההנחיה מטפלת בהם במפורש, כי בלעדיהם התוסף נראה כאילו עובד ובפועל לא:

  • טפסים ב-React: אם רק משנים את הערך בשדה, הטופס לא מזהה את השינוי ושולח שדה ריק. צריך לעדכן את הערך בדרך שהספרייה מזהה.
  • שדות מוסתרים: אתרים כמו SmartRecruiters מסתירים את השדות במבנה דף נפרד, וחיפוש רגיל לא מוצא אותם.
  • טפסים בתוך מסגרת: הרבה חברות מטמיעות טופס של Comeet או Greenhouse בתוך האתר שלהן, ואז צריך למלא גם בתוך המסגרת.
  • “הבא” זה לא “הגש”: בטופס של כמה שלבים, לחיצה על “הבא” לא אמורה לשמור את המשרה כהגשה.
  • תאריכים: בלי טיפול מיוחד, הגשה אחרי חצות נשמרת עם התאריך של אתמול, בגלל הפרש השעות מ-UTC.

בניתם? ספרו לי

אשמח לשמוע מה הוספתם או מה לא עבד. רעיונות להמשך: מילוי של שאלות שחוזרות על עצמן, כמה גרסאות של קורות חיים, או תמיכה באתר גיוס שאתם משתמשים בו.

  • Chrome extension
  • Coding agents
  • Job search