thoughtleaders-cli 0.7.2__py3-none-any.whl → 0.7.4__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: thoughtleaders-cli
3
- Version: 0.7.2
3
+ Version: 0.7.4
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=w380YYlqGtwTYG78LJyZu5ljV8MmIBzKLk-R9Kq8Apw,112
1
+ tl_cli/__init__.py,sha256=xK0gP7uGAavYeHM3ixCoYhRg6AEnE_DI83z7_FnnInc,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
@@ -21,7 +21,7 @@ tl_cli/commands/balance.py,sha256=JHSmKdAUNtd-9zlKZhit9_BmhHXrEk0kQ-oSlQ0Eo6Y,27
21
21
  tl_cli/commands/brands.py,sha256=rvYRrhkDhHDlvFpWrf8TtGbBt8KyNxHcVodjjFNpmHY,12443
22
22
  tl_cli/commands/bulk_import.py,sha256=d4y1k_lD52LPJcCqXxEmyHIqcIwomZgbjqs1_QxPjeQ,4536
23
23
  tl_cli/commands/changelog.py,sha256=D1PtDdHpawTlWqUHjKzVmv9yXLSU915UVmI3dZzEwyA,4241
24
- tl_cli/commands/channels.py,sha256=EgwTzweKsQEerBTwiWGCP1myCTW3KQFu4Om2j4Htfzk,17217
24
+ tl_cli/commands/channels.py,sha256=ALw2fgJL3w0dpYp3A41OECL0odenhSV16vFA7la7aQI,16878
25
25
  tl_cli/commands/credits.py,sha256=2xCht2e420LmaFBKNdKoMz8GlTh31qSWSlJAnVzoZic,7308
26
26
  tl_cli/commands/db.py,sha256=rdIQrxT7sdrPEnBbByNHvPr2X6iIg-wb19X9bWYwDRc,5053
27
27
  tl_cli/commands/deals.py,sha256=ZK9yneInsC6DXoCPS65oyLoVR0eRW1xdRlEN7oRp1pc,2174
@@ -30,7 +30,7 @@ tl_cli/commands/doctor.py,sha256=KUKglwhMc7B26XXy_3M0LkHu7wqfFO5T0YPHO1SH1VY,902
30
30
  tl_cli/commands/matches.py,sha256=K5o6B8FLECp7825dU4W3X8n-wuXvGJz57xpQPXeXQ-0,2886
31
31
  tl_cli/commands/proposals.py,sha256=khsjorluIfgrJ22DiwzIAFcYD4JbirjkOBz1KuQ0Sdk,2918
32
32
  tl_cli/commands/recommender.py,sha256=DIRvnSbV2TwvVUgA5luGJ7uQUOjHx2CULFsZVaqUYw4,18922
33
- tl_cli/commands/reports.py,sha256=1QDJN6FrUmWCuvaMxN884_bVdy-l7anBuS2jkCZF1VQ,22543
33
+ tl_cli/commands/reports.py,sha256=WcZWtBVRX49z-1Fw4T5iq_xZqks3QmIXlWVg5iVGs58,26532
34
34
  tl_cli/commands/schema.py,sha256=GCBEE4fDatQhVasLKKr7bkGhELRZ0scYm_hUCbDYmuA,5985
35
35
  tl_cli/commands/setup.py,sha256=QST6DCSxJHLFNX1UUJHwZ3hTDTnySATaV2Tc2JsdYYY,21920
36
36
  tl_cli/commands/snapshots.py,sha256=mWxnZI_UBzbZZHsA3uP0Q7Gt2XsLLoRD5dRNGt-mGUE,3428
@@ -40,14 +40,14 @@ tl_cli/commands/whoami.py,sha256=aUXwBRwh1vAGrvz8CKGfHYtEOKJCIDfwrGesKAwYZMk,786
40
40
  tl_cli/output/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
41
41
  tl_cli/output/formatter.py,sha256=pqlKmb2nZ1Z2e1A9m8l5mgVemJinVAP4in1tUzFWHno,22522
42
42
  tl_cli/_plugin/.claude-plugin/marketplace.json,sha256=l56PMmyjfGXNGlV30wRyOAe74B6gJNCVNCxgsBbSNxc,446
