@jsonresume/jobs 0.7.0 → 0.8.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
@@ -1,8 +1,8 @@
1
- # @jsonresume/job-search
1
+ # @jsonresume/jobs
2
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)
3
+ [![npm version](https://img.shields.io/npm/v/@jsonresume/jobs)](https://www.npmjs.com/package/@jsonresume/jobs)
4
+ [![license](https://img.shields.io/npm/l/@jsonresume/jobs)](./LICENSE)
5
+ [![node](https://img.shields.io/node/v/@jsonresume/jobs)](https://nodejs.org)
6
6
 
7
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
8
 
@@ -31,7 +31,7 @@ npx @jsonresume/jobs
31
31
  Or install globally:
32
32
 
33
33
  ```bash
34
- npm install -g @jsonresume/job-search
34
+ npm install -g @jsonresume/jobs
35
35
  jsonresume-jobs
36
36
  ```
37
37
 
@@ -221,7 +221,7 @@ This package includes a [Claude Code skill](https://docs.anthropic.com/en/docs/c
221
221
 
222
222
  ```bash
223
223
  mkdir -p ~/.claude/skills/jsonresume-hunt
224
- cp node_modules/@jsonresume/job-search/skills/jsonresume-hunt/SKILL.md \
224
+ cp node_modules/@jsonresume/jobs/skills/jsonresume-hunt/SKILL.md \
225
225
  ~/.claude/skills/jsonresume-hunt/SKILL.md
226
226
  ```
227
227
 
package/bin/cli.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * Just run: npx @jsonresume/jobs
7
7
  */
8
8
 
9
- const VERSION = '0.7.0';
9
+ const VERSION = '0.8.0';
10
10
 
11
11
  const BASE_URL =
12
12
  getArg('--base-url') ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsonresume/jobs",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "description": "Search Hacker News jobs matched against your JSON Resume",
6
6
  "bin": {
package/src/tui/App.js CHANGED
@@ -26,11 +26,11 @@ import HelpModal from './HelpModal.js';
26
26
 
27
27
  const TABS = ['all', 'interested', 'applied', 'maybe', 'passed'];
28
28
  const TAB_LABELS = {
29
- all: 'All Jobs',
30
- interested: 'Interested',
31
- applied: '📨 Applied',
32
- maybe: '? Maybe',
33
- passed: 'Passed',
29
+ all: 'All',
30
+ interested: 'Interested',
31
+ applied: 'Applied',
32
+ maybe: 'Maybe',
33
+ passed: 'Passed',
34
34
  };
35
35
 
36
36
  function InlineSearch({ query, onChange, onSubmit }) {
@@ -268,7 +268,6 @@ function App({ baseUrl, apiKey }) {
268
268
  reranking,
269
269
  error,
270
270
  aiEnabled: ai.hasKey,
271
- searchName: activeSearch?.name || null,
272
271
  toast: toastEl,
273
272
  });
274
273
 
package/src/tui/Header.js CHANGED
@@ -2,10 +2,10 @@ import { Box, Text } from 'ink';
2
2
  import { h } from './h.js';
3
3
 
4
4
  const FILTER_LABELS = {
5
- remote: (f) => `Remote: ${f.value}`,
6
- search: (f) => `Search: "${f.value}"`,
7
- minSalary: (f) => `Salary ≥ $${f.value}k`,
8
- days: (f) => `Last ${f.value} days`,
5
+ remote: () => 'Remote',
6
+ search: (f) => `"${f.value}"`,
7
+ minSalary: (f) => `≥$${f.value}k`,
8
+ days: (f) => `${f.value}d`,
9
9
  };
10
10
 
11
11
  export default function Header({
@@ -17,71 +17,93 @@ export default function Header({
17
17
  searchName,
18
18
  appliedQuery,
19
19
  }) {
20
+ const cols = process.stdout.columns || 80;
21
+
22
+ // ── Title row ─────────────────────────────────────
23
+ const titleRow = h(
24
+ Box,
25
+ { paddingX: 1, justifyContent: 'space-between' },
26
+ h(
27
+ Box,
28
+ { gap: 1 },
29
+ h(
30
+ Text,
31
+ { bold: true, color: 'black', backgroundColor: 'cyan' },
32
+ ' jsonresume-jobs '
33
+ ),
34
+ searchName
35
+ ? h(Text, { color: 'magenta', bold: true }, ` ${searchName}`)
36
+ : null
37
+ ),
38
+ h(Text, { dimColor: true }, '?:help /:profiles f:filters q:quit')
39
+ );
40
+
41
+ // ── Tab row ───────────────────────────────────────
20
42
  const tabElements = tabs.map((t) => {
21
43
  const active = t === tab;
22
44
  const count = counts[t] || 0;
23
- const label = `${tabLabels[t]} (${count})`;
45
+ if (count === 0 && !active && t !== 'all') return null;
46
+
47
+ const label = `${tabLabels[t]} ${count}`;
48
+
49
+ if (active) {
50
+ return h(
51
+ Box,
52
+ { key: t, marginRight: 1 },
53
+ h(
54
+ Text,
55
+ { bold: true, color: 'black', backgroundColor: 'white' },
56
+ ` ${label} `
57
+ )
58
+ );
59
+ }
24
60
  return h(
25
61
  Box,
26
62
  { key: t, marginRight: 1 },
63
+ h(Text, { dimColor: true }, ` ${label} `)
64
+ );
65
+ });
66
+
67
+ const tabRow = h(Box, { paddingX: 1 }, ...tabElements.filter(Boolean));
68
+
69
+ // ── Filter pills (only if active) ────────────────
70
+ const tags = [];
71
+ for (const f of filters || []) {
72
+ const label = FILTER_LABELS[f.type]?.(f) || f.value;
73
+ tags.push(
27
74
  h(
28
75
  Text,
29
- {
30
- bold: active,
31
- color: active ? 'cyan' : 'gray',
32
- underline: active,
33
- },
76
+ { key: f.type, color: 'black', backgroundColor: 'yellow' },
34
77
  ` ${label} `
35
78
  )
36
79
  );
37
- });
80
+ }
81
+ if (appliedQuery) {
82
+ tags.push(
83
+ h(
84
+ Text,
85
+ { key: 'find', color: 'black', backgroundColor: 'green' },
86
+ ` find:${appliedQuery} `
87
+ )
88
+ );
89
+ }
38
90
 
39
- const filterTags = (filters || []).map((f, i) => {
40
- const label = FILTER_LABELS[f.type]?.(f) || `${f.type}: ${f.value}`;
41
- return h(Text, { key: i, color: 'yellow' }, ` [${label}] `);
42
- });
91
+ const filterRow =
92
+ tags.length > 0 ? h(Box, { paddingX: 1, gap: 1 }, ...tags) : null;
43
93
 
44
- const hasFilters = filterTags.length > 0 || appliedQuery;
94
+ // ── Divider ───────────────────────────────────────
95
+ const divider = h(
96
+ Box,
97
+ { paddingX: 1 },
98
+ h(Text, { dimColor: true }, '─'.repeat(Math.max(10, cols - 2)))
99
+ );
45
100
 
46
101
  return h(
47
102
  Box,
48
- { flexDirection: 'column', marginBottom: 0 },
49
- h(
50
- Box,
51
- {
52
- paddingX: 1,
53
- borderStyle: 'single',
54
- borderColor: 'cyan',
55
- borderBottom: false,
56
- },
57
- h(Text, { bold: true, color: 'cyan' }, '⚡ '),
58
- h(Text, { bold: true, color: 'white' }, 'JSON Resume Job Search'),
59
- searchName ? h(Text, { color: 'magenta' }, ` 🔍 ${searchName}`) : null,
60
- h(Text, { color: 'gray' }, ' '),
61
- h(Text, { dimColor: true }, 'tab:sections /:searches ?:help')
62
- ),
63
- h(Box, { paddingX: 1, gap: 0 }, ...tabElements),
64
- hasFilters
65
- ? h(
66
- Box,
67
- { paddingX: 1 },
68
- filterTags.length > 0
69
- ? h(Text, { dimColor: true }, 'Filters:')
70
- : null,
71
- ...filterTags,
72
- appliedQuery
73
- ? h(Text, { color: 'yellow' }, ` [Find: "${appliedQuery}"] `)
74
- : null,
75
- h(Text, { dimColor: true }, ' f:manage')
76
- )
77
- : h(
78
- Box,
79
- { paddingX: 1 },
80
- h(
81
- Text,
82
- { dimColor: true },
83
- 'No filters active f:add n:quick search'
84
- )
85
- )
103
+ { flexDirection: 'column' },
104
+ titleRow,
105
+ tabRow,
106
+ filterRow,
107
+ divider
86
108
  );
87
109
  }
@@ -3,41 +3,33 @@ import { h } from './h.js';
3
3
 
4
4
  const KEYS = {
5
5
  list: [
6
- ['jk', 'nav'],
7
- ['enter', 'open'],
8
- ['i', ''],
9
- ['x', '📨'],
10
- ['m', '?'],
11
- ['p', ''],
6
+ ['j/k', 'nav'],
7
+ ['enter', 'detail'],
8
+ ['i', 'interested'],
9
+ ['x', 'applied'],
10
+ ['m', 'maybe'],
11
+ ['p', 'pass'],
12
12
  ['v', 'select'],
13
+ ['n', 'find'],
13
14
  ['space', 'AI'],
14
- ['f', 'filter'],
15
- ['/', 'search'],
16
- ['e', 'export'],
17
- ['?', 'help'],
18
- ['q', 'quit'],
19
15
  ],
20
16
  detail: [
21
- ['jk', 'nav'],
22
- ['JK', 'scroll detail'],
23
- ['i', ''],
24
- ['x', '📨'],
25
- ['m', '?'],
26
- ['p', '✗'],
27
- ['o', 'open'],
17
+ ['j/k', 'nav jobs'],
18
+ ['J/K', 'scroll'],
19
+ ['i/x/m/p', 'mark'],
20
+ ['o', 'open URL'],
28
21
  ['space', 'AI'],
29
22
  ['esc', 'back'],
30
- ['q', 'close'],
31
23
  ],
32
24
  filters: [
33
- ['jk', 'nav'],
25
+ ['j/k', 'nav'],
34
26
  ['enter', 'edit'],
35
27
  ['a', 'add'],
36
28
  ['d', 'delete'],
37
29
  ['esc', 'close'],
38
30
  ],
39
31
  searches: [
40
- ['jk', 'nav'],
32
+ ['j/k', 'nav'],
41
33
  ['enter', 'switch'],
42
34
  ['n', 'new'],
43
35
  ['d', 'delete'],
@@ -47,6 +39,15 @@ const KEYS = {
47
39
  help: [['?/esc', 'close']],
48
40
  };
49
41
 
42
+ function KeyHint({ k, label }) {
43
+ return h(
44
+ Box,
45
+ { marginRight: 1 },
46
+ h(Text, { color: 'cyan' }, k),
47
+ h(Text, { dimColor: true }, ` ${label}`)
48
+ );
49
+ }
50
+
50
51
  export default function StatusBar({
51
52
  view,
52
53
  jobCount,
@@ -55,54 +56,44 @@ export default function StatusBar({
55
56
  reranking,
56
57
  error,
57
58
  aiEnabled,
58
- searchName,
59
59
  toast,
60
60
  }) {
61
+ const cols = process.stdout.columns || 80;
61
62
  const keys = KEYS[view] || KEYS.list;
62
63
 
63
- const keyElements = keys.map(([key, label], i) =>
64
- h(
65
- Box,
66
- { key: i, marginRight: 1 },
67
- h(Text, { bold: true, color: 'cyan' }, key),
68
- h(Text, { dimColor: true }, `:${label}`)
69
- )
64
+ const divider = h(
65
+ Box,
66
+ { paddingX: 1 },
67
+ h(Text, { dimColor: true }, '─'.repeat(Math.max(10, cols - 2)))
70
68
  );
71
69
 
72
- return h(
70
+ const rightInfo = h(
73
71
  Box,
74
- {
75
- flexDirection: 'column',
76
- borderStyle: 'single',
77
- borderColor: 'gray',
78
- paddingX: 1,
79
- marginTop: 0,
80
- },
81
- toast
82
- ? h(
83
- Box,
84
- { justifyContent: 'space-between' },
85
- toast,
86
- h(
87
- Box,
88
- { gap: 1 },
89
- h(Text, { dimColor: true }, `${jobCount}/${totalCount} jobs`)
90
- )
91
- )
92
- : h(
72
+ { gap: 1 },
73
+ loading ? h(Text, { color: 'yellow' }, 'loading…') : null,
74
+ reranking ? h(Text, { color: 'magenta' }, 'reranking…') : null,
75
+ h(Text, { dimColor: true }, `${jobCount}/${totalCount}`),
76
+ aiEnabled ? null : h(Text, { dimColor: true }, 'no-AI')
77
+ );
78
+
79
+ const content = toast
80
+ ? h(Box, { paddingX: 1, justifyContent: 'space-between' }, toast, rightInfo)
81
+ : h(
82
+ Box,
83
+ { paddingX: 1, justifyContent: 'space-between' },
84
+ h(
93
85
  Box,
94
- { justifyContent: 'space-between' },
95
- h(Box, { flexWrap: 'wrap', gap: 0 }, ...keyElements),
96
- h(
97
- Box,
98
- { gap: 1 },
99
- loading ? h(Text, { color: 'yellow' }, '⏳') : null,
100
- reranking ? h(Text, { color: 'magenta' }, '🧠 reranking…') : null,
101
- h(Text, { dimColor: true }, `${jobCount}/${totalCount} jobs`),
102
- searchName ? h(Text, { color: 'magenta' }, '🔍') : null,
103
- aiEnabled ? null : h(Text, { color: 'gray' }, '(no AI)')
104
- )
86
+ { flexWrap: 'wrap' },
87
+ ...keys.map(([k, label], i) => h(KeyHint, { key: i, k, label }))
105
88
  ),
106
- error ? h(Text, { color: 'red' }, `Error: ${error}`) : null
89
+ rightInfo
90
+ );
91
+
92
+ return h(
93
+ Box,
94
+ { flexDirection: 'column' },
95
+ divider,
96
+ content,
97
+ error ? h(Box, { paddingX: 1 }, h(Text, { color: 'red' }, error)) : null
107
98
  );
108
99
  }