warmpath 0.3.1__tar.gz → 0.3.3__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: warmpath
3
- Version: 0.3.1
3
+ Version: 0.3.3
4
4
  Summary: Local LinkedIn visible-connections scraper.
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: browser-cookie3>=0.20.1
@@ -0,0 +1,55 @@
1
+ ![Warmpath banner](warmpath.jpg)
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 import` and `auth status` print only `Logged in as <name>` after confirming your session with LinkedIn. If `auth status` cannot confirm a login, it prints `Not logged in` and exits with status 1. Both commands require a connection to LinkedIn. 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
+ ```
@@ -18,7 +18,7 @@ For the `skill` subcommand, the call path is:
18
18
  4. `find_skill_connections(...)`
19
19
  5. `render_skill_connections_result(result)`
20
20
 
21
- `run_skill_command` builds a LinkedIn API client from the session saved by `warmpath auth import --browser <browser>`, resolves the cache directory, calls the skill search pipeline, and prints the rendered result. Use `warmpath auth status` to check the saved session's expiry locally.
21
+ `run_skill_command` builds a LinkedIn API client from the session saved by `warmpath auth import --browser <browser>`, resolves the cache directory, calls the skill search pipeline, and prints the rendered result. Use `warmpath auth status` to check whether you are logged in and as whom.
22
22
 
23
23
  ## Defaults
24
24
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "warmpath"
3
- version = "0.3.1"
3
+ version = "0.3.3"
4
4
  description = "Local LinkedIn visible-connections scraper."
5
5
  requires-python = ">=3.10"
