@jsonresume/jobs 0.11.0 → 0.13.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 CHANGED
@@ -102,7 +102,7 @@ npx @jsonresume/jobs
102
102
 
103
103
  The TUI has three main regions:
104
104
 
105
- - **Header** — shows the app title, active search profile name, tab bar (All / Interested / Applied / Maybe / Passed) with live counts, and any active filter pills
105
+ - **Header** — shows the app title, active search profile name, tab bar (All / New / Reviewed / Interested / Applied / Maybe / Passed) with live counts, and any active filter pills
106
106
  - **Content area** — job list in list view, or a split-pane layout (40% compact list + 60% job detail) in detail view
107
107
  - **Status bar** — context-sensitive keyboard hints, loading/reranking indicators, and toast notifications
108
108
 
@@ -206,6 +206,8 @@ In split-pane detail view, the left pane shows a compact list with just score, t
206
206
  | Tab | Shows |
207
207
  |-----|-------|
208
208
  | All | All jobs from the current search (excludes passed/dismissed) |
209
+ | New | Jobs with no state and no dossier — completely untouched |
210
+ | Reviewed | Jobs with a dossier but no decision yet |
209
211
  | Interested | Jobs you marked with `i` |
210
212
  | Applied | Jobs you marked with `x` |
211
213
  | Maybe | Jobs you marked with `m` |
@@ -269,6 +271,228 @@ npx @jsonresume/jobs help # All options
269
271
 
270
272
  ![architecture](./architecture.svg)
271
273
 
