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/README.md ADDED
@@ -0,0 +1,356 @@
1
+ # Chameleon — Resume Tailor
2
+
3
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue)](https://python.org)
4
+ [![MIT License](https://img.shields.io/badge/license-MIT-green)](LICENSE)
5
+ [![OpenCode](https://img.shields.io/badge/OpenCode-ready-blueviolet)](https://opencode.ai)
6
+ [![Claude Code](https://img.shields.io/badge/Claude%20Code-ready-darkviolet)](https://claude.ai)
7
+ [![Codex](https://img.shields.io/badge/Codex-ready-black)](https://openai.com/codex)
8
+
9
+ Tailor your master resume to any job posting with AI. Produces ATS-optimized PDFs without inventing experience. Works as a **TUI**, **CLI**, or via **AI assistant** (OpenCode / Claude Code / Codex).
10
+
11
+ ---
12
+
13
+ ## Why Chameleon?
14
+
15
+ | Problem | Chameleon |
16
+ |---------|-----------|
17
+ | Generic resume rejected by ATS | Tailored keywords + **invisible ATS injection** |
18
+ | Fabricated skills get caught | **Strict no-fabrication** rules enforced by AI and code |
19
+ | Lost track of applications | **Persistent analysis** — JSON artifacts saved for reuse |
20
+ | Manual tailoring takes hours | **4-pass pipeline** runs in under 2 minutes |
21
+ | No feedback loop | **Score every CV** against the JD with category breakdowns |
22
+ | Scattered job platforms | **20-platform scanner** with API, RSS, and scraping |
23
+
24
+ ---
25
+
26
+ ## Quick Start
27
+
28
+ ### Prerequisites
29
+
30
+ - Python 3.10+
31
+ - An AI assistant binary: [OpenCode](https://opencode.ai), [Claude Code](https://claude.ai/claude-code), or [Codex CLI](https://openai.com/codex)
32
+
33
+ ### Install
34
+
35
+ ```bash
36
+ git clone https://github.com/davidalecrim1/chameleon.git
37
+ cd chameleon
38
+ make install-tools # creates .venv + installs rendercv
39
+ ```
40
+
41
+ ### Makefile Targets
42
+
43
+ | Target | Usage | Description |
44
+ |--------|-------|-------------|
45
+ | `install-tools` | `make install-tools` | Create `.venv`, install `rendercv[full]` |
46
+ | `render` | `make render FILE=<yaml>` | Render a CV YAML to PDF |
47
+ | `tailor` | `make tailor JD=<text> COMPANY=<name>` | Tailor CV for a job posting |
48
+ | `score` | `make score JD=<text>` | Score job match against profile |
49
+ | `scan` | `make scan QUERY=<query>` | Scan job platforms |
50
+ | `scan-all` | `make scan-all` | Scan all 20 platforms |
51
+ | `scan-tier1` | `make scan-tier1` | Scan Tier 1 platforms only |
52
+ | `list-platforms` | `make list-platforms` | List available scanner platforms |
53
+
54
+ ---
55
+
56
+ ## Three Ways to Use
57
+
58
+ ### 1. TUI — Full Terminal UI
59
+
60
+ ```bash
61
+ ./tui.sh
62
+ ```
63
+
64
+ Browse scored jobs from 20 platforms, tailor with `Ctrl+T`, view diffs between master and tailored YAML, and edit config live — all in the terminal.
65
+
66
+ **6 Tabs:**
67
+
68
+ | Tab | Shortcut | Shows |
69
+ |-----|----------|-------|
70
+ | **Jobs** | `Ctrl+1` | Scanned/pasted jobs with scores, gap analysis, experience fit |
71
+ | **Editor** | `Ctrl+2` | Inline nano-style editor for job descriptions |
72
+ | **Profile** | `Ctrl+3` | GitHub profile (languages, tech stack, matching repos) |
73
+ | **Scanner** | `Ctrl+4` | Live pipeline logs — scan progress, tailor phases, render status |
74
+ | **Tailored** | `Ctrl+5` | Tailored CVs with diff view, scores, PDF links |
75
+ | **Settings** | `Ctrl+6` | Live config editor — paths, scan queries, score weights, fonts, colors |
76
+
77
+ **Keybindings:**
78
+
79
+ | Key | Action |
80
+ |-----|--------|
81
+ | `Ctrl+Q` | Quit (with confirmation) |
82
+ | `Ctrl+S` | Scan job platforms |
83
+ | `Ctrl+Shift+S` / `F2` | Custom scan with platform/query options |
84
+ | `Ctrl+N` | Paste a job description |
85
+ | `Ctrl+E` | Edit selected job description |
86
+ | `Ctrl+T` | Tailor CV for selected job |
87
+ | `Ctrl+R` | Re-score all jobs |
88
+ | `Ctrl+D` | Delete selected job |
89
+ | `Ctrl+O` | Open job URL in browser |
90
+ | `Enter` | Show job detail or tailored CV diff |
91
+ | `/` | Focus search bar |
92
+ | `F1` | Help screen |
93
+ | `F5` | Refresh / re-score |
94
+ | `↑/↓` | Navigate rows |
95
+
96
+ Full reference: [docs/TUI.md](docs/TUI.md)
97
+
98
+ ---
99
+
100
+ ### 2. AI Assistant — Slash Commands
101
+
102
+ ```bash
103
+ opencode # or claude, or codex
104
+ ```
105
+
106
+ | Command | What it does |
107
+ |---------|-------------|
108
+ | `/init-cv <path>` | Import master CV from PDF or YAML |
109
+ | `/chameleon <url-or-text>` | Full pipeline: analyze → tailor → render |
110
+ | `/tailor-cv --analysis <id> --cv <name>` | Tailor from saved analysis |
111
+ | `/score-cv --analysis <id> --cv <path>` | Score against analysis |
112
+ | `/cover-letter <url-or-text>` | Generate cover letter |
113
+ | `/question "<text>"` | Answer screening question |
114
+ | `/render-cv [file]` | Re-render a YAML to PDF |
115
+ | `/scan-jobs [--query]` | Scan job platforms |
116
+ | `/job-matcher score --job-url <url>` | Score job against your profile |
117
+
118
+ ---
119
+
120
+ ### 3. CLI — Headless / Scripting
121
+
122
+ ```bash
123
+ # ── Unified CLI (recommended) ──────────────────────────────────────────
124
+
125
+ # Tailor a CV from text
126
+ ./chameleon tailor "Senior Rust Engineer at Acme Corp..." --company Acme
127
+
128
+ # Tailor from a URL (auto-crawls the job posting)
129
+ ./chameleon tailor https://jobs.example.com/senior-rust-engineer
130
+
131
+ # Tailor from a file
132
+ ./chameleon tailor jd.txt --company Acme --title "Rust Engineer"
133
+
134
+ # Pipe a job description via stdin
135
+ cat jd.txt | ./chameleon tailor --company Acme
136
+
137
+ # Score a job match against your profile
138
+ ./chameleon score "Python developer, Django, PostgreSQL..."
139
+ ./chameleon score https://jobs.example.com/python-dev --json
140
+
141
+ # Scan job platforms
142
+ ./chameleon scan -q "rust engineer" --tier1
143
+ ./chameleon scan -q "python" -p remoteok,remotive --json
144
+ ./chameleon scan --all --limit 10 -o output/scans/results.json
145
+
146
+ # Render a YAML to PDF
147
+ ./chameleon render templates/david_acme_rust_engineer_cv.yaml
148
+
149
+ # ── Windows ─────────────────────────────────────────────────────────────
150
+ chameleon.bat tailor "Senior Rust Engineer..." --company Acme
151
+ chameleon.bat score "Python developer..."
152
+ chameleon.bat scan -q "rust engineer" --tier1
153
+
154
+ # ── Direct Python modules (alternative) ─────────────────────────────────
155
+
156
+ # Tailor a CV directly
157
+ python3 -m scripts.tailor_cv "Senior Rust Engineer at Acme..." --company Acme --title Engineer
158
+
159
+ # Score a job against your profile
160
+ python3 -m scripts.job_matcher "Python developer..." --json
161
+
162
+ # Scan job platforms
163
+ python3 -m scripts.job_scanner.scanner --query "rust engineer" --platforms remoteok,hn_hiring --json
164
+
165
+ # Render a YAML
166
+ make render FILE=templates/david_acme_rust_engineer_cv.yaml
167
+
168
+ # ── Make targets ────────────────────────────────────────────────────────
169
+
170
+ make tailor JD="Senior Rust Engineer..." COMPANY=Acme
171
+ make score JD="Python developer..."
172
+ make scan QUERY="rust engineer" PLATFORMS=remoteok,hn_hiring
173
+ make scan-all QUERY="python"
174
+ make scan-tier1
175
+ make list-platforms
176
+ ```
177
+
178
+ ---
179
+
180
+ ## Example Workflow
181
+
182
+ ```bash
183
+ # 1. Import your master CV
184
+ /init-cv ~/Documents/resume.pdf
185
+ # → templates/david_cv.yaml
186
+
187
+ # 2. Tailor for a job posting
188
+ /chameleon https://jobs.example.com/senior-engineer-123
189
+ # → Analysis: output/job_analyses/a7c19f2d__Acme__Rust_Engineer.json
190
+ # → Tailored YAML: templates/david_acme_rust_engineer_cv.yaml
191
+ # → PDF: output/david_acme_rust_engineer_cv.pdf
192
+
193
+ # 3. Score the result against the same analysis
194
+ /score-cv --analysis a7c19f2d --cv templates/david_acme_rust_engineer_cv.yaml
195
+ # → Score: 85/100 (Languages: 22, Frameworks: 20, Domains: 23, CV Alignment: 20)
196
+ ```
197
+
198
+ ---
199
+
200
+ ## How It Works
201
+
202
+ ### 4-Pass Pipeline
203
+
204
+ When you invoke a tailor (`Ctrl+T` or `/chameleon`), the pipeline runs in 4 passes:
205
+
206
+ ```
207
+ Pass 1: TailoringBrief Algorithmic JD analysis — seniority, gaps, repo matching
208
+ Pass 2: AI Tailor LLM rewrites summary, reorders highlights/skills
209
+ Pass 3: AI Review Double review with tiebreaker, applies corrections
210
+ Pass 4: Render rendercv → PDF (+ optional ATS ghost injection)
211
+ ```
212
+
213
+ ### ATS Ghost Injection
214
+
215
+ When `ats_ghost_enabled` is on, the PDF is post-processed to inject invisible keyword text (`font-size: 1px, color: white`) at the bottom of every page. This ensures ATS parsers see important keywords even if the layout is unusual. Extra terms can be configured in Settings.
216
+
217
+ ---
218
+
219
+ ## Project Layout
220
+
221
+ ```
222
+ .chameleon/ Config JSON, SQLite DB, fonts, cache
223
+ .claude/ AI skills + agents (OpenCode / Claude Code)
224
+ .agents/ AI skills + agents (Codex)
225
+ opencode.json OpenCode project config (12 commands, permissions)
226
+ templates/ Master + tailored CV YAMLs
227
+ output/ Rendered PDFs, job analysis JSONs
228
+ profiles/ GitHub profile data
229
+ chameleon.sh CLI entry point (Unix/Mac)
230
+ chameleon.bat CLI entry point (Windows)
231
+ scan.sh / scan.bat Quick scanner wrapper
232
+ score.sh / score.bat Quick scorer wrapper
233
+ tui.sh / tui.bat TUI launcher
234
+ scripts/
235
+ tui/ Textual TUI (6 tabs, 19 keybindings)
236
+ job_scanner/ 20-platform job scanner
237
+ tailor_cv.py 4-pass pipeline entry point
238
+ tailor_brief.py Algorithmic JD analysis
239
+ tailor_prompts.py AI prompt templates
240
+ job_matcher.py Scoring engine (4-category rubric)
241
+ config.py JSON-backed configuration
242
+ db.py SQLite persistence
243
+ render.py RenderCV wrapper
244
+ ats_ghost.py Invisible ATS keyword injection
245
+ docs/
246
+ ARCHITECTURE.md Component architecture, data flow, config ref
247
+ TUI.md Full TUI keybinding and tab reference
248
+ ```
249
+
250
+ ---
251
+
252
+ ## Configuration
253
+
254
+ All settings live in `.chameleon/config.json`. Editable live via the Settings tab.
255
+
256
+ | Category | Key | Default | Description |
257
+ |----------|-----|---------|-------------|
258
+ | **Paths** | `master_cv_path` | `templates/my_cv.yaml` | Master CV YAML |
259
+ | | `profile_path` | `profiles/github_repos.json` | GitHub profile JSON |
260
+ | | `opencode_bin_path` | `~/.opencode/bin/opencode` | Path to opencode binary |
261
+ | **Scan** | `scan_queries` | `["python", "rust", "ai engineer"]` | Default scan queries |
262
+ | | `scan_platforms` | `"remoteok,remotive,hn_hiring"` | Default platforms |
263
+ | **Scoring** | `score_weight_*` | `25` each | Category weights (total 100) |
264
+ | | `score_threshold_strong` | `80` | Minimum score for "strong match" |
265
+ | | `score_threshold_good` | `60` | Minimum score for "good match" |
266
+ | | `score_threshold_moderate` | `40` | Minimum score for "moderate match" |
267
+ | **CV Design** | `cv_font_family` | `"Source Sans Pro"` | Font for rendered PDF |
268
+ | | `cv_font_size_body` | `9.5pt` | Body font size |
269
+ | | `cv_line_spacing` | `0.8em` | Line spacing |
270
+ | | `cv_entry_spacing` | `0.2cm` | Space between entries |
271
+ | | `cv_primary_color` | `#2b3a67` | Primary accent color |
272
+ | | `cv_secondary_color` | `#3a5a8c` | Secondary accent color |
273
+ | | `cv_page_margin_*` | `1.0-1.5cm` | Page margins |
274
+ | **ATS** | `ats_ghost_enabled` | `true` | Invisible keyword injection |
275
+ | | `ats_ghost_extra_terms` | `"django, flutter, ..."` | Extra ATS keywords |
276
+ | | `ats_ghost_font_size` | `1` | Font size for ghost text (px) |
277
+ | | `ats_ghost_color` | `#ffffff` | Font color for ghost text |
278
+
279
+ ---
280
+
281
+ ## Constraints
282
+
283
+ - **Never fabricate** experience, skills, metrics, titles, or dates
284
+ - **Preserve all facts** — company names, titles, locations, dates are immutable
285
+ - **Never edit the master CV** — always write a new file to `templates/`
286
+ - **Match JD language** — if the JD says "CI/CD" and your CV says "continuous integration", use "CI/CD"
287
+ - **2 pages max** — drop lower-impact bullets before adding new ones
288
+
289
+ ---
290
+
291
+ ## Troubleshooting
292
+
293
+ | Symptom | Likely Cause | Fix |
294
+ |---------|-------------|-----|
295
+ | `Ctrl+T` does nothing | No job selected | Select a row in the Jobs tab first |
296
+ | Tailor hangs / never completes | opencode binary not found | Set `opencode_bin_path` in Settings tab |
297
+ | Tailor hangs / never completes | opencode timeout (180s default) | Wait — long prompts take time; check Scanner tab for progress |
298
+ | `make render` fails | RenderCV not installed | Run `make install-tools` |
299
+ | Scan returns no results | Platform parser needs update | Try `--platforms hn_hiring` (most stable parser) |
300
+ | Tailored tab is empty | Tailor never succeeded | Check Scanner tab for error messages |
301
+ | `Ctrl+T` says "already in progress" | Previous tailor still running | Wait for completion or restart the TUI |
302
+ | PDF has no content / blank | Incompatible YAML schema | Run `/init-cv` on your master CV to normalize |
303
+ | AI assistant command not found | Skill not loaded | Check `opencode.json` has `.claude/skills` in `skills` array |
304
+ | ATS ghost not working | Feature disabled | Enable `ats_ghost_enabled` in Settings tab |
305
+ | Job URLs not opening | No `open`/`xdg-open` on system | Install `xdg-utils` (Linux) or check OS path |
306
+
307
+ ---
308
+
309
+ ## Architecture
310
+
311
+ ```
312
+ ┌──────────┐ ┌──────────┐ ┌──────────────────┐
313
+ │ TUI │ │ CLI │ │ AI Assistant │
314
+ │ (Textual)│ │ (Python) │ │ (Claude/Codex/OC) │
315
+ └────┬─────┘ └────┬─────┘ └────────┬─────────┘
316
+ │ │ │
317
+ ┌────┴─────────────┴──────────────────┴──────────┐
318
+ │ Python Backend │
319
+ │ tailor_cv.py tailor_brief.py job_matcher.py │
320
+ │ config.py db.py render.py │
321
+ │ job_scanner/ ats_ghost.py │
322
+ └──────────────────────────────────────────────────┘
323
+ │ │ │
324
+ ┌────┴─────────────┴──────────────────┴──────────┐
325
+ │ Storage │
326
+ │ .chameleon/ templates/ output/ profiles/ │
327
+ └──────────────────────────────────────────────────┘
328
+ ```
329
+
330
+ Key design decisions:
331
+ - **Layered architecture** — interfaces never touch storage directly
332
+ - **Subprocess kill registry** — no orphan processes on crash
333
+ - **TeeStream** — stdout from background threads appears in TUI log in real time
334
+ - **Crash-safe notifications** — debug toasts never throw, even during shutdown
335
+
336
+ See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for full detail.
337
+
338
+ ---
339
+
340
+ ## OpenCode Integration
341
+
342
+ This project provides `opencode.json` at the root — OpenCode auto-loads it on open:
343
+
344
+ - **12 slash commands** registered with usage hints (`/chameleon`, `/tailor-cv`, `/score-cv`, etc.)
345
+ - **Skill paths**: `.claude/skills/` and `.agents/skills/` scanned for `SKILL.md` definitions
346
+ - **All standard tools** pre-authorized (read, edit, bash, glob, grep, task, webfetch, websearch, question, skill)
347
+ - **Batch tool** enabled for parallel operations
348
+ - **Instructions**: `AGENTS.md` loaded as project context
349
+
350
+ For Claude Code: `CLAUDE.md` (synced copy of AGENTS.md) is auto-loaded on open.
351
+
352
+ ---
353
+
354
+ ## License
355
+
356
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env node
2
+ const { spawn, execSync } = require("child_process");
3
+ const path = require("path");
4
+ const fs = require("fs");
5
+
6
+ const ROOT = path.resolve(__dirname, "..");
7
+ const VENV_PYTHON = path.join(ROOT, ".venv", process.platform === "win32" ? "Scripts\\python.exe" : "bin", process.platform === "win32" ? "python.exe" : "python3");
8
+
9
+ function runSetup(args = []) {
10
+ const py = findPython();
11
+ console.log("[*] Running full setup (venv + deps + Playwright + Chromium)...\n");
12
+ try {
13
+ // First create venv if needed
14
+ if (!fs.existsSync(path.join(ROOT, ".venv"))) {
15
+ execSync(`${py} -m venv "${path.join(ROOT, ".venv")}"`, { stdio: "inherit", cwd: ROOT });
16
+ }
17
+ // Then run the shared setup module
18
+ const proc = spawn(VENV_PYTHON, ["-m", "scripts.setup", ...args], {
19
+ stdio: "inherit",
20
+ cwd: ROOT,
21
+ env: { ...process.env, PYTHONIOENCODING: "utf-8" }
22
+ });
23
+ proc.on("exit", (code) => {
24
+ if (code !== 0) {
25
+ console.error("[!] Setup failed. Make sure Python 3.10+ is installed.");
26
+ }
27
+ process.exit(code ?? 1);
28
+ });
29
+ } catch (e) {
30
+ console.error("[!] Setup failed:", e.message);
31
+ console.error(" Make sure Python 3.10+ is installed.");
32
+ process.exit(1);
33
+ }
34
+ }
35
+
36
+ function findPython() {
37
+ if (fs.existsSync(VENV_PYTHON)) return VENV_PYTHON;
38
+ try { return execSync("which python3 2>/dev/null || which python 2>/dev/null || echo python", { encoding: "utf8" }).trim(); }
39
+ catch { return "python3"; }
40
+ }
41
+
42
+ function ensureVenv() {
43
+ if (fs.existsSync(VENV_PYTHON)) return true;
44
+ runSetup();
45
+ return fs.existsSync(VENV_PYTHON);
46
+ }
47
+
48
+ function runChameleon(args) {
49
+ const py = findPython();
50
+ const pythonPath = fs.existsSync(VENV_PYTHON) ? VENV_PYTHON : py;
51
+ const proc = spawn(pythonPath, ["-m", "scripts.tailor_cv", ...args], {
52
+ stdio: "inherit",
53
+ cwd: ROOT,
54
+ env: { ...process.env, PYTHONIOENCODING: "utf-8", TERM: "xterm-256color" }
55
+ });
56
+ proc.on("exit", (code) => process.exit(code ?? 1));
57
+ }
58
+
59
+ function runTui(args) {
60
+ if (!fs.existsSync(VENV_PYTHON)) {
61
+ console.error("[!] Run 'chameleon setup' first to install dependencies.");
62
+ process.exit(1);
63
+ }
64
+ const proc = spawn(VENV_PYTHON, ["-m", "scripts.tui_app", ...args], {
65
+ stdio: "inherit",
66
+ cwd: ROOT,
67
+ env: { ...process.env, PYTHONIOENCODING: "utf-8", TERM: "xterm-256color" }
68
+ });
69
+ proc.on("exit", (code) => process.exit(code ?? 1));
70
+ }
71
+
72
+ function showHelp() {
73
+ console.log(`
74
+ Chameleon CLI v0.1.0 — AI Resume Tailor
75
+
76
+ Usage:
77
+ npx chameleon <command> [options]
78
+
79
+ Commands:
80
+ tailor <text/url/file> Tailor CV for a job posting
81
+ score <text/url/file> Score job match against your profile
82
+ scan [options] Scan 20+ job platforms
83
+ cover <text/url/file> Generate a cover letter
84
+ render <yaml_path> Render a CV YAML to PDF
85
+ tui Launch the interactive TUI
86
+ setup Install all dependencies + Playwright
87
+ wizard Interactive setup wizard
88
+ help Show this help
89
+
90
+ Examples:
91
+ npx chameleon tailor "Senior Rust Engineer at Acme"
92
+ npx chameleon tailor https://jobs.example.com/senior-rust
93
+ npx chameleon scan -q "rust engineer" --tier1
94
+ npx chameleon tui
95
+ npx chameleon setup
96
+ `);
97
+ }
98
+
99
+ async function main() {
100
+ const args = process.argv.slice(2);
101
+ if (args.length === 0) return showHelp();
102
+
103
+ const cmd = args[0];
104
+ const rest = args.slice(1);
105
+
106
+ switch (cmd) {
107
+ case "setup":
108
+ case "install":
109
+ runSetup(rest);
110
+ break;
111
+ case "tui":
112
+ if (!fs.existsSync(VENV_PYTHON)) { runSetup(); }
113
+ runTui(rest);
114
+ break;
115
+ case "wizard":
116
+ if (!fs.existsSync(VENV_PYTHON)) { runSetup(); }
117
+ const wizard = spawn(VENV_PYTHON, ["-m", "scripts.wizard"], { stdio: "inherit", cwd: ROOT });
118
+ wizard.on("exit", (code) => process.exit(code ?? 1));
119
+ break;
120
+ case "help":
121
+ case "--help":
122
+ case "-h":
123
+ showHelp();
124
+ break;
125
+ case "test":
126
+ if (!fs.existsSync(VENV_PYTHON)) { runSetup(); }
127
+ const tester = spawn(VENV_PYTHON, ["-m", "pytest", "tests/", "-v", "--tb=short"], { stdio: "inherit", cwd: ROOT });
128
+ tester.on("exit", (code) => process.exit(code ?? 1));
129
+ break;
130
+ default:
131
+ if (!fs.existsSync(VENV_PYTHON)) { runSetup(); }
132
+ runChameleon(args);
133
+ }
134
+ }
135
+
136
+ main().catch(console.error);