thoughtleaders-cli 0.5.0__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.
Files changed (59) hide show
  1. thoughtleaders_cli-0.5.0.dist-info/METADATA +215 -0
  2. thoughtleaders_cli-0.5.0.dist-info/RECORD +59 -0
  3. thoughtleaders_cli-0.5.0.dist-info/WHEEL +4 -0
  4. thoughtleaders_cli-0.5.0.dist-info/entry_points.txt +2 -0
  5. thoughtleaders_cli-0.5.0.dist-info/licenses/LICENSE +21 -0
  6. tl_cli/__init__.py +3 -0
  7. tl_cli/_completions.py +4 -0
  8. tl_cli/_plugin/.claude-plugin/marketplace.json +17 -0
  9. tl_cli/_plugin/.claude-plugin/plugin.json +12 -0
  10. tl_cli/_plugin/agents/tl-analyst.md +66 -0
  11. tl_cli/_plugin/commands/tl-balance.md +10 -0
  12. tl_cli/_plugin/commands/tl-brands.md +16 -0
  13. tl_cli/_plugin/commands/tl-channels.md +31 -0
  14. tl_cli/_plugin/commands/tl-reports.md +16 -0
  15. tl_cli/_plugin/commands/tl-sponsorships.md +23 -0
  16. tl_cli/_plugin/commands/tl.md +28 -0
  17. tl_cli/_plugin/hooks/hooks.json +26 -0
  18. tl_cli/_plugin/hooks/scripts/post-usage.sh +26 -0
  19. tl_cli/_plugin/hooks/scripts/pre-check.sh +30 -0
  20. tl_cli/_plugin/skills/tl/SKILL.md +413 -0
  21. tl_cli/_plugin/skills/tl/references/business-glossary.md +159 -0
  22. tl_cli/_plugin/skills/tl/references/elasticsearch-schema.md +259 -0
  23. tl_cli/_plugin/skills/tl/references/firebolt-schema.md +208 -0
  24. tl_cli/_plugin/skills/tl/references/postgres-schema.md +269 -0
  25. tl_cli/auth/__init__.py +0 -0
  26. tl_cli/auth/commands.py +49 -0
  27. tl_cli/auth/login.py +328 -0
  28. tl_cli/auth/pkce.py +21 -0
  29. tl_cli/auth/token_store.py +98 -0
  30. tl_cli/client/__init__.py +0 -0
  31. tl_cli/client/errors.py +72 -0
  32. tl_cli/client/http.py +109 -0
  33. tl_cli/commands/__init__.py +0 -0
  34. tl_cli/commands/ask.py +54 -0
  35. tl_cli/commands/balance.py +68 -0
  36. tl_cli/commands/brands.py +174 -0
  37. tl_cli/commands/changelog.py +119 -0
  38. tl_cli/commands/channels.py +291 -0
  39. tl_cli/commands/comments.py +63 -0
  40. tl_cli/commands/db.py +104 -0
  41. tl_cli/commands/deals.py +52 -0
  42. tl_cli/commands/describe.py +166 -0
  43. tl_cli/commands/doctor.py +70 -0
  44. tl_cli/commands/matches.py +69 -0
  45. tl_cli/commands/proposals.py +69 -0
  46. tl_cli/commands/reports.py +346 -0
  47. tl_cli/commands/schema.py +55 -0
  48. tl_cli/commands/setup.py +401 -0
  49. tl_cli/commands/snapshots.py +93 -0
  50. tl_cli/commands/sponsorships.py +193 -0
  51. tl_cli/commands/uploads.py +84 -0
  52. tl_cli/commands/whoami.py +206 -0
  53. tl_cli/config.py +55 -0
  54. tl_cli/filters.py +88 -0
  55. tl_cli/hints.py +53 -0
  56. tl_cli/main.py +209 -0
  57. tl_cli/output/__init__.py +0 -0
  58. tl_cli/output/formatter.py +436 -0
  59. tl_cli/self_update.py +173 -0
