chameleon-cv 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/AGENTS.md +186 -0
  2. package/LICENSE +21 -0
  3. package/Makefile +136 -0
  4. package/README.md +356 -0
  5. package/bin/chameleon.js +136 -0
  6. package/chameleon.bat +286 -0
  7. package/chameleon.sh +353 -0
  8. package/package.json +39 -0
  9. package/profiles/github_repos.json +1266 -0
  10. package/requirements.txt +5 -0
  11. package/scan.sh +12 -0
  12. package/score.sh +12 -0
  13. package/scripts/__init__.py +1 -0
  14. package/scripts/__main_tui__.py +11 -0
  15. package/scripts/__pycache__/__init__.cpython-312.pyc +0 -0
  16. package/scripts/__pycache__/__init__.cpython-313.pyc +0 -0
  17. package/scripts/__pycache__/__main_tui__.cpython-313.pyc +0 -0
  18. package/scripts/__pycache__/ats_ghost.cpython-313.pyc +0 -0
  19. package/scripts/__pycache__/cache.cpython-313.pyc +0 -0
  20. package/scripts/__pycache__/config.cpython-312.pyc +0 -0
  21. package/scripts/__pycache__/config.cpython-313.pyc +0 -0
  22. package/scripts/__pycache__/cover_letter.cpython-313.pyc +0 -0
  23. package/scripts/__pycache__/db.cpython-312.pyc +0 -0
  24. package/scripts/__pycache__/db.cpython-313.pyc +0 -0
  25. package/scripts/__pycache__/fonts.cpython-312.pyc +0 -0
  26. package/scripts/__pycache__/fonts.cpython-313.pyc +0 -0
  27. package/scripts/__pycache__/ghost.cpython-313.pyc +0 -0
  28. package/scripts/__pycache__/job_matcher.cpython-312.pyc +0 -0
  29. package/scripts/__pycache__/job_matcher.cpython-313.pyc +0 -0
  30. package/scripts/__pycache__/question.cpython-313.pyc +0 -0
  31. package/scripts/__pycache__/render.cpython-313.pyc +0 -0
  32. package/scripts/__pycache__/review.cpython-313.pyc +0 -0
  33. package/scripts/__pycache__/setup.cpython-312.pyc +0 -0
  34. package/scripts/__pycache__/tailor_brief.cpython-312.pyc +0 -0
  35. package/scripts/__pycache__/tailor_brief.cpython-313.pyc +0 -0
  36. package/scripts/__pycache__/tailor_cv.cpython-312.pyc +0 -0
  37. package/scripts/__pycache__/tailor_cv.cpython-313.pyc +0 -0
  38. package/scripts/__pycache__/tailor_prompts.cpython-312.pyc +0 -0
  39. package/scripts/__pycache__/tailor_prompts.cpython-313.pyc +0 -0
  40. package/scripts/__pycache__/tui_app.cpython-312.pyc +0 -0
  41. package/scripts/__pycache__/tui_app.cpython-313.pyc +0 -0
  42. package/scripts/ats_ghost.py +170 -0
  43. package/scripts/cache.py +63 -0
  44. package/scripts/cli.py +109 -0
  45. package/scripts/config.py +376 -0
  46. package/scripts/cover_letter.py +105 -0
  47. package/scripts/db.py +331 -0
  48. package/scripts/fonts.py +186 -0
  49. package/scripts/ghost.py +88 -0
  50. package/scripts/job_matcher.py +636 -0
  51. package/scripts/job_scanner/__init__.py +8 -0
  52. package/scripts/job_scanner/__main__.py +6 -0
  53. package/scripts/job_scanner/__pycache__/__init__.cpython-312.pyc +0 -0
  54. package/scripts/job_scanner/__pycache__/__init__.cpython-313.pyc +0 -0
  55. package/scripts/job_scanner/__pycache__/__main__.cpython-312.pyc +0 -0
  56. package/scripts/job_scanner/__pycache__/__main__.cpython-313.pyc +0 -0
  57. package/scripts/job_scanner/__pycache__/base.cpython-312.pyc +0 -0
  58. package/scripts/job_scanner/__pycache__/base.cpython-313.pyc +0 -0
  59. package/scripts/job_scanner/__pycache__/browser.cpython-312.pyc +0 -0
  60. package/scripts/job_scanner/__pycache__/browser.cpython-313.pyc +0 -0
  61. package/scripts/job_scanner/__pycache__/cli.cpython-313.pyc +0 -0
  62. package/scripts/job_scanner/__pycache__/registry.cpython-312.pyc +0 -0
  63. package/scripts/job_scanner/__pycache__/registry.cpython-313.pyc +0 -0
  64. package/scripts/job_scanner/__pycache__/scanner.cpython-312.pyc +0 -0
  65. package/scripts/job_scanner/__pycache__/scanner.cpython-313.pyc +0 -0
  66. package/scripts/job_scanner/base.py +197 -0
  67. package/scripts/job_scanner/browser.py +244 -0
  68. package/scripts/job_scanner/cli.py +6 -0
  69. package/scripts/job_scanner/parsers/__init__.py +24 -0
  70. package/scripts/job_scanner/parsers/__pycache__/__init__.cpython-312.pyc +0 -0
  71. package/scripts/job_scanner/parsers/__pycache__/__init__.cpython-313.pyc +0 -0
  72. package/scripts/job_scanner/parsers/__pycache__/adzuna.cpython-312.pyc +0 -0
  73. package/scripts/job_scanner/parsers/__pycache__/adzuna.cpython-313.pyc +0 -0
  74. package/scripts/job_scanner/parsers/__pycache__/arc.cpython-312.pyc +0 -0
  75. package/scripts/job_scanner/parsers/__pycache__/arc.cpython-313.pyc +0 -0
  76. package/scripts/job_scanner/parsers/__pycache__/crypto_careers.cpython-312.pyc +0 -0
  77. package/scripts/job_scanner/parsers/__pycache__/crypto_careers.cpython-313.pyc +0 -0
  78. package/scripts/job_scanner/parsers/__pycache__/dailyremote.cpython-312.pyc +0 -0
  79. package/scripts/job_scanner/parsers/__pycache__/dailyremote.cpython-313.pyc +0 -0
  80. package/scripts/job_scanner/parsers/__pycache__/flexjobs.cpython-313.pyc +0 -0
  81. package/scripts/job_scanner/parsers/__pycache__/himalayas.cpython-312.pyc +0 -0
  82. package/scripts/job_scanner/parsers/__pycache__/himalayas.cpython-313.pyc +0 -0
  83. package/scripts/job_scanner/parsers/__pycache__/hn_hiring.cpython-312.pyc +0 -0
  84. package/scripts/job_scanner/parsers/__pycache__/hn_hiring.cpython-313.pyc +0 -0
  85. package/scripts/job_scanner/parsers/__pycache__/indeed.cpython-313.pyc +0 -0
  86. package/scripts/job_scanner/parsers/__pycache__/jobicy.cpython-312.pyc +0 -0
  87. package/scripts/job_scanner/parsers/__pycache__/jobicy.cpython-313.pyc +0 -0
  88. package/scripts/job_scanner/parsers/__pycache__/jooble.cpython-312.pyc +0 -0
  89. package/scripts/job_scanner/parsers/__pycache__/jooble.cpython-313.pyc +0 -0
  90. package/scripts/job_scanner/parsers/__pycache__/linkedin.cpython-313.pyc +0 -0
  91. package/scripts/job_scanner/parsers/__pycache__/opentoworkremote.cpython-313.pyc +0 -0
  92. package/scripts/job_scanner/parsers/__pycache__/relocate.cpython-312.pyc +0 -0
  93. package/scripts/job_scanner/parsers/__pycache__/relocate.cpython-313.pyc +0 -0
  94. package/scripts/job_scanner/parsers/__pycache__/remote_rocketship.cpython-313.pyc +0 -0
  95. package/scripts/job_scanner/parsers/__pycache__/remoteok.cpython-312.pyc +0 -0
  96. package/scripts/job_scanner/parsers/__pycache__/remoteok.cpython-313.pyc +0 -0
  97. package/scripts/job_scanner/parsers/__pycache__/remotive.cpython-312.pyc +0 -0
  98. package/scripts/job_scanner/parsers/__pycache__/remotive.cpython-313.pyc +0 -0
  99. package/scripts/job_scanner/parsers/__pycache__/web3_career.cpython-313.pyc +0 -0
  100. package/scripts/job_scanner/parsers/__pycache__/wellfound.cpython-313.pyc +0 -0
  101. package/scripts/job_scanner/parsers/__pycache__/weworkremotely.cpython-312.pyc +0 -0
  102. package/scripts/job_scanner/parsers/__pycache__/weworkremotely.cpython-313.pyc +0 -0
  103. package/scripts/job_scanner/parsers/__pycache__/ycomb.cpython-313.pyc +0 -0
  104. package/scripts/job_scanner/parsers/adzuna.py +81 -0
  105. package/scripts/job_scanner/parsers/arc.py +65 -0
  106. package/scripts/job_scanner/parsers/crypto_careers.py +88 -0
  107. package/scripts/job_scanner/parsers/dailyremote.py +91 -0
  108. package/scripts/job_scanner/parsers/flexjobs.py +91 -0
  109. package/scripts/job_scanner/parsers/himalayas.py +47 -0
  110. package/scripts/job_scanner/parsers/hn_hiring.py +94 -0
  111. package/scripts/job_scanner/parsers/indeed.py +98 -0
  112. package/scripts/job_scanner/parsers/jobicy.py +60 -0
  113. package/scripts/job_scanner/parsers/jooble.py +68 -0
  114. package/scripts/job_scanner/parsers/linkedin.py +140 -0
  115. package/scripts/job_scanner/parsers/opentoworkremote.py +53 -0
  116. package/scripts/job_scanner/parsers/relocate.py +95 -0
  117. package/scripts/job_scanner/parsers/remote_rocketship.py +90 -0
  118. package/scripts/job_scanner/parsers/remoteok.py +63 -0
  119. package/scripts/job_scanner/parsers/remotive.py +57 -0
  120. package/scripts/job_scanner/parsers/web3_career.py +92 -0
  121. package/scripts/job_scanner/parsers/wellfound.py +94 -0
  122. package/scripts/job_scanner/parsers/weworkremotely.py +60 -0
  123. package/scripts/job_scanner/parsers/ycomb.py +83 -0
  124. package/scripts/job_scanner/registry.py +65 -0
  125. package/scripts/job_scanner/scanner.py +162 -0
  126. package/scripts/question.py +107 -0
  127. package/scripts/render.py +110 -0
  128. package/scripts/review.py +89 -0
  129. package/scripts/setup.py +139 -0
  130. package/scripts/tailor_brief.py +379 -0
  131. package/scripts/tailor_cv.py +746 -0
  132. package/scripts/tailor_prompts.py +101 -0
  133. package/scripts/tui/__init__.py +1 -0
  134. package/scripts/tui/__pycache__/__init__.cpython-312.pyc +0 -0
  135. package/scripts/tui/__pycache__/__init__.cpython-313.pyc +0 -0
  136. package/scripts/tui/__pycache__/app.cpython-312.pyc +0 -0
  137. package/scripts/tui/__pycache__/app.cpython-313.pyc +0 -0
  138. package/scripts/tui/__pycache__/helpers.cpython-312.pyc +0 -0
  139. package/scripts/tui/__pycache__/helpers.cpython-313.pyc +0 -0
  140. package/scripts/tui/__pycache__/screens.cpython-312.pyc +0 -0
  141. package/scripts/tui/__pycache__/screens.cpython-313.pyc +0 -0
  142. package/scripts/tui/__pycache__/widgets.cpython-312.pyc +0 -0
  143. package/scripts/tui/__pycache__/widgets.cpython-313.pyc +0 -0
  144. package/scripts/tui/app.py +1547 -0
  145. package/scripts/tui/helpers.py +65 -0
  146. package/scripts/tui/screens.py +642 -0
  147. package/scripts/tui/widgets.py +276 -0
  148. package/scripts/tui_app.py +25 -0
  149. package/scripts/wizard.py +109 -0
  150. package/tui.bat +50 -0
  151. package/tui.sh +59 -0
