Files
MaverickHQandClaude Opus 5.5 67d1b84003 fix(portals): keep hidden markup and invisible characters out of CLI output
Every portal CLI turned HTML into text with a bare tag strip, so the
text inside comments, <style>/<script> blocks and hidden or
display:none elements came out as visible text. Unicode tag
characters, bidi controls and zero-width characters passed through
untouched, raw from the portal's JSON or decoded from numeric
entities. A listing could carry an instruction that renders as nothing
on the page but reads as text to the model.

Each CLI now runs HTML through visibleHtml, extending the helper
jobindex already used for one of its page shapes, and strips invisible
characters at the one function every output format reads. The
/add-portal contract names both, with linkedin-search as the copy
source.

Each CLI's tests/hidden-text.test.ts drives the real path (the CLI in
a fresh process, the command handler, or the normalizer the handler
calls) and fails on master.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 16:07:33 +01:00
..

jobindex-cli

CLI for searching jobs on Jobindex.dk.

Base URL: https://www.jobindex.dk/ Authentication: None required. Format: The search HTML embeds a JSON Stash payload. The CLI extracts its structured search results and emits JSON, table, or plain output.


Installation

cd .agents/skills/jobindex-search/cli
bun install

Commands

Command Description
search Search for job listings
detail Fetch full detail for a single job listing

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.


search — Search for job listings

Endpoint: GET https://www.jobindex.dk/jobsoegning

bun run src/cli.ts search --query "<text>" [flags]

The portal returns 20 results per page (fixed — no --per-page flag). The CLI reads searchResponse.results from the page's var Stash = {...} script. --query is required; a query-free search exits with MISSING_REQUIRED before fetching.

Flags

Flag Type Default Description
--query / -q string — Required. Keyword search query (e.g. python, grafisk designer)
--page number 1 Page number (1-indexed)
--jobage number 9999 Max age of posting in days: 1, 7, 14, 30, or 9999 (all)
--sort string score Sort order: score (relevance) or date (newest first)
--limit number — Cap total results returned by the CLI (client-side)
--format string json Output format: json, table, plain

Sort options

Value Description
score Relevance / best match (default)
date Newest postings first

jobage options

Value Description
1 Posted today
7 Last 7 days
14 Last 14 days
30 Last 30 days
9999 All time (default)

Example

# Search for Python jobs posted in the last 7 days, sorted by date
bun run src/cli.ts search --query python --jobage 7 --sort date

# Search for "grafisk designer" jobs — show first 5 results
bun run src/cli.ts search --query "grafisk designer" --limit 5

# Page 2 of results for data engineer
bun run src/cli.ts search --query "data engineer" --page 2 --format table

Response shape

{
  "meta": {
    "total": 237,
    "page": 1,
    "perPage": 20
  },
  "results": [
    {
      "id": "h1647303",
      "title": "Data Engineer til opbygning af Gavefabrikkens dataplatform",
      "company": "Gavefabrikken",
      "companyUrl": "https://www.gavefabrikken.dk/",
      "location": "Valby",
      "date": "2026-03-12",
      "deadline": "2026-04-01",
      "url": "https://www.jobindex.dk/jobannonce/h1647303",
      "description": null
    }
  ]
}

Field notes:

  • id — string ID prefixed with h (e.g. h1647303). Use this with the detail command.
  • company — company name; may be null for some aggregated listings.
  • companyUrl — company homepage URL; may be null if not present.
  • location — city or area; may be null if not listed.
  • date — the posting's firstdate value; may be null.
  • deadline — the first ten characters of apply_deadline, falling back to lastdate; null when apply_deadline_asap is set or no date is present.
  • description — currently always null in search results. Use detail for the full ad.
  • url — canonical https://www.jobindex.dk/jobannonce/{id} URL; a result without an ID falls back to its share_url or url.
  • total in meta — numeric searchResponse.hitcount, falling back to the number of returned results if the count is absent.

Note on area filtering: The Jobindex API does not reliably support area/region filtering via query parameters. area and geoareaid params are silently ignored. To filter by location, use --query with a city name (e.g. --query "python aarhus") or apply --limit and filter the JSON output externally.


