Files
linkme/CLAUDE.md
T
rodado fb67c38edd chore: reorganize repo structure and update docs
- move temp scripts to scripts/, cv files to cv/, jobs.csv to results/
- update index.ts and recheck.ts CSV path to results/jobs.csv
- fix imports in scripts/ to use ../config.ts and ../fetch.ts
- update CLAUDE.md and README.md to reflect new structure
- add plans/ with LinkedIn post drafts
- update .gitignore: ignore results/, jobs/, cv binaries, .DS_Store
2026-06-03 16:01:32 +02:00

7.5 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Commands

command what it does
bun fetch run all searches in config.ts, append new jobs to results/jobs.csv
bun recheck fetch in raw mode, list filtered-out jobs + word-frequency keyword suggestions

Temp scripts: bun run scripts/<name>-temp.ts — never edit config.ts for one-off searches.

Structure

config.ts          # search keywords and areas
fetch.ts           # scraper, HTML parser, all filters
fetch-other.ts     # extra sources (datacareer.ch, stellen-basel.ch)
index.ts           # orchestrator: runs searches, writes CSV
recheck.ts         # raw-mode fetch, lists filtered-out jobs
scripts/           # one-off search scripts; import from ../config.ts and ../fetch.ts
cv/polo.md         # Rafael's profile — use for ranking jobs
cv/                # CV files (html, pdf, odt)
results/jobs.csv   # output — one row per job
jobs/              # notes on specific job leads
plans/             # LinkedIn post drafts

Architecture

  • config.ts — KEYWORDS string (joined with OR) and searches: SearchConfig[]. Edit here to add/remove search areas or keywords.
  • fetch.ts — HTTP scraping, HTML parsing, and all filters. Exports fetchJobs(config, { raw? }) and isEnglishContent(id).
  • fetch-other.ts — scrapes extra sources (datacareer.ch, stellen-basel.ch). Imported by index.ts.
  • index.ts — orchestrator: loads existing IDs from results/jobs.csv, calls fetchJobs, calls isEnglishContent for each new job, appends survivors to CSV.
  • recheck.ts — calls fetchJobs(..., { raw: true }) to bypass SOFTWARE_TITLE_RE, then lists every new job that would be filtered out and prints word-frequency suggestions.

Filter pipeline (applied in order)

  1. Switzerland location guard (card HTML) — extracts job-search-card__location from each card; drops jobs where location doesn't contain "Switzerland". Prevents LinkedIn from returning global results when geoId is ignored on unauthenticated requests.
  2. NON_ENGLISH_RE (title) — drops German gender markers, umlauts, /in suffix, French accents/markers. Runs inside fetchJobs.
  3. JUNIOR_TITLE_RE (title) — drops junior/intern/trainee/werkstudent. Runs inside fetchJobs.
  4. JAVA_TITLE_RE (title) — drops Java-titled roles. Runs inside fetchJobs.
  5. SOFTWARE_TITLE_RE (title) — requires a relevant tech/role term. Skipped when raw: true. Runs inside fetchJobs.
  6. isEnglishContent (description page) — fetches the job page, counts DE/FR indicator words, drops if ≥ 6 matches. Runs in index.ts only (not recheck.ts). Adds 350 ms delay per job.

Deduplication: job IDs are extracted from results/jobs.csv on startup; any ID already seen is skipped before content-checking.

SearchConfig

url overrides all filter params. Otherwise build from: keywords, geoId/location, workType, jobType, timePosted, seniority.

Known Swiss geoIds:

  • Basel city: 103383283
  • Basel Metropolitan Area: 90009885
  • Basel-Country: 107117530
  • Zürich city: 102436504
  • Zürich Metropolitan Area: 107814425
  • Zürich Area: 90009888

Use location: "City, Switzerland" (no geoId) when the geoId is unknown — LinkedIn resolves by name.

Auth warning: Do NOT send the li_at cookie in job search requests (/jobs/search/). With the cookie, LinkedIn serves the React SPA instead of server-rendered HTML, breaking the card parser entirely. The cookie is only useful for individual profile pages.

