mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-09-17 08:36:25 +00:00
Initial release: AI-powered job application framework
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
committed by
Mads Lorentzen
co-authored by
Claude Opus 4.6
commit
c66d599d75
@@ -0,0 +1,228 @@
|
||||
---
|
||||
name: jobdanmark-search
|
||||
version: 1.0.0
|
||||
description: >
|
||||
Make sure to use this skill whenever the user mentions anything related to Danish
|
||||
job listings, job search in Denmark, finding work in Denmark, or job vacancies on
|
||||
Jobdanmark — even if they don't explicitly mention jobdanmark.dk. Also invoke this
|
||||
skill for questions about specific Danish job categories, municipalities, job types,
|
||||
or salaries in a job-search context. Trigger phrases include:
|
||||
danish jobs, jobs in denmark, find job denmark, job search denmark, danish job listings,
|
||||
jobdanmark, job opslag, find job, jobsøgning, ledige stillinger, stillingsopslag,
|
||||
job i Danmark, fuldtidsjob, deltidsjob, studiejob, praktikplads, elev, fleksjob,
|
||||
IT job denmark, sygeplejersker job, håndværker job, ingeniør job, pædagog job,
|
||||
kontor job, leder job, salg job, hotel job, kirke job, job aarhus, job københavn,
|
||||
job odense, job aalborg, job sjælland, job jylland, job fyn, jobkategorier denmark,
|
||||
ledige job, ansøgningsfrist, søg job, job opslaget, jobopslag, danish vacancies,
|
||||
work in denmark, employment denmark, job denmark, jobs near me denmark,
|
||||
apprentice denmark, internship denmark, part-time denmark, full-time denmark.
|
||||
context: fork
|
||||
allowed-tools: Bash(bun run skills/jobdanmark-search/cli/src/cli.ts *)
|
||||
---
|
||||
|
||||
# Jobdanmark Search Skill
|
||||
|
||||
Access live Danish job listings from the Jobdanmark.dk public API. No authentication needed.
|
||||
Covers ~15,000+ active job listings across 10 categories and all Danish municipalities.
|
||||
|
||||
## When to use this skill
|
||||
|
||||
Invoke this skill when the user wants to:
|
||||
|
||||
- Search for job listings in Denmark (by keyword, location, category, or job type)
|
||||
- Browse available jobs in a specific Danish city, municipality, zip code, or region
|
||||
- Filter jobs by employment type (full-time, part-time, student job, apprentice, etc.)
|
||||
- Look up the full details of a specific job posting including description and deadline
|
||||
- List all job categories with current live job counts
|
||||
- Resolve a job search term into a job title ID or category ID for precise filtering
|
||||
- Find location suggestions (municipalities, zip codes, regions) for job search filters
|
||||
|
||||
## Commands
|
||||
|
||||
### Search job listings
|
||||
|
||||
```bash
|
||||
bun run skills/jobdanmark-search/cli/src/cli.ts search [flags]
|
||||
```
|
||||
|
||||
Key flags:
|
||||
- `--text <keyword>` — free-text search, e.g. `elektriker`, `sygeplejerske`, `softwareudvikler`
|
||||
- `--category <id>` — category ID (see Categories table below)
|
||||
- `--jobtitle-id <id>` — job title ID from `autocomplete` results
|
||||
- `--municipality <name>` — e.g. `Odense`, `København`, `Aarhus`
|
||||
- `--zip <code>` — zip code, e.g. `5000`, `8000`, `2100`
|
||||
- `--region <name>` — region name, e.g. `Midtjylland`, `Sjælland`
|
||||
- `--job-type <types>` — comma-separated: `fuldtid`, `deltid`, `fleksjob`, `elev`, `studiejob`, `praktik`
|
||||
- `--page <n>` — page number (30 items per page, server-enforced)
|
||||
- `--limit <n>` — cap total results returned by CLI
|
||||
- `--format json|table|plain`
|
||||
|
||||
> Per-page is fixed at 30 by the API. Use `--page` to paginate.
|
||||
|
||||
### Full job detail
|
||||
|
||||
```bash
|
||||
bun run skills/jobdanmark-search/cli/src/cli.ts detail <slug> [--format json|plain]
|
||||
```
|
||||
|
||||
`slug` is the URL path segment returned as `slug` in `search` results (e.g. `it-chef-soeges-til-rah`).
|
||||
Returns full structured job data from the job page's JSON-LD, including title, organization, location, employment type, deadline, and full HTML description.
|
||||
|
||||
### List categories with live counts
|
||||
|
||||
```bash
|
||||
bun run skills/jobdanmark-search/cli/src/cli.ts categories [--format json|table|plain]
|
||||
```
|
||||
|
||||
Returns all 10 job categories with current live job counts. Useful for giving the user an overview of the job market.
|
||||
|
||||
### Autocomplete job titles and categories
|
||||
|
||||
```bash
|
||||
bun run skills/jobdanmark-search/cli/src/cli.ts autocomplete --query "<text>" [--limit <n>]
|
||||
```
|
||||
|
||||
Use this to resolve search terms into precise job title IDs (`--jobtitle-id`) or category IDs (`--category`) for `search`. The `value` field in results is the ID to pass to `search`.
|
||||
|
||||
### Suggest locations
|
||||
|
||||
```bash
|
||||
bun run skills/jobdanmark-search/cli/src/cli.ts locations --query "<text>" [--limit <n>]
|
||||
```
|
||||
|
||||
Returns matching municipalities, zip codes, and regions. Use `value` from results as `--municipality` or `--zip` in `search`.
|
||||
|
||||
---
|
||||
|
||||
## Categories
|
||||
|
||||
| ID | Danish | English |
|
||||
|----|--------|---------|
|
||||
| 227972 | Pædagogik, Uddannelse og Forskning | Education, Research |
|
||||
| 227973 | Håndværk, Industri, Transport og Landbrug | Crafts, Industry, Transport, Agriculture |
|
||||
| 227974 | Salg, Kommunikation, Marketing, og Design | Sales, Communication, Marketing, Design |
|
||||
| 227975 | Pleje, Social og Sundhed | Care, Social, Health |
|
||||
| 227976 | Hotel, Service, Restauration og Sikkerhed | Hotel, Service, Restaurant, Security |
|
||||
| 227977 | Kontor, Finans og Økonomi | Office, Finance, Economy |
|
||||
| 227978 | IT, Ingeniør og Energi | IT, Engineering, Energy |
|
||||
| 227979 | Ledelse, HR og projektstyring | Management, HR, Project Management |
|
||||
| 543415 | Kirke, Kultur og Underholdning | Church, Culture, Entertainment |
|
||||
| 227980 | Øvrige job | Other jobs |
|
||||
|
||||
---
|
||||
|
||||
## How to use effectively
|
||||
|
||||
**Resolve locations first.** Use `locations` to find the correct municipality name or zip code before passing them to `search`:
|
||||
|
||||
```bash
|
||||
bun run skills/jobdanmark-search/cli/src/cli.ts locations --query "Aarhus" --format plain
|
||||
```
|
||||
|
||||
**Resolve job titles for precision.** Use `autocomplete` to get the exact job title ID when the user wants a specific role:
|
||||
|
||||
```bash
|
||||
bun run skills/jobdanmark-search/cli/src/cli.ts autocomplete --query "sygeplejerske" --format plain
|
||||
```
|
||||
|
||||
**Natural workflow: `search` → `detail`.**
|
||||
1. Use `search` to get a list of matching jobs with their `slug`.
|
||||
2. Call `detail <slug>` to get the full job posting with description, deadline, and organization details.
|
||||
|
||||
**Use `--format table` for comparisons**, `--format json` for data processing, and `--format plain` for single-record detail views.
|
||||
|
||||
**Pagination**: The API returns 30 items per page (server-enforced). Use `--page` to navigate through results. Use `--limit` to cap CLI output regardless of page size.
|
||||
|
||||
**Combine filters** for precise results — e.g. `--text` + `--job-type` + `--municipality` all work together in a single search request.
|
||||
|
||||
---
|
||||
|
||||
## Usage examples
|
||||
|
||||
### IT jobs in Copenhagen
|
||||
|
||||
```bash
|
||||
bun run skills/jobdanmark-search/cli/src/cli.ts search \
|
||||
--category 227978 \
|
||||
--municipality "København" \
|
||||
--job-type fuldtid \
|
||||
--format table
|
||||
```
|
||||
|
||||
### Nursing jobs anywhere in Denmark
|
||||
|
||||
```bash
|
||||
bun run skills/jobdanmark-search/cli/src/cli.ts search \
|
||||
--text "sygeplejerske" \
|
||||
--category 227975 \
|
||||
--format table
|
||||
```
|
||||
|
||||
### Student jobs in Aarhus
|
||||
|
||||
```bash
|
||||
bun run skills/jobdanmark-search/cli/src/cli.ts search \
|
||||
--municipality "Aarhus" \
|
||||
--job-type studiejob \
|
||||
--format table
|
||||
```
|
||||
|
||||
### What job categories are most active right now?
|
||||
|
||||
```bash
|
||||
bun run skills/jobdanmark-search/cli/src/cli.ts categories --format table
|
||||
```
|
||||
|
||||
### Full details for a specific job posting
|
||||
|
||||
```bash
|
||||
bun run skills/jobdanmark-search/cli/src/cli.ts detail it-chef-soeges-til-rah --format plain
|
||||
```
|
||||
|
||||
### Find jobs in zip code 8000
|
||||
|
||||
```bash
|
||||
bun run skills/jobdanmark-search/cli/src/cli.ts search --zip 8000 --format table
|
||||
```
|
||||
|
||||
### What electrician jobs are available?
|
||||
|
||||
```bash
|
||||
bun run skills/jobdanmark-search/cli/src/cli.ts search \
|
||||
--text "elektriker" \
|
||||
--category 227973 \
|
||||
--job-type "fuldtid,deltid" \
|
||||
--format table
|
||||
```
|
||||
|
||||
### Apprentice positions in all of Denmark
|
||||
|
||||
```bash
|
||||
bun run skills/jobdanmark-search/cli/src/cli.ts search \
|
||||
--job-type elev \
|
||||
--page 1 \
|
||||
--format table
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Output formats
|
||||
|
||||
| Format | Best for |
|
||||
|--------|----------|
|
||||
| `json` | Default — programmatic use, data processing, passing IDs between commands |
|
||||
| `table` | Quick human-readable overviews and comparisons |
|
||||
| `plain` | Single-record detail views (`detail`) |
|
||||
|
||||
All errors are written to **stderr** as `{ "error": "...", "code": "..." }` and the process exits with code `1`.
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- All data is from the public Jobdanmark.dk API — no credentials required.
|
||||
- Pagination is 1-indexed (`--page 1` is the first page). 30 items per page, server-enforced.
|
||||
- The `detail` command fetches the HTML job page and parses the embedded JSON-LD (schema.org/JobPosting). It does not use a separate JSON API.
|
||||
- `slug` in search results is extracted from the API's relative `url` field (the path after `/job/`).
|
||||
- `applicationDeadline` in search results can be `null` (no deadline set).
|
||||
- Job type values for filters: `fuldtid`, `deltid`, `fleksjob`, `elev`, `studiejob`, `praktik`.
|
||||
@@ -0,0 +1,452 @@
|
||||
# jobdanmark-cli
|
||||
|
||||
CLI for the [Jobdanmark.dk](https://www.jobdanmark.dk) public job search API.
|
||||
|
||||
**Base URL**: `https://jobdanmark.dk`
|
||||
**Authentication**: None required.
|
||||
**Format**: All responses are JSON.
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
cd skills/jobdanmark-search/cli
|
||||
bun install
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `search` | Search job listings with filters |
|
||||
| `detail` | Full detail for a single job posting (by slug) |
|
||||
| `categories` | List all job categories with live counts |
|
||||
| `autocomplete` | Suggest job titles and categories for a query |
|
||||
| `locations` | Suggest municipalities, zip codes, and regions for a query |
|
||||
|
||||
All commands accept `--format json|table|plain` (default: `json`).
|
||||
All errors are written to **stderr** as `{ "error": "...", "code": "..." }` and the process exits with code `1`.
|
||||
|
||||
---
|
||||
|
||||
## Job Types
|
||||
|
||||
| Value | Danish | English |
|
||||
|-------|--------|---------|
|
||||
| `fuldtid` | Fuldtid | Full-time |
|
||||
| `deltid` | Deltid | Part-time |
|
||||
| `fleksjob` | Fleksjob | Flex job |
|
||||
| `elev` | Elev | Apprentice |
|
||||
| `studiejob` | Studiejob | Student job |
|
||||
| `praktik` | Praktik | Internship |
|
||||
|
||||
---
|
||||
|
||||
## Categories
|
||||
|
||||
| ID | Danish Title |
|
||||
|----|-------------|
|
||||
| 227972 | Pædagogik, Uddannelse og Forskning |
|
||||
| 227973 | Håndværk, Industri, Transport og Landbrug |
|
||||
| 227974 | Salg, Kommunikation, Marketing, og Design |
|
||||
| 227975 | Pleje, Social og Sundhed |
|
||||
| 227976 | Hotel, Service, Restauration og Sikkerhed |
|
||||
| 227977 | Kontor, Finans og Økonomi |
|
||||
| 227978 | IT, Ingeniør og Energi |
|
||||
| 227979 | Ledelse, HR og projektstyring |
|
||||
| 543415 | Kirke, Kultur og Underholdning |
|
||||
| 227980 | Øvrige job |
|
||||
|
||||
---
|
||||
|
||||
## `search` — Search job listings
|
||||
|
||||
**Endpoint**: `POST https://jobdanmark.dk/api/jobsearch/search/{page}`
|
||||
|
||||
```bash
|
||||
bun run src/cli.ts search [flags]
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `--text` | string | — | Free-text keyword search (job title, keyword) |
|
||||
| `--category` | number | — | Category ID (see table above) |
|
||||
| `--jobtitle-id` | number | — | Job title ID from `autocomplete` results |
|
||||
| `--municipality` | string | — | Municipality name, e.g. `Odense`, `København` |
|
||||
| `--zip` | string | — | Zip code, e.g. `5000` |
|
||||
| `--region` | string | — | Region name |
|
||||
| `--job-type` | string | — | Comma-separated job types: `fuldtid,deltid,fleksjob,elev,studiejob,praktik` |
|
||||
| `--page` | number | `1` | Page number (30 items per page, server-enforced) |
|
||||
| `--limit` | number | — | Cap total results returned by CLI (client-side) |
|
||||
| `--format` | string | `json` | Output format: `json`, `table`, `plain` |
|
||||
|
||||
> **Note**: Per-page is fixed at 30 by the API. There is no `--per-page` flag.
|
||||
|
||||
> **Note**: Multiple `--job-type` values are passed as a comma-separated string and split by the CLI into the `jobTypes` array in the request body.
|
||||
|
||||
### Example
|
||||
|
||||
```bash
|
||||
bun run src/cli.ts search --text "elektriker" --job-type fuldtid --municipality "Odense"
|
||||
|
||||
bun run src/cli.ts search --category 227978 --job-type "fuldtid,deltid" --page 2 --format table
|
||||
|
||||
bun run src/cli.ts search --text "sygeplejerske" --zip 8000 --limit 10
|
||||
```
|
||||
|
||||
### Request body sent to API
|
||||
|
||||
```json
|
||||
{
|
||||
"jobTypes": ["fuldtid"],
|
||||
"filters": [
|
||||
{ "type": "freetext", "value": "elektriker", "displayText": "elektriker" },
|
||||
{ "type": "municipality", "value": "Odense", "displayText": "Odense" }
|
||||
],
|
||||
"locationMode": "Text",
|
||||
"distance": 50
|
||||
}
|
||||
```
|
||||
|
||||
> **Important**: All filter objects include a `displayText` field (required by the server). The `value` field is an integer for `category` and `jobtitle` filter types; a string for all others.
|
||||
|
||||
### Response shape
|
||||
|
||||
```json
|
||||
{
|
||||
"meta": {
|
||||
"currentPage": 1,
|
||||
"totalItems": 15610,
|
||||
"itemsPrPage": 30,
|
||||
"totalPages": 521
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"title": "IT-chef søges til RAH",
|
||||
"companyName": "Rah Service A/S",
|
||||
"companyLogo": {
|
||||
"key": "71f1c950-abcd-1234-efgh-000000000000",
|
||||
"url": "https://jobdanmark.dk/media/k1epc2kk/rah-service-logo.jpg",
|
||||
"focalPoint": null
|
||||
},
|
||||
"companyLogoSvgMarkup": null,
|
||||
"overlayColor": "#FFFFFF1F",
|
||||
"companyAddress": "Ndr Ringvej 4 6950 Ringkøbing",
|
||||
"jobTypes": ["fuldtid"],
|
||||
"boostJob": true,
|
||||
"publishedDate": "12-03-2026",
|
||||
"applicationDeadline": "10-04-2026",
|
||||
"url": "https://jobdanmark.dk/job/it-chef-soeges-til-rah",
|
||||
"slug": "it-chef-soeges-til-rah",
|
||||
"coverImage": {
|
||||
"key": "cf06eb46-abcd-1234-efgh-000000000000",
|
||||
"url": "https://jobdanmark.dk/media/idvbnt4y/rah-service-as-billede.png",
|
||||
"focalPoint": { "top": 0.488, "left": 0.499 }
|
||||
},
|
||||
"silhouetteLogo": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
> **Notes**:
|
||||
> - `url` is normalized to a full URL (CLI prepends `https://jobdanmark.dk` to the relative path from the API).
|
||||
> - `slug` is extracted from the relative `url` field (the path segment after `/job/`).
|
||||
> - `applicationDeadline` can be `null`.
|
||||
> - `companyLogo` can be `null`.
|
||||
> - `publishedDate` format: `"DD-MM-YYYY"`.
|
||||
> - `coverImage` can be `null`.
|
||||
|
||||
---
|
||||
|
||||
## `detail` — Full job posting detail
|
||||
|
||||
**Method**: Fetch HTML from `https://jobdanmark.dk/job/{slug}`, extract `<script type="application/ld+json">` block.
|
||||
|
||||
```bash
|
||||
bun run src/cli.ts detail <slug> [--format json|plain]
|
||||
```
|
||||
|
||||
The `slug` is the URL path segment returned as `slug` in `search` results (e.g. `it-chef-soeges-til-rah`).
|
||||
|
||||
### Example
|
||||
|
||||
```bash
|
||||
bun run src/cli.ts detail it-chef-soeges-til-rah
|
||||
bun run src/cli.ts detail it-chef-soeges-til-rah --format plain
|
||||
```
|
||||
|
||||
### Response shape
|
||||
|
||||
```json
|
||||
{
|
||||
"slug": "it-chef-soeges-til-rah",
|
||||
"url": "https://jobdanmark.dk/job/it-chef-soeges-til-rah",
|
||||
"title": "IT-chef søges til RAH",
|
||||
"datePosted": "2026-03-12",
|
||||
"validThrough": "2026-04-10",
|
||||
"employmentType": ["FULL_TIME"],
|
||||
"hiringOrganization": {
|
||||
"name": "Rah Service A/S",
|
||||
"logo": "https://jobdanmark.dk/media/k1epc2kk/rah-service-logo.jpg?width=50"
|
||||
},
|
||||
"jobLocation": {
|
||||
"streetAddress": "Ndr Ringvej 4 6950 Ringkøbing",
|
||||
"addressLocality": "Ringkøbing",
|
||||
"addressRegion": "Vestjylland",
|
||||
"postalCode": "6950",
|
||||
"addressCountry": "DK"
|
||||
},
|
||||
"description": "<p>Full HTML description...</p>"
|
||||
}
|
||||
```
|
||||
|
||||
> **Note**: The `hiringOrganization.logo` and `validThrough` fields may be `null` if not present in the JSON-LD. `jobLocation` fields may be `null` if the location data is absent.
|
||||
|
||||
---
|
||||
|
||||
## `categories` — List categories with live job counts
|
||||
|
||||
**Endpoint**: `GET https://jobdanmark.dk/api/categorycount/getcounts`
|
||||
|
||||
```bash
|
||||
bun run src/cli.ts categories [flags]
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `--limit` | number | — | Cap number of categories returned |
|
||||
| `--format` | string | `json` | Output format: `json`, `table`, `plain` |
|
||||
|
||||
### Example
|
||||
|
||||
```bash
|
||||
bun run src/cli.ts categories
|
||||
bun run src/cli.ts categories --format table
|
||||
```
|
||||
|
||||
### Response shape
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 227972,
|
||||
"title": "Pædagogik, Uddannelse og Forskning",
|
||||
"helpText": "Pædagog, lærer, forsker og meget mere...",
|
||||
"count": 1856
|
||||
},
|
||||
{
|
||||
"id": 227973,
|
||||
"title": "Håndværk, Industri, Transport og Landbrug",
|
||||
"helpText": "Elektriker, VVS, maler og meget mere...",
|
||||
"count": 2816
|
||||
},
|
||||
{
|
||||
"id": 227974,
|
||||
"title": "Salg, Kommunikation, Marketing, og Design",
|
||||
"helpText": "Sælger, marketingkoordinator og meget mere...",
|
||||
"count": 1423
|
||||
},
|
||||
{
|
||||
"id": 227975,
|
||||
"title": "Pleje, Social og Sundhed",
|
||||
"helpText": "Sygeplejerske, social- og sundhedsassistent og meget mere...",
|
||||
"count": 2104
|
||||
},
|
||||
{
|
||||
"id": 227976,
|
||||
"title": "Hotel, Service, Restauration og Sikkerhed",
|
||||
"helpText": "Tjener, kok, receptionist og meget mere...",
|
||||
"count": 987
|
||||
},
|
||||
{
|
||||
"id": 227977,
|
||||
"title": "Kontor, Finans og Økonomi",
|
||||
"helpText": "Bogholder, revisor, kontorassistent og meget mere...",
|
||||
"count": 1234
|
||||
},
|
||||
{
|
||||
"id": 227978,
|
||||
"title": "IT, Ingeniør og Energi",
|
||||
"helpText": "Programmør, datamatiker, ingeniør og meget mere...",
|
||||
"count": 1567
|
||||
},
|
||||
{
|
||||
"id": 227979,
|
||||
"title": "Ledelse, HR og projektstyring",
|
||||
"helpText": "Leder, HR-konsulent, projektleder og meget mere...",
|
||||
"count": 876
|
||||
},
|
||||
{
|
||||
"id": 543415,
|
||||
"title": "Kirke, Kultur og Underholdning",
|
||||
"helpText": "Præst, musiker, skuespiller og meget mere...",
|
||||
"count": 234
|
||||
},
|
||||
{
|
||||
"id": 227980,
|
||||
"title": "Øvrige job",
|
||||
"helpText": "Andre job der ikke passer i de øvrige kategorier...",
|
||||
"count": 2329
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `autocomplete` — Suggest job titles and categories
|
||||
|
||||
**Endpoint**: `GET https://jobdanmark.dk/api/search/autocomplete?q={query}`
|
||||
|
||||
```bash
|
||||
bun run src/cli.ts autocomplete --query <text> [flags]
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `--query` | string | **required** | Search text to autocomplete |
|
||||
| `--limit` | number | — | Cap total suggestions returned |
|
||||
| `--format` | string | `json` | Output format: `json`, `table`, `plain` |
|
||||
|
||||
### Example
|
||||
|
||||
```bash
|
||||
bun run src/cli.ts autocomplete --query "it"
|
||||
bun run src/cli.ts autocomplete --query "sygeplejerske" --format table
|
||||
```
|
||||
|
||||
### Response shape
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"title": "Jobtitler",
|
||||
"items": [
|
||||
{
|
||||
"id": "title__454",
|
||||
"text": "IT-konsulent",
|
||||
"value": 454,
|
||||
"category": "jobtitle",
|
||||
"slug": "it-konsulent"
|
||||
},
|
||||
{
|
||||
"id": "title__123",
|
||||
"text": "IT-supporter",
|
||||
"value": 123,
|
||||
"category": "jobtitle",
|
||||
"slug": "it-supporter"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Kategorier",
|
||||
"items": [
|
||||
{
|
||||
"id": "category__227978",
|
||||
"text": "IT, Ingeniør og Energi",
|
||||
"value": 227978,
|
||||
"category": "category",
|
||||
"slug": "it-ingenioer-og-energi"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
> **Note**: Returns an empty array `[]` when no suggestions match. `value` is a numeric ID usable as `--jobtitle-id` or `--category` in `search`. Groups with no matching items are omitted from the response.
|
||||
|
||||
---
|
||||
|
||||
## `locations` — Suggest location filters
|
||||
|
||||
**Endpoint**: `GET https://jobdanmark.dk/api/search/locations?q={query}`
|
||||
|
||||
```bash
|
||||
bun run src/cli.ts locations --query <text> [flags]
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `--query` | string | **required** | Location text to search (city, zip code, region) |
|
||||
| `--limit` | number | — | Cap total suggestions returned |
|
||||
| `--format` | string | `json` | Output format: `json`, `table`, `plain` |
|
||||
|
||||
### Example
|
||||
|
||||
```bash
|
||||
bun run src/cli.ts locations --query "Odense"
|
||||
bun run src/cli.ts locations --query "8000" --format table
|
||||
bun run src/cli.ts locations --query "Sjælland"
|
||||
```
|
||||
|
||||
### Response shape
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"title": "Kommune",
|
||||
"items": [
|
||||
{
|
||||
"id": "municipality__0461",
|
||||
"text": "Odense",
|
||||
"value": "Odense",
|
||||
"category": "municipality",
|
||||
"slug": "odense"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Postnummer",
|
||||
"items": [
|
||||
{
|
||||
"id": "zip__5000",
|
||||
"text": "5000 Odense C",
|
||||
"value": "5000",
|
||||
"category": "zip",
|
||||
"slug": "5000-odense-c"
|
||||
},
|
||||
{
|
||||
"id": "zip__5200",
|
||||
"text": "5200 Odense V",
|
||||
"value": "5200",
|
||||
"category": "zip",
|
||||
"slug": "5200-odense-v"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
> **Note**: Returns `[]` when no locations match. The `value` field for municipalities is the display name (string); for zip codes it is the numeric string. Use `value` directly as `--municipality` or `--zip` in `search`.
|
||||
|
||||
---
|
||||
|
||||
## Error handling
|
||||
|
||||
All errors are written to **stderr** in JSON format and exit with code `1`:
|
||||
|
||||
```json
|
||||
{ "error": "Job not found", "code": "NOT_FOUND" }
|
||||
{ "error": "API request failed: 400 Bad Request", "code": "API_ERROR" }
|
||||
{ "error": "--query is required", "code": "MISSING_REQUIRED" }
|
||||
{ "error": "Failed to parse JSON-LD from job page", "code": "PARSE_ERROR" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## URL construction
|
||||
|
||||
- Job detail pages: `https://jobdanmark.dk/job/{slug}`
|
||||
- Company logo images: `https://jobdanmark.dk{companyLogo.url}` (prepend base URL to relative path)
|
||||
- Cover images: `https://jobdanmark.dk{coverImage.url}` (prepend base URL to relative path)
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "jobdanmark-cli",
|
||||
"version": "1.0.0",
|
||||
"description": "CLI for the Jobdanmark.dk public job search API",
|
||||
"type": "module",
|
||||
"main": "src/cli.ts",
|
||||
"bin": {
|
||||
"jobdanmark": "src/cli.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "bun run src/cli.ts",
|
||||
"test": "bun test --timeout 30000",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bunli/core": "latest",
|
||||
"@bunli/utils": "latest",
|
||||
"node-html-parser": "^6.1.0",
|
||||
"zod": "^3.23.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.4.0",
|
||||
"@types/bun": "latest"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { createCLI } from "@bunli/core"
|
||||
import { search } from "./commands/search.js"
|
||||
import { detail } from "./commands/detail.js"
|
||||
import { categories } from "./commands/categories.js"
|
||||
import { autocomplete } from "./commands/autocomplete.js"
|
||||
import { locations } from "./commands/locations.js"
|
||||
|
||||
const cli = await createCLI({
|
||||
name: "jobdanmark-cli",
|
||||
version: "1.0.0",
|
||||
description: "CLI for the Jobdanmark.dk public job search API",
|
||||
})
|
||||
|
||||
cli.command(search)
|
||||
cli.command(detail)
|
||||
cli.command(categories)
|
||||
cli.command(autocomplete)
|
||||
cli.command(locations)
|
||||
|
||||
await cli.run()
|
||||
@@ -0,0 +1,67 @@
|
||||
export const BASE_URL = "https://jobdanmark.dk"
|
||||
|
||||
export async function apiFetch<T>(path: string, params?: Record<string, string>): Promise<T> {
|
||||
let url = `${BASE_URL}${path}`
|
||||
if (params && Object.keys(params).length > 0) {
|
||||
const qs = new URLSearchParams(params)
|
||||
url += `?${qs.toString()}`
|
||||
}
|
||||
|
||||
const maxRetries = 6
|
||||
let delay = 500
|
||||
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||
const response = await fetch(url)
|
||||
if (response.status === 429 || response.status >= 500) {
|
||||
if (attempt === maxRetries) {
|
||||
throw new Error(`API request failed: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
const jitter = Math.floor(Math.random() * 500)
|
||||
await new Promise((resolve) => setTimeout(resolve, delay + jitter))
|
||||
delay = Math.min(delay * 2, 5000)
|
||||
continue
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error(`API request failed: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
return response.json() as Promise<T>
|
||||
}
|
||||
throw new Error("API request failed after max retries")
|
||||
}
|
||||
|
||||
export async function apiPost<T>(path: string, body: unknown): Promise<T> {
|
||||
const url = `${BASE_URL}${path}`
|
||||
|
||||
const maxRetries = 6
|
||||
let delay = 500
|
||||
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
if (response.status === 429 || response.status >= 500) {
|
||||
if (attempt === maxRetries) {
|
||||
throw new Error(`API request failed: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
const jitter = Math.floor(Math.random() * 500)
|
||||
await new Promise((resolve) => setTimeout(resolve, delay + jitter))
|
||||
delay = Math.min(delay * 2, 5000)
|
||||
continue
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error(`API request failed: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
return response.json() as Promise<T>
|
||||
}
|
||||
throw new Error("API request failed after max retries")
|
||||
}
|
||||
|
||||
export function writeError(error: string, code: string): void {
|
||||
process.stderr.write(JSON.stringify({ error, code }) + "\n")
|
||||
}
|
||||
|
||||
export function stripHtml(html: string): string {
|
||||
return html.replace(/<[^>]*>/g, "").replace(/\s+/g, " ").trim()
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { join } from "path";
|
||||
|
||||
const CLI_PATH = join(import.meta.dir, "../src/cli.ts");
|
||||
|
||||
export interface CLIResult {
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
exitCode: number;
|
||||
}
|
||||
|
||||
export async function runCLI(args: string[]): Promise<CLIResult> {
|
||||
const proc = Bun.spawn(["bun", "run", CLI_PATH, ...args], {
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([
|
||||
new Response(proc.stdout).text(),
|
||||
new Response(proc.stderr).text(),
|
||||
proc.exited,
|
||||
]);
|
||||
|
||||
return { stdout: stdout.trim(), stderr: stderr.trim(), exitCode };
|
||||
}
|
||||
|
||||
export function parseJSON<T = unknown>(result: CLIResult): T {
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(
|
||||
`CLI exited with code ${result.exitCode}. stderr: ${result.stderr}`
|
||||
);
|
||||
}
|
||||
try {
|
||||
return JSON.parse(result.stdout) as T;
|
||||
} catch {
|
||||
throw new Error(
|
||||
`Failed to parse JSON. stdout: ${result.stdout}\nstderr: ${result.stderr}`
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user