274
+ ## API Reference
275
+
276
+ All endpoints live at `https://registry.jsonresume.org/api/v1`. Authenticated endpoints require a `Bearer` token in the `Authorization` header.
277
+
278
+ ### Authentication
279
+
280
+ ```bash
281
+ # Generate an API key (no auth required)
282
+ curl -s -X POST https://registry.jsonresume.org/api/v1/keys \
283
+ -H 'Content-Type: application/json' \
284
+ -d '{"username":"YOUR_GITHUB_USERNAME"}'
285
+ # → { "key": "jr_username_xxxxx", "username": "username" }
286
+
287
+ # Use in all subsequent requests
288
+ export JSONRESUME_API_KEY="jr_username_xxxxx"
289
+ AUTH="Authorization: Bearer $JSONRESUME_API_KEY"
290
+ ```
291
+
292
+ ### GET /api/v1/me
293
+
294
+ Returns your resume and profile.
295
+
296
+ ```bash
297
+ curl -s -H "$AUTH" https://registry.jsonresume.org/api/v1/me
298
+ # → { "username": "...", "resume": { ... } }
299
+ ```
300
+
301
+ ### GET /api/v1/jobs
302
+
303
+ Get jobs matched to your resume via vector similarity.
304
+
305
+ | Param | Type | Default | Description |
306
+ |-------|------|---------|-------------|
307
+ | `top` | int | 20 | Number of results (max 100) |
308
+ | `days` | int | 30 | How far back to search |
309
+ | `remote` | bool | false | Remote jobs only |
310
+ | `min_salary` | int | 0 | Minimum salary in thousands (e.g. `150` = $150k) |
311
+ | `search` | string | | Keyword filter across all fields |
312
+ | `search_id` | uuid | | Use a saved search profile's embedding |
313
+ | `rerank` | bool | auto | LLM reranking (defaults to true when `search_id` is set) |
314
+
315
+ ```bash
316
+ # Basic search
317
+ curl -s -H "$AUTH" "https://registry.jsonresume.org/api/v1/jobs?top=50&days=30"
318
+
319
+ # Remote jobs, $150k+, keyword filter
320
+ curl -s -H "$AUTH" "https://registry.jsonresume.org/api/v1/jobs?top=50&remote=true&min_salary=150&search=react"
321
+
322
+ # Using a search profile with reranking
323
+ curl -s -H "$AUTH" "https://registry.jsonresume.org/api/v1/jobs?top=50&search_id=UUID&rerank=true"
324
+ ```
325
+
326
+ **Response:**
327
+ ```json
328
+ {
329
+ "jobs": [
330
+ {
331
+ "id": 237247,
332
+ "uuid": "...",
333
+ "title": "Full-Stack Engineer",
334
+ "company": "Acme Corp",
335
+ "location": { "city": "San Francisco", "countryCode": "US" },
336
+ "remote": "Full",
337
+ "salary": "$180k - $220k",
338
+ "salary_usd": 180000,
339
+ "experience": "Senior",
340
+ "type": "Full-time",
341
+ "description": "...",
342
+ "skills": [{ "name": "React" }, { "name": "Node.js" }],
343
+ "url": "https://news.ycombinator.com/item?id=...",
344
+ "posted_at": "2026-03-01T...",
345
+ "similarity": 0.654,
346
+ "rerank_score": 8,
347
+ "combined_score": 0.74,
348
+ "state": "interested",
349
+ "has_dossier": true
350
+ }
351
+ ],
352
+ "total": 50
353
+ }
354
+ ```
355
+
356
+ ### POST /api/v1/jobs
357
+
358
+ Match jobs against a provided resume — **no auth required**. Useful for local mode or one-off queries.
359
+
360
+ ```bash
361
+ curl -s -X POST https://registry.jsonresume.org/api/v1/jobs \
362
+ -H 'Content-Type: application/json' \
363
+ -d '{
364
+ "resume": { "basics": { "name": "...", "label": "..." }, "skills": [...] },
365
+ "top": 20,
366
+ "days": 30,
367
+ "remote": true
368
+ }'
369
+ ```
370
+
371
+ ### GET /api/v1/jobs/:id
372
+
373
+ Full details for a single job including raw posting content.
374
+
375
+ ```bash
376
+ curl -s -H "$AUTH" https://registry.jsonresume.org/api/v1/jobs/237247
377
+ ```
378
+
379
+ ### PUT /api/v1/jobs/:id
380
+
381
+ Mark a job's state.
382
+
383
+ | Body Param | Type | Description |
384
+ |------------|------|-------------|
385
+ | `state` | string | `interested`, `applied`, `maybe`, `not_interested`, or `dismissed` |
386
+ | `feedback` | string | Optional reason/notes |
387
+
388
+ ```bash
389
+ # Mark as interested
390
+ curl -s -X PUT -H "$AUTH" -H 'Content-Type: application/json' \
391
+ https://registry.jsonresume.org/api/v1/jobs/237247 \
392
+ -d '{"state":"interested","feedback":"great remote role, strong tech stack"}'
393
+
394
+ # Mark as applied
395
+ curl -s -X PUT -H "$AUTH" -H 'Content-Type: application/json' \
396
+ https://registry.jsonresume.org/api/v1/jobs/237247 \
397
+ -d '{"state":"applied","feedback":"applied via email 2026-03-13"}'
398
+
399
+ # Pass on a job
400
+ curl -s -X PUT -H "$AUTH" -H 'Content-Type: application/json' \
401
+ https://registry.jsonresume.org/api/v1/jobs/237247 \
402
+ -d '{"state":"not_interested","feedback":"salary too low"}'
403
+ ```
404
+
405
+ ### GET /api/v1/jobs/:id/dossier
406
+
407
+ Fetch saved research dossier for a job.
408
+
409
+ ```bash
410
+ curl -s -H "$AUTH" https://registry.jsonresume.org/api/v1/jobs/237247/dossier
411
+ # → { "content": "# Dossier\n\n## Compensation...", "created_at": "..." }
412
+ # → { "content": null } if no dossier exists
413
+ ```
414
+
415
+ ### PUT /api/v1/jobs/:id/dossier
416
+
417
+ Save or update a research dossier.
418
+
419
+ ```bash
420
+ curl -s -X PUT -H "$AUTH" -H 'Content-Type: application/json' \
421
+ https://registry.jsonresume.org/api/v1/jobs/237247/dossier \
422
+ -d '{"content":"# Company Research\n\n..."}'
423
+ ```
424
+
425
+ ### PUT /api/v1/resume
426
+
427
+ Update your resume on the registry.
428
+
429
+ ```bash
430
+ curl -s -X PUT -H "$AUTH" -H 'Content-Type: application/json' \
431
+ https://registry.jsonresume.org/api/v1/resume \
432
+ -d @resume.json
433
+ # → { "username": "...", "message": "Resume updated" }
434
+ ```
435
+
436
+ ### GET /api/v1/searches
437
+
438
+ List your saved search profiles.
439
+
440
+ ```bash
441
+ curl -s -H "$AUTH" https://registry.jsonresume.org/api/v1/searches
442
+ # → { "searches": [{ "id": "uuid", "name": "Remote React", "prompt": "...", "filters": [...] }] }
443
+ ```
444
+
445
+ ### POST /api/v1/searches
446
+
447
+ Create a new search profile. The server generates a HyDE embedding from your prompt + resume.
448
+
449
+ ```bash
450
+ curl -s -X POST -H "$AUTH" -H 'Content-Type: application/json' \
451
+ https://registry.jsonresume.org/api/v1/searches \
452
+ -d '{"name":"Remote React","prompt":"remote React roles at climate tech startups"}'
453
+ # → { "search": { "id": "uuid", "name": "Remote React", "prompt": "..." } }
454
+ ```
455
+
456
+ ### PUT /api/v1/searches/:id
457
+
458
+ Update a search profile's name or filters.
459
+
460
+ ```bash
461
+ curl -s -X PUT -H "$AUTH" -H 'Content-Type: application/json' \
462
+ https://registry.jsonresume.org/api/v1/searches/UUID \
463
+ -d '{"name":"Updated Name","filters":[{"type":"remote"},{"type":"minSalary","value":"150"}]}'
464
+ ```
465
+
466
+ ### DELETE /api/v1/searches/:id
467
+
468
+ Delete a search profile.
469
+
470
+ ```bash
471
+ curl -s -X DELETE -H "$AUTH" https://registry.jsonresume.org/api/v1/searches/UUID
472
+ # → { "ok": true }
473
+ ```
474
+
475
+ ### Example: Automated Job Application Workflow
476
+
477
+ Use the API to build your own automation — e.g., have Claude in Chrome review your "maybe" jobs and apply:
478
+
479
+ ```bash
480
+ # 1. Get all jobs, filter to "maybe" state
481
+ JOBS=$(curl -s -H "$AUTH" "https://registry.jsonresume.org/api/v1/jobs?top=100" \
482
+ | jq '[.jobs[] | select(.state == "maybe")]')
483
+
484
+ # 2. For each, get the full detail + dossier
485
+ for ID in $(echo $JOBS | jq -r '.[].id'); do
486
+ DETAIL=$(curl -s -H "$AUTH" "https://registry.jsonresume.org/api/v1/jobs/$ID")
487
+ DOSSIER=$(curl -s -H "$AUTH" "https://registry.jsonresume.org/api/v1/jobs/$ID/dossier")
488
+ echo "$DETAIL" | jq '{title: .title, company: .company, url: .url}'
489
+ # ... use the detail + dossier to apply, then mark as applied
490
+ curl -s -X PUT -H "$AUTH" -H 'Content-Type: application/json' \
491
+ "https://registry.jsonresume.org/api/v1/jobs/$ID" \
492
+ -d '{"state":"applied","feedback":"auto-applied via script"}'
493
+ done
494
+ ```
495
+
272
496
  ## How Ranking Works
