warmpath 0.3.0__tar.gz → 0.3.2__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.
- {warmpath-0.3.0 → warmpath-0.3.2}/PKG-INFO +1 -1
- warmpath-0.3.2/README.md +55 -0
- {warmpath-0.3.0 → warmpath-0.3.2}/pyproject.toml +1 -1
- {warmpath-0.3.0 → warmpath-0.3.2}/tests/test_auth.py +133 -2
- {warmpath-0.3.0 → warmpath-0.3.2}/uv.lock +1 -1
- {warmpath-0.3.0 → warmpath-0.3.2}/warmpath/auth.py +5 -3
- {warmpath-0.3.0 → warmpath-0.3.2}/warmpath/cli.py +34 -2
- warmpath-0.3.0/README.md +0 -80
- {warmpath-0.3.0 → warmpath-0.3.2}/.gitignore +0 -0
- {warmpath-0.3.0 → warmpath-0.3.2}/AGENTS.md +0 -0
- {warmpath-0.3.0 → warmpath-0.3.2}/Taskfile.yaml +0 -0
- {warmpath-0.3.0 → warmpath-0.3.2}/UNLICENSE +0 -0
- {warmpath-0.3.0 → warmpath-0.3.2}/docs/skill.md +0 -0
- {warmpath-0.3.0 → warmpath-0.3.2}/scripts/release.sh +0 -0
- {warmpath-0.3.0 → warmpath-0.3.2}/tests/test_cli_commands.py +0 -0
- {warmpath-0.3.0 → warmpath-0.3.2}/tests/test_mutual_connections.py +0 -0
- {warmpath-0.3.0 → warmpath-0.3.2}/warmpath/__init__.py +0 -0
- {warmpath-0.3.0 → warmpath-0.3.2}/warmpath.jpg +0 -0
warmpath-0.3.2/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
# Warmpath
|
|
4
|
+
|
|
5
|
+
Find LinkedIn mutuals and warm paths using your logged-in browser session.
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
Log in to LinkedIn in a regular browser window, then import that session. Replace `chrome` with your browser's name if needed.
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
# Import and check your browser session
|
|
13
|
+
uvx warmpath auth import --browser chrome
|
|
14
|
+
uvx warmpath auth status
|
|
15
|
+
|
|
16
|
+
# Find people who can introduce you to a company
|
|
17
|
+
uvx warmpath company HashiCorp
|
|
18
|
+
uvx warmpath company https://www.linkedin.com/company/hashicorp/ --max-degree 2 --limit 5
|
|
19
|
+
|
|
20
|
+
# Find reachable people with a skill
|
|
21
|
+
uvx warmpath skill Flutter
|
|
22
|
+
uvx warmpath skill Leadership --max-depth 2
|
|
23
|
+
|
|
24
|
+
# Find mutual connections with a person
|
|
25
|
+
uvx warmpath human https://www.linkedin.com/in/mitchellh/
|
|
26
|
+
|
|
27
|
+
# Refresh cached results
|
|
28
|
+
uvx warmpath human https://www.linkedin.com/in/mitchellh/ --refresh-cache
|
|
29
|
+
|
|
30
|
+
# Show options for a command; also works with auth, skill, and human
|
|
31
|
+
uvx warmpath company --help
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Browser sessions
|
|
35
|
+
|
|
36
|
+
Supported browsers are Chrome, Chromium, Firefox, Edge, Brave, Safari, Arc, Vivaldi, Opera, Opera GX (`opera-gx`), and LibreWolf; availability depends on your operating system. Your OS may ask for keychain/keyring access. If the cookie store is locked, close the browser and retry. Private-window sessions cannot be imported.
|
|
37
|
+
|
|
38
|
+
Warmpath imports only LinkedIn's `li_at` and `JSESSIONID` cookies and manages the saved session automatically. `company`, `skill`, and `human` use it on subsequent runs. Failed imports preserve the previous session.
|
|
39
|
+
|
|
40
|
+
`auth status` fetches and prints the logged-in user's name from LinkedIn, along with the source browser, import time, cookie expiry, and storage location, without displaying cookie values. It requires a connection to LinkedIn and exits with an error if the user lookup fails. If the session expires or LinkedIn stops accepting it, log in again and repeat `auth import`.
|
|
41
|
+
|
|
42
|
+
## Development
|
|
43
|
+
|
|
44
|
+
Browser cookie import uses [browser-cookie3](https://github.com/borisbabic/browser_cookie3).
|
|
45
|
+
|
|
46
|
+
[go-task](https://taskfile.dev/) runs the repository checks. Install it with Homebrew on macOS, or follow the [installation guide](https://taskfile.dev/docs/installation) for other platforms. Then verify the tool, install the Python development dependencies, and run the checks:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
# macOS
|
|
50
|
+
brew install go-task
|
|
51
|
+
|
|
52
|
+
task --version
|
|
53
|
+
uv sync
|
|
54
|
+
task check
|
|
55
|
+
```
|
|
@@ -6,6 +6,7 @@ from unittest.mock import Mock
|
|
|
6
6
|
import pytest
|
|
7
7
|
from requests import Request
|
|
8
8
|
from requests.cookies import RequestsCookieJar
|
|
9
|
+
from requests.exceptions import ConnectionError, HTTPError, Timeout
|
|
9
10
|
|
|
10
11
|
from warmpath import auth, cli
|
|
11
12
|
|
|
@@ -17,6 +18,17 @@ def auth_path(tmp_path, monkeypatch):
|
|
|
17
18
|
return path
|
|
18
19
|
|
|
19
20
|
|
|
21
|
+
@pytest.fixture(autouse=True)
|
|
22
|
+
def profile_fetch(monkeypatch):
|
|
23
|
+
response = Mock()
|
|
24
|
+
response.json.return_value = {
|
|
25
|
+
"miniProfile": {"firstName": "Ada", "lastName": "Lovelace"},
|
|
26
|
+
}
|
|
27
|
+
fetch = Mock(return_value=response)
|
|
28
|
+
monkeypatch.setattr(cli.Linkedin, "_fetch", fetch)
|
|
29
|
+
return fetch
|
|
30
|
+
|
|
31
|
+
|
|
20
32
|
@pytest.fixture
|
|
21
33
|
def browser_cookies():
|
|
22
34
|
jar = RequestsCookieJar()
|
|
@@ -50,6 +62,43 @@ def test_import_reads_only_the_selected_browser(browser, browser_cookies, monkey
|
|
|
50
62
|
assert auth.load_auth().browser == browser
|
|
51
63
|
|
|
52
64
|
|
|
65
|
+
@pytest.mark.parametrize("secure", [0, 1])
|
|
66
|
+
def test_browser_cookie_integer_secure_flag_round_trips(secure, reader, browser_cookies, auth_path):
|
|
67
|
+
# browser-cookie3 passes Chrome's SQLite integer flags through to Cookie.
|
|
68
|
+
for cookie in list(browser_cookies):
|
|
69
|
+
browser_cookies.set_cookie(auth.browser_cookie3.create_cookie(
|
|
70
|
+
cookie.domain, cookie.path, secure, cookie.expires,
|
|
71
|
+
cookie.name, cookie.value, True,
|
|
72
|
+
))
|
|
73
|
+
|
|
74
|
+
cli.main(["auth", "import", "--browser", "chrome"])
|
|
75
|
+
cli.main(["auth", "status"])
|
|
76
|
+
|
|
77
|
+
payload = json.loads(auth_path.read_text())
|
|
78
|
+
assert all(record["secure"] is bool(secure) for record in payload["cookies"])
|
|
79
|
+
assert auth.load_cookies().get_dict() == browser_cookies.get_dict()
|
|
80
|
+
assert all(cookie.secure is bool(secure) for cookie in auth.load_cookies())
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@pytest.mark.parametrize("secure", [0, 1])
|
|
84
|
+
def test_existing_integer_secure_flags_load_without_reimport(secure, reader, auth_path):
|
|
85
|
+
auth.import_browser("chrome")
|
|
86
|
+
payload = json.loads(auth_path.read_text())
|
|
87
|
+
for record in payload["cookies"]:
|
|
88
|
+
record["secure"] = secure
|
|
89
|
+
auth_path.write_text(json.dumps(payload))
|
|
90
|
+
before = auth_path.read_bytes()
|
|
91
|
+
reader.reset_mock()
|
|
92
|
+
|
|
93
|
+
cli.main(["auth", "status"])
|
|
94
|
+
cookies = auth.load_cookies()
|
|
95
|
+
|
|
96
|
+
assert not auth.load_auth().missing_cookies()
|
|
97
|
+
assert all(cookie.secure is bool(secure) for cookie in cookies)
|
|
98
|
+
assert auth_path.read_bytes() == before
|
|
99
|
+
reader.assert_not_called()
|
|
100
|
+
|
|
101
|
+
|
|
53
102
|
def test_import_filters_unrelated_expired_and_out_of_scope_cookies(reader, browser_cookies, auth_path):
|
|
54
103
|
for name, domain, path, expires, value in [
|
|
55
104
|
("tracking", ".linkedin.com", "/", None, "unrelated-tracking-token"),
|
|
@@ -99,8 +148,9 @@ def test_imported_session_reaches_linkedin_with_csrf_and_cookie_attributes(reade
|
|
|
99
148
|
assert "Cookie" not in session.prepare_request(Request("GET", "http://www.linkedin.com/")).headers
|
|
100
149
|
|
|
101
150
|
|
|
102
|
-
def test_import_and_status_report_metadata_without_cookie_values(reader, capsys, auth_path):
|
|
151
|
+
def test_import_and_status_report_metadata_without_cookie_values(reader, capsys, auth_path, profile_fetch):
|
|
103
152
|
cli.main(["auth", "import", "--browser", "Chrome"])
|
|
153
|
+
profile_fetch.assert_not_called()
|
|
104
154
|
before = auth_path.read_bytes()
|
|
105
155
|
reader.reset_mock()
|
|
106
156
|
cli.main(["auth", "status"])
|
|
@@ -108,6 +158,7 @@ def test_import_and_status_report_metadata_without_cookie_values(reader, capsys,
|
|
|
108
158
|
output = capsys.readouterr()
|
|
109
159
|
assert output.err == ""
|
|
110
160
|
assert output.out.count("Status: ready (local expiry check)") == 2
|
|
161
|
+
assert output.out.count("User: Ada Lovelace") == 1
|
|
111
162
|
assert "Browser: chrome" in output.out
|
|
112
163
|
assert "Imported:" in output.out
|
|
113
164
|
assert str(auth_path) in output.out
|
|
@@ -117,6 +168,84 @@ def test_import_and_status_report_metadata_without_cookie_values(reader, capsys,
|
|
|
117
168
|
assert "private-csrf-token" not in output.out
|
|
118
169
|
assert auth_path.read_bytes() == before
|
|
119
170
|
reader.assert_not_called()
|
|
171
|
+
profile_fetch.assert_called_once_with("/me", timeout=15, evade=cli.no_delay)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@pytest.mark.parametrize("profile,expected", [
|
|
175
|
+
({"firstName": " Ada ", "lastName": " Lovelace "}, "Ada Lovelace"),
|
|
176
|
+
({"firstName": "Адель", "lastName": "Низамутдинов"}, "Адель Низамутдинов"),
|
|
177
|
+
({"firstName": "Ada"}, "Ada"),
|
|
178
|
+
({"lastName": "Lovelace"}, "Lovelace"),
|
|
179
|
+
])
|
|
180
|
+
def test_status_prints_the_logged_in_users_name(profile, expected, reader, profile_fetch, capsys):
|
|
181
|
+
auth.import_browser("chrome")
|
|
182
|
+
profile_fetch.return_value.json.return_value = {"miniProfile": profile}
|
|
183
|
+
|
|
184
|
+
cli.main(["auth", "status"])
|
|
185
|
+
|
|
186
|
+
assert f"User: {expected}\n" in capsys.readouterr().out
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@pytest.mark.parametrize("payload", [
|
|
190
|
+
{}, None, [], {"miniProfile": None}, {"miniProfile": "private-linkedin-token"},
|
|
191
|
+
{"miniProfile": {"firstName": " ", "lastName": None}},
|
|
192
|
+
{"miniProfile": {"firstName": ["private-linkedin-token"], "lastName": 123}},
|
|
193
|
+
])
|
|
194
|
+
def test_status_rejects_a_profile_without_a_name(payload, reader, profile_fetch, capsys):
|
|
195
|
+
auth.import_browser("chrome")
|
|
196
|
+
profile_fetch.return_value.json.return_value = payload
|
|
197
|
+
|
|
198
|
+
with pytest.raises(SystemExit) as error:
|
|
199
|
+
cli.main(["auth", "status"])
|
|
200
|
+
|
|
201
|
+
assert error.value.code == 2
|
|
202
|
+
output = capsys.readouterr()
|
|
203
|
+
assert "LinkedIn did not return a name" in output.err
|
|
204
|
+
assert "User:" not in output.out
|
|
205
|
+
assert "private-linkedin-token" not in output.out + output.err
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@pytest.mark.parametrize("failure", ["connection", "timeout", "http", "json"])
|
|
209
|
+
def test_status_handles_failed_user_lookup_without_exposing_secrets(failure, reader, profile_fetch, capsys, auth_path):
|
|
210
|
+
auth.import_browser("chrome")
|
|
211
|
+
before = auth_path.read_bytes()
|
|
212
|
+
reader.reset_mock()
|
|
213
|
+
if failure == "connection":
|
|
214
|
+
profile_fetch.side_effect = ConnectionError("private-linkedin-token")
|
|
215
|
+
elif failure == "timeout":
|
|
216
|
+
profile_fetch.side_effect = Timeout("private-linkedin-token")
|
|
217
|
+
elif failure == "http":
|
|
218
|
+
profile_fetch.return_value.raise_for_status.side_effect = HTTPError("private-linkedin-token")
|
|
219
|
+
else:
|
|
220
|
+
profile_fetch.return_value.json.side_effect = ValueError("private-linkedin-token")
|
|
221
|
+
|
|
222
|
+
with pytest.raises(SystemExit) as error:
|
|
223
|
+
cli.main(["auth", "status"])
|
|
224
|
+
|
|
225
|
+
assert error.value.code == 2
|
|
226
|
+
output = capsys.readouterr()
|
|
227
|
+
assert "Could not fetch the logged-in LinkedIn user" in output.err
|
|
228
|
+
assert "warmpath auth import --browser" in output.err
|
|
229
|
+
assert "User:" not in output.out
|
|
230
|
+
assert "private-linkedin-token" not in output.out + output.err
|
|
231
|
+
assert auth_path.read_bytes() == before
|
|
232
|
+
reader.assert_not_called()
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
@pytest.mark.parametrize("status_code", [401, 403])
|
|
236
|
+
def test_status_reports_rejected_sessions(status_code, reader, profile_fetch, capsys):
|
|
237
|
+
auth.import_browser("chrome")
|
|
238
|
+
profile_fetch.return_value.status_code = status_code
|
|
239
|
+
|
|
240
|
+
with pytest.raises(SystemExit) as error:
|
|
241
|
+
cli.main(["auth", "status"])
|
|
242
|
+
|
|
243
|
+
assert error.value.code == 2
|
|
244
|
+
output = capsys.readouterr()
|
|
245
|
+
assert "LinkedIn rejected the saved session" in output.err
|
|
246
|
+
assert "warmpath auth import --browser" in output.err
|
|
247
|
+
assert "User:" not in output.out
|
|
248
|
+
profile_fetch.return_value.json.assert_not_called()
|
|
120
249
|
|
|
121
250
|
|
|
122
251
|
@pytest.mark.skipif(os.name != "posix", reason="Unix file permissions")
|
|
@@ -215,6 +344,7 @@ def test_corrupt_store_has_reimport_guidance(content, auth_path, capsys):
|
|
|
215
344
|
|
|
216
345
|
@pytest.mark.parametrize("field,value", [
|
|
217
346
|
("domain", "example.org"), ("path", "/jobs"), ("secure", "true"),
|
|
347
|
+
("secure", 2), ("secure", -1), ("secure", 0.0), ("secure", 1.0), ("secure", None),
|
|
218
348
|
("expires", "tomorrow"), ("expires", 10**20),
|
|
219
349
|
("domain_specified", False), ("value", "token\r\nInjected: header"),
|
|
220
350
|
])
|
|
@@ -228,7 +358,7 @@ def test_invalid_stored_cookie_is_rejected(field, value, reader, auth_path):
|
|
|
228
358
|
auth.load_cookies()
|
|
229
359
|
|
|
230
360
|
|
|
231
|
-
def test_expired_session_is_reported_and_rejected_without_reimport(reader, capsys):
|
|
361
|
+
def test_expired_session_is_reported_and_rejected_without_reimport(reader, capsys, profile_fetch):
|
|
232
362
|
session = auth.import_browser("chrome")
|
|
233
363
|
next(cookie for cookie in session.cookies if cookie.name == "li_at").expires = 1
|
|
234
364
|
auth.save_auth(session)
|
|
@@ -249,6 +379,7 @@ def test_expired_session_is_reported_and_rejected_without_reimport(reader, capsy
|
|
|
249
379
|
assert api_error.value.code == 2
|
|
250
380
|
assert "warmpath auth import --browser chrome" in capsys.readouterr().err
|
|
251
381
|
reader.assert_not_called()
|
|
382
|
+
profile_fetch.assert_not_called()
|
|
252
383
|
|
|
253
384
|
|
|
254
385
|
@pytest.mark.parametrize("arguments", [[], ["import"], ["import", "--browser", "unknown"], ["login"]])
|
|
@@ -93,7 +93,7 @@ def save_auth(session: AuthSession) -> None:
|
|
|
93
93
|
"domain": cookie.domain,
|
|
94
94
|
"domain_specified": cookie.domain_specified,
|
|
95
95
|
"path": cookie.path,
|
|
96
|
-
"secure": cookie.secure,
|
|
96
|
+
"secure": bool(cookie.secure),
|
|
97
97
|
"expires": cookie.expires,
|
|
98
98
|
}
|
|
99
99
|
for cookie in session.cookies
|
|
@@ -173,7 +173,9 @@ def load_auth() -> AuthSession:
|
|
|
173
173
|
not isinstance(record.get(key), str)
|
|
174
174
|
for key in ("name", "value", "domain", "path")
|
|
175
175
|
)
|
|
176
|
-
|
|
176
|
+
# Older imports saved browser-cookie3's integer 0/1 Secure flag.
|
|
177
|
+
or type(record.get("secure")) not in (bool, int)
|
|
178
|
+
or record["secure"] not in (0, 1)
|
|
177
179
|
or type(record.get("domain_specified")) is not bool
|
|
178
180
|
or (
|
|
179
181
|
record.get("expires") is not None
|
|
@@ -186,7 +188,7 @@ def load_auth() -> AuthSession:
|
|
|
186
188
|
value=record["value"],
|
|
187
189
|
domain=record["domain"],
|
|
188
190
|
path=record["path"],
|
|
189
|
-
secure=record["secure"],
|
|
191
|
+
secure=bool(record["secure"]),
|
|
190
192
|
expires=record.get("expires"),
|
|
191
193
|
)
|
|
192
194
|
cookie.domain_specified = record["domain_specified"]
|
|
@@ -9,6 +9,7 @@ from typing import Any, Callable, NoReturn
|
|
|
9
9
|
from urllib.parse import unquote, urlparse
|
|
10
10
|
|
|
11
11
|
from open_linkedin_api import Linkedin
|
|
12
|
+
from requests.exceptions import RequestException
|
|
12
13
|
|
|
13
14
|
from warmpath import auth
|
|
14
15
|
|
|
@@ -109,6 +110,35 @@ def build_api() -> Any:
|
|
|
109
110
|
return api
|
|
110
111
|
|
|
111
112
|
|
|
113
|
+
def logged_in_user_name(api: Any) -> str:
|
|
114
|
+
try:
|
|
115
|
+
response = api._fetch("/me", timeout=15)
|
|
116
|
+
if response.status_code in (401, 403):
|
|
117
|
+
raise auth.AuthError(
|
|
118
|
+
"LinkedIn rejected the saved session. Sign in to LinkedIn, "
|
|
119
|
+
f"then reimport your session. {auth.IMPORT_HINT}"
|
|
120
|
+
)
|
|
121
|
+
response.raise_for_status()
|
|
122
|
+
data = response.json()
|
|
123
|
+
except (RequestException, ValueError) as exc:
|
|
124
|
+
raise auth.AuthError(
|
|
125
|
+
"Could not fetch the logged-in LinkedIn user. Check your connection "
|
|
126
|
+
f"and try again, or reimport your session. {auth.IMPORT_HINT}"
|
|
127
|
+
) from exc
|
|
128
|
+
|
|
129
|
+
profile = data.get("miniProfile") if isinstance(data, dict) else None
|
|
130
|
+
name = ""
|
|
131
|
+
if isinstance(profile, dict):
|
|
132
|
+
name = " ".join(
|
|
133
|
+
value.strip()
|
|
134
|
+
for key in ("firstName", "lastName")
|
|
135
|
+
if isinstance(value := profile.get(key), str) and value.strip()
|
|
136
|
+
)
|
|
137
|
+
if not name:
|
|
138
|
+
raise auth.AuthError("LinkedIn did not return a name for the logged-in user.")
|
|
139
|
+
return name
|
|
140
|
+
|
|
141
|
+
|
|
112
142
|
def profile_urn_id(api: Any, public_id: str) -> str:
|
|
113
143
|
urn_id = profile_urn_id_from_html(api, public_id)
|
|
114
144
|
if urn_id:
|
|
@@ -1743,7 +1773,7 @@ def parse_auth_args(argv: list[str]) -> argparse.Namespace:
|
|
|
1743
1773
|
choices=auth.BROWSERS,
|
|
1744
1774
|
help="Browser where you are logged in to LinkedIn.",
|
|
1745
1775
|
)
|
|
1746
|
-
commands.add_parser("status", help="
|
|
1776
|
+
commands.add_parser("status", help="Show the logged-in user and saved session details.")
|
|
1747
1777
|
return parser.parse_args(argv)
|
|
1748
1778
|
|
|
1749
1779
|
|
|
@@ -1757,6 +1787,8 @@ def run_auth_command(args: argparse.Namespace) -> None:
|
|
|
1757
1787
|
print(auth.render_status(session))
|
|
1758
1788
|
if session.missing_cookies():
|
|
1759
1789
|
raise SystemExit(1)
|
|
1790
|
+
if args.auth_command == "status":
|
|
1791
|
+
print(f"User: {logged_in_user_name(build_api())}")
|
|
1760
1792
|
except auth.AuthError as exc:
|
|
1761
1793
|
fail(str(exc), 2)
|
|
1762
1794
|
|
|
@@ -1771,7 +1803,7 @@ def parse_main_args(argv: list[str]) -> argparse.Namespace:
|
|
|
1771
1803
|
Import your LinkedIn session from a browser.
|
|
1772
1804
|
|
|
1773
1805
|
auth status
|
|
1774
|
-
|
|
1806
|
+
Show the logged-in user and saved session details.
|
|
1775
1807
|
|
|
1776
1808
|
human PROFILE_URL
|
|
1777
1809
|
Print mutual LinkedIn connections for a profile URL.
|
warmpath-0.3.0/README.md
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-

|
|
2
|
-
|
|
3
|
-
# Warmpath
|
|
4
|
-
|
|
5
|
-
Find LinkedIn mutuals and warm paths using your logged-in browser session.
|
|
6
|
-
|
|
7
|
-
## Setup
|
|
8
|
-
|
|
9
|
-
Log in to LinkedIn in a regular browser window, then import that session:
|
|
10
|
-
|
|
11
|
-
```sh
|
|
12
|
-
uvx warmpath auth import --browser chrome
|
|
13
|
-
uvx warmpath auth status
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
Import uses [browser-cookie3](https://github.com/borisbabic/browser_cookie3) to read your browser's local cookie store. Supported browsers are Chrome, Chromium, Firefox, Edge, Brave, Safari, Arc, Vivaldi, Opera, Opera GX (`opera-gx`), and LibreWolf; availability depends on your operating system. Your OS may ask for keychain/keyring access. If the cookie store is locked, close the browser and retry. Private-window sessions cannot be imported.
|
|
17
|
-
|
|
18
|
-
Warmpath imports only LinkedIn's `li_at` and `JSESSIONID` cookies and manages the saved session automatically. `company`, `skill`, and `human` use it on subsequent runs. Failed imports preserve the previous session.
|
|
19
|
-
|
|
20
|
-
`auth status` shows the source browser, import time, cookie expiry, and storage location without displaying cookie values. It checks the saved session locally; LinkedIn may revoke a session before its cookies expire. If the session expires or LinkedIn stops accepting it, log in again and repeat `auth import`.
|
|
21
|
-
|
|
22
|
-
## Development
|
|
23
|
-
|
|
24
|
-
[go-task](https://taskfile.dev/) is required to run the repository checks. Install it with Homebrew on macOS:
|
|
25
|
-
|
|
26
|
-
```sh
|
|
27
|
-
brew install go-task
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
For other platforms, follow the [go-task installation guide](https://taskfile.dev/docs/installation). Verify that the `task` command is available before running any checks:
|
|
31
|
-
|
|
32
|
-
```sh
|
|
33
|
-
task --version
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
Then install the Python development dependencies and run the full check suite:
|
|
37
|
-
|
|
38
|
-
```sh
|
|
39
|
-
uv sync
|
|
40
|
-
task check
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
## Usage
|
|
44
|
-
|
|
45
|
-
### Company
|
|
46
|
-
|
|
47
|
-
Who can introduce me into this company?
|
|
48
|
-
|
|
49
|
-
```sh
|
|
50
|
-
uvx warmpath company HashiCorp
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
### Skill
|
|
54
|
-
|
|
55
|
-
Which reachable people match this recruiting need?
|
|
56
|
-
|
|
57
|
-
```sh
|
|
58
|
-
uvx warmpath skill Flutter
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
### Human
|
|
62
|
-
|
|
63
|
-
Can I reach this exact person, and through whom?
|
|
64
|
-
|
|
65
|
-
```sh
|
|
66
|
-
uvx warmpath human https://www.linkedin.com/in/mitchellh/
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
## More Examples
|
|
70
|
-
|
|
71
|
-
```sh
|
|
72
|
-
uvx warmpath company "HashiCorp" --max-degree 2 --limit 5
|
|
73
|
-
uvx warmpath auth import --browser firefox
|
|
74
|
-
uvx warmpath company https://www.linkedin.com/company/hashicorp/
|
|
75
|
-
uvx warmpath skill Leadership --max-depth 2
|
|
76
|
-
uvx warmpath human https://www.linkedin.com/in/mitchellh/ --refresh-cache
|
|
77
|
-
uvx warmpath company --help
|
|
78
|
-
uvx warmpath skill --help
|
|
79
|
-
uvx warmpath human --help
|
|
80
|
-
```
|
|
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
|