thoughtleaders-cli 0.7.4__py3-none-any.whl → 0.7.5__py3-none-any.whl
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.
- {thoughtleaders_cli-0.7.4.dist-info → thoughtleaders_cli-0.7.5.dist-info}/METADATA +1 -1
- {thoughtleaders_cli-0.7.4.dist-info → thoughtleaders_cli-0.7.5.dist-info}/RECORD +9 -10
- tl_cli/__init__.py +1 -1
- tl_cli/_plugin/.claude-plugin/plugin.json +1 -1
- tl_cli/auth/commands.py +23 -5
- tl_cli/auth/login.py +23 -0
- tl_cli/auth/finalize.py +0 -88
- {thoughtleaders_cli-0.7.4.dist-info → thoughtleaders_cli-0.7.5.dist-info}/WHEEL +0 -0
- {thoughtleaders_cli-0.7.4.dist-info → thoughtleaders_cli-0.7.5.dist-info}/entry_points.txt +0 -0
- {thoughtleaders_cli-0.7.4.dist-info → thoughtleaders_cli-0.7.5.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: thoughtleaders-cli
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.5
|
|
4
4
|
Summary: ThoughtLeaders CLI — query sponsorship data, channels, brands, and intelligence
|
|
5
5
|
Project-URL: Homepage, https://thoughtleaders.io
|
|
6
6
|
Project-URL: Repository, https://github.com/ThoughtLeaders-io/thoughtleaders-cli
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
tl_cli/__init__.py,sha256=
|
|
1
|
+
tl_cli/__init__.py,sha256=42wtAYnJRvJF6uR3-XllRTU49NwpGdrk-2Ap1Lnu_a8,112
|
|
2
2
|
tl_cli/_completions.py,sha256=kOyEUqC26vbYvyXWi513WX8fF73qQLR5WWuRSe_wqyk,164
|
|
3
3
|
tl_cli/_typer_utils.py,sha256=ZiZsCVmEznPvBw-dYbr3tu3zWZ0iN6kjoQmK3gMqD28,860
|
|
4
4
|
tl_cli/config.py,sha256=UV_OYTXuQnAIqbi_oVCXx0hhIdZWR678RRapVv51UwQ,1859
|
|
@@ -7,9 +7,8 @@ tl_cli/hints.py,sha256=cT8kuDtkAZqwXkc2RV0Yg_abofK-g9UiXwTTBunX78U,1557
|
|
|
7
7
|
tl_cli/main.py,sha256=A_8b2SQjBKATxrjO7AGC5Ab1QWlP35gGo4TWzYZtlOM,5806
|
|
8
8
|
tl_cli/self_update.py,sha256=akXOWYgBX2otyaVlx9CDl04gG2s_hYigE2Vkpubt0SA,18302
|
|
9
9
|
tl_cli/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
-
tl_cli/auth/commands.py,sha256=
|
|
11
|
-
tl_cli/auth/
|
|
12
|
-
tl_cli/auth/login.py,sha256=6Jhfw7_eXGxZvUfNP33AZPRmnqmu_scvgt4AcOydsrE,10665
|
|
10
|
+
tl_cli/auth/commands.py,sha256=BiT-87UbZjsOJKtjv2dKcCwbAg4-Dp4oICRM0c97TFg,7409
|
|
11
|
+
tl_cli/auth/login.py,sha256=Rf65nhN7sjUNcaHYlsO7oYCiHqhGo8e8XwoGI2S4mgM,11487
|
|
13
12
|
tl_cli/auth/pkce.py,sha256=4Q6Ip-TeZFNG9c3swXNi4gH7mdMkltKa62gZZNybt8U,658
|
|
14
13
|
tl_cli/auth/token_store.py,sha256=TcZnUol4-8r0jMEJhOPmABCX12_5RkAln2xfWPNdmHk,3275
|
|
15
14
|
tl_cli/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -40,7 +39,7 @@ tl_cli/commands/whoami.py,sha256=aUXwBRwh1vAGrvz8CKGfHYtEOKJCIDfwrGesKAwYZMk,786
|
|
|
40
39
|
tl_cli/output/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
41
40
|
tl_cli/output/formatter.py,sha256=pqlKmb2nZ1Z2e1A9m8l5mgVemJinVAP4in1tUzFWHno,22522
|
|
42
41
|
tl_cli/_plugin/.claude-plugin/marketplace.json,sha256=l56PMmyjfGXNGlV30wRyOAe74B6gJNCVNCxgsBbSNxc,446
|
|
43
|
-
tl_cli/_plugin/.claude-plugin/plugin.json,sha256=
|
|
42
|
+
tl_cli/_plugin/.claude-plugin/plugin.json,sha256=p-kkTL4_8uxs1HMp_shuo7K82_3aT2bcUp-q980utQ0,466
|
|
44
43
|
tl_cli/_plugin/agents/tl-analyst.md,sha256=6J3X3NANkWg6OOUCvNirkN4ulIk80KSumPncDUBt75E,6761
|
|
45
44
|
tl_cli/_plugin/agents/youtube-comment-classifier.md,sha256=S5lr_htA98FIX0su8FJ2ntiHfbdK8OB2NQKC4lTnQcw,2178
|
|
46
45
|
tl_cli/_plugin/hooks/hooks.json,sha256=FSWibw1xAjA-suFV3fR8btIb2kQ82LQ08otTr-NpmFw,835
|
|
@@ -111,8 +110,8 @@ tl_cli/_plugin/skills/tl-top-partnerships/SKILL.md,sha256=hvH05hIaGlc0RfTE0GLBtD
|
|
|
111
110
|
tl_cli/_plugin/skills/tl-top-partnerships/scripts/top_partnerships.py,sha256=_13W6-HuD_jtl7AWQQcZQ0SQO9qODMymlcL-1s4-VwU,13248
|
|
112
111
|
tl_cli/_plugin/skills/tl-views-guarantee/SKILL.md,sha256=IH7q1WJDWri9TWJMiga1FMGJO_GKSbWwaDS6CVNZ9c0,9270
|
|
113
112
|
tl_cli/_plugin/skills/tl-views-guarantee/scripts/vg.py,sha256=Qp5poinHEqh9374anq0bLtlxj2YL6ipBicaT960-Cws,15825
|
|
114
|
-
thoughtleaders_cli-0.7.
|
|
115
|
-
thoughtleaders_cli-0.7.
|
|
116
|
-
thoughtleaders_cli-0.7.
|
|
117
|
-
thoughtleaders_cli-0.7.
|
|
118
|
-
thoughtleaders_cli-0.7.
|
|
113
|
+
thoughtleaders_cli-0.7.5.dist-info/METADATA,sha256=DrOuWCd6RmuSEdebcd8PU8b4FpRSJejLHQkAVXcvTw0,18452
|
|
114
|
+
thoughtleaders_cli-0.7.5.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
115
|
+
thoughtleaders_cli-0.7.5.dist-info/entry_points.txt,sha256=umZp-1BkGkHDG0bNZXpTXrjwW0HGf9IDFN40eAWuuvg,39
|
|
116
|
+
thoughtleaders_cli-0.7.5.dist-info/licenses/LICENSE,sha256=RUfdfLsn6jygiyrnnVUHt6r4IPwr2rbDm9Kixgtu8fo,1071
|
|
117
|
+
thoughtleaders_cli-0.7.5.dist-info/RECORD,,
|
tl_cli/__init__.py
CHANGED
tl_cli/auth/commands.py
CHANGED
|
@@ -8,9 +8,9 @@ from tl_cli._typer_utils import AlphaSortedTyperGroup
|
|
|
8
8
|
from rich.console import Console
|
|
9
9
|
from rich.prompt import Prompt
|
|
10
10
|
|
|
11
|
-
from tl_cli.auth.
|
|
12
|
-
from tl_cli.auth.login import login_browser, login_device_code
|
|
11
|
+
from tl_cli.auth.login import login_browser, login_device_code, revoke_refresh_token
|
|
13
12
|
from tl_cli.auth.token_store import KIND_API_KEY, StoredTokens, clear_tokens, load_tokens, save_tokens
|
|
13
|
+
from tl_cli.config import get_config
|
|
14
14
|
|
|
15
15
|
app = typer.Typer(cls=AlphaSortedTyperGroup, help="Authentication commands")
|
|
16
16
|
console = Console(stderr=True)
|
|
@@ -106,8 +106,6 @@ def login_cmd() -> None:
|
|
|
106
106
|
else:
|
|
107
107
|
login_browser()
|
|
108
108
|
|
|
109
|
-
finalize_signup()
|
|
110
|
-
|
|
111
109
|
|
|
112
110
|
def _login_api_key() -> None:
|
|
113
111
|
"""Store a user-supplied API key as the active credential.
|
|
@@ -171,7 +169,27 @@ def _login_api_key() -> None:
|
|
|
171
169
|
|
|
172
170
|
@app.command("logout")
|
|
173
171
|
def logout_cmd() -> None:
|
|
174
|
-
"""
|
|
172
|
+
"""Log out: revoke the refresh token at Auth0, then clear stored tokens."""
|
|
173
|
+
tokens = load_tokens()
|
|
174
|
+
# Revoke the long-lived credential server-side so a leaked/synced copy of
|
|
175
|
+
# the local token store can't keep minting access tokens. Best-effort —
|
|
176
|
+
# API-key auth has no refresh token, and an offline revoke must not block
|
|
177
|
+
# clearing local credentials.
|
|
178
|
+
if tokens and not tokens.is_api_key and tokens.refresh_token:
|
|
179
|
+
if revoke_refresh_token(tokens.refresh_token):
|
|
180
|
+
console.print("[dim]Refresh token revoked at Auth0.[/dim]")
|
|
181
|
+
else:
|
|
182
|
+
console.print(
|
|
183
|
+
"[yellow]Could not reach Auth0 to revoke the refresh token; "
|
|
184
|
+
"clearing local credentials anyway.[/yellow]"
|
|
185
|
+
)
|
|
186
|
+
# Revoking the refresh token doesn't end the browser SSO session that
|
|
187
|
+
# the interactive login established. Point the user at Auth0's logout
|
|
188
|
+
# URL so the next `tl auth login` doesn't silently SSO straight back in.
|
|
189
|
+
logout_url = f"https://{get_config().auth0_domain}/logout"
|
|
190
|
+
console.print(
|
|
191
|
+
f"To end your Auth0 browser session, visit: [cyan]{logout_url}[/cyan]"
|
|
192
|
+
)
|
|
175
193
|
clear_tokens()
|
|
176
194
|
console.print("[green]Logged out successfully.[/green]")
|
|
177
195
|
|
tl_cli/auth/login.py
CHANGED
|
@@ -212,6 +212,29 @@ def refresh_access_token(refresh_token: str) -> StoredTokens:
|
|
|
212
212
|
return tokens
|
|
213
213
|
|
|
214
214
|
|
|
215
|
+
def revoke_refresh_token(refresh_token: str) -> bool:
|
|
216
|
+
"""Best-effort revocation of a refresh token at Auth0 (RFC 7009).
|
|
217
|
+
|
|
218
|
+
Invalidates the long-lived credential server-side so it can no longer mint
|
|
219
|
+
new access tokens. Public-client call — `client_id` only, no secret. Returns
|
|
220
|
+
True on success; never raises — network / Auth0 errors are swallowed so
|
|
221
|
+
`tl auth logout` can still clear the local credentials when offline.
|
|
222
|
+
"""
|
|
223
|
+
config = get_config()
|
|
224
|
+
try:
|
|
225
|
+
response = httpx.post(
|
|
226
|
+
f"https://{config.auth0_domain}/oauth/revoke",
|
|
227
|
+
json={
|
|
228
|
+
"client_id": config.auth0_client_id,
|
|
229
|
+
"token": refresh_token,
|
|
230
|
+
},
|
|
231
|
+
timeout=10,
|
|
232
|
+
)
|
|
233
|
+
except httpx.HTTPError:
|
|
234
|
+
return False
|
|
235
|
+
return response.status_code == 200
|
|
236
|
+
|
|
237
|
+
|
|
215
238
|
def _exchange_code(
|
|
216
239
|
code: str,
|
|
217
240
|
code_verifier: str,
|
tl_cli/auth/finalize.py
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
"""Server-side signup finalize, called once per login.
|
|
2
|
-
|
|
3
|
-
After Auth0 returns a valid token the CLI asks the server whether an
|
|
4
|
-
account exists for the email. If yes, this is a no-op. If no, the CLI
|
|
5
|
-
prompts for a persona (Media Buyer or Creator) and POSTs it back; the
|
|
6
|
-
server creates the User, Organization, Profile and CreditAccount.
|
|
7
|
-
|
|
8
|
-
Errors here never abort login — the user can always retry the prompt
|
|
9
|
-
on the next call. We do print a clear message so it's obvious whether
|
|
10
|
-
the account is fully set up.
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
from __future__ import annotations
|
|
14
|
-
|
|
15
|
-
import typer
|
|
16
|
-
from rich.console import Console
|
|
17
|
-
from rich.prompt import Prompt
|
|
18
|
-
|
|
19
|
-
from tl_cli.client.errors import ApiError
|
|
20
|
-
from tl_cli.client.http import get_client
|
|
21
|
-
|
|
22
|
-
console = Console(stderr=True)
|
|
23
|
-
|
|
24
|
-
PERSONA_LABEL_TO_KEY = {
|
|
25
|
-
"Media Buyer": "media_buyer",
|
|
26
|
-
"Creator": "creator",
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def finalize_signup() -> None:
|
|
31
|
-
"""POST /auth/finalize, prompting for persona if the server asks for one."""
|
|
32
|
-
client = get_client()
|
|
33
|
-
try:
|
|
34
|
-
# First call: no body. Server tells us whether persona is required.
|
|
35
|
-
try:
|
|
36
|
-
result = client.post("/auth/finalize", {})
|
|
37
|
-
except ApiError as exc:
|
|
38
|
-
if exc.status_code == 400 and isinstance(exc.raw, dict) and exc.raw.get("code") == "persona_required":
|
|
39
|
-
result = _prompt_and_finalize(client, exc.raw.get("allowed_personas") or [])
|
|
40
|
-
elif exc.status_code == 404:
|
|
41
|
-
# Server predates this endpoint — silently skip; legacy
|
|
42
|
-
# accounts already exist and don't need provisioning.
|
|
43
|
-
return
|
|
44
|
-
else:
|
|
45
|
-
console.print(f"[yellow]Could not finalize signup: {exc.detail}[/yellow]")
|
|
46
|
-
return
|
|
47
|
-
|
|
48
|
-
if result.get("created"):
|
|
49
|
-
org = result.get("organization", {})
|
|
50
|
-
console.print(
|
|
51
|
-
f"[green]Account created for {org.get('name', 'your organization')}.[/green] "
|
|
52
|
-
"Run [bold]tl balance[/bold] to see your starter credits."
|
|
53
|
-
)
|
|
54
|
-
finally:
|
|
55
|
-
client.close()
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def _prompt_and_finalize(client, allowed: list[str]) -> dict:
|
|
59
|
-
"""Prompt the user for a persona, then retry /auth/finalize."""
|
|
60
|
-
console.print()
|
|
61
|
-
console.print("[bold]Welcome to ThoughtLeaders![/bold] We need one more detail to set up your account.")
|
|
62
|
-
console.print(" [cyan]1[/cyan] — Media Buyer (brands and agencies buying sponsorships)")
|
|
63
|
-
console.print(" [cyan]2[/cyan] — Creator (channels selling sponsorships)")
|
|
64
|
-
|
|
65
|
-
persona_key: str | None = None
|
|
66
|
-
while persona_key is None:
|
|
67
|
-
choice = Prompt.ask("I am a", choices=["1", "2"], default="1", console=console)
|
|
68
|
-
candidate = "media_buyer" if choice == "1" else "creator"
|
|
69
|
-
if allowed and candidate not in allowed:
|
|
70
|
-
console.print(f"[yellow]Server rejects persona '{candidate}'. Allowed: {', '.join(allowed)}.[/yellow]")
|
|
71
|
-
continue
|
|
72
|
-
persona_key = candidate
|
|
73
|
-
|
|
74
|
-
org_name = Prompt.ask(
|
|
75
|
-
"Organization name (optional, leave blank to use your email)",
|
|
76
|
-
default="",
|
|
77
|
-
console=console,
|
|
78
|
-
).strip()
|
|
79
|
-
|
|
80
|
-
body = {"persona": persona_key}
|
|
81
|
-
if org_name:
|
|
82
|
-
body["organization_name"] = org_name
|
|
83
|
-
|
|
84
|
-
try:
|
|
85
|
-
return client.post("/auth/finalize", body)
|
|
86
|
-
except ApiError as exc:
|
|
87
|
-
console.print(f"[red]Signup failed:[/red] {exc.detail}")
|
|
88
|
-
raise typer.Exit(1)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|