mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-09-17 00:26:26 +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,205 @@
|
||||
---
|
||||
name: jobnet-search
|
||||
version: 1.0.0
|
||||
description: >
|
||||
Make sure to use this skill whenever the user mentions anything related to Danish
|
||||
job searching, job listings, job vacancies, employment opportunities in Denmark, or
|
||||
the Danish government job portal — even if they don't mention jobnet.dk explicitly.
|
||||
Also invoke this skill for questions about specific job titles, occupations, employers,
|
||||
or regions in a Danish employment context. This skill covers the official Danish
|
||||
public job portal operated by STAR (Styrelsen for Arbejdsmarked og Rekruttering).
|
||||
Trigger phrases include: danish jobs, danish job search, jobnet, jobnet.dk, find job
|
||||
denmark, danish employment, job i danmark, job på jobnet, offentlige job, stillinger
|
||||
i det offentlige, public sector jobs denmark, government jobs denmark, STAR jobs,
|
||||
job ledige stillinger, ledig stilling, søg job, job opslag, job vacancy denmark,
|
||||
stillingopslag, jobopslag, sygepleje job, ingeniør job, lærer job, pædagog job,
|
||||
it-job denmark, jobs in copenhagen, jobs in aarhus, jobs in odense, deltidsjob,
|
||||
fuldtidsjob, fastansættelse, tidsbegrænset ansættelse, fleksjob, sygeplejerske job,
|
||||
social worker job denmark, occupation search denmark, esco occupation, job deadline,
|
||||
ansøgningsfrist, søg efter job, full time job denmark, part time job denmark.
|
||||
context: fork
|
||||
allowed-tools: Bash(bun run skills/jobnet-search/cli/src/cli.ts *)
|
||||
---
|
||||
|
||||
# Jobnet-Search Skill
|
||||
|
||||
Access live Danish job listings from the Jobnet.dk public API. No authentication needed.
|
||||
Jobnet is operated by STAR (Styrelsen for Arbejdsmarked og Rekruttering) and is Denmark's
|
||||
official government job portal — covering public sector positions as well as many private
|
||||
sector listings. Approximately 21,000+ active jobs at any time.
|
||||
|
||||
## When to use this skill
|
||||
|
||||
Invoke this skill when the user wants to:
|
||||
|
||||
- Search for job openings in Denmark by keyword, title, or employer
|
||||
- Filter jobs by region, work hours (full/part time), or employment duration (permanent/temporary)
|
||||
- Find jobs near a specific postal code within a given radius
|
||||
- Get full details for a specific job ad including description, contact persons, and application URL
|
||||
- Discover occupation types and ESCO categories for more precise job filtering
|
||||
- Explore autocomplete suggestions for Danish job titles or keywords
|
||||
- Find jobs in the public sector, healthcare, IT, education, or any other Danish industry
|
||||
|
||||
## Commands
|
||||
|
||||
### Search for job ads
|
||||
|
||||
```bash
|
||||
bun run skills/jobnet-search/cli/src/cli.ts search [flags]
|
||||
```
|
||||
|
||||
Key flags:
|
||||
- `--search-string <text>` — keyword search, e.g. `sygeplejerske`, `ingeniør`, `pædagog`
|
||||
- `--region <region>` — `HovedstadenOgBornholm`, `Midtjylland`, `Syddanmark`, `OevrigeSjaelland`, `Nordjylland`
|
||||
- `--postal-code <code>` — postal code for radius-based search, e.g. `2100`
|
||||
- `--radius <km>` — radius in km from postal code (default: `50`)
|
||||
- `--work-hours <type>` — `FullTime` or `PartTime`
|
||||
- `--duration <type>` — `Permanent` or `Temporary`
|
||||
- `--job-type <type>` — `Ordinaert`, `Efterloenner`, `Foertidspension`
|
||||
- `--occupation-area <id>` — occupation area identifier, e.g. `10000`
|
||||
- `--occupation-group <id>` — occupation group identifier, e.g. `10060`
|
||||
- `--order <type>` — `PublicationDate` (default), `BestMatch`, `ApplicationDate`
|
||||
- `--page / --per-page / --limit`
|
||||
- `--format json|table|plain`
|
||||
|
||||
### Full job ad detail
|
||||
|
||||
```bash
|
||||
bun run skills/jobnet-search/cli/src/cli.ts detail <jobAdId> [--format json|plain]
|
||||
```
|
||||
|
||||
`jobAdId` is the UUID from `search` results (the `jobAdId` field). Returns the complete job
|
||||
description, contact persons, application deadline, employer details, and direct application URL.
|
||||
|
||||
### Search occupation types
|
||||
|
||||
```bash
|
||||
bun run skills/jobnet-search/cli/src/cli.ts occupations --search-string <text> [--per-page <n>]
|
||||
```
|
||||
|
||||
Use this to discover ESCO occupation identifiers before passing them to `search` with
|
||||
`--occupation-area` or `--occupation-group`.
|
||||
|
||||
### Typeahead suggestions
|
||||
|
||||
```bash
|
||||
bun run skills/jobnet-search/cli/src/cli.ts suggestions --query <text> [--limit <n>]
|
||||
```
|
||||
|
||||
Returns Danish job title autocomplete strings. Useful for exploring valid Danish
|
||||
job titles before constructing a `search` query.
|
||||
|
||||
---
|
||||
|
||||
## How to use effectively
|
||||
|
||||
**Discover occupations first.** Use `occupations` or `suggestions` to find the right Danish
|
||||
term or ESCO identifier before running a `search`:
|
||||
|
||||
```bash
|
||||
bun run skills/jobnet-search/cli/src/cli.ts suggestions --query "syge"
|
||||
bun run skills/jobnet-search/cli/src/cli.ts occupations --search-string "sygeplejerske"
|
||||
```
|
||||
|
||||
**Natural workflow: `search` → `detail`.**
|
||||
1. Use `search` to get a list of matching jobs with their `jobAdId`.
|
||||
2. Call `detail <jobAdId>` to get the full job description, contact persons, and direct application link.
|
||||
|
||||
**Use `--format table` for comparisons**, `--format json` for data processing, and
|
||||
`--format plain` for single-record detail views (strips HTML from job body).
|
||||
|
||||
**Pagination**: `--per-page` controls server-side results per page. `--limit` caps what the
|
||||
CLI outputs. Use `--page` + `--per-page` to iterate through large result sets.
|
||||
|
||||
**Geographic search modes:**
|
||||
- Use `--region` for broad regional filtering (e.g. all of Midtjylland)
|
||||
- Use `--postal-code` + `--radius` for jobs near a specific location
|
||||
- Do not combine `--region` and `--postal-code` in the same query
|
||||
|
||||
**Order matters for intent:**
|
||||
- `PublicationDate` — newest postings first (default, good for "what's new")
|
||||
- `BestMatch` — relevance score (best when `--search-string` is provided)
|
||||
- `ApplicationDate` — earliest deadline first (good for urgent applications)
|
||||
|
||||
---
|
||||
|
||||
## Usage examples
|
||||
|
||||
### Jobs in Copenhagen area
|
||||
|
||||
```bash
|
||||
bun run skills/jobnet-search/cli/src/cli.ts search \
|
||||
--region HovedstadenOgBornholm \
|
||||
--per-page 10 \
|
||||
--format table
|
||||
```
|
||||
|
||||
### Nurse jobs nationwide
|
||||
|
||||
```bash
|
||||
bun run skills/jobnet-search/cli/src/cli.ts search \
|
||||
--search-string "sygeplejerske" \
|
||||
--work-hours FullTime \
|
||||
--duration Permanent \
|
||||
--order BestMatch \
|
||||
--per-page 10 \
|
||||
--format table
|
||||
```
|
||||
|
||||
### IT jobs near Aarhus within 30km
|
||||
|
||||
```bash
|
||||
bun run skills/jobnet-search/cli/src/cli.ts search \
|
||||
--search-string "udvikler" \
|
||||
--postal-code 8000 \
|
||||
--radius 30 \
|
||||
--work-hours FullTime \
|
||||
--format table
|
||||
```
|
||||
|
||||
### Full details of a job ad
|
||||
|
||||
```bash
|
||||
bun run skills/jobnet-search/cli/src/cli.ts detail 9ef43bce-d82b-4ea1-a098-7ff6520f99be --format plain
|
||||
```
|
||||
|
||||
### Jobs sorted by application deadline (urgent first)
|
||||
|
||||
```bash
|
||||
bun run skills/jobnet-search/cli/src/cli.ts search \
|
||||
--search-string "pædagog" \
|
||||
--region OevrigeSjaelland \
|
||||
--order ApplicationDate \
|
||||
--per-page 10
|
||||
```
|
||||
|
||||
### Discover occupation terms
|
||||
|
||||
```bash
|
||||
bun run skills/jobnet-search/cli/src/cli.ts suggestions --query "ingeniør" --limit 5
|
||||
bun run skills/jobnet-search/cli/src/cli.ts occupations --search-string "lærer" --per-page 5
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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`), strips HTML from job descriptions |
|
||||
|
||||
All errors are written to **stderr** as `{ "error": "...", "code": "..." }` and the process exits with code `1`.
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- All data is from the public `jobnet.dk/bff` REST API — no credentials required.
|
||||
- The API requires the `x-csrf: 1` header; the CLI adds this automatically.
|
||||
- Pagination is 1-indexed (`--page 1` is the first page).
|
||||
- `search` results omit the HTML job description — use `detail` to get it.
|
||||
- `detail --format plain` strips HTML tags for readable text output.
|
||||
- Job ad detail pages on jobnet.dk: `https://jobnet.dk/job/{jobAdId}`
|
||||
- `suggestions` is tuned for Danish job titles — English terms may return empty results.
|
||||
@@ -0,0 +1,373 @@
|
||||
# jobnet-cli
|
||||
|
||||
CLI for the [Jobnet.dk](https://jobnet.dk) Danish government job portal API.
|
||||
|
||||
**Base URL**: `https://jobnet.dk/bff`
|
||||
**Authentication**: No credentials required. All public endpoints only need the `x-csrf: 1` request header.
|
||||
**Format**: All responses are JSON.
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
cd skills/jobnet-search/cli
|
||||
bun install
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `search` | Search for job ads with filters |
|
||||
| `detail` | Full detail for a single job ad |
|
||||
| `occupations` | Search occupation types (for building filters) |
|
||||
| `suggestions` | Typeahead suggestions for job title / keyword search |
|
||||
|
||||
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`.
|
||||
|
||||
---
|
||||
|
||||
## Regions
|
||||
|
||||
| Value | Danish region |
|
||||
|-------|--------------|
|
||||
| `HovedstadenOgBornholm` | Hovedstaden og Bornholm |
|
||||
| `Midtjylland` | Midtjylland |
|
||||
| `Syddanmark` | Syddanmark |
|
||||
| `OevrigeSjaelland` | Øvrige Sjælland |
|
||||
| `Nordjylland` | Nordjylland |
|
||||
|
||||
---
|
||||
|
||||
## Order types
|
||||
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `PublicationDate` | Newest postings first (default) |
|
||||
| `BestMatch` | Relevance score (requires `--search-string`) |
|
||||
| `ApplicationDate` | Earliest deadline first |
|
||||
|
||||
---
|
||||
|
||||
## `search` — Search for job ads
|
||||
|
||||
**Endpoint**: `GET /FindJob/Search`
|
||||
|
||||
```bash
|
||||
bun run src/cli.ts search [flags]
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `--search-string` | string | — | Free-text keyword search (job title, skills, employer) |
|
||||
| `--region` | string | — | One region value (see Regions table) |
|
||||
| `--postal-code` | string | — | Postal code for radius search, e.g. `2100` |
|
||||
| `--radius` | number | `50` | Radius in km from postal code (requires `--postal-code`) |
|
||||
| `--work-hours` | string | — | `FullTime` or `PartTime` |
|
||||
| `--duration` | string | — | `Permanent` or `Temporary` |
|
||||
| `--job-type` | string | — | Announcement type: `Ordinaert`, `Efterloenner`, `Foertidspension` |
|
||||
| `--occupation-area` | string | — | Occupation area identifier, e.g. `10000` |
|
||||
| `--occupation-group` | string | — | Occupation group identifier, e.g. `10060` |
|
||||
| `--page` | number | `1` | Page number (1-indexed) |
|
||||
| `--per-page` | number | `10` | Results per page |
|
||||
| `--limit` | number | — | Cap total results returned by CLI |
|
||||
| `--order` | string | `PublicationDate` | Sort order (see Order types table) |
|
||||
| `--format` | string | `json` | Output format: `json`, `table`, `plain` |
|
||||
|
||||
### Example
|
||||
|
||||
```bash
|
||||
bun run src/cli.ts search \
|
||||
--search-string "sygeplejerske" \
|
||||
--region HovedstadenOgBornholm \
|
||||
--work-hours FullTime \
|
||||
--duration Permanent \
|
||||
--per-page 5 \
|
||||
--format table
|
||||
|
||||
bun run src/cli.ts search \
|
||||
--postal-code 8000 \
|
||||
--radius 25 \
|
||||
--per-page 10
|
||||
```
|
||||
|
||||
### Response shape
|
||||
|
||||
```json
|
||||
{
|
||||
"meta": {
|
||||
"totalJobAdCount": 21452,
|
||||
"pageNumber": 1,
|
||||
"resultsPerPage": 10,
|
||||
"searchString": "developer"
|
||||
},
|
||||
"facets": {
|
||||
"regions": [
|
||||
{ "type": "HovedstadenOgBornholm", "jobAdCount": 6642 }
|
||||
],
|
||||
"workHours": [
|
||||
{ "type": "FullTime", "jobAdCount": 17919 },
|
||||
{ "type": "PartTime", "jobAdCount": 3533 }
|
||||
],
|
||||
"employmentDurations": [
|
||||
{ "type": "Permanent", "jobAdCount": 18533 },
|
||||
{ "type": "Temporary", "jobAdCount": 2919 }
|
||||
],
|
||||
"occupationAreas": [
|
||||
{ "identifier": "10000", "jobAdCount": 3235 }
|
||||
],
|
||||
"countries": [
|
||||
{ "label": "Danmark", "identifier": "DK", "jobAdCount": 21164 }
|
||||
]
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"jobAdId": "9ef43bce-d82b-4ea1-a098-7ff6520f99be",
|
||||
"title": "Akademisk medarbejder med interesse for arbejdsmiljø og uddannelse",
|
||||
"hiringOrgName": "Region Midtjylland",
|
||||
"occupation": "Personalekonsulent",
|
||||
"municipality": "Viborg",
|
||||
"postalCode": 8800,
|
||||
"postalDistrictName": "Viborg",
|
||||
"country": "Danmark",
|
||||
"publicationDate": "2026-03-13T00:00:00+01:00",
|
||||
"applicationDeadline": "2026-04-05T21:59:00+02:00",
|
||||
"applicationDeadlineStatus": "ExpirationDate",
|
||||
"workHourPartTime": false,
|
||||
"isExternal": false,
|
||||
"hasLogo": true,
|
||||
"logoUrl": "/bff/SharedComponents/JobAdCard/CompanyLogo/ByJobAdId/9ef43bce-d82b-4ea1-a098-7ff6520f99be",
|
||||
"cvr": "29190925",
|
||||
"workPlaceAddress": "",
|
||||
"conceptUriDa": "http://data.star.dk/esco/occupation/426e017f-ebe5-4bea-b1eb-7d2d5ab3c6db",
|
||||
"isSeen": false,
|
||||
"isFavorite": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
> **Note**: The `description` field (raw HTML) is intentionally omitted from `search` results for brevity. Use `detail` to retrieve the full job description.
|
||||
|
||||
> **Note**: `resultsPerPage` and `pageNumber` must always be provided — omitting them while also providing `searchString` causes the API to return error 1014 ("Fejl i formatering af inputs").
|
||||
|
||||
---
|
||||
|
||||
## `detail` — Full job ad detail
|
||||
|
||||
**Endpoint**: `GET /FindJob/JobAdDetails/{id}`
|
||||
|
||||
```bash
|
||||
bun run src/cli.ts detail <id> [--format json|plain]
|
||||
```
|
||||
|
||||
The `id` is the `jobAdId` UUID from `search` results.
|
||||
|
||||
By default the CLI passes `incrementViews=false` to avoid polluting view counts.
|
||||
|
||||
### Example
|
||||
|
||||
```bash
|
||||
bun run src/cli.ts detail 9ef43bce-d82b-4ea1-a098-7ff6520f99be
|
||||
bun run src/cli.ts detail 9ef43bce-d82b-4ea1-a098-7ff6520f99be --format plain
|
||||
```
|
||||
|
||||
### Response shape
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "9ef43bce-d82b-4ea1-a098-7ff6520f99be",
|
||||
"title": "Akademisk medarbejder med interesse for arbejdsmiljø og uddannelse",
|
||||
"body": "<p>Full HTML job description...</p>",
|
||||
"publicationDateTime": "2026-03-13T08:06:58.0578635+01:00",
|
||||
"unpublicationDateTime": "2026-04-05T21:59:00+02:00",
|
||||
"approvalStatus": "Godkendt",
|
||||
"views": 2,
|
||||
"createdDateTime": "2026-03-13T08:06:58.3936145+01:00",
|
||||
"updatedDateTime": "2026-03-13T08:06:58.3936145+01:00",
|
||||
"isAnonymousEmployer": false,
|
||||
"hasLogo": true,
|
||||
"logoUrl": "/bff/SharedComponents/JobAdCard/CompanyLogo/ByJobAdId/9ef43bce-d82b-4ea1-a098-7ff6520f99be",
|
||||
"employer": {
|
||||
"cvrNumber": "29190925",
|
||||
"pNumber": "1003367314",
|
||||
"name": "Region Midtjylland",
|
||||
"hasCompanyLogo": true
|
||||
},
|
||||
"job": {
|
||||
"type": "Ordinaert",
|
||||
"address": {
|
||||
"streetName": "Heibergs Alle 5A",
|
||||
"city": "Viborg",
|
||||
"postalCode": "8800",
|
||||
"municipality": "Viborg",
|
||||
"countryCode": "DK",
|
||||
"countryName": "Danmark"
|
||||
},
|
||||
"noFixedWorkplace": false,
|
||||
"isLimitedPeriod": false,
|
||||
"isDisabilityFriendly": false,
|
||||
"isPartTime": false,
|
||||
"employmentDate": "2026-06-01T00:00:00+02:00",
|
||||
"conceptUriDa": "http://data.star.dk/esco/occupation/426e017f-ebe5-4bea-b1eb-7d2d5ab3c6db",
|
||||
"preferredLabelDa": "Personalekonsulent",
|
||||
"driversLicenses": [],
|
||||
"classifications": [],
|
||||
"shifts": [],
|
||||
"isFavorite": false
|
||||
},
|
||||
"application": {
|
||||
"deadlineDate": "2026-04-05T21:59:00+02:00",
|
||||
"availablePositions": 1,
|
||||
"contactPersons": [
|
||||
{
|
||||
"firstNames": "Jane",
|
||||
"lastName": "Doe",
|
||||
"phoneNumber": "+4512345678"
|
||||
}
|
||||
],
|
||||
"url": "https://midtjob.dk/ad/...",
|
||||
"urlText": "",
|
||||
"isApplicationDeadlineASAP": false
|
||||
},
|
||||
"organisationTypeId": 24,
|
||||
"user": "Emply 31430747"
|
||||
}
|
||||
```
|
||||
|
||||
> **Note**: `body` contains raw HTML. In `--format plain` output the CLI strips HTML tags to produce readable text.
|
||||
|
||||
> **Note**: `application.url` may be empty for some job ads — the employer may only accept applications through Jobnet's internal system.
|
||||
|
||||
---
|
||||
|
||||
## `occupations` — Search occupation types
|
||||
|
||||
**Endpoint**: `GET /OccupationSearch`
|
||||
|
||||
```bash
|
||||
bun run src/cli.ts occupations --search-string <text> [flags]
|
||||
```
|
||||
|
||||
Use this command to find occupation identifiers (ESCO concept URIs) to pass as filters to `search`.
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `--search-string` | string | **required** | Search term for occupation, e.g. `sygeplejerske` |
|
||||
| `--per-page` | number | `10` | Max results to return |
|
||||
| `--format` | string | `json` | Output format: `json`, `table`, `plain` |
|
||||
|
||||
### Example
|
||||
|
||||
```bash
|
||||
bun run src/cli.ts occupations --search-string "sygeplejerske" --per-page 5
|
||||
```
|
||||
|
||||
### Response shape
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"conceptUriDa": "http://data.star.dk/esco/occupation/56f5d45c-1234-4321-abcd-000000000000",
|
||||
"preferredLabelDa": "Sygeplejerske",
|
||||
"aliases": [
|
||||
{
|
||||
"aliasIdentifier": "some-uuid",
|
||||
"conceptUriDa": "http://data.star.dk/esco/occupation/56f5d45c-1234-4321-abcd-000000000000",
|
||||
"alternativeLabelDa": "Operationssygeplejerske"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
> **Note**: `conceptUriDa` is the full ESCO concept URI. The UUID portion (last path segment) can be used to build occupation filters for `search`.
|
||||
|
||||
---
|
||||
|
||||
## `suggestions` — Typeahead suggestions
|
||||
|
||||
**Endpoint**: `GET /FindJob/GetTypeaheadSuggestions`
|
||||
|
||||
```bash
|
||||
bun run src/cli.ts suggestions --query <text> [flags]
|
||||
```
|
||||
|
||||
Returns autocomplete strings for the search box — useful for exploring valid Danish job titles before running a `search`.
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `--query` | string | **required** | Partial search string to complete |
|
||||
| `--limit` | number | — | Cap number of suggestions returned |
|
||||
| `--format` | string | `json` | Output format: `json`, `table`, `plain` |
|
||||
|
||||
### Example
|
||||
|
||||
```bash
|
||||
bun run src/cli.ts suggestions --query "syge"
|
||||
bun run src/cli.ts suggestions --query "ingeniør" --limit 5
|
||||
```
|
||||
|
||||
### Response shape
|
||||
|
||||
```json
|
||||
[
|
||||
"sygepleje",
|
||||
"Sygeplejerske",
|
||||
"Sygeplejerske \"Lægeassistent\""
|
||||
]
|
||||
```
|
||||
|
||||
> **Note**: Suggestions are tuned for Danish job titles. English terms like "developer" may return an empty array.
|
||||
|
||||
---
|
||||
|
||||
## Error handling
|
||||
|
||||
All errors are written to **stderr** in JSON format and exit with code `1`:
|
||||
|
||||
```json
|
||||
{ "error": "Job ad not found", "code": "NOT_FOUND" }
|
||||
{ "error": "API request failed: 500 Internal Server Error", "code": "API_ERROR" }
|
||||
{ "error": "--query is required", "code": "MISSING_REQUIRED" }
|
||||
{ "error": "--search-string is required", "code": "MISSING_REQUIRED" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## URL construction
|
||||
|
||||
Job ad detail pages on jobnet.dk:
|
||||
|
||||
```
|
||||
https://jobnet.dk/job/{jobAdId}
|
||||
```
|
||||
|
||||
Company logo images (prefix relative logoUrl from API):
|
||||
|
||||
```
|
||||
https://jobnet.dk{logoUrl}
|
||||
```
|
||||
|
||||
Example: `https://jobnet.dk/bff/SharedComponents/JobAdCard/CompanyLogo/ByJobAdId/9ef43bce-d82b-4ea1-a098-7ff6520f99be`
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- All data is from the public `jobnet.dk/bff` REST API — no credentials required.
|
||||
- The `x-csrf: 1` header must be sent with every request.
|
||||
- Pagination is 1-indexed (`--page 1` is the first page).
|
||||
- `search` results intentionally omit the HTML `description` field — use `detail` to fetch it.
|
||||
- `body` in `detail` responses is raw HTML; use `--format plain` to get stripped text.
|
||||
- The `occupations` command helps discover ESCO occupation URIs usable as `--occupation-area` / `--occupation-group` seeds for narrowing search results.
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "jobnet-cli",
|
||||
"version": "1.0.0",
|
||||
"description": "CLI for the Jobnet.dk Danish government job portal API",
|
||||
"type": "module",
|
||||
"main": "src/cli.ts",
|
||||
"bin": {
|
||||
"jobnet": "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",
|
||||
"zod": "^3.23.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.4.0",
|
||||
"@types/bun": "latest"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { createCLI } from "@bunli/core"
|
||||
import { search } from "./commands/search.js"
|
||||
import { detail } from "./commands/detail.js"
|
||||
import { occupations } from "./commands/occupations.js"
|
||||
import { suggestions } from "./commands/suggestions.js"
|
||||
|
||||
const cli = await createCLI({
|
||||
name: "jobnet-cli",
|
||||
version: "0.1.0",
|
||||
description: "CLI for the Jobnet.dk Danish government job portal API",
|
||||
})
|
||||
|
||||
cli.command(search)
|
||||
cli.command(detail)
|
||||
cli.command(occupations)
|
||||
cli.command(suggestions)
|
||||
|
||||
await cli.run()
|
||||
@@ -0,0 +1,54 @@
|
||||
export const BASE_URL = "https://jobnet.dk/bff"
|
||||
|
||||
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, {
|
||||
headers: {
|
||||
"x-csrf": "1",
|
||||
},
|
||||
})
|
||||
|
||||
if (response.status === 429 || response.status >= 500) {
|
||||
if (attempt === maxRetries) {
|
||||
throw new Error(`API request failed: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
// Add jitter to spread out retries: base delay + random 0-500ms
|
||||
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(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.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