openings 0.1.16 → 0.1.18

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openings",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "Find evidence-grounded jobs, including relevant roles you may not have searched for, without accounts or API keys.",
5
5
  "author": { "name": "Openings contributors" },
6
6
  "license": "MIT",
package/README.md CHANGED
@@ -95,6 +95,8 @@ The packaged server reports each source you crawl to the shared Openings aggrega
95
95
 
96
96
  The server also sends anonymous usage events so we can see what people search for and improve coverage. Each install gets a random ID on first run, stored in the data directory. An event records the tool that ran, the countries, the intent fields you passed (roles, seniority, skills, remote, query text), the IDs of jobs you opened, and the skill and title values the parser extracted from a resume. It never includes the resume text, the quoted evidence, your name, or contact details, and no IP address is stored with it. Set `OPENINGS_USAGE=off` to stop usage events while keeping the shared index, or set `OPENINGS_AGGREGATOR_URL` to an empty string to keep everything local. The source entrypoint reports only when the aggregator variable is set.
97
97
 
98
+ On startup the server makes one request to the npm registry to learn the latest version. If yours is older, every tool result carries an `updateAvailable` note so your AI app can tell you to run `bun add --global openings`. Nothing on your machine is changed automatically. Set `OPENINGS_UPDATE_CHECK=off` to skip the check.
99
+
98
100
  ## Privacy
99
101
 
100
102
  Job data comes straight from public ATS endpoints and is stored only on your machine. Your MCP client reads the resume file and passes its content to a tool; Openings never sees the path and never persists the content or anything derived from it. Every proposed change stays subject to your review.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openings",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
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" },
package/src/catalog.ts CHANGED
@@ -227,10 +227,15 @@ async function fetchWorkdayJobs(company: Company, fetcher: Fetch, signal?: Abort
227
227
  for (const code of observer?.workdayCountries ?? []) {
228
228
  const facet = WORKDAY_COUNTRY_FACETS[code.toUpperCase()];
229
229
  if (!facet) continue;
230
- const head = await page(0, { locationCountry: [facet] });
231
- const extra = [head.jobs];
232
- for (let offset = limit; offset < Math.min(head.total, WORKDAY_LISTING_CAP); offset += limit) extra.push((await page(offset, { locationCountry: [facet] })).jobs);
233
- for (const job of extra.flat()) { const key = workdayJobKey(job); if (!seen.has(key)) { seen.add(key); jobs.push(job); } }
230
+ // Best effort: some tenants reject the facet with HTTP 400. The unfiltered 2,000 are still a valid crawl, so a failed pass is skipped, not fatal.
231
+ try {
232
+ const head = await page(0, { locationCountry: [facet] });
233
+ const extra = [head.jobs];
234
+ for (let offset = limit; offset < Math.min(head.total, WORKDAY_LISTING_CAP); offset += limit) extra.push((await page(offset, { locationCountry: [facet] })).jobs);
235
+ for (const job of extra.flat()) { const key = workdayJobKey(job); if (!seen.has(key)) { seen.add(key); jobs.push(job); } }
236
+ } catch (error) {
237
+ if (signal?.aborted) throw error;
238
+ }
234
239
  }
235
240
  }
236
241
  return jobs.map((job) => normalizeWorkday(company, source, job));
package/src/mcp.ts CHANGED
@@ -3,6 +3,7 @@ import { createRuntime } from "./runtime.ts";
3
3
  import { createToolHandler } from "./tools.ts";
4
4
  import { usageEventFor } from "./usage.ts";
5
5
  import { VERSION } from "./version.ts";
6
+ import { startUpdateCheck, type UpdateNotice } from "./update-check.ts";
6
7
 
