openings 0.1.0

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.
Files changed (52) hide show
  1. package/.codex-plugin/plugin.json +23 -0
  2. package/.mcp.json +8 -0
  3. package/LICENSE +21 -0
  4. package/README.md +110 -0
  5. package/data/companies.json +2550 -0
  6. package/docs/job-seeker-quickstart.md +109 -0
  7. package/package.json +42 -0
  8. package/skills/openings/SKILL.md +28 -0
  9. package/src/artifact-path.ts +48 -0
  10. package/src/atomic-file.ts +13 -0
  11. package/src/candidate-profile.ts +273 -0
  12. package/src/career-tracing.ts +181 -0
  13. package/src/catalog.ts +382 -0
  14. package/src/cli.ts +601 -0
  15. package/src/common-crawl-discovery.ts +137 -0
  16. package/src/company-seeds.ts +43 -0
  17. package/src/country-coverage.ts +203 -0
  18. package/src/crawl-reporting.ts +56 -0
  19. package/src/crawler.ts +146 -0
  20. package/src/enrichment-registry.ts +137 -0
  21. package/src/file-lock.ts +85 -0
  22. package/src/index.ts +37 -0
  23. package/src/intent-validation.ts +28 -0
  24. package/src/job-coverage.ts +73 -0
  25. package/src/job-fit-analysis.ts +247 -0
  26. package/src/job-matching.ts +445 -0
  27. package/src/job-recommendations.ts +193 -0
  28. package/src/job-search-preparation.ts +116 -0
  29. package/src/jobposting-probe.ts +167 -0
  30. package/src/local-jobs.ts +113 -0
  31. package/src/locations.ts +180 -0
  32. package/src/mcp.ts +98 -0
  33. package/src/package-mcp.ts +8 -0
  34. package/src/recruitee-round.ts +114 -0
  35. package/src/report-meta.ts +17 -0
  36. package/src/requirement-vocabulary.ts +111 -0
  37. package/src/resume-optimization.ts +118 -0
  38. package/src/runtime.ts +51 -0
  39. package/src/safe-get.ts +88 -0
  40. package/src/safe-head.ts +79 -0
  41. package/src/screening-requirements.ts +99 -0
  42. package/src/selected-job-lookup.ts +14 -0
  43. package/src/snapshot-catalog.ts +21 -0
  44. package/src/snapshot-export.ts +66 -0
  45. package/src/snapshot-store.ts +31 -0
  46. package/src/source-discovery-pipeline.ts +26 -0
  47. package/src/source-discovery.ts +231 -0
  48. package/src/source-enrichment.ts +92 -0
  49. package/src/source-pipeline.ts +245 -0
  50. package/src/source-verification.ts +297 -0
  51. package/src/tools.ts +194 -0
  52. package/src/types.ts +136 -0
