claude-code-arcane 1.4.0 → 2.0.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 +38 -0
- package/README.md +3 -3
- package/dist/cli.js +2 -0
- package/docs/SKILLS-CATALOG.md +6 -2
- package/docs/USER-GUIDE.md +1 -1
- package/docs/coding-standards.md +26 -0
- package/package.json +2 -1
- package/profiles/job-hunt.yaml +12 -2
- package/rules/drafter-reviewer.md +39 -0
- package/skills/cold-outreach/SKILL.md +10 -10
- package/skills/cold-outreach/references/recruiter-playbook.md +5 -5
- package/skills/cover-letter/SKILL.md +8 -8
- package/skills/cv-ats-export/SKILL.md +19 -6
- package/skills/cv-ats-export/scripts/cv_export.py +49 -4
- package/skills/cv-ats-export/scripts/verify_pdf.py +90 -0
- package/skills/cv-tailor/SKILL.md +7 -7
- package/skills/cv-tailor/references/ats-keywords.md +9 -9
- package/skills/interview-prep/SKILL.md +7 -7
- package/skills/interview-prep/references/star-framework.md +10 -10
- package/skills/job-aplicar/SKILL.md +88 -0
- package/skills/job-hunt/SKILL.md +20 -13
- package/skills/job-hunt/references/templates/Aplicacion.md +6 -2
- package/skills/job-hunt/references/templates/CV Custom.md +1 -1
- package/skills/job-hunt/references/templates/Contacto.md +1 -1
- package/skills/job-hunt/references/templates/Dashboard.md +19 -15
- package/skills/job-hunt/references/templates/Perfil.md +2 -2
- package/skills/job-outcome/SKILL.md +55 -0
- package/skills/job-scrape/SKILL.md +95 -0
- package/skills/job-scrape/references/dedup-playbook.md +25 -0
- package/skills/job-scrape/references/getonbrd-cli.md +62 -0
- package/skills/job-scrape/references/getonbrd-urls.md +37 -0
- package/skills/job-scrape/references/linkedin-cli.md +64 -0
- package/skills/job-scrape/references/linkedin-urls.md +42 -0
- package/skills/job-scrape/references/seen_jobs.schema.json +68 -0
- package/skills/job-scrape/scripts/getonbrd_search.py +501 -0
- package/skills/job-scrape/scripts/linkedin_search.py +428 -0
- package/skills/job-search/SKILL.md +22 -19
- package/skills/job-search/references/scoring-rubric.md +8 -8
- package/skills/job-upskill/SKILL.md +58 -0
- package/skills/linkedin-optimize/SKILL.md +5 -5
- package/skills/master-profile/SKILL.md +17 -17
- package/skills/network-map/SKILL.md +7 -7
- package/skills/personal-brand/SKILL.md +10 -10
- package/skills/personal-brand/references/post-pillars.md +14 -14
- package/skills/portfolio-site/SKILL.md +11 -11
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: job-scrape
|
|
3
|
+
description: "Search job boards (bundled LinkedIn + GetOnBoard CLIs) with dedup memory across runs, score new openings against a role profile and offer to create application notes. Triggers: buscar ofertas nuevas, scrape de ofertas, correr los CLIs de busqueda, buscar trabajo en LinkedIn o GetOnBoard, nueva corrida de busqueda, ofertas con dedup."
|
|
4
|
+
argument-hint: "[perfil] [--jobage N]"
|
|
5
|
+
category: "career"
|
|
6
|
+
user-invocable: true
|
|
7
|
+
allowed-tools: Read, Glob, Grep, Bash, Write, Edit
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# /job-scrape - Búsqueda de ofertas con memoria
|
|
11
|
+
|
|
12
|
+
Corre los CLIs de búsqueda bundleados (LinkedIn + GetOnBoard, en `scripts/`) con los criterios de un perfil, deduplica contra `seen_jobs.json` y contra las notas existentes, scorea lo nuevo y ofrece crear notas de aplicación. Adaptado del skill job-scraper de MadsLorentzen/ai-job-search.
|
|
13
|
+
|
|
14
|
+
Flags y datos que devuelve cada script: `references/linkedin-cli.md` y `references/getonbrd-cli.md`.
|
|
15
|
+
|
|
16
|
+
Argumentos: `$ARGUMENTS` (perfil, opcional; `--jobage N` para override de recencia, default 7 días).
|
|
17
|
+
|
|
18
|
+
Todas las rutas de notas son relativas al **career workspace** (flag `--workspace`, env `CAREER_WORKSPACE`, o `./career-workspace/`; lo crea `/job-hunt setup`). El estado de dedup vive en `<workspace>/tools/job_scraper/seen_jobs.json`.
|
|
19
|
+
|
|
20
|
+
## Requisitos
|
|
21
|
+
|
|
22
|
+
- **Python 3** en el PATH (los scripts usan solo la stdlib: cero dependencias, sin build, sin venv).
|
|
23
|
+
- Career workspace con al menos un perfil en `01-Perfiles/`.
|
|
24
|
+
- Opcional: skill `job-search` instalado (aporta la rúbrica de scoring completa).
|
|
25
|
+
|
|
26
|
+
## Pasos
|
|
27
|
+
|
|
28
|
+
1. **Perfil y criterios.** Si `$ARGUMENTS` no matchea una nota de `01-Perfiles/` (excluir `_index`), listar los perfiles y preguntar cuál usar. Leer del frontmatter: `keywords`, `keywords_excluir`, `match_score_minimo`, `salario_minimo_usd_mes`, `modalidad`, `seniority`, `regla_seniority`, `mercado_objetivo`.
|
|
29
|
+
|
|
30
|
+
2. **Cargar estado.** Leer `<workspace>/tools/job_scraper/seen_jobs.json` (si no existe, crearlo con el esqueleto de `references/seen_jobs.schema.json`; si hay notas previas en `03-Aplicaciones/`, ofrecer backfillearlas como `nota_creada`). Validar que toda entrada `nota_creada` apunte a una nota existente en `03-Aplicaciones/`; reportar punteros rotos sin frenar.
|
|
31
|
+
|
|
32
|
+
3. **Derivar queries.** 3-5 queries con forma de rol desde las keywords del perfil (ej. "fullstack developer", "backend developer"), no fragmentos de skill sueltos.
|
|
33
|
+
|
|
34
|
+
4. **CONFIRMACIÓN: correr los CLIs.** Antes de la primera corrida de la sesión, mostrar el warning y pedir approval explícito (mismo criterio que `/job-search` con Apify):
|
|
35
|
+
|
|
36
|
+
> ⚠️ El CLI de LinkedIn lee páginas públicas de LinkedIn. El acceso automatizado va **contra sus Terms of Service**, y el uso es a tu propia responsabilidad: personal, volumen bajo, nunca comercial ni barridos masivos. GetOnBoard usa su API pública v0 y no tiene esta restricción.
|
|
37
|
+
> ¿Corro ambos CLIs, solo GetOnBoard, o ninguno?
|
|
38
|
+
|
|
39
|
+
Si el usuario elige solo GetOnBoard, saltear LinkedIn y declarar la cobertura reducida en el resumen final.
|
|
40
|
+
|
|
41
|
+
5. **Ejecutar los CLIs** (tolerar el fallo de uno y reportar la degradación):
|
|
42
|
+
```
|
|
43
|
+
python .claude/skills/job-scrape/scripts/linkedin_search.py search -q "<query>" -l "<location>" --remote remote --jobage <N> --format json
|
|
44
|
+
python .claude/skills/job-scrape/scripts/getonbrd_search.py search -q "<query>" --jobage <N> --brief
|
|
45
|
+
```
|
|
46
|
+
La `<location>` de LinkedIn sale del `mercado_objetivo` del perfil; si es worldwide, usar además `-l "Remote"`. Volumen bajo: máximo ~5 queries por CLI por corrida.
|
|
47
|
+
|
|
48
|
+
**`--brief` no es opcional en el barrido:** los JD completos son ~80% del payload de GetOnBoard (~58 KB por query), y el quick score del paso 8 solo necesita los campos estructurados + el arranque de la description. El JD completo se trae en el paso 9, solo para las `high`. Si el CLI de LinkedIn emite un `warning` `NO_CARDS_PARSED` en stderr, la respuesta traía contenido pero el markup cambió: reportarlo como degradación de esa fuente, no como "no hay ofertas".
|
|
49
|
+
|
|
50
|
+
6. **Normalizar y dedup intra-batch.** Canonicalizar URLs (sin utm/tracking; LinkedIn → `jobs/view/<id>`; GetOnBoard `/empleos/` → `/jobs/`), calcular `job_key = slug(empresa)|slug(titulo)` (reglas exactas en `references/dedup-playbook.md`). Misma oferta en ambas fuentes = una entrada con dos `sources[]`.
|
|
51
|
+
|
|
52
|
+
7. **Dedup contra historial:**
|
|
53
|
+
- `seen_jobs.json`: entradas `nota_creada` o `descartado` se omiten de la tabla (contarlas aparte); entradas `new` re-vistas actualizan `last_seen` y se muestran marcadas "vista antes, sin decisión".
|
|
54
|
+
- `03-Aplicaciones/`: Grep por `link_oferta` con las URLs canónicas y por nombre `<Empresa> - <Rol>`. Si hay match no registrado en seen_jobs, registrarlo como `nota_creada` (autocuración).
|
|
55
|
+
- Matching blando (misma empresa + títulos muy parecidos): mostrar warning "posible duplicado de [[nota]]", no dedup automático.
|
|
56
|
+
|
|
57
|
+
8. **Quick score (todas las nuevas)** usando solo datos del listing:
|
|
58
|
+
- `low`: keyword de `keywords_excluir` en el título (o en la description truncada de GetOnBoard); seniority fuera de banda (aplicando `regla_seniority` del perfil si existe); `salary_max < salario_minimo_usd_mes`; o elegibilidad bloqueada (countries/remote_zone excluye el país del usuario según `mercado_objetivo`).
|
|
59
|
+
- `high`: título matchea keywords del perfil + seniority en banda + sin bloqueos.
|
|
60
|
+
- `medium`: el resto. Campos null = desconocido, no penalizar.
|
|
61
|
+
|
|
62
|
+
9. **Score completo solo para las `high`**, con la rúbrica de `.claude/skills/job-search/references/scoring-rubric.md` (si el skill job-search no está instalado, quedarse con el quick score y avisar). El JD completo se trae con `detail` de la fuente que corresponda (`detail <slug>` en GetOnBoard, `detail <id>` en LinkedIn), con un **cap de ~10 fetches por corrida sumando ambas fuentes** (ToS, volumen bajo), priorizando por quick score y recencia. Si las `high` exceden el cap, scorear las que entren y listar el resto con su quick score, declarando el corte.
|
|
63
|
+
|
|
64
|
+
10. **Presentar tabla** ordenada por score: Título | Empresa | Fuente(s) | Salario | Score | Veredicto. Preguntar si alguna se descarta ya (registrar `status: descartado` + `descarte_motivo`).
|
|
65
|
+
|
|
66
|
+
11. **Persistir** todas las entradas nuevas/actualizadas en `seen_jobs.json` y actualizar el campo `actualizado`. JSON válido siempre (es un data file con schema).
|
|
67
|
+
|
|
68
|
+
12. **Ofrecer crear notas** para las de score ≥ `match_score_minimo`, con approval explícito **por oferta** (o "todas") antes de escribir. Por cada confirmada:
|
|
69
|
+
- Nota en `03-Aplicaciones/<Empresa> - <Rol>.md` desde el template `Templates/Aplicacion.md` del workspace, frontmatter completo (`estado: interesado`, `match_score`, `fuente`, `link_oferta`, `salario_rango`, `modalidad`, `ubicacion`, `prioridad`) y sección "Match con mi perfil" con alineaciones y **gaps reales** (alimentan `/job-upskill`).
|
|
70
|
+
- Fila en `00-Dashboard.md`, sección de aplicaciones activas, respetando las columnas exactas del header y escapando `\|` en wikilinks con alias.
|
|
71
|
+
- Entrada seen_jobs → `status: nota_creada` + `nota`.
|
|
72
|
+
|
|
73
|
+
13. **Resumen final:** contadores (encontradas / ya vistas / nuevas / descartadas / notas creadas), fuentes efectivamente consultadas y cortes aplicados (cap de `detail`, fuente degradada), y sugerencia de `/job-aplicar` para las top.
|
|
74
|
+
|
|
75
|
+
## Qué NO hace
|
|
76
|
+
|
|
77
|
+
- No corre el CLI de LinkedIn sin la confirmación del paso 4.
|
|
78
|
+
- No crea notas sin confirmación individual.
|
|
79
|
+
- No genera CVs ni covers (eso es `/job-aplicar`).
|
|
80
|
+
- No marca `estado: aplicado` nunca.
|
|
81
|
+
- No edita notas existentes ni re-scorea ofertas ya con nota.
|
|
82
|
+
- No borra filas del dashboard ni entradas de seen_jobs.
|
|
83
|
+
- No se corre en loop ni excede los caps de volumen.
|
|
84
|
+
|
|
85
|
+
## Reglas
|
|
86
|
+
|
|
87
|
+
- Respetar la matriz de fuente de verdad de `references/dedup-playbook.md`: seen_jobs → nota → dashboard, unidireccional.
|
|
88
|
+
- Notas nuevas en el idioma del workspace, sin guiones largos, con wikilinks.
|
|
89
|
+
- Volumen bajo siempre en LinkedIn (el acceso automatizado va contra sus ToS; uso personal y puntual), y con el warning + approval del paso 4 en la primera corrida de la sesión. El profile pre-aprueba el permiso de Bash, no la decisión: la confirmación la pide el skill.
|
|
90
|
+
- `--brief` en el barrido de GetOnBoard; el JD completo solo vía `detail` para las `high`.
|
|
91
|
+
- Si ambos CLIs fallan, frenar y reportar (no degradar a scraping manual sin avisar).
|
|
92
|
+
|
|
93
|
+
## Handoff
|
|
94
|
+
|
|
95
|
+
Al cerrar la corrida, las ofertas con nota creada quedan READY para el pipeline de postulación: correr `/job-aplicar <nota|url>` con la mejor. Para gaps recurrentes detectados en los scores, `/job-upskill`.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# seen_jobs - estado de deduplicacion de /job-scrape
|
|
2
|
+
|
|
3
|
+
`seen_jobs.json` registra todas las ofertas vistas por `/job-scrape` para no volver a presentarlas en corridas siguientes. Schema formal en `seen_jobs.schema.json` (junto a este archivo). Vive en `<workspace>/tools/job_scraper/` (no entre las notas) porque es dato de maquina, no nota: el vault/Obsidian no lo indexa.
|
|
4
|
+
|
|
5
|
+
## Matriz de fuente de verdad (regla anti-divergencia)
|
|
6
|
+
|
|
7
|
+
| Dato | Fuente de verdad | Quien escribe |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| "Ya vi esta oferta" + triage + descartes pre-nota | `seen_jobs.json` | `/job-scrape` (y `/job-aplicar` al crear o descartar) |
|
|
10
|
+
| Estado del pipeline (`interesado -> aplicado -> ...`), match_score definitivo | Frontmatter de la nota en `03-Aplicaciones/` | `/job-aplicar` crea; `/job-outcome` actualiza |
|
|
11
|
+
| Vista humana de la cola | `00-Dashboard.md` | Derivado de las notas; `/job-aplicar` y `/job-outcome` fila a fila |
|
|
12
|
+
|
|
13
|
+
El flujo es **unidireccional**: seen_jobs -> nota -> dashboard. Cuando una entrada pasa a `status: nota_creada` se congela (solo conserva el puntero `nota`); el pipeline sigue en la nota y NUNCA se sincroniza de vuelta. Por eso no existe el estado `aplicado` en este archivo.
|
|
14
|
+
|
|
15
|
+
## Claves de dedup
|
|
16
|
+
|
|
17
|
+
1. **URL canonica** (por fuente): sin utm/tracking params; LinkedIn reducido a `linkedin.com/jobs/view/<id>`; GetOnBoard `/empleos/` -> `/jobs/`.
|
|
18
|
+
2. **`job_key`** = `slug(empresa)|slug(titulo)`: lowercase, sin acentos, sin puntuacion (conserva `.` interno: coderslab.io, .net), sin sufijos legales (inc, llc, sa...), sin calificadores del titulo entre parentesis ("(remote)", "(latam)"). Detecta la misma oferta publicada en dos fuentes con URLs distintas; se agrega a `sources[]` de la entrada existente en vez de duplicar.
|
|
19
|
+
3. Matching blando (misma empresa + alto solapamiento de tokens del titulo) = **warning** en `/job-scrape`, nunca dedup automatico (una empresa puede tener dos roles parecidos legitimos, o el mismo rol con titulo traducido).
|
|
20
|
+
|
|
21
|
+
## Mantenimiento
|
|
22
|
+
|
|
23
|
+
- **Punteros rotos:** `/job-scrape` valida al arrancar que toda entrada `nota_creada` apunte a una nota existente y reporta las rotas.
|
|
24
|
+
- **Poda (opt-in):** entradas `new`/`descartado` con `last_seen` de mas de 90 dias se pueden borrar. Solo a pedido del usuario, nunca automatico. Las `nota_creada` no se podan (son el historial de dedup contra aplicaciones reales).
|
|
25
|
+
- **Primer run:** el archivo arranca vacio (esqueleto del schema). Si ya existen notas en `03-Aplicaciones/`, `/job-scrape` ofrece backfillearlas como `nota_creada` (con `quick_score: null` y `url: "nota://<archivo>"` si la nota no tiene `link_oferta`).
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# GetOnBoard Search CLI
|
|
2
|
+
|
|
3
|
+
Busca ofertas en GetOnBoard (job board tech de LATAM) via su API pública v0. Sin auth, sin API key, **cero dependencias**: solo la stdlib de Python 3. Espejo del contrato de `linkedin_search.py` para que `/job-scrape` pueda usar ambos de forma intercambiable.
|
|
4
|
+
|
|
5
|
+
El script es `scripts/getonbrd_search.py`. Construido a medida siguiendo el patrón `/add-portal` de MadsLorentzen/ai-job-search (investigar endpoint → scaffold con el mismo contrato → test en vivo), y portado de TypeScript a Python por la regla de lenguaje por capa (`docs/coding-standards.md`); la salida de las dos versiones se comparó campo por campo sobre las mismas queries: identica.
|
|
6
|
+
|
|
7
|
+
## Uso
|
|
8
|
+
|
|
9
|
+
### Buscar ofertas
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
python .claude/skills/job-scrape/scripts/getonbrd_search.py search -q "<keywords>" [flags]
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Flags:
|
|
16
|
+
- `-q / --query <texto>` - **requerido**, minimo 3 chars. La API hace AND de todos los terminos.
|
|
17
|
+
- `--jobage <dias>` - publicada en los ultimos N dias (filtro client-side sobre `published_at`).
|
|
18
|
+
- `--remote <modo>` - `remote` | `hybrid` | `onsite` (filtro client-side).
|
|
19
|
+
- `--page <n>` - paginacion (20 resultados por pagina).
|
|
20
|
+
- `-n / --limit <n>` - tope de resultados (`0` no emite ninguno).
|
|
21
|
+
- `--brief` - solo en `json`: trunca cada `description` a 300 chars y marca `description_truncated`. Los JD completos son ~80% del payload, asi que para triage conviene `--brief` y despues `detail` del shortlist.
|
|
22
|
+
- `--format json|table|plain` - default `json`.
|
|
23
|
+
|
|
24
|
+
Sin flag `--location`: GetOnBoard es LATAM/remoto por naturaleza; filtrar con `--remote` y leer `countries` en el JSON.
|
|
25
|
+
|
|
26
|
+
### Detalle de una oferta
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
python .claude/skills/job-scrape/scripts/getonbrd_search.py detail <slug|url> [--format json|plain]
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Acepta el `ID` (slug) de los resultados de search o cualquier URL de getonbrd.com (`/jobs/...` o `/empleos/...`, con o sin categoria). Dos capas:
|
|
33
|
+
1. **Search API** (queries progresivas derivadas del slug): datos completos, incluye `applications_count`.
|
|
34
|
+
2. **Microdata fallback** (pagina SSR, schema.org JobPosting): para ofertas viejas o fuera del indice. Detecta y marca ofertas **CERRADAS**.
|
|
35
|
+
|
|
36
|
+
## Datos que devuelve (JSON)
|
|
37
|
+
|
|
38
|
+
`id` (slug), `title`, `company`, `location`, `date` (ISO), `url`, `seniority` (Junior/Semi Senior/Senior/Expert), `remote`, `remote_modality` (`remote_local`/`hybrid`/`no_remote`...), `remote_zone`, `countries`, `salary_min_usd_month`/`salary_max_usd_month` (USD/mes cuando esta publicado), `applications_count` (util para la dimension "calidad de la oferta" de la rubrica), `description` (texto plano).
|
|
39
|
+
|
|
40
|
+
## Ejemplos
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# Fullstack, ultimas 2 semanas
|
|
44
|
+
python .claude/skills/job-scrape/scripts/getonbrd_search.py search -q "fullstack developer" --jobage 14 --format table
|
|
45
|
+
|
|
46
|
+
# Triage barato: estructurado completo, JD truncado
|
|
47
|
+
python .claude/skills/job-scrape/scripts/getonbrd_search.py search -q "fullstack developer" --jobage 14 --brief
|
|
48
|
+
|
|
49
|
+
# .NET remoto
|
|
50
|
+
python .claude/skills/job-scrape/scripts/getonbrd_search.py search -q ".NET developer" --remote remote --format table
|
|
51
|
+
|
|
52
|
+
# JD completo de una oferta
|
|
53
|
+
python .claude/skills/job-scrape/scripts/getonbrd_search.py detail "https://www.getonbrd.com/jobs/programming/full-stack-developer-buildwithin-remote-b1ef" --format plain
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Notas
|
|
57
|
+
|
|
58
|
+
- API publica en beta (`api/v0`); si cambia, el fallback de microdata sigue funcionando.
|
|
59
|
+
- Errores a **stderr** como `{ "error": "...", "code": "..." }` con exit code `1`.
|
|
60
|
+
- Volumen bajo por cortesia; la API es publica pero no esta pensada para bulk scraping.
|
|
61
|
+
- Referencia de endpoints: `getonbrd-urls.md`.
|
|
62
|
+
- Tests de los parsers: `tests/job_scrape/test_getonbrd_search.py` en el repo de Arcane.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# GetOnBoard API v0 - referencia de endpoints (verificada 2026-07-13)
|
|
2
|
+
|
|
3
|
+
Base: `https://www.getonbrd.com/api/v0` - sin autenticacion para endpoints publicos. Formato JSON:API.
|
|
4
|
+
|
|
5
|
+
## Search de ofertas
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
GET /search/jobs?query=<q>&per_page=<n>&page=<n>&expand[]=seniority&expand[]=company
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
- `query`: requerido, min 3 chars. Hace AND de todos los terminos (queries largas sobre-restringen).
|
|
12
|
+
- `per_page` / `page`: paginacion; `meta: { page, per_page, total_pages }` en la respuesta.
|
|
13
|
+
- `expand[]`: resuelve las referencias `seniority` y `company` inline (sin expand vienen solo como `{id, type}`).
|
|
14
|
+
|
|
15
|
+
Respuesta por job (`data[]`): `id` = slug, `links.public_url`, y en `attributes`:
|
|
16
|
+
`title`, `description` (HTML), `functions` (HTML), `desirable` (HTML), `benefits` (HTML), `remote` (bool), `remote_modality` (`remote_local` | `hybrid` | `no_remote` | ...), `remote_zone`, `countries[]`, `min_salary`/`max_salary` (USD/mes, null si no publican), `published_at` (unix), `applications_count`, `seniority.data.attributes.name`, `company.data.attributes.name`, `lang`, `perks[]`.
|
|
17
|
+
|
|
18
|
+
## Catalogos publicos
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
GET /seniorities -> Junior=2, Semi Senior=3, Senior=4, Expert=5, Sin experiencia=1
|
|
22
|
+
GET /categories -> categorias (programming, design-ux, etc.)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Lo que NO es publico
|
|
26
|
+
|
|
27
|
+
- `GET /jobs/<slug>` devuelve **401** (endpoint privado, requiere API key de cuenta).
|
|
28
|
+
Por eso `detail` del script usa search + filtro por id exacto, con fallback a microdata.
|
|
29
|
+
|
|
30
|
+
## Paginas SSR (fallback de detail)
|
|
31
|
+
|
|
32
|
+
- `https://www.getonbrd.com/jobs/<slug>` redirige 301 a `/jobs/<categoria>/<slug>`.
|
|
33
|
+
- La pagina renderiza microdata schema.org JobPosting via atributos `itemprop`:
|
|
34
|
+
`title`, `hiringOrganization > name`, `datePosted`, `baseSalary > minValue/maxValue`,
|
|
35
|
+
`description`, `qualifications` (seniority), `address`.
|
|
36
|
+
- Ofertas cerradas: pierden el `itemtype="http://schema.org/JobPosting"` pero conservan
|
|
37
|
+
los `itemprop`; la pagina contiene el texto "Closed job" / "No longer accepting".
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# LinkedIn Search CLI
|
|
2
|
+
|
|
3
|
+
Busca ofertas en el job board publico de LinkedIn (endpoints `jobs-guest`) para cualquier pais/region y remoto. Sin autenticacion, sin API key, **cero dependencias**: solo la stdlib de Python 3.
|
|
4
|
+
|
|
5
|
+
El script es `scripts/linkedin_search.py`. Portado del CLI en TypeScript vendoreado de [MadsLorentzen/ai-job-search](https://github.com/MadsLorentzen/ai-job-search) (`.agents/skills/linkedin-search/`): los scripts de skill corren en el proyecto del usuario sin build step, y TypeScript ahi implica exigir Node 24+ para el type stripping nativo (ver `docs/coding-standards.md`). La traduccion se verifico comparando la salida de las dos versiones sobre el mismo HTML y el mismo detalle: identica.
|
|
6
|
+
|
|
7
|
+
Diferencias respecto del upstream, todas deliberadas:
|
|
8
|
+
- `normalize_id` acepta el slash final: la URL del boton de compartir es `/jobs/view/<id>/?refId=...` y el regex original solo anclaba en `?`.
|
|
9
|
+
- Warning `NO_CARDS_PARSED` a stderr cuando la respuesta trae contenido pero se parsean 0 cards (cambio de markup vs "no hay ofertas").
|
|
10
|
+
- ` ` se normaliza a espacio comun: U+00A0 es invisible y rompe el matching de keywords en el JD.
|
|
11
|
+
- Salida UTF-8 forzada: en Windows `stdout` es cp1252 y cualquier acento del JD reventaba al redirigir.
|
|
12
|
+
|
|
13
|
+
## ⚠️ Uso personal, volumen bajo
|
|
14
|
+
|
|
15
|
+
Lee paginas publicas de LinkedIn; el acceso automatizado va contra sus Terms of Service. **Mantener volumen bajo** (busquedas puntuales, no barridos masivos ni uso comercial). El script reintenta 429/5xx con backoff exponencial. `/job-scrape` pide confirmacion explicita antes de la primera corrida de la sesion.
|
|
16
|
+
|
|
17
|
+
## Comandos
|
|
18
|
+
|
|
19
|
+
### Buscar ofertas
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
python .claude/skills/job-scrape/scripts/linkedin_search.py search -l "<lugar>" [flags]
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Flags:
|
|
26
|
+
- `-l / --location <texto>` - **requerido.** Place string de LinkedIn: `"Spain"`, `"European Union"`, `"Remote"`, `"Berlin, Germany"`.
|
|
27
|
+
- `-q / --query <texto>` - keywords (titulo, skill, rol). Recomendado.
|
|
28
|
+
- `--jobage <dias>` - publicado en los ultimos `1`, `7`, `14` o `30` dias.
|
|
29
|
+
- `--remote <modo>` - `remote`, `hybrid` u `onsite`.
|
|
30
|
+
- `--page <n>` - paginacion (10 resultados por pagina).
|
|
31
|
+
- `-n / --limit <n>` - tope de resultados emitidos (`0` no emite ninguno).
|
|
32
|
+
- `--format json|table|plain` - default `json`.
|
|
33
|
+
|
|
34
|
+
### Detalle completo de una oferta
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
python .claude/skills/job-scrape/scripts/linkedin_search.py detail <id|url> [--format json|plain]
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Acepta el ID numerico del search, una URL `jobs/view/...` (con o sin slash final y query params, como la que da el boton de compartir) o un URN. Devuelve descripcion completa, seniority, tipo de empleo, industrias y link.
|
|
41
|
+
|
|
42
|
+
## Ejemplos
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# Fullstack remoto en un pais, ultima semana
|
|
46
|
+
python .claude/skills/job-scrape/scripts/linkedin_search.py search -q "full stack developer" -l "Spain" --remote remote --jobage 7 --format table
|
|
47
|
+
|
|
48
|
+
# Backend .NET remoto en una region
|
|
49
|
+
python .claude/skills/job-scrape/scripts/linkedin_search.py search -q ".NET developer" -l "European Union" --remote remote --jobage 14 --format table
|
|
50
|
+
|
|
51
|
+
# Game developer remoto global
|
|
52
|
+
python .claude/skills/job-scrape/scripts/linkedin_search.py search -q "game developer" -l "Remote" --jobage 30 --format table
|
|
53
|
+
|
|
54
|
+
# JD completo de una oferta
|
|
55
|
+
python .claude/skills/job-scrape/scripts/linkedin_search.py detail 4434569000 --format plain
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Notas
|
|
59
|
+
|
|
60
|
+
- Errores van a **stderr** como `{ "error": "...", "code": "..." }` con exit code `1`. Los diagnosticos no fatales usan la misma forma con clave `warning` (ej. `NO_CARDS_PARSED`) y no cambian el exit code.
|
|
61
|
+
- El search de LinkedIn **no es determinista**: dos corridas seguidas con los mismos parametros pueden devolver ofertas distintas. No asumir que "no aparece" significa "no existe"; el dedup de `/job-scrape` esta justamente para eso.
|
|
62
|
+
- `--format json` sirve para encadenar: sacar IDs y pasarlos a `detail`, o scorear ofertas contra los criterios (`match_score`) antes de crear notas en `03-Aplicaciones/` del career workspace.
|
|
63
|
+
- Referencia de como se construyen las URLs upstream: `linkedin-urls.md`.
|
|
64
|
+
- Tests de los parsers: `tests/job_scrape/test_linkedin_search.py` en el repo de Arcane.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# LinkedIn Jobs URL Reference
|
|
2
|
+
|
|
3
|
+
Public, unauthenticated `jobs-guest` endpoints used by this skill. Global — the same
|
|
4
|
+
endpoints serve every market; only the `location` value changes.
|
|
5
|
+
|
|
6
|
+
> Personal use only — automated access is against LinkedIn's Terms of Service; keep volume low.
|
|
7
|
+
|
|
8
|
+
## Search
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
GET https://www.linkedin.com/jobs-guest/jobs/api/seeMoreJobPostings/search
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Query params:
|
|
15
|
+
|
|
16
|
+
| Param | Meaning | Example |
|
|
17
|
+
|-------|---------|---------|
|
|
18
|
+
| `keywords` | Free-text query | `data engineer` |
|
|
19
|
+
| `location` | Place string | `Mumbai, Maharashtra, India` · `Berlin, Germany` · `Remote` |
|
|
20
|
+
| `f_TPR` | Posted-within window (seconds) | `r604800` (7d), `r2592000` (30d) |
|
|
21
|
+
| `f_WT` | Workplace type | `1` on-site · `2` remote · `3` hybrid |
|
|
22
|
+
| `start` | Pagination offset (10/page) | `0`, `10`, `20`, … |
|
|
23
|
+
|
|
24
|
+
Returns an HTML list of job cards (one `<li>` per posting). The script parses each card by
|
|
25
|
+
its `data-entity-urn="urn:li:jobPosting:<id>"` and extracts title, company, location, date, URL.
|
|
26
|
+
|
|
27
|
+
## Detail
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
GET https://www.linkedin.com/jobs-guest/jobs/api/jobPosting/<jobId>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Returns a single job's HTML: title (`top-card-layout__title`), company
|
|
34
|
+
(`topcard__org-name-link`), location (`topcard__flavor--bullet`), the rich description
|
|
35
|
+
(`show-more-less-html__markup` / `description__text`), and job-criteria items
|
|
36
|
+
(seniority, employment type, job function, industries).
|
|
37
|
+
|
|
38
|
+
## Notes
|
|
39
|
+
|
|
40
|
+
- No authentication required.
|
|
41
|
+
- Respect rate limits: the script backs off on 429/5xx.
|
|
42
|
+
- Country-agnostic: pass any `--location` (city, region, country, or "Remote").
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"title": "seen_jobs",
|
|
4
|
+
"description": "Registro de ofertas vistas por /job-scrape para deduplicacion entre corridas. Fuente de verdad SOLO del triage pre-nota; el estado del pipeline vive en el frontmatter de las notas de 03-Aplicaciones/ del career workspace y el dashboard es una vista derivada.",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["schema_version", "actualizado", "jobs"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"$schema": { "type": "string" },
|
|
9
|
+
"schema_version": { "type": "integer", "const": 1 },
|
|
10
|
+
"actualizado": { "type": "string", "format": "date", "description": "Fecha ISO de la ultima escritura" },
|
|
11
|
+
"jobs": {
|
|
12
|
+
"type": "object",
|
|
13
|
+
"description": "Mapa job_key -> entrada. job_key = slug normalizado de 'empresa|titulo' (lowercase, sin acentos, sin puntuacion, sin sufijos legales de empresa, sin calificadores '(remote)'/'(latam)' del titulo).",
|
|
14
|
+
"additionalProperties": {
|
|
15
|
+
"type": "object",
|
|
16
|
+
"required": ["title", "company", "sources", "first_seen", "last_seen", "status"],
|
|
17
|
+
"properties": {
|
|
18
|
+
"title": { "type": "string", "description": "Titulo como se vio la primera vez" },
|
|
19
|
+
"company": { "type": "string" },
|
|
20
|
+
"sources": {
|
|
21
|
+
"type": "array",
|
|
22
|
+
"minItems": 1,
|
|
23
|
+
"items": {
|
|
24
|
+
"type": "object",
|
|
25
|
+
"required": ["source", "url", "first_seen", "last_seen"],
|
|
26
|
+
"properties": {
|
|
27
|
+
"source": { "enum": ["linkedin", "getonbrd", "manual", "otro"] },
|
|
28
|
+
"source_id": { "type": ["string", "null"], "description": "ID nativo de la fuente (numerico LinkedIn, slug GetOnBoard)" },
|
|
29
|
+
"url": { "type": "string", "description": "URL canonica (sin utm/tracking; linkedin.com/jobs/view/<id>; getonbrd /empleos/ -> /jobs/)" },
|
|
30
|
+
"publicada": { "type": ["string", "null"], "format": "date" },
|
|
31
|
+
"first_seen": { "type": "string", "format": "date" },
|
|
32
|
+
"last_seen": { "type": "string", "format": "date" }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"first_seen": { "type": "string", "format": "date", "description": "Primera vez vista en cualquier fuente" },
|
|
37
|
+
"last_seen": { "type": "string", "format": "date" },
|
|
38
|
+
"perfil": { "type": ["string", "null"], "description": "Nota de perfil contra la que se evaluo (nombre de la nota de 01-Perfiles/)" },
|
|
39
|
+
"quick_score": { "enum": ["high", "medium", "low", null], "description": "Triage barato sobre datos del listing" },
|
|
40
|
+
"match_score": { "type": ["integer", "null"], "minimum": 0, "maximum": 100, "description": "Score de la rubrica completa de job-search, si se corrio" },
|
|
41
|
+
"status": {
|
|
42
|
+
"enum": ["new", "descartado", "nota_creada"],
|
|
43
|
+
"description": "Estados del triage. Deliberadamente NO existe 'aplicado': al crear la nota la entrada se congela y el pipeline sigue en el frontmatter de la nota (lo mueve /job-outcome)."
|
|
44
|
+
},
|
|
45
|
+
"descarte_motivo": { "type": ["string", "null"], "description": "Obligatorio si status=descartado (ver allOf)" },
|
|
46
|
+
"nota": { "type": ["string", "null"], "description": "Nombre de archivo de la nota en 03-Aplicaciones/ (obligatorio si status=nota_creada, ver allOf)" },
|
|
47
|
+
"salario_max_usd_mes": { "type": ["integer", "null"], "description": "Salario maximo publicado en USD/mes, para re-triage sin re-fetch" }
|
|
48
|
+
},
|
|
49
|
+
"allOf": [
|
|
50
|
+
{
|
|
51
|
+
"if": { "properties": { "status": { "const": "descartado" } }, "required": ["status"] },
|
|
52
|
+
"then": {
|
|
53
|
+
"required": ["descarte_motivo"],
|
|
54
|
+
"properties": { "descarte_motivo": { "type": "string", "minLength": 1 } }
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"if": { "properties": { "status": { "const": "nota_creada" } }, "required": ["status"] },
|
|
59
|
+
"then": {
|
|
60
|
+
"required": ["nota"],
|
|
61
|
+
"properties": { "nota": { "type": "string", "minLength": 1 } }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|