flowcvcli 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flowcvcli-0.2.0/.env.example +15 -0
- flowcvcli-0.2.0/LICENSE +21 -0
- flowcvcli-0.2.0/MANIFEST.in +9 -0
- flowcvcli-0.2.0/PKG-INFO +213 -0
- flowcvcli-0.2.0/README.md +183 -0
- flowcvcli-0.2.0/docs/API.md +208 -0
- flowcvcli-0.2.0/docs/RENDERING.md +124 -0
- flowcvcli-0.2.0/flowcvcli/__init__.py +8 -0
- flowcvcli-0.2.0/flowcvcli/__main__.py +5 -0
- flowcvcli-0.2.0/flowcvcli/api.py +29 -0
- flowcvcli-0.2.0/flowcvcli/cli.py +335 -0
- flowcvcli-0.2.0/flowcvcli/client.py +300 -0
- flowcvcli-0.2.0/flowcvcli/config.py +87 -0
- flowcvcli-0.2.0/flowcvcli/content.py +168 -0
- flowcvcli-0.2.0/flowcvcli/customization.py +85 -0
- flowcvcli-0.2.0/flowcvcli/markup.py +53 -0
- flowcvcli-0.2.0/flowcvcli/personal.py +66 -0
- flowcvcli-0.2.0/flowcvcli/photo.py +95 -0
- flowcvcli-0.2.0/flowcvcli/resume.py +122 -0
- flowcvcli-0.2.0/flowcvcli.egg-info/PKG-INFO +213 -0
- flowcvcli-0.2.0/flowcvcli.egg-info/SOURCES.txt +24 -0
- flowcvcli-0.2.0/flowcvcli.egg-info/dependency_links.txt +1 -0
- flowcvcli-0.2.0/flowcvcli.egg-info/entry_points.txt +2 -0
- flowcvcli-0.2.0/flowcvcli.egg-info/top_level.txt +1 -0
- flowcvcli-0.2.0/pyproject.toml +46 -0
- flowcvcli-0.2.0/setup.cfg +4 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Copy to `.env` (in the dir you run `flowcv` from, or ~/.config/flowcvcli/.env)
|
|
2
|
+
# and fill in. Real environment variables override this file. Secrets are gitignored.
|
|
3
|
+
|
|
4
|
+
# Auth — pick ONE:
|
|
5
|
+
# (a) the session cookie: DevTools > Application > Cookies > app.flowcv.com >
|
|
6
|
+
# copy the `flowcvsidapp` value (only this cookie is needed for auth):
|
|
7
|
+
FLOWCV_COOKIE=flowcvsidapp=s%3A...
|
|
8
|
+
# (b) or credentials — the tool logs in and caches the session to
|
|
9
|
+
# ~/.config/flowcvcli/session (override with $FLOWCV_SESSION_FILE):
|
|
10
|
+
# FLOWCV_EMAIL=you@example.com
|
|
11
|
+
# FLOWCV_PASSWORD=...
|
|
12
|
+
|
|
13
|
+
# RESUME_ID is OPTIONAL: with one resume the tool auto-selects it; set this (or
|
|
14
|
+
# pass --resume-id) only if your account has several. Get ids via `flowcv resumes`.
|
|
15
|
+
# FLOWCV_RESUME_ID=00000000-0000-0000-0000-000000000000
|
flowcvcli-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dannyota
|
|
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,9 @@
|
|
|
1
|
+
include LICENSE
|
|
2
|
+
include README.md
|
|
3
|
+
include .env.example
|
|
4
|
+
recursive-include docs *.md
|
|
5
|
+
|
|
6
|
+
# never ship local/dev artifacts or anything with personal data
|
|
7
|
+
exclude flowcv.py
|
|
8
|
+
prune .playwright-mcp
|
|
9
|
+
global-exclude *.pyc __pycache__ .env .flowcv_env .flowcv_session *.pdf *.html
|
flowcvcli-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flowcvcli
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Control a FlowCV resume from the command line or Python — content, design, templates, photo, publish and PDF export — via FlowCV's private JSON API. Zero dependencies.
|
|
5
|
+
Author: dannyota
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/dannyota/flowcvcli
|
|
8
|
+
Project-URL: Repository, https://github.com/dannyota/flowcvcli
|
|
9
|
+
Project-URL: Documentation, https://github.com/dannyota/flowcvcli/blob/main/docs/API.md
|
|
10
|
+
Project-URL: Issues, https://github.com/dannyota/flowcvcli/issues
|
|
11
|
+
Keywords: flowcv,resume,cv,cli,resume-builder,json-api,automation
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Office/Business
|
|
25
|
+
Classifier: Topic :: Utilities
|
|
26
|
+
Requires-Python: >=3.8
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# flowcvcli
|
|
32
|
+
|
|
33
|
+
[](https://pypi.org/project/flowcvcli/)
|
|
34
|
+
[](https://pypi.org/project/flowcvcli/)
|
|
35
|
+
[](LICENSE)
|
|
36
|
+
|
|
37
|
+
Control a [FlowCV](https://flowcv.com) resume from the **command line** or from
|
|
38
|
+
**Python** — content, header & links, **customization**, **templates**,
|
|
39
|
+
**avatar**, reorder/hide, multi-resume management, publish, and **PDF export**.
|
|
40
|
+
It drives FlowCV's private JSON API (the same calls the web app makes), so it
|
|
41
|
+
works for any FlowCV resume with your own session. **Zero dependencies** (Python
|
|
42
|
+
standard library only), so it's easy to drop into scripts and LLM agents.
|
|
43
|
+
|
|
44
|
+
> Unofficial and not affiliated with FlowCV. It uses FlowCV's undocumented
|
|
45
|
+
> internal API and may break if that changes; use it with your own account and at
|
|
46
|
+
> your own risk (mind FlowCV's Terms of Service). See [`docs/API.md`](docs/API.md)
|
|
47
|
+
> for the reverse-engineered API and [`docs/RENDERING.md`](docs/RENDERING.md) for
|
|
48
|
+
> how the editor renders the live preview and persists edits.
|
|
49
|
+
|
|
50
|
+
## Install
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install flowcvcli # installs the `flowcv` command
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Or run from source without installing:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
git clone https://github.com/dannyota/flowcvcli && cd flowcvcli
|
|
60
|
+
python3 flowcv.py --help # equivalent to the `flowcv` command
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Configure
|
|
64
|
+
|
|
65
|
+
Put a `.env` in the directory you run `flowcv` from (or in
|
|
66
|
+
`~/.config/flowcvcli/.env`). Real environment variables override it.
|
|
67
|
+
|
|
68
|
+
```dotenv
|
|
69
|
+
# Auth — pick ONE:
|
|
70
|
+
FLOWCV_COOKIE=flowcvsidapp=s%3A... # your session cookie, OR
|
|
71
|
+
# FLOWCV_EMAIL=you@example.com # log in with credentials instead
|
|
72
|
+
# FLOWCV_PASSWORD=... # (session cached to ~/.config/flowcvcli/session)
|
|
73
|
+
|
|
74
|
+
# FLOWCV_RESUME_ID=... # optional; only if your account has several resumes
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
- **Cookie**: DevTools → Application → Cookies → `app.flowcv.com` → copy the
|
|
78
|
+
`flowcvsidapp` value. That single cookie is the auth.
|
|
79
|
+
- **Credentials**: with `FLOWCV_EMAIL` + `FLOWCV_PASSWORD` the tool logs in and
|
|
80
|
+
caches the session (re-login is automatic when the cookie expires). The cache
|
|
81
|
+
is written `0600` to `~/.config/flowcvcli/session` (override with
|
|
82
|
+
`$FLOWCV_SESSION_FILE`).
|
|
83
|
+
- **Resume id** is optional: with one resume it's auto-selected; with several,
|
|
84
|
+
set `FLOWCV_RESUME_ID` or pass `--resume-id <id>` (run `flowcv resumes` to list).
|
|
85
|
+
|
|
86
|
+
## CLI
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
flowcv resumes # list resumes (id, title, share token)
|
|
90
|
+
flowcv show [section] # sections + entries (ids, labels, dates)
|
|
91
|
+
flowcv dump <section> <id> # one entry, fields + rich text
|
|
92
|
+
|
|
93
|
+
# manage resumes (multi-resume / paid plans)
|
|
94
|
+
flowcv new "My Second Resume" # new resume (same details+style, no content) -> prints id
|
|
95
|
+
flowcv duplicate ["Copy title"] # full copy of the current resume
|
|
96
|
+
flowcv rename "New Title" # rename the current resume
|
|
97
|
+
flowcv delete-resume --yes # permanent (refuses without --yes)
|
|
98
|
+
|
|
99
|
+
# content (markdown mini-format below); `add` creates the section if needed
|
|
100
|
+
flowcv add work --set title="Engineer" --set company="Acme" \
|
|
101
|
+
--set start=01/2022 --set end=Present --text $'- Did a measurable thing.'
|
|
102
|
+
flowcv desc work <id> --file role.md
|
|
103
|
+
flowcv field work <id> employer --text "Acme Corp"
|
|
104
|
+
flowcv rm work <id>
|
|
105
|
+
|
|
106
|
+
# reorder / hide / sections
|
|
107
|
+
flowcv reorder work <id3> <id1> <id2> # set entry order (all of the section's ids)
|
|
108
|
+
flowcv hide work <id> ; flowcv show-entry work <id>
|
|
109
|
+
flowcv rename-section skill "Core Skills"
|
|
110
|
+
flowcv section-icon skill head-side-brain
|
|
111
|
+
flowcv rm-section custom1 --yes # delete a section + its entries
|
|
112
|
+
flowcv reorder-sections profile work skill education # one-column order
|
|
113
|
+
|
|
114
|
+
# header details & links (links are social entries: orcid, googlescholar, github…)
|
|
115
|
+
flowcv pd jobTitle --text "Security Leader"
|
|
116
|
+
flowcv link orcid ORCID https://orcid.org/0000-0000-0000-0000
|
|
117
|
+
flowcv unlink orcid ; flowcv links
|
|
118
|
+
|
|
119
|
+
# avatar
|
|
120
|
+
flowcv avatar set https://example.com/me.png # upload from URL or file
|
|
121
|
+
flowcv avatar on | off | remove
|
|
122
|
+
|
|
123
|
+
# styling (a delta into resume.customization) and templates
|
|
124
|
+
flowcv customize font.fontFamily "Source Sans Pro"
|
|
125
|
+
flowcv customize colors.basic.single '"#0e374e"'
|
|
126
|
+
flowcv templates # lists each as [free] / [PAID] (paid needs a subscription)
|
|
127
|
+
flowcv apply-template <templateId> # warns first if the template is paid
|
|
128
|
+
|
|
129
|
+
# render & share
|
|
130
|
+
flowcv download -o resume.pdf # the rendered PDF
|
|
131
|
+
flowcv download --token <webToken> -o out.pdf # any PUBLIC resume by its share token (no auth)
|
|
132
|
+
flowcv share | publish | unpublish
|
|
133
|
+
|
|
134
|
+
flowcv login # refresh the cached session
|
|
135
|
+
flowcv md2html --file role.md # preview HTML (offline)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Any command takes `--resume-id <id>` to target a specific resume. (From source,
|
|
139
|
+
replace `flowcv` with `python3 flowcv.py`.)
|
|
140
|
+
|
|
141
|
+
## Library (for scripts & LLM agents)
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
from flowcvcli import FlowCV
|
|
145
|
+
|
|
146
|
+
fc = FlowCV() # or FlowCV(resume_id="...")
|
|
147
|
+
fc.set_personal_field("fullName", "Jane Doe")
|
|
148
|
+
fc.add_entry("work", sets={"jobTitle": "Engineer", "employer": "Acme",
|
|
149
|
+
"startDateNew": "01/2022", "endDateNew": "Present"},
|
|
150
|
+
md="- Shipped a thing with **measurable** impact.")
|
|
151
|
+
fc.set("font.fontFamily", "Source Sans Pro") # a customization delta
|
|
152
|
+
fc.set_photo("https://example.com/me.png") # avatar from URL
|
|
153
|
+
fc.apply_template("a3fb6c37-...") # a design from list_templates()
|
|
154
|
+
fc.save_pdf("resume.pdf") # render to PDF
|
|
155
|
+
|
|
156
|
+
# structure & resume management
|
|
157
|
+
fc.reorder_entries("work", ["id3", "id1", "id2"]) # set entry order
|
|
158
|
+
fc.rename_section("skill", "Core Skills"); fc.delete_section("custom1")
|
|
159
|
+
fc.hide_entry("work", "id", hidden=True)
|
|
160
|
+
new_id = fc.create_resume("Second Resume") # or fc.duplicate_resume()
|
|
161
|
+
fc.rename_resume("New Title"); fc.delete_resume() # delete is permanent
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Build → render → check → improve
|
|
165
|
+
|
|
166
|
+
The PDF *is* the rendered output. An agent can write content, `save_pdf(...)`,
|
|
167
|
+
**open the PDF to see the actual layout**, then adjust and re-render — a closed
|
|
168
|
+
feedback loop for building a resume from raw info.
|
|
169
|
+
|
|
170
|
+
## Markdown mini-format (`desc` / `add`)
|
|
171
|
+
|
|
172
|
+
| You write | You get |
|
|
173
|
+
|---|---|
|
|
174
|
+
| blank line | block separator |
|
|
175
|
+
| `## Heading` / `**Whole line bold**` | bold subheader |
|
|
176
|
+
| `- item` | bullet (consecutive = one list) |
|
|
177
|
+
| anything else | justified paragraph |
|
|
178
|
+
| `**bold**` inline | `<strong>bold</strong>` |
|
|
179
|
+
|
|
180
|
+
## How it works
|
|
181
|
+
|
|
182
|
+
- **Read-modify-write**: edits fetch the resume, change one part, and send it
|
|
183
|
+
back — unrelated fields are never touched.
|
|
184
|
+
- New entries append to the bottom of their section; use `reorder` to change order.
|
|
185
|
+
- The on-screen preview is client-side HTML; the **PDF download is a separate
|
|
186
|
+
server render** of the same data (details in [`docs/RENDERING.md`](docs/RENDERING.md)).
|
|
187
|
+
|
|
188
|
+
> **Scope:** this tool covers **resumes**. The same FlowCV account also has Cover
|
|
189
|
+
> Letters, Job Tracker, Email Signatures and Personal Websites (separate APIs —
|
|
190
|
+
> see `docs/API.md` "Other FlowCV products"); documented but not implemented here.
|
|
191
|
+
|
|
192
|
+
## Project layout
|
|
193
|
+
|
|
194
|
+
```
|
|
195
|
+
flowcvcli/ # the package (import flowcvcli)
|
|
196
|
+
config.py # resolve resume id + auth from .env / env vars
|
|
197
|
+
client.py # HTTP, login, cookie-jar session, retry, get_resume
|
|
198
|
+
markup.py # markdown <-> FlowCV rich-text HTML
|
|
199
|
+
content.py # sections & entries (add/edit/reorder/hide/sections)
|
|
200
|
+
personal.py # header details & links
|
|
201
|
+
customization.py # styling deltas & templates
|
|
202
|
+
photo.py # avatar upload / toggle
|
|
203
|
+
resume.py # list, create/duplicate/rename/delete, download, publish
|
|
204
|
+
api.py # FlowCV = Client + all mixins
|
|
205
|
+
cli.py / __main__.py # the `flowcv` command
|
|
206
|
+
docs/API.md # reverse-engineered API reference
|
|
207
|
+
docs/RENDERING.md # how the editor renders the preview & debounces saves
|
|
208
|
+
flowcv.py # source-tree entry point (python3 flowcv.py …)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## License
|
|
212
|
+
|
|
213
|
+
[MIT](LICENSE) © dannyota
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# flowcvcli
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/flowcvcli/)
|
|
4
|
+
[](https://pypi.org/project/flowcvcli/)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
Control a [FlowCV](https://flowcv.com) resume from the **command line** or from
|
|
8
|
+
**Python** — content, header & links, **customization**, **templates**,
|
|
9
|
+
**avatar**, reorder/hide, multi-resume management, publish, and **PDF export**.
|
|
10
|
+
It drives FlowCV's private JSON API (the same calls the web app makes), so it
|
|
11
|
+
works for any FlowCV resume with your own session. **Zero dependencies** (Python
|
|
12
|
+
standard library only), so it's easy to drop into scripts and LLM agents.
|
|
13
|
+
|
|
14
|
+
> Unofficial and not affiliated with FlowCV. It uses FlowCV's undocumented
|
|
15
|
+
> internal API and may break if that changes; use it with your own account and at
|
|
16
|
+
> your own risk (mind FlowCV's Terms of Service). See [`docs/API.md`](docs/API.md)
|
|
17
|
+
> for the reverse-engineered API and [`docs/RENDERING.md`](docs/RENDERING.md) for
|
|
18
|
+
> how the editor renders the live preview and persists edits.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install flowcvcli # installs the `flowcv` command
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Or run from source without installing:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
git clone https://github.com/dannyota/flowcvcli && cd flowcvcli
|
|
30
|
+
python3 flowcv.py --help # equivalent to the `flowcv` command
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Configure
|
|
34
|
+
|
|
35
|
+
Put a `.env` in the directory you run `flowcv` from (or in
|
|
36
|
+
`~/.config/flowcvcli/.env`). Real environment variables override it.
|
|
37
|
+
|
|
38
|
+
```dotenv
|
|
39
|
+
# Auth — pick ONE:
|
|
40
|
+
FLOWCV_COOKIE=flowcvsidapp=s%3A... # your session cookie, OR
|
|
41
|
+
# FLOWCV_EMAIL=you@example.com # log in with credentials instead
|
|
42
|
+
# FLOWCV_PASSWORD=... # (session cached to ~/.config/flowcvcli/session)
|
|
43
|
+
|
|
44
|
+
# FLOWCV_RESUME_ID=... # optional; only if your account has several resumes
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
- **Cookie**: DevTools → Application → Cookies → `app.flowcv.com` → copy the
|
|
48
|
+
`flowcvsidapp` value. That single cookie is the auth.
|
|
49
|
+
- **Credentials**: with `FLOWCV_EMAIL` + `FLOWCV_PASSWORD` the tool logs in and
|
|
50
|
+
caches the session (re-login is automatic when the cookie expires). The cache
|
|
51
|
+
is written `0600` to `~/.config/flowcvcli/session` (override with
|
|
52
|
+
`$FLOWCV_SESSION_FILE`).
|
|
53
|
+
- **Resume id** is optional: with one resume it's auto-selected; with several,
|
|
54
|
+
set `FLOWCV_RESUME_ID` or pass `--resume-id <id>` (run `flowcv resumes` to list).
|
|
55
|
+
|
|
56
|
+
## CLI
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
flowcv resumes # list resumes (id, title, share token)
|
|
60
|
+
flowcv show [section] # sections + entries (ids, labels, dates)
|
|
61
|
+
flowcv dump <section> <id> # one entry, fields + rich text
|
|
62
|
+
|
|
63
|
+
# manage resumes (multi-resume / paid plans)
|
|
64
|
+
flowcv new "My Second Resume" # new resume (same details+style, no content) -> prints id
|
|
65
|
+
flowcv duplicate ["Copy title"] # full copy of the current resume
|
|
66
|
+
flowcv rename "New Title" # rename the current resume
|
|
67
|
+
flowcv delete-resume --yes # permanent (refuses without --yes)
|
|
68
|
+
|
|
69
|
+
# content (markdown mini-format below); `add` creates the section if needed
|
|
70
|
+
flowcv add work --set title="Engineer" --set company="Acme" \
|
|
71
|
+
--set start=01/2022 --set end=Present --text $'- Did a measurable thing.'
|
|
72
|
+
flowcv desc work <id> --file role.md
|
|
73
|
+
flowcv field work <id> employer --text "Acme Corp"
|
|
74
|
+
flowcv rm work <id>
|
|
75
|
+
|
|
76
|
+
# reorder / hide / sections
|
|
77
|
+
flowcv reorder work <id3> <id1> <id2> # set entry order (all of the section's ids)
|
|
78
|
+
flowcv hide work <id> ; flowcv show-entry work <id>
|
|
79
|
+
flowcv rename-section skill "Core Skills"
|
|
80
|
+
flowcv section-icon skill head-side-brain
|
|
81
|
+
flowcv rm-section custom1 --yes # delete a section + its entries
|
|
82
|
+
flowcv reorder-sections profile work skill education # one-column order
|
|
83
|
+
|
|
84
|
+
# header details & links (links are social entries: orcid, googlescholar, github…)
|
|
85
|
+
flowcv pd jobTitle --text "Security Leader"
|
|
86
|
+
flowcv link orcid ORCID https://orcid.org/0000-0000-0000-0000
|
|
87
|
+
flowcv unlink orcid ; flowcv links
|
|
88
|
+
|
|
89
|
+
# avatar
|
|
90
|
+
flowcv avatar set https://example.com/me.png # upload from URL or file
|
|
91
|
+
flowcv avatar on | off | remove
|
|
92
|
+
|
|
93
|
+
# styling (a delta into resume.customization) and templates
|
|
94
|
+
flowcv customize font.fontFamily "Source Sans Pro"
|
|
95
|
+
flowcv customize colors.basic.single '"#0e374e"'
|
|
96
|
+
flowcv templates # lists each as [free] / [PAID] (paid needs a subscription)
|
|
97
|
+
flowcv apply-template <templateId> # warns first if the template is paid
|
|
98
|
+
|
|
99
|
+
# render & share
|
|
100
|
+
flowcv download -o resume.pdf # the rendered PDF
|
|
101
|
+
flowcv download --token <webToken> -o out.pdf # any PUBLIC resume by its share token (no auth)
|
|
102
|
+
flowcv share | publish | unpublish
|
|
103
|
+
|
|
104
|
+
flowcv login # refresh the cached session
|
|
105
|
+
flowcv md2html --file role.md # preview HTML (offline)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Any command takes `--resume-id <id>` to target a specific resume. (From source,
|
|
109
|
+
replace `flowcv` with `python3 flowcv.py`.)
|
|
110
|
+
|
|
111
|
+
## Library (for scripts & LLM agents)
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from flowcvcli import FlowCV
|
|
115
|
+
|
|
116
|
+
fc = FlowCV() # or FlowCV(resume_id="...")
|
|
117
|
+
fc.set_personal_field("fullName", "Jane Doe")
|
|
118
|
+
fc.add_entry("work", sets={"jobTitle": "Engineer", "employer": "Acme",
|
|
119
|
+
"startDateNew": "01/2022", "endDateNew": "Present"},
|
|
120
|
+
md="- Shipped a thing with **measurable** impact.")
|
|
121
|
+
fc.set("font.fontFamily", "Source Sans Pro") # a customization delta
|
|
122
|
+
fc.set_photo("https://example.com/me.png") # avatar from URL
|
|
123
|
+
fc.apply_template("a3fb6c37-...") # a design from list_templates()
|
|
124
|
+
fc.save_pdf("resume.pdf") # render to PDF
|
|
125
|
+
|
|
126
|
+
# structure & resume management
|
|
127
|
+
fc.reorder_entries("work", ["id3", "id1", "id2"]) # set entry order
|
|
128
|
+
fc.rename_section("skill", "Core Skills"); fc.delete_section("custom1")
|
|
129
|
+
fc.hide_entry("work", "id", hidden=True)
|
|
130
|
+
new_id = fc.create_resume("Second Resume") # or fc.duplicate_resume()
|
|
131
|
+
fc.rename_resume("New Title"); fc.delete_resume() # delete is permanent
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Build → render → check → improve
|
|
135
|
+
|
|
136
|
+
The PDF *is* the rendered output. An agent can write content, `save_pdf(...)`,
|
|
137
|
+
**open the PDF to see the actual layout**, then adjust and re-render — a closed
|
|
138
|
+
feedback loop for building a resume from raw info.
|
|
139
|
+
|
|
140
|
+
## Markdown mini-format (`desc` / `add`)
|
|
141
|
+
|
|
142
|
+
| You write | You get |
|
|
143
|
+
|---|---|
|
|
144
|
+
| blank line | block separator |
|
|
145
|
+
| `## Heading` / `**Whole line bold**` | bold subheader |
|
|
146
|
+
| `- item` | bullet (consecutive = one list) |
|
|
147
|
+
| anything else | justified paragraph |
|
|
148
|
+
| `**bold**` inline | `<strong>bold</strong>` |
|
|
149
|
+
|
|
150
|
+
## How it works
|
|
151
|
+
|
|
152
|
+
- **Read-modify-write**: edits fetch the resume, change one part, and send it
|
|
153
|
+
back — unrelated fields are never touched.
|
|
154
|
+
- New entries append to the bottom of their section; use `reorder` to change order.
|
|
155
|
+
- The on-screen preview is client-side HTML; the **PDF download is a separate
|
|
156
|
+
server render** of the same data (details in [`docs/RENDERING.md`](docs/RENDERING.md)).
|
|
157
|
+
|
|
158
|
+
> **Scope:** this tool covers **resumes**. The same FlowCV account also has Cover
|
|
159
|
+
> Letters, Job Tracker, Email Signatures and Personal Websites (separate APIs —
|
|
160
|
+
> see `docs/API.md` "Other FlowCV products"); documented but not implemented here.
|
|
161
|
+
|
|
162
|
+
## Project layout
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
flowcvcli/ # the package (import flowcvcli)
|
|
166
|
+
config.py # resolve resume id + auth from .env / env vars
|
|
167
|
+
client.py # HTTP, login, cookie-jar session, retry, get_resume
|
|
168
|
+
markup.py # markdown <-> FlowCV rich-text HTML
|
|
169
|
+
content.py # sections & entries (add/edit/reorder/hide/sections)
|
|
170
|
+
personal.py # header details & links
|
|
171
|
+
customization.py # styling deltas & templates
|
|
172
|
+
photo.py # avatar upload / toggle
|
|
173
|
+
resume.py # list, create/duplicate/rename/delete, download, publish
|
|
174
|
+
api.py # FlowCV = Client + all mixins
|
|
175
|
+
cli.py / __main__.py # the `flowcv` command
|
|
176
|
+
docs/API.md # reverse-engineered API reference
|
|
177
|
+
docs/RENDERING.md # how the editor renders the preview & debounces saves
|
|
178
|
+
flowcv.py # source-tree entry point (python3 flowcv.py …)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## License
|
|
182
|
+
|
|
183
|
+
[MIT](LICENSE) © dannyota
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# FlowCV private API — reference
|
|
2
|
+
|
|
3
|
+
Reverse-engineered from the FlowCV web app (`app.flowcv.com`). Unofficial; for
|
|
4
|
+
personal use. Base: `https://app.flowcv.com/api`. All app endpoints are
|
|
5
|
+
same-origin and authenticated by the **`flowcvsidapp`** session cookie alone
|
|
6
|
+
(other cookies — `i18n`, `loggedin`, `appVersion` — are not needed for auth).
|
|
7
|
+
|
|
8
|
+
Standard JSON envelope: `{ "success": bool, "data": ..., "error": "", "code": int }`.
|
|
9
|
+
A missing endpoint returns `code:404`; an existing endpoint with a bad/empty body
|
|
10
|
+
returns `code:500` (handler ran, validation failed) — useful for probing.
|
|
11
|
+
|
|
12
|
+
For **how the editor renders the live preview and when it persists edits**, see
|
|
13
|
+
[`RENDERING.md`](RENDERING.md). Short version: the preview is client-side React
|
|
14
|
+
HTML (no PDF/canvas), edits update instantly with no network, and saves are
|
|
15
|
+
debounced into the `save_entry` / `save_personal_details` / `save_customization`
|
|
16
|
+
PATCHes documented below — i.e. exactly what this tool sends.
|
|
17
|
+
|
|
18
|
+
**Editor boot sequence** (what the SPA fetches on load): `GET /auth/init_user`,
|
|
19
|
+
`GET /resumes/all`, `GET /letters/all`, `GET /trackers/all`, `GET /signatures/all`,
|
|
20
|
+
`GET /websites/all`, `GET /users/fetch_subscription_infos`,
|
|
21
|
+
`GET /users/invoices/pending_review`, then `GET /resumes/{id}` for the open resume.
|
|
22
|
+
|
|
23
|
+
## Auth
|
|
24
|
+
|
|
25
|
+
| Method | Path | Body | Notes |
|
|
26
|
+
|---|---|---|---|
|
|
27
|
+
| GET | `/auth/init_user` | — | seeds an (anonymous) session cookie. The web app calls it before login, but it is **optional** — login works standalone (the browser request skips it). |
|
|
28
|
+
| POST | `/auth/login` | `multipart/form-data`: `email`, `password` (+ empty `resumeData=undefined`, `letterData=undefined`, `resumeImg`, `letterImg`) | sets `flowcvsidapp` cookie on success. **Rate-limited per source IP** (≈100/day) — exhausting it on one machine doesn't affect another. |
|
|
29
|
+
|
|
30
|
+
Login flow: (optionally GET `init_user` for an anonymous session) → POST `login`
|
|
31
|
+
on the same cookie jar → the jar now holds the authenticated `flowcvsidapp`.
|
|
32
|
+
`init_user` is best-effort; a failure there must not block the login.
|
|
33
|
+
|
|
34
|
+
## Resumes (resume-level)
|
|
35
|
+
|
|
36
|
+
| Method | Path | Body / Query | Returns |
|
|
37
|
+
|---|---|---|---|
|
|
38
|
+
| GET | `/resumes/all` | — | `data.resumes[]` (id, title, webToken, webResumeLive, order, …) |
|
|
39
|
+
| GET | `/resumes/{resumeId}` | — | `data.resume` (full resume object) |
|
|
40
|
+
| POST | `/resumes/create` | `{clientResume: {…full resume object…}}` | create a resume. The body must be a **complete** resume object (every NOT-NULL column), so the reliable way is to **clone a full existing resume** (`GET /resumes/{id}`), reassign `id`+`uuid`, set `title`, empty `content` (or keep it for a duplicate), and **drop** `webToken`/`feedbackToken`/`createdAt`/`updatedAt` (server regenerates). A hand-built partial body fails with Postgres `23502` (not-null). Note: the one-resume free-plan cap is **not** enforced on this endpoint. |
|
|
41
|
+
| POST | `/resumes/duplicate` | `{resumeId}` | native duplicate — but returned a generic error in testing; duplicating via `create` (clone, keep `content`) is what this tool does instead. |
|
|
42
|
+
| PATCH | `/resumes/rename_resume` | `{resumeId, resumeTitle}` | rename a resume |
|
|
43
|
+
| DELETE | `/resumes/delete_resume?resumeId={id}` | — | **permanently delete** a resume (irreversible) |
|
|
44
|
+
| PATCH | `/resumes/apply_template` | `{resumeId, templateId, customization: {…template's full customization…}, personalDetails: {…current…}}` | applies a design. `templateId` + `customization` come from the template list (below). |
|
|
45
|
+
| PATCH | `/resumes/publish_web_resume` | `{publish: bool, resumeId}` | toggle the public web resume |
|
|
46
|
+
| GET | `/resumes/download?resumeId={id}&previewPageCount={n}` | — | **PDF bytes** (`application/pdf`). `previewPageCount` does not truncate; any value returns the full doc. |
|
|
47
|
+
| GET | `/api/public/download_resume?token={webToken}` | — | **public** PDF of any *shared* resume by its web token — no auth/ownership needed. (Only when the resume's download is enabled; otherwise 400.) |
|
|
48
|
+
| DELETE | `/resumes/delete_entry?resumeId§ionId&entryId` | — | delete a content entry (see below) |
|
|
49
|
+
|
|
50
|
+
The full-resume GET also exposes `webToken` (public URL
|
|
51
|
+
`https://flowcv.com/resume/{webToken}`), `webResumeLive`, `feedbackToken`. Top-level
|
|
52
|
+
resume keys (for the `create` clone): `id, userId, mongoId, title, order,
|
|
53
|
+
feedbackToken, webToken, uuid, feedbackEnabled, webResumeLive,
|
|
54
|
+
webResumeDownloadBtn, webResumeSearchIndex, webResumeCached, personalDetails,
|
|
55
|
+
content, customization, feedback, businessDetails, downloads,
|
|
56
|
+
usingBusinessTemplateId, schemaVersion, lastChangeAt, createdAt, updatedAt, lng,
|
|
57
|
+
tags`.
|
|
58
|
+
|
|
59
|
+
## Content (sections & entries)
|
|
60
|
+
|
|
61
|
+
`data.resume.content` is a map of `sectionId → { entries[], iconKey, displayName,
|
|
62
|
+
sectionType }`. Known sections: `profile` (Summary), `work` (Experience),
|
|
63
|
+
`education`, `skill`, `publication`, `organisation`, `custom1` (sectionType
|
|
64
|
+
`custom`), plus language/certificate/interest/project/course/award/reference/
|
|
65
|
+
declaration.
|
|
66
|
+
|
|
67
|
+
| Method | Path | Body | Notes |
|
|
68
|
+
|---|---|---|---|
|
|
69
|
+
| PATCH | `/resumes/save_entry` | `{resumeId, sectionId, entry}` | **update** an existing entry (send the whole entry object). |
|
|
70
|
+
| PATCH | `/resumes/save_entry` | `{resumeId, sectionId, entry:{id, isHidden:false}, sectionType, sectionDisplayName, sectionIconKey}` | **create** an entry — required extra section-meta fields. If the section doesn't exist yet, this also **creates the section**. New entries append to the bottom. Populate fields with a follow-up update call. |
|
|
71
|
+
| DELETE | `/resumes/delete_entry?resumeId§ionId&entryId` | — | delete an entry |
|
|
72
|
+
| PATCH | `/resumes/save_entries_order` | `{resumeId, sectionId, newEntriesIdsOrder:[id,…], disableAutoSort:true}` | **reorder entries** within a section (the array order). `disableAutoSort` keeps the manual order (else FlowCV auto-sorts by date). |
|
|
73
|
+
| PATCH | `/resumes/save_section_name` | `{resumeId, sectionId, displayName}` | **rename** a section heading |
|
|
74
|
+
| PATCH | `/resumes/save_section_icon` | `{resumeId, sectionId, iconKey}` | change a section's icon |
|
|
75
|
+
| DELETE | `/resumes/delete_section?resumeId§ionId` | — | **delete a whole section** and all its entries |
|
|
76
|
+
|
|
77
|
+
To **hide/show** a single entry, `save_entry` it with `entry.isHidden = true|false`
|
|
78
|
+
(it stays in the resume but is omitted from output). **Reorder sections** by
|
|
79
|
+
writing `customization.sectionOrder.<layout>.sectionsSorted` (a list of section
|
|
80
|
+
ids) via `save_customization` — section order lives in `customization`, keyed per
|
|
81
|
+
column layout (`one`, `two`, `mix`), not in `content`. (`save_section` exists too
|
|
82
|
+
but 500s on every body shape tried; the granular `save_section_*` endpoints above
|
|
83
|
+
are what the app actually uses. `reorder_entries`/`reorder_sections`/`rename_section`
|
|
84
|
+
are all 404 — the real names are `save_entries_order`/`save_section_name`.)
|
|
85
|
+
|
|
86
|
+
Section meta (`sectionType`, `displayName`, `iconKey`) for creating sections:
|
|
87
|
+
`profile`→(profile, Summary, address-card), `work`→(work, Professional
|
|
88
|
+
Experience, briefcase), `education`→(education, Education, graduation-cap),
|
|
89
|
+
`skill`→(skill, Skills, head-side-brain), `publication`→(publication,
|
|
90
|
+
Publications, newspaper), `organisation`→(organisation, Organisations,
|
|
91
|
+
house-user), `custom1`→(custom, Custom, star).
|
|
92
|
+
|
|
93
|
+
Rich-text fields are HTML: `<p style="text-align: justify">…</p>` for paragraphs,
|
|
94
|
+
`<p…><strong>…</strong></p>` for bold subheaders, `<ul><li…><p…>…</p></li></ul>`
|
|
95
|
+
for bullets. `profile` entries use a `text` field; `skill` entries use `skill`
|
|
96
|
+
(title) + `infoHtml`; most others use `description`.
|
|
97
|
+
|
|
98
|
+
No reorder endpoint (`reorder_entries` 404s). To reorder, reassign entry content
|
|
99
|
+
across the existing array slots.
|
|
100
|
+
|
|
101
|
+
## Personal details & header links
|
|
102
|
+
|
|
103
|
+
| Method | Path | Body |
|
|
104
|
+
|---|---|---|
|
|
105
|
+
| PATCH | `/resumes/save_personal_details` | `{resumeId, personalDetails: {…full object…}}` |
|
|
106
|
+
|
|
107
|
+
Always send the **full** `personalDetails` object with only the target field
|
|
108
|
+
changed (it replaces the whole object). Header links live in
|
|
109
|
+
`personalDetails.social` as `{platform: {display, link}}` (e.g. `linkedIn`,
|
|
110
|
+
`orcid`, `googlescholar`) and are shown per `personalDetails.detailsOrder`
|
|
111
|
+
(e.g. `["displayEmail","phone","address","linkedIn","orcid","googlescholar"]`).
|
|
112
|
+
The legacy single link is `personalDetails.website` + `websiteLink`.
|
|
113
|
+
|
|
114
|
+
## Photo / avatar
|
|
115
|
+
|
|
116
|
+
| Method | Path | Body |
|
|
117
|
+
|---|---|---|
|
|
118
|
+
| POST | `/resumes/upload_profile_pic` | `multipart/form-data`: `resumeId` + `file` (image bytes) → `{data:{imageId:"avatar/….png"}}` |
|
|
119
|
+
|
|
120
|
+
Then save the id into `personalDetails.photo` (via `save_personal_details`):
|
|
121
|
+
`{imageId, shape:"round", xPct, yPct, widthPct, heightPct, originalWidth, originalHeight}`
|
|
122
|
+
(use a whole-image crop: xPct≈yPct≈0.0005, widthPct≈heightPct≈0.999). Toggle
|
|
123
|
+
display with the customization delta `header.photo.show` = `true|false`.
|
|
124
|
+
|
|
125
|
+
## Customization (styling)
|
|
126
|
+
|
|
127
|
+
| Method | Path | Body |
|
|
128
|
+
|---|---|---|
|
|
129
|
+
| PATCH | `/resumes/save_customization` | `{resumeId, customizationUpdates: [{path, value}, …]}` |
|
|
130
|
+
|
|
131
|
+
**Delta API**: each update is a dot-`path` into `resume.customization` and a new
|
|
132
|
+
`value`. Examples:
|
|
133
|
+
- Columns: `layout.colsFromDetails.top|left|right` = `"one"|"two"`
|
|
134
|
+
- Font: `font.fontFamily` = `"Source Sans Pro"`, `font.selected` = `"serif"|"sans"`
|
|
135
|
+
- Colors: `colors.basic.single` = `"#0e374e"`, `colors.mode` = `"basic"|"advanced"`
|
|
136
|
+
- Spacing: `spacing.fontSize`, `spacing.lineHeight`, `spacing.marginHorizontal`
|
|
137
|
+
- Headings: `heading.style` = `"line"|"box"`, `heading.capitalization`
|
|
138
|
+
- Page: `pageFormat` = `"A4"|"Letter"`
|
|
139
|
+
|
|
140
|
+
The full `customization` schema is visible in `GET /resumes/{id}` (under
|
|
141
|
+
`data.resume.customization`) and in the `create` default.
|
|
142
|
+
|
|
143
|
+
The **Customize** panel groups (each = one or more delta paths under
|
|
144
|
+
`customization`) are: **Document** (page format, date format), **Templates**
|
|
145
|
+
(browse/apply, below), **Layout** (`layout.colsFromDetails…` columns one/two/mix,
|
|
146
|
+
per-section placement), **Font Size**, **Spacing** (`spacing.*`), **Entry Layout**,
|
|
147
|
+
**Section Headings** (`heading.style`, `heading.capitalization`, heading icons),
|
|
148
|
+
**Font** — separate **body font** and **name font** — **Colors**
|
|
149
|
+
(`colors.mode`/`colors.basic.single`, accent, and *Color Area*: full / page /
|
|
150
|
+
header / border), **Header** (text alignment, details arrangement, icon style),
|
|
151
|
+
**Photo** (`header.photo.show`), **Link Styling**, **Footer** (toggle page
|
|
152
|
+
numbers / email / name), and per-**Section** customizations. "Create template"
|
|
153
|
+
publishes the current design as a shareable template. The panel also has
|
|
154
|
+
**undo/redo**. All of these are just `save_customization` deltas — discover exact
|
|
155
|
+
paths by diffing `data.resume.customization` before/after a change in the UI.
|
|
156
|
+
|
|
157
|
+
## Templates
|
|
158
|
+
|
|
159
|
+
| Method | Path | Returns |
|
|
160
|
+
|---|---|---|
|
|
161
|
+
| GET | `/pubcache/published-resume-templates` | the full template catalog (id, title, customization, `isPremium`, …) |
|
|
162
|
+
| GET | `/api/resume-templates/get_shared_template?resumeId={id}` | the template shared/applied to a resume |
|
|
163
|
+
|
|
164
|
+
Each catalog entry has **`isPremium`** (bool): `false` = free, `true` = needs a
|
|
165
|
+
FlowCV subscription to apply. Show this to users before they apply one.
|
|
166
|
+
|
|
167
|
+
To apply a template: pick its `templateId` + `customization` from the catalog and
|
|
168
|
+
PATCH `apply_template` (above).
|
|
169
|
+
|
|
170
|
+
## Download & share menu (resume editor)
|
|
171
|
+
|
|
172
|
+
The editor's top-right controls map to these endpoints:
|
|
173
|
+
|
|
174
|
+
| UI control | Endpoint / effect |
|
|
175
|
+
|---|---|
|
|
176
|
+
| **Download** button | `GET /resumes/download` → PDF (server render). Shows a "✅ downloaded" modal after. |
|
|
177
|
+
| ⋯ → **Download via email** | emails the PDF to the account (send-email endpoint; body not captured). |
|
|
178
|
+
| ⋯ → **Get shareable link** | the **web resume**: *Enable sharing* = `publish_web_resume {publish}`; link is `https://flowcv.com/resume/{webToken}`; *Display download button* gates the public `public/download_resume?token=` PDF (off → 400). |
|
|
179
|
+
|
|
180
|
+
## AI Tools (per resume, Pro plan) — `/resume/ai-tools`
|
|
181
|
+
|
|
182
|
+
Gated behind the **Pro** subscription ("AI features are available on our Pro
|
|
183
|
+
plan"). Two tools observed (both Beta): **Translate resume** (create a translated
|
|
184
|
+
copy in another language, layout intact) and **Check spelling & grammar** (scan +
|
|
185
|
+
fix suggestions). Endpoints not captured (Pro-gated on the test account).
|
|
186
|
+
|
|
187
|
+
## Other FlowCV products (same account & session, separate APIs)
|
|
188
|
+
|
|
189
|
+
FlowCV is more than resumes. The same `flowcvsidapp` session authenticates these
|
|
190
|
+
sibling products — each with its own `…/all` list endpoint, all fetched on editor
|
|
191
|
+
load. This tool currently covers **resumes only**; these are documented for
|
|
192
|
+
discovery, not yet implemented:
|
|
193
|
+
|
|
194
|
+
| Product | List endpoint | UI |
|
|
195
|
+
|---|---|---|
|
|
196
|
+
| **Cover Letters** | `GET /api/letters/all` | `/cover-letters` |
|
|
197
|
+
| **Job Tracker** | `GET /api/trackers/all` | `/job-tracker` |
|
|
198
|
+
| **Email Signatures** | `GET /api/signatures/all` | email-signature generator |
|
|
199
|
+
| **Personal Websites** | `GET /api/websites/all` | personal-site builder |
|
|
200
|
+
|
|
201
|
+
Account/billing: `GET /api/users/fetch_subscription_infos` (plan + entitlements;
|
|
202
|
+
free accounts get one resume, premium templates and AI gated),
|
|
203
|
+
`GET /api/users/invoices/pending_review`. The user object from `auth/login` also
|
|
204
|
+
carries `paid`, `activePlans`, AB-test flags, and `numberOfLogins`.
|
|
205
|
+
|
|
206
|
+
> Free vs paid recap: first resume is free forever; additional resumes, premium
|
|
207
|
+
> templates (`isPremium`), AI Tools, and likely the public-download button are
|
|
208
|
+
> Pro features. Show users the free/paid split before they hit a 400 or upsell.
|