package/chameleon.bat ADDED
@@ -0,0 +1,286 @@
1
+ @echo off
2
+ setlocal enabledelayedexpansion
3
+
4
+ REM Chameleon CLI — unified entry point for Windows
5
+ set "CHAMELEON_DIR=%~dp0"
6
+ set "VENV_PYTHON=%CHAMELEON_DIR%.venv\Scripts\python.exe"
7
+
8
+ REM Resolve Python
9
+ set "PYTHON_BIN="
10
+ if exist "%VENV_PYTHON%" (
11
+ set "PYTHON_BIN=%VENV_PYTHON%"
12
+ ) else (
13
+ where python >nul 2>&1 && set "PYTHON_BIN=python"
14
+ )
15
+
16
+ if "%PYTHON_BIN%"=="" (
17
+ echo [!] No Python found. Install Python 3.10+ and run: make install-tools
18
+ exit /b 1
19
+ )
20
+
21
+ if "%~1"=="" goto :help
22
+ if /I "%~1"=="help" goto :help
23
+ if /I "%~1"=="--help" goto :help
24
+ if /I "%~1"=="-h" goto :help
25
+ if /I "%~1"=="--version" goto :version
26
+ if /I "%~1"=="-v" goto :version
27
+
28
+ if /I "%~1"=="tailor" goto :tailor
29
+ if /I "%~1"=="tailor-cv" goto :tailor
30
+ if /I "%~1"=="score" goto :score
31
+ if /I "%~1"=="score-cv" goto :score
32
+ if /I "%~1"=="match" goto :score
33
+ if /I "%~1"=="scan" goto :scan
34
+ if /I "%~1"=="scan-jobs" goto :scan
35
+ if /I "%~1"=="cover" goto :cover
36
+ if /I "%~1"=="cover-letter" goto :cover
37
+ if /I "%~1"=="render" goto :render
38
+ if /I "%~1"=="init" goto :init
39
+ if /I "%~1"=="init-cv" goto :init
40
+ if /I "%~1"=="tui" goto :tui
41
+ if /I "%~1"=="setup" goto :setup
42
+ if /I "%~1"=="install" goto :setup
43
+
44
+ echo Unknown command: %~1
45
+ echo Run 'chameleon help' for usage.
46
+ exit /b 1
47
+
48
+ REM ════════════════════════════════════════════════════════════════════════
49
+ REM TAILOR
50
+ REM ════════════════════════════════════════════════════════════════════════
51
+ :tailor
52
+ shift
53
+ set "JD_INPUT="
54
+ set "COMPANY="
55
+ set "TITLE="
56
+ set "EXTRA_ARGS="
57
+
58
+ :tailor_parse
59
+ if "%~1"=="" goto :tailor_run
60
+ if /I "%~1"=="--company" (
61
+ set "COMPANY=%~2"
62
+ shift & shift
63
+ goto :tailor_parse
64
+ )
65
+ if /I "%~1"=="--title" (
66
+ set "TITLE=%~2"
67
+ shift & shift
68
+ goto :tailor_parse
69
+ )
70
+ if /I "%~1"=="--no-review" (
71
+ set "EXTRA_ARGS=!EXTRA_ARGS! --no-review"
72
+ shift
73
+ goto :tailor_parse
74
+ )
75
+ if /I "%~1"=="--no-render" (
76
+ set "EXTRA_ARGS=!EXTRA_ARGS! --no-render"
77
+ shift
78
+ goto :tailor_parse
79
+ )
80
+ if /I "%~1"=="--json" (
81
+ set "EXTRA_ARGS=!EXTRA_ARGS! --json"
82
+ shift
83
+ goto :tailor_parse
84
+ )
85
+ if /I "%~1"=="--cv" (
86
+ set "EXTRA_ARGS=!EXTRA_ARGS! --cv %~2"
87
+ shift & shift
88
+ goto :tailor_parse
89
+ )
90
+ if "%~1"=="-" (
91
+ echo Unknown option: %~2
92
+ exit /b 1
93
+ )
94
+ set "JD_INPUT=%~1"
95
+ shift
96
+ goto :tailor_parse
97
+
98
+ :tailor_run
99
+ if "%JD_INPUT%"=="" (
100
+ echo Error: No job description provided.
101
+ echo Usage: chameleon tailor ^<jd_text_or_url_or_file^> [--company X] [--title Y]
102
+ exit /b 1
103
+ )
104
+
105
+ REM Check if input is a URL and crawl it
106
+ echo %JD_INPUT% | findstr /R "^https*://" >nul 2>&1
107
+ if %errorlevel%==0 (
108
+ echo [*] Crawling URL: %JD_INPUT%
109
+ for /f "delims=" %%i in ('"%PYTHON_BIN%" -c "import sys; import httpx; r=httpx.get('%JD_INPUT%',timeout=30,follow_redirects=True,headers={'User-Agent':'Mozilla/5.0'}); text=r.text; import re; text=re.sub(r'<script[^>]*>.*?</script>','',text,flags=re.DOTALL|re.IGNORECASE); text=re.sub(r'<style[^>]*>.*?</style>','',text,flags=re.DOTALL|re.IGNORECASE); text=re.sub(r'<[^>]+>',' ',text); import html; text=html.unescape(text); text=re.sub(r'\s+',' ',text).strip(); print(text[:10000])" 2^>nul') do set "JD_TEXT=%%i"
110
+ if "!JD_TEXT!"=="" (
111
+ echo Error: Failed to crawl URL
112
+ exit /b 1
113
+ )
114
+ goto :tailor_exec
115
+ )
116
+
117
+ REM Check if input is a file
118
+ if exist "%JD_INPUT%" (
119
+ set /p JD_TEXT=<"%JD_INPUT%"
120
+ ) else (
121
+ set "JD_TEXT=%JD_INPUT%"
122
+ )
123
+
124
+ :tailor_exec
125
+ cd /d "%CHAMELEON_DIR%"
126
+ "%PYTHON_BIN%" -m scripts.tailor_cv "%JD_TEXT%" --company "%COMPANY%" --title "%TITLE%" %EXTRA_ARGS%
127
+ exit /b %errorlevel%
128
+
129
+ REM ════════════════════════════════════════════════════════════════════════
130
+ REM SCORE
131
+ REM ════════════════════════════════════════════════════════════════════════
132
+ :score
133
+ shift
134
+ set "JD_INPUT="
135
+ set "EXTRA_ARGS="
136
+
137
+ :score_parse
138
+ if "%~1"=="" goto :score_run
139
+ if /I "%~1"=="--cv" (
140
+ set "EXTRA_ARGS=!EXTRA_ARGS! --cv %~2"
141
+ shift & shift
142
+ goto :score_parse
143
+ )
144
+ if /I "%~1"=="--json" (
145
+ set "EXTRA_ARGS=!EXTRA_ARGS! --json"
146
+ shift
147
+ goto :score_parse
148
+ )
149
+ set "JD_INPUT=%~1"
150
+ shift
151
+ goto :score_parse
152
+
153
+ :score_run
154
+ if "%JD_INPUT%"=="" (
155
+ echo Error: No job description provided.
156
+ echo Usage: chameleon score ^<jd_text_or_url_or_file^> [--cv path] [--json]
157
+ exit /b 1
158
+ )
159
+
160
+ cd /d "%CHAMELEON_DIR%"
161
+ "%PYTHON_BIN%" -m scripts.job_matcher "%JD_INPUT%" %EXTRA_ARGS%
162
+ exit /b %errorlevel%
163
+
164
+ REM ════════════════════════════════════════════════════════════════════════
165
+ REM SCAN
166
+ REM ════════════════════════════════════════════════════════════════════════
167
+ :scan
168
+ shift
169
+ cd /d "%CHAMELEON_DIR%"
170
+ set "SCAN_ARGS="
171
+ :scan_parse
172
+ if "%~1"=="" goto :scan_run
173
+ set "SCAN_ARGS=!SCAN_ARGS! %~1"
174
+ shift
175
+ goto :scan_parse
176
+ :scan_run
177
+ "%PYTHON_BIN%" -m scripts.job_scanner.scanner %SCAN_ARGS%
178
+ exit /b %errorlevel%
179
+
180
+ REM ════════════════════════════════════════════════════════════════════════
181
+ REM COVER
182
+ REM ════════════════════════════════════════════════════════════════════════
183
+ :cover
184
+ shift
185
+ echo [*] Cover letter generation requires an AI assistant.
186
+ echo Use: opencode then /cover-letter ^<url-or-text^>
187
+ exit /b 0
188
+
189
+ REM ════════════════════════════════════════════════════════════════════════
190
+ REM RENDER
191
+ REM ════════════════════════════════════════════════════════════════════════
192
+ :render
193
+ shift
194
+ if "%~1"=="" (
195
+ echo Error: No YAML path provided.
196
+ echo Usage: chameleon render ^<yaml_path^>
197
+ exit /b 1
198
+ )
199
+ cd /d "%CHAMELEON_DIR%"
200
+ "%PYTHON_BIN%" scripts/render.py %~1
201
+ exit /b %errorlevel%
202
+
203
+ REM ════════════════════════════════════════════════════════════════════════
204
+ REM INIT
205
+ REM ════════════════════════════════════════════════════════════════════════
206
+ :init
207
+ shift
208
+ echo [*] CV initialization requires an AI assistant.
209
+ echo Use: opencode then /init-cv %~1
210
+ exit /b 0
211
+
212
+ REM ════════════════════════════════════════════════════════════════════════
213
+ REM TUI
214
+ REM ════════════════════════════════════════════════════════════════════════
215
+ :tui
216
+ shift
217
+ cd /d "%CHAMELEON_DIR%"
218
+ call tui.bat %*
219
+ exit /b %errorlevel%
220
+
221
+ REM ════════════════════════════════════════════════════════════════════════
222
+ REM SETUP
223
+ REM ════════════════════════════════════════════════════════════════════════
224
+ :setup
225
+ cd /d "%CHAMELEON_DIR%"
226
+ if not exist "%VENV_PYTHON%" (
227
+ python -m venv .venv
228
+ )
229
+ "%VENV_PYTHON%" -m scripts.setup %*
230
+ exit /b %errorlevel%
231
+
232
+ REM ════════════════════════════════════════════════════════════════════════
233
+ REM HELP
234
+ REM ════════════════════════════════════════════════════════════════════════
235
+ :help
236
+ echo Chameleon CLI — AI Resume Tailor
237
+ echo.
238
+ echo Usage:
239
+ echo chameleon tailor ^<jd_text_or_url_or_file^> [options] Tailor CV for a job
240
+ echo chameleon score ^<jd_text_or_url_or_file^> [options] Score job match
241
+ echo chameleon scan [options] Scan job platforms
242
+ echo chameleon cover ^<jd_text_or_url_or_file^> [options] Generate cover letter
243
+ echo chameleon render ^<yaml_path^> Render YAML to PDF
244
+ echo chameleon init ^<pdf_or_yaml_path^> Import master CV
245
+ echo chameleon tui Launch the TUI
246
+ echo chameleon setup Install all dependencies
247
+ echo chameleon help Show this help
248
+ echo.
249
+ echo TAILOR options:
250
+ echo --company ^<name^> Company name (auto-detected if omitted)
251
+ echo --title ^<role^> Role title (auto-detected if omitted)
252
+ echo --no-review Skip AI review pass
253
+ echo --no-render Skip PDF rendering
254
+ echo --cv ^<path^> Master CV path
255
+ echo --json Output JSON instead of text
256
+ echo.
257
+ echo SCORE options:
258
+ echo --cv ^<path^> CV path to score against
259
+ echo --json Output JSON
260
+ echo.
261
+ echo SCAN options:
262
+ echo -q, --query ^<query^> Search keywords
263
+ echo -p, --platforms ^<list^> Comma-separated platforms
264
+ echo -a, --all Scan all platforms
265
+ echo --tier1 Tier 1 platforms only
266
+ echo -n, --limit ^<n^> Max jobs per platform (default: 25)
267
+ echo -o, --output ^<file^> Save results to JSON file
268
+ echo --json Output JSON
269
+ echo.
270
+ echo INPUT:
271
+ echo ^<text^> Job description text in quotes
272
+ echo ^<url^> Job posting URL (will be crawled)
273
+ echo ^<file^> Path to a text file with the JD
274
+ echo.
275
+ echo EXAMPLES:
276
+ echo chameleon tailor "Senior Rust Engineer at Acme..." --company Acme
277
+ echo chameleon tailor https://jobs.example.com/senior-rust
278
+ echo chameleon score "Python developer, Django, PostgreSQL..."
279
+ echo chameleon scan -q "rust engineer" --tier1 --json
280
+ echo chameleon render templates\david_acme_rust_engineer_cv.yaml
281
+ echo.
282
+ exit /b 0
283
+
284
+ :version
285
+ echo Chameleon CLI v1.0
286
+ exit /b 0
package/chameleon.sh ADDED
@@ -0,0 +1,353 @@
1
+ #!/bin/bash
2
+ # Chameleon CLI — unified entry point for tailoring, scoring, scanning, cover letters
3
+ set -euo pipefail
4
+
5
+ CHAMELEON_DIR="$(cd "$(dirname "$0")" && pwd)"
6
+ VENV_PYTHON="$CHAMELEON_DIR/.venv/bin/python3"
7
+
8
+ # ── Resolve venv python ──────────────────────────────────────────────────
9
+ find_python() {
10
+ if [[ -f "$VENV_PYTHON" ]]; then
11
+ echo "$VENV_PYTHON"
12
+ elif command -v python3 &>/dev/null; then
13
+ echo "python3"
14
+ elif command -v python &>/dev/null; then
15
+ echo "python"
16
+ else
17
+ echo ""
18
+ fi
19
+ }
20
+
21
+ PYTHON_BIN="$(find_python)"
22
+ if [[ -z "$PYTHON_BIN" ]]; then
23
+ echo "[!] No Python found. Install Python 3.10+ and run: make install-tools" >&2
24
+ exit 1
25
+ fi
26
+
27
+ # ── URL crawler: fetch job description from a URL ────────────────────────
28
+ crawl_url() {
29
+ local url="$1"
30
+ "$PYTHON_BIN" -c "
31
+ import sys
32
+ try:
33
+ import httpx
34
+ except ImportError:
35
+ import urllib.request
36
+ import re
37
+ import html as html_mod
38
+ req = urllib.request.Request('$url', headers={'User-Agent': 'Mozilla/5.0 (compatible; ChameleonBot/1.0)'})
39
+ with urllib.request.urlopen(req, timeout=30) as resp:
40
+ raw = resp.read().decode('utf-8', errors='replace')
41
+ text = re.sub(r'<script[^>]*>.*?</script>', '', raw, flags=re.DOTALL | re.IGNORECASE)
42
+ text = re.sub(r'<style[^>]*>.*?</style>', '', text, flags=re.DOTALL | re.IGNORECASE)
43
+ text = re.sub(r'<[^>]+>', ' ', text)
44
+ text = html_mod.unescape(text)
45
+ text = re.sub(r'\s+', ' ', text).strip()
46
+ print(text[:10000])
47
+ sys.exit(0)
48
+
49
+ try:
50
+ resp = httpx.get('$url', timeout=30, follow_redirects=True, headers={
51
+ 'User-Agent': 'Mozilla/5.0 (compatible; ChameleonBot/1.0)'
52
+ })
53
+ resp.raise_for_status()
54
+ from html.parser import HTMLParser
55
+ class TextExtractor(HTMLParser):
56
+ def __init__(self):
57
+ super().__init__()
58
+ self.parts = []
59
+ self.skip = False
60
+ def handle_starttag(self, tag, attrs):
61
+ if tag in ('script', 'style'):
62
+ self.skip = True
63
+ def handle_endtag(self, tag):
64
+ if tag in ('script', 'style'):
65
+ self.skip = False
66
+ def handle_data(self, data):
67
+ if not self.skip:
68
+ self.parts.append(data)
69
+ te = TextExtractor()
70
+ te.feed(resp.text)
71
+ text = ' '.join(te.parts)
72
+ import re as _re
73
+ text = _re.sub(r'\s+', ' ', text).strip()
74
+ print(text[:10000])
75
+ except Exception as e:
76
+ print(f'Error crawling URL: {e}', file=sys.stderr)
77
+ sys.exit(1)
78
+ "
79
+ }
80
+
81
+ # ── Read stdin if available ──────────────────────────────────────────────
82
+ read_stdin() {
83
+ if [[ ! -t 0 ]]; then
84
+ cat
85
+ fi
86
+ }
87
+
88
+ # ── Help ─────────────────────────────────────────────────────────────────
89
+ show_help() {
90
+ cat <<'EOF'
91
+ Chameleon CLI — AI Resume Tailor
92
+
93
+ Usage:
94
+ chameleon tailor <jd_text_or_url_or_file> [options] Tailor CV for a job
95
+ chameleon score <jd_text_or_url_or_file> [options] Score job match
96
+ chameleon scan [options] Scan job platforms
97
+ chameleon cover <jd_text_or_url_or_file> [options] Generate cover letter
98
+ chameleon render <yaml_path> Render YAML to PDF
99
+ chameleon init <pdf_or_yaml_path> Import master CV
100
+ chameleon setup Install all dependencies
101
+ chameleon tui Launch the TUI
102
+ chameleon help Show this help
103
+
104
+ TAILOR options:
105
+ --company <name> Company name (auto-detected if omitted)
106
+ --title <role> Role title (auto-detected if omitted)
107
+ --no-review Skip AI review pass
108
+ --no-render Skip PDF rendering
109
+ --cv <path> Master CV path (default: from config)
110
+ --json Output JSON instead of text
111
+
112
+ SCORE options:
113
+ --cv <path> CV path to score against
114
+ --json Output JSON
115
+
116
+ SCAN options:
117
+ -q, --query <query> Search keywords
118
+ -p, --platforms <list> Comma-separated platforms
119
+ -a, --all Scan all platforms
120
+ --tier1 Tier 1 platforms only (API/RSS)
121
+ -n, --limit <n> Max jobs per platform (default: 25)
122
+ -o, --output <file> Save results to JSON file
123
+ --json Output JSON
124
+ --list-platforms List available platforms
125
+
126
+ COVER options:
127
+ --resume <path> Resume YAML to ground the letter
128
+ --greeting <name> Custom greeting name
129
+
130
+ INPUT:
131
+ <text> Job description text in quotes
132
+ <url> Job posting URL (will be crawled)
133
+ <file> Path to a text file with the JD
134
+ Piped stdin Cat a file or pipe text
135
+
136
+ EXAMPLES:
137
+ ./chameleon tailor "Senior Rust Engineer at Acme..." --company Acme
138
+ ./chameleon tailor https://jobs.example.com/senior-rust
139
+ ./chameleon score "Python developer, Django, PostgreSQL..."
140
+ ./chameleon scan -q "rust engineer" --tier1 --json
141
+ ./chameleon cover https://example.com/job --resume templates/david_cv.yaml
142
+ cat jd.txt | ./chameleon tailor --company Acme --title Engineer
143
+ ./chameleon render templates/david_acme_rust_engineer_cv.yaml
144
+
145
+ EOF
146
+ exit 0
147
+ }
148
+
149
+ # ── Main dispatch ────────────────────────────────────────────────────────
150
+ if [[ $# -lt 1 ]]; then
151
+ show_help
152
+ fi
153
+
154
+ COMMAND="$1"
155
+ shift
156
+
157
+ case "$COMMAND" in
158
+ tailor|tailor-cv)
159
+ JD_INPUT=""
160
+ COMPANY=""
161
+ TITLE=""
162
+ NO_REVIEW=""
163
+ NO_RENDER=""
164
+ CV_PATH=""
165
+ AS_JSON=""
166
+
167
+ while [[ $# -gt 0 ]]; do
168
+ case "$1" in
169
+ --company) COMPANY="$2"; shift 2 ;;
170
+ --title) TITLE="$2"; shift 2 ;;
171
+ --no-review) NO_REVIEW="yes"; shift ;;
172
+ --no-render) NO_RENDER="yes"; shift ;;
173
+ --cv) CV_PATH="$2"; shift 2 ;;
174
+ --json) AS_JSON="yes"; shift ;;
175
+ -*) echo "Unknown option: $1" >&2; exit 1 ;;
176
+ *) JD_INPUT="$1"; shift ;;
177
+ esac
178
+ done
179
+
180
+ if [[ -z "$JD_INPUT" ]]; then
181
+ JD_INPUT="$(read_stdin)"
182
+ fi
183
+
184
+ if [[ -z "$JD_INPUT" ]]; then
185
+ echo "Error: No job description provided. Pass text, URL, file path, or pipe via stdin." >&2
186
+ echo "Usage: chameleon tailor <jd_text_or_url_or_file> [--company X] [--title Y]" >&2
187
+ exit 1
188
+ fi
189
+
190
+ if [[ "$JD_INPUT" =~ ^https?:// ]]; then
191
+ echo "[*] Crawling URL: $JD_INPUT" >&2
192
+ JD_TEXT="$(crawl_url "$JD_INPUT")"
193
+ if [[ -z "$JD_TEXT" ]]; then
194
+ echo "Error: Failed to crawl URL" >&2
195
+ exit 1
196
+ fi
197
+ elif [[ -f "$JD_INPUT" ]]; then
198
+ JD_TEXT="$(cat "$JD_INPUT")"
199
+ else
200
+ JD_TEXT="$JD_INPUT"
201
+ fi
202
+
203
+ cd "$CHAMELEON_DIR"
204
+ set -- "$JD_TEXT"
205
+ [[ -n "$COMPANY" ]] && set -- "$@" "--company" "$COMPANY"
206
+ [[ -n "$TITLE" ]] && set -- "$@" "--title" "$TITLE"
207
+ [[ -n "$NO_REVIEW" ]] && set -- "$@" "--no-review"
208
+ [[ -n "$NO_RENDER" ]] && set -- "$@" "--no-render"
209
+ [[ -n "$AS_JSON" ]] && set -- "$@" "--json"
210
+ exec "$PYTHON_BIN" -m scripts.tailor_cv "$@"
211
+ ;;
212
+
213
+ score|score-cv|match)
214
+ JD_INPUT=""
215
+ CV_PATH=""
216
+ AS_JSON=""
217
+
218
+ while [[ $# -gt 0 ]]; do
219
+ case "$1" in
220
+ --cv) CV_PATH="$2"; shift 2 ;;
221
+ --json) AS_JSON="yes"; shift ;;
222
+ -*) echo "Unknown option: $1" >&2; exit 1 ;;
223
+ *) JD_INPUT="$1"; shift ;;
224
+ esac
225
+ done
226
+
227
+ if [[ -z "$JD_INPUT" ]]; then
228
+ JD_INPUT="$(read_stdin)"
229
+ fi
230
+
231
+ if [[ -z "$JD_INPUT" ]]; then
232
+ echo "Error: No job description provided." >&2
233
+ echo "Usage: chameleon score <jd_text_or_url_or_file> [--cv <path>] [--json]" >&2
234
+ exit 1
235
+ fi
236
+
237
+ if [[ "$JD_INPUT" =~ ^https?:// ]]; then
238
+ echo "[*] Crawling URL: $JD_INPUT" >&2
239
+ JD_TEXT="$(crawl_url "$JD_INPUT")"
240
+ if [[ -z "$JD_TEXT" ]]; then
241
+ echo "Error: Failed to crawl URL" >&2
242
+ exit 1
243
+ fi
244
+ elif [[ -f "$JD_INPUT" ]]; then
245
+ JD_TEXT="$(cat "$JD_INPUT")"
246
+ else
247
+ JD_TEXT="$JD_INPUT"
248
+ fi
249
+
250
+ cd "$CHAMELEON_DIR"
251
+ set -- "$JD_TEXT"
252
+ [[ -n "$CV_PATH" ]] && set -- "$@" "--cv" "$CV_PATH"
253
+ [[ -n "$AS_JSON" ]] && set -- "$@" "--json"
254
+ exec "$PYTHON_BIN" -m scripts.job_matcher "$@"
255
+ ;;
256
+
257
+ scan|scan-jobs)
258
+ cd "$CHAMELEON_DIR"
259
+ exec "$PYTHON_BIN" -m scripts.job_scanner.scanner "$@"
260
+ ;;
261
+
262
+ cover|cover-letter)
263
+ JD_INPUT=""
264
+ RESUME_PATH=""
265
+
266
+ while [[ $# -gt 0 ]]; do
267
+ case "$1" in
268
+ --resume) RESUME_PATH="$2"; shift 2 ;;
269
+ -*) echo "Unknown option: $1" >&2; exit 1 ;;
270
+ *) JD_INPUT="$1"; shift ;;
271
+ esac
272
+ done
273
+
274
+ if [[ -z "$JD_INPUT" ]]; then
275
+ JD_INPUT="$(read_stdin)"
276
+ fi
277
+
278
+ if [[ -z "$JD_INPUT" ]]; then
279
+ echo "Error: No job description provided." >&2
280
+ echo "Usage: chameleon cover <jd_text_or_url_or_file> [--resume <path>]" >&2
281
+ exit 1
282
+ fi
283
+
284
+ if [[ "$JD_INPUT" =~ ^https?:// ]]; then
285
+ echo "[*] Crawling URL: $JD_INPUT" >&2
286
+ JD_TEXT="$(crawl_url "$JD_INPUT")"
287
+ elif [[ -f "$JD_INPUT" ]]; then
288
+ JD_TEXT="$(cat "$JD_INPUT")"
289
+ else
290
+ JD_TEXT="$JD_INPUT"
291
+ fi
292
+
293
+ # Save extracted text and show instructions
294
+ COVER_DIR="$CHAMELEON_DIR/output/cover_letters"
295
+ mkdir -p "$COVER_DIR"
296
+ COVER_FILE="$COVER_DIR/cover_$(date +%Y%m%d_%H%M%S).txt"
297
+ echo "$JD_TEXT" > "$COVER_FILE"
298
+ echo "[*] Job description saved to: $COVER_FILE" >&2
299
+ echo "[*] To generate a cover letter, run in opencode:" >&2
300
+ echo " opencode" >&2
301
+ echo " /cover-letter --resume ${RESUME_PATH:-<resume_yaml>}" >&2
302
+ ;;
303
+
304
+ render)
305
+ if [[ $# -lt 1 ]]; then
306
+ echo "Error: No YAML path provided." >&2
307
+ echo "Usage: chameleon render <yaml_path>" >&2
308
+ exit 1
309
+ fi
310
+ cd "$CHAMELEON_DIR"
311
+ exec "$PYTHON_BIN" scripts/render.py "$@"
312
+ ;;
313
+
314
+ init|init-cv)
315
+ if [[ $# -lt 1 ]]; then
316
+ echo "Error: No file path provided." >&2
317
+ echo "Usage: chameleon init <pdf_or_yaml_path>" >&2
318
+ exit 1
319
+ fi
320
+ echo "[*] CV initialization requires an AI assistant (use /init-cv in opencode)."
321
+ echo " opencode"
322
+ echo " /init-cv $1"
323
+ ;;
324
+
325
+ tui)
326
+ cd "$CHAMELEON_DIR"
327
+ exec ./tui.sh "$@"
328
+ ;;
329
+
330
+ setup|install)
331
+ cd "$CHAMELEON_DIR"
332
+ if [[ ! -f "$VENV_PYTHON" ]]; then
333
+ echo "[*] Creating virtual environment..." >&2
334
+ "$PYTHON_BIN" -m venv .venv 2>/dev/null || python3 -m venv .venv 2>/dev/null || true
335
+ fi
336
+ exec "$VENV_PYTHON" -m scripts.setup "$@"
337
+ ;;
338
+
339
+ help|--help|-h)
340
+ show_help
341
+ ;;
342
+
343
+ --version|-v)
344
+ echo "Chameleon CLI v1.0"
345
+ exit 0
346
+ ;;
347
+
348
+ *)
349
+ echo "Unknown command: $COMMAND" >&2
350
+ echo "Run 'chameleon help' for usage." >&2
351
+ exit 1
352
+ ;;
353
+ esac
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "chameleon-cv",
3
+ "version": "0.1.0",
4
+ "description": "AI-powered resume tailor — tailor your CV to any job posting with ATS-optimized PDFs",
5
+ "keywords": ["resume", "cv", "tailor", "ats", "job-search", "ai", "rendercv"],
6
+ "homepage": "https://github.com/HautlyS/chameleon#readme",
7
+ "bugs": { "url": "https://github.com/HautlyS/chameleon/issues" },
8
+ "license": "MIT",
9
+ "author": "chameleon",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/HautlyS/chameleon.git"
13
+ },
14
+ "bin": {
15
+ "chameleon": "bin/chameleon.js"
16
+ },
17
+ "files": [
18
+ "bin/",
19
+ "scripts/",
20
+ "profiles/",
21
+ "chameleon.sh",
22
+ "chameleon.bat",
23
+ "tui.sh",
24
+ "tui.bat",
25
+ "scan.sh",
26
+ "score.sh",
27
+ "Makefile",
28
+ "requirements.txt",
29
+ "open.json",
30
+ "AGENTS.md"
31
+ ],
32
+ "engines": { "node": ">=18" },
33
+ "scripts": {
34
+ "postinstall": "node bin/chameleon.js setup --auto",
35
+ "chameleon": "node bin/chameleon.js",
36
+ "setup": "node bin/chameleon.js setup",
37
+ "test": "node bin/chameleon.js test"
38
+ }
39
+ }