jobgrep 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 (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +109 -0
  3. package/dist/bin/jobgrep.d.ts +2 -0
  4. package/dist/bin/jobgrep.js +5 -0
  5. package/dist/bin/jobgrep.js.map +1 -0
  6. package/dist/src/auth.d.ts +10 -0
  7. package/dist/src/auth.js +58 -0
  8. package/dist/src/auth.js.map +1 -0
  9. package/dist/src/cli.d.ts +6 -0
  10. package/dist/src/cli.js +141 -0
  11. package/dist/src/cli.js.map +1 -0
  12. package/dist/src/config.d.ts +12 -0
  13. package/dist/src/config.js +22 -0
  14. package/dist/src/config.js.map +1 -0
  15. package/dist/src/formatters.d.ts +4 -0
  16. package/dist/src/formatters.js +44 -0
  17. package/dist/src/formatters.js.map +1 -0
  18. package/dist/src/http.d.ts +2 -0
  19. package/dist/src/http.js +18 -0
  20. package/dist/src/http.js.map +1 -0
  21. package/dist/src/index.d.ts +1 -0
  22. package/dist/src/index.js +2 -0
  23. package/dist/src/index.js.map +1 -0
  24. package/dist/src/normalize.d.ts +26 -0
  25. package/dist/src/normalize.js +287 -0
  26. package/dist/src/normalize.js.map +1 -0
  27. package/dist/src/paths.d.ts +3 -0
  28. package/dist/src/paths.js +20 -0
  29. package/dist/src/paths.js.map +1 -0
  30. package/dist/src/provider/serpapi.d.ts +9 -0
  31. package/dist/src/provider/serpapi.js +60 -0
  32. package/dist/src/provider/serpapi.js.map +1 -0
  33. package/dist/src/query.d.ts +5 -0
  34. package/dist/src/query.js +42 -0
  35. package/dist/src/query.js.map +1 -0
  36. package/dist/src/search.d.ts +7 -0
  37. package/dist/src/search.js +27 -0
  38. package/dist/src/search.js.map +1 -0
  39. package/dist/src/types.d.ts +53 -0
  40. package/dist/src/types.js +2 -0
  41. package/dist/src/types.js.map +1 -0
  42. package/package.json +51 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 sa-ma
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,109 @@
1
+ # jobgrep
2
+
3
+ `jobgrep` is a Node.js CLI for searching recent jobs across Ashby, Greenhouse, Lever, and Workable listings using SerpAPI-backed Google results.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npx jobgrep sources
9
+ ```
10
+
11
+ ```bash
12
+ npm install -g jobgrep
13
+ jobgrep sources
14
+ ```
15
+
16
+ ## Setup
17
+
18
+ `jobgrep` needs a SerpAPI key for `search`.
19
+
20
+ Use an environment variable:
21
+
22
+ ```bash
23
+ export SERPAPI_API_KEY=your_key_here
24
+ jobgrep search --role "frontend engineer"
25
+ ```
26
+
27
+ Or save the key once in the app config directory:
28
+
29
+ ```bash
30
+ jobgrep auth set your_key_here
31
+ jobgrep auth show
32
+ ```
33
+
34
+ Auth precedence:
35
+
36
+ 1. `SERPAPI_API_KEY`
37
+ 2. saved config from `jobgrep auth set`
38
+
39
+ ## Quick Start
40
+
41
+ ```bash
42
+ jobgrep search --role "frontend engineer" --location London --date 7d
43
+ jobgrep search --role "product designer" --json
44
+ jobgrep search --role "backend engineer" --csv --pages 3
45
+ jobgrep sources
46
+ ```
47
+
48
+ ## Commands
49
+
50
+ ### `jobgrep search`
51
+
52
+ Search recent job listings across supported boards.
53
+
54
+ Options:
55
+
56
+ - `--role <role>`: required role or title query
57
+ - `--level <level>`: `junior`, `mid`, or `senior`
58
+ - `--location <location>`: location substring match
59
+ - `--date <range>`: `24h`, `7d`, or `30d`
60
+ - `--limit <count>`: maximum results to return, default `20`
61
+ - `--pages <count>`: number of SerpAPI pages to fetch
62
+ - `--refresh`: bypass SerpAPI cache
63
+ - `--json`: emit JSON
64
+ - `--csv`: emit CSV
65
+
66
+ ### `jobgrep sources`
67
+
68
+ Print supported job board domains and local config path.
69
+
70
+ ### `jobgrep auth set <apiKey>`
71
+
72
+ Save a SerpAPI key in the local app config directory.
73
+
74
+ ### `jobgrep auth show`
75
+
76
+ Show whether `jobgrep` can resolve a key and whether it came from the environment or local config.
77
+
78
+ ### `jobgrep auth clear`
79
+
80
+ Remove the saved local key.
81
+
82
+ ## Output Modes
83
+
84
+ - Default output is a table for terminal use.
85
+ - `--json` emits machine-readable JSON.
86
+ - `--csv` emits CSV with `company,title,location,board,url,postedAt,foundAt`.
87
+
88
+ ## Supported Boards
89
+
90
+ - Ashby: `jobs.ashbyhq.com`
91
+ - Greenhouse: `boards.greenhouse.io`
92
+ - Greenhouse hosted boards: `job-boards.greenhouse.io`
93
+ - Lever: `jobs.lever.co`
94
+ - Workable: `apply.workable.com`
95
+
96
+ ## Notes
97
+
98
+ - Searches use live SerpAPI Google results constrained to supported board domains.
99
+ - One SerpAPI page is fetched by default; use `--pages N` for deeper pagination.
100
+ - `--refresh` forces a fresh SerpAPI request with `no_cache=true`.
101
+ - `sources` does not require authentication.
102
+
103
+ ## Release
104
+
105
+ ```bash
106
+ pnpm test
107
+ npm pack --dry-run
108
+ npm publish
109
+ ```
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import { runCli } from "../src/cli.js";
3
+ const exitCode = await runCli(process.argv.slice(2));
4
+ process.exit(exitCode);
5
+ //# sourceMappingURL=jobgrep.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jobgrep.js","sourceRoot":"","sources":["../../bin/jobgrep.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAEvC,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AACrD,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC"}
@@ -0,0 +1,10 @@
1
+ export type ApiKeySource = "env" | "config";
2
+ export interface ResolvedApiKey {
3
+ value: string;
4
+ source: ApiKeySource;
5
+ }
6
+ export declare function getStoredApiKey(): string | null;
7
+ export declare function setStoredApiKey(apiKey: string): void;
8
+ export declare function clearStoredApiKey(): boolean;
9
+ export declare function resolveApiKey(): ResolvedApiKey;
10
+ export declare function maskApiKey(apiKey: string): string;
@@ -0,0 +1,58 @@
1
+ import { readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { SERP_API_KEY_ENV } from "./config.js";
3
+ import { getApiKeyConfigPath } from "./paths.js";
4
+ export function getStoredApiKey() {
5
+ try {
6
+ const raw = readFileSync(getApiKeyConfigPath(), "utf8");
7
+ const parsed = JSON.parse(raw);
8
+ return typeof parsed.serpApiKey === "string" && parsed.serpApiKey.length > 0 ? parsed.serpApiKey : null;
9
+ }
10
+ catch (error) {
11
+ if (isMissingFileError(error)) {
12
+ return null;
13
+ }
14
+ throw new Error(`Failed to read auth config: ${error instanceof Error ? error.message : String(error)}`);
15
+ }
16
+ }
17
+ export function setStoredApiKey(apiKey) {
18
+ if (apiKey.trim().length === 0) {
19
+ throw new Error("api key must not be empty");
20
+ }
21
+ const config = {
22
+ serpApiKey: apiKey.trim(),
23
+ };
24
+ writeFileSync(getApiKeyConfigPath(), `${JSON.stringify(config, null, 2)}\n`, "utf8");
25
+ }
26
+ export function clearStoredApiKey() {
27
+ try {
28
+ rmSync(getApiKeyConfigPath());
29
+ return true;
30
+ }
31
+ catch (error) {
32
+ if (isMissingFileError(error)) {
33
+ return false;
34
+ }
35
+ throw new Error(`Failed to clear auth config: ${error instanceof Error ? error.message : String(error)}`);
36
+ }
37
+ }
38
+ export function resolveApiKey() {
39
+ const envValue = process.env[SERP_API_KEY_ENV]?.trim();
40
+ if (envValue) {
41
+ return { value: envValue, source: "env" };
42
+ }
43
+ const storedValue = getStoredApiKey();
44
+ if (storedValue) {
45
+ return { value: storedValue, source: "config" };
46
+ }
47
+ throw new Error(`Missing ${SERP_API_KEY_ENV} environment variable or saved API key. Run "jobgrep auth set <api-key>" or export ${SERP_API_KEY_ENV}.`);
48
+ }
49
+ export function maskApiKey(apiKey) {
50
+ if (apiKey.length <= 8) {
51
+ return "*".repeat(apiKey.length);
52
+ }
53
+ return `${apiKey.slice(0, 4)}...${apiKey.slice(-4)}`;
54
+ }
55
+ function isMissingFileError(error) {
56
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
57
+ }
58
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/auth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import { type SearchServiceOptions } from "./search.js";
2
+ export interface CliIo {
3
+ stdout(message: string): void;
4
+ stderr(message: string): void;
5
+ }
6
+ export declare function runCli(argv: string[], io?: CliIo, dependencies?: SearchServiceOptions): Promise<number>;
@@ -0,0 +1,141 @@
1
+ import { Command } from "commander";
2
+ import { clearStoredApiKey, getStoredApiKey, maskApiKey, resolveApiKey, setStoredApiKey } from "./auth.js";
3
+ import { SERP_API_KEY_ENV, SUPPORTED_BOARD_DOMAINS } from "./config.js";
4
+ import { formatCsv, formatJson, formatTable } from "./formatters.js";
5
+ import { getConfigDir } from "./paths.js";
6
+ import { runSearch } from "./search.js";
7
+ function parseLimit(value) {
8
+ const parsed = Number.parseInt(value, 10);
9
+ if (!Number.isInteger(parsed) || parsed <= 0) {
10
+ throw new Error("limit must be a positive integer");
11
+ }
12
+ return parsed;
13
+ }
14
+ function parsePages(value) {
15
+ const parsed = Number.parseInt(value, 10);
16
+ if (!Number.isInteger(parsed) || parsed <= 0) {
17
+ throw new Error("pages must be a positive integer");
18
+ }
19
+ return parsed;
20
+ }
21
+ function validateLevel(value) {
22
+ if (value === "junior" || value === "mid" || value === "senior") {
23
+ return value;
24
+ }
25
+ throw new Error("level must be one of: junior, mid, senior");
26
+ }
27
+ function validateDate(value) {
28
+ if (value === "24h" || value === "7d" || value === "30d") {
29
+ return value;
30
+ }
31
+ throw new Error("date must be one of: 24h, 7d, 30d");
32
+ }
33
+ export async function runCli(argv, io = defaultIo(), dependencies = {}) {
34
+ const program = new Command();
35
+ program.name("jobgrep");
36
+ program.exitOverride();
37
+ program.configureOutput({
38
+ writeOut: (message) => io.stdout(message),
39
+ writeErr: (message) => io.stderr(message),
40
+ });
41
+ program
42
+ .command("search")
43
+ .requiredOption("--role <role>", "role or title query")
44
+ .option("--level <level>", "junior | mid | senior", validateLevel)
45
+ .option("--location <location>", "location substring match")
46
+ .option("--date <range>", "24h | 7d | 30d", validateDate)
47
+ .option("--limit <count>", "maximum results", parseLimit, 20)
48
+ .option("--pages <count>", "number of SerpAPI pages to fetch", parsePages, 1)
49
+ .option("--refresh", "bypass cached results and hit SerpAPI")
50
+ .option("--json", "emit json")
51
+ .option("--csv", "emit csv")
52
+ .action(async (options) => {
53
+ if (options.json && options.csv) {
54
+ throw new Error("choose only one of --json or --csv");
55
+ }
56
+ const records = await runSearch({
57
+ role: options.role,
58
+ level: options.level,
59
+ location: options.location,
60
+ date: options.date,
61
+ limit: options.limit,
62
+ pages: options.pages,
63
+ refresh: options.refresh,
64
+ }, dependencies);
65
+ if (options.json) {
66
+ io.stdout(formatJson(records));
67
+ return;
68
+ }
69
+ if (options.csv) {
70
+ io.stdout(formatCsv(records));
71
+ return;
72
+ }
73
+ io.stdout(`${formatTable(records)}\n`);
74
+ });
75
+ program.command("sources").action(() => {
76
+ const lines = [
77
+ ...SUPPORTED_BOARD_DOMAINS.map((domain) => `domain: ${domain}`),
78
+ `auth: env ${SERP_API_KEY_ENV}`,
79
+ `config_dir: ${getConfigDir()}`,
80
+ ];
81
+ io.stdout(`${lines.join("\n")}\n`);
82
+ });
83
+ const auth = program.command("auth").description("manage the stored SerpAPI key");
84
+ auth
85
+ .command("set")
86
+ .argument("<apiKey>", "SerpAPI API key")
87
+ .action((apiKey) => {
88
+ setStoredApiKey(apiKey);
89
+ io.stdout(`saved api key to ${getConfigDir()}\n`);
90
+ });
91
+ auth.command("show").action(() => {
92
+ const resolved = tryResolveApiKey();
93
+ const stored = getStoredApiKey();
94
+ if (!resolved) {
95
+ io.stdout(`api_key: missing\nconfig_dir: ${getConfigDir()}\n`);
96
+ return;
97
+ }
98
+ const lines = [
99
+ `api_key: ${maskApiKey(resolved.value)}`,
100
+ `source: ${resolved.source === "env" ? SERP_API_KEY_ENV : "config"}`,
101
+ `config_dir: ${getConfigDir()}`,
102
+ `stored_key: ${stored ? "present" : "absent"}`,
103
+ ];
104
+ io.stdout(`${lines.join("\n")}\n`);
105
+ });
106
+ auth.command("clear").action(() => {
107
+ const removed = clearStoredApiKey();
108
+ io.stdout(`${removed ? "cleared" : "no saved"} api key\n`);
109
+ });
110
+ try {
111
+ await program.parseAsync(argv, { from: "user" });
112
+ return 0;
113
+ }
114
+ catch (error) {
115
+ if (error instanceof Error && "code" in error) {
116
+ const code = error.code;
117
+ if (code === "commander.helpDisplayed") {
118
+ return 0;
119
+ }
120
+ io.stderr(`${error.message}\n`);
121
+ return 1;
122
+ }
123
+ io.stderr(`${error instanceof Error ? error.message : String(error)}\n`);
124
+ return 1;
125
+ }
126
+ }
127
+ function defaultIo() {
128
+ return {
129
+ stdout: (message) => process.stdout.write(message),
130
+ stderr: (message) => process.stderr.write(message),
131
+ };
132
+ }
133
+ function tryResolveApiKey() {
134
+ try {
135
+ return resolveApiKey();
136
+ }
137
+ catch {
138
+ return null;
139
+ }
140
+ }
141
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import type { Board } from "./types.js";
2
+ export declare const SUPPORTED_BOARD_DOMAINS: readonly ["jobs.ashbyhq.com", "boards.greenhouse.io", "job-boards.greenhouse.io", "jobs.lever.co", "apply.workable.com"];
3
+ export declare const BOARD_DOMAIN_ALIASES: Array<{
4
+ board: Board;
5
+ domains: string[];
6
+ }>;
7
+ export declare const CACHE_TTL_MS: number;
8
+ export declare const SERP_API_ENDPOINT = "https://serpapi.com/search.json";
9
+ export declare const SERP_API_KEY_ENV = "SERPAPI_API_KEY";
10
+ export declare const SERP_API_PAGE_SIZE = 10;
11
+ export declare const SERP_API_MAX_PAGES = 5;
12
+ export declare function supportedBoards(): Board[];
@@ -0,0 +1,22 @@
1
+ export const SUPPORTED_BOARD_DOMAINS = [
2
+ "jobs.ashbyhq.com",
3
+ "boards.greenhouse.io",
4
+ "job-boards.greenhouse.io",
5
+ "jobs.lever.co",
6
+ "apply.workable.com",
7
+ ];
8
+ export const BOARD_DOMAIN_ALIASES = [
9
+ { board: "ashby", domains: ["jobs.ashbyhq.com"] },
10
+ { board: "greenhouse", domains: ["boards.greenhouse.io", "job-boards.greenhouse.io"] },
11
+ { board: "lever", domains: ["jobs.lever.co"] },
12
+ { board: "workable", domains: ["apply.workable.com"] },
13
+ ];
14
+ export const CACHE_TTL_MS = 30 * 60 * 1000;
15
+ export const SERP_API_ENDPOINT = "https://serpapi.com/search.json";
16
+ export const SERP_API_KEY_ENV = "SERPAPI_API_KEY";
17
+ export const SERP_API_PAGE_SIZE = 10;
18
+ export const SERP_API_MAX_PAGES = 5;
19
+ export function supportedBoards() {
20
+ return ["ashby", "greenhouse", "lever", "workable"];
21
+ }
22
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/config.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,uBAAuB,GAAG;IACrC,kBAAkB;IAClB,sBAAsB;IACtB,0BAA0B;IAC1B,eAAe;IACf,oBAAoB;CACZ,CAAC;AAEX,MAAM,CAAC,MAAM,oBAAoB,GAA+C;IAC9E,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,kBAAkB,CAAC,EAAE;IACjD,EAAE,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,CAAC,sBAAsB,EAAE,0BAA0B,CAAC,EAAE;IACtF,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,eAAe,CAAC,EAAE;IAC9C,EAAE,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC,oBAAoB,CAAC,EAAE;CACvD,CAAC;AAEF,MAAM,CAAC,MAAM,YAAY,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAC3C,MAAM,CAAC,MAAM,iBAAiB,GAAG,iCAAiC,CAAC;AACnE,MAAM,CAAC,MAAM,gBAAgB,GAAG,iBAAiB,CAAC;AAClD,MAAM,CAAC,MAAM,kBAAkB,GAAG,EAAE,CAAC;AACrC,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC;AAEpC,MAAM,UAAU,eAAe;IAC7B,OAAO,CAAC,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC;AACtD,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { OutputRecord } from "./types.js";
2
+ export declare function formatTable(records: OutputRecord[]): string;
3
+ export declare function formatJson(records: OutputRecord[]): string;
4
+ export declare function formatCsv(records: OutputRecord[]): string;
@@ -0,0 +1,44 @@
1
+ export function formatTable(records) {
2
+ if (records.length === 0) {
3
+ return "No matching jobs found.";
4
+ }
5
+ return records
6
+ .map((record, index) => {
7
+ const lines = [
8
+ `${index + 1}. ${record.company}`,
9
+ ` ${record.title}`,
10
+ ` ${record.board}${record.location ? ` | ${record.location}` : ""}`,
11
+ ` ${record.url}`,
12
+ ];
13
+ if (record.postedAt) {
14
+ lines.splice(3, 0, ` posted ${record.postedAt}`);
15
+ }
16
+ return lines.join("\n");
17
+ })
18
+ .join("\n\n");
19
+ }
20
+ export function formatJson(records) {
21
+ return `${JSON.stringify(records, null, 2)}\n`;
22
+ }
23
+ function escapeCsvField(value) {
24
+ if (/[",\n]/.test(value)) {
25
+ return `"${value.replaceAll('"', '""')}"`;
26
+ }
27
+ return value;
28
+ }
29
+ export function formatCsv(records) {
30
+ const header = ["company", "title", "location", "board", "url", "postedAt", "foundAt"];
31
+ const rows = records.map((record) => [
32
+ record.company,
33
+ record.title,
34
+ record.location ?? "",
35
+ record.board,
36
+ record.url,
37
+ record.postedAt ?? "",
38
+ record.foundAt,
39
+ ]
40
+ .map((value) => escapeCsvField(String(value)))
41
+ .join(","));
42
+ return `${[header.join(","), ...rows].join("\n")}\n`;
43
+ }
44
+ //# sourceMappingURL=formatters.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"formatters.js","sourceRoot":"","sources":["../../src/formatters.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,WAAW,CAAC,OAAuB;IACjD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,yBAAyB,CAAC;IACnC,CAAC;IAED,OAAO,OAAO;SACX,GAAG,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE;QACrB,MAAM,KAAK,GAAG;YACZ,GAAG,KAAK,GAAG,CAAC,KAAK,MAAM,CAAC,OAAO,EAAE;YACjC,MAAM,MAAM,CAAC,KAAK,EAAE;YACpB,MAAM,MAAM,CAAC,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE;YACrE,MAAM,MAAM,CAAC,GAAG,EAAE;SACnB,CAAC;QACF,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;YACpB,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,aAAa,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC;QACrD,CAAC;QACD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC,CAAC;SACD,IAAI,CAAC,MAAM,CAAC,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,OAAuB;IAChD,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC;AACjD,CAAC;AAED,SAAS,cAAc,CAAC,KAAa;IACnC,IAAI,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,IAAI,KAAK,CAAC,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC;IAC5C,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,OAAuB;IAC/C,MAAM,MAAM,GAAG,CAAC,SAAS,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,SAAS,CAAC,CAAC;IACvF,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAClC;QACE,MAAM,CAAC,OAAO;QACd,MAAM,CAAC,KAAK;QACZ,MAAM,CAAC,QAAQ,IAAI,EAAE;QACrB,MAAM,CAAC,KAAK;QACZ,MAAM,CAAC,GAAG;QACV,MAAM,CAAC,QAAQ,IAAI,EAAE;QACrB,MAAM,CAAC,OAAO;KACf;SACE,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,cAAc,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;SAC7C,IAAI,CAAC,GAAG,CAAC,CACb,CAAC;IAEF,OAAO,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;AACvD,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare function fetchText(url: string, init?: RequestInit): Promise<string>;
2
+ export declare function fetchJson<T>(url: string, init?: RequestInit): Promise<T>;
@@ -0,0 +1,18 @@
1
+ export async function fetchText(url, init) {
2
+ const response = await fetch(url, {
3
+ ...init,
4
+ headers: {
5
+ "user-agent": "jobgrep/0.1.0",
6
+ ...(init?.headers ?? {}),
7
+ },
8
+ });
9
+ if (!response.ok) {
10
+ throw new Error(`Request failed with status ${response.status} for ${url}`);
11
+ }
12
+ return response.text();
13
+ }
14
+ export async function fetchJson(url, init) {
15
+ const text = await fetchText(url, init);
16
+ return JSON.parse(text);
17
+ }
18
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.js","sourceRoot":"","sources":["../../src/http.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,GAAW,EAAE,IAAkB;IAC7D,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;QAChC,GAAG,IAAI;QACP,OAAO,EAAE;YACP,YAAY,EAAE,eAAe;YAC7B,GAAG,CAAC,IAAI,EAAE,OAAO,IAAI,EAAE,CAAC;SACzB;KACF,CAAC,CAAC;IAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CAAC,8BAA8B,QAAQ,CAAC,MAAM,QAAQ,GAAG,EAAE,CAAC,CAAC;IAC9E,CAAC;IAED,OAAO,QAAQ,CAAC,IAAI,EAAE,CAAC;AACzB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,SAAS,CAAI,GAAW,EAAE,IAAkB;IAChE,MAAM,IAAI,GAAG,MAAM,SAAS,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IACxC,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAM,CAAC;AAC/B,CAAC"}
@@ -0,0 +1 @@
1
+ export { runCli } from "./cli.js";
@@ -0,0 +1,2 @@
1
+ export { runCli } from "./cli.js";
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC"}
@@ -0,0 +1,26 @@
1
+ import type { Board, DateRange, Level, NormalizedJobHit, OrganicSearchHit, OutputRecord } from "./types.js";
2
+ export declare function normalizeWhitespace(value: string): string;
3
+ export declare function normalizeText(value: string): string;
4
+ export declare function normalizeUrl(value: string): string;
5
+ export declare function buildDedupeKey(company: string, title: string, url: string): string;
6
+ export declare function normalizeDate(value: string | number | Date | null | undefined): string | null;
7
+ export declare function expandRole(role: string): string[];
8
+ export declare function expandLevel(level?: Level): string[];
9
+ export declare function recencyDays(range?: DateRange): number | null;
10
+ export declare function toOutputRecord(job: NormalizedJobHit): OutputRecord;
11
+ export declare function detectBoardFromUrl(url: string): Board | null;
12
+ export declare function extractCompanyFromUrl(url: string): string | null;
13
+ export declare function parsePostedDate(value: string | null | undefined, now?: Date): string | null;
14
+ export declare function parseLocationHint(snippet: string | undefined, requestedLocation?: string): string | null;
15
+ export declare function splitTitleAndCompany(rawTitle: string, fallbackCompany: string | null): {
16
+ title: string;
17
+ company: string;
18
+ };
19
+ export declare function isSupportedJobUrl(url: string): boolean;
20
+ export declare function normalizeOrganicHit(hit: OrganicSearchHit, options: {
21
+ searchedAt: string;
22
+ requestedLocation?: string;
23
+ now?: Date;
24
+ }): NormalizedJobHit | null;
25
+ export declare function filterAndDeduplicateResults(results: NormalizedJobHit[], limit: number): NormalizedJobHit[];
26
+ export declare function filterByDateRange(results: NormalizedJobHit[], range?: DateRange, now?: Date): NormalizedJobHit[];
@@ -0,0 +1,287 @@
1
+ import { createHash } from "node:crypto";
2
+ import { BOARD_DOMAIN_ALIASES } from "./config.js";
3
+ const ROLE_SYNONYMS = {
4
+ "frontend engineer": ["frontend engineer", "frontend developer"],
5
+ "backend engineer": ["backend engineer", "backend developer"],
6
+ "full stack engineer": ["full stack engineer", "fullstack engineer", "full stack developer"],
7
+ };
8
+ const LEVEL_SYNONYMS = {
9
+ senior: ["senior", "staff", "lead"],
10
+ mid: ["mid", "intermediate"],
11
+ junior: ["junior", "graduate", "entry level"],
12
+ };
13
+ export function normalizeWhitespace(value) {
14
+ return value.replace(/\s+/g, " ").trim();
15
+ }
16
+ export function normalizeText(value) {
17
+ return normalizeWhitespace(value).toLowerCase();
18
+ }
19
+ export function normalizeUrl(value) {
20
+ try {
21
+ const url = new URL(value);
22
+ url.hash = "";
23
+ for (const key of [...url.searchParams.keys()]) {
24
+ if (key.toLowerCase().startsWith("utm_")) {
25
+ url.searchParams.delete(key);
26
+ }
27
+ }
28
+ if ((url.protocol === "https:" && url.port === "443") || (url.protocol === "http:" && url.port === "80")) {
29
+ url.port = "";
30
+ }
31
+ url.pathname = url.pathname.replace(/\/+$/, "") || "/";
32
+ return url.toString();
33
+ }
34
+ catch {
35
+ return value.trim();
36
+ }
37
+ }
38
+ export function buildDedupeKey(company, title, url) {
39
+ const payload = [normalizeText(company), normalizeText(title), normalizeUrl(url)].join("|");
40
+ return createHash("sha256").update(payload).digest("hex");
41
+ }
42
+ export function normalizeDate(value) {
43
+ if (!value) {
44
+ return null;
45
+ }
46
+ const date = value instanceof Date ? value : new Date(value);
47
+ if (Number.isNaN(date.getTime())) {
48
+ return null;
49
+ }
50
+ return date.toISOString();
51
+ }
52
+ export function expandRole(role) {
53
+ const normalized = normalizeText(role);
54
+ return Array.from(new Set(ROLE_SYNONYMS[normalized] ?? [normalized]));
55
+ }
56
+ export function expandLevel(level) {
57
+ if (!level) {
58
+ return [];
59
+ }
60
+ return LEVEL_SYNONYMS[level];
61
+ }
62
+ export function recencyDays(range) {
63
+ if (!range) {
64
+ return null;
65
+ }
66
+ switch (range) {
67
+ case "24h":
68
+ return 1;
69
+ case "7d":
70
+ return 7;
71
+ case "30d":
72
+ return 30;
73
+ }
74
+ }
75
+ export function toOutputRecord(job) {
76
+ return {
77
+ company: job.company,
78
+ title: job.title,
79
+ location: job.location,
80
+ board: job.board,
81
+ url: job.url,
82
+ postedAt: job.postedAt,
83
+ foundAt: job.foundAt,
84
+ };
85
+ }
86
+ export function detectBoardFromUrl(url) {
87
+ try {
88
+ const host = new URL(url).hostname.toLowerCase();
89
+ for (const entry of BOARD_DOMAIN_ALIASES) {
90
+ if (entry.domains.some((domain) => host === domain || host.endsWith(`.${domain}`))) {
91
+ return entry.board;
92
+ }
93
+ }
94
+ return null;
95
+ }
96
+ catch {
97
+ return null;
98
+ }
99
+ }
100
+ export function extractCompanyFromUrl(url) {
101
+ try {
102
+ const parsed = new URL(url);
103
+ const segments = parsed.pathname.split("/").filter(Boolean);
104
+ if (parsed.hostname.includes("lever.co") || parsed.hostname.includes("ashbyhq.com")) {
105
+ return segments[0] ? titleCase(segments[0].replace(/[-_]+/g, " ")) : null;
106
+ }
107
+ if (parsed.hostname.includes("greenhouse.io")) {
108
+ return segments[0] ? titleCase(segments[0].replace(/[-_]+/g, " ")) : null;
109
+ }
110
+ if (parsed.hostname.includes("workable.com")) {
111
+ if (segments[0] === "j") {
112
+ return null;
113
+ }
114
+ return segments[0] ? titleCase(segments[0].replace(/[-_]+/g, " ")) : null;
115
+ }
116
+ return null;
117
+ }
118
+ catch {
119
+ return null;
120
+ }
121
+ }
122
+ export function parsePostedDate(value, now = new Date()) {
123
+ if (!value) {
124
+ return null;
125
+ }
126
+ const direct = normalizeDate(value);
127
+ if (direct) {
128
+ return direct;
129
+ }
130
+ if (/\btoday\b/i.test(value)) {
131
+ return now.toISOString();
132
+ }
133
+ if (/\byesterday\b/i.test(value)) {
134
+ return new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString();
135
+ }
136
+ const relative = value.match(/(\d+)\s+(hour|day|week|month)s?\s+ago/i);
137
+ if (relative) {
138
+ const amount = Number.parseInt(relative[1], 10);
139
+ const unit = relative[2].toLowerCase();
140
+ const date = new Date(now);
141
+ const multiplier = unit === "hour" ? 60 * 60 * 1000 : unit === "day" ? 24 * 60 * 60 * 1000 : unit === "week" ? 7 * 24 * 60 * 60 * 1000 : 30 * 24 * 60 * 60 * 1000;
142
+ date.setTime(date.getTime() - amount * multiplier);
143
+ return date.toISOString();
144
+ }
145
+ return null;
146
+ }
147
+ export function parseLocationHint(snippet, requestedLocation) {
148
+ if (!snippet) {
149
+ return requestedLocation ? titleCase(requestedLocation) : null;
150
+ }
151
+ if (requestedLocation && normalizeText(snippet).includes(normalizeText(requestedLocation))) {
152
+ return titleCase(requestedLocation);
153
+ }
154
+ const match = snippet.match(/\b(remote|hybrid|onsite|on-site|london|new york|san francisco|berlin|amsterdam|paris|dublin|king's cross)\b/i);
155
+ return match ? titleCase(match[1].replace("on-site", "onsite")) : null;
156
+ }
157
+ export function splitTitleAndCompany(rawTitle, fallbackCompany) {
158
+ const trimmed = normalizeWhitespace(rawTitle);
159
+ const separators = [" @ ", " | ", " - ", " — ", " – ", " at "];
160
+ for (const separator of separators) {
161
+ if (trimmed.includes(separator)) {
162
+ const parts = trimmed.split(separator).map(normalizeWhitespace).filter(Boolean);
163
+ if (parts.length >= 2) {
164
+ return {
165
+ title: parts[0],
166
+ company: parts[parts.length - 1],
167
+ };
168
+ }
169
+ }
170
+ }
171
+ return {
172
+ title: trimmed,
173
+ company: fallbackCompany ?? "Unknown company",
174
+ };
175
+ }
176
+ export function isSupportedJobUrl(url) {
177
+ try {
178
+ const parsed = new URL(url);
179
+ const host = parsed.hostname.toLowerCase();
180
+ const segments = parsed.pathname.split("/").filter(Boolean);
181
+ if (host.includes("ashbyhq.com")) {
182
+ if (segments.length < 2) {
183
+ return false;
184
+ }
185
+ if (segments.length === 2 && !looksLikeAshbyJobId(segments[1])) {
186
+ return false;
187
+ }
188
+ if (segments.length >= 2 && looksLikeAshbyJobId(segments[1])) {
189
+ return true;
190
+ }
191
+ return segments.length >= 3 && looksLikeAshbyJobId(segments[1]);
192
+ }
193
+ if (host.includes("lever.co")) {
194
+ return (segments.length === 2 &&
195
+ segments[0].length > 0 &&
196
+ segments[1].length >= 8 &&
197
+ !segments[1].includes("."));
198
+ }
199
+ if (host.includes("workable.com")) {
200
+ return segments[0] === "j" && segments.length >= 2;
201
+ }
202
+ if (host.includes("greenhouse.io")) {
203
+ const jobsIndex = segments.findIndex((segment) => segment === "jobs");
204
+ return jobsIndex >= 0 && segments.length > jobsIndex + 1;
205
+ }
206
+ return false;
207
+ }
208
+ catch {
209
+ return false;
210
+ }
211
+ }
212
+ export function normalizeOrganicHit(hit, options) {
213
+ if (!hit.link || !hit.title) {
214
+ return null;
215
+ }
216
+ if (!isSupportedJobUrl(hit.link)) {
217
+ return null;
218
+ }
219
+ const board = detectBoardFromUrl(hit.link);
220
+ if (!board) {
221
+ return null;
222
+ }
223
+ const normalizedUrl = normalizeUrl(hit.link);
224
+ const fallbackCompany = extractCompanyFromUrl(normalizedUrl);
225
+ const parsed = splitTitleAndCompany(hit.title, fallbackCompany);
226
+ if (!looksLikeJobTitle(parsed.title)) {
227
+ return null;
228
+ }
229
+ const postedAt = parsePostedDate(hit.date ?? hit.snippet, options.now);
230
+ const location = parseLocationHint(hit.snippet, options.requestedLocation);
231
+ const dedupeKey = buildDedupeKey(parsed.company, parsed.title, normalizedUrl);
232
+ return {
233
+ company: parsed.company,
234
+ title: parsed.title,
235
+ location,
236
+ board,
237
+ url: normalizedUrl,
238
+ postedAt,
239
+ foundAt: options.searchedAt,
240
+ normalizedUrl,
241
+ dedupeKey,
242
+ };
243
+ }
244
+ export function filterAndDeduplicateResults(results, limit) {
245
+ const byUrl = new Set();
246
+ const output = [];
247
+ for (const result of results) {
248
+ if (byUrl.has(result.normalizedUrl)) {
249
+ continue;
250
+ }
251
+ byUrl.add(result.normalizedUrl);
252
+ output.push(result);
253
+ if (output.length >= limit) {
254
+ break;
255
+ }
256
+ }
257
+ return output;
258
+ }
259
+ export function filterByDateRange(results, range, now = new Date()) {
260
+ const days = recencyDays(range);
261
+ if (days === null) {
262
+ return results;
263
+ }
264
+ const cutoff = now.getTime() - days * 24 * 60 * 60 * 1000;
265
+ return results.filter((result) => {
266
+ if (!result.postedAt) {
267
+ return false;
268
+ }
269
+ const effective = normalizeDate(result.postedAt);
270
+ return effective ? new Date(effective).getTime() >= cutoff : false;
271
+ });
272
+ }
273
+ function titleCase(value) {
274
+ return value
275
+ .split(/\s+/)
276
+ .filter(Boolean)
277
+ .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
278
+ .join(" ");
279
+ }
280
+ function looksLikeAshbyJobId(value) {
281
+ return /^[a-z0-9-]{8,}$/i.test(value);
282
+ }
283
+ function looksLikeJobTitle(value) {
284
+ const normalized = normalizeText(value);
285
+ return /(engineer|developer|designer|manager|scientist|analyst|architect|product|qa|devops)/.test(normalized);
286
+ }
287
+ //# sourceMappingURL=normalize.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalize.js","sourceRoot":"","sources":["../../src/normalize.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export declare function getDataDir(): string;
2
+ export declare function getConfigDir(): string;
3
+ export declare function getApiKeyConfigPath(): string;
@@ -0,0 +1,20 @@
1
+ import { mkdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import envPaths from "env-paths";
4
+ function ensure(path) {
5
+ mkdirSync(path, { recursive: true });
6
+ return path;
7
+ }
8
+ function getAppPaths() {
9
+ return envPaths("jobgrep", { suffix: "" });
10
+ }
11
+ export function getDataDir() {
12
+ return ensure(process.env.JOBGREP_DATA_DIR ?? getAppPaths().data);
13
+ }
14
+ export function getConfigDir() {
15
+ return ensure(process.env.JOBGREP_CONFIG_DIR ?? getAppPaths().config);
16
+ }
17
+ export function getApiKeyConfigPath() {
18
+ return join(getConfigDir(), "auth.json");
19
+ }
20
+ //# sourceMappingURL=paths.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/paths.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,QAAQ,MAAM,WAAW,CAAC;AAEjC,SAAS,MAAM,CAAC,IAAY;IAC1B,SAAS,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACrC,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,WAAW;IAClB,OAAO,QAAQ,CAAC,SAAS,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,CAAC;AAC7C,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,OAAO,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,WAAW,EAAE,CAAC,IAAI,CAAC,CAAC;AACpE,CAAC;AAED,MAAM,UAAU,YAAY;IAC1B,OAAO,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,CAAC;AACxE,CAAC;AAED,MAAM,UAAU,mBAAmB;IACjC,OAAO,IAAI,CAAC,YAAY,EAAE,EAAE,WAAW,CAAC,CAAC;AAC3C,CAAC"}
@@ -0,0 +1,9 @@
1
+ import type { SearchProvider, SearchProviderResult, SearchQuery } from "../types.js";
2
+ export declare class SerpApiSearchProvider implements SearchProvider {
3
+ private readonly apiKey;
4
+ constructor(apiKey?: string | undefined);
5
+ search(query: SearchQuery, options?: {
6
+ refresh?: boolean;
7
+ pages?: number;
8
+ }): Promise<SearchProviderResult>;
9
+ }
@@ -0,0 +1,60 @@
1
+ import { SERP_API_ENDPOINT, SERP_API_KEY_ENV, SERP_API_MAX_PAGES, SERP_API_PAGE_SIZE } from "../config.js";
2
+ import { fetchJson } from "../http.js";
3
+ import { toGoogleTbs } from "../query.js";
4
+ export class SerpApiSearchProvider {
5
+ apiKey;
6
+ constructor(apiKey = process.env[SERP_API_KEY_ENV]) {
7
+ if (!apiKey) {
8
+ throw new Error(`Missing ${SERP_API_KEY_ENV} environment variable`);
9
+ }
10
+ this.apiKey = apiKey;
11
+ }
12
+ async search(query, options) {
13
+ const pageCount = Math.max(1, Math.min(options?.pages ?? inferPageCount(query.limit), SERP_API_MAX_PAGES));
14
+ const responses = [];
15
+ const hits = [];
16
+ let hasNextPage = true;
17
+ for (let page = 0; page < pageCount && hasNextPage; page += 1) {
18
+ const params = new URLSearchParams({
19
+ engine: "google",
20
+ api_key: this.apiKey,
21
+ q: query.q,
22
+ google_domain: "google.com",
23
+ num: String(SERP_API_PAGE_SIZE),
24
+ start: String(page * SERP_API_PAGE_SIZE),
25
+ });
26
+ if (query.location) {
27
+ params.set("location", query.location);
28
+ }
29
+ const tbs = toGoogleTbs(query.date);
30
+ if (tbs) {
31
+ params.set("tbs", tbs);
32
+ }
33
+ if (options?.refresh) {
34
+ params.set("no_cache", "true");
35
+ }
36
+ const response = await fetchJson(`${SERP_API_ENDPOINT}?${params.toString()}`);
37
+ if (response.error) {
38
+ throw new Error(`SerpAPI error: ${response.error}`);
39
+ }
40
+ responses.push(response);
41
+ hits.push(...(Array.isArray(response.organic_results) ? response.organic_results : []));
42
+ hasNextPage = Boolean(response.serpapi_pagination?.next);
43
+ if (!response.organic_results || response.organic_results.length < SERP_API_PAGE_SIZE) {
44
+ break;
45
+ }
46
+ if (!hasNextPage) {
47
+ break;
48
+ }
49
+ }
50
+ return {
51
+ searchedAt: new Date().toISOString(),
52
+ rawResponse: responses,
53
+ hits,
54
+ };
55
+ }
56
+ }
57
+ function inferPageCount(limit) {
58
+ return Math.max(2, Math.ceil(limit / SERP_API_PAGE_SIZE) + 2);
59
+ }
60
+ //# sourceMappingURL=serpapi.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serpapi.js","sourceRoot":"","sources":["../../../src/provider/serpapi.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ import type { DateRange, SearchOptions, SearchQuery } from "./types.js";
2
+ export declare function buildSearchQuery(options: SearchOptions): SearchQuery;
3
+ export declare function buildSiteGroup(domains: readonly string[]): string;
4
+ export declare function buildOrGroup(values: string[], quote: boolean): string;
5
+ export declare function toGoogleTbs(date?: DateRange): string | null;
@@ -0,0 +1,42 @@
1
+ import { SUPPORTED_BOARD_DOMAINS } from "./config.js";
2
+ import { expandLevel, expandRole } from "./normalize.js";
3
+ export function buildSearchQuery(options) {
4
+ const roleTerms = expandRole(options.role);
5
+ const levelTerms = expandLevel(options.level);
6
+ const domains = [...SUPPORTED_BOARD_DOMAINS];
7
+ const groups = [
8
+ buildOrGroup(roleTerms, true),
9
+ levelTerms.length > 0 ? buildOrGroup(levelTerms, true) : null,
10
+ options.location ? `"${options.location}"` : null,
11
+ buildSiteGroup(domains),
12
+ ].filter(Boolean);
13
+ return {
14
+ q: groups.join("\n"),
15
+ roleTerms,
16
+ levelTerms,
17
+ domains,
18
+ location: options.location,
19
+ date: options.date,
20
+ limit: options.limit,
21
+ };
22
+ }
23
+ export function buildSiteGroup(domains) {
24
+ return `(${domains.map((domain) => `site:${domain}`).join(" OR ")})`;
25
+ }
26
+ export function buildOrGroup(values, quote) {
27
+ const normalized = values.map((value) => (quote ? `"${value}"` : value));
28
+ return `(${normalized.join(" OR ")})`;
29
+ }
30
+ export function toGoogleTbs(date) {
31
+ switch (date) {
32
+ case "24h":
33
+ return "qdr:d";
34
+ case "7d":
35
+ return "qdr:w";
36
+ case "30d":
37
+ return "qdr:m";
38
+ default:
39
+ return null;
40
+ }
41
+ }
42
+ //# sourceMappingURL=query.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query.js","sourceRoot":"","sources":["../../src/query.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAGzD,MAAM,UAAU,gBAAgB,CAAC,OAAsB;IACrD,MAAM,SAAS,GAAG,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3C,MAAM,UAAU,GAAG,WAAW,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAC9C,MAAM,OAAO,GAAG,CAAC,GAAG,uBAAuB,CAAC,CAAC;IAE7C,MAAM,MAAM,GAAG;QACb,YAAY,CAAC,SAAS,EAAE,IAAI,CAAC;QAC7B,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI;QAC7D,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,IAAI;QACjD,cAAc,CAAC,OAAO,CAAC;KACxB,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAElB,OAAO;QACL,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC;QACpB,SAAS;QACT,UAAU;QACV,OAAO;QACP,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,KAAK,EAAE,OAAO,CAAC,KAAK;KACrB,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,OAA0B;IACvD,OAAO,IAAI,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,QAAQ,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC;AACvE,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,MAAgB,EAAE,KAAc;IAC3D,MAAM,UAAU,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;IACzE,OAAO,IAAI,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC;AACxC,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,IAAgB;IAC1C,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,KAAK;YACR,OAAO,OAAO,CAAC;QACjB,KAAK,IAAI;YACP,OAAO,OAAO,CAAC;QACjB,KAAK,KAAK;YACR,OAAO,OAAO,CAAC;QACjB;YACE,OAAO,IAAI,CAAC;IAChB,CAAC;AACH,CAAC"}
@@ -0,0 +1,7 @@
1
+ import type { JobResult, SearchOptions, SearchProvider } from "./types.js";
2
+ export interface SearchServiceOptions {
3
+ provider?: SearchProvider;
4
+ providerFactory?: (apiKey: string) => SearchProvider;
5
+ now?: Date;
6
+ }
7
+ export declare function runSearch(options: SearchOptions, dependencies?: SearchServiceOptions): Promise<JobResult[]>;
@@ -0,0 +1,27 @@
1
+ import { resolveApiKey } from "./auth.js";
2
+ import { filterAndDeduplicateResults, filterByDateRange, normalizeOrganicHit, toOutputRecord, } from "./normalize.js";
3
+ import { SerpApiSearchProvider } from "./provider/serpapi.js";
4
+ import { buildSearchQuery } from "./query.js";
5
+ export async function runSearch(options, dependencies = {}) {
6
+ const provider = dependencies.provider ??
7
+ (dependencies.providerFactory
8
+ ? dependencies.providerFactory(resolveApiKey().value)
9
+ : new SerpApiSearchProvider(resolveApiKey().value));
10
+ const now = dependencies.now ?? new Date();
11
+ const query = buildSearchQuery(options);
12
+ const providerResult = await provider.search(query, {
13
+ refresh: options.refresh,
14
+ pages: options.pages ?? 1,
15
+ });
16
+ const normalized = providerResult.hits
17
+ .map((hit) => normalizeOrganicHit(hit, {
18
+ searchedAt: providerResult.searchedAt,
19
+ requestedLocation: options.location,
20
+ now,
21
+ }))
22
+ .filter((result) => result !== null);
23
+ const filtered = filterByDateRange(normalized, options.date, now);
24
+ const deduped = filterAndDeduplicateResults(filtered, options.limit);
25
+ return deduped.map(toOutputRecord);
26
+ }
27
+ //# sourceMappingURL=search.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search.js","sourceRoot":"","sources":["../../src/search.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EACL,2BAA2B,EAC3B,iBAAiB,EACjB,mBAAmB,EACnB,cAAc,GACf,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAS9C,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,OAAsB,EAAE,eAAqC,EAAE;IAC7F,MAAM,QAAQ,GACZ,YAAY,CAAC,QAAQ;QACrB,CAAC,YAAY,CAAC,eAAe;YAC3B,CAAC,CAAC,YAAY,CAAC,eAAe,CAAC,aAAa,EAAE,CAAC,KAAK,CAAC;YACrD,CAAC,CAAC,IAAI,qBAAqB,CAAC,aAAa,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;IACxD,MAAM,GAAG,GAAG,YAAY,CAAC,GAAG,IAAI,IAAI,IAAI,EAAE,CAAC;IAC3C,MAAM,KAAK,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;IACxC,MAAM,cAAc,GAAG,MAAM,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE;QAClD,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;KAC1B,CAAC,CAAC;IACH,MAAM,UAAU,GAAG,cAAc,CAAC,IAAI;SACnC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CACX,mBAAmB,CAAC,GAAG,EAAE;QACvB,UAAU,EAAE,cAAc,CAAC,UAAU;QACrC,iBAAiB,EAAE,OAAO,CAAC,QAAQ;QACnC,GAAG;KACJ,CAAC,CACH;SACA,MAAM,CAAC,CAAC,MAAM,EAAwC,EAAE,CAAC,MAAM,KAAK,IAAI,CAAC,CAAC;IAE7E,MAAM,QAAQ,GAAG,iBAAiB,CAAC,UAAU,EAAE,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IAClE,MAAM,OAAO,GAAG,2BAA2B,CAAC,QAAQ,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IACrE,OAAO,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;AACrC,CAAC"}
@@ -0,0 +1,53 @@
1
+ export type Board = "ashby" | "greenhouse" | "lever" | "workable";
2
+ export type Level = "junior" | "mid" | "senior";
3
+ export type DateRange = "24h" | "7d" | "30d";
4
+ export interface JobResult {
5
+ company: string;
6
+ title: string;
7
+ location: string | null;
8
+ board: Board;
9
+ url: string;
10
+ postedAt: string | null;
11
+ foundAt: string;
12
+ }
13
+ export interface SearchOptions {
14
+ role: string;
15
+ level?: Level;
16
+ location?: string;
17
+ date?: DateRange;
18
+ limit: number;
19
+ pages?: number;
20
+ refresh?: boolean;
21
+ }
22
+ export interface SearchQuery {
23
+ q: string;
24
+ roleTerms: string[];
25
+ levelTerms: string[];
26
+ domains: string[];
27
+ location?: string;
28
+ date?: DateRange;
29
+ limit: number;
30
+ }
31
+ export interface OrganicSearchHit {
32
+ position?: number;
33
+ title?: string;
34
+ link?: string;
35
+ snippet?: string;
36
+ date?: string;
37
+ }
38
+ export interface SearchProviderResult {
39
+ searchedAt: string;
40
+ rawResponse: unknown;
41
+ hits: OrganicSearchHit[];
42
+ }
43
+ export interface SearchProvider {
44
+ search(query: SearchQuery, options?: {
45
+ refresh?: boolean;
46
+ pages?: number;
47
+ }): Promise<SearchProviderResult>;
48
+ }
49
+ export interface NormalizedJobHit extends JobResult {
50
+ normalizedUrl: string;
51
+ dedupeKey: string;
52
+ }
53
+ export type OutputRecord = JobResult;
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "jobgrep",
3
+ "version": "0.1.0",
4
+ "description": "CLI for searching recent jobs across Ashby, Greenhouse, Lever, and Workable via SerpAPI",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "jobgrep": "./dist/bin/jobgrep.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "keywords": [
14
+ "jobs",
15
+ "cli",
16
+ "search",
17
+ "serpapi",
18
+ "greenhouse",
19
+ "lever",
20
+ "ashby",
21
+ "workable"
22
+ ],
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/sa-ma/jobgrep.git"
26
+ },
27
+ "bugs": {
28
+ "url": "https://github.com/sa-ma/jobgrep/issues"
29
+ },
30
+ "homepage": "https://github.com/sa-ma/jobgrep#readme",
31
+ "scripts": {
32
+ "build": "rm -rf dist && tsc -p tsconfig.json",
33
+ "dev": "tsx bin/jobgrep.ts",
34
+ "test": "vitest run tests",
35
+ "prepack": "pnpm build"
36
+ },
37
+ "engines": {
38
+ "node": ">=22"
39
+ },
40
+ "dependencies": {
41
+ "cheerio": "^1.2.0",
42
+ "commander": "^14.0.3",
43
+ "env-paths": "^4.0.0"
44
+ },
45
+ "devDependencies": {
46
+ "@types/node": "^25.5.0",
47
+ "tsx": "^4.21.0",
48
+ "typescript": "^5.9.3",
49
+ "vitest": "^4.1.0"
50
+ }
51
+ }