crawlforge-extractors 1.5.0 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "crawlforge-extractors",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
4
4
  "description": "Extraction logic shared by the CrawlForge MCP server and REST API — scrape templates, charset-correct capped body reading, structural fingerprinting, and embedded-state extraction. One implementation, so the two surfaces cannot drift apart.",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -652,7 +652,10 @@ export const ATS_TEMPLATES = [
652
652
  'Read a company\'s published Teamtailor jobs from the careers site\'s documented RSS feed ' +
653
653
  'rather than the rendered page: title, department, locations, remote status and plain-text ' +
654
654
  'description for every open role. The feed returns the first 100 jobs unless per_page says otherwise.',
655
- targetPattern: /teamtailor\.com\/jobs(\.rss)?(\?|$)/i,
655
+ // The careers-site root counts as a target: it is what a user pastes, and
656
+ // resolveUrl turns it into <host>/jobs.rss. A deeper path (/jobs/internal/,
657
+ // which robots disallows anyway) still does not match.
658
+ targetPattern: /teamtailor\.com\/?(?:jobs(\.rss)?\/?)?(\?|#|$)/i,
656
659
 
657
660
  /** `company` is the subdomain in https://<company>.teamtailor.com. */
658
661
  listUrl(params = {}) {
@@ -671,7 +674,13 @@ export const ATS_TEMPLATES = [
671
674
  resolveUrl(url) {
672
675
  const parsed = new URL(url);
673
676
  if (parsed.pathname.endsWith('.rss')) return url;
674
- parsed.pathname = `${parsed.pathname.replace(/\/$/, '')}.rss`;
677
+ // The feed is the JOBS page with ".rss" appended, so a careers-site root
678
+ // has to gain the jobs path first. Stripping the trailing slash off "/"
679
+ // leaves "", which built "<host>/.rss" — a URL Teamtailor answers with
680
+ // 403 on every tenant tested. The bare root is what a user actually
681
+ // pastes, so it has to resolve to the documented "<host>/jobs.rss".
682
+ const path = parsed.pathname.replace(/\/+$/, '');
683
+ parsed.pathname = `${path || '/jobs'}.rss`;
675
684
  return parsed.toString();
676
685
  },
677
686
 
@@ -681,9 +690,14 @@ export const ATS_TEMPLATES = [
681
690
  const $ = load(body, { xmlMode: true });
682
691
  const items = $('channel > item');
683
692
 
684
- if (!$('rss').length || !items.length) {
693
+ // Only a response that is not a feed is an error. A valid feed with no
694
+ // <item> is a company with nothing open right now — normative.teamtailor.com
695
+ // serves exactly that — and every sibling ATS connector reports an empty
696
+ // board as count: 0 rather than throwing. Conflating the two turned
697
+ // "nobody is hiring" into "the tool is broken".
698
+ if (!$('rss').length) {
685
699
  throw new Error(
686
- `No Teamtailor job feed at ${url}: ${$('rss').length ? 'the feed has no items' : 'the response is not an RSS feed'}. ` +
700
+ `No Teamtailor job feed at ${url}: the response is not an RSS feed. ` +
687
701
  'The feed is the careers site jobs page with ".rss" appended, e.g. ' +
688
702
  'https://<company>.teamtailor.com/jobs.rss.'
689
703
  );