43
- tl_cli/_plugin/.claude-plugin/plugin.json,sha256=_Im3uBChUbEhtgMa7p_Efg10yDoSSS0GtrAKAL_E6ME,466
43
+ tl_cli/_plugin/.claude-plugin/plugin.json,sha256=8RBuXpnRetjlzB0k2KrWjRquy_KrutSRFcioWI6r1Lo,466
44
44
  tl_cli/_plugin/agents/tl-analyst.md,sha256=6J3X3NANkWg6OOUCvNirkN4ulIk80KSumPncDUBt75E,6761
45
45
  tl_cli/_plugin/agents/youtube-comment-classifier.md,sha256=S5lr_htA98FIX0su8FJ2ntiHfbdK8OB2NQKC4lTnQcw,2178
46
46
  tl_cli/_plugin/hooks/hooks.json,sha256=FSWibw1xAjA-suFV3fR8btIb2kQ82LQ08otTr-NpmFw,835
47
47
  tl_cli/_plugin/hooks/scripts/load-tl-skill.mjs,sha256=EBsyZ-caei-CBJsRtqzJXJs_20O3H22MuVmDpu96umo,805
48
48
  tl_cli/_plugin/hooks/scripts/post-usage.sh,sha256=WVvZLkZik6lbeZ20Kh-wgm4JkRFHFN0Uwl4C8S3Y0sY,759
49
49
  tl_cli/_plugin/hooks/scripts/pre-check.sh,sha256=E9KeuXy6yeHEBOnOFW4hDW-Et-Dbp1Oh--3WXKfOX78,898
50
- tl_cli/_plugin/skills/tl/SKILL.md,sha256=fKxHgoe0oX1vFwVFUTzF95h4fq5fAmFlodqZr7cdbeA,58174
50
+ tl_cli/_plugin/skills/tl/SKILL.md,sha256=YDpZ3DtP_HjEfZtC17ELG9kk-OixRXNvRL7VWwrrMXI,59099
51
51
  tl_cli/_plugin/skills/tl/references/business-glossary.md,sha256=FCS-qBOGpdJCmHdglRGRjAuTQAtzpxJNpMkEWThuvlI,17779
52
52
  tl_cli/_plugin/skills/tl/references/elasticsearch-schema.md,sha256=OpHvixZ8UcZYJd8GdwgumryFyKwPAxj3AvPkl1QreMY,9316
53
53
  tl_cli/_plugin/skills/tl/references/firebolt-schema.md,sha256=KagpSWWEWIRfsAWz271PvAqVbSPvWLoogWhCA_XFSZw,10642
@@ -111,8 +111,8 @@ tl_cli/_plugin/skills/tl-top-partnerships/SKILL.md,sha256=hvH05hIaGlc0RfTE0GLBtD
111
111
  tl_cli/_plugin/skills/tl-top-partnerships/scripts/top_partnerships.py,sha256=_13W6-HuD_jtl7AWQQcZQ0SQO9qODMymlcL-1s4-VwU,13248
112
112
  tl_cli/_plugin/skills/tl-views-guarantee/SKILL.md,sha256=IH7q1WJDWri9TWJMiga1FMGJO_GKSbWwaDS6CVNZ9c0,9270
113
113
  tl_cli/_plugin/skills/tl-views-guarantee/scripts/vg.py,sha256=Qp5poinHEqh9374anq0bLtlxj2YL6ipBicaT960-Cws,15825
114
- thoughtleaders_cli-0.7.2.dist-info/METADATA,sha256=7BN2D4RB5Aq2zWM6krJwjpR6IAFOZczDvjnUeN67q0A,18452
115
- thoughtleaders_cli-0.7.2.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
116
- thoughtleaders_cli-0.7.2.dist-info/entry_points.txt,sha256=umZp-1BkGkHDG0bNZXpTXrjwW0HGf9IDFN40eAWuuvg,39
117
- thoughtleaders_cli-0.7.2.dist-info/licenses/LICENSE,sha256=RUfdfLsn6jygiyrnnVUHt6r4IPwr2rbDm9Kixgtu8fo,1071
118
- thoughtleaders_cli-0.7.2.dist-info/RECORD,,
114
+ thoughtleaders_cli-0.7.4.dist-info/METADATA,sha256=MPMDYcGN8eGh3eYWdKN3mDoPGz8E5sijXa2c2FpOybI,18452
115
+ thoughtleaders_cli-0.7.4.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
116
+ thoughtleaders_cli-0.7.4.dist-info/entry_points.txt,sha256=umZp-1BkGkHDG0bNZXpTXrjwW0HGf9IDFN40eAWuuvg,39
117
+ thoughtleaders_cli-0.7.4.dist-info/licenses/LICENSE,sha256=RUfdfLsn6jygiyrnnVUHt6r4IPwr2rbDm9Kixgtu8fo,1071
118
+ thoughtleaders_cli-0.7.4.dist-info/RECORD,,
tl_cli/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """ThoughtLeaders CLI — query sponsorship data, channels, brands, and intelligence."""
2
2
 
