job-hunter-kit 0.1__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.
- job_hunter_kit-0.1/LICENSE +21 -0
- job_hunter_kit-0.1/PKG-INFO +123 -0
- job_hunter_kit-0.1/README.md +80 -0
- job_hunter_kit-0.1/job_hunter/__init__.py +6 -0
- job_hunter_kit-0.1/job_hunter/agent_context/__init__.py +24 -0
- job_hunter_kit-0.1/job_hunter/agent_context/_types.py +25 -0
- job_hunter_kit-0.1/job_hunter/agent_context/_utils.py +44 -0
- job_hunter_kit-0.1/job_hunter/agent_context/batch.py +125 -0
- job_hunter_kit-0.1/job_hunter/agent_context/briefing.py +194 -0
- job_hunter_kit-0.1/job_hunter/agent_context/candidates.py +219 -0
- job_hunter_kit-0.1/job_hunter/agent_context/lifecycle.py +205 -0
- job_hunter_kit-0.1/job_hunter/agent_context/score_context.py +77 -0
- job_hunter_kit-0.1/job_hunter/agent_context/stories.py +121 -0
- job_hunter_kit-0.1/job_hunter/briefing.py +74 -0
- job_hunter_kit-0.1/job_hunter/cli/__init__.py +1094 -0
- job_hunter_kit-0.1/job_hunter/cli/__main__.py +7 -0
- job_hunter_kit-0.1/job_hunter/cli/_dispatch.py +134 -0
- job_hunter_kit-0.1/job_hunter/config/__init__.py +27 -0
- job_hunter_kit-0.1/job_hunter/config/defaults.py +307 -0
- job_hunter_kit-0.1/job_hunter/config/loader.py +224 -0
- job_hunter_kit-0.1/job_hunter/constants.py +9 -0
- job_hunter_kit-0.1/job_hunter/core/__init__.py +1 -0
- job_hunter_kit-0.1/job_hunter/core/api_budget.py +139 -0
- job_hunter_kit-0.1/job_hunter/core/config.py +66 -0
- job_hunter_kit-0.1/job_hunter/core/config_schema.py +35 -0
- job_hunter_kit-0.1/job_hunter/core/llm_utils.py +84 -0
- job_hunter_kit-0.1/job_hunter/core/metrics.py +22 -0
- job_hunter_kit-0.1/job_hunter/core/url_liveness.py +49 -0
- job_hunter_kit-0.1/job_hunter/core/utils.py +64 -0
- job_hunter_kit-0.1/job_hunter/data_contract.py +89 -0
- job_hunter_kit-0.1/job_hunter/linkedin/__init__.py +0 -0
- job_hunter_kit-0.1/job_hunter/linkedin/_config.py +190 -0
- job_hunter_kit-0.1/job_hunter/linkedin/defaults.yml +81 -0
- job_hunter_kit-0.1/job_hunter/linkedin/drafts.py +160 -0
- job_hunter_kit-0.1/job_hunter/linkedin/engagement.py +598 -0
- job_hunter_kit-0.1/job_hunter/linkedin/ideas.py +135 -0
- job_hunter_kit-0.1/job_hunter/llm/__init__.py +9 -0
- job_hunter_kit-0.1/job_hunter/llm/client.py +246 -0
- job_hunter_kit-0.1/job_hunter/models.py +221 -0
- job_hunter_kit-0.1/job_hunter/pipeline/__init__.py +0 -0
- job_hunter_kit-0.1/job_hunter/pipeline/cover_writer.py +197 -0
- job_hunter_kit-0.1/job_hunter/pipeline/enrichment.py +184 -0
- job_hunter_kit-0.1/job_hunter/pipeline/hunt.py +193 -0
- job_hunter_kit-0.1/job_hunter/pipeline/llm_stage.py +74 -0
- job_hunter_kit-0.1/job_hunter/pipeline/orchestrator.py +492 -0
- job_hunter_kit-0.1/job_hunter/pipeline/pdf_compiler.py +115 -0
- job_hunter_kit-0.1/job_hunter/pipeline/readme_writer.py +201 -0
- job_hunter_kit-0.1/job_hunter/pipeline/resolve_hunt_region.py +122 -0
- job_hunter_kit-0.1/job_hunter/pipeline/scorer.py +284 -0
- job_hunter_kit-0.1/job_hunter/pipeline/snapshot.py +43 -0
- job_hunter_kit-0.1/job_hunter/pipeline/tailor.py +120 -0
- job_hunter_kit-0.1/job_hunter/pipeline/tailorer.py +223 -0
- job_hunter_kit-0.1/job_hunter/pipeline/validator.py +235 -0
- job_hunter_kit-0.1/job_hunter/sources/__init__.py +0 -0
- job_hunter_kit-0.1/job_hunter/sources/_base.py +57 -0
- job_hunter_kit-0.1/job_hunter/sources/_policy.py +298 -0
- job_hunter_kit-0.1/job_hunter/sources/_scraper.py +11 -0
- job_hunter_kit-0.1/job_hunter/sources/adzuna_source.py +182 -0
- job_hunter_kit-0.1/job_hunter/sources/ai_web_search.py +507 -0
- job_hunter_kit-0.1/job_hunter/sources/arbeitsagentur_source.py +104 -0
- job_hunter_kit-0.1/job_hunter/sources/ats/__init__.py +53 -0
- job_hunter_kit-0.1/job_hunter/sources/ats/_base.py +68 -0
- job_hunter_kit-0.1/job_hunter/sources/ats/ashby.py +59 -0
- job_hunter_kit-0.1/job_hunter/sources/ats/bamboohr.py +79 -0
- job_hunter_kit-0.1/job_hunter/sources/ats/breezy.py +58 -0
- job_hunter_kit-0.1/job_hunter/sources/ats/dispatch.py +70 -0
- job_hunter_kit-0.1/job_hunter/sources/ats/greenhouse.py +64 -0
- job_hunter_kit-0.1/job_hunter/sources/ats/hibob.py +81 -0
- job_hunter_kit-0.1/job_hunter/sources/ats/lever.py +74 -0
- job_hunter_kit-0.1/job_hunter/sources/ats/personio.py +62 -0
- job_hunter_kit-0.1/job_hunter/sources/ats/recruitee.py +55 -0
- job_hunter_kit-0.1/job_hunter/sources/ats/smartrecruiters.py +82 -0
- job_hunter_kit-0.1/job_hunter/sources/ats/teamtailor.py +58 -0
- job_hunter_kit-0.1/job_hunter/sources/ats/workable.py +57 -0
- job_hunter_kit-0.1/job_hunter/sources/ats/workday.py +64 -0
- job_hunter_kit-0.1/job_hunter/sources/ats_urls.py +212 -0
- job_hunter_kit-0.1/job_hunter/sources/boards/__init__.py +75 -0
- job_hunter_kit-0.1/job_hunter/sources/career_pages/__init__.py +174 -0
- job_hunter_kit-0.1/job_hunter/sources/career_pages/_ats_patterns.py +129 -0
- job_hunter_kit-0.1/job_hunter/sources/career_pages/_jsonld.py +117 -0
- job_hunter_kit-0.1/job_hunter/sources/career_pages/_ladder.py +73 -0
- job_hunter_kit-0.1/job_hunter/sources/career_pages/_rendering.py +122 -0
- job_hunter_kit-0.1/job_hunter/sources/career_pages/_sitemap.py +101 -0
- job_hunter_kit-0.1/job_hunter/sources/careerjet_source.py +180 -0
- job_hunter_kit-0.1/job_hunter/sources/glints_source.py +177 -0
- job_hunter_kit-0.1/job_hunter/sources/gulftalent_source.py +198 -0
- job_hunter_kit-0.1/job_hunter/sources/himalayas_source.py +122 -0
- job_hunter_kit-0.1/job_hunter/sources/jd_fetcher.py +778 -0
- job_hunter_kit-0.1/job_hunter/sources/job_boards.py +289 -0
- job_hunter_kit-0.1/job_hunter/sources/jobbank_source.py +130 -0
- job_hunter_kit-0.1/job_hunter/sources/jobicy_source.py +89 -0
- job_hunter_kit-0.1/job_hunter/sources/jobspy_source.py +263 -0
- job_hunter_kit-0.1/job_hunter/sources/jobstreet_source.py +258 -0
- job_hunter_kit-0.1/job_hunter/sources/jooble_source.py +124 -0
- job_hunter_kit-0.1/job_hunter/sources/llm_search.py +15 -0
- job_hunter_kit-0.1/job_hunter/sources/mycareersfuture_source.py +145 -0
- job_hunter_kit-0.1/job_hunter/sources/orchestrator.py +114 -0
- job_hunter_kit-0.1/job_hunter/sources/reed_source.py +154 -0
- job_hunter_kit-0.1/job_hunter/sources/remoteok_source.py +87 -0
- job_hunter_kit-0.1/job_hunter/sources/remotive_source.py +89 -0
- job_hunter_kit-0.1/job_hunter/sources/scraper/__init__.py +23 -0
- job_hunter_kit-0.1/job_hunter/sources/scraper/_boards.py +227 -0
- job_hunter_kit-0.1/job_hunter/sources/scraper/_companies.py +32 -0
- job_hunter_kit-0.1/job_hunter/sources/scraper/_config.py +36 -0
- job_hunter_kit-0.1/job_hunter/sources/scraper/_discovery.py +88 -0
- job_hunter_kit-0.1/job_hunter/sources/scraper/_stats.py +91 -0
- job_hunter_kit-0.1/job_hunter/sources/search_providers/__init__.py +122 -0
- job_hunter_kit-0.1/job_hunter/sources/search_providers/_constants.py +76 -0
- job_hunter_kit-0.1/job_hunter/sources/search_providers/_result.py +34 -0
- job_hunter_kit-0.1/job_hunter/sources/search_providers/_url_utils.py +61 -0
- job_hunter_kit-0.1/job_hunter/sources/search_providers/ats_discovery.py +378 -0
- job_hunter_kit-0.1/job_hunter/sources/search_providers/discovery.py +41 -0
- job_hunter_kit-0.1/job_hunter/sources/search_providers/fetchers.py +267 -0
- job_hunter_kit-0.1/job_hunter/sources/search_providers/preflight.py +608 -0
- job_hunter_kit-0.1/job_hunter/sources/search_providers/providers.py +151 -0
- job_hunter_kit-0.1/job_hunter/sources/search_providers/router.py +270 -0
- job_hunter_kit-0.1/job_hunter/sources/source_config.py +91 -0
- job_hunter_kit-0.1/job_hunter/sources/the_muse_source.py +100 -0
- job_hunter_kit-0.1/job_hunter/sources/web_search/__init__.py +38 -0
- job_hunter_kit-0.1/job_hunter/sources/weworkremotely_source.py +95 -0
- job_hunter_kit-0.1/job_hunter/sources/workingnomads_source.py +75 -0
- job_hunter_kit-0.1/job_hunter/templates/__init__.py +1 -0
- job_hunter_kit-0.1/job_hunter/templates/workspace/.claude/skills/job-hunter/SKILL.md +137 -0
- job_hunter_kit-0.1/job_hunter/templates/workspace/.claude/skills/job-hunter/modes/batch.md +99 -0
- job_hunter_kit-0.1/job_hunter/templates/workspace/.claude/skills/job-hunter/modes/brief.md +17 -0
- job_hunter_kit-0.1/job_hunter/templates/workspace/.claude/skills/job-hunter/modes/finalize.md +55 -0
- job_hunter_kit-0.1/job_hunter/templates/workspace/.claude/skills/job-hunter/modes/interview.md +33 -0
- job_hunter_kit-0.1/job_hunter/templates/workspace/.claude/skills/job-hunter/modes/one.md +40 -0
- job_hunter_kit-0.1/job_hunter/templates/workspace/.claude/skills/job-hunter/modes/outreach.md +34 -0
- job_hunter_kit-0.1/job_hunter/templates/workspace/.claude/skills/job-hunter/modes/research.md +41 -0
- job_hunter_kit-0.1/job_hunter/templates/workspace/.claude/skills/job-hunter/modes/score.md +68 -0
- job_hunter_kit-0.1/job_hunter/templates/workspace/.claude/skills/job-hunter/modes/screen.md +44 -0
- job_hunter_kit-0.1/job_hunter/templates/workspace/.claude/skills/job-hunter/modes/search.md +38 -0
- job_hunter_kit-0.1/job_hunter/templates/workspace/.claude/skills/job-hunter/modes/stories.md +33 -0
- job_hunter_kit-0.1/job_hunter/templates/workspace/.claude/skills/job-hunter/modes/tailor.md +44 -0
- job_hunter_kit-0.1/job_hunter/templates/workspace/.claude/skills/linkedin/SKILL.md +47 -0
- job_hunter_kit-0.1/job_hunter/templates/workspace/.claude/skills/linkedin/modes/draft.md +31 -0
- job_hunter_kit-0.1/job_hunter/templates/workspace/.claude/skills/linkedin/modes/engage.md +31 -0
- job_hunter_kit-0.1/job_hunter/templates/workspace/.claude/skills/linkedin/modes/ideas.md +29 -0
- job_hunter_kit-0.1/job_hunter/templates/workspace/.claude/skills/linkedin/modes/network.md +30 -0
- job_hunter_kit-0.1/job_hunter/templates/workspace/.claude/skills/setup/SKILL.md +39 -0
- job_hunter_kit-0.1/job_hunter/templates/workspace/.claude/skills/setup/modes/doctor.md +44 -0
- job_hunter_kit-0.1/job_hunter/templates/workspace/.claude/skills/setup/modes/onboard.md +45 -0
- job_hunter_kit-0.1/job_hunter/templates/workspace/.claude/skills/setup/modes/region.md +57 -0
- job_hunter_kit-0.1/job_hunter/templates/workspace/.claude/skills/setup/modes/stories.md +33 -0
- job_hunter_kit-0.1/job_hunter/templates/workspace/.claude/skills/setup/modes/style.md +33 -0
- job_hunter_kit-0.1/job_hunter/templates/workspace/.env.example +24 -0
- job_hunter_kit-0.1/job_hunter/templates/workspace/.github/copilot-instructions.md +49 -0
- job_hunter_kit-0.1/job_hunter/templates/workspace/.github/workflows/find-jobs.yml +172 -0
- job_hunter_kit-0.1/job_hunter/templates/workspace/.github/workflows/linkedin.yml +72 -0
- job_hunter_kit-0.1/job_hunter/templates/workspace/.github/workflows/tailor-job.yml +92 -0
- job_hunter_kit-0.1/job_hunter/templates/workspace/.gitignore +18 -0
- job_hunter_kit-0.1/job_hunter/templates/workspace/AGENTS.md +53 -0
- job_hunter_kit-0.1/job_hunter/templates/workspace/CLAUDE.md +1 -0
- job_hunter_kit-0.1/job_hunter/templates/workspace/GEMINI.md +1 -0
- job_hunter_kit-0.1/job_hunter/templates/workspace/README.md +30 -0
- job_hunter_kit-0.1/job_hunter/templates/workspace/config/job_hunter.yml +105 -0
- job_hunter_kit-0.1/job_hunter/templates/workspace/config/schemas/job_hunter.schema.json +148 -0
- job_hunter_kit-0.1/job_hunter/templates/workspace/outputs/state/discovered_urls.yml +5 -0
- job_hunter_kit-0.1/job_hunter/templates/workspace/profile/altacv.cls +491 -0
- job_hunter_kit-0.1/job_hunter/templates/workspace/profile/career_context.md +89 -0
- job_hunter_kit-0.1/job_hunter/templates/workspace/profile/resume_double_column.tex +237 -0
- job_hunter_kit-0.1/job_hunter/templates/workspace/profile/resume_single_column.tex +220 -0
- job_hunter_kit-0.1/job_hunter/templates/workspace/profile/story_bank.md +25 -0
- job_hunter_kit-0.1/job_hunter/tracker.py +182 -0
- job_hunter_kit-0.1/job_hunter/tracking/__init__.py +0 -0
- job_hunter_kit-0.1/job_hunter/tracking/discovery_cache.py +110 -0
- job_hunter_kit-0.1/job_hunter/tracking/tracker.py +80 -0
- job_hunter_kit-0.1/job_hunter/update_safety.py +40 -0
- job_hunter_kit-0.1/job_hunter/ux/__init__.py +0 -0
- job_hunter_kit-0.1/job_hunter/ux/analytics.py +135 -0
- job_hunter_kit-0.1/job_hunter/ux/applications.py +286 -0
- job_hunter_kit-0.1/job_hunter/ux/briefing.py +82 -0
- job_hunter_kit-0.1/job_hunter/ux/dashboard.py +78 -0
- job_hunter_kit-0.1/job_hunter/ux/health.py +322 -0
- job_hunter_kit-0.1/job_hunter/workspace/__init__.py +1 -0
- job_hunter_kit-0.1/job_hunter/workspace/_assets.py +127 -0
- job_hunter_kit-0.1/job_hunter/workspace/init.py +84 -0
- job_hunter_kit-0.1/job_hunter/workspace/manifest.py +105 -0
- job_hunter_kit-0.1/job_hunter/workspace/skills.py +37 -0
- job_hunter_kit-0.1/job_hunter_kit.egg-info/PKG-INFO +123 -0
- job_hunter_kit-0.1/job_hunter_kit.egg-info/SOURCES.txt +234 -0
- job_hunter_kit-0.1/job_hunter_kit.egg-info/dependency_links.txt +1 -0
- job_hunter_kit-0.1/job_hunter_kit.egg-info/entry_points.txt +2 -0
- job_hunter_kit-0.1/job_hunter_kit.egg-info/requires.txt +27 -0
- job_hunter_kit-0.1/job_hunter_kit.egg-info/top_level.txt +1 -0
- job_hunter_kit-0.1/pyproject.toml +130 -0
- job_hunter_kit-0.1/setup.cfg +4 -0
- job_hunter_kit-0.1/tests/test_agent_context.py +773 -0
- job_hunter_kit-0.1/tests/test_ai_web_search.py +272 -0
- job_hunter_kit-0.1/tests/test_api_budget.py +210 -0
- job_hunter_kit-0.1/tests/test_applications.py +160 -0
- job_hunter_kit-0.1/tests/test_arbeitsagentur_source.py +88 -0
- job_hunter_kit-0.1/tests/test_ats.py +521 -0
- job_hunter_kit-0.1/tests/test_ats_urls.py +112 -0
- job_hunter_kit-0.1/tests/test_career_pages.py +321 -0
- job_hunter_kit-0.1/tests/test_cli.py +354 -0
- job_hunter_kit-0.1/tests/test_config.py +180 -0
- job_hunter_kit-0.1/tests/test_config_yaml.py +73 -0
- job_hunter_kit-0.1/tests/test_core_utils.py +13 -0
- job_hunter_kit-0.1/tests/test_cover_writer.py +99 -0
- job_hunter_kit-0.1/tests/test_dashboard_analytics.py +68 -0
- job_hunter_kit-0.1/tests/test_data_contract.py +73 -0
- job_hunter_kit-0.1/tests/test_enrichment.py +78 -0
- job_hunter_kit-0.1/tests/test_find_jobs_e2e.py +88 -0
- job_hunter_kit-0.1/tests/test_health.py +101 -0
- job_hunter_kit-0.1/tests/test_himalayas_source.py +122 -0
- job_hunter_kit-0.1/tests/test_hunt_pipeline.py +81 -0
- job_hunter_kit-0.1/tests/test_jd_fetcher.py +223 -0
- job_hunter_kit-0.1/tests/test_job_boards.py +446 -0
- job_hunter_kit-0.1/tests/test_job_policy.py +139 -0
- job_hunter_kit-0.1/tests/test_jobspy_source.py +240 -0
- job_hunter_kit-0.1/tests/test_linkedin.py +464 -0
- job_hunter_kit-0.1/tests/test_llm_client.py +76 -0
- job_hunter_kit-0.1/tests/test_llm_utils.py +64 -0
- job_hunter_kit-0.1/tests/test_models.py +32 -0
- job_hunter_kit-0.1/tests/test_new_sources.py +785 -0
- job_hunter_kit-0.1/tests/test_orchestrator.py +343 -0
- job_hunter_kit-0.1/tests/test_pdf_compiler.py +139 -0
- job_hunter_kit-0.1/tests/test_preflight.py +75 -0
- job_hunter_kit-0.1/tests/test_remotive_source.py +85 -0
- job_hunter_kit-0.1/tests/test_resolve_hunt_region.py +102 -0
- job_hunter_kit-0.1/tests/test_scorer.py +284 -0
- job_hunter_kit-0.1/tests/test_scraper.py +600 -0
- job_hunter_kit-0.1/tests/test_search_providers.py +553 -0
- job_hunter_kit-0.1/tests/test_skill_contracts.py +171 -0
- job_hunter_kit-0.1/tests/test_skills.py +232 -0
- job_hunter_kit-0.1/tests/test_source_base.py +41 -0
- job_hunter_kit-0.1/tests/test_source_preflight.py +157 -0
- job_hunter_kit-0.1/tests/test_sources.py +1599 -0
- job_hunter_kit-0.1/tests/test_tailorer.py +134 -0
- job_hunter_kit-0.1/tests/test_the_muse_source.py +73 -0
- job_hunter_kit-0.1/tests/test_tracker.py +106 -0
- job_hunter_kit-0.1/tests/test_url_liveness.py +34 -0
- job_hunter_kit-0.1/tests/test_validator.py +234 -0
- job_hunter_kit-0.1/tests/test_workspace_init.py +131 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Abdul Basit
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: job-hunter-kit
|
|
3
|
+
Version: 0.1
|
|
4
|
+
Summary: Job search automation with LLM API pipeline mode and Claude Code agent mode.
|
|
5
|
+
Author: Abdul Basit
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/abdulrbasit/job-hunter
|
|
8
|
+
Project-URL: Repository, https://github.com/abdulrbasit/job-hunter
|
|
9
|
+
Project-URL: Issues, https://github.com/abdulrbasit/job-hunter/issues
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Office/Business
|
|
18
|
+
Requires-Python: >=3.12
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: typer>=0.12
|
|
22
|
+
Requires-Dist: anthropic>=0.50.0
|
|
23
|
+
Requires-Dist: openai>=1.68.0
|
|
24
|
+
Requires-Dist: google-genai>=1.0.0
|
|
25
|
+
Requires-Dist: requests>=2.31.0
|
|
26
|
+
Requires-Dist: beautifulsoup4>=4.12.0
|
|
27
|
+
Requires-Dist: pyyaml>=6.0
|
|
28
|
+
Requires-Dist: python-jobspy>=0.20.0
|
|
29
|
+
Requires-Dist: jsonschema>=4.0
|
|
30
|
+
Requires-Dist: headroom-ai>=0.1.0; sys_platform != "win32"
|
|
31
|
+
Provides-Extra: browser
|
|
32
|
+
Requires-Dist: playwright>=1.40.0; extra == "browser"
|
|
33
|
+
Provides-Extra: dev
|
|
34
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
35
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
36
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
37
|
+
Requires-Dist: ty>=0.0.1a20; extra == "dev"
|
|
38
|
+
Provides-Extra: secrets
|
|
39
|
+
Requires-Dist: keyring>=24.0.0; extra == "secrets"
|
|
40
|
+
Provides-Extra: all
|
|
41
|
+
Requires-Dist: job-hunter-kit[browser,secrets]; extra == "all"
|
|
42
|
+
Dynamic: license-file
|
|
43
|
+
|
|
44
|
+
# Job Hunter
|
|
45
|
+
|
|
46
|
+
Job Hunter is an installable Python package for running a personal job-search workspace. It has one CLI and two modes:
|
|
47
|
+
|
|
48
|
+
- `agent`: Python handles deterministic work; Claude/Codex skills handle screening, scoring, tailoring, and writing.
|
|
49
|
+
- `llm-api`: Python runs the autonomous LLM-backed pipeline for unattended jobs.
|
|
50
|
+
|
|
51
|
+
The default mode is `agent`.
|
|
52
|
+
|
|
53
|
+
## Install
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install job-hunter-kit
|
|
57
|
+
# or
|
|
58
|
+
uv tool install job-hunter-kit
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Create a Workspace
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
job-hunter init my-job-hunter-workspace
|
|
65
|
+
cd my-job-hunter-workspace
|
|
66
|
+
cp .env.example .env
|
|
67
|
+
job-hunter config check
|
|
68
|
+
job-hunter doctor
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Edit `config/job_hunter.yml` with deterministic machine choices: titles, regions, exclusions, profile paths, mode, scoring thresholds, LLM search gate, and provider/model choices. Put personal positioning and writing preferences in `profile/career_context.md`. Secrets use fixed environment variable names in `.env` or GitHub Actions.
|
|
72
|
+
|
|
73
|
+
## Daily Use
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
job-hunter hunt --region primary
|
|
77
|
+
job-hunter brief
|
|
78
|
+
job-hunter dashboard --no-interactive
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
In `agent` mode, open the workspace in Claude Code or Codex and use:
|
|
82
|
+
|
|
83
|
+
```text
|
|
84
|
+
/job-hunter brief
|
|
85
|
+
/job-hunter batch
|
|
86
|
+
/job-hunter one <url>
|
|
87
|
+
/job-hunter finalize
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
In `llm-api` mode, `job-hunter hunt` runs scrape, score, tailor, cover letter, PDF, tracker, and README updates in one pipeline.
|
|
91
|
+
|
|
92
|
+
## Public CLI
|
|
93
|
+
|
|
94
|
+
- `job-hunter init <workspace>` creates a workspace.
|
|
95
|
+
- `job-hunter config check` validates `config/job_hunter.yml`.
|
|
96
|
+
- `job-hunter doctor` checks setup health.
|
|
97
|
+
- `job-hunter hunt` discovers and enriches jobs.
|
|
98
|
+
- `job-hunter brief` writes the daily briefing.
|
|
99
|
+
- `job-hunter tailor` processes provided job URLs or JD text.
|
|
100
|
+
- `job-hunter dashboard`, `job-hunter applications`, and `job-hunter analytics` inspect application state.
|
|
101
|
+
- `job-hunter update-skills` refreshes bundled `.claude/skills/` only.
|
|
102
|
+
- `job-hunter version` and `job-hunter update-info` show version and upgrade guidance.
|
|
103
|
+
|
|
104
|
+
Support commands such as `agent-context`, `import-job`, `compile-pdf`, `update-readme`, `mark-processed`, `discard-job`, `cleanup-transient`, and `finalize-run` exist for skills and automation.
|
|
105
|
+
|
|
106
|
+
## Data Contract
|
|
107
|
+
|
|
108
|
+
Deterministic user choices live in `config/job_hunter.yml`; human career and writing guidance lives in `profile/career_context.md`. Persistent URL dedup lives in `outputs/state/discovered_urls.yml`. Product updates must not overwrite `config/`, `profile/`, `outputs/`, or `.env`.
|
|
109
|
+
|
|
110
|
+
See `DATA_CONTRACT.md` for the full contract.
|
|
111
|
+
|
|
112
|
+
## Development
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
uv sync --extra dev
|
|
116
|
+
uv run pytest tests/ -q --tb=short
|
|
117
|
+
uv run ruff format --check job_hunter tests .github/scripts
|
|
118
|
+
uv run ruff check job_hunter tests .github/scripts
|
|
119
|
+
uv run ty check job_hunter tests
|
|
120
|
+
uv build
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
MIT licensed. See `CONTRIBUTING.md`.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Job Hunter
|
|
2
|
+
|
|
3
|
+
Job Hunter is an installable Python package for running a personal job-search workspace. It has one CLI and two modes:
|
|
4
|
+
|
|
5
|
+
- `agent`: Python handles deterministic work; Claude/Codex skills handle screening, scoring, tailoring, and writing.
|
|
6
|
+
- `llm-api`: Python runs the autonomous LLM-backed pipeline for unattended jobs.
|
|
7
|
+
|
|
8
|
+
The default mode is `agent`.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install job-hunter-kit
|
|
14
|
+
# or
|
|
15
|
+
uv tool install job-hunter-kit
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Create a Workspace
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
job-hunter init my-job-hunter-workspace
|
|
22
|
+
cd my-job-hunter-workspace
|
|
23
|
+
cp .env.example .env
|
|
24
|
+
job-hunter config check
|
|
25
|
+
job-hunter doctor
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Edit `config/job_hunter.yml` with deterministic machine choices: titles, regions, exclusions, profile paths, mode, scoring thresholds, LLM search gate, and provider/model choices. Put personal positioning and writing preferences in `profile/career_context.md`. Secrets use fixed environment variable names in `.env` or GitHub Actions.
|
|
29
|
+
|
|
30
|
+
## Daily Use
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
job-hunter hunt --region primary
|
|
34
|
+
job-hunter brief
|
|
35
|
+
job-hunter dashboard --no-interactive
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
In `agent` mode, open the workspace in Claude Code or Codex and use:
|
|
39
|
+
|
|
40
|
+
```text
|
|
41
|
+
/job-hunter brief
|
|
42
|
+
/job-hunter batch
|
|
43
|
+
/job-hunter one <url>
|
|
44
|
+
/job-hunter finalize
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
In `llm-api` mode, `job-hunter hunt` runs scrape, score, tailor, cover letter, PDF, tracker, and README updates in one pipeline.
|
|
48
|
+
|
|
49
|
+
## Public CLI
|
|
50
|
+
|
|
51
|
+
- `job-hunter init <workspace>` creates a workspace.
|
|
52
|
+
- `job-hunter config check` validates `config/job_hunter.yml`.
|
|
53
|
+
- `job-hunter doctor` checks setup health.
|
|
54
|
+
- `job-hunter hunt` discovers and enriches jobs.
|
|
55
|
+
- `job-hunter brief` writes the daily briefing.
|
|
56
|
+
- `job-hunter tailor` processes provided job URLs or JD text.
|
|
57
|
+
- `job-hunter dashboard`, `job-hunter applications`, and `job-hunter analytics` inspect application state.
|
|
58
|
+
- `job-hunter update-skills` refreshes bundled `.claude/skills/` only.
|
|
59
|
+
- `job-hunter version` and `job-hunter update-info` show version and upgrade guidance.
|
|
60
|
+
|
|
61
|
+
Support commands such as `agent-context`, `import-job`, `compile-pdf`, `update-readme`, `mark-processed`, `discard-job`, `cleanup-transient`, and `finalize-run` exist for skills and automation.
|
|
62
|
+
|
|
63
|
+
## Data Contract
|
|
64
|
+
|
|
65
|
+
Deterministic user choices live in `config/job_hunter.yml`; human career and writing guidance lives in `profile/career_context.md`. Persistent URL dedup lives in `outputs/state/discovered_urls.yml`. Product updates must not overwrite `config/`, `profile/`, `outputs/`, or `.env`.
|
|
66
|
+
|
|
67
|
+
See `DATA_CONTRACT.md` for the full contract.
|
|
68
|
+
|
|
69
|
+
## Development
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
uv sync --extra dev
|
|
73
|
+
uv run pytest tests/ -q --tb=short
|
|
74
|
+
uv run ruff format --check job_hunter tests .github/scripts
|
|
75
|
+
uv run ruff check job_hunter tests .github/scripts
|
|
76
|
+
uv run ty check job_hunter tests
|
|
77
|
+
uv build
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
MIT licensed. See `CONTRIBUTING.md`.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Agent context package — bounded context builders for Claude Code skills."""
|
|
2
|
+
|
|
3
|
+
from job_hunter.agent_context.batch import build_candidate_batch, screen_candidate_batch
|
|
4
|
+
from job_hunter.agent_context.briefing import brief_context, linkedin_weekly_context, llm_search_config
|
|
5
|
+
from job_hunter.agent_context.candidates import build_candidate_queue, candidate_from_queue
|
|
6
|
+
from job_hunter.agent_context.lifecycle import candidate_lifecycle, validate_score_file
|
|
7
|
+
from job_hunter.agent_context.score_context import score_context
|
|
8
|
+
from job_hunter.agent_context.stories import final_stories_text, story_by_id, story_index
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"brief_context",
|
|
12
|
+
"build_candidate_batch",
|
|
13
|
+
"build_candidate_queue",
|
|
14
|
+
"candidate_from_queue",
|
|
15
|
+
"candidate_lifecycle",
|
|
16
|
+
"final_stories_text",
|
|
17
|
+
"linkedin_weekly_context",
|
|
18
|
+
"llm_search_config",
|
|
19
|
+
"score_context",
|
|
20
|
+
"screen_candidate_batch",
|
|
21
|
+
"story_by_id",
|
|
22
|
+
"story_index",
|
|
23
|
+
"validate_score_file",
|
|
24
|
+
]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Shared types and constants for agent_context sub-modules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
MAX_SNIPPET_CHARS = 700
|
|
9
|
+
MAX_JD_CHARS = 6000
|
|
10
|
+
DEFAULT_QUEUE_PATH = "outputs/state/agent_candidate_queue.json"
|
|
11
|
+
DEFAULT_CANDIDATE_SCOPE = "briefing-backlog"
|
|
12
|
+
JD_LIFECYCLE_IMPORT_STATUSES = {"thin", "fetch_failed", "page_noise"}
|
|
13
|
+
STORY_HEADING_RE = re.compile(r"^###\s+([A-Za-z0-9]+-\d+)\s+[—-]\s+(.+?)\s*$")
|
|
14
|
+
RATING_RE = re.compile(r"Rating:\s*([0-9]+(?:\.[0-9]+)?/10)")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class StoryBlock:
|
|
19
|
+
story_id: str
|
|
20
|
+
title: str
|
|
21
|
+
role: str
|
|
22
|
+
rating: str
|
|
23
|
+
tags: list[str]
|
|
24
|
+
summary: str
|
|
25
|
+
text: str
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Shared utility helpers for agent_context sub-modules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import yaml
|
|
11
|
+
|
|
12
|
+
from job_hunter.tracker import repo_path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _root(root: Path | None = None) -> Path:
|
|
16
|
+
return root if root is not None else repo_path()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _read_yaml(path: Path) -> Any:
|
|
20
|
+
if not path.exists():
|
|
21
|
+
return {}
|
|
22
|
+
return yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _read_json_or_yaml(path: Path) -> Any:
|
|
26
|
+
text = path.read_text(encoding="utf-8")
|
|
27
|
+
if path.suffix.lower() == ".json":
|
|
28
|
+
return json.loads(text)
|
|
29
|
+
return yaml.safe_load(text) or {}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _clip(value: Any, limit: int) -> str:
|
|
33
|
+
text = re.sub(r"\s+", " ", str(value or "")).strip()
|
|
34
|
+
if len(text) <= limit:
|
|
35
|
+
return text
|
|
36
|
+
suffix = " ... [truncated]"
|
|
37
|
+
if limit <= len(suffix):
|
|
38
|
+
return suffix[:limit]
|
|
39
|
+
return text[: limit - len(suffix)].rstrip() + suffix
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _resolve_path(root: Path, path: Path | str) -> Path:
|
|
43
|
+
resolved = Path(path)
|
|
44
|
+
return resolved if resolved.is_absolute() else root / resolved
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Candidate batch building and screening helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from datetime import date
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from job_hunter.agent_context._utils import _read_yaml, _root
|
|
12
|
+
from job_hunter.agent_context.candidates import _title_key
|
|
13
|
+
from job_hunter.config import get_config
|
|
14
|
+
from job_hunter.constants import DEFAULT_BATCH_SIZE
|
|
15
|
+
from job_hunter.sources._policy import JobPolicy
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def build_candidate_batch(
|
|
19
|
+
queue: dict[str, Any],
|
|
20
|
+
*,
|
|
21
|
+
batch_size: int = DEFAULT_BATCH_SIZE,
|
|
22
|
+
batch_number: int = 1,
|
|
23
|
+
) -> dict[str, Any]:
|
|
24
|
+
jobs = queue.get("jobs", []) if isinstance(queue, dict) else []
|
|
25
|
+
selected = jobs[:batch_size]
|
|
26
|
+
return {
|
|
27
|
+
"generated": date.today().isoformat(),
|
|
28
|
+
"batch_number": batch_number,
|
|
29
|
+
"batch_size": batch_size,
|
|
30
|
+
"source_queue_count": len(jobs),
|
|
31
|
+
"count": len(selected),
|
|
32
|
+
"jobs": selected,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _applied_title_keys(root: Path) -> set[str]:
|
|
37
|
+
keys: set[str] = set()
|
|
38
|
+
jobs_dir = root / "outputs" / "jobs"
|
|
39
|
+
if jobs_dir.exists():
|
|
40
|
+
for meta_path in jobs_dir.glob("*/meta.json"):
|
|
41
|
+
try:
|
|
42
|
+
meta = json.loads(meta_path.read_text(encoding="utf-8"))
|
|
43
|
+
except (OSError, json.JSONDecodeError, UnicodeDecodeError):
|
|
44
|
+
continue
|
|
45
|
+
key = _title_key(meta)
|
|
46
|
+
if key != "::":
|
|
47
|
+
keys.add(key)
|
|
48
|
+
|
|
49
|
+
readme_path = root / "README.md"
|
|
50
|
+
if readme_path.exists():
|
|
51
|
+
try:
|
|
52
|
+
readme = readme_path.read_text(encoding="utf-8")
|
|
53
|
+
except UnicodeDecodeError:
|
|
54
|
+
readme = readme_path.read_text(encoding="utf-8", errors="replace")
|
|
55
|
+
for match in re.finditer(r"\[([^\]]+?) @ ([^\]]+?)\]\(https?://", readme):
|
|
56
|
+
title, company = match.group(1), match.group(2)
|
|
57
|
+
keys.add(_title_key({"company": company, "title": title}))
|
|
58
|
+
return keys
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _region_config(search_config: dict[str, Any], region: str) -> dict[str, Any]:
|
|
62
|
+
regions = search_config.get("regions", {}) or {}
|
|
63
|
+
cfg = regions.get(region, {})
|
|
64
|
+
return cfg if isinstance(cfg, dict) else {}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def screen_candidate_batch(
|
|
68
|
+
batch: dict[str, Any],
|
|
69
|
+
*,
|
|
70
|
+
root: Path | None = None,
|
|
71
|
+
) -> dict[str, Any]:
|
|
72
|
+
base = _root(root)
|
|
73
|
+
search_config = get_config("job_hunter") if base == _root() else _read_yaml(base / "config" / "job_hunter.yml")
|
|
74
|
+
policy = JobPolicy(search_config)
|
|
75
|
+
title_filters = search_config.get("job_titles", []) or []
|
|
76
|
+
applied_keys = _applied_title_keys(base)
|
|
77
|
+
|
|
78
|
+
retained: list[dict[str, Any]] = []
|
|
79
|
+
skipped: list[dict[str, Any]] = []
|
|
80
|
+
for candidate in batch.get("jobs", []):
|
|
81
|
+
reasons: list[str] = []
|
|
82
|
+
title = str(candidate.get("title") or "")
|
|
83
|
+
snippet = str(candidate.get("snippet") or "")
|
|
84
|
+
region = str(candidate.get("region") or "")
|
|
85
|
+
if not policy.accepts_job_content(candidate, title_filters):
|
|
86
|
+
if policy.is_excluded_company(str(candidate.get("company") or "")):
|
|
87
|
+
reasons.append("excluded_company")
|
|
88
|
+
if policy.is_excluded_language(title, snippet):
|
|
89
|
+
reasons.append("excluded_language")
|
|
90
|
+
if any(term.lower() in title.lower() for term in policy.excluded_title_terms):
|
|
91
|
+
reasons.append("excluded_title")
|
|
92
|
+
if policy.is_excluded_industry(snippet):
|
|
93
|
+
reasons.append("excluded_industry")
|
|
94
|
+
if not reasons:
|
|
95
|
+
reasons.append("title_not_matched")
|
|
96
|
+
if policy.has_wrong_location(candidate, _region_config(search_config, region)):
|
|
97
|
+
reasons.append("wrong_location")
|
|
98
|
+
if policy.is_stale_posting(title, snippet):
|
|
99
|
+
reasons.append("stale_posting")
|
|
100
|
+
if _title_key(candidate) in applied_keys:
|
|
101
|
+
reasons.append("duplicate_application")
|
|
102
|
+
|
|
103
|
+
row = {
|
|
104
|
+
"candidate_id": candidate.get("candidate_id"),
|
|
105
|
+
"queue_index": candidate.get("queue_index"),
|
|
106
|
+
"title": candidate.get("title"),
|
|
107
|
+
"company": candidate.get("company"),
|
|
108
|
+
"url": candidate.get("url"),
|
|
109
|
+
"reasons": reasons,
|
|
110
|
+
}
|
|
111
|
+
if reasons:
|
|
112
|
+
skipped.append(row)
|
|
113
|
+
else:
|
|
114
|
+
retained.append(row)
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
"generated": date.today().isoformat(),
|
|
118
|
+
"batch_number": batch.get("batch_number", 1),
|
|
119
|
+
"batch_size": batch.get("batch_size", DEFAULT_BATCH_SIZE),
|
|
120
|
+
"loaded": len(batch.get("jobs", [])),
|
|
121
|
+
"retained_count": len(retained),
|
|
122
|
+
"skipped_count": len(skipped),
|
|
123
|
+
"retained": retained,
|
|
124
|
+
"skipped": skipped,
|
|
125
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""Briefing and LinkedIn weekly context helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
from datetime import date, datetime, timedelta
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from job_hunter.agent_context._utils import (
|
|
11
|
+
_read_json_or_yaml,
|
|
12
|
+
_read_yaml,
|
|
13
|
+
_root,
|
|
14
|
+
)
|
|
15
|
+
from job_hunter.agent_context.candidates import (
|
|
16
|
+
_candidate_files,
|
|
17
|
+
_jobs_from_candidate_file,
|
|
18
|
+
build_candidate_queue,
|
|
19
|
+
)
|
|
20
|
+
from job_hunter.agent_context.stories import story_index
|
|
21
|
+
from job_hunter.config.defaults import EXCLUDED_LISTING_URL_PATTERNS, STALE_INDICATORS
|
|
22
|
+
from job_hunter.constants import DEFAULT_BATCH_SIZE
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _latest_commit_subject(root: Path) -> str:
|
|
26
|
+
try:
|
|
27
|
+
result = subprocess.run(
|
|
28
|
+
["git", "log", "-1", "--pretty=%s"], # noqa: S607
|
|
29
|
+
cwd=root,
|
|
30
|
+
text=True,
|
|
31
|
+
capture_output=True,
|
|
32
|
+
timeout=5,
|
|
33
|
+
check=False,
|
|
34
|
+
)
|
|
35
|
+
except Exception:
|
|
36
|
+
return "unknown"
|
|
37
|
+
return result.stdout.strip() or "unknown"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def brief_context(*, root: Path | None = None) -> str:
|
|
41
|
+
base = _root(root)
|
|
42
|
+
subject = _latest_commit_subject(base)
|
|
43
|
+
discovery = "yes" if "discovery" in subject.lower() else "no"
|
|
44
|
+
queue = build_candidate_queue(root=base, today_only=False, limit=10000)
|
|
45
|
+
candidate_lines = []
|
|
46
|
+
for path in _candidate_files(base):
|
|
47
|
+
try:
|
|
48
|
+
jobs = _jobs_from_candidate_file(path)
|
|
49
|
+
candidate_lines.append(f"- `{path.name}`: {len(jobs)} candidates")
|
|
50
|
+
except Exception:
|
|
51
|
+
candidate_lines.append(f"- `{path.name}`: unreadable")
|
|
52
|
+
if not candidate_lines:
|
|
53
|
+
candidate_lines.append("- none")
|
|
54
|
+
|
|
55
|
+
today = date.today().isoformat()
|
|
56
|
+
jobs_dir = base / "outputs" / "jobs"
|
|
57
|
+
today_rows: list[str] = []
|
|
58
|
+
if jobs_dir.exists():
|
|
59
|
+
for folder in sorted(jobs_dir.iterdir()):
|
|
60
|
+
if not folder.is_dir():
|
|
61
|
+
continue
|
|
62
|
+
meta = _read_json_or_yaml(folder / "meta.json") if (folder / "meta.json").exists() else {}
|
|
63
|
+
if not (folder.name.startswith(today) or meta.get("date") == today):
|
|
64
|
+
continue
|
|
65
|
+
score = _read_yaml(folder / "score.yml")
|
|
66
|
+
today_rows.append(
|
|
67
|
+
"| {folder} | {company} | {title} | {score} | {decision} |".format(
|
|
68
|
+
folder=folder.name,
|
|
69
|
+
company=meta.get("company", ""),
|
|
70
|
+
title=meta.get("title", ""),
|
|
71
|
+
score=score.get("score", ""),
|
|
72
|
+
decision=score.get("decision", score.get("status", "")),
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
jobs_table = "\n".join(today_rows) if today_rows else "No job folders created today."
|
|
77
|
+
candidate_summary = "\n".join(candidate_lines[:20])
|
|
78
|
+
if len(candidate_lines) > 20:
|
|
79
|
+
candidate_summary += f"\n- ... {len(candidate_lines) - 20} more file(s)"
|
|
80
|
+
|
|
81
|
+
return f"""# Agent Brief - {today}
|
|
82
|
+
|
|
83
|
+
Latest commit: {subject}
|
|
84
|
+
Discovery commit: {discovery}
|
|
85
|
+
|
|
86
|
+
## Candidate Snapshots
|
|
87
|
+
{candidate_summary}
|
|
88
|
+
|
|
89
|
+
Unprocessed queue: {queue["count"]} candidate(s) from {len(queue["source_files"])} file(s).
|
|
90
|
+
|
|
91
|
+
## Today's Jobs
|
|
92
|
+
| Folder | Company | Title | Score | Decision |
|
|
93
|
+
|---|---|---|---|---|
|
|
94
|
+
{jobs_table}
|
|
95
|
+
|
|
96
|
+
Next:
|
|
97
|
+
- Run `/job-hunter batch` when candidates are ready.
|
|
98
|
+
- Run `/job-hunter one <url>` for a single posting.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _linkedin_job_limit(root: Path, days: int, limit: int | None) -> tuple[int, str]:
|
|
103
|
+
if limit is not None and limit > 0:
|
|
104
|
+
return limit, "cli"
|
|
105
|
+
scoring = _read_yaml(root / "config" / "job_hunter.yml").get("scoring", {})
|
|
106
|
+
daily_limit = int(scoring.get("batch_size") or 0)
|
|
107
|
+
if daily_limit > 0:
|
|
108
|
+
return daily_limit * max(days, 1), "config:scoring.batch_size * days"
|
|
109
|
+
return 0, "unlimited"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _effective_region_titles(region_config: dict[str, Any], global_titles: list[str]) -> list[str]:
|
|
113
|
+
titles = region_config.get("job_titles") or global_titles
|
|
114
|
+
return [str(title) for title in titles if str(title).strip()]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _llm_search_regions(config: dict[str, Any]) -> list[dict[str, Any]]:
|
|
118
|
+
global_titles = [str(title) for title in config.get("job_titles", []) or [] if str(title).strip()]
|
|
119
|
+
regions: list[dict[str, Any]] = []
|
|
120
|
+
for region_name, region_config in (config.get("regions") or {}).items():
|
|
121
|
+
if not isinstance(region_config, dict) or not region_config.get("enabled", True):
|
|
122
|
+
continue
|
|
123
|
+
regions.append(
|
|
124
|
+
{
|
|
125
|
+
"region": str(region_name),
|
|
126
|
+
"country": str(region_config.get("country") or ""),
|
|
127
|
+
"location": str(region_config.get("location") or ""),
|
|
128
|
+
"search_lang": str(region_config.get("search_lang") or ""),
|
|
129
|
+
"primary": bool(region_config.get("primary", False)),
|
|
130
|
+
"job_titles": _effective_region_titles(region_config, global_titles),
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
return regions
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def llm_search_config(*, root: Path | None = None) -> dict[str, Any]:
|
|
137
|
+
"""Return compact region/title/exclusion context for agent-driven web search."""
|
|
138
|
+
base = _root(root)
|
|
139
|
+
config = _read_yaml(base / "config" / "job_hunter.yml")
|
|
140
|
+
ljs = (config.get("search") or {}).get("llm_search") or {}
|
|
141
|
+
scoring = config.get("scoring") or {}
|
|
142
|
+
exclusions = config.get("exclusions") or {}
|
|
143
|
+
return {
|
|
144
|
+
"enabled": bool(ljs.get("enabled", False)),
|
|
145
|
+
"trigger_threshold": int(ljs.get("trigger_threshold", 999)),
|
|
146
|
+
"max_results_per_run": int(ljs.get("max_results_per_run", 20)),
|
|
147
|
+
"batch_size": int(scoring.get("batch_size", DEFAULT_BATCH_SIZE)),
|
|
148
|
+
"searches_per_title_per_region": 5,
|
|
149
|
+
"regions": _llm_search_regions(config),
|
|
150
|
+
"exclusions": {
|
|
151
|
+
"excluded_companies": [str(company) for company in exclusions.get("companies", []) or []],
|
|
152
|
+
"excluded_title_terms": [str(term) for term in exclusions.get("title_terms", []) or []],
|
|
153
|
+
"excluded_url_patterns": [str(pattern) for pattern in EXCLUDED_LISTING_URL_PATTERNS],
|
|
154
|
+
"excluded_languages": [str(language) for language in exclusions.get("languages", []) or []],
|
|
155
|
+
"stale_indicators": [str(indicator) for indicator in STALE_INDICATORS],
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def linkedin_weekly_context(
|
|
161
|
+
*,
|
|
162
|
+
root: Path | None = None,
|
|
163
|
+
days: int = 7,
|
|
164
|
+
limit: int | None = None,
|
|
165
|
+
) -> dict[str, Any]:
|
|
166
|
+
base = _root(root)
|
|
167
|
+
job_limit, limit_source = _linkedin_job_limit(base, days, limit)
|
|
168
|
+
cutoff = datetime.now() - timedelta(days=days)
|
|
169
|
+
jobs: list[dict[str, Any]] = []
|
|
170
|
+
jobs_dir = base / "outputs" / "jobs"
|
|
171
|
+
if jobs_dir.exists():
|
|
172
|
+
for folder in sorted(jobs_dir.iterdir(), key=lambda p: p.stat().st_mtime, reverse=True):
|
|
173
|
+
if not folder.is_dir() or datetime.fromtimestamp(folder.stat().st_mtime) < cutoff:
|
|
174
|
+
continue
|
|
175
|
+
meta = _read_json_or_yaml(folder / "meta.json") if (folder / "meta.json").exists() else {}
|
|
176
|
+
score = _read_yaml(folder / "score.yml")
|
|
177
|
+
jobs.append(
|
|
178
|
+
{
|
|
179
|
+
"slug": folder.name,
|
|
180
|
+
"company": meta.get("company", ""),
|
|
181
|
+
"title": meta.get("title", ""),
|
|
182
|
+
"score": score.get("score", ""),
|
|
183
|
+
"decision": score.get("decision", score.get("status", "")),
|
|
184
|
+
}
|
|
185
|
+
)
|
|
186
|
+
if job_limit and len(jobs) >= job_limit:
|
|
187
|
+
break
|
|
188
|
+
return {
|
|
189
|
+
"days": days,
|
|
190
|
+
"job_limit": job_limit or None,
|
|
191
|
+
"job_limit_source": limit_source,
|
|
192
|
+
"jobs": jobs,
|
|
193
|
+
"story_index": story_index(root=base),
|
|
194
|
+
}
|