Commit Graph
39 Commits
Author SHA1 Message Date
Mads LorentzenandClaude Opus 5 48965960d3 fix(jobbank-search)!: emit deadline as YYYY-MM-DD in search output
The feed's DD.MM.YYYY parenthetical passed through raw - documented, but
contradicting the /scrape contract, every other portal, and this CLI's
own detail command for the same job, and ambiguous to a date parser
(01.09.2026: 1 Sep or 9 Jan). The known shape converts to ISO; løbende
still maps to null; unrecognized shapes pass through for /rank's
defensive handling. Breaking for anything parsing the old format - the
README's own search example already showed ISO. Review finding F5
(2026-08-19), decision approved by Mads.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 20:31:20 +02:00
Mads LorentzenandClaude Opus 5 b067928da7 fix(jobindex-search): map ASAP deadlines to null per the /scrape contract
apply_deadline_asap was emitted as the string "ASAP" on ~half of live
results - undocumented, contradicting the CLI's own README, and breaking
every consumer that does date arithmetic on the field (rank's sweep,
outcome's deadline check, notion-sync's typed date column). ASAP means
"no stated deadline", which the schema already defines null to mean.
The flag wins over any date field that happens to be present. Review
finding F12 (2026-08-19), decision approved by Mads.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 20:29:40 +02:00
Mads LorentzenandClaude Opus 5 2edf8c41f1 test(portals): cover linkedin card date/location and jobindex parseSearchPage
The linkedin fixture was purpose-built for entity decoding and had no
<time> or location element, so removing the date extraction - a /scrape
contract field on a default-ON portal - survived the suite. jobindex's
parseSearchPage (the Stash parser behind every search) had zero tests,
so meta.total silently dropping hitcount survived too. Both mutations
now fail exactly the new tests. The ASAP deadline branch is deliberately
left to the F12 fix, which changes its behaviour to null. Review finding
F35 (2026-08-19).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 20:03:00 +02:00
Mads LorentzenandClaude Opus 5 7aba0b4a9d fix(jobdanmark-search): accept a comma after the postcode in location extraction
The location regex required whitespace after the 4-digit postcode, but
live companyAddress values frequently read "2670, Greve" - those results
emitted location: null (7/30 in the review's live sample; 1/30 after this
fix), leaving /scrape's geography filter nothing to act on. Extraction is
now a helper with a comma fallback that requires a non-digit city start,
so a 4-digit street number never wins over the real postcode, and the
captured city is trimmed. Review finding F2 (2026-08-19).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 19:46:34 +02:00
Oscar Madera df7919cdf5 fix(jobbank-search): emit date key derived from posted in search output (#342)
jobbank search output was missing the shared contract's date field; it now emits date as YYYY-MM-DD derived from posted (kept unchanged), null when the feed item has no pubDate. The inline result mapping is extracted into an exported normalizeSearchItem (behavior-preserving) so the derivation is pinned by tests.

Co-authored-by: oscarbol09 <80536682+oscarbol09@users.noreply.github.com>
2026-08-19 08:32:49 +02:00
Oscar Madera 17bc698697 fix(jobdanmark-search): emit the /scrape contract fields in search output (#340)
Adds additive normalization so jobdanmark search output carries the cross-portal contract fields: company from companyName, location as the city after the postal code in companyAddress (null-safe - a missing or null address yields null instead of crashing the search), and date/deadline converted from DD-MM-YYYY to YYYY-MM-DD with safe passthrough on unexpected formats. Native fields unchanged.

Co-authored-by: oscarbol09 <80536682+oscarbol09@users.noreply.github.com>
2026-08-19 08:32:06 +02:00
Oscar Madera 04186b9a37 fix(jobnet-search): emit the /scrape contract fields in search output (#339)
Adds additive normalization so jobnet search output carries the cross-portal contract fields (company, location, date, deadline with the 1900-01-01 NotDisclosed sentinel mapped to null, and url). Emits the public /find-job/{jobAdId} route and corrects the skill's own stale /job/ documentation, which redirects anonymous visitors into the MitID login flow.

Co-authored-by: oscarbol09 <80536682+oscarbol09@users.noreply.github.com>
2026-08-19 08:20:23 +02:00
Mads LorentzenandClaude Fable 5 da12d6e38e fix(cli): honest User-Agent token on linkedin-search
The last portal CLI still sending a full Chrome spoof after #283 and
4551346. Live-verified: search and detail endpoints serve identical
responses to Mozilla/5.0 (compatible; linkedin-search-cli/1.0).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 15:53:46 +02:00
Gurnoor Kaur b167efae3b feat(linkedin-search): add --jobage-minutes for sub-day freshness windows (#302)
jobageToTPR() only emits whole-day f_TPR windows, so a search can't be
restricted to postings from the last N minutes. LinkedIn's f_TPR filters
server-side down to one-second granularity (confirmed empirically), so
this is a pure window-construction change via a new minutesToTPR()
helper - no HTML parsing changes needed.

--jobage-minutes and --jobage both express a freshness window; passing
both is rejected with CONFLICTING_AGE_FLAGS rather than one silently
overriding the other.
2026-08-07 15:50:23 +02:00
Mads LorentzenandClaude Fable 5 cffacfdde0 feat(portals): ship the Danish demo portals disabled, /setup enables them for Danish-market users (#288)
A non-Danish user's /scrape ran all four Danish boards by default,
spending tokens on irrelevant listings. The portals stay in-tree as the
maintainer's demonstration instance, one flag away.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 08:02:11 +02:00
Oscar Madera 16d441e74c feat(cli): identify jobnet and jobdanmark API requests with an honest User-Agent (#283)
* fix(cli): send User-Agent on jobnet and jobdanmark API requests

apiFetch/apiPost hit the portals' APIs without a User-Agent header, while every other Danish-portal CLI sends one on purpose (jobbank exports USER_AGENT and its tests assert it; jobindex sets it on htmlFetch). Requests without one are rejected by the portals' bot filters.

* fix(cli): satisfy strict typecheck in user-agent regression test

* refactor(cli): reframe user-agent tests as honest self-identification

* docs(changelog): entry for #283 user-agent self-identification
2026-08-06 08:00:11 +02:00
Mads LorentzenandClaude Fable 5 45513464dc fix(cli): honest User-Agent tokens on jobbank and jobdanmark detail
jobbank sent a full Chrome browser string and jobdanmark's detail
command a bare Mozilla/5.0. Both now use the (compatible; <portal>-cli/1.0)
token per the identification posture settled in #277. Verified live:
both portals serve identical responses to the honest token.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 06:34:00 +02:00
Oscar Madera eef9c47461 fix(cli): reject negative and fractional filter flags in Danish portal CLIs (#281)
Follow-up to #191: it tightened page/limit/per-page, but five filter flags still used bare z.coerce.number() and accepted negative and fractional values that were sent raw to the portals (e.g. --jobage=-5, --radius=2.5).

jobindex --jobage, jobnet --radius, jobdanmark --category/--jobtitle-id and jobbank --company now use .int().min(1), mirroring #191. Adds 8 network-free regression tests (a negative and a fractional case per flag) using the same validation-error pattern as the existing cli-flag-validation suites.
2026-08-05 06:27:43 +02:00
Ilya Strelov e3af401087 feat(freehire-search): search returns each hit's full description (#251)
The skill queried /api/v1/jobs/search, whose `description` is the search
index's truncated preview — and the CLI dropped it entirely, so a result
carried only title/company/location/date/url. Reading a posting therefore
meant a `detail` call per hit, which is exactly what job-scraper's Step 2
prescribes: "fetch full detail with that portal's `detail` command".

freehire exposes a search endpoint for programmatic consumers,
/api/v1/agent/jobs/search: same query, ranking, facets and pagination, but
asked to (`include_description=true`) it replaces the preview with the
posting's full description read from the database, rendered as
`description_format=markdown|text|html`. Reproduce the difference:

  curl -s "https://freehire.me/api/v1/jobs/search?q=golang&limit=1" \
    | jq -r '.data[0].description | length'          # preview, capped
  curl -s "https://freehire.me/api/v1/agent/jobs/search?q=golang&limit=1\
&include_description=true&description_format=markdown" \
    | jq -r '.data[0].description | length'          # full text

So `search` now calls that endpoint, always asking for full descriptions,
and each JSON result carries `description` verbatim — no client-side HTML
stripping, since the API already rendered it. Markdown is the default
because it preserves the headings and requirement lists /rank reasons over;
`--description-format text|html` selects the others. The flag is validated
client-side: the API answers an unrecognized format with raw HTML rather
than an error, so a typo would silently change the output instead of
failing.

`table` and `plain` stay description-free — a full posting body would swamp
a scannable list — and `detail` is untouched, for looking one posting up by
slug (including a closed one, absent from search).

One behaviour change beyond the endpoint: a 404 from the search path used
to be folded into an empty result set. On the agent endpoint a 404 means
the instance predates it — a self-hosted freehire behind FREEHIRE_API_URL —
so it is now reported as an error naming the path, instead of a plausible
"no results" that hides the misconfiguration.

Tests cover the requested URL and params, verbatim (unstripped) markdown,
the null-when-absent case, the 404-is-an-error contract, and the flag
validation. All network-free.
2026-07-28 21:19:24 +02:00
Ayobami Adegoke 1c74a57c5e test(cli): pin the 429/5xx retry contract in all six portal CLIs (#246)
* test(cli): pin the 429/5xx retry contract in all six portal CLIs

The portal-skill contract requires backoff on 429/5xx, and every CLI
implements it - a retry loop with exponential delay and jitter - but
nothing verified the loops actually retry, stop retrying on plain
4xx, or give up after the documented attempt budget. A regression
here is invisible: a CLI that stops retrying still works on every
healthy request.

Each CLI gains tests/retry-backoff.test.ts, network-free, using the
request-timeout.test.ts pattern from #197 (import the fetch wrapper,
stub globalThis.fetch): a stubbed fetch counts attempts, and a
stubbed setTimeout fires immediately so the exhaustion case does not
sleep through the real 500ms -> 5s/8s backoff schedule (tests run in
milliseconds, not ~17s).

Three assertions per fetch wrapper, adapted to each CLI's documented
semantics:

- a 429 is retried and the next attempt's result is returned
- a plain 4xx is not retried (jobbank's fetchWithUA RETURNS the
  response for callers to handle - pinned as such; linkedin's
  htmlFetch returns "" on 404; freehire's apiGet returns null)
- persistent 5xx gives up after the initial attempt plus six
  retries (7 fetch calls) with the status in the error

freehire additionally pins its documented graceful-degradation
contract: a connection failure fails fast with no retry. jobdanmark
exercises both apiFetch and apiPost, which carry separate copies of
the loop that could drift apart.

Mutation-checked: changing maxRetries in jobindex makes the
exhaustion test fail, so the tests distinguish the current behavior
from a silently altered one.

Verified: bun test green in all six CLIs (jobindex 19, jobnet 20,
jobbank 20, jobdanmark 21, linkedin 21, freehire 31 - 0 fail);
tsc --noEmit clean in all six; python3 tools/lint_skills.py OK.

* test(jobindex): pin apiFetch's retry loop alongside htmlFetch's

Review parity gap: jobindex carries two separate copies of the retry
loop and only htmlFetch was exercised, so apiFetch's retry budget
could drift silently - the same situation jobdanmark's test already
handles for its apiFetch/apiPost pair.

apiFetch gets the same three assertions, adapted to its documented
semantics (JSON return on success, throw on plain 4xx): a 429 is
retried and the next attempt's parsed body returned, a 400 is not
retried, persistent 5xx gives up after the initial attempt plus six
retries (7 calls).

Mutation-checked on the new axis: changing apiFetch's maxRetries
(the file's first copy of the loop) fails its exhaustion test while
htmlFetch's tests stay green, so each wrapper is now pinned
independently.

Verified: bun test 30 pass / 0 fail (full jobindex suite);
tsc --noEmit clean.
2026-07-28 20:11:46 +02:00
Ilya Strelov 1ae66ad094 chore(freehire-search): point at freehire.me (domain migrated from freehire.dev) (#229)
freehire moved its primary domain from freehire.dev to freehire.me. Update the
freehire-search skill's default API base URL, help text, docs, and examples.

Backward-compatible: FREEHIRE_API_URL still overrides the base (self-hosting),
and normalizeSlug is host-agnostic so pasted freehire.dev/jobs/<slug> URLs still
resolve. The GitHub repo link (github.com/strelov1/freehire) is unchanged. All
27 CLI tests pass; the freehire.me API answers 200 for /jobs/search + /jobs/facets.
2026-07-23 10:17:10 +02:00
Mads LorentzenandClaude Opus 4.8 a68028bc54 fix(cli): pin @types/bun and @bunli/* to concrete versions to stop CI type-drift (#226)
CI runs `bun install` (not --frozen-lockfile) and the CLIs' package.json
pinned @types/bun, @bunli/core, and @bunli/utils to "latest", so each fresh
install could resolve a different version than the lockfile. When a "latest"
bun-types resolved that didn't satisfy the tsconfig (lib: ["ESNext"] with no
DOM, types: ["bun-types"] as the only source of Response/URL/fetch globals),
`bun run typecheck` failed across every .ts file - a transient red on PRs that
never touched TypeScript (observed on #207, which changes only SKILL.md).

Pin the three previously-floating dev/framework deps to the versions the
lockfiles already resolve, so behavior is unchanged and the drift class is
gone:
- @types/bun: latest -> 1.3.14  (all 6 CLIs)
- @bunli/core:  latest -> 0.9.1  (jobbank, jobdanmark, jobindex, jobnet)
- @bunli/utils: latest -> 0.6.0  (jobbank, jobdanmark, jobindex, jobnet)

The ^-ranged deps (node-html-parser, zod, typescript) are left as-is; they are
semver-guarded and were not the cause. No lockfiles committed (bun.lock stays
gitignored per existing policy). Verified: all 6 CLIs install and `bun run
typecheck` clean with the pins.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 17:18:46 +02:00
Oscar Madera 78281e8bea fix(jobdanmark): narrow soft-404 detection to avoid rejecting real postings (#206)
The jobdanmark detail parser flagged a soft-404 by testing whether the page title contained the substring '404' anywhere, so a legitimate posting titled e.g. 'Room 404 Cleaner' was wrongly rejected as NOT_FOUND. Narrows title matching to startsWith('404') plus specific error phrases ('page not found', Danish 'siden blev ikke fundet'), keeping the existing body-text backstop. Verified: strictly reduces false-positives, the real 404-page title ('404 | Jobdanmark') still detected, tests pass network-free.

By @oscarbol09.
2026-07-21 08:11:20 +02:00
Oscar Madera d3eea27b90 fix(portals): depth-track div extraction so nested job descriptions aren't truncated (#204)
The jobindex and linkedin detail parsers matched description containers with a non-greedy regex that stops at the first inner </div>, so any posting whose description contains nested divs was silently truncated (jobindex dropped later sections; linkedin dropped everything after the first block). Replaces the regex with a depth-tracked extractDivContent scanner that walks div open/close markers to the matching close. Verified: truncation bug reproduced against real markup fixtures, depth arithmetic correct (no off-by-one/infinite-loop), 28 tests pass network-free, no regression on non-nested divs. Malformed-HTML over-grabs rather than truncates - the safer failure, cleaned by downstream stripTags/decode.

By @oscarbol09.
2026-07-21 08:11:17 +02:00
Oscar Madera 73fb71587c fix(cli): catch unhandled promise rejections in self-contained CLIs (#203)
The freehire and linkedin CLIs called main().then(code => process.exit(code)) with no .catch(). If main() throws or returns a rejected promise, the .then() never runs, process.exit() is never called, and the runtime terminates with exit code 0 - a runtime failure becomes indistinguishable from success (and would suppress the Step 1c WebSearch fallback, which keys on non-zero exit). Adds a .catch() that writes the same JSON error shape the rest of each file already uses ({error, code: INTERNAL_ERROR} to stderr) and exits 1. Scoped to the two self-contained CLIs only; the bunli portals catch internally via defineCommand.

By @oscarbol09.
2026-07-20 21:54:10 +02:00
Thejesh Reddy 9cad956cc9 fix(portals): add a 15s request timeout to every board fetch (#197)
None of the board CLIs set a fetch timeout, and the retry loops react only to HTTP status codes, not to a connection that is accepted then never responds (black-holed TCP, hung TLS, stalled proxy) - so await fetch(...) never settles and the command hangs with no output and no exit. freehire's helper even documented a fast-degrade contract its try/catch didn't deliver on a mid-flight stall. Adds signal: AbortSignal.timeout(15000) to every fetch across all six CLIs, with network-free tests asserting the signal is present on each request wrapper.

By @thejesh23. Verified: 8 timeout tests pass locally with fetch stubbed (no network), and would fail on the pre-fix code.

Closes #196
2026-07-20 20:19:27 +02:00
Ayobami Adegoke 70e0eb43ce fix(cli): reject negative and fractional count/pagination flags in Danish portal CLIs (#191)
Bare z.coerce.number() accepted negative and fractional values for count/pagination flags, and slice(0, limit) with a negative limit silently dropped trailing results instead of erroring. Tightens the schemas to .int().min(1) across all four Danish portal CLIs (including jobnet occupations --per-page) with network-free validation tests.

By @ayobamiseun.
2026-07-19 19:38:48 +02:00
落尘 3a184bc115 fix(jobbank): parse JobPosting entries nested in JSON-LD @graph (#190)
Extracts the JSON-LD JobPosting lookup into a recursive parseJobPostingJsonLd helper that handles top-level objects, arrays, and @graph wrappers (including nested combinations), keeps skipping malformed scripts, and covers all four cases with network-free Bun tests.

By @luochen211. Closes #189.
2026-07-19 19:38:32 +02:00
Ayobami Adegoke da2c3bbec9 test(cli): cover Jobindex and Jobnet error contracts (#172)
Extend the offline CLI contract tests to the two remaining Danish
portal CLIs. Both implement the documented error contract (JSON
errors on stderr, exit 1) but had no test locking it in:

- jobindex-search: search without --query, detail without an ID,
  and bunli numeric-option validation (--page not-a-number)
- jobnet-search: detail without an ID, occupations without
  --search-string, suggestions without --query, and numeric-option
  validation

All asserted paths exit before any network request, matching the
no-live-portal-requests CI policy. Assertions were written against
observed CLI output, not assumed shapes.
2026-07-17 21:56:13 +02:00
Erik Pastor Rios 37595187d6 feat(job-scraper): per-portal enabled toggle honored by /scrape (#160)
Implements the mechanism approved in discussion #93: enabled: true|false frontmatter on portal skills (missing key = enabled), honored during /scrape portal discovery, with skipped portals reported visibly in the run summary.
2026-07-15 20:20:37 +02:00
Ayobami Adegoke 03798ec974 test(cli): cover Jobbank and Jobdanmark contracts (#148)
Jobbank and Jobdanmark each had only one narrow test, leaving required-argument errors, RSS normalization, JSON-LD variants, and malformed-page handling unprotected.

Add network-free fixture and subprocess tests for repeated RSS filters, description and ID parsing, stderr JSON errors, Bunli numeric validation, JSON-LD objects and arrays, optional fields, not-found pages, and parse failures.

The suites now cover eight Jobbank cases and six Jobdanmark cases without making live portal requests.
2026-07-13 20:45:38 +02:00
Ayobami Adegoke ff78dae48a test(jobnet): extract pure helpers and add normalization tests (#133)
* docs: clean all add-template compile artifacts

* Add Jobnet CLI normalization tests
2026-07-12 18:02:17 +02:00
Kienne d0846aad1d Add Jobdanmark detail HTML fallback (#115) 2026-07-10 08:05:40 +02:00
Kienne 1bc119dffd Report Jobbank Cloudflare blocking clearly (#114) 2026-07-10 08:05:24 +02:00
Ilya Strelov b8d35a4b69 Add freehire-search: country-agnostic freehire.dev aggregator skill (#85)
* feat(freehire-search): add country-agnostic freehire.dev aggregator skill

Adds a portal-search skill over the freehire.dev public JSON API — an
open-source IT job aggregator normalizing ~50 ATS platforms across many
markets into one schema. Like linkedin-search it is country-agnostic and
zero-dependency (plain bun + fetch), but it queries a JSON API rather than
scraping HTML, so results carry structured facets (skills/seniority/region).

Honors the portal-skill contract: search + detail commands, --format
json|table|plain, stderr JSON errors with exit 1, backoff on 429/5xx. Reads
are public (no API key) — the same zero-signup bar as linkedin-search. The
hosted-service dependency (best-effort, no SLA) is labeled prominently in
SKILL.md, and FREEHIRE_API_URL swaps the base URL for a self-hosted backend.

Scoped tech-first: triggers cover software/data/engineering roles, where the
faceted filtering is strong; non-tech coverage exists but is still maturing.

Network-free tests (mocked fetch + pure reshape/parse functions); CI matrix
updated to typecheck the new CLI.

* refactor(freehire-search): clarity pass on cli flag parsing

No behavior change. Replace a nested ternary and a comma-operator side effect
in a ternary with explicit if/else, and fix a comment that described facets
while sitting on the alias map.

* refactor(freehire-search): tighten to boundary contracts, trim comments

- Validate/normalize at boundaries, trust the declared types inside: drop the
  redundant '?? []' guards on facet arrays the wire contract already guarantees,
  and the re-filter in buildQuery (commaList already stripped empties).
- Model enrichment as always-present (an unenriched job serializes it as {}),
  removing the '?? {}' guard.
- Replace the positional table-row builder with a declarative column list; add a
  shared shortDate and a labeled-field helper for detail's plain output.
- Extract stringFlag for the string-or-bare-boolean flags (--remote/--query/...).
- Dedup the response parse in apiGet to a single tolerant read (drop safeJson).
- SKILL.md: document partial data + the 'none' unspecified-region facet.
- Trim restating comments to the reference skills' density.
2026-07-09 06:04:35 +02:00
Kushida fc9e3e1f32 fix: respect zero LinkedIn result limits (#76) 2026-07-08 20:57:41 +02:00
Mads LorentzenandClaude Fable 5 a5ffcc39ff chore: untrack tracker CSV, scope scraper Bash permission, fix portal SKILL.md paths (#71)
- Untrack job_search_tracker.csv: it was both tracked and listed in
  .gitignore (same inconsistency class as the settings.local.json fix
  in #27). Users' personal rows risked merge conflicts on every pull;
  commands already create the file with the standard header when it
  is missing.
- Scope job-scraper's allowed-tools Bash entry (from #52) to
  'bun --version' and the portal-CLI invocation pattern, adopting the
  tighter form proposed in #65.
- Fix all five portal SKILL.mds documenting 'bun run skills/...'
  paths that do not resolve from the repo root ('.agents/skills/...'
  is correct) - now load-bearing since #52 wired /scrape to read
  these docs for CLI invocations. Surfaced in #66.
- Teach tools/lint_skills.py to glob-expand allowed-tools bun run
  targets so scoped wildcard permissions lint correctly.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 17:14:39 +02:00
Yiğit ERDOĞAN 844b245583 fix(jobindex-search): decode hex HTML entities in CLI output (#56)
decodeHtmlEntities (duplicated in src/helpers.ts and
src/commands/detail.ts) only handled decimal numeric character
references (&#233;); the equally valid hexadecimal form (&#xE9;) fell
through undecoded and surfaced as raw text in titles, companies,
locations and descriptions. This bites Danish content especially
(ae/o/aa often arrive as entities). It also used String.fromCharCode,
which corrupts supplementary-plane code points (e.g. emoji, U+1F600).

Add a hexadecimal numeric-entity rule and route both decimal and hex
through a fromCodePoint-based helper with a valid-range guard, in both
copies. Add network-free unit tests via the exported parseJobCards.
2026-07-07 19:41:23 +02:00
Yiğit ERDOĞAN b27a3b5e81 fix(linkedin-search): decode hex HTML entities in CLI output (#55)
decodeHtmlEntities only handled decimal numeric character references
(&#233;); the equally valid hexadecimal form (&#xE9;) fell through
undecoded and surfaced as raw text in titles, companies, locations and
descriptions. It also used String.fromCharCode, which corrupts
supplementary-plane code points (e.g. emoji, U+1F600).

Add a hexadecimal numeric-entity rule and route both decimal and hex
through a fromCodePoint-based helper with a valid-range guard. Add
network-free unit tests covering hex, uppercase-X hex, decimal
(regression) and astral code points via the exported parse functions.
2026-07-07 19:40:32 +02:00
Mads LorentzenandClaude Fable 5 f3d4448cca fix: restore Danish CLI commands/ and tsconfig files dropped by old .gitignore (#53)
The pre-#21 .gitignore's unanchored 'commands/' rule silently excluded
.agents/skills/*/cli/src/commands/ (and the tsconfigs) from the initial
release, so every clone's four Danish portal CLIs failed on import with
'Cannot find module ./commands/search.js'. #21 fixed the rule but the
files were never restored - git history has no trace of them.

Restored from the maintainer's working copies, including the updated
jobindex helpers.ts (Jobindex moved search results from the JSON
endpoint, which now returns 204, into an embedded HTML Stash blob).

Verified: all four CLIs typecheck and return live results with their
documented flags. Surfaced while reviewing #52.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 17:39:47 +02:00
Mads LorentzenandClaude Fable 5 a0d576e0ca chore: add test script to linkedin-search cli package.json (#38)
Matches the jobindex-search convention; follow-up noted in #35 review.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 21:25:25 +02:00
AshutoshandClaude Sonnet 4.6 e595663dc1 fix: NaN filter bypass in LinkedIn CLI --jobage/--page/--limit flags (#35)
* fix: silent zero output in salary converter and NaN filter bypass in LinkedIn CLI

Bug #10 (convert_salary_excel.py):
openpyxl ws[row_index] random access fails silently under read_only=True,
leaving headers empty and producing no output. Fix: save the header row
values during the existing iter_rows scan so ws[header_row] is never called.

Bug #5 (.agents/skills/linkedin-search/cli/src/cli.ts):
parseInt on --jobage/--page/--limit flags returns NaN on non-numeric input.
NaN propagates silently — the jobage filter is dropped, page/limit are broken.
Fix: validate each parsed int, exit 1 with a structured BAD_ARG error on NaN.

Also adds:
- tests/test_bug10_salary_converter.py: 10 scenarios, 31 assertions (all green)
- tests/test_bug5_linkedin_cli.sh: 8 scenarios, 16 assertions (all green)
- docs/bugfixes.md: root cause, impact, and fix explanation for both bugs,
  plus a note on the pre-existing detect_column_type "n" pattern issue

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: address PR review — drop salary converter change, move test to bun

- Drop tools/convert_salary_excel.py change (bug not reproducible on
  modern openpyxl; reviewer confirmed master works correctly)
- Drop tests/test_bug10_salary_converter.py and top-level tests/ dir
- Drop docs/bugfixes.md (analysis belongs in PR description, not repo)
- Replace tests/test_bug5_linkedin_cli.sh with a proper bun test file
  at .agents/skills/linkedin-search/cli/tests/cli-flag-validation.test.ts
  following the jobindex-search/cli/tests/ convention (runCLI/parseJSON
  helpers, describe/test/expect, descriptive names)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-06 21:23:29 +02:00
d8f38fe766 Add country-agnostic linkedin-search skill (#20)
A general-purpose, field-agnostic job-search skill built on LinkedIn's public
jobs-guest endpoints. Works for any market out of the box — location is an
explicit required flag (no country default). Zero runtime dependencies (bun only);
search + detail commands.

Includes a personal-use / Terms-of-Service note (automated access is against
LinkedIn's ToS — keep volume low, non-commercial).

(Pairs with the .gitignore fix in #21, which lets skills under .agents/ be tracked.)

Co-authored-by: Akhil Tripathi <kodabear@Akhils-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 20:26:19 +02:00
AI Job SearchandClaude Opus 4.6 c66d599d75 Initial release: AI-powered job application framework
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 08:34:06 +01:00