3
- __version__ = "0.7.2"
3
+ __version__ = "0.7.4"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tl-cli",
3
- "version": "0.7.2",
3
+ "version": "0.7.4",
4
4
  "description": "ThoughtLeaders CLI — query sponsorship deals, channels, brands, uploads, and intelligence from the terminal",
5
5
  "author": {
6
6
  "name": "ThoughtLeaders",
@@ -15,9 +15,9 @@ If doing a database query, follow this recipe:
15
15
 
16
16
  * First, run `tl whoami` to confirm the API is working and to find out user metadata and limits.
17
17
  * Always read `references/business-glossary.md`
18
- * If doing a PostgreSQL (pg) query: first read `references/postgres-schema.md`, then run `tl schema pg`
19
- * If doing an ElasticSearch (es) query: first read `references/elasticsearch-schema.md`, then run `tl schema es`
20
- * If doing a Firebolt (fb) query: first read `references/firebolt-schema.md`, then run `tl schema fb`
18
+ * If doing a PostgreSQL (pg) query: always first read `references/postgres-schema.md`, then run `tl schema pg`
19
+ * If doing an ElasticSearch (es) query: always first read `references/elasticsearch-schema.md`, then run `tl schema es`
20
+ * If doing a Firebolt (fb) query: always first read `references/firebolt-schema.md`, then run `tl schema fb`
21
21
 
22
22
  **Process data with shell tools, not your context window.** Don't pull large result sets into your reasoning context just to filter, sort, count, or extract a field - that wastes tokens and slows you down. Pipe `tl … --json` (or `--csv`, or `--toon`) into `jq` (for JSON), `rg` or `duckdb` (for CSV), or `yq` (for YAML) as appropriate, and read only the answer back. Pick the tool by shape:
23
23
 
@@ -150,6 +150,8 @@ Unless the user specifically asks for running a specific report or showing the r
150
150
 
151
151
  If the user says yes (or uses any of the save-trigger phrases, like `save it`, `save the list`, `make a report`, `persist this`, `turn this into a campaign`, `I want to come back to this`), invoke the `tl-save-report` skill — it owns the filter-vs-list decision flow, the FilterSet mapping, and the `tl reports create` / `tl reports save-list` save call. **Don't try to compose the report config yourself**; hand off to the skill.
152
152
 
153
+ **When the user already has specific IDs in hand** — "make a report of these sponsorship IDs", "save these exact channels", or a curated set you just resolved — that is always a **list-style** report: pin the IDs with `tl reports save-list <entity> --ids-file` (or add them to an existing report with `tl bulk-import <entity> -c <report-id>`). **Never** route an explicit-ID request through `tl reports create "<natural-language prompt>"` — the prompt path builds *predicate filters* and pins none of those IDs, so the report comes back showing unrelated records, not the ones the user named. After creating it, confirm the report actually contains those records before sharing the link.
154
+
153
155
  Skip the save offer when the result clearly doesn't fit a report type — a single scalar count, an aggregate roll-up across entity types, view-curve time series, schema introspection output, or anything that isn't a list of channels / brands / videos / sponsorships. A trailing offer on those would just be noise.
154
156
 
155
157
  Prefer writing shell code, `jq` commands, or `duckdb` commands that fetch or analysise large sets of data instead of analysing it yourself. On Mac and Linux, create temporary files in `/tmp` that can be analysed later in different ways. On Windows, create them in the directory pointed to by the `%TEMP%` environment variable. When coding, do it in Python.
@@ -765,7 +767,7 @@ tl channels similar 29834 msn:no --limit 30 # non-MSN channels
765
767
  tl channels similar 29834 tpp:yes --limit 30 # TPP (TL-managed) channels only
766
768
  tl channels similar 29834 min-subs:1000000 exclude:477487 --limit 15 # client-side filters
767
769
  ```
768
- **Both `tl channels show` and `tl channels similar` accept either a numeric channel ID or a channel name.** Name arguments are case-insensitive partial matches; if more than one active channel matches, the command prints a candidates table (channel_id, subscribers, name) and exits 1 so you can retry with a specific ID. The `msn` filter on `similar` is tri-state: `yes` (only MSN channels — the default), `no` (only non-MSN channels), `both` (no MSN filter). `tl channels look-alike` is a hidden alias for `similar` that matches the internal "look-alike channels" terminology.
770
+ **Both `tl channels show` and `tl channels similar` accept either a numeric channel ID or a channel name.** Name arguments are case-insensitive partial matches; if more than one active channel matches, the command prints a candidates table (channel_id, subscribers, name) and exits 1 so you can retry with a specific ID. The `msn` filter on `similar` is tri-state: `yes` (only MSN channels — the default), `no` (only non-MSN channels), `both` (no MSN filter). `tl channels look-alike` is a hidden alias for `similar` that matches the internal "look-alike channels" terminology. `tl channels show` returns a `tl_url` field — the canonical ThoughtLeaders web-app analysis page for the channel; use it verbatim when linking a user to the channel instead of constructing a URL by hand.
769
771
 
770
772
  ### "Browse the recommender" (categories, demographics, formats):
771
773
  ```bash
@@ -64,14 +64,6 @@ def show_cmd(
64
64
  client = get_client()
65
65
  try:
66
66
  data = client.get(f"/channels/{encoded_ref}")
67
- for i, r in enumerate(data.get("results", []) if isinstance(data.get("results"), list) else []):
68
- renamed = {}
69
- for k, v in r.items():
70
- if k == "id":
71
- renamed["channel_id"] = v
72
- else:
73
- renamed[k] = v
74
- data["results"][i] = renamed
75
67
  output_single(data, fmt)
76
68
  if fmt == "table" and data.get("show_cta"):
77
69
  record = data.get("results", data)
@@ -22,6 +22,71 @@ REPORT_TYPE_LABELS = {1: "Content", 2: "Brands", 3: "Channels", 8: "Sponsorships
22
22
 
23
23
  POLL_INTERVAL = 2 # seconds between server polls
24
24
 
25
+ # Filterset keys that pin specific entities into a report (the curated-ID
26
+ # lists). A report built from these holds exactly those records; a report built
27
+ # from predicate filters re-evaluates against live data and pins nothing.
28
+ _PIN_ENTITIES = ("channels", "brands", "sponsorships", "articles")
29
+ # Filterset keys that are structural, not user-facing predicate filters.
30
+ _NON_PREDICATE_KEYS = {"keyword_operator", "sort"}
31
+
32
+
33
+ def _summarize_report_contents(config: dict) -> dict:
34
+ """Summarize what a saved report config actually contains.
35
+
36
+ Returns the report-type label, a per-entity count of *pinned* IDs (the
37
+ curated lists that freeze specific channels/brands/sponsorships/articles
38
+ into the report), and the active predicate-filter keys. Lets a caller —
39
+ human or agent — catch a filters-vs-pinned-IDs mismatch (or a wholly blank
40
+ report) before sharing the link: a report built from a natural-language
41
+ prompt yields predicate filters and pins *nothing*, so "pinned: none" is
42
+ the tell that it doesn't contain the specific records the user named.
43
+ """
44
+ filterset = config.get("filterset") or {}
45
+ pinned = {
46
+ entity: len(filterset[entity])
47
+ for entity in _PIN_ENTITIES
48
+ if isinstance(filterset.get(entity), list) and filterset[entity]
49
+ }
50
+ skip = set(_PIN_ENTITIES) | {f"exclude_{e}" for e in _PIN_ENTITIES} | _NON_PREDICATE_KEYS
51
+ filters = sorted(
52
+ key
53
+ for key, value in filterset.items()
54
+ if key not in skip and value not in (None, "", [], {}, False)
55
+ )
56
+ return {
57
+ "report_type": REPORT_TYPE_LABELS.get(config.get("report_type"), config.get("report_type")),
58
+ "pinned": pinned,
59
+ "filters": filters,
60
+ }
61
+
62
+
63
+ def _render_contents_line(summary: dict) -> str:
64
+ """One-line human rendering of a `_summarize_report_contents` result."""
65
+ pinned = summary.get("pinned") or {}
66
+ pin_str = ", ".join(f"{count} {entity}" for entity, count in pinned.items()) if pinned else "none"
67
+ filters = summary.get("filters") or []
68
+ filt_str = ", ".join(filters) if filters else "none"
69
+ return f"Contents: {summary.get('report_type')} report · pinned: {pin_str} · filters: {filt_str}"
70
+
71
+
72
+ def _print_contents_summary(summary: dict) -> None:
73
+ """Surface a saved report's contents on stderr (safe in every output mode).
74
+
75
+ A report with no pinned entities *and* no filters gets a loud warning —
76
+ that blank/default state is the failure behind "you sent me an empty
77
+ report". Otherwise a dim one-liner lets the caller confirm the report holds
78
+ what the user actually asked for.
79
+ """
80
+ line = _render_contents_line(summary)
81
+ if not summary.get("pinned") and not summary.get("filters"):
82
+ err.print(
83
+ f"[yellow]⚠ {line}\n"
84
+ f" This report has no filters and no pinned entities — it will show "
85
+ f"a blank/default view.[/yellow]"
86
+ )
87
+ else:
88
+ err.print(f"[dim]{line}[/dim]")
89
+
25
90
 
26
91
  @app.callback(invoke_without_command=True)
27
92
  def reports(
@@ -330,6 +395,13 @@ def create_report(
330
395
  With a prompt, runs the AI Report Builder pipeline (keyword research, config
331
396
  generation, review) and saves the resulting campaign.
332
397
 
398
+ Have specific IDs already? A report of *exactly* the channels / brands /
399
+ sponsorships / articles you already have the IDs for is a different job —
400
+ use `tl reports save-list <entity> --ids-file` to pin those IDs into a new
401
+ report, or `tl bulk-import <entity> -c <report-id>` to add them to an
402
+ existing one. A natural-language prompt here builds *predicate filters* and
403
+ pins none of those IDs, so the report comes back showing unrelated records.
404
+
333
405
  With --config '<json>' or --config-file <path>, skips the orchestration
334
406
  pipeline and saves the provided config directly. Useful when an external
335
407
  agent (e.g. the tl-report-builder Claude Code skill) has already produced a
@@ -404,6 +476,13 @@ def create_report(
404
476
  report_url = result.get("report_url", "")
405
477
  campaign_id = result.get("campaign_id", "")
406
478
 
479
+ # Echo what the report actually contains. Surfaced even under
480
+ # --yes/--json (the agent path), so a report built from a prompt —
481
+ # which yields predicate filters and pins no specific IDs — can be
482
+ # spotted before its link is shared.
483
+ summary = _summarize_report_contents(config)
484
+ data["report_contents"] = summary
485
+
407
486
  if toon_output:
408
487
  print(toon_encode(data))
409
488
  elif json_output:
@@ -422,6 +501,8 @@ def create_report(
422
501
  if usage:
423
502
  err.print(f"\n [dim]{usage.get('credits_charged', 0)} credits · {usage.get('balance_remaining', '?')} remaining[/dim]")
424
503
 
504
+ _print_contents_summary(summary)
505
+
425
506
  except ApiError as e:
426
507
  handle_api_error(e)
427
508
  finally:
@@ -541,23 +622,26 @@ def save_list_cmd(
541
622
  finally:
542
623
  client.close()
543
624
 
625
+ summary = _summarize_report_contents(config)
626
+ data["report_contents"] = summary
627
+
544
628
  if fmt == "toon":
545
629
  print(toon_encode(data))
546
- return
547
- if fmt == "json":
630
+ elif fmt == "json":
548
631
  print(json.dumps(data, indent=2, default=str))
549
- return
632
+ else:
633
+ results = data.get("results", [{}])
634
+ result = results[0] if results else {}
635
+ err.print()
636
+ err.print("[green bold]Report created![/green bold]")
637
+ err.print(f" Campaign ID: {result.get('campaign_id', '?')}")
638
+ err.print(f" URL: https://app.thoughtleaders.io{result.get('report_url', '')}")
639
+ err.print(
640
+ "\n[dim]Refine columns, widgets, title, or description with "
641
+ "`tl reports update <id>`.[/dim]"
642
+ )
550
643
 
551
- results = data.get("results", [{}])
552
- result = results[0] if results else {}
553
- err.print()
554
- err.print("[green bold]Report created![/green bold]")
555
- err.print(f" Campaign ID: {result.get('campaign_id', '?')}")
556
- err.print(f" URL: https://app.thoughtleaders.io{result.get('report_url', '')}")
557
- err.print(
558
- "\n[dim]Refine columns, widgets, title, or description with "
559
- "`tl reports update <id>`.[/dim]"
560
- )
644
+ _print_contents_summary(summary)
561
645
 
562
646
 
563
647
  @app.command("update")