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/cli.ts ADDED
@@ -0,0 +1,601 @@
1
+ #!/usr/bin/env bun
2
+ import { readFile } from "node:fs/promises";
3
+ import { relative, resolve } from "node:path";
4
+ import { createRuntime } from "./runtime.ts";
5
+ import { runSourceVerification } from "./source-pipeline.ts";
6
+ import { runSourceDiscovery, runYcSourceDiscovery } from "./source-discovery.ts";
7
+ import { discoverAndPromote } from "./source-discovery-pipeline.ts";
8
+ import { traceCareerSources } from "./career-tracing.ts";
9
+ import { discoverCommonCrawlSources } from "./common-crawl-discovery.ts";
10
+ import { generateYcCompanySeeds } from "./company-seeds.ts";
11
+ import { exportSnapshot } from "./snapshot-export.ts";
12
+ import { enrichSourcesFromCompanies } from "./source-enrichment.ts";
13
+ import { forceReleaseFileLock, inspectFileLock } from "./file-lock.ts";
14
+ import { generateCountryCoverageReport } from "./country-coverage.ts";
15
+ import { probeJobPostingJsonLd } from "./jobposting-probe.ts";
16
+ import { mergeAttemptedRoundLeads, prepareRecruiteeRoundArtifacts } from "./recruitee-round.ts";
17
+
18
+ const HELP = `Openings — search public company job boards
19
+
20
+ Usage:
21
+ openings crawl [--country CODE | --companies FILE] [--concurrency N] [--source-cache-hours N] [--source-limit N] [--delay-ms N] [--workday-page-delay-ms N] [--data-dir PATH]
22
+ openings snapshot export [--input FILE] [--output-dir PATH]
23
+ openings coverage report --country CODE [--snapshot FILE] [--catalog FILE] [--candidates FILE] [--registry FILE] [--output FILE] [--as-of ISO]
24
+ openings sources verify CANDIDATES.json [--output FILE] [--state-file FILE] [--concurrency N] [--workday-concurrency N] [--limit N] [--require-country CODE] [--registry FILE] [--retry-deferred]
25
+ openings sources discover FEED.json [--country CODE] [--registry FILE] [--output FILE] [--catalog FILE] [--report FILE]
26
+ openings sources discover-yc --country CODE [--registry FILE] [--output FILE] [--catalog FILE] [--report FILE]
27
+ openings sources seed-companies-yc --country CODE [--output FILE]
28
+ openings sources discover-common-crawl [--country CODE] [--provider recruitee] [--index-record-limit N] [--sample-token-limit N] [--exclude-token TOKEN] [--report-only] [--registry FILE] [--output FILE] [--report FILE] [--index-url URL]
29
+ openings sources enrich COMPANIES.json [--companies FILE]... [--evidence-kind authoritative_dataset|company_registry] [--registry FILE] [--output FILE] [--report FILE]
30
+ openings sources trace-careers COMPANIES.json [--country CODE] [--registry FILE] [--common-crawl-report FILE] [--search-key-env NAME] [--output FILE] [--catalog FILE] [--report FILE]
31
+ openings sources probe-jobposting COMPANIES.md [--catalog FILE] [--report FILE] [--company-limit 10|20]
32
+ openings sources prepare-recruitee-round IDENTITIES.json [--catalog FILE] [--artifacts DIR]
33
+ openings sources merge-attempted-round-leads ISOLATED_REGISTRY [--registry FILE]
34
+ openings search [words] [--country CODE|--india] [--location PLACE] [--remote|--onsite]
35
+ [--limit N] [--stale-days N] [--offline] [--data-dir PATH]
36
+ openings get JOB_ID [--stale-days N] [--offline] [--data-dir PATH]
37
+ openings lock inspect TARGET
38
+ openings lock force-release TARGET --force
39
+ openings --help
40
+
41
+ Results are JSON so humans and agents can use the same command.`;
42
+
43
+ export async function run(args: string[]): Promise<number> {
44
+ const [command, ...rest] = args;
45
+ if (!command || command === "--help" || command === "-h") {
46
+ console.log(HELP);
47
+ return 0;
48
+ }
49
+
50
+ if (command === "get") {
51
+ const id = rest[0];
52
+ if (!id) return fail("get requires a JOB_ID");
53
+ const settings = parseRuntimeOptions(rest.slice(1));
54
+ if (typeof settings === "string") return fail(settings);
55
+ const result = await createRuntime({ dataDir: settings.dataDir }).get(id, settings);
56
+ if (!result.job) return fail(`Job not found: ${id}`, 2);
57
+ console.log(JSON.stringify(result, null, 2));
58
+ return 0;
59
+ }
60
+
61
+ if (command === "crawl") {
62
+ const parsed = parseCrawl(rest);
63
+ if (typeof parsed === "string") return fail(parsed);
64
+ const runtime = createRuntime({
65
+ dataDir: parsed.dataDir, concurrency: parsed.concurrency, sourceCacheHours: parsed.sourceCacheHours,
66
+ sourceLimit: parsed.sourceLimit, crawlDelayMs: parsed.delayMs, workdayPageDelayMs: parsed.workdayPageDelayMs,
67
+ });
68
+ const slugs = parsed.companiesFile ? await readCompanyFile(parsed.companiesFile) : undefined;
69
+ console.log(JSON.stringify(await runtime.crawl({ country: parsed.country, slugs }), null, 2));
70
+ return 0;
71
+ }
72
+
73
+ if (command === "lock") {
74
+ const action = rest[0];
75
+ const target = rest[1];
76
+ if (!target) return fail("lock requires an action and target file");
77
+ if (action === "inspect" && rest.length === 2) { console.log(JSON.stringify(await inspectFileLock(target), null, 2)); return 0; }
78
+ if (action === "force-release" && rest[2] === "--force" && rest.length === 3) { console.log(JSON.stringify({ released: await forceReleaseFileLock(target) }, null, 2)); return 0; }
79
+ return fail("lock supports `inspect TARGET` or `force-release TARGET --force`");
80
+ }
81
+
82
+ if (command === "snapshot") {
83
+ if (rest[0] !== "export") return fail("snapshot requires the `export` subcommand");
84
+ const parsed = parseSnapshotExport(rest.slice(1));
85
+ if (typeof parsed === "string") return fail(parsed);
86
+ console.log(JSON.stringify(await exportSnapshot(parsed.input, parsed.outputDir), null, 2));
87
+ return 0;
88
+ }
89
+
90
+ if (command === "coverage") {
91
+ if (rest[0] !== "report") return fail("coverage requires the `report` subcommand");
92
+ const parsed = parseCoverageReport(rest.slice(1));
93
+ if (typeof parsed === "string") return fail(parsed);
94
+ console.log(JSON.stringify(await generateCountryCoverageReport(parsed, { country: parsed.country, asOf: parsed.asOf }), null, 2));
95
+ return 0;
96
+ }
97
+
98
+ if (command === "sources") {
99
+ if (rest[0] === "prepare-recruitee-round") {
100
+ const parsed = parsePrepareRecruiteeRound(rest.slice(1));
101
+ if (typeof parsed === "string") return fail(parsed);
102
+ console.log(JSON.stringify(await prepareRecruiteeRoundArtifacts(parsed.identityPath, parsed.catalog, parsed), null, 2));
103
+ return 0;
104
+ }
105
+ if (rest[0] === "merge-attempted-round-leads") {
106
+ const parsed = parseMergeAttemptedRoundLeads(rest.slice(1));
107
+ if (typeof parsed === "string") return fail(parsed);
108
+ console.log(JSON.stringify(await mergeAttemptedRoundLeads(parsed.isolatedRegistryPath, parsed.registry), null, 2));
109
+ return 0;
110
+ }
111
+ if (rest[0] === "probe-jobposting") {
112
+ const parsed = parseJobPostingProbe(rest.slice(1));
113
+ if (typeof parsed === "string") return fail(parsed);
114
+ console.log(JSON.stringify(await probeJobPostingJsonLd(parsed.inputPath, parsed.catalog, parsed.report, { companyLimit: parsed.companyLimit }), null, 2));
115
+ return 0;
116
+ }
117
+ if (rest[0] === "enrich") {
118
+ const parsed = parseSourceEnrichment(rest.slice(1));
119
+ if (typeof parsed === "string") return fail(parsed);
120
+ console.log(JSON.stringify(await enrichSourcesFromCompanies(parsed.registry, parsed.companies, parsed.output, parsed.report, { evidenceKind: parsed.evidenceKind }), null, 2));
121
+ return 0;
122
+ }
123
+ if (rest[0] === "seed-companies-yc") {
124
+ const parsed = parseYcCompanySeeds(rest.slice(1));
125
+ if (typeof parsed === "string") return fail(parsed);
126
+ console.log(JSON.stringify(await generateYcCompanySeeds(parsed.output, parsed), null, 2));
127
+ return 0;
128
+ }
129
+ if (rest[0] === "trace-careers") {
130
+ const parsed = parseCareerTracing(rest.slice(1));
131
+ if (typeof parsed === "string") return fail(parsed);
132
+ const discovery = await traceCareerSources(parsed.inputPath, parsed.output, parsed.report, parsed);
133
+ const promotion = await runSourceVerification(parsed.output, parsed.catalog, parsed);
134
+ console.log(JSON.stringify({ discovery: compactCareerTraceReport(discovery), promotion }, null, 2));
135
+ return 0;
136
+ }
137
+ if (rest[0] === "discover-common-crawl") {
138
+ const parsed = parseCommonCrawlDiscovery(rest.slice(1));
139
+ if (typeof parsed === "string") return fail(parsed);
140
+ console.log(JSON.stringify(compactCommonCrawlReport(await discoverCommonCrawlSources(parsed.output, parsed.report, parsed)), null, 2));
141
+ return 0;
142
+ }
143
+ if (rest[0] === "discover-yc") {
144
+ const parsed = parseYcDiscovery(rest.slice(1));
145
+ if (typeof parsed === "string") return fail(parsed);
146
+ console.log(JSON.stringify(compactDiscoveryPromotion(await discoverAndPromote(
147
+ () => runYcSourceDiscovery(parsed.output, parsed.report, parsed), parsed.output, parsed.catalog, parsed,
148
+ )), null, 2));
149
+ return 0;
150
+ }
151
+ if (rest[0] === "discover") {
152
+ const parsed = parseSourceDiscovery(rest.slice(1));
153
+ if (typeof parsed === "string") return fail(parsed);
154
+ console.log(JSON.stringify(compactDiscoveryPromotion(await discoverAndPromote(
155
+ () => runSourceDiscovery(parsed.feedPath, parsed.output, parsed.report, parsed), parsed.output, parsed.catalog, parsed,
156
+ )), null, 2));
157
+ return 0;
158
+ }
159
+ if (rest[0] !== "verify") return fail("sources requires a discovery, tracing, or `verify` subcommand");
160
+ const parsed = parseSourceVerification(rest.slice(1));
161
+ if (typeof parsed === "string") return fail(parsed);
162
+ console.log(JSON.stringify(await runSourceVerification(parsed.candidatesPath, parsed.output, { concurrency: parsed.concurrency, providerConcurrency: { workday: parsed.workdayConcurrency }, limit: parsed.limit, requireCountry: parsed.requireCountry, registryPath: parsed.registryPath, retryDeferred: parsed.retryDeferred, batchStatePath: parsed.batchStatePath }), null, 2));
163
+ return 0;
164
+ }
165
+
166
+ if (command === "search") {
167
+ const parsed = parseSearch(rest);
168
+ if (typeof parsed === "string") return fail(parsed);
169
+ const runtime = createRuntime({ dataDir: parsed.dataDir });
170
+ const result = await runtime.search(parsed.query, parsed);
171
+ console.log(JSON.stringify({ jobs: result.jobs, count: result.jobs.length, snapshot: result.snapshot }, null, 2));
172
+ return 0;
173
+ }
174
+
175
+ return fail(`Unknown command: ${command}`);
176
+ }
177
+
178
+ function parseJobPostingProbe(args: string[]) {
179
+ const inputPath = args[0];
180
+ if (!inputPath || inputPath.startsWith("--")) return "sources probe-jobposting requires a company Markdown file";
181
+ let catalog = "data/companies.json";
182
+ let report = ".openings/jobposting-probe-report.json";
183
+ let companyLimit = 10;
184
+ for (let index = 1; index < args.length; index += 1) {
185
+ const arg = args[index];
186
+ if (arg === "--catalog") { catalog = args[++index] ?? ""; if (!catalog) return "--catalog requires a file"; }
187
+ else if (arg === "--report") { report = args[++index] ?? ""; if (!report) return "--report requires a file"; }
188
+ else if (arg === "--company-limit") { companyLimit = Number(args[++index]); if (companyLimit !== 10 && companyLimit !== 20) return "--company-limit must be 10 or 20"; }
189
+ else return `Unknown option: ${arg}`;
190
+ }
191
+ return { inputPath, catalog, report, companyLimit };
192
+ }
193
+
194
+ function parsePrepareRecruiteeRound(args: string[]) {
195
+ const identityPath = args[0];
196
+ if (!identityPath || identityPath.startsWith("--")) return "sources prepare-recruitee-round requires an identity JSON file";
197
+ let catalog = "data/companies.json";
198
+ let artifactRoot = ".openings/round5-recruitee";
199
+ for (let index = 1; index < args.length; index += 1) {
200
+ const arg = args[index];
201
+ if (arg === "--catalog") catalog = args[++index] ?? "";
202
+ else if (arg === "--artifacts") artifactRoot = args[++index] ?? "";
203
+ else return `Unknown option: ${arg}`;
204
+ if (![catalog, artifactRoot].every(Boolean)) return `${arg} requires a path`;
205
+ }
206
+ if (!underOpenings(artifactRoot)) return "--artifacts must be under .openings";
207
+ return { identityPath, catalog, artifactRoot };
208
+ }
209
+
210
+ function parseMergeAttemptedRoundLeads(args: string[]) {
211
+ const isolatedRegistryPath = args[0];
212
+ if (!isolatedRegistryPath || isolatedRegistryPath.startsWith("--")) return "sources merge-attempted-round-leads requires an isolated registry file";
213
+ let registry = "data/enrichment-leads.json";
214
+ for (let index = 1; index < args.length; index += 1) {
215
+ const arg = args[index];
216
+ if (arg === "--registry") { registry = args[++index] ?? ""; if (!registry) return "--registry requires a file"; }
217
+ else return `Unknown option: ${arg}`;
218
+ }
219
+ return { isolatedRegistryPath, registry };
220
+ }
221
+
222
+ function parseYcCompanySeeds(args: string[]) {
223
+ let output = ".openings/company-domains.json";
224
+ let country: string | undefined;
225
+ for (let index = 0; index < args.length; index += 1) {
226
+ const arg = args[index];
227
+ if (arg === "--country") { country = parseCountry(args[++index]); if (!country) return "--country requires a two-letter country code"; }
228
+ else if (arg === "--output") { output = args[++index] ?? ""; if (!output) return "--output requires a file"; }
229
+ else return `Unknown option: ${arg}`;
230
+ }
231
+ if (!country) return "sources seed-companies-yc requires --country CODE";
232
+ return { country, output };
233
+ }
234
+
235
+ function parseSourceEnrichment(args: string[]) {
236
+ const firstCompanyInput = args[0];
237
+ if (!firstCompanyInput || firstCompanyInput.startsWith("--")) return "sources enrich requires a company JSON file";
238
+ const companies = [firstCompanyInput];
239
+ let registry = "data/enrichment-leads.json";
240
+ let output = "data/source-candidates.json";
241
+ let report = ".openings/source-enrichment-report.json";
242
+ let evidenceKind: "authoritative_dataset" | "company_registry" | undefined;
243
+ for (let index = 1; index < args.length; index += 1) {
244
+ const arg = args[index];
245
+ if (arg === "--registry") { registry = args[++index] ?? ""; if (!registry) return "--registry requires a file"; }
246
+ else if (arg === "--companies") { const value = args[++index] ?? ""; if (!value) return "--companies requires a file"; companies.push(value); }
247
+ else if (arg === "--output") { output = args[++index] ?? ""; if (!output) return "--output requires a file"; }
248
+ else if (arg === "--report") { report = args[++index] ?? ""; if (!report) return "--report requires a file"; }
249
+ else if (arg === "--evidence-kind") { const value = args[++index]; if (value !== "authoritative_dataset" && value !== "company_registry") return "--evidence-kind requires authoritative_dataset or company_registry"; evidenceKind = value; }
250
+ else return `Unknown option: ${arg}`;
251
+ }
252
+ return { companies, registry, output, report, evidenceKind };
253
+ }
254
+
255
+ function parseSnapshotExport(args: string[]) {
256
+ let input = ".openings/snapshot.json";
257
+ let outputDir = ".openings/dist";
258
+ for (let index = 0; index < args.length; index += 1) {
259
+ const arg = args[index];
260
+ if (arg === "--input") { input = args[++index] ?? ""; if (!input) return "--input requires a file"; }
261
+ else if (arg === "--output-dir") { outputDir = args[++index] ?? ""; if (!outputDir) return "--output-dir requires a path"; }
262
+ else return `Unknown option: ${arg}`;
263
+ }
264
+ return { input, outputDir };
265
+ }
266
+
267
+ function parseCoverageReport(args: string[]) {
268
+ let country: string | undefined;
269
+ let snapshot = ".openings/snapshot.json";
270
+ let catalog = "data/companies.json";
271
+ let candidates = "data/source-candidates.json";
272
+ let registry = "data/enrichment-leads.json";
273
+ let output: string | undefined;
274
+ let asOf: Date | undefined;
275
+ for (let index = 0; index < args.length; index += 1) {
276
+ const arg = args[index];
277
+ if (arg === "--country") { country = parseCountry(args[++index]); if (!country) return "--country requires a two-letter country code"; }
278
+ else if (["--snapshot", "--catalog", "--candidates", "--registry", "--output"].includes(arg ?? "")) {
279
+ const value = args[++index];
280
+ if (!value) return `${arg} requires a file`;
281
+ if (arg === "--snapshot") snapshot = value;
282
+ else if (arg === "--catalog") catalog = value;
283
+ else if (arg === "--candidates") candidates = value;
284
+ else if (arg === "--registry") registry = value;
285
+ else output = value;
286
+ } else if (arg === "--as-of") {
287
+ const value = args[++index];
288
+ const parsed = value ? new Date(value) : undefined;
289
+ if (!parsed || !Number.isFinite(parsed.getTime()) || parsed.toISOString() !== value) return "--as-of requires a canonical ISO timestamp";
290
+ asOf = parsed;
291
+ } else return `Unknown option: ${arg}`;
292
+ }
293
+ if (!country) return "coverage report requires --country CODE";
294
+ return { country, snapshot, catalog, candidates, registry, output: output ?? `.openings/country-coverage-${country.toLowerCase()}.json`, asOf };
295
+ }
296
+
297
+ function parseCareerTracing(args: string[]) {
298
+ const inputPath = args[0];
299
+ if (!inputPath || inputPath.startsWith("--")) return "sources trace-careers requires a company JSON file";
300
+ const parsed = parseDiscoveryOutputs(args.slice(1), ".openings/career-trace-report.json", true);
301
+ return typeof parsed === "string" ? parsed : { inputPath, ...parsed };
302
+ }
303
+
304
+ function parseCommonCrawlDiscovery(args: string[]) {
305
+ let output = "data/source-candidates.json";
306
+ let report = ".openings/common-crawl-discovery-report.json";
307
+ let country: string | undefined;
308
+ let indexUrl: string | undefined;
309
+ let registryPath: string | undefined = "data/enrichment-leads.json";
310
+ let provider: "recruitee" | undefined;
311
+ let indexRecordLimit: number | undefined;
312
+ let sampleTokenLimit: number | undefined;
313
+ const excludeTokens: string[] = [];
314
+ let reportOnly = false;
315
+ for (let index = 0; index < args.length; index += 1) {
316
+ const arg = args[index];
317
+ if (arg === "--country") { country = parseCountry(args[++index]); if (!country) return "--country requires a two-letter country code"; }
318
+ else if (arg === "--output") { output = args[++index] ?? ""; if (!output) return "--output requires a file"; }
319
+ else if (arg === "--report") { report = args[++index] ?? ""; if (!report) return "--report requires a file"; }
320
+ else if (arg === "--index-url") { indexUrl = args[++index]; if (!indexUrl) return "--index-url requires a URL"; try { new URL(indexUrl); } catch { return "--index-url requires a valid URL"; } }
321
+ else if (arg === "--registry") { if (reportOnly) return "Use either --registry or --report-only, not both"; registryPath = args[++index] ?? ""; if (!registryPath) return "--registry requires a file"; }
322
+ else if (arg === "--report-only") { if (registryPath !== "data/enrichment-leads.json") return "Use either --registry or --report-only, not both"; reportOnly = true; registryPath = undefined; }
323
+ else if (arg === "--provider") { const value = args[++index]; if (value !== "recruitee") return "--provider currently supports only recruitee"; provider = value; }
324
+ else if (arg === "--index-record-limit") { indexRecordLimit = Number(args[++index]); if (!Number.isInteger(indexRecordLimit) || indexRecordLimit < 1 || indexRecordLimit > 2_000) return "--index-record-limit must be an integer from 1 to 2000"; }
325
+ else if (arg === "--sample-token-limit") { sampleTokenLimit = Number(args[++index]); if (!Number.isInteger(sampleTokenLimit) || sampleTokenLimit < 1 || sampleTokenLimit > 60) return "--sample-token-limit must be an integer from 1 to 60"; }
326
+ else if (arg === "--exclude-token") { const value = args[++index]?.trim(); if (!value || !/^[a-z0-9-]+$/i.test(value)) return "--exclude-token requires an ATS token"; excludeTokens.push(value.toLowerCase()); }
327
+ else return `Unknown option: ${arg}`;
328
+ }
329
+ if ((indexRecordLimit !== undefined || sampleTokenLimit !== undefined || excludeTokens.length || registryPath === undefined) && provider !== "recruitee") return "bounded report-only discovery requires --provider recruitee";
330
+ if (provider === "recruitee" && (!indexUrl || indexRecordLimit === undefined || sampleTokenLimit === undefined || !reportOnly)) return "--provider recruitee requires --index-url, --index-record-limit, --sample-token-limit, and --report-only";
331
+ if (provider === "recruitee" && !underOpenings(report)) return "bounded Recruitee discovery --report must be under .openings";
332
+ if (sampleTokenLimit !== undefined && indexRecordLimit !== undefined && sampleTokenLimit > indexRecordLimit) return "--sample-token-limit cannot exceed --index-record-limit";
333
+ return { output, report, country, indexUrl, registryPath, provider, indexRecordLimit, sampleTokenLimit, excludeTokens };
334
+ }
335
+
336
+ function parseDiscoveryOutputs(args: string[], defaultReport: string, requireCatalog: boolean) {
337
+ let output = "data/source-candidates.json";
338
+ let report = defaultReport;
339
+ let catalog = "data/companies.json";
340
+ let country: string | undefined;
341
+ let concurrency = 10;
342
+ let searchKey: string | undefined;
343
+ let commonCrawlReportPath: string | undefined;
344
+ let registryPath = "data/enrichment-leads.json";
345
+ for (let index = 0; index < args.length; index += 1) {
346
+ const arg = args[index];
347
+ if (arg === "--country") { country = parseCountry(args[++index]); if (!country) return "--country requires a two-letter country code"; }
348
+ else if (arg === "--output") { output = args[++index] ?? ""; if (!output) return "--output requires a file"; }
349
+ else if (arg === "--report") { report = args[++index] ?? ""; if (!report) return "--report requires a file"; }
350
+ else if (arg === "--catalog" && requireCatalog) { catalog = args[++index] ?? ""; if (!catalog) return "--catalog requires a file"; }
351
+ else if (arg === "--search-key-env") {
352
+ const name = args[++index];
353
+ if (!name) return "--search-key-env requires an environment variable name";
354
+ searchKey = process.env[name];
355
+ if (!searchKey) return `Environment variable ${name} is not set`;
356
+ }
357
+ else if (arg === "--common-crawl-report") {
358
+ commonCrawlReportPath = args[++index];
359
+ if (!commonCrawlReportPath) return "--common-crawl-report requires a file";
360
+ }
361
+ else if (arg === "--registry") { registryPath = args[++index] ?? ""; if (!registryPath) return "--registry requires a file"; }
362
+ else if (arg === "--concurrency") { concurrency = Number(args[++index]); if (!Number.isInteger(concurrency) || concurrency < 1 || concurrency > 100) return "--concurrency must be an integer from 1 to 100"; }
363
+ else return `Unknown option: ${arg}`;
364
+ }
365
+ return { output, report, catalog, country, concurrency, searchKey, commonCrawlReportPath, registryPath };
366
+ }
367
+
368
+ function parseYcDiscovery(args: string[]) {
369
+ let output = "data/source-candidates.json";
370
+ let report = ".openings/yc-discovery-report.json";
371
+ let catalog = "data/companies.json";
372
+ let country: string | undefined;
373
+ let concurrency = 10;
374
+ let registryPath = "data/enrichment-leads.json";
375
+ for (let index = 0; index < args.length; index += 1) {
376
+ const arg = args[index];
377
+ if (arg === "--country") {
378
+ country = parseCountry(args[++index]);
379
+ if (!country) return "--country requires a two-letter country code";
380
+ } else if (arg === "--output") output = args[++index] ?? "";
381
+ else if (arg === "--registry") { registryPath = args[++index] ?? ""; if (!registryPath) return "--registry requires a file"; }
382
+ else if (arg === "--catalog") catalog = args[++index] ?? "";
383
+ else if (arg === "--report") report = args[++index] ?? "";
384
+ else if (arg === "--concurrency") {
385
+ concurrency = Number(args[++index]);
386
+ if (!Number.isInteger(concurrency) || concurrency < 1 || concurrency > 100) return "--concurrency must be an integer from 1 to 100";
387
+ } else return `Unknown option: ${arg}`;
388
+ }
389
+ if (!country) return "sources discover-yc requires --country CODE";
390
+ if (!output) return "--output requires a file";
391
+ if (!report) return "--report requires a file";
392
+ if (!catalog) return "--catalog requires a file";
393
+ return { output, report, catalog, country, concurrency, registryPath };
394
+ }
395
+
396
+ function parseSourceDiscovery(args: string[]) {
397
+ const feedPath = args[0];
398
+ if (!feedPath || feedPath.startsWith("--")) return "sources discover requires a feed JSON file";
399
+ let output = "data/source-candidates.json";
400
+ let report = ".openings/discovery-report.json";
401
+ let catalog = "data/companies.json";
402
+ let country: string | undefined;
403
+ let concurrency = 10;
404
+ let registryPath = "data/enrichment-leads.json";
405
+ for (let index = 1; index < args.length; index += 1) {
406
+ const arg = args[index];
407
+ if (arg === "--country") {
408
+ country = parseCountry(args[++index]);
409
+ if (!country) return "--country requires a two-letter country code";
410
+ } else if (arg === "--output") {
411
+ output = args[++index] ?? "";
412
+ if (!output) return "--output requires a file";
413
+ } else if (arg === "--report") {
414
+ report = args[++index] ?? "";
415
+ if (!report) return "--report requires a file";
416
+ } else if (arg === "--catalog") {
417
+ catalog = args[++index] ?? "";
418
+ if (!catalog) return "--catalog requires a file";
419
+ } else if (arg === "--registry") {
420
+ registryPath = args[++index] ?? "";
421
+ if (!registryPath) return "--registry requires a file";
422
+ } else if (arg === "--concurrency") {
423
+ concurrency = Number(args[++index]);
424
+ if (!Number.isInteger(concurrency) || concurrency < 1 || concurrency > 100) return "--concurrency must be an integer from 1 to 100";
425
+ } else return `Unknown option: ${arg}`;
426
+ }
427
+ return { feedPath, output, report, catalog, country, concurrency, registryPath };
428
+ }
429
+
430
+ function parseSearch(args: string[]) {
431
+ const queryWords: string[] = [];
432
+ let location: string | undefined;
433
+ let country: string | undefined;
434
+ let remote: boolean | undefined;
435
+ let limit: number | undefined;
436
+ let offline = false;
437
+ let staleDays = 14;
438
+ let dataDir: string | undefined;
439
+
440
+ for (let index = 0; index < args.length; index += 1) {
441
+ const arg = args[index];
442
+ if (arg === "--location") {
443
+ location = args[++index];
444
+ if (!location) return "--location requires a value";
445
+ } else if (arg === "--country") {
446
+ country = parseCountry(args[++index]);
447
+ if (!country) return "--country requires a two-letter country code";
448
+ } else if (arg === "--remote") remote = true;
449
+ else if (arg === "--india") country = "IN";
450
+ else if (arg === "--onsite") remote = false;
451
+ else if (arg === "--offline") offline = true;
452
+ else if (arg === "--stale-days") {
453
+ staleDays = Number(args[++index]);
454
+ if (!Number.isFinite(staleDays) || staleDays < 0) return "--stale-days must be zero or greater";
455
+ } else if (arg === "--data-dir") {
456
+ dataDir = args[++index];
457
+ if (!dataDir) return "--data-dir requires a value";
458
+ }
459
+ else if (arg === "--limit") {
460
+ limit = Number(args[++index]);
461
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100) return "--limit must be an integer from 1 to 100";
462
+ } else if (arg?.startsWith("--")) return `Unknown option: ${arg}`;
463
+ else if (arg) queryWords.push(arg);
464
+ }
465
+
466
+ return { query: { query: queryWords.join(" ") || undefined, location, country, remote, limit }, offline, staleDays, dataDir };
467
+ }
468
+
469
+ function parseRuntimeOptions(args: string[]) {
470
+ let offline = false;
471
+ let staleDays = 14;
472
+ let dataDir: string | undefined;
473
+ for (let index = 0; index < args.length; index += 1) {
474
+ const arg = args[index];
475
+ if (arg === "--offline") offline = true;
476
+ else if (arg === "--stale-days") {
477
+ staleDays = Number(args[++index]);
478
+ if (!Number.isFinite(staleDays) || staleDays < 0) return "--stale-days must be zero or greater";
479
+ } else if (arg === "--data-dir") {
480
+ dataDir = args[++index];
481
+ if (!dataDir) return "--data-dir requires a value";
482
+ } else return `Unknown option: ${arg}`;
483
+ }
484
+ return { offline, staleDays, dataDir };
485
+ }
486
+
487
+ function parseCrawl(args: string[]) {
488
+ let country: string | undefined;
489
+ let companiesFile: string | undefined;
490
+ let dataDir: string | undefined;
491
+ let concurrency = 10;
492
+ let delayMs = 0;
493
+ let workdayPageDelayMs = 0;
494
+ let sourceCacheHours = 24;
495
+ let sourceLimit = 0;
496
+ for (let index = 0; index < args.length; index += 1) {
497
+ const arg = args[index];
498
+ if (arg === "--country") {
499
+ country = parseCountry(args[++index]);
500
+ if (!country) return "--country requires a two-letter country code";
501
+ } else if (arg === "--companies") companiesFile = args[++index];
502
+ else if (arg === "--data-dir") dataDir = args[++index];
503
+ else if (arg === "--concurrency") {
504
+ concurrency = Number(args[++index]);
505
+ if (!Number.isInteger(concurrency) || concurrency < 1 || concurrency > 100) return "--concurrency must be an integer from 1 to 100";
506
+ } else if (arg === "--delay-ms") {
507
+ delayMs = Number(args[++index]);
508
+ if (!Number.isInteger(delayMs) || delayMs < 0 || delayMs > 60_000) return "--delay-ms must be an integer from 0 to 60000";
509
+ } else if (arg === "--workday-page-delay-ms") {
510
+ workdayPageDelayMs = Number(args[++index]);
511
+ if (!Number.isInteger(workdayPageDelayMs) || workdayPageDelayMs < 0 || workdayPageDelayMs > 60_000) return "--workday-page-delay-ms must be an integer from 0 to 60000";
512
+ } else if (arg === "--source-cache-hours") {
513
+ sourceCacheHours = Number(args[++index]);
514
+ if (!Number.isFinite(sourceCacheHours) || sourceCacheHours < 0 || sourceCacheHours > 8760) return "--source-cache-hours must be a number from 0 to 8760";
515
+ } else if (arg === "--source-limit") {
516
+ sourceLimit = Number(args[++index]);
517
+ if (!Number.isInteger(sourceLimit) || sourceLimit < 0 || sourceLimit > 100_000) return "--source-limit must be an integer from 0 to 100000";
518
+ } else return `Unknown option: ${arg}`;
519
+ }
520
+ if (country && companiesFile) return "Use either --country or --companies, not both";
521
+ if (args.includes("--companies") && !companiesFile) return "--companies requires a file";
522
+ if (args.includes("--data-dir") && !dataDir) return "--data-dir requires a value";
523
+ return { country, companiesFile, dataDir, concurrency, delayMs, workdayPageDelayMs, sourceCacheHours, sourceLimit };
524
+ }
525
+
526
+ function parseCountry(value: string | undefined): string | undefined {
527
+ return value && /^[a-z]{2}$/i.test(value) ? value.toUpperCase() : undefined;
528
+ }
529
+
530
+ function underOpenings(path: string) { const root = resolve(".openings"); const relation = relative(root, resolve(path)); return Boolean(relation) && !relation.startsWith(".."); }
531
+
532
+ async function readCompanyFile(path: string): Promise<string[]> {
533
+ return (await readFile(path, "utf8")).split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
534
+ }
535
+
536
+ function parseSourceVerification(args: string[]) {
537
+ const candidatesPath = args[0];
538
+ if (!candidatesPath || candidatesPath.startsWith("--")) return "sources verify requires a candidate JSON file";
539
+ let output = "data/companies.json";
540
+ let batchStatePath: string | undefined;
541
+ let concurrency = 10;
542
+ let workdayConcurrency = 2;
543
+ let requireCountry: string | undefined;
544
+ let registryPath: string | undefined;
545
+ let retryDeferred = false;
546
+ let limit: number | undefined;
547
+ for (let index = 1; index < args.length; index += 1) {
548
+ const arg = args[index];
549
+ if (arg === "--output") {
550
+ output = args[++index] ?? "";
551
+ if (!output) return "--output requires a file";
552
+ } else if (arg === "--state-file") {
553
+ batchStatePath = args[++index] ?? "";
554
+ if (!batchStatePath) return "--state-file requires a file";
555
+ } else if (arg === "--concurrency") {
556
+ concurrency = Number(args[++index]);
557
+ if (!Number.isInteger(concurrency) || concurrency < 1 || concurrency > 100) return "--concurrency must be an integer from 1 to 100";
558
+ } else if (arg === "--workday-concurrency") {
559
+ workdayConcurrency = Number(args[++index]);
560
+ if (!Number.isInteger(workdayConcurrency) || workdayConcurrency < 1 || workdayConcurrency > 10) return "--workday-concurrency must be an integer from 1 to 10";
561
+ } else if (arg === "--require-country") {
562
+ requireCountry = parseCountry(args[++index]);
563
+ if (!requireCountry) return "--require-country requires a two-letter country code";
564
+ } else if (arg === "--limit") {
565
+ limit = Number(args[++index]);
566
+ if (!Number.isInteger(limit) || limit < 1) return "--limit must be a positive integer";
567
+ } else if (arg === "--registry") {
568
+ registryPath = args[++index];
569
+ if (!registryPath) return "--registry requires a file";
570
+ } else if (arg === "--retry-deferred") {
571
+ retryDeferred = true;
572
+ } else return `Unknown option: ${arg}`;
573
+ }
574
+ return { candidatesPath, output, batchStatePath, concurrency, workdayConcurrency, limit, requireCountry, registryPath, retryDeferred };
575
+ }
576
+
577
+ function fail(message: string, code = 1): number {
578
+ console.error(message);
579
+ return code;
580
+ }
581
+
582
+ function compactDiscoveryReport(report: import("./source-discovery.ts").SourceDiscoveryReport) {
583
+ const { unresolved: _unresolved, rejections: _rejections, ...summary } = report;
584
+ return summary;
585
+ }
586
+
587
+ function compactDiscoveryPromotion(result: import("./source-discovery-pipeline.ts").DiscoveryPromotionResult) {
588
+ return { discovery: compactDiscoveryReport(result.discovery), promotion: result.promotion };
589
+ }
590
+
591
+ function compactCareerTraceReport(report: import("./career-tracing.ts").CareerTraceReport) {
592
+ const { unresolvedCompanies: _unresolved, rejections: _rejections, failureDetails: _failures, ...summary } = report;
593
+ return summary;
594
+ }
595
+
596
+ function compactCommonCrawlReport(report: import("./common-crawl-discovery.ts").CommonCrawlDiscoveryReport) {
597
+ const { leads: _leads, availableLeads: _available, rejections: _rejections, ...summary } = report;
598
+ return { ...summary, availableLeadCount: report.availableLeads.length, sampledLeadCount: report.leads.length };
599
+ }
600
+
601
+ if (import.meta.main) process.exitCode = await run(Bun.argv.slice(2));