273
497
 
274
498
  The system uses a five-stage pipeline to match and rank jobs against your resume.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsonresume/jobs",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "type": "module",
5
5
  "description": "Search Hacker News jobs matched against your JSON Resume",
6
6
  "bin": {
@@ -9,6 +9,7 @@ export default function AIPanel({
9
9
  error,
10
10
  onDismiss,
11
11
  onExport,
12
+ onRegenerate,
12
13
  onMark,
13
14
  job,
14
15
  isActive,
@@ -54,6 +55,9 @@ export default function AIPanel({
54
55
  }, 3000);
55
56
  }
56
57
  }
58
+ if (input === 'r' && job && onRegenerate && !loading) {
59
+ onRegenerate(job);
60
+ }
57
61
  // Mark keys
58
62
  if (job && onMark) {
59
63
  if (input === 'i') onMark(job.id, 'interested');
@@ -106,14 +110,15 @@ export default function AIPanel({
106
110
  : '';
107
111
 
108
112
  const exportHint = text ? ' · e export' : '';
113
+ const regenHint = isCover && !loading && job ? ' · r regenerate' : '';
109
114
  const statusLine =
110
115
  loading && text
111
116
  ? 'Streaming… ESC to cancel'
112
117
  : loading
113
118
  ? 'ESC to cancel'
114
119
  : scrollHint
115
- ? `${scrollHint}${exportHint} · ESC to dismiss`
116
- : `ESC to dismiss${exportHint}`;
120
+ ? `${scrollHint}${exportHint}${regenHint} · ESC to dismiss`
121
+ : `ESC to dismiss${exportHint}${regenHint}`;
117
122
 
118
123
  return h(
119
124
  Box,
package/src/tui/App.js CHANGED
@@ -24,9 +24,19 @@ import StatusBar from './StatusBar.js';
24
24
  import AIPanel from './AIPanel.js';
25
25
  import HelpModal from './HelpModal.js';
26
26
 
27
- const TABS = ['all', 'interested', 'applied', 'maybe', 'passed'];
27
+ const TABS = [
28
+ 'all',
29
+ 'new',
30
+ 'reviewed',
31
+ 'interested',
32
+ 'applied',
33
+ 'maybe',
34
+ 'passed',
35
+ ];
28
36
  const TAB_LABELS = {
29
37
  all: 'All',
38
+ new: 'New',
39
+ reviewed: 'Reviewed',
30
40
  interested: 'Interested',
31
41
  applied: 'Applied',
32
42
  maybe: 'Maybe',
@@ -111,6 +121,7 @@ function App({ baseUrl, apiKey, apiClient }) {
111
121
  });
112
122
  }, [searchesHook.searches]);
113
123
 
124
+ const ai = useAI(resume);
114
125
  const {
115
126
  jobs: rawJobs,
116
127
  allJobs,
@@ -119,11 +130,15 @@ function App({ baseUrl, apiKey, apiClient }) {
119
130
  error,
120
131
  markJob,
121
132
  forceRefresh,
122
- } = useJobs(api, activeFilters, tab, activeSearchId);
123
- const ai = useAI(resume);
133
+ } = useJobs(api, activeFilters, tab, activeSearchId, ai.getDossierStatus);
124
134
  const { toast, show: showToast } = useToast();
125
135
  const [confirmExit, setConfirmExit] = useState(false);
126
136
 
137
+ // Seed dossier icons from server-side flags when jobs load
138
+ useEffect(() => {
139
+ if (allJobs.length) ai.seedDossierFlags(allJobs);
140
+ }, [allJobs, ai]);
141
+
127
142
  // Kill claude processes on exit (Ctrl+C)
128
143
  useEffect(() => {
129
144
  const cleanup = () => ai.cancel();
@@ -304,6 +319,16 @@ function App({ baseUrl, apiKey, apiClient }) {
304
319
 
305
320
  const counts = {
306
321
  all: allJobs.length,
322
+ new: allJobs.filter(
323
+ (j) =>
324
+ !j.state &&
325
+ !j.has_dossier &&
326
+ ai.getDossierStatus(j.id) !== 'done' &&
327
+ ai.getDossierStatus(j.id) !== 'generating'
328
+ ).length,
329
+ reviewed: allJobs.filter(
330
+ (j) => (j.has_dossier || ai.getDossierStatus(j.id) === 'done') && !j.state
331
+ ).length,
307
332
  interested: allJobs.filter((j) => j.state === 'interested').length,
308
333
  applied: allJobs.filter((j) => j.state === 'applied').length,
309
334
  maybe: allJobs.filter((j) => j.state === 'maybe').length,
@@ -449,6 +474,10 @@ function App({ baseUrl, apiKey, apiClient }) {
449
474
  if (f) showToast(`Saved ./${f}`, 'export');
450
475
  return f;
451
476
  },
477
+ onRegenerate: (job) => {
478
+ ai.regenerateDossier(job, api);
479
+ showToast('Regenerating dossier…', 'info');
480
+ },
452
481
  isActive: true,
453
482
  })
454
483
  )
package/src/tui/useAI.js CHANGED
@@ -214,7 +214,20 @@ ${jobText}
214
214
 
215
215
  Research everything you can and produce a complete dossier covering:
216
216
 
217
- ### 1. Company Deep Dive
217
+ ### 1. Compensation Context
218
+ - Market rate for this role/level/location
219
+ - How the listed salary compares
220
+ - Negotiation leverage points
221
+
222
+ ### 2. AI Engineering Culture
223
+ - Does the company use or encourage AI coding tools (Copilot, Claude Code, Cursor, agentic workflows)?
224
+ - Any public statements, blog posts, or job descriptions mentioning AI-assisted development?
225
+ - Is the engineering culture likely to embrace engineers who ship faster using AI tools, or is there resistance?
226
+ - Look for signals: do they mention "AI-native", "agentic", "LLM-augmented" workflows, or similar?
227
+ - Are there concerns about AI tool usage (IP policies, code review friction, etc.)?
228
+ - Rating: AI-Forward / AI-Friendly / Neutral / Unknown / AI-Resistant
229
+
230
+ ### 3. Company Deep Dive
218
231
  - What the company does, their products/services
219
232
  - Funding stage, size, recent news
220
233
  - Tech stack and engineering culture
@@ -222,38 +235,33 @@ Research everything you can and produce a complete dossier covering:
222
235
  - Employee sentiment and Glassdoor/Blind highlights
223
236
  - Any red flags or concerns
224
237
 
225
- ### 2. Role Analysis
238
+ ### 4. Role Analysis
226
239
  - What you'd actually be doing day-to-day
227
240
  - Team context — who you'd work with
228
241
  - Growth trajectory for this role
229
242
  - How this role fits into the company's current priorities
230
243
 
231
- ### 3. Fit Assessment
244
+ ### 5. Fit Assessment
232
245
  - Matching skills and experience (be specific, reference the candidate's actual background)
233
246
  - Skill gaps to acknowledge or address
234
247
  - Adjacent experience that transfers
235
248
  - Overall fit rating: Strong / Good / Stretch
236
249
 
237
- ### 4. Cover Letter Talking Points
250
+ ### 6. Cover Letter Talking Points
238
251
  - 5-7 specific bullet points to mention, each referencing the candidate's real experience
239
252
  - What angle to take (technical depth? leadership? domain expertise?)
240
253
  - What to emphasize vs. downplay
241
254
 
242
- ### 5. Contact Info
255
+ ### 7. Contact Info
243
256
  - Email addresses from the posting
244
257
  - Who posted (HN username from the URL if available)
245
258
  - Best way to reach out
246
259
 
247
- ### 6. Interview Prep
260
+ ### 8. Interview Prep
248
261
  - Questions they'll likely ask for this role
249
262
  - Questions the candidate should ask
250
263
  - Topics to research before interviewing
251
264
 
252
- ### 7. Compensation Context
253
- - Market rate for this role/level/location
254
- - How the listed salary compares
255
- - Negotiation leverage points
256
-
257
265
  Be thorough, specific, and opinionated. Reference the candidate's actual experience when making recommendations.`;
258
266
 
259
267
  try {
@@ -458,6 +466,22 @@ Be thorough, specific, and opinionated. Reference the candidate's actual experie
458
466
  return filename;
459
467
  }, []);
460
468
 
469
+ // Seed cache with server-side dossier flags from job list
470
+ const seedDossierFlags = useCallback((jobs) => {
471
+ let changed = false;
472
+ for (const job of jobs) {
473
+ if (job.has_dossier && !dossierCache.current.has(job.id)) {
474
+ dossierCache.current.set(job.id, {
475
+ text: '',
476
+ done: true,
477
+ loading: false,
478
+ });
479
+ changed = true;
480
+ }
481
+ }
482
+ if (changed) bumpTick();
483
+ }, []);
484
+
461
485
  // Expose dossier status for job list icons
462
486
  // Returns: 'generating' | 'done' | null
463
487
  const getDossierStatus = useCallback((jobId) => {
@@ -468,6 +492,19 @@ Be thorough, specific, and opinionated. Reference the candidate's actual experie
468
492
  return null;
469
493
  }, []);
470
494
 
495
+ const regenerateDossier = useCallback(
496
+ (job, api) => {
497
+ // Clear cached dossier so dossier() starts fresh
498
+ dossierCache.current.delete(job.id);
499
+ cancel();
500
+ setText('');
501
+ setError(null);
502
+ bumpTick();
503
+ dossier(job, api);
504
+ },
505
+ [dossier, cancel]
506
+ );
507
+
471
508
  return {
472
509
  text,
473
510
  loading,
@@ -476,8 +513,10 @@ Be thorough, specific, and opinionated. Reference the candidate's actual experie
476
513
  mode,
477
514
  hasActiveProcess: Boolean(childRef.current),
478
515
  getDossierStatus,
516
+ seedDossierFlags,
479
517
  summarizeJob,
480
518
  dossier,
519
+ regenerateDossier,
481
520
  batchReview,
482
521
  exportDossier,
483
522
  cancel,
@@ -1,7 +1,7 @@
1
1
  import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
2
2
  import { getCached, setCache, updateCachedJob } from '../cache.js';
3
3
 
4
- export function useJobs(api, activeFilters, tab, searchId) {
4
+ export function useJobs(api, activeFilters, tab, searchId, getDossierStatus) {
5
5
  const [allJobs, setAllJobs] = useState([]);
6
6
  const [loading, setLoading] = useState(true);
7
7
  const [reranking, setReranking] = useState(false);
@@ -135,6 +135,22 @@ export function useJobs(api, activeFilters, tab, searchId) {
135
135
  }
136
136
  }
137
137
 
138
+ if (tab === 'new')
139
+ return filtered.filter(
140
+ (j) =>
141
+ !j.state &&
142
+ !j.has_dossier &&
143
+ (!getDossierStatus ||
144
+ (getDossierStatus(j.id) !== 'done' &&
145
+ getDossierStatus(j.id) !== 'generating'))
146
+ );
147
+ if (tab === 'reviewed')
148
+ return filtered.filter(
149
+ (j) =>
150
+ !j.state &&
151
+ (j.has_dossier ||
152
+ (getDossierStatus && getDossierStatus(j.id) === 'done'))
153
+ );
138
154
  if (tab === 'interested')
139
155
  return filtered.filter((j) => j.state === 'interested');
140
156
  if (tab === 'applied') return filtered.filter((j) => j.state === 'applied');
@@ -145,7 +161,7 @@ export function useJobs(api, activeFilters, tab, searchId) {
145
161
  return filtered.filter(
146
162
  (j) => j.state !== 'not_interested' && j.state !== 'dismissed'
147
163
  );
148
- }, [allJobs, activeFilters, tab]);
164
+ }, [allJobs, activeFilters, tab, getDossierStatus]);
149
165
 
150
166
  const markJob = useCallback(
151
167
  async (id, state, feedback) => {