@jsonresume/jobs 0.7.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.
package/README.md ADDED
@@ -0,0 +1,270 @@
1
+ # @jsonresume/job-search
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@jsonresume/job-search)](https://www.npmjs.com/package/@jsonresume/job-search)
4
+ [![license](https://img.shields.io/npm/l/@jsonresume/job-search)](./LICENSE)
5
+ [![node](https://img.shields.io/node/v/@jsonresume/job-search)](https://nodejs.org)
6
+
7
+ Search Hacker News "Who is Hiring" jobs matched against your [JSON Resume](https://jsonresume.org). Jobs are semantically ranked using AI embeddings — your resume is compared against hundreds of monthly job postings to surface the best fits.
8
+
9
+ ## Quick Start
10
+
11
+ ```bash
12
+ npx @jsonresume/jobs
13
+ ```
14
+
15
+ That's it. The CLI walks you through login on first run — all you need is a resume hosted at [registry.jsonresume.org](https://registry.jsonresume.org).
16
+
17
+ ## Prerequisites
18
+
19
+ - **Node.js** >= 18
20
+ - A JSON Resume on the [JSON Resume Registry](https://registry.jsonresume.org) (free, backed by your GitHub Gist)
21
+ - *(Optional)* `OPENAI_API_KEY` — enables AI summaries and batch ranking in the TUI
22
+
23
+ ## Installation
24
+
25
+ Run directly with npx (no install needed):
26
+
27
+ ```bash
28
+ npx @jsonresume/jobs
29
+ ```
30
+
31
+ Or install globally:
32
+
33
+ ```bash
34
+ npm install -g @jsonresume/job-search
35
+ jsonresume-jobs
36
+ ```
37
+
38
+ ## Authentication
39
+
40
+ On first run, the CLI prompts for your GitHub username, verifies your resume exists on the registry, generates an API key, and saves it to `~/.jsonresume/config.json`. Future runs skip straight to the TUI.
41
+
42
+ You can also authenticate manually:
43
+
44
+ ```bash
45
+ # Via environment variable
46
+ export JSONRESUME_API_KEY=jr_yourname_xxxxx
47
+
48
+ # Generate a key via curl
49
+ curl -s -X POST https://registry.jsonresume.org/api/v1/keys \
50
+ -H 'Content-Type: application/json' \
51
+ -d '{"username":"YOUR_GITHUB_USERNAME"}'
52
+ ```
53
+
54
+ To clear saved credentials:
55
+
56
+ ```bash
57
+ npx @jsonresume/jobs logout
58
+ ```
59
+
60
+ ## Interactive TUI
61
+
62
+ The default command launches a full terminal interface for browsing and managing jobs.
63
+
64
+ ```bash
65
+ npx @jsonresume/jobs
66
+ ```
67
+
68
+ ### Features
69
+
70
+ - **Split-pane detail view** — press Enter to see a compact job list on the left and full details on the right, navigate jobs without leaving the detail panel
71
+ - **Tab-based views** — All / Interested / Applied / Maybe / Passed with live counts
72
+ - **Persistent filters** — remote, salary, keyword, days — saved to disk per search profile
73
+ - **Custom search profiles** — targeted searches like "remote React jobs in climate tech" with AI-powered reranking
74
+ - **Two-pass loading** — results appear instantly from vector search, then reshuffle when LLM reranking completes in the background
75
+ - **Batch operations** — select multiple jobs with `v`, then bulk-mark them all at once
76
+ - **Inline search** — press `n` to quickly filter visible jobs by keyword
77
+ - **Export** — press `e` to export your shortlist to a markdown file
78
+ - **Toast notifications** — instant feedback on every action
79
+ - **Help modal** — press `?` for a full keyboard reference
80
+ - **AI summaries** — per-job summaries and batch ranking (requires `OPENAI_API_KEY`)
81
+ - **Vim-style navigation** — `j`/`k`, `g`/`G`, `Ctrl+U`/`Ctrl+D`, `/` for search profiles, `f` for filters
82
+
83
+ ### Keyboard Shortcuts
84
+
85
+ #### List View
86
+
87
+ | Key | Action |
88
+ |-----|--------|
89
+ | `j` / `↓` | Move down |
90
+ | `k` / `↑` | Move up |
91
+ | `g` / `G` | Jump to first / last job |
92
+ | `Ctrl+U` / `Ctrl+D` | Page up / page down |
93
+ | `Enter` | Open split-pane detail view |
94
+ | `i` | Mark interested |
95
+ | `x` | Mark applied |
96
+ | `m` | Mark maybe |
97
+ | `p` | Mark passed |
98
+ | `v` | Toggle batch selection |
99
+ | `Tab` / `Shift+Tab` | Next / previous tab |
100
+ | `n` | Inline keyword search |
101
+ | `f` | Manage filters |
102
+ | `/` | Search profiles |
103
+ | `Space` | AI summary |
104
+ | `S` | AI batch review |
105
+ | `e` | Export shortlist to markdown |
106
+ | `R` | Force refresh |
107
+ | `?` | Help modal |
108
+ | `q` | Quit |
109
+
110
+ #### Detail View (Split Pane)
111
+
112
+ | Key | Action |
113
+ |-----|--------|
114
+ | `j` / `k` | Navigate jobs (updates detail pane) |
115
+ | `J` / `K` | Scroll detail content |
116
+ | `o` | Open HN post in browser |
117
+ | `i` / `x` / `m` / `p` | Mark job state |
118
+ | `Space` | AI summary |
119
+ | `Esc` / `q` | Back to full list |
120
+
121
+ #### Filters & Search Profiles
122
+
123
+ | Key | Action |
124
+ |-----|--------|
125
+ | `a` | Add filter |
126
+ | `d` | Delete filter / search profile |
127
+ | `n` | New search profile |
128
+ | `Enter` | Select / edit |
129
+ | `Esc` | Close |
130
+
131
+ ## CLI Commands
132
+
133
+ For scripting and pipelines, the CLI also supports direct commands:
134
+
135
+ ```bash
136
+ npx @jsonresume/jobs search # Find matching jobs
137
+ npx @jsonresume/jobs search --remote --min-salary 150 # Remote, $150k+ salary
138
+ npx @jsonresume/jobs search --search "rust" # Keyword filter
139
+ npx @jsonresume/jobs detail 181420 # Full job details
140
+ npx @jsonresume/jobs mark 181420 interested # Mark a job
141
+ npx @jsonresume/jobs me # Your resume summary
142
+ npx @jsonresume/jobs update ./resume.json # Update your resume
143
+ npx @jsonresume/jobs help # All options
144
+ ```
145
+
146
+ ### Command Reference
147
+
148
+ | Command | Description |
149
+ |---------|-------------|
150
+ | *(default)* | Launch interactive TUI |
151
+ | `search` | Find matching jobs (table output) |
152
+ | `detail <id>` | Show full details for a job |
153
+ | `mark <id> <state>` | Set job state: `interested`, `not_interested`, `applied`, `maybe`, `dismissed` |
154
+ | `me` | Show your resume summary |
155
+ | `update <file>` | Upload a new version of your resume |
156
+ | `logout` | Remove saved API key |
157
+ | `help` | Show help |
158
+
159
+ ### Search Options
160
+
161
+ | Flag | Description |
162
+ |------|-------------|
163
+ | `--top N` | Number of results (default: 20, max: 100) |
164
+ | `--days N` | How far back to look (default: 30) |
165
+ | `--remote` | Remote jobs only |
166
+ | `--min-salary N` | Minimum salary in thousands (e.g. `150` = $150k) |
167
+ | `--search TERM` | Keyword filter (title, company, skills) |
168
+ | `--interested` | Show only jobs marked interested |
169
+ | `--applied` | Show only jobs marked applied |
170
+ | `--json` | Output raw JSON for piping |
171
+
172
+ ### Mark States
173
+
174
+ | State | Icon | Meaning |
175
+ |-------|------|---------|
176
+ | `interested` | ⭐ | You want this job |
177
+ | `applied` | 📨 | You've applied |
178
+ | `maybe` | ? | Considering it |
179
+ | `not_interested` | ✗ | Not for you |
180
+ | `dismissed` | 👁 | Hide from results |
181
+
182
+ ## How Ranking Works
183
+
184
+ The system uses a five-stage pipeline to match and rank jobs against your resume.
185
+
186
+ ### Stage 1: Embedding Generation
187
+
188
+ Your JSON Resume is fetched from [registry.jsonresume.org](https://registry.jsonresume.org) and converted to text (label, summary, skills, work history). This text is embedded using OpenAI's `text-embedding-3-large` model into a 3072-dimensional vector.
189
+
190
+ Job postings from HN's monthly "Who is Hiring?" threads are parsed by GPT into structured data (title, company, skills, salary, remote, location) and embedded into the same vector space.
191
+
192
+ ### Stage 2: Vector Similarity Search
193
+
194
+ Your resume embedding is compared against all job embeddings using cosine similarity via [pgvector](https://github.com/pgvector/pgvector). The top ~500 candidates are retrieved in ~200ms. This is purely semantic — it finds jobs that "sound like" your resume. Jobs you've already passed on are excluded server-side so you always get fresh results.
195
+
196
+ ### Stage 3: Custom Search Profiles
197
+
198
+ When you create a search profile (e.g. "remote React roles at climate tech startups"), two techniques boost the prompt's influence:
199
+
200
+ **HyDE (Hypothetical Document Embedding):** Instead of naively blending your prompt into resume text, the system generates a hypothetical ideal job posting matching your preferences. This creates a document-to-document comparison, which is far more effective than query-to-document matching.
201
+
202
+ **Embedding Interpolation:** The resume and HyDE vectors are combined: `0.65 × hyde + 0.35 × resume`. This gives your search intent 65% influence on ranking, versus plain resume matching where the resume dominates ~80% of the signal.
203
+
204
+ ### Stage 4: LLM Reranking
205
+
206
+ For custom searches, the top 30 vector results are re-scored by `gpt-4.1-mini`. Each job receives a 1–10 relevance score considering skill alignment, experience level, location fit, and your stated preferences.
207
+
208
+ The final score blends both signals: `0.4 × vector_score + 0.6 × llm_score`. This lets the LLM override semantic similarity — a job that's a great vector match but contradicts your preferences gets pushed down.
209
+
210
+ In the TUI, this runs as a two-pass load: jobs appear instantly from vector search, then reshuffle when reranking finishes in the background.
211
+
212
+ ### Stage 5: Client-side Filtering
213
+
214
+ After server-side ranking, the TUI applies local filters (remote only, minimum salary, keyword, days). Filters are persisted per search profile, so switching profiles restores each one's filters.
215
+
216
+ ## Claude Code Skill
217
+
218
+ This package includes a [Claude Code skill](https://docs.anthropic.com/en/docs/claude-code/skills) that turns job searching into a guided, AI-assisted experience.
219
+
220
+ ### Install
221
+
222
+ ```bash
223
+ mkdir -p ~/.claude/skills/jsonresume-hunt
224
+ cp node_modules/@jsonresume/job-search/skills/jsonresume-hunt/SKILL.md \
225
+ ~/.claude/skills/jsonresume-hunt/SKILL.md
226
+ ```
227
+
228
+ ### Use
229
+
230
+ In Claude Code, type:
231
+
232
+ ```
233
+ /jsonresume-hunt
234
+ /jsonresume-hunt remote React jobs over $150k
235
+ ```
236
+
237
+ The skill interviews you about what you're looking for, runs multiple searches, researches top companies, does skill gap analysis, collects your decisions, and generates a markdown tracker with your shortlist and outreach drafts.
238
+
239
+ ## Environment Variables
240
+
241
+ | Variable | Required | Description |
242
+ |----------|----------|-------------|
243
+ | `JSONRESUME_API_KEY` | No | API key (auto-generated on first run if not set) |
244
+ | `OPENAI_API_KEY` | No | Enables AI summaries and batch ranking in TUI |
245
+ | `JSONRESUME_BASE_URL` | No | API base URL override (default: `https://registry.jsonresume.org`) |
246
+
247
+ ## Data Storage
248
+
249
+ | Path | Contents |
250
+ |------|----------|
251
+ | `~/.jsonresume/config.json` | API key and username |
252
+ | `~/.jsonresume/filters.json` | Saved filter presets per search profile |
253
+ | `~/.jsonresume/cache/` | Cached job results (auto-expires) |
254
+
255
+ ## Contributing
256
+
257
+ This package is part of the [jsonresume.org](https://github.com/jsonresume/jsonresume.org) monorepo.
258
+
259
+ ```bash
260
+ git clone https://github.com/jsonresume/jsonresume.org.git
261
+ cd jsonresume.org
262
+ pnpm install
263
+ node packages/job-search/bin/cli.js help
264
+ ```
265
+
266
+ See the repo root [CLAUDE.md](../../CLAUDE.md) for code standards and contribution guidelines.
267
+
268
+ ## License
269
+
270
+ [MIT](./LICENSE)
package/bin/cli.js ADDED
@@ -0,0 +1,380 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * @jsonresume/job-search — Search HN jobs matched to your JSON Resume
5
+ *
6
+ * Just run: npx @jsonresume/jobs
7
+ */
8
+
9
+ const VERSION = '0.7.0';
10
+
11
+ const BASE_URL =
12
+ getArg('--base-url') ||
13
+ process.env.JSONRESUME_BASE_URL ||
14
+ 'https://registry.jsonresume.org';
15
+
16
+ // ── Arg parsing ────────────────────────────────────────────
17
+
18
+ function getArg(name) {
19
+ const idx = process.argv.indexOf(name);
20
+ if (idx === -1) return null;
21
+ return process.argv[idx + 1];
22
+ }
23
+
24
+ function hasFlag(name) {
25
+ return process.argv.includes(name);
26
+ }
27
+
28
+ // ── API client ─────────────────────────────────────────────
29
+
30
+ let _apiKey;
31
+
32
+ async function getApiKey() {
33
+ if (_apiKey) return _apiKey;
34
+ const { ensureApiKey } = await import('../src/auth.js');
35
+ _apiKey = await ensureApiKey(BASE_URL);
36
+ return _apiKey;
37
+ }
38
+
39
+ async function api(path, options = {}) {
40
+ const apiKey = await getApiKey();
41
+ const url = `${BASE_URL}/api/v1${path}`;
42
+ const res = await fetch(url, {
43
+ ...options,
44
+ headers: {
45
+ Authorization: `Bearer ${apiKey}`,
46
+ 'Content-Type': 'application/json',
47
+ ...options.headers,
48
+ },
49
+ });
50
+
51
+ const data = await res.json();
52
+ if (!res.ok) {
53
+ console.error(`Error: ${data.error || res.statusText}`);
54
+ process.exit(1);
55
+ }
56
+ return data;
57
+ }
58
+
59
+ // ── Formatters ─────────────────────────────────────────────
60
+
61
+ function formatSalary(salary, salaryUsd) {
62
+ if (salaryUsd) return `$${Math.round(salaryUsd / 1000)}k`;
63
+ if (salary) return salary;
64
+ return '—';
65
+ }
66
+
67
+ function formatLocation(loc, remote) {
68
+ const parts = [];
69
+ if (loc?.city) parts.push(loc.city);
70
+ if (loc?.countryCode) parts.push(loc.countryCode);
71
+ if (remote) parts.push(`(${remote})`);
72
+ return parts.join(', ') || '—';
73
+ }
74
+
75
+ function stateIcon(state) {
76
+ const icons = {
77
+ interested: '⭐',
78
+ applied: '📨',
79
+ not_interested: '✗',
80
+ dismissed: '👁',
81
+ maybe: '?',
82
+ };
83
+ return icons[state] || ' ';
84
+ }
85
+
86
+ // ── Commands ───────────────────────────────────────────────
87
+
88
+ async function cmdSearch() {
89
+ const params = new URLSearchParams();
90
+ params.set('top', getArg('--top') || '20');
91
+ params.set('days', getArg('--days') || '30');
92
+ if (hasFlag('--remote')) params.set('remote', 'true');
93
+ if (getArg('--min-salary')) params.set('min_salary', getArg('--min-salary'));
94
+ if (getArg('--search')) params.set('search', getArg('--search'));
95
+
96
+ const { jobs } = await api(`/jobs?${params}`);
97
+
98
+ let filtered = jobs;
99
+ if (hasFlag('--interested'))
100
+ filtered = jobs.filter((j) => j.state === 'interested');
101
+ if (hasFlag('--applied'))
102
+ filtered = jobs.filter((j) => j.state === 'applied');
103
+
104
+ if (hasFlag('--json')) {
105
+ console.log(JSON.stringify(filtered, null, 2));
106
+ return;
107
+ }
108
+
109
+ if (filtered.length === 0) {
110
+ console.log('No matching jobs found.');
111
+ return;
112
+ }
113
+
114
+ console.log(
115
+ `\n Found ${filtered.length} matching jobs (last ${params.get(
116
+ 'days'
117
+ )} days)\n`
118
+ );
119
+ console.log(
120
+ ' ' +
121
+ 'St'.padEnd(3) +
122
+ 'Score'.padEnd(7) +
123
+ 'ID'.padEnd(8) +
124
+ 'Title'.padEnd(35) +
125
+ 'Company'.padEnd(25) +
126
+ 'Location'.padEnd(25) +
127
+ 'Salary'.padEnd(10)
128
+ );
129
+ console.log(' ' + '─'.repeat(112));
130
+
131
+ for (const j of filtered) {
132
+ const line =
133
+ ' ' +
134
+ stateIcon(j.state).padEnd(3) +
135
+ String(j.similarity).padEnd(7) +
136
+ String(j.id).padEnd(8) +
137
+ (j.title || '').slice(0, 33).padEnd(35) +
138
+ (j.company || '').slice(0, 23).padEnd(25) +
139
+ formatLocation(j.location, j.remote).slice(0, 23).padEnd(25) +
140
+ formatSalary(j.salary, j.salary_usd).padEnd(10);
141
+ console.log(line);
142
+ }
143
+ console.log('');
144
+ }
145
+
146
+ async function cmdDetail() {
147
+ const id = process.argv[3];
148
+ if (!id) {
149
+ console.error('Usage: jsonresume-jobs detail <job_id>');
150
+ process.exit(1);
151
+ }
152
+
153
+ const job = await api(`/jobs/${id}`);
154
+
155
+ if (hasFlag('--json')) {
156
+ console.log(JSON.stringify(job, null, 2));
157
+ return;
158
+ }
159
+
160
+ console.log(`\n${'═'.repeat(70)}`);
161
+ console.log(` ${job.title || 'Unknown'} at ${job.company || 'Unknown'}`);
162
+ console.log(`${'═'.repeat(70)}`);
163
+ console.log(` ID: ${job.id}`);
164
+ console.log(` Location: ${formatLocation(job.location, job.remote)}`);
165
+ console.log(` Salary: ${formatSalary(job.salary, job.salary_usd)}`);
166
+ console.log(` Type: ${job.type || '—'}`);
167
+ console.log(` Experience: ${job.experience || '—'}`);
168
+ console.log(` Posted: ${job.posted_at || '—'}`);
169
+ console.log(
170
+ ` State: ${
171
+ job.state ? `${stateIcon(job.state)} ${job.state}` : 'none'
172
+ }`
173
+ );
174
+ console.log(` HN URL: ${job.url || '—'}`);
175
+
176
+ if (job.description) {
177
+ console.log(`\n Description:\n ${job.description}`);
178
+ }
179
+
180
+ if (job.skills?.length) {
181
+ console.log(`\n Skills: ${job.skills.map((s) => s.name).join(', ')}`);
182
+ }
183
+
184
+ if (job.responsibilities?.length) {
185
+ console.log(`\n Responsibilities:`);
186
+ job.responsibilities.forEach((r) => console.log(` • ${r}`));
187
+ }
188
+
189
+ if (job.qualifications?.length) {
190
+ console.log(`\n Qualifications:`);
191
+ job.qualifications.forEach((q) => console.log(` • ${q}`));
192
+ }
193
+ console.log('');
194
+ }
195
+
196
+ async function cmdMark() {
197
+ const id = process.argv[3];
198
+ const state = process.argv[4];
199
+ const feedback = getArg('--feedback');
200
+
201
+ if (!id || !state) {
202
+ console.error(
203
+ 'Usage: jsonresume-jobs mark <job_id> <interested|not_interested|applied|dismissed|maybe> [--feedback "reason"]'
204
+ );
205
+ process.exit(1);
206
+ }
207
+
208
+ const body = { state };
209
+ if (feedback) body.feedback = feedback;
210
+
211
+ const result = await api(`/jobs/${id}`, {
212
+ method: 'PUT',
213
+ body: JSON.stringify(body),
214
+ });
215
+
216
+ const desc = result.job_title
217
+ ? `${result.job_title} at ${result.job_company}`
218
+ : `#${result.id}`;
219
+ console.log(`${stateIcon(result.state)} Marked ${desc} as ${result.state}`);
220
+ }
221
+
222
+ async function cmdMe() {
223
+ const data = await api('/me');
224
+
225
+ if (hasFlag('--json')) {
226
+ console.log(JSON.stringify(data, null, 2));
227
+ return;
228
+ }
229
+
230
+ const r = data.resume;
231
+ console.log(`\n ${r.basics?.name} (${data.username})`);
232
+ console.log(` ${r.basics?.label || ''}`);
233
+ console.log(
234
+ ` ${r.basics?.location?.city || ''}, ${
235
+ r.basics?.location?.countryCode || ''
236
+ }`
237
+ );
238
+ console.log(`\n Skills: ${(r.skills || []).map((s) => s.name).join(', ')}`);
239
+ console.log(` Work: ${(r.work || []).length} entries`);
240
+ console.log(` Projects: ${(r.projects || []).length} entries`);
241
+ console.log('');
242
+ }
243
+
244
+ async function cmdUpdate() {
245
+ const filePath = process.argv[3];
246
+ if (!filePath) {
247
+ console.error('Usage: jsonresume-jobs update <path-to-resume.json>');
248
+ process.exit(1);
249
+ }
250
+
251
+ const fs = await import('node:fs');
252
+ const path = await import('node:path');
253
+ const resolved = path.resolve(filePath);
254
+
255
+ let resume;
256
+ try {
257
+ const raw = fs.readFileSync(resolved, 'utf-8');
258
+ resume = JSON.parse(raw);
259
+ } catch (err) {
260
+ console.error(`Error reading ${resolved}: ${err.message}`);
261
+ process.exit(1);
262
+ }
263
+
264
+ if (!resume.basics) {
265
+ console.error('Invalid resume — must have a "basics" section.');
266
+ process.exit(1);
267
+ }
268
+
269
+ const result = await api('/resume', {
270
+ method: 'PUT',
271
+ body: JSON.stringify(resume),
272
+ });
273
+
274
+ console.log(`Resume updated for ${result.username}.`);
275
+ }
276
+
277
+ async function cmdLogout() {
278
+ const { loadConfig, saveConfig } = await import('../src/auth.js');
279
+ const config = loadConfig();
280
+ const username = config.username || 'unknown';
281
+ saveConfig({});
282
+ console.log(`Logged out (removed saved key for ${username}).`);
283
+ }
284
+
285
+ function cmdHelp() {
286
+ console.log(`
287
+ jsonresume-jobs v${VERSION} — Search HN "Who is Hiring" jobs matched to your JSON Resume
288
+
289
+ QUICK START
290
+ npx @jsonresume/jobs Launch the interactive TUI (logs in automatically)
291
+
292
+ COMMANDS
293
+ (default) Interactive TUI with AI features
294
+ search Find matching jobs (table output)
295
+ detail <id> Show full details for a job
296
+ mark <id> <state> Mark a job's state
297
+ me Show your resume summary
298
+ update <file> Update your resume on the registry
299
+ logout Remove saved API key
300
+ help Show this help message
301
+
302
+ SEARCH OPTIONS
303
+ --top N Number of results (default: 20, max: 100)
304
+ --days N How far back to look (default: 30)
305
+ --remote Remote jobs only
306
+ --min-salary N Minimum salary in thousands (e.g. 150)
307
+ --search TERM Keyword filter (searches title, company, skills)
308
+ --interested Show only jobs you marked interested
309
+ --applied Show only jobs you marked applied
310
+
311
+ MARK STATES
312
+ interested You want this job
313
+ not_interested Not for you
314
+ applied You've applied
315
+ maybe Considering it
316
+ dismissed Hide from results
317
+
318
+ GLOBAL OPTIONS
319
+ --json Output raw JSON (for piping / Claude Code)
320
+ --base-url URL API base URL (default: https://registry.jsonresume.org)
321
+ --feedback "reason" Add a reason when marking (with mark command)
322
+
323
+ ENVIRONMENT
324
+ JSONRESUME_API_KEY Your API key (optional — auto-login if not set)
325
+ OPENAI_API_KEY Enable AI features (summaries, reranking)
326
+ JSONRESUME_BASE_URL API base URL override
327
+
328
+ EXAMPLES
329
+ npx @jsonresume/jobs # TUI
330
+ npx @jsonresume/jobs search --remote --min-salary 150 # CLI search
331
+ npx @jsonresume/jobs detail 181420 # Job details
332
+ npx @jsonresume/jobs mark 181420 interested --feedback "great" # Mark job
333
+ `);
334
+ }
335
+
336
+ // ── Main ───────────────────────────────────────────────────
337
+
338
+ async function main() {
339
+ const cmd = process.argv[2] || '';
340
+
341
+ if (cmd === 'help' || cmd === '--help' || cmd === '-h') {
342
+ return cmdHelp();
343
+ }
344
+
345
+ if (cmd === '--version' || cmd === '-v') {
346
+ console.log(VERSION);
347
+ return;
348
+ }
349
+
350
+ if (cmd === 'logout') {
351
+ return cmdLogout();
352
+ }
353
+
354
+ switch (cmd) {
355
+ case 'search':
356
+ return cmdSearch();
357
+ case 'detail':
358
+ return cmdDetail();
359
+ case 'mark':
360
+ return cmdMark();
361
+ case 'me':
362
+ return cmdMe();
363
+ case 'update':
364
+ return cmdUpdate();
365
+ case 'browse':
366
+ case 'tui':
367
+ case '':
368
+ default: {
369
+ // Default: launch TUI
370
+ const apiKey = await getApiKey();
371
+ const { default: runTUI } = await import('../src/tui/App.js');
372
+ return runTUI({ baseUrl: BASE_URL, apiKey });
373
+ }
374
+ }
375
+ }
376
+
377
+ main().catch((e) => {
378
+ console.error(`Fatal: ${e.message}`);
379
+ process.exit(1);
380
+ });
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@jsonresume/jobs",
3
+ "version": "0.7.0",
4
+ "type": "module",
5
+ "description": "Search Hacker News jobs matched against your JSON Resume",
6
+ "bin": {
7
+ "jsonresume-jobs": "./bin/cli.js",
8
+ "jobs": "./bin/cli.js"
9
+ },
10
+ "files": [
11
+ "bin",
12
+ "src",
13
+ "skills",
14
+ "README.md"
15
+ ],
16
+ "keywords": [
17
+ "jsonresume",
18
+ "jobs",
19
+ "hacker-news",
20
+ "job-search",
21
+ "cli",
22
+ "tui",
23
+ "ink"
24
+ ],
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://github.com/jsonresume/jsonresume.org",
28
+ "directory": "packages/job-search"
29
+ },
30
+ "license": "MIT",
31
+ "author": "Thomas Davis <thomasalwyndavis@gmail.com>",
32
+ "engines": {
33
+ "node": ">=18"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "dependencies": {
39
+ "@ai-sdk/anthropic": "^3.0.58",
40
+ "@ai-sdk/openai": "^3.0.41",
41
+ "ai": "^6.0.116",
42
+ "ink": "^6.8.0",
43
+ "ink-spinner": "^5.0.0",
44
+ "ink-text-input": "^6.0.0",
45
+ "react": "^19.2.4"
46
+ }
47
+ }