@@ -0,0 +1,346 @@
1
+ """tl reports — List, run, and create reports."""
2
+
3
+ import json
4
+ import time
5
+
6
+ import typer
7
+ from rich.console import Console
8
+ from rich.panel import Panel
9
+ from rich.text import Text
10
+
11
+ from tl_cli.client.errors import ApiError, handle_api_error
12
+ from tl_cli.client.http import get_client
13
+ from tl_cli.output.formatter import detect_format, output
14
+
15
+ app = typer.Typer(help="Saved reports (list, run, create)")
16
+ err = Console(stderr=True)
17
+
18
+ # Report type labels matching Django's ReportType enum
19
+ REPORT_TYPE_LABELS = {1: "Content", 2: "Brands", 3: "Channels", 8: "Sponsorships"}
20
+
21
+ POLL_INTERVAL = 2 # seconds between server polls
22
+
23
+
24
+ @app.callback(invoke_without_command=True)
25
+ def reports(
26
+ ctx: typer.Context,
27
+ json_output: bool = typer.Option(False, "--json", help="JSON output"),
28
+ csv_output: bool = typer.Option(False, "--csv", help="CSV output"),
29
+ md_output: bool = typer.Option(False, "--md", help="Markdown output"),
30
+ toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
31
+ ) -> None:
32
+ """List your organization's saved reports (free, no credits).
33
+
34
+ Examples:
35
+ tl reports
36
+ tl reports --json
37
+ """
38
+ if ctx.invoked_subcommand is not None:
39
+ return
40
+
41
+ fmt = detect_format(json_output, csv_output, md_output, toon_output)
42
+
43
+ client = get_client()
44
+ try:
45
+ data = client.get("/reports")
46
+ for r in data.get("results", []):
47
+ r["report_id"] = r.pop("id", None)
48
+ output(
49
+ data,
50
+ fmt,
51
+ columns=["report_id", "title", "report_type", "created_by", "updated_at"],
52
+ title="Saved Reports",
53
+ )
54
+ except ApiError as e:
55
+ handle_api_error(e)
56
+ finally:
57
+ client.close()
58
+
59
+
60
+ @app.command("run")
61
+ def run_report(
62
+ report_id: int = typer.Argument(..., help="Report ID to execute"),
63
+ since: str | None = typer.Option(None, "--since", help="Override start date"),
64
+ until: str | None = typer.Option(None, "--until", help="Override end date"),
65
+ columns: str | None = typer.Option(None, "--columns", help="Comma-separated list of columns to display (overrides defaults)"),
66
+ json_output: bool = typer.Option(False, "--json", help="JSON output"),
67
+ csv_output: bool = typer.Option(False, "--csv", help="CSV output"),
68
+ md_output: bool = typer.Option(False, "--md", help="Markdown output"),
69
+ toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
70
+ limit: int = typer.Option(100, "--limit", "-l", help="Max results"),
71
+ offset: int = typer.Option(0, "--offset", help="Pagination offset"),
72
+ ) -> None:
73
+ """Run a saved report with its configured filters.
74
+
75
+ Credits are charged based on the results returned (varies by report type).
76
+
77
+ Examples:
78
+ tl reports run 789
79
+ tl reports run 789 --since 2026-01-01 --json
80
+ tl reports run 789 --columns brand_id,name,views_sum,channel_count
81
+ """
82
+ fmt = detect_format(json_output, csv_output, md_output, toon_output)
83
+
84
+ params: dict[str, str] = {"limit": str(limit), "offset": str(offset)}
85
+ if since:
86
+ params["since"] = since
87
+ if until:
88
+ params["until"] = until
89
+
90
+ client = get_client()
91
+ try:
92
+ data = client.get(f"/reports/{report_id}/run", params=params)
93
+
94
+ # Build title from report metadata
95
+ report_title = data.get("report_title", "")
96
+ report_type = data.get("report_type", "")
97
+ title = f"Report #{report_id}"
98
+ if report_title:
99
+ title = f"{report_title} (#{report_id})"
100
+
101
+ # Column selection priority: --columns flag > server display_columns > auto-detect
102
+ col_list = None
103
+ if columns:
104
+ col_list = [c.strip() for c in columns.split(",") if c.strip()]
105
+ elif data.get("display_columns"):
106
+ col_list = data["display_columns"]
107
+
108
+ output(data, fmt, columns=col_list, title=title)
109
+ except ApiError as e:
110
+ handle_api_error(e)
111
+ finally:
112
+ client.close()
113
+
114
+
115
+ # ---------------------------------------------------------------------------
116
+ # tl reports create — AI Report Builder (server-side)
117
+ # ---------------------------------------------------------------------------
118
+
119
+
120
+ def _format_preview(config: dict) -> Panel:
121
+ """Format a report config as a Rich panel for terminal display."""
122
+ lines = Text()
123
+
124
+ report_type = config.get("report_type", 3)
125
+ lines.append("Report Type: ", style="bold")
126
+ lines.append(f"{REPORT_TYPE_LABELS.get(report_type, report_type)}\n")
127
+
128
+ title = config.get("report_title", "Untitled")
129
+ lines.append("Title: ", style="bold")
130
+ lines.append(f"{title}\n")
131
+
132
+ filterset = config.get("filterset", {})
133
+ keyword_groups = filterset.get("keyword_groups", [])
134
+ if keyword_groups:
135
+ lines.append("\nKeywords: ", style="bold")
136
+ kw_texts = []
137
+ for g in keyword_groups:
138
+ if isinstance(g, dict):
139
+ text = g.get("text", "")
140
+ if g.get("exclude"):
141
+ kw_texts.append(f"-{text}")
142
+ else:
143
+ kw_texts.append(text)
144
+ lines.append(", ".join(kw_texts[:20]))
145
+ if len(kw_texts) > 20:
146
+ lines.append(f" ... and {len(kw_texts) - 20} more")
147
+ lines.append("\n")
148
+
149
+ filters = []
150
+ if filterset.get("languages"):
151
+ filters.append(f"Languages: {', '.join(str(lang) for lang in filterset['languages'])}")
152
+ if filterset.get("content_categories"):
153
+ filters.append(f"Categories: {', '.join(str(c) for c in filterset['content_categories'])}")
154
+ if filterset.get("youtube_views_from") or filterset.get("youtube_views_to"):
155
+ vf = filterset.get("youtube_views_from", "")
156
+ vt = filterset.get("youtube_views_to", "")
157
+ filters.append(f"Views: {vf} - {vt}")
158
+ if filterset.get("days_ago"):
159
+ filters.append(f"Last {filterset['days_ago']} days")
160
+
161
+ if filters:
162
+ lines.append("\nFilters: ", style="bold")
163
+ lines.append("; ".join(filters))
164
+ lines.append("\n")
165
+
166
+ summary = config.get("summary", "")
167
+ if summary:
168
+ lines.append("\nSummary: ", style="bold dim")
169
+ lines.append(summary, style="dim")
170
+ lines.append("\n")
171
+
172
+ return Panel(lines, title="[bold]Report Preview[/bold]", border_style="blue")
173
+
174
+
175
+ def _poll_for_result(client, task_id: str, timeout: int) -> dict:
176
+ """Poll the server for the orchestration result."""
177
+ deadline = time.time() + timeout
178
+ last_message = ""
179
+
180
+ with err.status("[bold blue]Analyzing your request...[/bold blue]") as status:
181
+ while time.time() < deadline:
182
+ data = client.get(f"/reports/poll/{task_id}")
183
+
184
+ for entry in data.get("status_log", []):
185
+ if isinstance(entry, dict):
186
+ msg = entry.get("description", "") or entry.get("title", "")
187
+ if msg and msg != last_message:
188
+ status.update(f"[bold blue]{msg}[/bold blue]")
189
+ last_message = msg
190
+
191
+ if data.get("finished"):
192
+ result = data.get("end_result")
193
+ if data.get("error") or not result:
194
+ err.print("[red]Report generation failed on the server.[/red]")
195
+ raise typer.Exit(1)
196
+ return result
197
+
198
+ time.sleep(POLL_INTERVAL)
199
+
200
+ err.print(f"[red]Orchestration timed out after {timeout}s[/red]")
201
+ raise typer.Exit(1)
202
+
203
+
204
+ def _handle_follow_up(result: dict) -> str:
205
+ """Display follow-up question and get user's answer."""
206
+ question = result.get("question", "Could you provide more details?")
207
+ suggestions = result.get("suggestions", [])
208
+ err.print(f"\n[yellow]{question}[/yellow]")
209
+ if suggestions:
210
+ for i, s in enumerate(suggestions, 1):
211
+ title = s.get("title", s) if isinstance(s, dict) else s
212
+ err.print(f" [dim]{i}.[/dim] {title}")
213
+ err.print()
214
+
215
+ answer = typer.prompt("Your answer")
216
+
217
+ # Allow picking by number
218
+ try:
219
+ idx = int(answer.strip()) - 1
220
+ if 0 <= idx < len(suggestions):
221
+ s = suggestions[idx]
222
+ answer = s.get("title", s) if isinstance(s, dict) else s
223
+ except ValueError:
224
+ pass
225
+
226
+ return answer
227
+
228
+
229
+ @app.command("create")
230
+ def create_report(
231
+ prompt: str = typer.Argument(..., help="Natural language description of the report you want"),
232
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
233
+ json_output: bool = typer.Option(False, "--json", help="Output raw JSON config"),
234
+ timeout: int = typer.Option(300, "--timeout", help="Max orchestration time in seconds"),
235
+ ) -> None:
236
+ """Create a report from a natural language description.
237
+
238
+ Sends your prompt to the ThoughtLeaders server, which runs the AI Report
239
+ Builder pipeline (keyword research, config generation, review). Then
240
+ confirms with the server to create the campaign.
241
+
242
+ Examples:
243
+ tl reports create "gaming channels sponsoring energy drinks"
244
+ tl reports create "tech review channels with 100K+ subscribers" --yes
245
+ tl reports create "beauty brands on YouTube" --json
246
+ """
247
+ client = get_client()
248
+ try:
249
+ conversation: list[dict[str, str]] = []
250
+ current_prompt = prompt
251
+
252
+ while True:
253
+ # Send prompt to server, poll for result
254
+ try:
255
+ create_data = client.post("/reports/create", json_body={
256
+ "prompt": current_prompt,
257
+ "conversation": conversation,
258
+ })
259
+ except ApiError as e:
260
+ if e.status_code == 503:
261
+ err.print("[red]AI Report Builder is temporarily unavailable. Please try again later.[/red]")
262
+ raise typer.Exit(1)
263
+ handle_api_error(e)
264
+ raise typer.Exit(1)
265
+
266
+ task_id = create_data.get("task_id")
267
+ if not task_id:
268
+ err.print("[red]Server did not return a task ID.[/red]")
269
+ raise typer.Exit(1)
270
+
271
+ result = _poll_for_result(client, task_id, timeout)
272
+ action = result.get("action", "")
273
+
274
+ # Server wraps response: "preview" → config in result["config"]
275
+ if action == "follow_up":
276
+ answer = _handle_follow_up(result)
277
+ conversation.append({"role": "user", "content": current_prompt})
278
+ conversation.append({"role": "assistant", "content": result.get("question", "")})
279
+ current_prompt = answer
280
+ continue
281
+
282
+ if action in ("error", "unsupported"):
283
+ message = result.get("message", "Request could not be processed.")
284
+ err.print(f"\n[red]{message}[/red]")
285
+ raise typer.Exit(1)
286
+
287
+ if action == "preview":
288
+ config = result.get("config", {})
289
+ elif action == "create_report":
290
+ config = result
291
+ else:
292
+ err.print(f"[yellow]Unexpected action: {action}[/yellow]")
293
+ if json_output:
294
+ print(json.dumps(result, indent=2, default=str))
295
+ raise typer.Exit(1)
296
+
297
+ break
298
+
299
+ # --- Show preview ---
300
+ if json_output:
301
+ print(json.dumps(config, indent=2, default=str))
302
+ if not yes:
303
+ raise typer.Exit(0)
304
+ else:
305
+ err.print()
306
+ err.print(_format_preview(config))
307
+
308
+ # --- Confirm ---
309
+ if not yes:
310
+ confirmed = typer.confirm("Create this report?", default=True)
311
+ if not confirmed:
312
+ err.print("[dim]Cancelled.[/dim]")
313
+ raise typer.Exit(0)
314
+
315
+ # --- Save to server ---
316
+ data = client.post("/reports/confirm", json_body={
317
+ "config": config,
318
+ "prompts": [prompt],
319
+ "reasoning": "",
320
+ })
321
+
322
+ results = data.get("results", [{}])
323
+ result = results[0] if results else {}
324
+ report_url = result.get("report_url", "")
325
+ campaign_id = result.get("campaign_id", "")
326
+
327
+ if json_output:
328
+ print(json.dumps(data, indent=2, default=str))
329
+ else:
330
+ err.print()
331
+ err.print("[green bold]Report created![/green bold]")
332
+ err.print(f" Campaign ID: {campaign_id}")
333
+ err.print(f" URL: https://app.thoughtleaders.io{report_url}")
334
+
335
+ unresolved = result.get("unresolved_names", [])
336
+ if unresolved:
337
+ err.print(f"\n [yellow]Unresolved names:[/yellow] {', '.join(unresolved)}")
338
+
339
+ usage = data.get("usage")
340
+ if usage:
341
+ err.print(f"\n [dim]{usage.get('credits_charged', 0)} credits · {usage.get('balance_remaining', '?')} remaining[/dim]")
342
+
343
+ except ApiError as e:
344
+ handle_api_error(e)
345
+ finally:
346
+ client.close()
@@ -0,0 +1,55 @@
1
+ """tl schema — Show raw-db schema documentation for `tl db pg|fb|es`."""
2
+
3
+ import json
4
+
5
+ import typer
6
+ from rich.console import Console
7
+ from rich.markdown import Markdown
8
+
9
+ from tl_cli.client.errors import ApiError, handle_api_error
10
+ from tl_cli.client.http import get_client
11
+
12
+ app = typer.Typer(help="Show schema documentation for raw db queries (`tl db pg|fb|es`)")
13
+ console = Console()
14
+
15
+
16
+ def _show(db: str, json_output: bool) -> None:
17
+ client = get_client()
18
+ try:
19
+ data = client.get(f"/raw/{db}/schema")
20
+ if json_output:
21
+ print(json.dumps(data, indent=2, default=str))
22
+ return
23
+ content = data.get("content", "")
24
+ if console.is_terminal:
25
+ console.print(Markdown(content))
26
+ else:
27
+ print(content)
28
+ except ApiError as e:
29
+ handle_api_error(e)
30
+ finally:
31
+ client.close()
32
+
33
+
34
+ @app.command("pg")
35
+ def pg_cmd(
36
+ json_output: bool = typer.Option(False, "--json", help="JSON output"),
37
+ ) -> None:
38
+ """Show PostgreSQL schema reference (for `tl db pg`)."""
39
+ _show("pg", json_output)
40
+
41
+
42
+ @app.command("fb")
43
+ def fb_cmd(
44
+ json_output: bool = typer.Option(False, "--json", help="JSON output"),
45
+ ) -> None:
46
+ """Show Firebolt schema (live: tables and column types) for `tl db fb`."""
47
+ _show("fb", json_output)
48
+
49
+
50
+ @app.command("es")
51
+ def es_cmd(
52
+ json_output: bool = typer.Option(False, "--json", help="JSON output"),
53
+ ) -> None:
54
+ """Show Elasticsearch document shape for `tl db es`."""
55
+ _show("es", json_output)