finch-cli 0.1.1__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {finch_cli-0.1.1 → finch_cli-0.2.0}/PKG-INFO +14 -6
- {finch_cli-0.1.1 → finch_cli-0.2.0}/README.md +11 -4
- {finch_cli-0.1.1 → finch_cli-0.2.0}/finch_cli/__init__.py +1 -1
- finch_cli-0.2.0/finch_cli/auth.py +192 -0
- {finch_cli-0.1.1 → finch_cli-0.2.0}/finch_cli/cli.py +109 -16
- {finch_cli-0.1.1 → finch_cli-0.2.0}/pyproject.toml +5 -1
- {finch_cli-0.1.1 → finch_cli-0.2.0}/.gitignore +0 -0
- {finch_cli-0.1.1 → finch_cli-0.2.0}/LICENSE +0 -0
- {finch_cli-0.1.1 → finch_cli-0.2.0}/REDDIT_POST.md +0 -0
- {finch_cli-0.1.1 → finch_cli-0.2.0}/docs/tui-jobs.svg +0 -0
- {finch_cli-0.1.1 → finch_cli-0.2.0}/docs/tui-library.svg +0 -0
- {finch_cli-0.1.1 → finch_cli-0.2.0}/docs/tui-tailor.svg +0 -0
- {finch_cli-0.1.1 → finch_cli-0.2.0}/examples/base_resume.md +0 -0
- {finch_cli-0.1.1 → finch_cli-0.2.0}/examples/job_post.txt +0 -0
- {finch_cli-0.1.1 → finch_cli-0.2.0}/finch_cli/__main__.py +0 -0
- {finch_cli-0.1.1 → finch_cli-0.2.0}/finch_cli/fetch.py +0 -0
- {finch_cli-0.1.1 → finch_cli-0.2.0}/finch_cli/jobs.py +0 -0
- {finch_cli-0.1.1 → finch_cli-0.2.0}/finch_cli/output.py +0 -0
- {finch_cli-0.1.1 → finch_cli-0.2.0}/finch_cli/score.py +0 -0
- {finch_cli-0.1.1 → finch_cli-0.2.0}/finch_cli/storage.py +0 -0
- {finch_cli-0.1.1 → finch_cli-0.2.0}/finch_cli/tailor.py +0 -0
- {finch_cli-0.1.1 → finch_cli-0.2.0}/finch_cli/tui.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: finch-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Tailor your resume to any job posting from your terminal.
|
|
5
5
|
Project-URL: Homepage, https://applyfinch.com
|
|
6
6
|
Project-URL: Repository, https://github.com/applyEasy/finch-cli
|
|
@@ -21,6 +21,7 @@ Classifier: Topic :: Utilities
|
|
|
21
21
|
Requires-Python: >=3.10
|
|
22
22
|
Requires-Dist: click>=8.1.0
|
|
23
23
|
Requires-Dist: httpx>=0.27.0
|
|
24
|
+
Requires-Dist: lxml-html-clean>=0.4
|
|
24
25
|
Requires-Dist: openai>=1.30.0
|
|
25
26
|
Requires-Dist: rich>=13.0.0
|
|
26
27
|
Requires-Dist: textual>=0.83.0
|
|
@@ -42,13 +43,20 @@ a terminal client for job hunting. browse fresh internship + new-grad postings,
|
|
|
42
43
|
|
|
43
44
|
```
|
|
44
45
|
pip install finch-cli
|
|
45
|
-
|
|
46
|
+
finch login
|
|
46
47
|
finch ui
|
|
47
48
|
```
|
|
48
49
|
|
|
49
|
-
|
|
50
|
+
`finch login` opens a sign-in link in your browser. once you sign in with google or email on applyfinch.com, the cli is paired and tailoring just works -- no api key to manage, the backend pays the llm bill.
|
|
51
|
+
|
|
52
|
+
prefer your own key? skip `finch login` and set one instead:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
export DEEPSEEK_API_KEY=sk-...
|
|
56
|
+
finch ui
|
|
57
|
+
```
|
|
50
58
|
|
|
51
|
-
|
|
59
|
+
deepseek keys are cheap (~30x cheaper than the major frontier models). get one at https://platform.deepseek.com/api_keys. any openai-compatible chat-completions endpoint also works (openai, together, groq, fireworks): pass `--base-url` and `--api-key`.
|
|
52
60
|
|
|
53
61
|
## what it does
|
|
54
62
|
|
|
@@ -58,7 +66,7 @@ three tabs, one keyboard.
|
|
|
58
66
|
|
|
59
67
|
**library** is the list of tailored resumes you've saved. Markdown preview on the right.
|
|
60
68
|
|
|
61
|
-
**tailor** is the three-pane editor: base resume / job posting / tailored output.
|
|
69
|
+
**tailor** is the three-pane editor: base resume / job posting / tailored output. the model rewrites bullets and ordering to fit the posting. won't invent skills you don't have. a match-analysis panel at the bottom shows an ats-style score, which job keywords your resume covers, which ones it doesn't, and how much the tailored version moved the needle vs the base.
|
|
62
70
|
|
|
63
71
|
## install (pure cli, no tui)
|
|
64
72
|
|
|
@@ -10,13 +10,20 @@ a terminal client for job hunting. browse fresh internship + new-grad postings,
|
|
|
10
10
|
|
|
11
11
|
```
|
|
12
12
|
pip install finch-cli
|
|
13
|
-
|
|
13
|
+
finch login
|
|
14
14
|
finch ui
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
`finch login` opens a sign-in link in your browser. once you sign in with google or email on applyfinch.com, the cli is paired and tailoring just works -- no api key to manage, the backend pays the llm bill.
|
|
18
|
+
|
|
19
|
+
prefer your own key? skip `finch login` and set one instead:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
export DEEPSEEK_API_KEY=sk-...
|
|
23
|
+
finch ui
|
|
24
|
+
```
|
|
18
25
|
|
|
19
|
-
|
|
26
|
+
deepseek keys are cheap (~30x cheaper than the major frontier models). get one at https://platform.deepseek.com/api_keys. any openai-compatible chat-completions endpoint also works (openai, together, groq, fireworks): pass `--base-url` and `--api-key`.
|
|
20
27
|
|
|
21
28
|
## what it does
|
|
22
29
|
|
|
@@ -26,7 +33,7 @@ three tabs, one keyboard.
|
|
|
26
33
|
|
|
27
34
|
**library** is the list of tailored resumes you've saved. Markdown preview on the right.
|
|
28
35
|
|
|
29
|
-
**tailor** is the three-pane editor: base resume / job posting / tailored output.
|
|
36
|
+
**tailor** is the three-pane editor: base resume / job posting / tailored output. the model rewrites bullets and ordering to fit the posting. won't invent skills you don't have. a match-analysis panel at the bottom shows an ats-style score, which job keywords your resume covers, which ones it doesn't, and how much the tailored version moved the needle vs the base.
|
|
30
37
|
|
|
31
38
|
## install (pure cli, no tui)
|
|
32
39
|
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""finch-cli sign-in flow.
|
|
2
|
+
|
|
3
|
+
Talks to the applyfinch.com backend's device-flow endpoints so users can sign
|
|
4
|
+
in by clicking a link in their terminal instead of pasting an API key. Once
|
|
5
|
+
signed in, the local tailor command POSTs to /api/v1/cli/tailor and the
|
|
6
|
+
backend pays the upstream LLM bill.
|
|
7
|
+
|
|
8
|
+
Token storage: ~/.config/finch-cli/token (chmod 600 on POSIX). One file, one
|
|
9
|
+
token. `finch logout` deletes it.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import platform
|
|
16
|
+
import stat
|
|
17
|
+
import sys
|
|
18
|
+
import time
|
|
19
|
+
import webbrowser
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
import httpx
|
|
23
|
+
|
|
24
|
+
DEFAULT_FINCH_BASE_URL = "https://applyfinch.com"
|
|
25
|
+
FINCH_BASE_URL = os.environ.get("FINCH_BASE_URL", DEFAULT_FINCH_BASE_URL).rstrip("/")
|
|
26
|
+
START_TIMEOUT = 15
|
|
27
|
+
POLL_TIMEOUT = 10
|
|
28
|
+
POLL_BACKOFF_AFTER_404 = 3
|
|
29
|
+
MAX_POLL_SECONDS = 300
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AuthError(RuntimeError):
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _config_dir() -> Path:
|
|
37
|
+
base = os.environ.get("XDG_CONFIG_HOME")
|
|
38
|
+
if base:
|
|
39
|
+
return Path(base) / "finch-cli"
|
|
40
|
+
return Path.home() / ".config" / "finch-cli"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def token_path() -> Path:
|
|
44
|
+
return _config_dir() / "token"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def load_token() -> str | None:
|
|
48
|
+
p = token_path()
|
|
49
|
+
if not p.is_file():
|
|
50
|
+
return None
|
|
51
|
+
try:
|
|
52
|
+
token = p.read_text(encoding="utf-8").strip()
|
|
53
|
+
except OSError:
|
|
54
|
+
return None
|
|
55
|
+
return token or None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def save_token(token: str) -> None:
|
|
59
|
+
p = token_path()
|
|
60
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
61
|
+
p.write_text(token + "\n", encoding="utf-8")
|
|
62
|
+
if os.name == "posix":
|
|
63
|
+
try:
|
|
64
|
+
os.chmod(p, stat.S_IRUSR | stat.S_IWUSR)
|
|
65
|
+
except OSError:
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def clear_token() -> bool:
|
|
70
|
+
p = token_path()
|
|
71
|
+
if not p.exists():
|
|
72
|
+
return False
|
|
73
|
+
try:
|
|
74
|
+
p.unlink()
|
|
75
|
+
return True
|
|
76
|
+
except OSError:
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _device_label() -> str:
|
|
81
|
+
try:
|
|
82
|
+
node = platform.node() or "unknown"
|
|
83
|
+
except Exception:
|
|
84
|
+
node = "unknown"
|
|
85
|
+
return f"finch-cli on {platform.system()} ({node})"[:120]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def start_device_flow() -> dict:
|
|
89
|
+
url = f"{FINCH_BASE_URL}/api/v1/cli/start"
|
|
90
|
+
try:
|
|
91
|
+
with httpx.Client(timeout=START_TIMEOUT) as client:
|
|
92
|
+
r = client.post(url, json={"device_label": _device_label()})
|
|
93
|
+
except httpx.RequestError as e:
|
|
94
|
+
raise AuthError(f"Could not reach {FINCH_BASE_URL}: {e}") from e
|
|
95
|
+
if r.status_code != 200:
|
|
96
|
+
raise AuthError(f"start failed: HTTP {r.status_code} {r.text[:200]}")
|
|
97
|
+
data = r.json()
|
|
98
|
+
for field in ("device_code", "user_code", "verification_url", "expires_in", "interval"):
|
|
99
|
+
if field not in data:
|
|
100
|
+
raise AuthError(f"start response missing {field}")
|
|
101
|
+
return data
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def poll_for_token(device_code: str, interval: int, expires_in: int) -> str:
|
|
105
|
+
url = f"{FINCH_BASE_URL}/api/v1/cli/poll"
|
|
106
|
+
poll_interval = max(2, int(interval))
|
|
107
|
+
started = time.monotonic()
|
|
108
|
+
deadline = started + min(expires_in, MAX_POLL_SECONDS)
|
|
109
|
+
|
|
110
|
+
while time.monotonic() < deadline:
|
|
111
|
+
try:
|
|
112
|
+
with httpx.Client(timeout=POLL_TIMEOUT) as client:
|
|
113
|
+
r = client.post(url, json={"device_code": device_code})
|
|
114
|
+
except httpx.RequestError:
|
|
115
|
+
time.sleep(poll_interval)
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
if r.status_code == 410:
|
|
119
|
+
raise AuthError("Device code expired. Run `finch login` again.")
|
|
120
|
+
if r.status_code != 200:
|
|
121
|
+
time.sleep(poll_interval + POLL_BACKOFF_AFTER_404)
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
data = r.json()
|
|
126
|
+
except Exception:
|
|
127
|
+
time.sleep(poll_interval)
|
|
128
|
+
continue
|
|
129
|
+
|
|
130
|
+
status = data.get("status")
|
|
131
|
+
if status == "approved":
|
|
132
|
+
token = data.get("token")
|
|
133
|
+
if not token:
|
|
134
|
+
raise AuthError("Approved but no token returned.")
|
|
135
|
+
return token
|
|
136
|
+
if status == "pending":
|
|
137
|
+
time.sleep(poll_interval)
|
|
138
|
+
continue
|
|
139
|
+
if status == "expired":
|
|
140
|
+
raise AuthError("Device code expired. Run `finch login` again.")
|
|
141
|
+
time.sleep(poll_interval)
|
|
142
|
+
|
|
143
|
+
raise AuthError("Timed out waiting for sign-in. Run `finch login` again.")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def open_browser_safely(url: str) -> bool:
|
|
147
|
+
try:
|
|
148
|
+
if sys.platform.startswith("linux") and not os.environ.get("DISPLAY") and not os.environ.get("WAYLAND_DISPLAY"):
|
|
149
|
+
return False
|
|
150
|
+
return webbrowser.open_new_tab(url)
|
|
151
|
+
except Exception:
|
|
152
|
+
return False
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def call_tailor(token: str, base_resume: str, job_text: str, model: str | None = None) -> str:
|
|
156
|
+
url = f"{FINCH_BASE_URL}/api/v1/cli/tailor"
|
|
157
|
+
payload: dict = {"resume": base_resume, "job": job_text}
|
|
158
|
+
if model:
|
|
159
|
+
payload["model"] = model
|
|
160
|
+
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
|
161
|
+
try:
|
|
162
|
+
with httpx.Client(timeout=120) as client:
|
|
163
|
+
r = client.post(url, json=payload, headers=headers)
|
|
164
|
+
except httpx.RequestError as e:
|
|
165
|
+
raise AuthError(f"Could not reach {FINCH_BASE_URL}: {e}") from e
|
|
166
|
+
|
|
167
|
+
if r.status_code == 401:
|
|
168
|
+
raise AuthError("Your finch token is no longer valid. Run `finch logout && finch login`.")
|
|
169
|
+
if r.status_code == 503:
|
|
170
|
+
raise AuthError("The finch backend is misconfigured. Try again later or use --api-key.")
|
|
171
|
+
if r.status_code >= 400:
|
|
172
|
+
try:
|
|
173
|
+
err = r.json().get("error")
|
|
174
|
+
except Exception:
|
|
175
|
+
err = r.text[:200]
|
|
176
|
+
raise AuthError(f"Backend tailor failed (HTTP {r.status_code}): {err}")
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
data = r.json()
|
|
180
|
+
return data["tailored"]
|
|
181
|
+
except Exception as e:
|
|
182
|
+
raise AuthError(f"Bad backend response: {e}") from e
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def revoke_token(token: str) -> bool:
|
|
186
|
+
url = f"{FINCH_BASE_URL}/api/v1/cli/revoke"
|
|
187
|
+
try:
|
|
188
|
+
with httpx.Client(timeout=10) as client:
|
|
189
|
+
r = client.post(url, headers={"Authorization": f"Bearer {token}"})
|
|
190
|
+
return r.status_code == 200
|
|
191
|
+
except httpx.RequestError:
|
|
192
|
+
return False
|
|
@@ -9,6 +9,17 @@ import click
|
|
|
9
9
|
from rich.console import Console
|
|
10
10
|
|
|
11
11
|
from . import __version__
|
|
12
|
+
from .auth import (
|
|
13
|
+
AuthError,
|
|
14
|
+
FINCH_BASE_URL,
|
|
15
|
+
call_tailor,
|
|
16
|
+
clear_token,
|
|
17
|
+
load_token,
|
|
18
|
+
open_browser_safely,
|
|
19
|
+
poll_for_token,
|
|
20
|
+
revoke_token,
|
|
21
|
+
start_device_flow,
|
|
22
|
+
)
|
|
12
23
|
from .fetch import FetchError, fetch_job
|
|
13
24
|
from .output import write_or_print
|
|
14
25
|
from .tailor import DEFAULT_BASE_URL, DEFAULT_MODEL, TailorError, tailor_resume
|
|
@@ -110,28 +121,110 @@ def tailor(
|
|
|
110
121
|
sys.exit(1)
|
|
111
122
|
console.print(f"[dim]Fetched job posting ({len(job_text):,} chars).[/dim]")
|
|
112
123
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
f"[yellow]warning:[/] sending your API key to {base_url}. "
|
|
116
|
-
f"only use --base-url for providers you trust."
|
|
117
|
-
)
|
|
124
|
+
finch_token = load_token()
|
|
125
|
+
use_backend = finch_token is not None and not api_key and not base_url
|
|
118
126
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
job_text,
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
+
if use_backend:
|
|
128
|
+
console.print(f"[dim]Signed in as finch user. Tailoring via {FINCH_BASE_URL}.[/dim]")
|
|
129
|
+
try:
|
|
130
|
+
with console.status("Tailoring resume against the job...", spinner="dots"):
|
|
131
|
+
tailored = call_tailor(finch_token, base_resume, job_text, model=None)
|
|
132
|
+
except AuthError as e:
|
|
133
|
+
console.print(f"[red]Tailoring failed:[/red] {e}")
|
|
134
|
+
sys.exit(1)
|
|
135
|
+
else:
|
|
136
|
+
if base_url and base_url != DEFAULT_BASE_URL:
|
|
137
|
+
console.print(
|
|
138
|
+
f"[yellow]warning:[/] sending your API key to {base_url}. "
|
|
139
|
+
f"only use --base-url for providers you trust."
|
|
127
140
|
)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
141
|
+
try:
|
|
142
|
+
with console.status("Tailoring resume against the job...", spinner="dots"):
|
|
143
|
+
tailored = tailor_resume(
|
|
144
|
+
base_resume,
|
|
145
|
+
job_text,
|
|
146
|
+
model=model,
|
|
147
|
+
api_key=api_key,
|
|
148
|
+
base_url=base_url,
|
|
149
|
+
)
|
|
150
|
+
except TailorError as e:
|
|
151
|
+
console.print(f"[red]Tailoring failed:[/red] {e}")
|
|
152
|
+
sys.exit(1)
|
|
131
153
|
|
|
132
154
|
write_or_print(tailored, out_path)
|
|
133
155
|
|
|
134
156
|
|
|
157
|
+
@main.command()
|
|
158
|
+
def login() -> None:
|
|
159
|
+
"""Sign in to finch through your browser. No API key needed."""
|
|
160
|
+
if load_token():
|
|
161
|
+
console.print("[yellow]You're already signed in.[/] Run [bold]finch logout[/] first to sign in as someone else.")
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
with console.status("Starting sign-in...", spinner="dots"):
|
|
166
|
+
data = start_device_flow()
|
|
167
|
+
except AuthError as e:
|
|
168
|
+
console.print(f"[red]Could not start sign-in:[/red] {e}")
|
|
169
|
+
sys.exit(1)
|
|
170
|
+
|
|
171
|
+
url = data["verification_url"]
|
|
172
|
+
user_code = data["user_code"]
|
|
173
|
+
|
|
174
|
+
console.print()
|
|
175
|
+
console.print(f"[bold]Open this URL in your browser to sign in:[/bold]")
|
|
176
|
+
console.print(f" [cyan]{url}[/cyan]")
|
|
177
|
+
console.print()
|
|
178
|
+
console.print(f"If your browser does not open automatically, the code is [bold]{user_code}[/bold].")
|
|
179
|
+
console.print()
|
|
180
|
+
|
|
181
|
+
opened = open_browser_safely(url)
|
|
182
|
+
if opened:
|
|
183
|
+
console.print("[dim]Opened your browser. Sign in, then come back here.[/dim]")
|
|
184
|
+
else:
|
|
185
|
+
console.print("[dim]Could not open a browser automatically. Open the URL above on any device.[/dim]")
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
with console.status("Waiting for sign-in...", spinner="dots"):
|
|
189
|
+
token = poll_for_token(data["device_code"], data["interval"], data["expires_in"])
|
|
190
|
+
except AuthError as e:
|
|
191
|
+
console.print(f"[red]Sign-in failed:[/red] {e}")
|
|
192
|
+
sys.exit(1)
|
|
193
|
+
|
|
194
|
+
from .auth import save_token
|
|
195
|
+
save_token(token)
|
|
196
|
+
console.print()
|
|
197
|
+
console.print("[green]Signed in.[/green] You can now run [bold]finch tailor[/] without an API key.")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@main.command()
|
|
201
|
+
def logout() -> None:
|
|
202
|
+
"""Sign out of finch and delete the local token."""
|
|
203
|
+
token = load_token()
|
|
204
|
+
if not token:
|
|
205
|
+
console.print("You're not signed in.")
|
|
206
|
+
return
|
|
207
|
+
revoke_token(token)
|
|
208
|
+
cleared = clear_token()
|
|
209
|
+
if cleared:
|
|
210
|
+
console.print("[green]Signed out.[/green]")
|
|
211
|
+
else:
|
|
212
|
+
console.print("[yellow]Could not delete the local token file.[/]")
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@main.command()
|
|
216
|
+
def whoami() -> None:
|
|
217
|
+
"""Show whether you're signed in to finch."""
|
|
218
|
+
token = load_token()
|
|
219
|
+
if not token:
|
|
220
|
+
console.print("Not signed in. Run [bold]finch login[/].")
|
|
221
|
+
return
|
|
222
|
+
from .auth import token_path
|
|
223
|
+
console.print("[green]Signed in to finch.[/green]")
|
|
224
|
+
console.print(f" token file: {token_path()}")
|
|
225
|
+
console.print(f" backend: {FINCH_BASE_URL}")
|
|
226
|
+
|
|
227
|
+
|
|
135
228
|
@main.command()
|
|
136
229
|
@click.option(
|
|
137
230
|
"--demo",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "finch-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
description = "Tailor your resume to any job posting from your terminal."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -23,6 +23,10 @@ classifiers = [
|
|
|
23
23
|
dependencies = [
|
|
24
24
|
"click>=8.1.0",
|
|
25
25
|
"httpx>=0.27.0",
|
|
26
|
+
# lxml_html_clean is a transitive of trafilatura but newer lxml split it
|
|
27
|
+
# out, so declare it explicitly here to keep `pip install finch-cli` working
|
|
28
|
+
# on a clean machine.
|
|
29
|
+
"lxml_html_clean>=0.4",
|
|
26
30
|
"openai>=1.30.0",
|
|
27
31
|
"rich>=13.0.0",
|
|
28
32
|
"textual>=0.83.0",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|