openings 0.1.17 → 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.
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +2 -0
- package/package.json +1 -1
- package/src/mcp.ts +10 -5
- package/src/update-check.ts +39 -0
- package/src/version.ts +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openings",
|
|
3
|
-
"version": "0.1.
|
|
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
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
|
-
|
|
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
|
|
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.
|
|
1
|
+
export const VERSION = "0.1.18";
|