This file provides guidance to AI coding agents and human contributors when working on this repository. It is intentionally public so contributors and AI tools can share a common operating manual.
PocketResume is a Chrome Extension (Manifest V3) built with vanilla HTML/CSS/JS. No bundler / build pipeline for app code; the only build step is esbuild for the Pro (Clerk) bundle. Edit source files, reload the unpacked extension in Chrome.
Key runtime entrypoints (declared in manifest.json):
background.js — pipeline orchestration + AI API callscontent.js — extracts page text from the active tabpopup.html + popup.js — user actions + PDF generation for PocketResume layoutsoptions.html + options.js — API keys + multiple resumes + togglesresume-renderers.js — Jake, Deedy, Academic CV PDF layoutsanalytics.js (service-worker client) + track-client.js (page-side helper) — anonymous usage stats sent to the Convex backendsrc/cloud-sync.js (source) → cloud-sync.js (bundle, gitignored) — auth, plan gating, pricing, resume syncconvex/ — auth config + resume schema/functions + analytics functionsThe extension runs without Node at runtime, but the repo uses npm to manage dependencies (and to bundle cloud-sync.js + update the vendored jsPDF build).
npm ci
# or
npm install
src/cloud-sync.js (Pro sign-in, plan gating, pricing table, resume sync) is bundled by esbuild into cloud-sync.js at the repo root (gitignored). Required env vars (CLERK_PUBLISHABLE_KEY, CONVEX_URL) are injected at build time. See .env.example and package.json → scripts/build-clerk.mjs.
cp .env.example .env.local
# fill in your own Clerk + Convex values
npm run build:clerk
popup.html loads jsPDF from libs/jspdf.umd.min.js (vendored). If you bump jspdf, copy the built artifact into libs/.
npm install jspdf@latest
cp node_modules/jspdf/dist/jspdf.umd.min.js libs/jspdf.umd.min.js
tracker.html loads the Newton’s Cradle loader from libs/ldrs-newtons-cradle.js (vendored esbuild bundle of the ldrs web component, used as the plan-check loader in the Job Tracker header). If you bump ldrs, rebuild the bundle:
npm install ldrs@latest
npm run build:ldrs
No dev server.
chrome://extensions → Developer mode → Load unpacked → select repo rootDebugging:
npx convex dev
Requires CONVEX_DEPLOYMENT in .env.local. Generated code goes to convex/_generated/ (gitignored).
None configured. No test runner or linter. Validate by hand: load unpacked, generate a resume with a real API key, inspect the PDF + the service worker console.
popup.js).chrome.runtime.sendMessage({ type: 'START_GENERATION', payload: { tabId, resumeStyle, resumeId } }).background.js) handles START_GENERATION:
chrome.storage.local (API key per provider, resumes, cover letter toggle).GET_PAGE_CONTENT. Falls back to injecting content.js via chrome.scripting.executeScript(...).{ status: 'success', data: <resumeJsonString>, coverLetterData: <coverLetterJsonString|null> } or { status: 'error', message: <string> }.JSON.parse(...) the model output.generatePDF(...) in popup.jswindow.ResumeRenderers.generateResumePDF(...) in resume-renderers.jsgenerateCoverLetterPDF(...) in popup.jssetError() stores the raw message, maps it to a short human-readable string via mapErrorMessage(...), and persists the red data-status="error" state until the popup closes or Generate is clicked again. It also reveals the “?” button (#errorInfoBtn), which opens the error modal (#errorModal) with the mapped message plus a Copy Details button for the full raw error. Exception: a missing/unconfigured API key or resume no longer produces the red error — the setup card shows instead (see onboarding flow below).New/unconfigured users get a setup card in the popup plus a spotlight tour on the options page instead of a red error.
popup.js): if the config check (API key per provider / custom endpoint + resume content) fails, renderSetupCard(...) shows #setupCard — a checklist (provider / API key / master resume / save settings) with live checkmarks. Generate stays disabled.onboarding: { step: N } to chrome.storage.local (N is 1-based into TOUR_STEPS in options.js) and calls chrome.runtime.openOptionsPage(). “Skip setup” sets onboarding.dismissed = true, which collapses the card to the compact #setupCompact variant on later opens (the red error never returns for a missing-config state).options.js): if onboarding.step is a number, the spotlight tour opens at that step. A chrome.storage.onChanged listener also starts the tour if the options page is already open when the popup sets the step.TOUR_STEPS (options.js) walks through: provider icons → API key → model (optional) → #resumeContentTextarea → #refineResumeBtn (explain only, no AI call forced) → #save. The highlight uses a box-shadow spotlight and is pointer-events: none, so the user interacts with the real UI while the tour guides.onboarding: { step: null, dismissed: true }. Clicking Save Settings while the tour is active (tourNotifySaved()) jumps straight to the finish card.onboardingCompleted is not yet set, the popup shows the one-time “Setup complete” card and persists onboardingCompleted: true.options.js).REFINE_RESUME to background with source text.*ResumeRefinement(...) function — rewrites source into a cross-style master resume (no job-description tailoring).EXTRACT_RESUME_JSON to background with source text.*ResumeExtraction(...) function — extracts structured JSON profile from raw text.jsonContent on the resume entry and persisted. Used as jsonContent in the generation pipeline.One-time onboarding for Form Filler: the user answers common application-form questions once; fills reuse them.
#appProfileDetails, left panel). Groups: Basics (first/last name, email, phone), Location (street, apt, city, state, postal code, country), Work eligibility (authorized / sponsorship / 18+ / relocate / remote preference — Yes/No selects), Preferences (salary amount + currency + period, start date, years of experience), Links (LinkedIn, portfolio, GitHub), opt-in EEO self-identification (gender, race, hispanic/Latino, veteran, disability — only used when a form asks; local only), and Custom Q&A (free-form question/answer rows matched by question text).PROFILE_AUTOFILL with the active resume’s jsonContent (fallback content). Background generateApplicationProfileFromResume(...) does one strict-JSON extraction call → options fills empty inputs only and reports “Filled X of Y fields”.applicationProfile via the global Save Settings button. Completion = non-empty firstName + lastName; the section’s status pill shows “Ready” / “Not set up”.appProfileOnboarding: { active: true } and open the options page; options consumes the flag and starts the profile spotlight tour (PROFILE_TOUR_STEPS, reuses the tour overlay via tourOpenAt(step, 'profile')). “Guide me” in the section restarts it. Saved answers keep working if the tour is skipped.#fillProfileCard (“Complete setup” → same trigger) instead of running the fill. Generation flow is unaffected.applicationProfile completeness (see Form filler setup above) and blocks with the setup card if incomplete.FILL_APPLICATION_FORM to background with { tabId, resumeId }.START_GENERATION), injects form-filler.js into all frames via chrome.scripting.executeScript, then runs __PocketResumeForm.detect(...) per frame and merges results (field ids are frame-token prefixed). Detection covers text inputs, textareas, selects (only placeholder-unselected ones; matched by option text and value), radio groups, checkbox groups, single question-style checkboxes (consent labels excluded), email/url/date inputs, and contenteditables.resolveFormAnswers(fields, applicationProfile) in form-profile.js splits fields. Tier 1: canonical label matchers → applicationProfile values (name split, address, salary formatting by field type incl. hourly conversion, yes/no, EEO gated by eeoOptIn). Tier 2: customQA match (normalized equality, containment, Jaccard ≥ 0.85). Select/radio/checkbox-group answers must fuzzy-match one of the field’s options, else the field falls to AI. Single checkboxes get boolean intent (Yes → check, No → leave unchecked). Zero tokens for resolved fields.generateFormAnswers(context, userProfile, unresolvedFields, applicationProfile) — one AI call for only the unresolved fields (essays, company-specific questions). Same prompt + a SAVED PROFILE JSON line for grounding; skipped entirely when nothing is unresolved (0 tokens).normalizeFormAnswers (id validation + maxLength truncation), then __PocketResumeForm.fill(...) per frame (native value setters + input/change events for React/Vue compatibility). Checkbox groups: only matching options are checked, never unchecked; already-ticked groups are skipped entirely."... (N from saved answers)"), replies { status: 'success', filled, total, cached }, and logs [FormFill] Filled X of Y fields. (Z saved, W AI). form_filled analytics carries cached (string count; whitelisted in PARAM_FIELDS on both sides).setError path. The popup background glows via body[data-fill-status]: filling (amber pulse) → fill-success (green, auto-reverts after 4s) or fill-failure (red, persists until the next Fill/Generate click). Kept separate from body[data-status] so the two flows never fight over the glow.Safety rules (enforced in form-filler.js + prompt): never submits the form, never overwrites already-filled fields, never unchecks anything the user already ticked, checkbox/consent widgets with consent-style labels (consent/agree/terms/privacy/newsletter/marketing/subscribe/opt-in/gdpr/cookies) are never touched, skips hidden/disabled/readonly/captcha/search fields, caps at 30 fields. Known gap: custom div widgets ([role="checkbox"]/[role="radio"] without real inputs, legacy Workday) are not detected.
Configured by getResumeStyleConfig(...) in background.js:
| UI Style | promptStyle | layout | PDF Renderer |
|---|---|---|---|
| basic | basic | pocketresume | popup.js → generatePDF |
| professional | professional | pocketresume | popup.js → generatePDF |
| faang | faang | pocketresume | popup.js → generatePDF |
| jake | faang | jake | resume-renderers.js → renderJakeLayout |
| deedy | faang | deedy | resume-renderers.js → renderDeedyLayout |
| academic-cv | academic-cv | academic-cv | resume-renderers.js → renderAcademicCvLayout |
Settings are stored in chrome.storage.local, managed in options.js.
Important keys:
apiProvider: "google" | "openrouter" | "openai" | "anthropic" | "custom"geminiApiKey / openrouterApiKey / openaiApiKey / anthropicApiKey: stringgoogleModel / openaiModel / anthropicModel / openrouterModel: string model override (“” = provider default)customEndpoints: array of { id, name, baseUrl, apiKey, model, extraBody } (OpenAI-compatible endpoints; apiKey may be empty for local servers; extraBody is an optional raw JSON string shallow-merged into the request body)activeCustomEndpointId: which custom endpoint is active when apiProvider is "custom"resumes: array of { id, label, content, jsonContent, lastRefineBackup, lastRefineAppliedAt } (up to 3)selectedResumeId: which resume is active in the popupcloudSyncStatus: "idle" | "syncing" | "synced" | "error" (Pro sync indicator, written by src/cloud-sync.js)resumeType: "basic" | "professional" | "faang" | "jake" | "deedy" | "academic-cv"coverLetterEnabled: booleanapplicationProfile: Form Filler answers — { firstName, lastName, email, phone, streetAddress, addressLine2, city, state, postalCode, country, salaryAmount, salaryCurrency, salaryPeriod, startDate, yearsExperience, workAuthorized, needsSponsorship, over18, willingToRelocate, remotePreference, linkedin, website, github, eeoOptIn, eeo: { gender, race, hispanicLatino, veteran, disability }, customQA: [{ id, question, answer }], updatedAt }appProfileOnboarding: { active: boolean } — trigger for the Form Filler setup spotlight tour (set by the popup, consumed by the options page)Legacy migration: userProfile → resumes[0].content
Five providers supported, selected via apiProvider:
gemini-2.5-flash, API key from Google AI Studiogpt-4o-mini, API key from OpenAI Platformclaude-3-5-haiku-20241022, API key from Anthropic Consoleopenai/gpt-oss-120b:free, API key from OpenRoutercustomEndpoints; the active one is used. No API key required for local servers.Model overrides per provider are stored in the *Model keys; empty string falls back to the defaults in PROVIDER_DEFAULT_MODELS (background.js). The options page can fetch available models from each provider’s list endpoint.
All providers share one request path: executeProviderChat(context, prompt, label) in background.js handles the three wire formats (OpenAI-compatible chat completions, Anthropic messages, Gemini generateContent). The 6 pipelines call it via generateTailoredResume, generateCoverLetterText, extractResumeProfileJson, refineResumeSource, generateFormAnswers, and generateApplicationProfileFromResume.
Custom endpoints require a runtime host permission for the endpoint’s origin. manifest.json declares optional_host_permissions: ["https://*/*", "http://*/*"]; the options page calls chrome.permissions.request({ origins: [origin + '/*'] }) when saving or testing an endpoint.
One paid plan (“PocketResume Pro”) gates everything: resume cloud sync, plan gating, and the full Job Tracker. Sign-in, the embedded pricing table, and sync run through Clerk + Convex. The separate “Cloud Sync” plan was merged into PocketResume Pro in v7.9 (Clerk Billing now exposes a single paid plan; live in instance config under billing.plans, editable via clerk config patch / Dashboard); hasCloudSyncAccess() still honors legacy cloud_sync/pro/premium subscriptions and metadata for existing subscribers.
Architecture:
src/cloud-sync.js — IIFE source, bundled by esbuild → cloud-sync.js (gitignored). Loaded by options.html and tracker.html (popup no longer loads it). Provides auth (signIn/isSignedIn/getUserProfile), plan checks (hasCloudSyncAccess), the Clerk pricing table mount, and resume sync (pushAllResumes/pullAllResumes/onLocalResumesChanged via Convex)background.js — auto-pushes resume changes when signed in (chrome.storage.onChanged → onLocalResumesChanged, debounced 2s)options.js — account chip (Sign In / See Plans), Push Local to Cloud / Restore from Cloud, pricing table mount under Settings → PocketResume Protracker.js — checkPlanAccess() gates the Job Tracker trial/lock via window.CloudSyncconvex/auth.config.ts — Clerk → Convex auth wiring; requires CLERK_FRONTEND_API_URL env varconvex/schema.ts — resumes table shape + analytics tablesconvex/resumes.ts — list, upsert, remove queries/mutationsSign-in behavior: all Clerk redirects (signInForceRedirectUrl, signUpForceRedirectUrl, afterSignOutUrl, signOut redirectUrl) point at options.html — never popup.html (navigating the Settings tab to the popup breaks the React tree with removeChild errors). The sign-in modal is themed dark via appearance.variables passed to clerk.load(...) — explicit input colors are required or typed text inherits the page’s light color and becomes invisible inside Clerk’s light-styled inputs.
Credentials are never hardcoded. The build step injects CLERK_PUBLISHABLE_KEY and CONVEX_URL from .env.local into the bundle. Contributors must set up their own Clerk + Convex accounts.
PocketResume/
├── manifest.json # Manifest V3 entrypoint wiring
├── background.js # Service worker: pipeline + AI calls
├── content.js # Content script: page text extraction
├── form-filler.js # [injected on demand] Form detect/fill/toast for the Fill Form feature
├── form-profile.js # Saved-answer resolver: canonical matchers + custom Q&A matching
├── popup.html / popup.js # Popup UI + PocketResume PDF generation
├── options.html / options.js# Settings: API keys, resumes, toggles
├── resume-renderers.js # Jake / Deedy / Academic CV PDF layouts
├── analytics.js # Anonymous usage-stats client (imported by background.js)
├── track-client.js # Page-side trackEvent helper (popup/options/tracker)
├── src/cloud-sync.js # Pro auth/plan/pricing/sync source (bundled → cloud-sync.js)
├── cloud-sync.js # [generated, gitignored] esbuild bundle
├── convex/ # Convex backend
│ ├── auth.config.ts
│ ├── schema.ts
│ ├── resumes.ts
│ ├── analytics.ts
│ ├── crons.ts
│ └── _generated/ # [generated, gitignored]
├── libs/jspdf.umd.min.js # Vendored jsPDF
├── libs/ldrs-newtons-cradle.js # [generated, gitignored? no—committed] vendored ldrs Newton's Cradle web component
├── scripts/build-clerk.mjs # Build script for the Pro (Clerk) bundle
├── .env.example # Template for .env.local
├── AGENTS.md # This file
├── CONTRIBUTING.md
├── CODE_OF_CONDUCT.md
├── SECURITY.md
├── LICENSE
└── package.json # Build scripts only (no runtime deps)
background.js (executeProviderChat + the 5 pipeline functions). Update the style config table above if the schema or layout mapping changes.content.js (extractPageText) and the truncation logic in background.js.form-filler.js (detect / fill / toast) and the FILL_APPLICATION_FORM handler + generateFormAnswers prompt in background.js.form-profile.js (CANONICAL_MATCHERS, resolveFormAnswers) — imported by background.js.options.html + options.js (#appProfileDetails section, APP_PROFILE_FIELDS, PROFILE_TOUR_STEPS), background.js (PROFILE_AUTOFILL handler + generateApplicationProfileFromResume), popup gating in popup.js (isFormFillerProfileComplete / #fillProfileCard).popup.js (generatePDF / generateCoverLetterPDF).resume-renderers.js (renderJakeLayout / renderDeedyLayout / renderAcademicCvLayout).options.js / options.html.popup.html / popup.js.popup.js (setError / mapErrorMessage). The keyword-based map turns long provider errors into short friendly strings; un-matched messages truncate to ~200 chars.analytics.js (client: queue + consent + send), convex/analytics.ts (ingest + summary + cleanup), track-client.js (page-side trackEvent helper). Event names must be whitelisted in both analytics.js (EVENT_NAMES) and convex/analytics.ts (EVENT_NAMES).manifest.json.src/cloud-sync.js (then npm run build:clerk), options.js (account chip + Push/Restore), background.js (auto-push listener), tracker.js (checkPlanAccess).convex/schema.ts, convex/resumes.ts, convex/auth.config.ts (then npx convex dev).background.js).convex/ backend.chrome.storage.local for persistence; do not introduce new global state.key field is intentionally absent. Chrome assigns a fresh extension ID on first load. Do not re-add it (it would lock all contributors to one ID).host_permissions in manifest.json includes the Clerk + Convex domains contributors will need to override. Update both the manifest and this file if you add a new provider.chrome.storage.local each time.content.js. The page’s CSP applies.cloud-sync.js (build artifact), convex/_generated/, or anything from .env.local. See .gitignore.PocketResume is privacy-first by default. See privacy-policy.md for the full policy. The Chrome extension:
chrome.storage.local; cloud sync is opt-in via PocketResume Pro and only talks to the user’s own Convex backend