openings 0.1.22 → 0.1.24

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": "openings",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
4
4
  "description": "A free, candidate-safe job-search substrate for AI agents",
5
5
  "license": "MIT",
6
6
  "repository": { "type": "git", "url": "git+https://github.com/abhay-avagama/hiring-agent.git" },
@@ -157,7 +157,7 @@ function withAttempt(lead: EnrichmentLead, attempt: LeadAttempt): EnrichmentLead
157
157
  }
158
158
 
159
159
  function uniqueSlug(lead: EnrichmentLead, used: Set<string>): string | null {
160
- const raw = lead.ats === "workday" ? (lead.token.split("/")[1] ?? lead.token) : lead.token;
160
+ const raw = lead.ats === "workday" ? (lead.token.split("/")[1] ?? lead.token) : lead.ats === "keka" ? lead.token.split("/")[0]! : lead.ats === "zohorecruit" ? lead.token.split(".")[0]! : lead.token;
161
161
  const base = raw.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || lead.ats;
162
162
  for (const candidate of [base, `${lead.ats}-${base}`]) if (!used.has(candidate)) return candidate;
163
163
  return null;
@@ -22,7 +22,8 @@ export interface RecommendJobsResult {
22
22
  profile: CandidateProfile;
23
23
  matches: JobMatch[];
24
24
  exploration: ReturnType<typeof matchJobs>["exploration"];
25
- filteredOut: FilteredJob[];
25
+ /** Counts by reason plus a small sample: the full list once ran to every job in the index and blew past MCP payload limits. */
26
+ filteredOut: { total: number; byReason: Record<string, number>; sample: FilteredJob[] };
26
27
  assumptions: string[];
27
28
  ranking: { mode: "evidence" | "keyword"; minimumPercent: number };
28
29
  snapshot: SnapshotStatus;
@@ -143,15 +144,20 @@ function matchSnapshot(profile: CandidateProfile, intent: CandidateIntent, snaps
143
144
  return matchJobs(profile, intent, jobs, jobs.length, ranking);
144
145
  }
145
146
 
146
- function limitMatching<T extends ReturnType<typeof matchSnapshot>>(matching: T, limit = 20): T {
147
- const matches = matching.matches.slice(0, Math.max(0, limit));
148
- const ids = new Set(matches.map((match) => match.job.id));
149
- return { ...matching, matches, exploration: {
150
- ...matching.exploration,
151
- directMatches: matching.exploration.directMatches.filter((match) => ids.has(match.job.id)),
152
- hiddenMatches: matching.exploration.hiddenMatches.filter((match) => ids.has(match.job.id)),
153
- stretchMatches: matching.exploration.stretchMatches.filter((match) => ids.has(match.job.id)),
154
- } };
147
+ const EXCERPT_CHARS = 280;
148
+ /** Transport shape: descriptions become excerpts (get_job returns the full text), exploration lists carry the same compact matches, filtered jobs are summarised. */
149
+ function limitMatching(matching: ReturnType<typeof matchSnapshot>, limit = 20): Omit<ReturnType<typeof matchSnapshot>, "filteredOut"> & { filteredOut: RecommendJobsResult["filteredOut"] } {
150
+ const compact = (match: JobMatch): JobMatch => ({ ...match, job: { ...match.job, description: match.job.description.length > EXCERPT_CHARS ? `${match.job.description.slice(0, EXCERPT_CHARS).trimEnd()}…` : match.job.description } });
151
+ const matches = matching.matches.slice(0, Math.max(0, limit)).map(compact);
152
+ const byId = new Map(matches.map((match) => [match.job.id, match]));
153
+ const pick = (list: JobMatch[]) => list.filter((match) => byId.has(match.job.id)).map((match) => byId.get(match.job.id)!);
154
+ const byReason: Record<string, number> = {};
155
+ for (const entry of matching.filteredOut) for (const reason of entry.reasons) { const key = reason.split(":")[0]!; byReason[key] = (byReason[key] ?? 0) + 1; }
156
+ return {
157
+ ...matching, matches,
158
+ exploration: { ...matching.exploration, directMatches: pick(matching.exploration.directMatches), hiddenMatches: pick(matching.exploration.hiddenMatches), stretchMatches: pick(matching.exploration.stretchMatches) },
159
+ filteredOut: { total: matching.filteredOut.length, byReason, sample: matching.filteredOut.slice(0, 20) },
160
+ };
155
161
  }
156
162
 
157
163
  function preCutoffMatchCount(matching: ReturnType<typeof matchSnapshot>): number {
package/src/providers.ts CHANGED
@@ -192,10 +192,11 @@ const keka: ProviderSpec = {
192
192
  resolve(url) {
193
193
  const tenant = /^([a-z0-9-]+)\.keka\.com$/i.exec(url.hostname)?.[1]?.toLowerCase();
194
194
  if (!tenant || ["www", "app", "hr", "academy", "developers", "help", "cdn", "api"].includes(tenant) && tenant !== "hr") return null;
195
- const org = /\/(?:careers\/api\/embedjobs\/default\/active|ats\/documents)\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i.exec(url.pathname)?.[1];
195
+ const org = /\/(?:careers\/api\/embedjobs(?:\/default\/active)?|ats\/documents)\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i.exec(url.pathname)?.[1];
196
196
  return org ? `${tenant}/${org.toLowerCase()}` : null;
197
197
  },
198
- canonicalUrl: (token) => `https://${token.split("/")[0]}.keka.com/careers`,
198
+ // The embed page is a real listing a person can open and it carries the org id, so the canonical URL round-trips through resolve().
199
+ canonicalUrl: (token) => `https://${token.split("/")[0]}.keka.com/careers/api/embedjobs/${token.split("/")[1] ?? ""}`,
199
200
  endpoint: (token) => `https://${token.split("/")[0]}.keka.com/careers/api/embedjobs/default/active/${token.split("/")[1] ?? ""}`,
200
201
  jobsFromBody: (body) => asRecords(body),
201
202
  providerName: () => "",
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const VERSION = "0.1.22";
1
+ export const VERSION = "0.1.24";