package/src/tools.ts ADDED
@@ -0,0 +1,194 @@
1
+ import type { Catalog } from "./catalog.ts";
2
+ import type { RecommendJobsResult } from "./job-recommendations.ts";
3
+ import type { AnalyzeJobFitResult } from "./job-fit-analysis.ts";
4
+ import type { OptimizeResumeResult } from "./resume-optimization.ts";
5
+ import type { SearchQuery } from "./types.ts";
6
+ import type { JobCoverageSummary } from "./job-coverage.ts";
7
+ import type { PrepareJobSearchResult } from "./job-search-preparation.ts";
8
+
9
+ export interface ToolDefinition {
10
+ name: "prepare_job_search" | "get_job_coverage" | "recommend_jobs" | "analyze_job_fit" | "optimize_resume" | "search_jobs" | "get_job";
11
+ description: string;
12
+ inputSchema: Record<string, unknown>;
13
+ }
14
+
15
+ interface JobWorkflows {
16
+ prepareJobSearch(input: unknown): Promise<PrepareJobSearchResult>;
17
+ getJobCoverage(input: unknown): Promise<JobCoverageSummary>;
18
+ recommend(input: unknown): Promise<RecommendJobsResult>;
19
+ analyzeJobFit(input: unknown): Promise<AnalyzeJobFitResult>;
20
+ optimizeResume(input: unknown): Promise<OptimizeResumeResult>;
21
+ }
22
+
23
+ export function createToolHandler(catalog: Catalog, workflows: JobWorkflows) {
24
+ const definitions: ToolDefinition[] = [
25
+ {
26
+ name: "prepare_job_search",
27
+ description: "Initialize or refresh the local job index from verified public sources in resumable batches of at most ten, then report current coverage and whether to call again. This may use the network and write only job data under the local Openings data directory; it never processes a resume.",
28
+ inputSchema: {
29
+ type: "object",
30
+ properties: {
31
+ countries: { ...countryArray(), minItems: 1, maxItems: 20 },
32
+ continuation: { type: "string", description: "Opaque token returned by the preceding preparation batch" },
33
+ },
34
+ required: ["countries"],
35
+ additionalProperties: false,
36
+ },
37
+ },
38
+ {
39
+ name: "get_job_coverage",
40
+ description: "Report current job-level coverage for one or more countries before a candidate supplies a resume.",
41
+ inputSchema: {
42
+ type: "object",
43
+ properties: { countries: { ...countryArray(), minItems: 1, maxItems: 20 } },
44
+ required: ["countries"],
45
+ additionalProperties: false,
46
+ },
47
+ },
48
+ {
49
+ name: "recommend_jobs",
50
+ description: "Parse a resume, apply explicit job intent, rank evidence-grounded matches, separate direct, hidden title-family, and stretch opportunities, and optionally refresh the local snapshot once.",
51
+ inputSchema: {
52
+ type: "object",
53
+ properties: {
54
+ resume: resumeSchema(),
55
+ intent: intentSchema(),
56
+ ranking: {
57
+ type: "object",
58
+ properties: {
59
+ mode: { type: "string", enum: ["evidence", "keyword"], default: "evidence" },
60
+ minimumPercent: { type: "number", minimum: 0, maximum: 100, default: 0 },
61
+ },
62
+ additionalProperties: false,
63
+ },
64
+ refresh: {
65
+ type: "object",
66
+ properties: {
67
+ policy: { type: "string", enum: ["auto", "never", "always"], default: "auto" },
68
+ minimumMatches: { type: "integer", minimum: 0, default: 5 },
69
+ staleDays: { type: "number", minimum: 0, default: 14 },
70
+ },
71
+ additionalProperties: false,
72
+ },
73
+ limit: { type: "integer", minimum: 1, maximum: 100, default: 20 },
74
+ },
75
+ required: ["resume", "intent"], additionalProperties: false,
76
+ },
77
+ },
78
+ {
79
+ name: "analyze_job_fit",
80
+ description: "Analyze one stable job id against explicit, verbatim resume evidence; report support, gaps, screening risks, and interview preparation without inventing candidate facts.",
81
+ inputSchema: {
82
+ type: "object",
83
+ properties: { jobId: { type: "string", minLength: 1 }, resume: resumeSchema(), intent: intentSchema() },
84
+ required: ["jobId", "resume"], additionalProperties: false,
85
+ },
86
+ },
87
+ {
88
+ name: "optimize_resume",
89
+ description: "Propose an evidence-grounded resume revision for one selected job without overwriting the original or inserting unsupported claims.",
90
+ inputSchema: {
91
+ type: "object",
92
+ properties: {
93
+ jobId: { type: "string", minLength: 1 },
94
+ resume: resumeSchema(),
95
+ output: { type: "string", enum: ["suggestions", "unified_diff", "revised_markdown"] },
96
+ },
97
+ required: ["jobId", "resume", "output"], additionalProperties: false,
98
+ },
99
+ },
100
+ {
101
+ name: "search_jobs",
102
+ description: "Search the local job snapshot by role, location, country eligibility, and work mode.",
103
+ inputSchema: {
104
+ type: "object",
105
+ properties: {
106
+ query: { type: "string", description: "Words to match in job title or company" },
107
+ location: { type: "string", description: "Case-insensitive location substring" },
108
+ country: { type: "string", pattern: "^[A-Za-z]{2}$", description: "Two-letter country code for job eligibility, such as IN or DE" },
109
+ remote: { type: "boolean", description: "True for remote-only; false for non-remote-only" },
110
+ limit: { type: "integer", minimum: 1, maximum: 100, default: 50 },
111
+ },
112
+ additionalProperties: false,
113
+ },
114
+ },
115
+ {
116
+ name: "get_job",
117
+ description: "Get the full description and application URL for a job returned by recommend_jobs or search_jobs.",
118
+ inputSchema: {
119
+ type: "object",
120
+ properties: { id: { type: "string", description: "Stable job id returned by search_jobs" } },
121
+ required: ["id"],
122
+ additionalProperties: false,
123
+ },
124
+ },
125
+ ];
126
+
127
+ return {
128
+ list: () => definitions,
129
+ async call(name: string, input: Record<string, unknown>) {
130
+ if (name === "prepare_job_search") return workflows.prepareJobSearch(input);
131
+ if (name === "get_job_coverage") return workflows.getJobCoverage(input);
132
+ if (name === "recommend_jobs") return workflows.recommend(input);
133
+ if (name === "analyze_job_fit") return workflows.analyzeJobFit(input);
134
+ if (name === "optimize_resume") return workflows.optimizeResume(input);
135
+ if (name === "search_jobs") {
136
+ assertToolKeys(input, ["query", "location", "country", "remote", "limit"], "search_jobs");
137
+ if (input.query !== undefined && typeof input.query !== "string") throw new Error("query must be a string");
138
+ if (input.location !== undefined && typeof input.location !== "string") throw new Error("location must be a string");
139
+ if (input.remote !== undefined && typeof input.remote !== "boolean") throw new Error("remote must be a boolean");
140
+ if (input.limit !== undefined && (!Number.isInteger(input.limit) || (input.limit as number) < 1 || (input.limit as number) > 100)) throw new Error("limit must be an integer between 1 and 100");
141
+ const query: SearchQuery = {};
142
+ if (typeof input.query === "string") query.query = input.query;
143
+ if (typeof input.location === "string") query.location = input.location;
144
+ if (typeof input.country === "string" && /^[a-z]{2}$/i.test(input.country)) query.country = input.country.toUpperCase();
145
+ else if (input.country !== undefined) throw new Error("country must be a two-letter code");
146
+ if (typeof input.remote === "boolean") query.remote = input.remote;
147
+ if (typeof input.limit === "number") query.limit = input.limit;
148
+ return { jobs: await catalog.search(query) };
149
+ }
150
+ if (name === "get_job") {
151
+ assertToolKeys(input, ["id"], "get_job");
152
+ if (typeof input.id !== "string" || !input.id) throw new Error("get_job requires a non-empty id");
153
+ return { job: await catalog.get(input.id) };
154
+ }
155
+ throw new Error(`Unknown tool: ${name}`);
156
+ },
157
+ };
158
+ }
159
+
160
+ function resumeSchema(): Record<string, unknown> {
161
+ return {
162
+ type: "object",
163
+ properties: {
164
+ content: { type: "string", minLength: 1, description: "Resume content supplied directly; filesystem paths are not accepted" },
165
+ format: { type: "string", enum: ["text", "markdown", "pdf_base64", "docx_base64"] },
166
+ },
167
+ required: ["content", "format"], additionalProperties: false,
168
+ };
169
+ }
170
+
171
+ function intentSchema(): Record<string, unknown> {
172
+ return {
173
+ type: "object",
174
+ properties: {
175
+ roles: stringArray(), countries: countryArray(), locations: stringArray(), remote: { type: "boolean" }, seniority: stringArray(),
176
+ requiredSkills: stringArray(), excludedTerms: stringArray(),
177
+ excludedCountries: countryArray(), excludedLocations: stringArray(), excludedRoles: stringArray(),
178
+ },
179
+ additionalProperties: false,
180
+ };
181
+ }
182
+
183
+ function assertToolKeys(input: Record<string, unknown>, allowed: string[], tool: string): void {
184
+ const unknown = Object.keys(input).find((key) => !allowed.includes(key));
185
+ if (unknown) throw new Error(`${tool} does not accept field: ${unknown}`);
186
+ }
187
+
188
+ function stringArray(): Record<string, unknown> {
189
+ return { type: "array", items: { type: "string", minLength: 1 }, uniqueItems: true };
190
+ }
191
+
192
+ function countryArray(): Record<string, unknown> {
193
+ return { type: "array", items: { type: "string", pattern: "^[A-Za-z]{2}$" }, uniqueItems: true };
194
+ }
package/src/types.ts ADDED
@@ -0,0 +1,136 @@
1
+ export type Ats = "greenhouse" | "lever" | "ashby" | "workday" | "recruitee";
2
+
3
+ export interface DomainEvidence {
4
+ kind: "authoritative_dataset" | "company_registry" | "company_redirect";
5
+ reference: string;
6
+ }
7
+
8
+ export interface Company {
9
+ slug: string;
10
+ name: string;
11
+ ats: Ats;
12
+ token: string;
13
+ cohorts?: string[];
14
+ companyDomain?: string;
15
+ sourceUrl?: string;
16
+ discoveredFrom?: DiscoveryProvenance;
17
+ verification?: SourceVerification;
18
+ domainEvidence?: DomainEvidence;
19
+ }
20
+
21
+ export type DiscoveryChannel = "search" | "career_page" | "provider_directory" | "community" | "dataset" | "legacy";
22
+
23
+ export interface DiscoveryProvenance {
24
+ channel: DiscoveryChannel;
25
+ reference: string;
26
+ }
27
+
28
+ export interface SourceCandidate {
29
+ slug?: string;
30
+ companyName: string;
31
+ companyDomain: string;
32
+ sourceUrl: string;
33
+ cohorts?: string[];
34
+ discoveredFrom: DiscoveryProvenance;
35
+ domainEvidence?: DomainEvidence;
36
+ }
37
+
38
+ export interface SourceVerification {
39
+ checkedAt: string;
40
+ canonicalSourceUrl: string;
41
+ observedCompanyName: string;
42
+ identityEvidence: "provider_company_name" | "provider_tenant" | "structured_domain_link" | "company_redirect";
43
+ contentType: string;
44
+ payloadVersion: string;
45
+ jobCount: number;
46
+ }
47
+
48
+ export interface VerifiedCompany extends Company {
49
+ companyDomain: string;
50
+ sourceUrl: string;
51
+ discoveredFrom: DiscoveryProvenance;
52
+ verification: SourceVerification;
53
+ }
54
+
55
+ export type SourceRejectionReason = "invalid_candidate" | "unsupported_source" | "duplicate_source" | "duplicate_company" | "duplicate_slug" | "unreachable" | "invalid_payload" | "empty_board" | "identity_mismatch" | "no_country_jobs";
56
+
57
+ export interface RejectedSource extends SourceCandidate {
58
+ reason: SourceRejectionReason;
59
+ detail: string;
60
+ }
61
+
62
+ export interface SourceVerificationResult {
63
+ verified: VerifiedCompany[];
64
+ rejected: RejectedSource[];
65
+ }
66
+
67
+ export type WorkMode = "remote" | "hybrid" | "onsite" | "unknown";
68
+ export type EligibilityConfidence = "explicit" | "inferred" | "unknown";
69
+
70
+ export interface JobSummary {
71
+ id: string;
72
+ company: string;
73
+ title: string;
74
+ location: string;
75
+ remote: boolean;
76
+ workMode: WorkMode;
77
+ eligibleCountries: string[];
78
+ excludedCountries: string[];
79
+ eligibleRegions: string[];
80
+ eligibilityConfidence: EligibilityConfidence;
81
+ url: string;
82
+ updatedAt?: string;
83
+ }
84
+
85
+ export interface Job extends JobSummary {
86
+ description: string;
87
+ }
88
+
89
+ export interface SearchQuery {
90
+ query?: string;
91
+ location?: string;
92
+ country?: string;
93
+ remote?: boolean;
94
+ limit?: number;
95
+ }
96
+
97
+ export interface CrawlFailure {
98
+ source: string;
99
+ error: string;
100
+ }
101
+
102
+ export interface CrawlReport {
103
+ startedAt: string;
104
+ finishedAt: string;
105
+ considered?: number;
106
+ selected: number;
107
+ cached?: number;
108
+ deferred?: number;
109
+ succeeded: number;
110
+ failed: CrawlFailure[];
111
+ sources?: CrawlSourceResult[];
112
+ }
113
+
114
+ export interface CrawlSourceResult {
115
+ source: string;
116
+ status: "succeeded" | "failed";
117
+ attempts: number;
118
+ durationMs: number;
119
+ jobs: number;
120
+ countryJobs: Record<string, number>;
121
+ throttles: number;
122
+ backoffMs: number;
123
+ error?: string;
124
+ }
125
+
126
+ export interface JobPartition {
127
+ fetchedAt: string;
128
+ jobs: Job[];
129
+ }
130
+
131
+ export interface JobSnapshot {
132
+ version: 1;
133
+ updatedAt: string;
134
+ partitions: Record<string, JobPartition>;
135
+ lastCrawl: CrawlReport;
136
+ }