openings 0.1.48 → 0.1.50
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/README.md +2 -0
- package/package.json +1 -1
- package/src/tools.ts +136 -25
- package/src/version.ts +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Openings
|
|
2
2
|
|
|
3
|
+
[](https://smithery.ai/servers/avagama/openings)
|
|
4
|
+
|
|
3
5
|
A free, candidate-safe job search for AI agents.
|
|
4
6
|
|
|
5
7
|
Openings indexes public company job boards into a private index on your machine and exposes it to any MCP client. Your agent can find roles that fit a resume, explain the fit with evidence, and propose truthful resume improvements. Installed this way there are no accounts and no API keys; there are never model calls and never a way to submit an application.
|
package/package.json
CHANGED
package/src/tools.ts
CHANGED
|
@@ -9,15 +9,21 @@ import type { PrepareJobSearchResult } from "./job-search-preparation.ts";
|
|
|
9
9
|
|
|
10
10
|
export interface ToolDefinition {
|
|
11
11
|
name: "prepare_job_search" | "get_job_coverage" | "recommend_jobs" | "analyze_job_fit" | "optimize_resume" | "search_jobs" | "get_job";
|
|
12
|
+
/** Human-readable name for a consent screen or tool picker, where a snake_case identifier reads poorly. */
|
|
13
|
+
title: string;
|
|
12
14
|
description: string;
|
|
13
15
|
inputSchema: Record<string, unknown>;
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
+
/** The shape of structuredContent. Deliberately permissive: a caller may rely on the fields named here, and a
|
|
17
|
+
* result that grows a field must not fail validation in an app that pinned an older schema. */
|
|
18
|
+
outputSchema: Record<string, unknown>;
|
|
19
|
+
/** Nothing here applies, submits, or writes on the candidate's behalf. Preparation is the one tool that writes
|
|
20
|
+
* at all: it caches the job index on disk, so claiming readOnlyHint for it would be untrue. */
|
|
21
|
+
annotations: { readOnlyHint: boolean; destructiveHint: false; idempotentHint: true; openWorldHint: boolean };
|
|
16
22
|
}
|
|
17
23
|
|
|
18
24
|
/** Tools that reach employer boards over the network are open-world; the rest read the local index. */
|
|
19
25
|
const annotationsFor = (name: ToolDefinition["name"]): ToolDefinition["annotations"] =>
|
|
20
|
-
({ readOnlyHint:
|
|
26
|
+
({ readOnlyHint: name !== "prepare_job_search", destructiveHint: false, idempotentHint: true, openWorldHint: name === "prepare_job_search" || name === "recommend_jobs" || name === "get_job" });
|
|
21
27
|
|
|
22
28
|
interface JobWorkflows {
|
|
23
29
|
prepareJobSearch(input: unknown): Promise<PrepareJobSearchResult>;
|
|
@@ -31,30 +37,44 @@ export function createToolHandler(catalog: Catalog, workflows: JobWorkflows, opt
|
|
|
31
37
|
const definitions: Array<Omit<ToolDefinition, "annotations">> = [
|
|
32
38
|
{
|
|
33
39
|
name: "prepare_job_search",
|
|
34
|
-
|
|
40
|
+
title: "Prepare job search",
|
|
41
|
+
description: "Download and refresh the local job index so searches have data to read. The first call fetches the shared index of every verified source (thousands of employers); later calls crawl only missing or stale sources, at most 25 per call, returning a continuation token until nextAction reports ready. Call it when a search says setup is needed, not before every search, and not at all on the hosted server, where the index is already prepared and this returns ready at once. Uses the network and writes job data under the local Openings data directory; it never reads, writes or transmits a resume.",
|
|
35
42
|
inputSchema: {
|
|
36
43
|
type: "object",
|
|
37
44
|
properties: {
|
|
38
|
-
countries: { ...countryArray(), minItems: 1, maxItems: 20 },
|
|
39
|
-
continuation: { type: "string", description: "Opaque token
|
|
45
|
+
countries: { ...countryArray(), minItems: 1, maxItems: 20, description: "Two-letter codes whose sources should be prepared, such as IN or US" },
|
|
46
|
+
continuation: { type: "string", description: "Opaque token from the previous call's result. Pass it back to prepare the next batch of at most 25 sources; omit it to start" },
|
|
40
47
|
},
|
|
41
48
|
required: ["countries"],
|
|
42
49
|
additionalProperties: false,
|
|
43
50
|
},
|
|
51
|
+
outputSchema: output({
|
|
52
|
+
status: { type: "string", enum: ["ready", "partial"], description: "ready means searches can run now; partial means sources are still missing" },
|
|
53
|
+
nextAction: { type: "string", enum: ["ready", "call_again", "retry_later"], description: "call_again means pass continuation back for the next batch; retry_later means the network refused and waiting is the fix" },
|
|
54
|
+
continuation: { type: "string", description: "Token for the next batch; present only when nextAction is call_again" },
|
|
55
|
+
note: { type: "string" },
|
|
56
|
+
networkAttempted: { type: "boolean", description: "Whether this call actually reached out to employer boards" },
|
|
57
|
+
sources: { type: "object", additionalProperties: true, description: "How many sources are in the catalog, indexed, fresh, stale, missing and pending" },
|
|
58
|
+
coverage: coverageSchema(),
|
|
59
|
+
crawl: { type: "object", additionalProperties: true, description: "What this batch crawled: selected, succeeded, and the sources that failed" },
|
|
60
|
+
}, ["status", "nextAction", "coverage"]),
|
|
44
61
|
},
|
|
45
62
|
{
|
|
46
63
|
name: "get_job_coverage",
|
|
47
|
-
|
|
64
|
+
title: "Check job coverage",
|
|
65
|
+
description: "Report how many live roles the index holds for each country and how recently they were posted. Use it to set expectations before asking a candidate for anything, or to judge whether preparation is worth running; use search_jobs instead to see the roles themselves. Reads the index already on hand and never crawls.",
|
|
48
66
|
inputSchema: {
|
|
49
67
|
type: "object",
|
|
50
|
-
properties: { countries: { ...countryArray(), minItems: 1, maxItems: 20 } },
|
|
68
|
+
properties: { countries: { ...countryArray(), minItems: 1, maxItems: 20, description: "Two-letter codes to report coverage for, such as IN or US" } },
|
|
51
69
|
required: ["countries"],
|
|
52
70
|
additionalProperties: false,
|
|
53
71
|
},
|
|
72
|
+
outputSchema: coverageSchema(),
|
|
54
73
|
},
|
|
55
74
|
{
|
|
56
75
|
name: "recommend_jobs",
|
|
57
|
-
|
|
76
|
+
title: "Recommend jobs from a resume",
|
|
77
|
+
description: "Rank jobs against a resume, quoting the evidence for each match, and separate direct matches from hidden title-family and stretch roles. Use it when the candidate has chosen to share a resume and wants matching; prefer search_jobs for plain filtering, which needs no resume and answers faster. The resume is parsed in memory for this call and never stored. When matches are thin it may refresh the local snapshot once, per the refresh policy.",
|
|
58
78
|
inputSchema: {
|
|
59
79
|
type: "object",
|
|
60
80
|
properties: {
|
|
@@ -62,50 +82,95 @@ export function createToolHandler(catalog: Catalog, workflows: JobWorkflows, opt
|
|
|
62
82
|
intent: intentSchema(),
|
|
63
83
|
ranking: {
|
|
64
84
|
type: "object",
|
|
85
|
+
description: "How matches are scored and how weak a match may be before it is dropped",
|
|
65
86
|
properties: {
|
|
66
|
-
mode: { type: "string", enum: ["evidence", "keyword"], default: "evidence" },
|
|
87
|
+
mode: { type: "string", enum: ["evidence", "keyword"], default: "evidence", description: "evidence scores a match only on requirements the resume text supports; keyword scores on term overlap alone" },
|
|
67
88
|
minimumPercent: { type: "number", minimum: 0, maximum: 100, default: 0 },
|
|
68
89
|
},
|
|
69
90
|
additionalProperties: false,
|
|
70
91
|
},
|
|
71
92
|
refresh: {
|
|
72
93
|
type: "object",
|
|
94
|
+
description: "Whether this call may crawl employer boards before ranking. Crawling costs seconds; the default only does it when the result would otherwise be thin",
|
|
73
95
|
properties: {
|
|
74
|
-
policy: { type: "string", enum: ["auto", "never", "always"], default: "auto" },
|
|
75
|
-
minimumMatches: { type: "integer", minimum: 0, default: 5 },
|
|
76
|
-
staleDays: { type: "number", minimum: 0, default: 14 },
|
|
96
|
+
policy: { type: "string", enum: ["auto", "never", "always"], default: "auto", description: "auto crawls once only when matches are thin and the snapshot is stale; never keeps the snapshot as it is; always crawls first" },
|
|
97
|
+
minimumMatches: { type: "integer", minimum: 0, default: 5, description: "With auto, the match count below which a refresh is worth the wait" },
|
|
98
|
+
staleDays: { type: "number", minimum: 0, default: 14, description: "With auto, how old the snapshot must be before a refresh is considered" },
|
|
77
99
|
},
|
|
78
100
|
additionalProperties: false,
|
|
79
101
|
},
|
|
80
|
-
limit: { type: "integer", minimum: 1, maximum: 100, default: 20 },
|
|
102
|
+
limit: { type: "integer", minimum: 1, maximum: 100, default: 20, description: "How many ranked matches to return" },
|
|
81
103
|
},
|
|
82
104
|
required: ["resume", "intent"], additionalProperties: false,
|
|
83
105
|
},
|
|
106
|
+
outputSchema: output({
|
|
107
|
+
outcome: { type: "string", enum: ["matches", "widened", "no_matches"], description: "widened means the date window had to open up to find anything; no_matches means say so rather than searching again silently" },
|
|
108
|
+
matches: { type: "array", description: "Ranked matches, each carrying the job, its scores, and the resume text that supports them", items: { type: "object", additionalProperties: true } },
|
|
109
|
+
explanation: { type: "string", description: "Why the result looks the way it does, in words meant for the candidate" },
|
|
110
|
+
nextMoves: { type: "array", items: { type: "string" }, description: "What to try next when the result is thin; relay these instead of inventing advice" },
|
|
111
|
+
window: { type: "object", additionalProperties: true, description: "Which age windows were walked and which one the results came from" },
|
|
112
|
+
profile: { type: "object", additionalProperties: true, description: "What was read from the resume. It is returned so claims can be checked, and is not stored" },
|
|
113
|
+
exploration: { type: "object", additionalProperties: true, description: "Direct, hidden title-family and stretch groupings" },
|
|
114
|
+
filteredOut: { type: "object", additionalProperties: true, description: "Counts by reason, with a small sample, for roles the intent excluded" },
|
|
115
|
+
assumptions: { type: "array", items: { type: "string" }, description: "Anything inferred rather than stated; worth repeating to the candidate" },
|
|
116
|
+
ranking: { type: "object", additionalProperties: true },
|
|
117
|
+
coverage: coverageSchema(),
|
|
118
|
+
snapshot: { type: "object", additionalProperties: true, description: "Age and size of the index these matches came from" },
|
|
119
|
+
refresh: { type: "object", additionalProperties: true, description: "Whether this call crawled, and what it found" },
|
|
120
|
+
shortfall: { type: "object", additionalProperties: true, description: "Present when fewer matches came back than asked for" },
|
|
121
|
+
nextActions: { type: "array", items: { type: "string" } },
|
|
122
|
+
}, ["outcome", "matches", "explanation"]),
|
|
84
123
|
},
|
|
85
124
|
{
|
|
86
125
|
name: "analyze_job_fit",
|
|
87
|
-
|
|
126
|
+
title: "Analyze fit for one job",
|
|
127
|
+
description: "Explain how one job fits a resume: which requirements the resume supports, which it does not, the screening risks, and what to prepare for an interview, each tied to text quoted from the resume rather than inferred. Use it after search_jobs or recommend_jobs has produced a job id and the candidate wants depth on a single role instead of a list. The resume is parsed in memory for this call and never stored.",
|
|
88
128
|
inputSchema: {
|
|
89
129
|
type: "object",
|
|
90
|
-
properties: {
|
|
130
|
+
properties: {
|
|
131
|
+
jobId: { type: "string", minLength: 1, description: "Stable job id from search_jobs or recommend_jobs" },
|
|
132
|
+
resume: resumeSchema(), intent: intentSchema(),
|
|
133
|
+
},
|
|
91
134
|
required: ["jobId", "resume"], additionalProperties: false,
|
|
92
135
|
},
|
|
136
|
+
outputSchema: output({
|
|
137
|
+
job: jobSchema("The job that was analyzed"),
|
|
138
|
+
assessment: { type: "object", additionalProperties: true, description: "The overall read, with its reasoning" },
|
|
139
|
+
scores: { type: "object", additionalProperties: true },
|
|
140
|
+
supported: { type: "array", items: { type: "object", additionalProperties: true }, description: "Requirements the resume supports, each with the quoted evidence" },
|
|
141
|
+
partiallySupported: { type: "array", items: { type: "object", additionalProperties: true }, description: "Requirements with partial evidence, and what is missing" },
|
|
142
|
+
unsupported: { type: "array", items: { type: "string" }, description: "Requirements the resume does not evidence at all" },
|
|
143
|
+
screeningRisks: { type: "array", items: { type: "string" }, description: "What is likely to stop this application early" },
|
|
144
|
+
interviewPreparationGaps: { type: "array", items: { type: "string" } },
|
|
145
|
+
profile: { type: "object", additionalProperties: true, description: "What was read from the resume; not stored" },
|
|
146
|
+
}, ["job", "assessment"]),
|
|
93
147
|
},
|
|
94
148
|
{
|
|
95
149
|
name: "optimize_resume",
|
|
96
|
-
|
|
150
|
+
title: "Optimize a resume for one job",
|
|
151
|
+
description: "Propose a revision of a resume for one specific job, as suggestions, a unified diff or revised markdown. It rewrites emphasis and wording only: the original is never overwritten and no claim the resume does not already support is added. Use it after analyze_job_fit has shown which gaps are real. The resume is parsed in memory for this call and never stored.",
|
|
97
152
|
inputSchema: {
|
|
98
153
|
type: "object",
|
|
99
154
|
properties: {
|
|
100
|
-
jobId: { type: "string", minLength: 1 },
|
|
155
|
+
jobId: { type: "string", minLength: 1, description: "Stable job id from search_jobs or recommend_jobs" },
|
|
101
156
|
resume: resumeSchema(),
|
|
102
|
-
output: { type: "string", enum: ["suggestions", "unified_diff", "revised_markdown"] },
|
|
157
|
+
output: { type: "string", enum: ["suggestions", "unified_diff", "revised_markdown"], description: "suggestions lists changes to consider; unified_diff shows them as a patch; revised_markdown returns the rewritten resume" },
|
|
103
158
|
},
|
|
104
159
|
required: ["jobId", "resume", "output"], additionalProperties: false,
|
|
105
160
|
},
|
|
161
|
+
outputSchema: output({
|
|
162
|
+
output: { type: "string", enum: ["suggestions", "unified_diff", "revised_markdown"], description: "Which form was produced" },
|
|
163
|
+
suggestions: { type: "array", items: { type: "object", additionalProperties: true }, description: "Each proposed change with the evidence behind it" },
|
|
164
|
+
content: { type: "string", description: "The diff or rewritten resume, when that form was asked for" },
|
|
165
|
+
gaps: { type: "array", items: { type: "string" }, description: "What the resume cannot honestly claim, and so was left alone" },
|
|
166
|
+
job: jobSchema("The job the revision targets"),
|
|
167
|
+
profile: { type: "object", additionalProperties: true },
|
|
168
|
+
originalOverwritten: { type: "boolean", description: "Always false: the candidate's resume is never modified in place" },
|
|
169
|
+
}, ["output", "suggestions", "originalOverwritten"]),
|
|
106
170
|
},
|
|
107
171
|
{
|
|
108
172
|
name: "search_jobs",
|
|
173
|
+
title: "Search jobs",
|
|
109
174
|
description: "Search jobs immediately from role, country, location and optional stated experience. No resume required. Newest first; automatically widens 7, 14, 30 days then all dates until 5 matches. For another page reuse the same filters and returned window.daysUsed as maxAgeDays, with pagination.nextOffset. Resume-based ranking is optional via recommend_jobs.",
|
|
110
175
|
inputSchema: {
|
|
111
176
|
type: "object",
|
|
@@ -115,16 +180,23 @@ export function createToolHandler(catalog: Catalog, workflows: JobWorkflows, opt
|
|
|
115
180
|
country: { type: "string", pattern: "^[A-Za-z]{2}$", description: "Two-letter country code for job eligibility, such as IN or DE" },
|
|
116
181
|
remote: { type: "boolean", description: "True for remote-only; false for non-remote-only" },
|
|
117
182
|
maxAgeDays: { type: "integer", minimum: 0, maximum: 365, description: "Omit to widen 7/14/30/all until 5 matches. Explicit positive values exclude undated roles; 0 includes all dates." },
|
|
118
|
-
limit: { type: "integer", minimum: 1, maximum: 100, default: 50 },
|
|
119
|
-
offset: { type: "integer", minimum: 0, maximum: 1000000, default: 0 },
|
|
183
|
+
limit: { type: "integer", minimum: 1, maximum: 100, default: 50, description: "How many jobs to return in this page" },
|
|
184
|
+
offset: { type: "integer", minimum: 0, maximum: 1000000, default: 0, description: "Where the page starts. Use pagination.nextOffset from the previous result rather than counting by hand" },
|
|
120
185
|
experienceYears: { type: "number", minimum: 0, maximum: 60, description: "Years of experience to compare with the posting's stated min/max range; not a qualification or fit verdict." },
|
|
121
186
|
includeUnknownExperience: { type: "boolean", default: false, description: "With experienceYears, also keep roles without a stated range, clearly unknown rather than matched." },
|
|
122
187
|
},
|
|
123
188
|
additionalProperties: false,
|
|
124
189
|
},
|
|
190
|
+
outputSchema: output({
|
|
191
|
+
jobs: { type: "array", items: jobSchema("One matching job"), description: "Matches, newest first" },
|
|
192
|
+
window: { type: "object", additionalProperties: true, description: "The age window the results came from, and whether it had to widen. Say which window was used" },
|
|
193
|
+
pagination: { type: "object", additionalProperties: true, description: "offset, limit, total and nextOffset. nextOffset is null on the last page" },
|
|
194
|
+
guidance: { type: "string", description: "How to present these results honestly; meant for the assistant, not the candidate" },
|
|
195
|
+
}, ["jobs", "window", "pagination"]),
|
|
125
196
|
},
|
|
126
197
|
{
|
|
127
198
|
name: "get_job",
|
|
199
|
+
title: "Get job details",
|
|
128
200
|
description: "Get the full description and application URL for a job returned by recommend_jobs or search_jobs.",
|
|
129
201
|
inputSchema: {
|
|
130
202
|
type: "object",
|
|
@@ -132,6 +204,7 @@ export function createToolHandler(catalog: Catalog, workflows: JobWorkflows, opt
|
|
|
132
204
|
required: ["id"],
|
|
133
205
|
additionalProperties: false,
|
|
134
206
|
},
|
|
207
|
+
outputSchema: output({ job: jobSchema("The full job, including its description and the employer's application URL") }, ["job"]),
|
|
135
208
|
},
|
|
136
209
|
];
|
|
137
210
|
|
|
@@ -191,9 +264,10 @@ export function createToolHandler(catalog: Catalog, workflows: JobWorkflows, opt
|
|
|
191
264
|
function resumeSchema(): Record<string, unknown> {
|
|
192
265
|
return {
|
|
193
266
|
type: "object",
|
|
267
|
+
description: "The resume itself, passed inline. It is parsed in memory for this call and never written to disk, logged, or sent anywhere else.",
|
|
194
268
|
properties: {
|
|
195
269
|
content: { type: "string", minLength: 1, description: "Resume content supplied directly; filesystem paths are not accepted" },
|
|
196
|
-
format: { type: "string", enum: ["text", "markdown", "pdf_base64", "docx_base64"] },
|
|
270
|
+
format: { type: "string", enum: ["text", "markdown", "pdf_base64", "docx_base64"], description: "How content is encoded: plain text, markdown, or base64 of a PDF or DOCX file" },
|
|
197
271
|
},
|
|
198
272
|
required: ["content", "format"], additionalProperties: false,
|
|
199
273
|
};
|
|
@@ -202,16 +276,53 @@ function resumeSchema(): Record<string, unknown> {
|
|
|
202
276
|
function intentSchema(): Record<string, unknown> {
|
|
203
277
|
return {
|
|
204
278
|
type: "object",
|
|
279
|
+
description: "What the candidate is actually looking for, stated explicitly rather than guessed from the resume. Every field is optional; the ones given narrow the result, and the excluded* fields remove roles the candidate does not want to see.",
|
|
205
280
|
properties: {
|
|
206
|
-
roles: stringArray(),
|
|
207
|
-
|
|
208
|
-
|
|
281
|
+
roles: { ...stringArray(), description: "Role titles the candidate is looking for, in their own words, such as \"backend engineer\" or \"data analyst\". Matched against the job title" },
|
|
282
|
+
countries: { ...countryArray(), description: "Two-letter codes the candidate may work in, such as IN or US. This is eligibility to work, not where the office is" },
|
|
283
|
+
locations: { ...stringArray(), description: "Cities or regions to keep, matched as case-insensitive substrings of the job's location, such as \"Bengaluru\" or \"Delhi NCR\"" },
|
|
284
|
+
remote: { type: "boolean", description: "True to keep only remote roles, false to drop them; omit to keep both" },
|
|
285
|
+
seniority: { ...stringArray(), description: "Levels to keep, in the posting's own vocabulary, such as \"senior\" or \"lead\". Not a years-of-experience filter: use experienceYears in search_jobs for that" },
|
|
286
|
+
requiredSkills: { ...stringArray(), description: "Skills a role must state to be kept, such as \"kubernetes\". Each is matched as a whole word" },
|
|
287
|
+
excludedTerms: { ...stringArray(), description: "Words that disqualify a role, matched as whole words in the title, such as \"intern\" or \"sales\"" },
|
|
288
|
+
excludedCountries: { ...countryArray(), description: "Two-letter codes to drop even when a role is otherwise eligible" },
|
|
289
|
+
excludedLocations: { ...stringArray(), description: "Cities or regions to drop, matched like locations" },
|
|
290
|
+
excludedRoles: { ...stringArray(), description: "Role titles to drop, matched like roles" },
|
|
209
291
|
maxAgeDays: { type: "integer", minimum: 0, maximum: 365, description: "Only roles posted within this many days. Default 30 (undated roles kept); an explicit value also drops undated roles; 0 includes older roles" },
|
|
210
292
|
},
|
|
211
293
|
additionalProperties: false,
|
|
212
294
|
};
|
|
213
295
|
}
|
|
214
296
|
|
|
297
|
+
/** An object schema that documents the fields a caller can rely on without forbidding the ones it does not know. */
|
|
298
|
+
function output(properties: Record<string, unknown>, required: string[] = []): Record<string, unknown> {
|
|
299
|
+
return { type: "object", properties, ...(required.length ? { required } : {}), additionalProperties: true };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function jobSchema(description: string): Record<string, unknown> {
|
|
303
|
+
return {
|
|
304
|
+
type: "object", description, additionalProperties: true,
|
|
305
|
+
properties: {
|
|
306
|
+
id: { type: "string", description: "Stable id to pass to get_job, analyze_job_fit or optimize_resume" },
|
|
307
|
+
title: { type: "string" }, company: { type: "string" }, location: { type: "string" },
|
|
308
|
+
remote: { type: "boolean" }, workMode: { type: "string", enum: ["remote", "hybrid", "onsite", "unknown"] },
|
|
309
|
+
url: { type: "string", description: "The employer's own posting, which is where an application is made" },
|
|
310
|
+
updatedAt: { type: "string", description: "When the board says the role was posted or last updated; absent when the board states none" },
|
|
311
|
+
age: { type: "string", enum: ["new", "older", "stale", "undated"], description: "Bucketed posting age; undated means the board gave no date, not that the role is fresh" },
|
|
312
|
+
postedDaysAgo: { type: "number" },
|
|
313
|
+
experience: { type: ["object", "null"], description: "Years the posting itself states as { min, max }; null when it states none, absent when the description was not read", additionalProperties: true },
|
|
314
|
+
eligibleCountries: { type: "array", items: { type: "string" }, description: "Two-letter codes the role is open to" },
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function coverageSchema(): Record<string, unknown> {
|
|
320
|
+
return output({
|
|
321
|
+
snapshotUpdatedAt: { type: "string", description: "When the index this answer came from was last refreshed" },
|
|
322
|
+
countries: { type: "array", description: "Per-country live and recent role counts", items: { type: "object", additionalProperties: true } },
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
215
326
|
function assertToolKeys(input: Record<string, unknown>, allowed: string[], tool: string): void {
|
|
216
327
|
const unknown = Object.keys(input).find((key) => !allowed.includes(key));
|
|
217
328
|
if (unknown) throw new Error(`${tool} does not accept field: ${unknown}`);
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.1.
|
|
1
|
+
export const VERSION = "0.1.50";
|