@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 +6 -6
- package/bin/cli.js +1 -1
- package/package.json +1 -1
- package/src/tui/App.js +5 -6
- package/src/tui/Header.js +76 -54
- package/src/tui/StatusBar.js +52 -61
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# @jsonresume/
|
|
1
|
+
# @jsonresume/jobs
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/@jsonresume/jobs)
|
|
4
|
+
[](./LICENSE)
|
|
5
|
+
[](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/
|
|
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/
|
|
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
package/package.json
CHANGED
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
|
|
30
|
-
interested: '
|
|
31
|
-
applied: '
|
|
32
|
-
maybe: '
|
|
33
|
-
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: (
|
|
6
|
-
search: (f) => `
|
|
7
|
-
minSalary: (f) =>
|
|
8
|
-
days: (f) =>
|
|
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
|
-
|
|
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
|
|
40
|
-
|
|
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
|
-
|
|
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'
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
}
|
package/src/tui/StatusBar.js
CHANGED
|
@@ -3,41 +3,33 @@ import { h } from './h.js';
|
|
|
3
3
|
|
|
4
4
|
const KEYS = {
|
|
5
5
|
list: [
|
|
6
|
-
['
|
|
7
|
-
['enter', '
|
|
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
|
-
['
|
|
22
|
-
['
|
|
23
|
-
['i', '
|
|
24
|
-
['
|
|
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
|
-
['
|
|
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
|
-
['
|
|
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
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
70
|
+
const rightInfo = h(
|
|
73
71
|
Box,
|
|
74
|
-
{
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
{
|
|
95
|
-
h(
|
|
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
|
-
|
|
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
|
}
|