Searching any city / mode temporarily

Write a throwaway script in scripts/ (e.g. scripts/bern-temp.ts) — never touch config.ts unless the user explicitly asks to save the change. Delete temp scripts after the session unless the user asks to keep them.

Common patterns:

// City search (onsite + hybrid)
import { KEYWORDS } from "../config.ts";
import { fetchJobs } from "../fetch.ts";
import type { SearchConfig } from "../config.ts";

const config: SearchConfig = {
  label: "Bern", keywords: KEYWORDS, location: "Bern, Switzerland",
  workType: ["onsite", "hybrid"], timePosted: "month",
};
const jobs = await fetchJobs(config);
for (const j of jobs) console.log(j.title, j.company, j.link);
// Remote-only across all Switzerland
const config: SearchConfig = {
  label: "Switzerland Remote", keywords: KEYWORDS, location: "Switzerland",
  workType: ["remote"], timePosted: "month",
};
// Description-level keyword check (e.g. Ruby/Rails)
const RUBY_RE = /\bruby\b|\brails\b/i;
// fetch each job page, test RUBY_RE against the description text

Extra filters for temp scripts (import from ../fetch.ts):

  • JAVA_TITLE_RE — drop Java roles
  • JUNIOR_TITLE_RE — drop junior/intern
  • isEnglishContent(id) — drop DE/FR description content
  • Add your own inline RE for EMEA, .NET, etc.

Ranking jobs against the CV

Read cv/polo.md for the full profile. Key signals:

  • Seniority: Senior / Staff / Principal / Lead / Architect — weight titles accordingly
  • Core stack: Python · TypeScript · Ruby on Rails · React · Node.js
  • Domain strengths: platform engineering · data pipelines · AI/LLM integrations · DevOps (Docker, Kubernetes, CI/CD, AWS)
  • Background edges: fintech/CBDC (Moedax), civic tech (Tactical Tech / GFOSS), independent consulting (ExtraPolo, 40+ projects)
  • Languages: English (full) · German B1 TELC certified · Spanish (full) — flag roles requiring C1+ German
  • Location: based in Basel, open to Basel / Bern / Zürich; EU citizen, no permit needed

Output format

Present jobs as a markdown table: #, Title, Company, Why it fits, Link.

Links use just the job ID as anchor text: [4192954199](https://www.linkedin.com/jobs/view/4192954199/)

  1. Write a temp script in scripts/ with the right SearchConfig (city, workType, custom keywords).
  2. Apply extra inline filters as needed: EMEA, .NET, description-level keyword check.
  3. Run isEnglishContent per job to drop DE/FR content.
  4. Rank survivors against cv/polo.md, present as a table.
  5. Delete temp scripts after the session unless asked to keep them.

LinkedIn session token

.env contains LI_AT — the LinkedIn session cookie (li_at value). Use it to fetch authenticated LinkedIn pages via curl:

curl -s "https://www.linkedin.com/in/<slug>" \
  -H "Cookie: li_at=$(grep LI_AT .env | cut -d= -f2-)" \
  -H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36" \
  --compressed -o /tmp/profile.html

What works: Full HTML pages for profiles that don't 999-block (varies by profile). Extract data with Python regex on the HTML.

What doesn't: LinkedIn Voyager API returns 401/403 (CSRF issues). ch.linkedin.com slugs sometimes work when www.linkedin.com returns HTTP 999. Public profiles without login redirect to a 1530-byte JS stub — no useful data.

Tip: Profile data is embedded as escaped JSON in the HTML. Use html.unescape() + regex for "headline", "locationName", defaultLocalizedName fields.

Workflow: "check if new keywords should be added"

  1. Run bun recheck.
  2. Identify false negatives — real software roles that got dropped.
  3. Ignore generic frequency suggestions (engineer, development, senior).
  4. Propose specific additions to SOFTWARE_TITLE_RE in fetch.ts or words to NON_ENGLISH_RE.