hiringcafe-cli 0.1.4__tar.gz → 0.2.0__tar.gz
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.
- hiringcafe_cli-0.2.0/PKG-INFO +243 -0
- hiringcafe_cli-0.2.0/README.md +220 -0
- {hiringcafe_cli-0.1.4 → hiringcafe_cli-0.2.0}/pyproject.toml +1 -1
- {hiringcafe_cli-0.1.4 → hiringcafe_cli-0.2.0}/pyproject.toml.orig +1 -1
- {hiringcafe_cli-0.1.4 → hiringcafe_cli-0.2.0}/src/hiringcafe_cli/__init__.py +5 -1
- {hiringcafe_cli-0.1.4 → hiringcafe_cli-0.2.0}/src/hiringcafe_cli/count.py +1 -1
- {hiringcafe_cli-0.1.4 → hiringcafe_cli-0.2.0}/src/hiringcafe_cli/http_client.py +60 -43
- {hiringcafe_cli-0.1.4 → hiringcafe_cli-0.2.0}/src/hiringcafe_cli/job.py +11 -10
- hiringcafe_cli-0.2.0/src/hiringcafe_cli/mcp.py +185 -0
- hiringcafe_cli-0.2.0/src/hiringcafe_cli/probe.py +37 -0
- hiringcafe_cli-0.2.0/src/hiringcafe_cli/search.py +101 -0
- hiringcafe_cli-0.1.4/PKG-INFO +0 -145
- hiringcafe_cli-0.1.4/README.md +0 -122
- hiringcafe_cli-0.1.4/src/hiringcafe_cli/probe.py +0 -140
- hiringcafe_cli-0.1.4/src/hiringcafe_cli/search.py +0 -140
- {hiringcafe_cli-0.1.4 → hiringcafe_cli-0.2.0}/LICENSE +0 -0
- {hiringcafe_cli-0.1.4 → hiringcafe_cli-0.2.0}/src/hiringcafe_cli/auth.py +0 -0
- {hiringcafe_cli-0.1.4 → hiringcafe_cli-0.2.0}/src/hiringcafe_cli/cache.py +0 -0
- {hiringcafe_cli-0.1.4 → hiringcafe_cli-0.2.0}/src/hiringcafe_cli/firebase.py +0 -0
- {hiringcafe_cli-0.1.4 → hiringcafe_cli-0.2.0}/src/hiringcafe_cli/io_limits.py +0 -0
- {hiringcafe_cli-0.1.4 → hiringcafe_cli-0.2.0}/src/hiringcafe_cli/saved_jobs.py +0 -0
- {hiringcafe_cli-0.1.4 → hiringcafe_cli-0.2.0}/src/hiringcafe_cli/saved_jobs_list.py +0 -0
- {hiringcafe_cli-0.1.4 → hiringcafe_cli-0.2.0}/src/hiringcafe_cli/saved_jobs_remove.py +0 -0
- {hiringcafe_cli-0.1.4 → hiringcafe_cli-0.2.0}/src/hiringcafe_cli/saved_jobs_save.py +0 -0
- {hiringcafe_cli-0.1.4 → hiringcafe_cli-0.2.0}/src/hiringcafe_cli/saved_jobs_stage.py +0 -0
- {hiringcafe_cli-0.1.4 → hiringcafe_cli-0.2.0}/src/hiringcafe_cli/search_state.py +0 -0
- {hiringcafe_cli-0.1.4 → hiringcafe_cli-0.2.0}/src/hiringcafe_cli/session.py +0 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hiringcafe-cli
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Spec-driven Python CLI for Hiring Cafe job search and saved-job tracking
|
|
5
|
+
Author: chen-gdp
|
|
6
|
+
Author-email: chen-gdp <146709874+chen-gdp@users.noreply.github.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Project-URL: Homepage, https://github.com/chen-gdp/hiringcafe-cli
|
|
19
|
+
Project-URL: Repository, https://github.com/chen-gdp/hiringcafe-cli
|
|
20
|
+
Project-URL: Issues, https://github.com/chen-gdp/hiringcafe-cli/issues
|
|
21
|
+
Project-URL: Changelog, https://github.com/chen-gdp/hiringcafe-cli/blob/main/CHANGELOG.md
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# HiringCafe CLI
|
|
25
|
+
|
|
26
|
+
Give your AI agent Hiring Cafe superpowers: search jobs, inspect postings, and keep your real Hiring Cafe saved-job board in sync without a browser.
|
|
27
|
+
|
|
28
|
+
[](https://pypi.org/project/hiringcafe-cli/)
|
|
29
|
+
[](https://pypi.org/project/hiringcafe-cli/)
|
|
30
|
+
[](https://github.com/chen-gdp/hiringcafe-cli/actions/workflows/ci.yaml)
|
|
31
|
+
[](LICENSE)
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
hiringcafe search "python developer" --jsonl
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The CLI gives your agent structured access to public jobs and, when authenticated, selected actions on your real Hiring Cafe account. Public search and job details use Hiring Cafe's [documented read-only MCP connector](https://hiringcafe.com/documentation/hiringcafe-claude-connector): each search returns at most 10 jobs, with at most one job per company and no pagination or filters yet. Requests are rate-limited; avoid high-frequency automated polling.
|
|
38
|
+
|
|
39
|
+
> **Unofficial client.** This project is not affiliated with, endorsed by, or supported by Hiring Cafe. Use your own account, respect the service, and expect upstream APIs to change.
|
|
40
|
+
|
|
41
|
+
## Contents
|
|
42
|
+
|
|
43
|
+
- [Install](#install)
|
|
44
|
+
- [For agents](#for-agents)
|
|
45
|
+
- [Commands](#commands)
|
|
46
|
+
- [Authentication](#authentication)
|
|
47
|
+
- [Saved jobs](#saved-jobs)
|
|
48
|
+
- [Caching and local data](#caching-and-local-data)
|
|
49
|
+
- [Exit codes](#exit-codes)
|
|
50
|
+
- [Development](#development)
|
|
51
|
+
- [Project status](#project-status)
|
|
52
|
+
|
|
53
|
+
## Install
|
|
54
|
+
|
|
55
|
+
HiringCafe CLI is published on PyPI and supports Python 3.11–3.14 with no runtime dependencies.
|
|
56
|
+
|
|
57
|
+
Install it as a persistent CLI tool:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
uv tool install hiringcafe-cli
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Or, if you prefer pipx:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pipx install hiringcafe-cli
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Run it once without installing anything:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
uvx --from hiringcafe-cli hiringcafe --version
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Verify the installed version:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
hiringcafe --version
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## For agents
|
|
82
|
+
|
|
83
|
+
The smallest useful loop is install, search, and consume machine-readable output:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
uv tool install hiringcafe-cli
|
|
87
|
+
hiringcafe search "python developer" --page 0 --json
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
For a fresh result, bypass the anonymous-read cache:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
hiringcafe search "python developer" --no-cache --json
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Stream the returned jobs as one JSON object per line (up to 10 results):
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
hiringcafe search "python developer" --jsonl
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Extract the normalized jobs from one JSON page:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
hiringcafe search "python developer" --page 0 --json | jq -c '.result.jobs[]'
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Ask only for the result count:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
hiringcafe count "python developer" --json
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Import an existing Firebase refresh token without an interactive password prompt:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
printf '%s\n' "$HIRINGCAFE_REFRESH_TOKEN" | hiringcafe auth import
|
|
118
|
+
hiringcafe saved-jobs list --json
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Agent automation contract:
|
|
122
|
+
|
|
123
|
+
- JSON success output goes to stdout; diagnostics and cache notices go to stderr.
|
|
124
|
+
- Stable exit codes describe transport, input, rate-limit, auth, and conflict outcomes.
|
|
125
|
+
- Provider passwords and Firebase ID tokens are never written to disk.
|
|
126
|
+
- Authenticated mutations are never retried or cached.
|
|
127
|
+
- Anonymous search/count responses use a 15-minute cache; job details use 24 hours.
|
|
128
|
+
- Search returns at most 10 jobs, one per company; pagination and filters are not supported by the current upstream interface.
|
|
129
|
+
- Invalid arguments fail before a network request is made.
|
|
130
|
+
|
|
131
|
+
## Commands
|
|
132
|
+
|
|
133
|
+
| Command | Purpose |
|
|
134
|
+
|---|---|
|
|
135
|
+
| `hiringcafe search "query" --json` | Search jobs and print structured JSON |
|
|
136
|
+
| `hiringcafe search "query" --jsonl` | Stream up to 10 search results as JSON Lines |
|
|
137
|
+
| `hiringcafe count "query" --json` | Print only the result count |
|
|
138
|
+
| `hiringcafe job show <objectID> --json` | Inspect one job posting |
|
|
139
|
+
| `hiringcafe auth login --email you@example.com` | Authenticate interactively |
|
|
140
|
+
| `hiringcafe auth import` | Import an existing Firebase refresh token |
|
|
141
|
+
| `hiringcafe auth status` | Check whether a local session exists |
|
|
142
|
+
| `hiringcafe auth logout` | Remove the local session |
|
|
143
|
+
| `hiringcafe saved-jobs list --json` | List saved jobs |
|
|
144
|
+
| `hiringcafe saved-jobs save <objectID>` | Save a job |
|
|
145
|
+
| `hiringcafe saved-jobs stage <objectID> --stage applied` | Change a job stage |
|
|
146
|
+
| `hiringcafe saved-jobs remove <objectID> --yes` | Remove a saved job |
|
|
147
|
+
| `hiringcafe cache purge` | Remove cached anonymous reads |
|
|
148
|
+
|
|
149
|
+
Every command supports `--help`, for example:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
hiringcafe search --help
|
|
153
|
+
hiringcafe saved-jobs save --help
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Authentication
|
|
157
|
+
|
|
158
|
+
For human use, log in with your Hiring Cafe email:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
hiringcafe auth login --email you@example.com
|
|
162
|
+
hiringcafe auth status
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The password is read from hidden input (or `HIRINGCAFE_PASSWORD` for automation), exchanged once for Firebase credentials, and only the refresh token is stored.
|
|
166
|
+
|
|
167
|
+
For automation, import a refresh token that you already hold:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
printf '%s\n' "$HIRINGCAFE_REFRESH_TOKEN" | hiringcafe auth import
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The CLI stores only the refresh token in an owner-only local session file. It does not store your password or short-lived Firebase ID token. Browser OAuth login is intentionally deferred until its real provider and loopback callback behavior can be verified.
|
|
174
|
+
|
|
175
|
+
## Saved jobs
|
|
176
|
+
|
|
177
|
+
These commands modify your real Hiring Cafe account. Review object IDs and confirmation prompts before changing data.
|
|
178
|
+
|
|
179
|
+
`saved-jobs save` currently depends on an older raw job-hydration endpoint that is returning a Cloudflare challenge from the CLI host. It may fail before saving; the read-only MCP connector does not support saved-job writes or provide the raw object needed by this command. Listing, staging, and removing saved jobs use separate authenticated flows.
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
hiringcafe saved-jobs save <objectID> --stage saved
|
|
183
|
+
hiringcafe saved-jobs stage <objectID> --stage interviewing
|
|
184
|
+
hiringcafe saved-jobs remove <objectID> --yes
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Valid stages are `saved`, `applied`, `interviewing`, `offer`, `rejected`, `ghosted`, and `hidden`.
|
|
188
|
+
|
|
189
|
+
## Caching and local data
|
|
190
|
+
|
|
191
|
+
| Data | Location | Notes |
|
|
192
|
+
|---:|---|---|
|
|
193
|
+
| Refresh token | `~/.config/hiringcafe-cli/session.json` | Owner-only file; removed by logout |
|
|
194
|
+
| Anonymous cache | `~/.cache/hiringcafe-cli/cache/v1/` | Public search and job data only |
|
|
195
|
+
|
|
196
|
+
Set `HIRINGCAFE_CONFIG_HOME` or `HIRINGCAFE_CACHE_HOME` to use different roots.
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
hiringcafe search "python developer" --no-cache
|
|
200
|
+
hiringcafe search "python developer" --cache-ttl 3600
|
|
201
|
+
hiringcafe cache purge
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Authenticated saved-job operations are never cached.
|
|
205
|
+
|
|
206
|
+
## Exit codes
|
|
207
|
+
|
|
208
|
+
| Code | Agent action |
|
|
209
|
+
|---:|---|
|
|
210
|
+
| `0` | Success |
|
|
211
|
+
| `1` | Transport, HTTP, parsing, or API error; inspect stderr |
|
|
212
|
+
| `2` | Invalid arguments or search state; fix the request |
|
|
213
|
+
| `3` | Rate limited; wait and retry |
|
|
214
|
+
| `4` | No stored credential; authenticate first |
|
|
215
|
+
| `5` | Stored credential rejected; re-authenticate |
|
|
216
|
+
| `6` | Write conflict or prerequisite missing; refresh state |
|
|
217
|
+
|
|
218
|
+
## Development
|
|
219
|
+
|
|
220
|
+
Requires Python 3.11+ and [uv](https://docs.astral.sh/uv/).
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
git clone https://github.com/chen-gdp/hiringcafe-cli.git
|
|
224
|
+
cd hiringcafe-cli
|
|
225
|
+
uv sync
|
|
226
|
+
uv run pytest
|
|
227
|
+
uv run hiringcafe --help
|
|
228
|
+
uv run pre-commit run --all-files
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Normal tests do not access the network. The verified anonymous endpoint probe is opt-in:
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
LIVE=1 uv run hiringcafe probe
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Every implementation change traces to `specs/pr-*/spec.md` and the roadmap. Never commit credentials, Firebase tokens, cookies, real UIDs, emails, or private account data.
|
|
238
|
+
|
|
239
|
+
## Project status
|
|
240
|
+
|
|
241
|
+
Public search, job details, authentication, saved-job listing/staging/removal, retries, caching, Python 3.11–3.14 CI, and tagged PyPI releases are implemented. Saved-job save is currently blocked by the challenged raw hydration route. Filters/facets, tracker search, saved-job pagination, and browser OAuth login remain deferred until their upstream contracts are verified.
|
|
242
|
+
|
|
243
|
+
See [`roadmap.md`](roadmap.md) and [`specs/`](specs/) for the complete implementation status and PR specifications.
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# HiringCafe CLI
|
|
2
|
+
|
|
3
|
+
Give your AI agent Hiring Cafe superpowers: search jobs, inspect postings, and keep your real Hiring Cafe saved-job board in sync without a browser.
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/hiringcafe-cli/)
|
|
6
|
+
[](https://pypi.org/project/hiringcafe-cli/)
|
|
7
|
+
[](https://github.com/chen-gdp/hiringcafe-cli/actions/workflows/ci.yaml)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
hiringcafe search "python developer" --jsonl
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The CLI gives your agent structured access to public jobs and, when authenticated, selected actions on your real Hiring Cafe account. Public search and job details use Hiring Cafe's [documented read-only MCP connector](https://hiringcafe.com/documentation/hiringcafe-claude-connector): each search returns at most 10 jobs, with at most one job per company and no pagination or filters yet. Requests are rate-limited; avoid high-frequency automated polling.
|
|
15
|
+
|
|
16
|
+
> **Unofficial client.** This project is not affiliated with, endorsed by, or supported by Hiring Cafe. Use your own account, respect the service, and expect upstream APIs to change.
|
|
17
|
+
|
|
18
|
+
## Contents
|
|
19
|
+
|
|
20
|
+
- [Install](#install)
|
|
21
|
+
- [For agents](#for-agents)
|
|
22
|
+
- [Commands](#commands)
|
|
23
|
+
- [Authentication](#authentication)
|
|
24
|
+
- [Saved jobs](#saved-jobs)
|
|
25
|
+
- [Caching and local data](#caching-and-local-data)
|
|
26
|
+
- [Exit codes](#exit-codes)
|
|
27
|
+
- [Development](#development)
|
|
28
|
+
- [Project status](#project-status)
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
HiringCafe CLI is published on PyPI and supports Python 3.11–3.14 with no runtime dependencies.
|
|
33
|
+
|
|
34
|
+
Install it as a persistent CLI tool:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
uv tool install hiringcafe-cli
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Or, if you prefer pipx:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pipx install hiringcafe-cli
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Run it once without installing anything:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
uvx --from hiringcafe-cli hiringcafe --version
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Verify the installed version:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
hiringcafe --version
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## For agents
|
|
59
|
+
|
|
60
|
+
The smallest useful loop is install, search, and consume machine-readable output:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
uv tool install hiringcafe-cli
|
|
64
|
+
hiringcafe search "python developer" --page 0 --json
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
For a fresh result, bypass the anonymous-read cache:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
hiringcafe search "python developer" --no-cache --json
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Stream the returned jobs as one JSON object per line (up to 10 results):
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
hiringcafe search "python developer" --jsonl
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Extract the normalized jobs from one JSON page:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
hiringcafe search "python developer" --page 0 --json | jq -c '.result.jobs[]'
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Ask only for the result count:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
hiringcafe count "python developer" --json
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Import an existing Firebase refresh token without an interactive password prompt:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
printf '%s\n' "$HIRINGCAFE_REFRESH_TOKEN" | hiringcafe auth import
|
|
95
|
+
hiringcafe saved-jobs list --json
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Agent automation contract:
|
|
99
|
+
|
|
100
|
+
- JSON success output goes to stdout; diagnostics and cache notices go to stderr.
|
|
101
|
+
- Stable exit codes describe transport, input, rate-limit, auth, and conflict outcomes.
|
|
102
|
+
- Provider passwords and Firebase ID tokens are never written to disk.
|
|
103
|
+
- Authenticated mutations are never retried or cached.
|
|
104
|
+
- Anonymous search/count responses use a 15-minute cache; job details use 24 hours.
|
|
105
|
+
- Search returns at most 10 jobs, one per company; pagination and filters are not supported by the current upstream interface.
|
|
106
|
+
- Invalid arguments fail before a network request is made.
|
|
107
|
+
|
|
108
|
+
## Commands
|
|
109
|
+
|
|
110
|
+
| Command | Purpose |
|
|
111
|
+
|---|---|
|
|
112
|
+
| `hiringcafe search "query" --json` | Search jobs and print structured JSON |
|
|
113
|
+
| `hiringcafe search "query" --jsonl` | Stream up to 10 search results as JSON Lines |
|
|
114
|
+
| `hiringcafe count "query" --json` | Print only the result count |
|
|
115
|
+
| `hiringcafe job show <objectID> --json` | Inspect one job posting |
|
|
116
|
+
| `hiringcafe auth login --email you@example.com` | Authenticate interactively |
|
|
117
|
+
| `hiringcafe auth import` | Import an existing Firebase refresh token |
|
|
118
|
+
| `hiringcafe auth status` | Check whether a local session exists |
|
|
119
|
+
| `hiringcafe auth logout` | Remove the local session |
|
|
120
|
+
| `hiringcafe saved-jobs list --json` | List saved jobs |
|
|
121
|
+
| `hiringcafe saved-jobs save <objectID>` | Save a job |
|
|
122
|
+
| `hiringcafe saved-jobs stage <objectID> --stage applied` | Change a job stage |
|
|
123
|
+
| `hiringcafe saved-jobs remove <objectID> --yes` | Remove a saved job |
|
|
124
|
+
| `hiringcafe cache purge` | Remove cached anonymous reads |
|
|
125
|
+
|
|
126
|
+
Every command supports `--help`, for example:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
hiringcafe search --help
|
|
130
|
+
hiringcafe saved-jobs save --help
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Authentication
|
|
134
|
+
|
|
135
|
+
For human use, log in with your Hiring Cafe email:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
hiringcafe auth login --email you@example.com
|
|
139
|
+
hiringcafe auth status
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The password is read from hidden input (or `HIRINGCAFE_PASSWORD` for automation), exchanged once for Firebase credentials, and only the refresh token is stored.
|
|
143
|
+
|
|
144
|
+
For automation, import a refresh token that you already hold:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
printf '%s\n' "$HIRINGCAFE_REFRESH_TOKEN" | hiringcafe auth import
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The CLI stores only the refresh token in an owner-only local session file. It does not store your password or short-lived Firebase ID token. Browser OAuth login is intentionally deferred until its real provider and loopback callback behavior can be verified.
|
|
151
|
+
|
|
152
|
+
## Saved jobs
|
|
153
|
+
|
|
154
|
+
These commands modify your real Hiring Cafe account. Review object IDs and confirmation prompts before changing data.
|
|
155
|
+
|
|
156
|
+
`saved-jobs save` currently depends on an older raw job-hydration endpoint that is returning a Cloudflare challenge from the CLI host. It may fail before saving; the read-only MCP connector does not support saved-job writes or provide the raw object needed by this command. Listing, staging, and removing saved jobs use separate authenticated flows.
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
hiringcafe saved-jobs save <objectID> --stage saved
|
|
160
|
+
hiringcafe saved-jobs stage <objectID> --stage interviewing
|
|
161
|
+
hiringcafe saved-jobs remove <objectID> --yes
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Valid stages are `saved`, `applied`, `interviewing`, `offer`, `rejected`, `ghosted`, and `hidden`.
|
|
165
|
+
|
|
166
|
+
## Caching and local data
|
|
167
|
+
|
|
168
|
+
| Data | Location | Notes |
|
|
169
|
+
|---:|---|---|
|
|
170
|
+
| Refresh token | `~/.config/hiringcafe-cli/session.json` | Owner-only file; removed by logout |
|
|
171
|
+
| Anonymous cache | `~/.cache/hiringcafe-cli/cache/v1/` | Public search and job data only |
|
|
172
|
+
|
|
173
|
+
Set `HIRINGCAFE_CONFIG_HOME` or `HIRINGCAFE_CACHE_HOME` to use different roots.
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
hiringcafe search "python developer" --no-cache
|
|
177
|
+
hiringcafe search "python developer" --cache-ttl 3600
|
|
178
|
+
hiringcafe cache purge
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Authenticated saved-job operations are never cached.
|
|
182
|
+
|
|
183
|
+
## Exit codes
|
|
184
|
+
|
|
185
|
+
| Code | Agent action |
|
|
186
|
+
|---:|---|
|
|
187
|
+
| `0` | Success |
|
|
188
|
+
| `1` | Transport, HTTP, parsing, or API error; inspect stderr |
|
|
189
|
+
| `2` | Invalid arguments or search state; fix the request |
|
|
190
|
+
| `3` | Rate limited; wait and retry |
|
|
191
|
+
| `4` | No stored credential; authenticate first |
|
|
192
|
+
| `5` | Stored credential rejected; re-authenticate |
|
|
193
|
+
| `6` | Write conflict or prerequisite missing; refresh state |
|
|
194
|
+
|
|
195
|
+
## Development
|
|
196
|
+
|
|
197
|
+
Requires Python 3.11+ and [uv](https://docs.astral.sh/uv/).
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
git clone https://github.com/chen-gdp/hiringcafe-cli.git
|
|
201
|
+
cd hiringcafe-cli
|
|
202
|
+
uv sync
|
|
203
|
+
uv run pytest
|
|
204
|
+
uv run hiringcafe --help
|
|
205
|
+
uv run pre-commit run --all-files
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Normal tests do not access the network. The verified anonymous endpoint probe is opt-in:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
LIVE=1 uv run hiringcafe probe
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Every implementation change traces to `specs/pr-*/spec.md` and the roadmap. Never commit credentials, Firebase tokens, cookies, real UIDs, emails, or private account data.
|
|
215
|
+
|
|
216
|
+
## Project status
|
|
217
|
+
|
|
218
|
+
Public search, job details, authentication, saved-job listing/staging/removal, retries, caching, Python 3.11–3.14 CI, and tagged PyPI releases are implemented. Saved-job save is currently blocked by the challenged raw hydration route. Filters/facets, tracker search, saved-job pagination, and browser OAuth login remain deferred until their upstream contracts are verified.
|
|
219
|
+
|
|
220
|
+
See [`roadmap.md`](roadmap.md) and [`specs/`](specs/) for the complete implementation status and PR specifications.
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
4
|
import sys
|
|
5
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
5
6
|
|
|
6
7
|
from hiringcafe_cli import (
|
|
7
8
|
auth,
|
|
@@ -21,7 +22,10 @@ from hiringcafe_cli.saved_jobs import VALID_STAGES
|
|
|
21
22
|
from hiringcafe_cli.saved_jobs_list import FirestoreError
|
|
22
23
|
from hiringcafe_cli.search_state import InvalidSearchState
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
try:
|
|
26
|
+
__version__ = version("hiringcafe-cli")
|
|
27
|
+
except PackageNotFoundError: # pragma: no cover - source-tree fallback
|
|
28
|
+
__version__ = "0.0.0+unknown"
|
|
25
29
|
|
|
26
30
|
EXIT_CONTRACT = """\
|
|
27
31
|
Exit codes:
|
|
@@ -8,7 +8,7 @@ from hiringcafe_cli.search import search
|
|
|
8
8
|
def count(
|
|
9
9
|
query: str, raw_search_state: str | None = None, no_cache: bool = False, cache_ttl: int | None = None
|
|
10
10
|
) -> dict:
|
|
11
|
-
"""Count results
|
|
11
|
+
"""Count results using the supported public search response.
|
|
12
12
|
|
|
13
13
|
Args:
|
|
14
14
|
query: Query-text mode.
|
|
@@ -18,8 +18,9 @@ BROWSER_HEADERS = {
|
|
|
18
18
|
"(KHTML, like Gecko) Chrome/128.0 Safari/537.36 hiringcafe-cli/0.1"
|
|
19
19
|
),
|
|
20
20
|
}
|
|
21
|
-
REQUIRED_SSR_KEYS = ("ssrHits", "ssrPage", "ssrTotalCount", "ssrPageSize", "ssrIsLastPage")
|
|
22
21
|
RETRYABLE_STATUSES = {408, 429, *range(500, 600)}
|
|
22
|
+
MCP_URL_PATH = "/api/mcp"
|
|
23
|
+
MCP_TOOLS = {"search_jobs", "get_job_details"}
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
class HireCafeHTTPError(RuntimeError):
|
|
@@ -88,41 +89,6 @@ class RetryPolicy:
|
|
|
88
89
|
return random.uniform(0, self.base * (2**attempt))
|
|
89
90
|
|
|
90
91
|
|
|
91
|
-
def parse_ssr_page(html: str) -> dict:
|
|
92
|
-
"""Parse and validate pageProps from a HiringCafe SSR response.
|
|
93
|
-
|
|
94
|
-
Args:
|
|
95
|
-
html: Complete SSR HTML document.
|
|
96
|
-
|
|
97
|
-
Returns:
|
|
98
|
-
The validated pageProps object.
|
|
99
|
-
|
|
100
|
-
Raises:
|
|
101
|
-
ValueError: If __NEXT_DATA__ is missing, malformed, or incomplete.
|
|
102
|
-
"""
|
|
103
|
-
marker_start = '<script id="__NEXT_DATA__" type="application/json">'
|
|
104
|
-
fallback_start = '<script id="__NEXT_DATA__">'
|
|
105
|
-
starts = [index for index in (html.find(marker_start), html.find(fallback_start)) if index >= 0]
|
|
106
|
-
if not starts:
|
|
107
|
-
raise ValueError("missing __NEXT_DATA__")
|
|
108
|
-
start = min(starts)
|
|
109
|
-
content_start = html.index(">", start) + 1
|
|
110
|
-
end = html.find("</script>", content_start)
|
|
111
|
-
if end < 0:
|
|
112
|
-
raise ValueError("unterminated __NEXT_DATA__")
|
|
113
|
-
try:
|
|
114
|
-
payload = json.loads(html[content_start:end])
|
|
115
|
-
except json.JSONDecodeError as error:
|
|
116
|
-
raise ValueError("invalid __NEXT_DATA__ JSON") from error
|
|
117
|
-
properties = payload.get("props", {}).get("pageProps", {})
|
|
118
|
-
if not isinstance(properties, dict):
|
|
119
|
-
raise ValueError("__NEXT_DATA__ pageProps is not an object")
|
|
120
|
-
for key in REQUIRED_SSR_KEYS:
|
|
121
|
-
if key not in properties:
|
|
122
|
-
raise ValueError(f"missing SSR key: {key}")
|
|
123
|
-
return properties
|
|
124
|
-
|
|
125
|
-
|
|
126
92
|
class WebClient:
|
|
127
93
|
"""Client that owns every anonymous HiringCafe web URL."""
|
|
128
94
|
|
|
@@ -139,18 +105,69 @@ class WebClient:
|
|
|
139
105
|
self.timeout = timeout
|
|
140
106
|
self.retry_policy = RetryPolicy(timeout=timeout)
|
|
141
107
|
|
|
142
|
-
def
|
|
143
|
-
"""
|
|
108
|
+
def call_mcp_tool(self, name: str, arguments: dict[str, str | int]) -> str:
|
|
109
|
+
"""Call a documented read-only HiringCafe MCP tool and return its text content.
|
|
144
110
|
|
|
145
111
|
Args:
|
|
146
|
-
|
|
147
|
-
|
|
112
|
+
name: Supported public tool name.
|
|
113
|
+
arguments: Tool arguments encoded as JSON.
|
|
148
114
|
|
|
149
115
|
Returns:
|
|
150
|
-
|
|
116
|
+
Joined textual content from the MCP result.
|
|
117
|
+
|
|
118
|
+
Raises:
|
|
119
|
+
ValueError: If the tool name or JSON-RPC response is invalid.
|
|
120
|
+
HireCafeHTTPError: If HiringCafe returns an HTTP error.
|
|
121
|
+
HireCafeTransportError: If the request cannot complete.
|
|
151
122
|
"""
|
|
152
|
-
|
|
153
|
-
|
|
123
|
+
if name not in MCP_TOOLS:
|
|
124
|
+
raise ValueError("unsupported HiringCafe MCP tool")
|
|
125
|
+
body = json.dumps(
|
|
126
|
+
{
|
|
127
|
+
"jsonrpc": "2.0",
|
|
128
|
+
"id": 1,
|
|
129
|
+
"method": "tools/call",
|
|
130
|
+
"params": {"name": name, "arguments": arguments},
|
|
131
|
+
},
|
|
132
|
+
separators=(",", ":"),
|
|
133
|
+
).encode()
|
|
134
|
+
request = Request(
|
|
135
|
+
f"{self.base_url}{MCP_URL_PATH}",
|
|
136
|
+
data=body,
|
|
137
|
+
headers={
|
|
138
|
+
"Accept": "application/json, text/event-stream",
|
|
139
|
+
"Content-Type": "application/json",
|
|
140
|
+
"User-Agent": "hiringcafe-cli",
|
|
141
|
+
},
|
|
142
|
+
method="POST",
|
|
143
|
+
)
|
|
144
|
+
try:
|
|
145
|
+
with self.urlopen(request, timeout=self.timeout) as response:
|
|
146
|
+
response_body = read_limited(response, "mcp").decode("utf-8")
|
|
147
|
+
except HTTPError as error:
|
|
148
|
+
raise HireCafeHTTPError("mcp", error.code, error.reason) from error
|
|
149
|
+
except (URLError, TimeoutError, OSError) as error:
|
|
150
|
+
raise HireCafeTransportError("mcp", error) from error
|
|
151
|
+
payload = self._json(response_body)
|
|
152
|
+
if payload.get("jsonrpc") != "2.0" or type(payload.get("id")) is not int or payload["id"] != 1:
|
|
153
|
+
raise ValueError("invalid HiringCafe MCP response")
|
|
154
|
+
result = payload.get("result")
|
|
155
|
+
if "error" in payload or not isinstance(result, dict) or result.get("isError"):
|
|
156
|
+
raise ValueError("HiringCafe MCP tool call failed")
|
|
157
|
+
content = result.get("content")
|
|
158
|
+
if not isinstance(content, list):
|
|
159
|
+
raise ValueError("HiringCafe MCP response has no text content")
|
|
160
|
+
texts = []
|
|
161
|
+
for item in content:
|
|
162
|
+
if not isinstance(item, dict) or item.get("type") != "text":
|
|
163
|
+
continue
|
|
164
|
+
text = item.get("text")
|
|
165
|
+
if not isinstance(text, str):
|
|
166
|
+
raise ValueError("HiringCafe MCP response has invalid text content")
|
|
167
|
+
texts.append(text)
|
|
168
|
+
if not texts:
|
|
169
|
+
raise ValueError("HiringCafe MCP response has no text content")
|
|
170
|
+
return "\n".join(texts)
|
|
154
171
|
|
|
155
172
|
def job_description(self, object_id: str) -> dict:
|
|
156
173
|
"""Fetch a job-description JSON object.
|