detail — Fetch full job listing detail

URL: https://www.jobindex.dk/jobannonce/{id}/{slug}

bun run src/cli.ts detail <id> [--format json|plain]

The id is the job ID from search results (e.g. h1647303). The slug is optional — the CLI fetches the canonical URL by first constructing https://www.jobindex.dk/jobannonce/{id} and following any redirect, or by using the full URL from the url field in search results.

You may also pass the full URL directly as the id argument.

Flags

Flag Type Default Description
--format string json Output format: json, plain

Example

# Using ID from search results
bun run src/cli.ts detail h1647303

# Using full URL
bun run src/cli.ts detail "https://www.jobindex.dk/jobannonce/h1647303/data-engineer-til-opbygning-af-gavefabrikkens-dataplatform"

# Plain text output
bun run src/cli.ts detail h1647303 --format plain

Response shape

{
  "id": "h1647303",
  "title": "Data Engineer til opbygning af Gavefabrikkens dataplatform",
  "company": "Gavefabrikken",
  "companyUrl": "https://www.gavefabrikken.dk/",
  "location": "Valby, København",
  "date": "2026-03-12",
  "deadline": "2026-04-01",
  "employmentType": "Fastansættelse",
  "hours": "Fuldtid",
  "applyUrl": "https://www.gavefabrikken.dk/jobs/apply/123",
  "url": "https://www.jobindex.dk/jobannonce/h1647303/data-engineer-til-opbygning-af-gavefabrikkens-dataplatform",
  "description": "Full job description text here..."
}

Field notes:

Jobindex serves detail pages in two shapes, and field availability differs: a jobindex-native page (recognisable by its jd-* facts blocks) carries company, location, an ISO deadline, employment type and hours; an external ATS passthrough (the employer's hosted ad, e.g. hr-manager/Talentech, served through jobindex) has no reliable company anchor, so company is null there rather than the ATS brand, and location/deadline come from the ad's own widgets when present.

  • id / url — always the jobindex id and its jobannonce URL, never the page's og:url/canonical (on passthrough pages those point at the external ATS, not the posting).
  • deadline — YYYY-MM-DD or null; Danish long dates ("13. september 2026") and DD-MM-YYYY widget dates are converted.
  • employmentType / hours — from the native facts blocks; null on passthrough pages.
  • companyUrl — currently always null; no page shape carries a usable company link.
  • applyUrl — the Jobindex redirect link (/c?t=...) when present; null otherwise.
  • description — plain text of the ad body (HTML stripped), falling back to the page's meta description when the body is empty.
  • All fields except id, title, and url may be null.

Error handling

All errors are written to stderr in JSON format and exit with code 1:

{ "error": "Job not found", "code": "NOT_FOUND" }
{ "error": "API request failed: 500 Internal Server Error", "code": "API_ERROR" }
{ "error": "Failed to parse job listing HTML", "code": "PARSE_ERROR" }
{ "error": "--query is required", "code": "MISSING_REQUIRED" }

URL construction

Job detail pages on jobindex.dk:

  • https://www.jobindex.dk/jobannonce/{id}/{slug}

Search emits the canonical URL without a slug. When calling detail with an ID or full posting URL, the CLI rebuilds https://www.jobindex.dk/jobannonce/{id} and follows redirects.


Parsing notes

Search results from Stash

The former /jobsoegning.json endpoint no longer provides the search payload. The HTML search page embeds structured data in a var Stash = {...} script. The CLI locates a nested searchResponse object with a results array, for example under jobsearch.result_app.storeData.searchResponse:

{
  "hitcount": 237,
  "results": [
    {
      "tid": "h1647303",
      "headline": "Data Engineer",
      "company": { "name": "Gavefabrikken", "homeurl": "https://www.gavefabrikken.dk/" },
      "area": "Valby",
      "firstdate": "2026-03-12",
      "apply_deadline": "2026-04-01T00:00:00"
    }
  ]
}

The active search path maps these fields directly; it does not parse job-card HTML or hitcount_html. extractStash and parseSearchPage in src/helpers.ts implement this path. The legacy parseJobCards helper is not used by search.