6
6
  dependencies = [
@@ -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()
@@ -136,26 +148,124 @@ def test_imported_session_reaches_linkedin_with_csrf_and_cookie_attributes(reade
136
148
  assert "Cookie" not in session.prepare_request(Request("GET", "http://www.linkedin.com/")).headers
137
149
 
138
150
 
139
- def test_import_and_status_report_metadata_without_cookie_values(reader, capsys, auth_path):
151
+ def test_import_and_status_print_only_the_logged_in_user(reader, capsys, auth_path, profile_fetch):
140
152
  cli.main(["auth", "import", "--browser", "Chrome"])
153
+ output = capsys.readouterr()
154
+ assert output.out == "Logged in as Ada Lovelace\n"
155
+ assert output.err == ""
156
+ profile_fetch.assert_called_once_with("/me", timeout=15, evade=cli.no_delay)
157
+ profile_fetch.reset_mock()
141
158
  before = auth_path.read_bytes()
142
159
  reader.reset_mock()
143
160
  cli.main(["auth", "status"])
144
161
 
145
162
  output = capsys.readouterr()
146
163
  assert output.err == ""
147
- assert output.out.count("Status: ready (local expiry check)") == 2
148
- assert "Browser: chrome" in output.out
149
- assert "Imported:" in output.out
150
- assert str(auth_path) in output.out
151
- assert "Cookie: JSESSIONID; expires: session" in output.out
152
- assert "Cookie: li_at; expires: 2100-01-01T00:00:00+00:00" in output.out
153
- assert "private-linkedin-token" not in output.out
154
- assert "private-csrf-token" not in output.out
164
+ assert output.out == "Logged in as Ada Lovelace\n"
165
+ assert auth_path.read_bytes() == before
166
+ reader.assert_not_called()
167
+ profile_fetch.assert_called_once_with("/me", timeout=15, evade=cli.no_delay)
168
+
169
+
170
+ @pytest.mark.parametrize("command", [["status"], ["import", "--browser", "chrome"]])
171
+ @pytest.mark.parametrize("profile,expected", [
172
+ ({"firstName": " Ada ", "lastName": " Lovelace "}, "Ada Lovelace"),
173
+ ({"firstName": "Адель", "lastName": "Низамутдинов"}, "Адель Низамутдинов"),
174
+ ({"firstName": "Ada"}, "Ada"),
175
+ ({"lastName": "Lovelace"}, "Lovelace"),
176
+ ])
177
+ def test_auth_prints_the_logged_in_users_name(command, profile, expected, reader, profile_fetch, capsys):
178
+ auth.import_browser("chrome")
179
+ profile_fetch.return_value.json.return_value = {"miniProfile": profile}
180
+
181
+ cli.main(["auth", *command])
182
+
183
+ output = capsys.readouterr()
184
+ assert output.out == f"Logged in as {expected}\n"
185
+ assert output.err == ""
186
+
187
+
188
+ @pytest.mark.parametrize("payload", [
189
+ {}, None, [], {"miniProfile": None}, {"miniProfile": "private-linkedin-token"},
190
+ {"miniProfile": {"firstName": " ", "lastName": None}},
191
+ {"miniProfile": {"firstName": ["private-linkedin-token"], "lastName": 123}},
192
+ ])
193
+ def test_status_rejects_a_profile_without_a_name(payload, reader, profile_fetch, capsys):
194
+ auth.import_browser("chrome")
195
+ profile_fetch.return_value.json.return_value = payload
196
+
197
+ with pytest.raises(SystemExit) as error:
198
+ cli.main(["auth", "status"])
199
+
200
+ assert error.value.code == 1
201
+ output = capsys.readouterr()
202
+ assert output.out == "Not logged in\n"
203
+ assert output.err == ""
204
+
205
+
206
+ @pytest.mark.parametrize("failure", ["connection", "timeout", "http", "json"])
207
+ def test_status_handles_failed_user_lookup_without_exposing_secrets(failure, reader, profile_fetch, capsys, auth_path):
208
+ auth.import_browser("chrome")
209
+ before = auth_path.read_bytes()
210
+ reader.reset_mock()
211
+ if failure == "connection":
212
+ profile_fetch.side_effect = ConnectionError("private-linkedin-token")
213
+ elif failure == "timeout":
214
+ profile_fetch.side_effect = Timeout("private-linkedin-token")
215
+ elif failure == "http":
216
+ profile_fetch.return_value.raise_for_status.side_effect = HTTPError("private-linkedin-token")
217
+ else:
218
+ profile_fetch.return_value.json.side_effect = ValueError("private-linkedin-token")
219
+
220
+ with pytest.raises(SystemExit) as error:
221
+ cli.main(["auth", "status"])
222
+
223
+ assert error.value.code == 1
224
+ output = capsys.readouterr()
225
+ assert output.out == "Not logged in\n"
226
+ assert output.err == ""
155
227
  assert auth_path.read_bytes() == before
156
228
  reader.assert_not_called()
157
229
 
158
230
 
231
+ @pytest.mark.parametrize("status_code", [401, 403])
232
+ def test_status_reports_rejected_sessions(status_code, reader, profile_fetch, capsys):
233
+ auth.import_browser("chrome")
234
+ profile_fetch.return_value.status_code = status_code
235
+
236
+ with pytest.raises(SystemExit) as error:
237
+ cli.main(["auth", "status"])
238
+
239
+ assert error.value.code == 1
240
+ output = capsys.readouterr()
241
+ assert output.out == "Not logged in\n"
242
+ assert output.err == ""
243
+ profile_fetch.return_value.json.assert_not_called()
244
+
245
+
246
+ @pytest.mark.parametrize("failure", ["rejected", "connection", "missing_name"])
247
+ def test_failed_import_lookup_preserves_previous_session(failure, reader, browser_cookies, auth_path, profile_fetch, capsys):
248
+ auth.import_browser("chrome")
249
+ before = auth_path.read_bytes()
250
+ next(cookie for cookie in browser_cookies if cookie.name == "li_at").value = "new-private-token"
251
+ if failure == "rejected":
252
+ profile_fetch.return_value.status_code = 401
253
+ elif failure == "connection":
254
+ profile_fetch.side_effect = ConnectionError("new-private-token")
255
+ else:
256
+ profile_fetch.return_value.json.return_value = {}
257
+
258
+ with pytest.raises(SystemExit) as error:
259
+ cli.main(["auth", "import", "--browser", "chrome"])
260
+
261
+ assert error.value.code == 2
262
+ output = capsys.readouterr()
263
+ assert output.out == ""
264
+ assert output.err
265
+ assert "new-private-token" not in output.err
266
+ assert auth_path.read_bytes() == before
267
+
268
+
159
269
  @pytest.mark.skipif(os.name != "posix", reason="Unix file permissions")
160
270
  def test_auth_store_is_private_even_when_replacing_an_existing_file(reader, auth_path):
161
271
  auth_path.parent.mkdir()
@@ -217,7 +327,7 @@ def test_failed_save_preserves_previous_session_and_cleans_up(reader, auth_path,
217
327
 
218
328
 
219
329
  @pytest.mark.parametrize("command", [
220
- ["auth", "status"], ["company", "Acme"], ["skill", "Python"],
330
+ ["company", "Acme"], ["skill", "Python"],
221
331
  ["human", "https://www.linkedin.com/in/example/"],
222
332
  ])
223
333
  def test_missing_auth_has_import_guidance_without_browser_or_api_access(command, reader, monkeypatch, capsys, auth_path):
@@ -234,19 +344,34 @@ def test_missing_auth_has_import_guidance_without_browser_or_api_access(command,
234
344
  linkedin.assert_not_called()
235
345
 
236
346
 
347
+ def test_missing_auth_status_prints_not_logged_in(reader, monkeypatch, capsys, auth_path):
348
+ linkedin = Mock()
349
+ monkeypatch.setattr(cli, "Linkedin", linkedin)
350
+
351
+ with pytest.raises(SystemExit) as error:
352
+ cli.main(["auth", "status"])
353
+
354
+ assert error.value.code == 1
355
+ output = capsys.readouterr()
356
+ assert output.out == "Not logged in\n"
357
+ assert output.err == ""
358
+ assert not auth_path.exists()
359
+ reader.assert_not_called()
360
+ linkedin.assert_not_called()
361
+
362
+
237
363
  @pytest.mark.parametrize("content", [b"", b"[]", b"{}", b"private-token", b"\xff"])
238
- def test_corrupt_store_has_reimport_guidance(content, auth_path, capsys):
364
+ def test_corrupt_store_status_prints_not_logged_in(content, auth_path, capsys):
239
365
  auth_path.parent.mkdir()
240
366
  auth_path.write_bytes(content)
241
367
 
242
368
  with pytest.raises(SystemExit) as error:
243
369
  cli.main(["auth", "status"])
244
370
 
245
- assert error.value.code == 2
371
+ assert error.value.code == 1
246
372
  output = capsys.readouterr()
247
- assert "saved LinkedIn session is invalid" in output.err
248
- assert "warmpath auth import --browser" in output.err
249
- assert "private-token" not in output.out + output.err
373
+ assert output.out == "Not logged in\n"
374
+ assert output.err == ""
250
375
  assert auth_path.read_bytes() == content
251
376
 
252
377
 
@@ -266,7 +391,7 @@ def test_invalid_stored_cookie_is_rejected(field, value, reader, auth_path):
266
391
  auth.load_cookies()
267
392
 
268
393
 
269
- def test_expired_session_is_reported_and_rejected_without_reimport(reader, capsys):
394
+ def test_expired_session_is_reported_and_rejected_without_reimport(reader, capsys, profile_fetch):
270
395
  session = auth.import_browser("chrome")
271
396
  next(cookie for cookie in session.cookies if cookie.name == "li_at").expires = 1
272
397
  auth.save_auth(session)
@@ -277,9 +402,8 @@ def test_expired_session_is_reported_and_rejected_without_reimport(reader, capsy
277
402
 
278
403
  assert status_error.value.code == 1
279
404
  output = capsys.readouterr()
280
- assert "Status: expired or incomplete" in output.out
281
- assert "Missing or expired: li_at" in output.out
282
- assert "warmpath auth import --browser chrome" in output.out
405
+ assert output.out == "Not logged in\n"
406
+ assert output.err == ""
283
407
 
284
408
  with pytest.raises(SystemExit) as api_error:
285
409
  cli.build_api()
@@ -287,6 +411,7 @@ def test_expired_session_is_reported_and_rejected_without_reimport(reader, capsy
287
411
  assert api_error.value.code == 2
288
412
  assert "warmpath auth import --browser chrome" in capsys.readouterr().err
289
413
  reader.assert_not_called()
414
+ profile_fetch.assert_not_called()
290
415
 
291
416
 
292
417
  @pytest.mark.parametrize("arguments", [[], ["import"], ["import", "--browser", "unknown"], ["login"]])
@@ -616,7 +616,7 @@ wheels = [
616
616
 
617
617
  [[package]]
618
618
  name = "warmpath"
619
- version = "0.3.1"
619
+ version = "0.3.3"
620
620
  source = { editable = "." }
621
621
  dependencies = [
622
622
  { name = "browser-cookie3" },
@@ -125,7 +125,7 @@ def save_auth(session: AuthSession) -> None:
125
125
  temporary.unlink(missing_ok=True)
126
126
 
127
127
 
128
- def import_browser(browser: str) -> AuthSession:
128
+ def import_browser(browser: str, *, save: bool = True) -> AuthSession:
129
129
  if browser not in BROWSERS:
130
130
  raise AuthError(f"Unsupported browser. Choose from: {', '.join(BROWSERS)}.")
131
131
  try:
@@ -147,7 +147,8 @@ def import_browser(browser: str) -> AuthSession:
147
147
  f"{', '.join(missing)}. Sign in to LinkedIn in a regular browser window, "
148
148
  f"then run warmpath auth import --browser {browser} again."
149
149
  )
150
- save_auth(session)
150
+ if save:
151
+ save_auth(session)
151
152
  return session
152
153
 
153
154
 
@@ -213,22 +214,3 @@ def load_cookies() -> RequestsCookieJar:
213
214
  f"Sign in to LinkedIn, then run warmpath auth import --browser {session.browser}."
214
215
  )
215
216
  return select_cookies(session.cookies)
216
-
217
-
218
- def render_status(session: AuthSession) -> str:
219
- missing = session.missing_cookies()
220
- lines = [
221
- f"Status: {'expired or incomplete' if missing else 'ready'} (local expiry check)",
222
- f"Browser: {session.browser}",
223
- f"Imported: {session.imported_at.isoformat()}",
224
- f"Store: {auth_store_path()}",
225
- ]
226
- for cookie in sorted(session.cookies, key=lambda cookie: cookie.name):
227
- expiry = "session"
228
- if cookie.expires is not None:
229
- expiry = datetime.fromtimestamp(cookie.expires, timezone.utc).isoformat()
230
- lines.append(f"Cookie: {cookie.name}; expires: {expiry}")
231
- if missing:
232
- lines.append(f"Missing or expired: {', '.join(missing)}")
233
- lines.append(f"Run warmpath auth import --browser {session.browser} again.")
234
- return "\n".join(lines)
@@ -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
 
@@ -99,9 +100,9 @@ def use_fast_fetches(api: Any) -> None:
99
100
  api._post = fast_post
100
101
 
101
102
 
102
- def build_api() -> Any:
103
+ def build_api(session: auth.AuthSession | None = None) -> Any:
103
104
  try:
104
- cookies = auth.load_cookies()
105
+ cookies = auth.select_cookies(session.cookies) if session is not None else auth.load_cookies()
105
106
  except auth.AuthError as exc:
106
107
  fail(str(exc), 2)
107
108
  api = Linkedin("", "", cookies=cookies)
@@ -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,21 +1773,28 @@ 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="Check the saved session and cookie expiry locally.")
1776
+ commands.add_parser("status", help="Show whether you are logged in and as whom.")
1747
1777
  return parser.parse_args(argv)
1748
1778
 
1749
1779
 
1750
1780
  def run_auth_command(args: argparse.Namespace) -> None:
1751
1781
  try:
1752
1782
  session = (
1753
- auth.import_browser(args.browser)
1783
+ auth.import_browser(args.browser, save=False)
1754
1784
  if args.auth_command == "import"
1755
1785
  else auth.load_auth()
1756
1786
  )
1757
- print(auth.render_status(session))
1758
1787
  if session.missing_cookies():
1788
+ print("Not logged in")
1759
1789
  raise SystemExit(1)
1790
+ name = logged_in_user_name(build_api(session))
1791
+ if args.auth_command == "import":
1792
+ auth.save_auth(session)
1793
+ print(f"Logged in as {name}")
1760
1794
  except auth.AuthError as exc:
1795
+ if args.auth_command == "status":
1796
+ print("Not logged in")
1797
+ raise SystemExit(1)
1761
1798
  fail(str(exc), 2)
1762
1799
 
1763
1800
 
@@ -1771,7 +1808,7 @@ def parse_main_args(argv: list[str]) -> argparse.Namespace:
1771
1808
  Import your LinkedIn session from a browser.
1772
1809
 
1773
1810
  auth status
1774
- Check the saved session and cookie expiry locally.
1811
+ Show whether you are logged in and as whom.
1775
1812
 
1776
1813
  human PROFILE_URL
1777
1814
  Print mutual LinkedIn connections for a profile URL.
warmpath-0.3.1/README.md DELETED
@@ -1,80 +0,0 @@
1
- ![Warmpath banner](warmpath.jpg)
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