7
8
  interface RpcRequest {
8
9
  jsonrpc: "2.0";
@@ -16,7 +17,9 @@ interface ToolHandler {
16
17
  call(name: string, input: Record<string, unknown>): Promise<unknown>;
17
18
  }
18
19
 
19
- export function createMcpHandler(tools: ToolHandler) {
20
+ export function createMcpHandler(tools: ToolHandler, options: { update?: () => UpdateNotice | null } = {}) {
21
+ /** A pending update rides on every tool result so the AI app can prompt the person; nothing is changed on their machine. */
22
+ const withUpdate = (payload: unknown) => { const update = options.update?.(); return update && isRecord(payload) ? { ...payload, updateAvailable: update } : payload; };
20
23
  return async (value: unknown) => {
21
24
  if (!isRpcRequest(value)) return { jsonrpc: "2.0" as const, id: invalidRequestId(value), error: { code: -32600, message: "Invalid Request" } };
22
25
  const request = value;
@@ -24,7 +27,8 @@ export function createMcpHandler(tools: ToolHandler) {
24
27
  const base = { jsonrpc: "2.0" as const, id: request.id ?? null };
25
28
  try {
26
29
  if (request.method === "initialize") {
27
- return { ...base, result: { protocolVersion: "2025-06-18", capabilities: { tools: {} }, serverInfo: { name: "openings", version: VERSION } } };
30
+ const update = options.update?.();
31
+ return { ...base, result: { protocolVersion: "2025-06-18", capabilities: { tools: {} }, serverInfo: { name: "openings", version: VERSION }, ...(update ? { instructions: update.message } : {}) } };
28
32
  }
29
33
  if (request.method === "ping") return { ...base, result: {} };
30
34
  if (request.method === "tools/list") return { ...base, result: { tools: tools.list() } };
@@ -36,11 +40,11 @@ export function createMcpHandler(tools: ToolHandler) {
36
40
  if (args !== undefined && !isRecord(args)) throw new Error("tools/call arguments must be an object");
37
41
  try {
38
42
  const result = await tools.call(name, args ?? {});
39
- return { ...base, result: { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], isError: false } };
43
+ return { ...base, result: { content: [{ type: "text", text: JSON.stringify(withUpdate(result), null, 2) }], isError: false } };
40
44
  } catch (error) {
41
45
  const details = errorDetails(error);
42
46
  const payload = { error: { message: error instanceof Error ? error.message : String(error), ...(details ?? {}) } };
43
- return { ...base, result: { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }], isError: true } };
47
+ return { ...base, result: { content: [{ type: "text", text: JSON.stringify(withUpdate(payload), null, 2) }], isError: true } };
44
48
  }
45
49
  }
46
50
  if (request.method.startsWith("notifications/")) return null;
@@ -78,7 +82,8 @@ export async function serve() {
78
82
  search: async (query: import("./types.ts").SearchQuery) => (await runtime.search(query, { offline: false, staleDays: 14 })).jobs,
79
83
  get: async (id: string) => (await runtime.get(id, { offline: false, staleDays: 14 })).job,
80
84
  };
81
- const handle = createMcpHandler(createToolHandler(catalog, runtime, { onCall: (name, input, result) => runtime.usage?.record(usageEventFor(name, input, result)) }));
85
+ const updates = startUpdateCheck({ enabled: (process.env.OPENINGS_UPDATE_CHECK ?? "on").toLowerCase() !== "off" });
86
+ const handle = createMcpHandler(createToolHandler(catalog, runtime, { onCall: (name, input, result) => runtime.usage?.record(usageEventFor(name, input, result)) }), { update: updates.get });
82
87
  const decoder = new TextDecoder();
83
88
  let buffer = "";
84
89
  for await (const chunk of Bun.stdin.stream()) {
@@ -0,0 +1,39 @@
1
+ import { VERSION } from "./version.ts";
2
+
3
+ export interface UpdateNotice { installed: string; latest: string; run: string; message: string }
4
+ type Fetch = (input: string, init?: RequestInit) => Promise<Response>;
5
+
6
+ export const UPDATE_COMMAND = "bun add --global openings";
7
+
8
+ /**
9
+ * One small request to the npm registry at startup, never blocking a tool call. The result rides on every tool
10
+ * response so the AI app can prompt the person to update; the server itself never modifies the install.
11
+ */
12
+ export function startUpdateCheck(options: { current?: string; fetcher?: Fetch; timeoutMs?: number; enabled?: boolean } = {}): { get(): UpdateNotice | null; ready: Promise<void> } {
13
+ const current = options.current ?? VERSION;
14
+ let notice: UpdateNotice | null = null;
15
+ const ready = options.enabled === false ? Promise.resolve() : (async () => {
16
+ try {
17
+ const response = await (options.fetcher ?? globalThis.fetch)("https://registry.npmjs.org/openings/latest", { signal: AbortSignal.timeout(options.timeoutMs ?? 5_000), headers: { accept: "application/json" } });
18
+ if (!response.ok) return;
19
+ const body = await response.json() as { version?: unknown };
20
+ if (typeof body.version === "string" && isNewer(body.version, current)) notice = updateNotice(current, body.version);
21
+ } catch { /* offline or blocked: no notice */ }
22
+ })();
23
+ return { get: () => notice, ready };
24
+ }
25
+
26
+ export function updateNotice(installed: string, latest: string): UpdateNotice {
27
+ return { installed, latest, run: UPDATE_COMMAND, message: `Openings ${latest} is available; ${installed} is installed. Tell the person to run "${UPDATE_COMMAND}" and restart their AI app to get fixes and new coverage.` };
28
+ }
29
+
30
+ /** Numeric dot-version comparison; pre-release suffixes are ignored. */
31
+ export function isNewer(candidate: string, current: string): boolean {
32
+ const parse = (value: string) => value.split(".").map((part) => Number.parseInt(part, 10) || 0);
33
+ const [left, right] = [parse(candidate), parse(current)];
34
+ for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
35
+ const difference = (left[index] ?? 0) - (right[index] ?? 0);
36
+ if (difference !== 0) return difference > 0;
37
+ }
38
+ return false;
39
+ }
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const VERSION = "0.1.16";
1
+ export const VERSION = "0.1.18";