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.
Files changed (27) hide show
  1. hiringcafe_cli-0.2.0/PKG-INFO +243 -0
  2. hiringcafe_cli-0.2.0/README.md +220 -0
  3. {hiringcafe_cli-0.1.4 → hiringcafe_cli-0.2.0}/pyproject.toml +1 -1
  4. {hiringcafe_cli-0.1.4 → hiringcafe_cli-0.2.0}/pyproject.toml.orig +1 -1
  5. {hiringcafe_cli-0.1.4 → hiringcafe_cli-0.2.0}/src/hiringcafe_cli/__init__.py +5 -1
  6. {hiringcafe_cli-0.1.4 → hiringcafe_cli-0.2.0}/src/hiringcafe_cli/count.py +1 -1
  7. {hiringcafe_cli-0.1.4 → hiringcafe_cli-0.2.0}/src/hiringcafe_cli/http_client.py +60 -43
  8. {hiringcafe_cli-0.1.4 → hiringcafe_cli-0.2.0}/src/hiringcafe_cli/job.py +11 -10
  9. hiringcafe_cli-0.2.0/src/hiringcafe_cli/mcp.py +185 -0
  10. hiringcafe_cli-0.2.0/src/hiringcafe_cli/probe.py +37 -0
  11. hiringcafe_cli-0.2.0/src/hiringcafe_cli/search.py +101 -0
  12. hiringcafe_cli-0.1.4/PKG-INFO +0 -145
  13. hiringcafe_cli-0.1.4/README.md +0 -122
  14. hiringcafe_cli-0.1.4/src/hiringcafe_cli/probe.py +0 -140
  15. hiringcafe_cli-0.1.4/src/hiringcafe_cli/search.py +0 -140
  16. {hiringcafe_cli-0.1.4 → hiringcafe_cli-0.2.0}/LICENSE +0 -0
  17. {hiringcafe_cli-0.1.4 → hiringcafe_cli-0.2.0}/src/hiringcafe_cli/auth.py +0 -0
  18. {hiringcafe_cli-0.1.4 → hiringcafe_cli-0.2.0}/src/hiringcafe_cli/cache.py +0 -0
  19. {hiringcafe_cli-0.1.4 → hiringcafe_cli-0.2.0}/src/hiringcafe_cli/firebase.py +0 -0
  20. {hiringcafe_cli-0.1.4 → hiringcafe_cli-0.2.0}/src/hiringcafe_cli/io_limits.py +0 -0
  21. {hiringcafe_cli-0.1.4 → hiringcafe_cli-0.2.0}/src/hiringcafe_cli/saved_jobs.py +0 -0
  22. {hiringcafe_cli-0.1.4 → hiringcafe_cli-0.2.0}/src/hiringcafe_cli/saved_jobs_list.py +0 -0
  23. {hiringcafe_cli-0.1.4 → hiringcafe_cli-0.2.0}/src/hiringcafe_cli/saved_jobs_remove.py +0 -0
  24. {hiringcafe_cli-0.1.4 → hiringcafe_cli-0.2.0}/src/hiringcafe_cli/saved_jobs_save.py +0 -0
  25. {hiringcafe_cli-0.1.4 → hiringcafe_cli-0.2.0}/src/hiringcafe_cli/saved_jobs_stage.py +0 -0
  26. {hiringcafe_cli-0.1.4 → hiringcafe_cli-0.2.0}/src/hiringcafe_cli/search_state.py +0 -0
  27. {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
+ [![PyPI](https://img.shields.io/pypi/v/hiringcafe-cli.svg)](https://pypi.org/project/hiringcafe-cli/)
29
+ [![Python](https://img.shields.io/pypi/pyversions/hiringcafe-cli.svg)](https://pypi.org/project/hiringcafe-cli/)
30
+ [![CI](https://github.com/chen-gdp/hiringcafe-cli/actions/workflows/ci.yaml/badge.svg)](https://github.com/chen-gdp/hiringcafe-cli/actions/workflows/ci.yaml)
31
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
+ [![PyPI](https://img.shields.io/pypi/v/hiringcafe-cli.svg)](https://pypi.org/project/hiringcafe-cli/)
6
+ [![Python](https://img.shields.io/pypi/pyversions/hiringcafe-cli.svg)](https://pypi.org/project/hiringcafe-cli/)
7
+ [![CI](https://github.com/chen-gdp/hiringcafe-cli/actions/workflows/ci.yaml/badge.svg)](https://github.com/chen-gdp/hiringcafe-cli/actions/workflows/ci.yaml)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "hiringcafe-cli"
3
- version = "0.1.4"
3
+ version = "0.2.0"
4
4
  description = "Spec-driven Python CLI for Hiring Cafe job search and saved-job tracking"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "hiringcafe-cli"
3
- version = "0.1.4"
3
+ version = "0.2.0"
4
4
  description = "Spec-driven Python CLI for Hiring Cafe job search and saved-job tracking"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -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
- __version__ = "0.1.0"
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 by requesting page zero.
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 search_page(self, search_state: dict, page: int) -> dict:
143
- """Fetch one SSR search page.
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
- search_state: Complete search state object.
147
- page: Nonnegative page number.
112
+ name: Supported public tool name.
113
+ arguments: Tool arguments encoded as JSON.
148
114
 
149
115
  Returns:
150
- Validated ``props.pageProps``.
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
- query = urllib.parse.urlencode({"searchState": json.dumps(search_state), "page": page})
153
- return parse_ssr_page(self._get(f"{self.base_url}/?{query}", "search"))
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.