jd-intel 0.8.0 → 0.8.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": "jd-intel",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "Fetch and normalize job descriptions across every major ATS (Greenhouse, Lever, Ashby, Workday, and more), for your AI assistant. No copy-paste.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -1,4 +1,5 @@
1
1
  import { normalize } from '../normalizer.js';
2
+ import { atsErrorFromStatus } from '../errors.js';
2
3
 
3
4
  const API_URL = 'https://jobs.ashbyhq.com/api/non-user-graphql';
4
5
  const BOARD_URL = 'https://api.ashbyhq.com/posting-api/job-board';
@@ -28,7 +29,7 @@ async function fetchAshbyRest(slug) {
28
29
 
29
30
  if (!resp.ok) {
30
31
  if (resp.status === 404) return [];
31
- throw new Error(`Ashby REST API error for ${slug}: ${resp.status}`);
32
+ throw atsErrorFromStatus(resp.status, `Ashby REST API error for ${slug}: ${resp.status}`);
32
33
  }
33
34
 
34
35
  const data = await resp.json();
@@ -1,4 +1,5 @@
1
1
  import { normalize, stripHtml } from '../normalizer.js';
2
+ import { atsErrorFromStatus } from '../errors.js';
2
3
 
3
4
  const BASE_URL = 'https://boards-api.greenhouse.io/v1/boards';
4
5
 
@@ -16,7 +17,7 @@ export async function fetchGreenhouse(slug) {
16
17
 
17
18
  if (!resp.ok) {
18
19
  if (resp.status === 404) return []; // Company not found or no jobs
19
- throw new Error(`Greenhouse API error for ${slug}: ${resp.status}`);
20
+ throw atsErrorFromStatus(resp.status, `Greenhouse API error for ${slug}: ${resp.status}`);
20
21
  }
21
22
 
22
23
  const data = await resp.json();
@@ -1,4 +1,5 @@
1
1
  import { normalize, stripHtml } from '../normalizer.js';
2
+ import { atsErrorFromStatus } from '../errors.js';
2
3
 
3
4
  const BASE_URL = 'https://api.lever.co/v0/postings';
4
5
 
@@ -15,7 +16,7 @@ export async function fetchLever(slug) {
15
16
 
16
17
  if (!resp.ok) {
17
18
  if (resp.status === 404) return [];
18
- throw new Error(`Lever API error for ${slug}: ${resp.status}`);
19
+ throw atsErrorFromStatus(resp.status, `Lever API error for ${slug}: ${resp.status}`);
19
20
  }
20
21
 
21
22
  const jobs = await resp.json();
@@ -1,4 +1,5 @@
1
1
  import { normalize, stripHtml } from '../normalizer.js';
2
+ import { atsErrorFromStatus } from '../errors.js';
2
3
 
3
4
  /**
4
5
  * Fetch jobs from a Recruitee career site.
@@ -18,7 +19,7 @@ export async function fetchRecruitee(slug) {
18
19
 
19
20
  if (!resp.ok) {
20
21
  if (resp.status === 404) return []; // No Recruitee site for this slug
21
- throw new Error(`Recruitee API error for ${slug}: ${resp.status}`);
22
+ throw atsErrorFromStatus(resp.status, `Recruitee API error for ${slug}: ${resp.status}`);
22
23
  }
23
24
 
24
25
  const data = await resp.json();
@@ -1,4 +1,5 @@
1
1
  import { normalize, stripHtml } from '../normalizer.js';
2
+ import { atsErrorFromStatus } from '../errors.js';
2
3
 
3
4
  const BASE_URL = 'https://api.smartrecruiters.com/v1/companies';
4
5
  const PAGE_SIZE = 100;
@@ -30,7 +31,7 @@ export async function fetchSmartrecruiters(slug) {
30
31
 
31
32
  if (!resp.ok) {
32
33
  if (resp.status === 404) return []; // Company not found
33
- throw new Error(`SmartRecruiters API error for ${slug}: ${resp.status}`);
34
+ throw atsErrorFromStatus(resp.status, `SmartRecruiters API error for ${slug}: ${resp.status}`);
34
35
  }
35
36
 
36
37
  const data = await resp.json();
@@ -1,4 +1,5 @@
1
1
  import { normalize, stripHtml } from '../normalizer.js';
2
+ import { atsErrorFromStatus } from '../errors.js';
2
3
 
3
4
  /**
4
5
  * Fetch jobs from a TeamTailor career site via its public RSS feed.
@@ -43,7 +44,7 @@ async function resolveFeed(slug, method = 'GET') {
43
44
  });
44
45
  if (resp.ok) return resp;
45
46
  if (resp.status !== 404) {
46
- throw new Error(`TeamTailor RSS error for ${slug}: ${resp.status}`);
47
+ throw atsErrorFromStatus(resp.status, `TeamTailor RSS error for ${slug}: ${resp.status}`);
47
48
  }
48
49
  // 404 on this host — try the next region.
49
50
  }
@@ -1,4 +1,5 @@
1
1
  import { normalize, stripHtml } from '../normalizer.js';
2
+ import { atsErrorFromStatus } from '../errors.js';
2
3
 
3
4
  const MAX_DETAIL_FETCHES = 100;
4
5
  const LIST_PAGE_SIZE = 20;
@@ -49,7 +50,7 @@ export async function fetchWorkday(slug, ctx = {}) {
49
50
  if (!resp.ok) {
50
51
  if (resp.status === 404) return []; // wrong site / no such board
51
52
  if (offset === 0) {
52
- throw new Error(`Workday API error for ${slug} (${tenant}/${env}/${site}): ${resp.status}`);
53
+ throw atsErrorFromStatus(resp.status, `Workday API error for ${slug} (${tenant}/${env}/${site}): ${resp.status}`);
53
54
  }
54
55
  break; // mid-paging failure: keep what we have
55
56
  }
package/src/errors.js ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Error taxonomy + typed error for the jd-intel toolkit.
3
+ *
4
+ * Codes are short and stable; the MCP tool descriptions teach the AI what each
5
+ * one means. Adapters throw AtsError with a code so callers map failures by
6
+ * `err.code` instead of parsing the message.
7
+ */
8
+
9
+ export const ERROR_CODES = {
10
+ COMPANY_NOT_FOUND: 'company_not_found', // Slug not in registry and not detected
11
+ ATS_UNREACHABLE: 'ats_unreachable', // Known ATS failed (500, timeout)
12
+ PARTIAL_FAILURE: 'partial_failure', // Discovery mode; some adapters failed
13
+ INVALID_ARGS: 'invalid_args', // Missing required, wrong type, bad pattern
14
+ NO_RESULTS: 'no_results', // Query succeeded, filters returned nothing
15
+ RATE_LIMITED: 'rate_limited', // Upstream returned 429
16
+ };
17
+
18
+ /**
19
+ * Thrown by adapters on an upstream HTTP failure. Carries a stable `code`
20
+ * (ats_unreachable / rate_limited) so the MCP layer maps it without parsing
21
+ * the message. Extends Error, so the message, stack, and `instanceof Error`
22
+ * all keep working for existing library consumers.
23
+ */
24
+ export class AtsError extends Error {
25
+ constructor(code, message) {
26
+ super(message);
27
+ this.name = 'AtsError';
28
+ this.code = code;
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Helper for adapters: build an AtsError from an HTTP status (429 => rate
34
+ * limited, anything else => unreachable) with the given message.
35
+ */
36
+ export function atsErrorFromStatus(status, message) {
37
+ return new AtsError(status === 429 ? ERROR_CODES.RATE_LIMITED : ERROR_CODES.ATS_UNREACHABLE, message);
38
+ }
package/src/index.js CHANGED
@@ -139,3 +139,8 @@ export { applyFilters } from './filters.js';
139
139
  // Re-export the list of supported ATS names (e.g. so the MCP layer can report
140
140
  // the full set detectAts probes, instead of hardcoding a stale subset).
141
141
  export { ATS_NAMES };
142
+
143
+ // Error taxonomy + typed error. Adapters throw AtsError with a stable .code
144
+ // (ats_unreachable / rate_limited) so the MCP layer maps failures without
145
+ // parsing messages. ERROR_CODES is the single source of truth for both.
146
+ export { ERROR_CODES, AtsError } from './errors.js';