thoughtleaders-cli 0.6.5__tar.gz → 0.6.7__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.
Files changed (69) hide show
  1. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/.claude-plugin/plugin.json +1 -1
  2. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/AGENTS.md +2 -2
  3. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/PKG-INFO +6 -6
  4. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/README.md +5 -5
  5. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/agents/tl-analyst.md +2 -2
  6. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/docs/architecture.md +2 -2
  7. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/pyproject.toml +1 -1
  8. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/skills/tl/SKILL.md +48 -23
  9. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/skills/tl/references/elasticsearch-schema.md +2 -4
  10. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/skills/tl/references/firebolt-schema.md +1 -1
  11. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/skills/tl/references/postgres-schema.md +2 -0
  12. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/__init__.py +1 -1
  13. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/commands/brands.py +46 -2
  14. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/commands/channels.py +4 -4
  15. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/commands/recommender.py +77 -28
  16. thoughtleaders_cli-0.6.7/src/tl_cli/commands/schema.py +83 -0
  17. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/main.py +0 -60
  18. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/uv.lock +1 -1
  19. thoughtleaders_cli-0.6.5/src/tl_cli/commands/schema.py +0 -55
  20. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/.claude-plugin/marketplace.json +0 -0
  21. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/.github/workflows/python-publish.yml +0 -0
  22. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/.gitignore +0 -0
  23. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/CLAUDE.md +0 -0
  24. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/LICENSE +0 -0
  25. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/commands/tl-balance.md +0 -0
  26. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/commands/tl-reports.md +0 -0
  27. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/commands/tl-sponsorships.md +0 -0
  28. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/commands/tl.md +0 -0
  29. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/hooks/hooks.json +0 -0
  30. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/hooks/scripts/post-usage.sh +0 -0
  31. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/hooks/scripts/pre-check.sh +0 -0
  32. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/skills/tl/references/business-glossary.md +0 -0
  33. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/_completions.py +0 -0
  34. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/auth/__init__.py +0 -0
  35. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/auth/commands.py +0 -0
  36. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/auth/login.py +0 -0
  37. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/auth/pkce.py +0 -0
  38. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/auth/token_store.py +0 -0
  39. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/client/__init__.py +0 -0
  40. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/client/errors.py +0 -0
  41. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/client/http.py +0 -0
  42. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/commands/__init__.py +0 -0
  43. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/commands/ask.py +0 -0
  44. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/commands/balance.py +0 -0
  45. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/commands/changelog.py +0 -0
  46. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/commands/comments.py +0 -0
  47. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/commands/db.py +0 -0
  48. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/commands/deals.py +0 -0
  49. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/commands/describe.py +0 -0
  50. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/commands/doctor.py +0 -0
  51. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/commands/matches.py +0 -0
  52. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/commands/proposals.py +0 -0
  53. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/commands/reports.py +0 -0
  54. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/commands/setup.py +0 -0
  55. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/commands/snapshots.py +0 -0
  56. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/commands/sponsorships.py +0 -0
  57. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/commands/uploads.py +0 -0
  58. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/commands/whoami.py +0 -0
  59. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/config.py +0 -0
  60. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/filters.py +0 -0
  61. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/hints.py +0 -0
  62. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/output/__init__.py +0 -0
  63. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/output/formatter.py +0 -0
  64. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/src/tl_cli/self_update.py +0 -0
  65. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/tests/__init__.py +0 -0
  66. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/tests/test_auth.py +0 -0
  67. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/tests/test_filters.py +0 -0
  68. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/tests/test_output.py +0 -0
  69. {thoughtleaders_cli-0.6.5 → thoughtleaders_cli-0.6.7}/tests/test_sponsorships.py +0 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tl-cli",
3
- "version": "0.6.5",
3
+ "version": "0.6.7",
4
4
  "description": "ThoughtLeaders CLI — query sponsorship deals, channels, brands, uploads, and intelligence from the terminal",
5
5
  "author": {
6
6
  "name": "ThoughtLeaders",
@@ -1,6 +1,6 @@
1
1
  # Project Overview
2
2
 
3
- **tl-cli** is a Python CLI for querying ThoughtLeaders sponsorship data (sponsorships, channels, brands, uploads, snapshots, reports, vector recommender). Built with Typer + Rich + httpx. Designed as an "agent-first tool" — the CLI handles structured commands and output, while the user's AI agent (Claude) provides intelligence.
3
+ **tl-cli** is a Python CLI for querying ThoughtLeaders sponsorship data (sponsorships, channels, brands, uploads, snapshots, reports, recommender). Built with Typer + Rich + httpx. Designed as an "agent-first tool" — the CLI handles structured commands and output, while the user's AI agent (Claude) provides intelligence.
4
4
 
5
5
  # Architecture
6
6
 
@@ -20,7 +20,7 @@ When adding a new data command, follow this pattern. See `sponsorships.py` for t
20
20
 
21
21
  `deals`, `matches`, and `proposals` are shortcut commands that delegate to sponsorships' `do_list`/`do_show`/`do_create` with a pre-set status filter. They reject explicit `status:` filters — users should use `tl sponsorships list` for finer-grained status filtering.
22
22
 
23
- `recommender` (`commands/recommender.py`) wraps the vector-recommender API at `/api/cli/v1/recommender/*` — `tags` (free), `top-channels` / `top-profiles` / `top-brands`, `inspect-channel`, `inspect-brand`, `similar-to-profile` (all 50 credits flat, Intelligence-gated). The three `top-*` URLs share one server resolver; `top-brands` dedupes the underlying profile rows by brand. Channel→channel and brand→brand similarity stay on `tl channels similar` / `tl brands similar`. When updating the SKILL or examples, prefer steering category/topic discovery (e.g. "Cooking channels") to `tl recommender top-channels "<tag>"` rather than `WHERE content_category = <code>` SQL — the recommender is ranked, not equality-based. The underlying recommender code uses "element"/"field_name" terminology; the CLI/API layer renames these to "tag" at the boundary.
23
+ `recommender` (`commands/recommender.py`) wraps the recommender API at `/api/cli/v1/recommender/*` — `tags` (free), `top-channels` / `top-profiles` / `top-brands`, `inspect-channel`, `inspect-brand`, `similar-to-profile` (all 25 credits flat, Intelligence-gated). The three `top-*` URLs share one server resolver; `top-brands` dedupes the underlying profile rows by brand. Channel→channel and brand→brand similarity stay on `tl channels similar` / `tl brands similar`. When updating the SKILL or examples, prefer steering category/topic discovery (e.g. "Cooking channels") to `tl recommender top-channels "<tag>"` rather than `WHERE content_category = <code>` SQL — the recommender is ranked, not equality-based. The underlying recommender code uses "element"/"field_name" terminology; the CLI/API layer renames these to "tag" at the boundary.
24
24
 
25
25
  ## Filter Parsing (`filters.py`)
26
26
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: thoughtleaders-cli
3
- Version: 0.6.5
3
+ Version: 0.6.7
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
@@ -83,7 +83,7 @@ tl uploads show 1174310:0BehkmVa7ak
83
83
 
84
84
  # Search channels via raw SQL — `tl db pg` against thoughtleaders_channel
85
85
  # (run `tl schema pg` once to confirm the live column set).
86
- # NOTE: For topic / category discovery, prefer the vector recommender over
86
+ # NOTE: For topic / category discovery, prefer the recommender over
87
87
  # `content_category` equality — `tl recommender top-channels "<tag>"`
88
88
  # returns channels ranked by how strongly they load on the topic, not just
89
89
  # rows where the single category code matches exactly.
@@ -101,22 +101,22 @@ tl db pg "SELECT id, channel_name FROM thoughtleaders_channel
101
101
  tl channels show 12345
102
102
  tl channels show "Economics Explained"
103
103
 
104
- # Find similar channels (vector recommender, 50 credits, Intelligence plan).
104
+ # Find similar channels (recommender, 25 credits, Intelligence plan).
105
105
  # msn: is tri-state (default msn:yes): yes = MSN only, no = non-MSN only, both = no filter.
106
106
  # tpp: is tri-state (default tpp:both): yes = TPP only, no = non-TPP only, both = no filter.
107
107
  # Same ID-or-name resolution rules as `channels show`.
108
108
  tl channels similar 12345 --limit 10
109
109
  tl channels similar "Tremending girls" min-score:0.85 --limit 5
110
110
 
111
- # Vector recommender — discovery by category/demographic tag (Intelligence plan).
112
- # `tags` is free; `top`, `inspect-*`, and `similar-to-profile` cost 50 credits flat.
111
+ # Recommender — discovery by category/demographic tag (Intelligence plan).
112
+ # `tags` is free; `top-*`, `inspect-*`, `similar-to-profile`, and `similar-brands-to-channel` cost 25 credits flat.
113
113
  tl recommender tags # List every tag (free)
114
114
  tl recommender tags cooking # Search tag names by substring
115
115
  tl recommender top-channels "Cooking" msn:yes --limit 50 # Top channels for a tag
116
116
  tl recommender top-profiles "Cooking" mbn:yes --limit 30 # Top brand profiles (one brand → potentially multiple profiles)
117
117
  tl recommender top-brands "Cooking" --limit 30 # Top brands (deduped from profiles)
118
118
  tl recommender inspect-channel 12345 # Per-tag breakdown of a channel's vector
119
- tl recommender inspect-brand Nike # Per-tag breakdown of a brand's ideal vector
119
+ tl recommender inspect-brand Nike # Per-tag breakdown of a brand's ideal profile
120
120
  tl recommender similar-to-profile 842 # Channels closest to a brand profile
121
121
 
122
122
  # Brand intelligence
@@ -56,7 +56,7 @@ tl uploads show 1174310:0BehkmVa7ak
56
56
 
57
57
  # Search channels via raw SQL — `tl db pg` against thoughtleaders_channel
58
58
  # (run `tl schema pg` once to confirm the live column set).
59
- # NOTE: For topic / category discovery, prefer the vector recommender over
59
+ # NOTE: For topic / category discovery, prefer the recommender over
60
60
  # `content_category` equality — `tl recommender top-channels "<tag>"`
61
61
  # returns channels ranked by how strongly they load on the topic, not just
62
62
  # rows where the single category code matches exactly.
@@ -74,22 +74,22 @@ tl db pg "SELECT id, channel_name FROM thoughtleaders_channel
74
74
  tl channels show 12345
75
75
  tl channels show "Economics Explained"
76
76
 
77
- # Find similar channels (vector recommender, 50 credits, Intelligence plan).
77
+ # Find similar channels (recommender, 25 credits, Intelligence plan).
78
78
  # msn: is tri-state (default msn:yes): yes = MSN only, no = non-MSN only, both = no filter.
79
79
  # tpp: is tri-state (default tpp:both): yes = TPP only, no = non-TPP only, both = no filter.
80
80
  # Same ID-or-name resolution rules as `channels show`.
81
81
  tl channels similar 12345 --limit 10
82
82
  tl channels similar "Tremending girls" min-score:0.85 --limit 5
83
83
 
84
- # Vector recommender — discovery by category/demographic tag (Intelligence plan).
85
- # `tags` is free; `top`, `inspect-*`, and `similar-to-profile` cost 50 credits flat.
84
+ # Recommender — discovery by category/demographic tag (Intelligence plan).
85
+ # `tags` is free; `top-*`, `inspect-*`, `similar-to-profile`, and `similar-brands-to-channel` cost 25 credits flat.
86
86
  tl recommender tags # List every tag (free)
87
87
  tl recommender tags cooking # Search tag names by substring
88
88
  tl recommender top-channels "Cooking" msn:yes --limit 50 # Top channels for a tag
89
89
  tl recommender top-profiles "Cooking" mbn:yes --limit 30 # Top brand profiles (one brand → potentially multiple profiles)
90
90
  tl recommender top-brands "Cooking" --limit 30 # Top brands (deduped from profiles)
91
91
  tl recommender inspect-channel 12345 # Per-tag breakdown of a channel's vector
92
- tl recommender inspect-brand Nike # Per-tag breakdown of a brand's ideal vector
92
+ tl recommender inspect-brand Nike # Per-tag breakdown of a brand's ideal profile
93
93
  tl recommender similar-to-profile 842 # Channels closest to a brand profile
94
94
 
95
95
  # Brand intelligence
@@ -16,7 +16,7 @@ For anything beyond a trivially simple lookup, write a single raw query against
16
16
  - **Elasticsearch (`tl db es`)** — transcript / brand-mention text search, video-level aggregations, demographic country-share filters that compose with content predicates.
17
17
  - **Firebolt (`tl db fb`)** — custom time-series shapes (multi-channel growth comparisons, milestone-age slices). For default shapes, prefer `tl snapshots`.
18
18
 
19
- Reserve structured commands for: single-record `show` by ID, plain filtered `list` with one or two filters that the structured vocabulary already supports, `tl channels similar` / `tl brands similar` (vector KNN), `tl reports run`, and `tl snapshots`.
19
+ Reserve structured commands for: single-record `show` by ID, plain filtered `list` with one or two filters that the structured vocabulary already supports, `tl channels similar` / `tl brands similar` (similarity search), `tl reports run`, and `tl snapshots`.
20
20
 
21
21
  One raw query beats N paginated structured walks stitched in `jq`/Python — on cost, latency, and the ES `from+size = 10000` cap.
22
22
 
@@ -69,7 +69,7 @@ Then suggest `tl comments add <id> "..."` for each.
69
69
  ### Multi-step research (mix raw + similarity)
70
70
  "Find channels similar to the ones Nike sponsors and compare their pricing"
71
71
  1. `tl db pg` to find the top channels Nike has sponsored (one aggregation, ranked).
72
- 2. `tl channels similar <top-channel-id> --json --limit 20` per seed — vector KNN is server-side and has no SQL equivalent. The `msn:` filter is tri-state with default `msn:yes` (MSN channels only); use `msn:both` to broaden, `msn:no` for non-MSN only.
72
+ 2. `tl channels similar <top-channel-id> --json --limit 20` per seed — similarity search is server-side and has no SQL equivalent. The `msn:` filter is tri-state with default `msn:yes` (MSN channels only); use `msn:both` to broaden, `msn:no` for non-MSN only.
73
73
  3. Union + dedupe + compile comparison table.
74
74
 
75
75
  ### Report comparison (saved reports)
@@ -50,10 +50,10 @@ All data commands use explicit subcommands: `list`, `show`, `create`/`add`. Runn
50
50
  | `tl uploads show <id> [<id>...]` | Show upload detail(s) by ID |
51
51
  | `tl channels show <id-or-name>` | Channel detail, including active adspots with price/cost/CPM |
52
52
  | `tl channels history <id-or-name>` | Sponsorship history (videos with detected sponsors) |
53
- | `tl channels similar <id-or-name>` | Vector-similarity recommender. 50 credits; Intelligence plan. Tri-state `msn:` (default `yes`) and `tpp:` (default `both`) filters. Ambiguous names return 400 + candidates list. Hidden `look-alike` alias. |
53
+ | `tl channels similar <id-or-name>` | Similarity recommender. 25 credits; Intelligence plan. Tri-state `msn:` (default `yes`) and `tpp:` (default `both`) filters. Ambiguous names return 400 + candidates list. Hidden `look-alike` alias. |
54
54
  | `tl brands show <brand>` | Brand intelligence report |
55
55
  | `tl brands history <brand> [--channel <id>]` | Brand sponsorship history; videos where the brand was detected |
56
- | `tl brands similar <brand>` | Find similar brands (profile vector KNN, 50 credits) |
56
+ | `tl brands similar <brand>` | Find similar brands (similarity search, 25 credits) |
57
57
  | `tl snapshots channel <id>` | Channel metrics over time (Firebolt channel_metrics) |
58
58
  | `tl snapshots video <id> --channel <id>` | Video view curve (Firebolt article_metrics, --channel required) |
59
59
  | `tl comments list <adlink-id>` | List comments on a sponsorship (free) |
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "thoughtleaders-cli"
7
- version = "0.6.5"
7
+ version = "0.6.7"
8
8
  description = "ThoughtLeaders CLI — query sponsorship data, channels, brands, and intelligence"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -20,6 +20,8 @@ Decision rule:
20
20
 
21
21
  Always run `tl describe show <resource>` before using a structured command, and `tl schema pg|fb|es` before writing a raw query.
22
22
 
23
+ **When you only need the schema of one table, you MUST call `tl schema pg <table>` (or `tl schema fb <table>`) — never the unscoped form.** The unscoped `tl schema pg` returns *every* table visible to your role, which is dozens of tables and tens of thousands of tokens; the single-table form returns only the section you need, in the same markdown layout. Reaching for the unscoped form when you already know the table name is a tokens/latency tax with zero benefit. ES has no per-table form (the index is a single document shape) — `tl schema es` is the only call there.
24
+
23
25
  **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`) into `jq`, `yq`, `rg`, or `duckdb` and read only the answer back. Pick the tool by shape:
24
26
 
25
27
  - **`jq`** — filter, project, and transform JSON. The default for `tl … --json` post-processing.
@@ -67,6 +69,7 @@ Other key concepts:
67
69
  - **Comments** — notes attached to sponsorships
68
70
  - **Adspots** — types of ads a channel carries (e.g. mention, dedicated video, product placement). Returned by `tl channels show`; each carries price/cost.
69
71
  - **MSN** (Media Selling Network) — the ~11k YouTube channels that have opted in to receive sponsorship offers. A channels is in the MSN group if the `channel.media_selling_network_join_date` field is not null.
72
+ - **MBN** (Media Buying Network) — the brand-side counterpart to MSN: brand profiles that have opted in to receive proposed sponsorships. A profile is in the MBN group if the `profile.media_buying_network_join_date` field is not null.
70
73
  - **TPP** (ThoughtLeaders Partner Program, a.k.a. "TL channels") — the smaller, exclusive ~169 channels TL manages directly. A channel is in the TPP group if the `channel.is_tl_channel` is True.
71
74
  - **`demographics_updated_at`** (on channel detail) — ISO timestamp of when demographic screenshots were last uploaded and processed via OCR. If non-null, the channel has demographics screenshots on file. If null, no screenshots have been uploaded. Use this to check whether a channel has demographics data from screenshots.
72
75
  - **`impression`** (on channels) — projected views per video on that channel. Forward-looking estimate. May be null when not yet computed.
@@ -95,11 +98,29 @@ When querying sponsorship bookings, query by `status:sold` and filter the the da
95
98
 
96
99
  Where possible, if searching for a sponsorship match between channels and brands, first search for what do similar brands sponsor / which brands is the channel usually sponsored by. The similarity judgement should be preferably based on similar topics, similar upload frequency, similar channel sizes, and only after all that, on demographics.
97
100
 
98
- Use the `tl channels similar` and `tl brands similar` commands to explore 1:1 similarity between known channels or brands. For category- or topic-driven discovery (e.g. "find me Cooking channels", "who scores high on USA share?"), use `tl recommender top-channels "<tag>"` (or `top-brands`/`top-profiles`) against the vector recommender — that's faster, ranked by category-strength. Run `tl recommender tags` to discover the valid tag names.
101
+ Use the `tl channels similar` and `tl brands similar` commands to explore 1:1 similarity between known channels or brands. For category- or topic-driven discovery (e.g. "find me Cooking channels", "who scores high on USA share?"), use `tl recommender top-channels "<tag>"` (or `top-brands`/`top-profiles`) against the recommender — that's faster, ranked by category-strength. Run `tl recommender tags` to discover the valid tag names.
99
102
 
100
103
  ## Workflow
101
104
 
102
- At the start of session, always run a `tl help` command to find out which commands are available, and the `tl whoami` command to find out what you have access to.
105
+ At the start of session, always run `tl --help` to find out which command groups are available, and `tl whoami` to find out what you have access to.
106
+
107
+ ### How to discover commands and subcommands
108
+
109
+ The CLI exposes three different discovery surfaces — pick by what you actually need:
110
+
111
+ | You want to know… | Run |
112
+ |---|---|
113
+ | Top-level command groups (`sponsorships`, `channels`, `db`, `recommender`, etc.) | `tl --help` |
114
+ | Subcommands of a group (`tl recommender` → `tags`, `top-channels`, `inspect-brand`, …) | `tl <group> --help` (e.g. `tl recommender --help`, `tl db --help`) |
115
+ | Arguments and flags for a specific leaf command | `tl <group> <subcommand> --help` (e.g. `tl recommender top-channels --help`) |
116
+ | Fields, filters, credit rates for a **data resource** (sponsorships, uploads, snapshots, reports, comments, recommender) | `tl describe show <resource> --json` |
117
+ | The live PG/ES/Firebolt schema for raw `tl db` queries | `tl schema pg` / `tl schema es` / `tl schema fb` |
118
+ | The schema of a **single** PG / Firebolt table | **`tl schema pg <table>`** / **`tl schema fb <table>`** — strongly preferred when you only need one |
119
+
120
+ Notes:
121
+ - Use `--help` everywhere — there is no separate `tl help` command. `tl help` returns "No such command 'help'".
122
+ - **`tl describe show channels`** and **`tl describe show brands`** intentionally do not list fields/filters — channel and brand search live in raw SQL (`tl db pg`) and the recommender, not in a structured list endpoint. They print a notice steering you there.
123
+ - `--help` describes **CLI shape**; `tl describe` describes **data shape**. They don't overlap.
103
124
 
104
125
  Unless the user specifically asks for running a specific report or showing the result of a specific report, find the data by using other, low-level commands.
105
126
 
@@ -132,18 +153,20 @@ tl uploads list [filters...] # Video uploads from ES — list curve, m
132
153
  tl uploads show <id> # Upload detail (2 credits)
133
154
  tl channels show <id-or-name> # Channel detail (2 credits; accepts numeric ID or name) — for channel search use raw SQL on thoughtleaders_channel
134
155
  tl channels history <id-or-name> # Sponsorship history (5 credits/result, linear)
135
- tl channels similar <id-or-name> # Vector-similarity recommender (50 credits flat; Intelligence plan)
156
+ tl channels similar <id-or-name> # Similarity recommender (25 credits flat; Intelligence plan)
136
157
  tl brands show <id-or-name> # Brand detail (1 credit)
137
158
  tl brands history <id-or-name> # Sponsorship history (5 credits/result, linear)
138
159
  tl brands history <query> --channel <id> # Brand mentions on specific channel
139
- tl brands similar <id-or-name> # Find similar brands via profile vector KNN (50 credits flat)
140
- tl recommender tags [query] # List vector tag names categories, demographics, formats (free)
141
- tl recommender top-channels "<tag>" # Top channels loaded on a vector tag (50 credits; Intelligence)
142
- tl recommender top-profiles "<tag>" # Top brand profiles loaded on a vector tag (50 credits)
143
- tl recommender top-brands "<tag>" # Top brands (deduped from profiles) loaded on a vector tag (50 credits)
144
- tl recommender inspect-channel <ref> # Show a channel's feature-vector breakdown (50 credits; Intelligence)
145
- tl recommender inspect-brand <ref> # Show a brand profile's ideal-vector breakdown (50 credits; Intelligence)
146
- tl recommender similar-to-profile <id> # Channels closest to a brand profile's ideal vector (50 credits; Intelligence)
160
+ tl brands history-stats <id-or-name> # Aggregate roll-up: counts, total/avg/median views, first/last seen, by-year, top channels (5 credits flat)
161
+ tl brands history-stats <q> --channel <id> # Same roll-up, narrowed to one channel
162
+ tl brands similar <id-or-name> # Find similar brands via similarity search (25 credits flat)
163
+ tl recommender tags [query] # List similarity tag names categories, demographics, formats (free)
164
+ tl recommender top-channels "<tag>" # Top channels loaded on a similarity tag (25 credits; Intelligence)
165
+ tl recommender top-profiles "<tag>" # Top brand profiles loaded on a similarity tag (25 credits)
166
+ tl recommender top-brands "<tag>" # Top brands (deduped from profiles) loaded on a similarity tag (25 credits)
167
+ tl recommender inspect-channel <ref> # Show a channel's similarity-profile breakdown (25 credits; Intelligence)
168
+ tl recommender inspect-brand <ref> # Show a brand profile's ideal similarity-profile breakdown (25 credits; Intelligence)
169
+ tl recommender similar-to-profile <id> # Channels closest to a brand profile's ideal profile (25 credits; Intelligence)
147
170
  tl snapshots channel <id> # Channel metrics over time — list curve, mult 1.2 (Firebolt-backed)
148
171
  tl snapshots video <id> --channel <id> # Video view curve — list curve, mult 1.2 (--channel required!)
149
172
  tl reports # List saved reports — list curve, mult 1.3
@@ -196,7 +219,7 @@ Structured commands are still the right tool for: single-record `show` by ID, pl
196
219
  | Custom Firebolt shape (milestone-age slices, multi-channel growth comparisons) | **`tl db fb`** |
197
220
  | Single-record detail lookup by ID | `tl <resource> show <id>` |
198
221
  | Plain filtered list with one or two simple filters | `tl <resource> list` |
199
- | Channel/brand similarity (vector KNN, server-implemented) | `tl channels similar`, `tl brands similar` |
222
+ | Channel/brand similarity (server-implemented similarity search) | `tl channels similar`, `tl brands similar` |
200
223
  | Saved reports | `tl reports`, `tl reports run` |
201
224
  | Time-series view-curve / channel growth (default shape with interpolation) | `tl snapshots channel`, `tl snapshots video` |
202
225
 
@@ -289,7 +312,7 @@ See [references/business-glossary.md](references/business-glossary.md) for reven
289
312
  | **AdLink INSERT** with custom price/cost/owner/`weighted_price`/`created_where` | **Unavailable** — `tl sponsorships create` exists but only creates a free *proposal* between a channel and a brand. The `tl db pg` sanitizer accepts SELECT only — no INSERT/UPDATE. | Done in the app or by a human with DB access. |
290
313
  | Pre-insert validation queries (joining `adspot ↔ channel ↔ profile ↔ org` to confirm MSN, integration=1, persona, plan) | **Available** via `tl db pg`. | One SELECT joining the four tables. Use `thoughtleaders_channel.media_selling_network_join_date IS NOT NULL` for MSN, `thoughtleaders_adspot.integration = 1` for mention adspots, `thoughtleaders_profile.persona` for the persona code (see persona constants in `references/postgres-schema.md`). |
291
314
  | Firebolt cross-table or join queries; filtering on non-indexed columns in WHERE | **Unavailable** — not accepted. | Fetch a wider slice keyed on `channel_id` (and optionally `id`), filter the rest in `jq`/Python. |
292
- | ES `query_string`, `regexp`, `wildcard`, `fuzzy`, `more_like_this`, parent/child joins; any `script_*`; multiple aggregations in one body | **Unavailable** — not accepted. | Rewrite using `term`/`terms`/`match`/`bool`/`nested`. For multi-agg dashboards, run multiple `tl db es` calls and combine client-side. For "similar"-style queries, try `tl channels similar` / `tl brands similar` (vector KNN, server-implemented). |
315
+ | ES `query_string`, `regexp`, `wildcard`, `fuzzy`, `more_like_this`, parent/child joins; any `script_*`; multiple aggregations in one body | **Unavailable** — not accepted. | Rewrite using `term`/`terms`/`match`/`bool`/`nested`. For multi-agg dashboards, run multiple `tl db es` calls and combine client-side. For "similar"-style queries, try `tl channels similar` / `tl brands similar` (server-implemented similarity search). |
293
316
  | ES deep pagination beyond `from+size = 10,000` | **Unavailable** via raw — `scroll` and `pit` aren't allowlisted; `search_after` is allowed but `from` is still capped. | Use `search_after` with `sort` to walk past 10k. For huge sweeps, narrow with `publication_date` ranges. |
294
317
  | ES index introspection (`_cat/indices`, mappings) | **Unavailable** — only `_search` is wired. | Read [references/elasticsearch-schema.md](references/elasticsearch-schema.md). It's manually maintained — update it when you discover new fields. |
295
318
  | Schema introspection on Postgres (`information_schema.columns`, `pg_class`, …) | **Partial** — catalog-resolving casts and many `pg_*` helpers are blocked. | Use `tl schema pg` for the live table/column listing, or read [references/postgres-schema.md](references/postgres-schema.md). |
@@ -300,8 +323,10 @@ If a user asks for one of the **Unavailable** items, say so explicitly and propo
300
323
  ```bash
301
324
  tl describe # List all resources with credit costs (free)
302
325
  tl describe show <resource> --json # Fields, filters, credit rates (free)
303
- tl schema pg # PostgreSQL schema reference for `tl db pg` (free)
304
- tl schema fb # Live Firebolt tables and column types for `tl db fb` (free)
326
+ tl schema pg # PostgreSQL schema reference for `tl db pg` (free) — every visible table
327
+ tl schema pg <table> # PostgreSQL schema for a SINGLE table (free) same markdown shape
328
+ tl schema fb # Live Firebolt tables and column types for `tl db fb` (free) — both tables
329
+ tl schema fb <table> # Firebolt schema for a SINGLE table (free) — `article_metrics` or `channel_metrics`
305
330
  tl schema es # Elasticsearch document shape for `tl db es` (free)
306
331
  tl balance --json # Credit balance (free)
307
332
  tl whoami # Current user, org, brands (free)
@@ -325,14 +350,14 @@ Date filters accept keywords: `today`, `yesterday`, `tomorrow`.
325
350
 
326
351
  #### Channel discovery — recommender first, raw SQL second
327
352
 
328
- For category- or demographic-driven discovery, **use the vector recommender, not `content_category` SQL.** The recommender ranks channels by how strongly they load on a category/demographic tag (cosine-style scores), instead of forcing exact equality on a single integer code. It also returns the matching brand profiles alongside the channels — useful when the user actually wants to know "who buys this kind of inventory."
353
+ For category- or demographic-driven discovery, **use the recommender, not `content_category` SQL.** The recommender ranks channels by how strongly they load on a category/demographic tag (similarity scores), instead of forcing exact equality on a single integer code. It also returns the matching brand profiles alongside the channels — useful when the user actually wants to know "who buys this kind of inventory."
329
354
 
330
355
  ```bash
331
356
  # Discover the right tag name first (free)
332
357
  tl recommender tags cooking
333
358
  tl recommender tags "usa"
334
359
 
335
- # Top channels & profiles loaded on a vector tag (50 credits; Intelligence)
360
+ # Top channels & profiles loaded on a similarity tag (25 credits; Intelligence)
336
361
  tl recommender top-channels "Cooking" msn:yes --limit 50
337
362
  tl recommender top-channels "Tech" --limit 30
338
363
  tl recommender top-brands "USA share" mbn:yes --limit 50
@@ -434,7 +459,7 @@ tl reports run 42 --json
434
459
 
435
460
  "Find Cooking channels with US-heavy mobile audiences":
436
461
  ```bash
437
- # Use the vector recommender for the topic, then narrow with structured filters / SQL on the IDs.
462
+ # Use the recommender for the topic, then narrow with structured filters / SQL on the IDs.
438
463
  tl recommender top-channels "Cooking" msn:yes --limit 100 --json \
439
464
  | jq -r '.results[].channel_id' \
440
465
  | paste -sd, - \
@@ -452,7 +477,7 @@ tl recommender top-channels "Cooking" msn:yes --limit 100 --json \
452
477
  tl sponsorships list status:sold primary-device:mobile min-us-share:60 --json
453
478
  ```
454
479
 
455
- "Find channels similar to one I know" (vector-similarity recommender, 50 credits per call):
480
+ "Find channels similar to one I know" (similarity recommender, 25 credits per call):
456
481
  ```bash
457
482
  tl channels similar 29834 --limit 10 # by ID (defaults to msn:yes, tpp:both)
458
483
  tl channels similar "Tremending girls" --limit 5 # by unique name
@@ -464,16 +489,16 @@ tl channels similar 29834 min-subs:1000000 exclude:477487 --limit 15 # client-s
464
489
  ```
465
490
  **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.
466
491
 
467
- "Browse the vector recommender" (categories, demographics, formats — `tl recommender tags` is free):
492
+ "Browse the recommender" (categories, demographics, formats — `tl recommender tags` is free):
468
493
  ```bash
469
494
  tl recommender tags # Full tag list (free)
470
495
  tl recommender tags cooking # Search tag names by substring
471
- tl recommender top-channels "Cooking" msn:yes --limit 50 # Top channels loaded on a tag (50 credits)
496
+ tl recommender top-channels "Cooking" msn:yes --limit 50 # Top channels loaded on a tag (25 credits)
472
497
  tl recommender top-profiles "Cooking" --limit 30 # Top brand profiles for the tag
473
498
  tl recommender top-brands "USA share" mbn:yes --limit 30 # Top brands (deduped) — demographic tag, MBN only
474
499
  tl recommender top-channels "Tech" exclude-for-profile:842 # Drop channels already proposed for profile 842
475
500
  tl recommender inspect-channel 29834 # Per-tag breakdown of a channel's vector
476
- tl recommender inspect-brand Nike # Per-tag breakdown of a brand's ideal vector
477
- tl recommender similar-to-profile 842 --limit 30 # Channels closest to a brand profile's ideal vector
501
+ tl recommender inspect-brand Nike # Per-tag breakdown of a brand's ideal profile
502
+ tl recommender similar-to-profile 842 --limit 30 # Channels closest to a brand profile's ideal profile
478
503
  ```
479
504
  Use `tl recommender top` for category/topic discovery (it's ranked) and `tl channels similar` / `tl brands similar` for 1:1 lookalike searches.
@@ -158,11 +158,9 @@ The full table below applies to **channel parent docs only**:
158
158
  ### Other indices
159
159
 
160
160
  - `tl-ingest` — ingestion queue. **Don't query.** Internal pipeline state.
161
- - `tl-feature-vectors-channel`, `tl-feature-vectors-channel-profile` — channel similarity vectors.
161
+ - `tl-similarity-profiles-channel`, `tl-similarity-profiles-channel-profile` — channel similarity vectors.
162
162
  - `tl-vectors-brand-company-descriptions-*` — brand similarity vectors.
163
- - `tl-vectors-channel-audience-*`, `tl-vectors-channel-topic-descriptions-*`, `tl-vectors-channel-features` — channel feature vectors.
164
-
165
- Note: `knn` queries against vector indices are **not currently accepted** as a top-level key. For "find similar" results, use `tl channels similar` / `tl brands similar` — they wrap the vector search server-side.
163
+ - `tl-vectors-channel-audience-*`, `tl-vectors-channel-topic-descriptions-*`, `tl-vectors-channel-features` — channel similarity profiles.
166
164
 
167
165
  ## Common Query Patterns
168
166
 
@@ -116,7 +116,7 @@ Every Firebolt workflow has two steps:
116
116
  **Step 1 — get `channel_id` and (optionally) video IDs from PG/ES.**
117
117
 
118
118
  ```bash
119
- # Channels matching some category (vector recommender — preferred over content_category equality)
119
+ # Channels matching some category (recommender — preferred over content_category equality)
120
120
  tl recommender top-channels "Tech" msn:yes --limit 50 --json \
121
121
  | jq '.results[].channel_id'
122
122
 
@@ -27,6 +27,8 @@ The main deals table. Each row = one sponsorship deal between a brand and a YouT
27
27
  > - ❌ `organization_id` — there is NO direct org FK. Org is reached via `creator_profile_id → profile.organization_id → organization`.
28
28
  > - ❌ `channel_id` — channel is reached via `ad_spot_id → adspot.channel_id → channel`.
29
29
  > - ❌ `youtube_id` (on channel) — use `external_channel_id`.
30
+ > - ❌ `msn_join_date` (on channel) — use `media_selling_network_join_date`.
31
+ > - ❌ `mbn_join_date` (on profile) — use `media_buying_network_join_date`.
30
32
 
31
33
  #### Key Columns
32
34
 
@@ -1,3 +1,3 @@
1
1
  """ThoughtLeaders CLI — query sponsorship data, channels, brands, and intelligence."""
2
2
 
3
- __version__ = "0.6.5"
3
+ __version__ = "0.6.7"
@@ -118,6 +118,50 @@ def history_cmd(
118
118
  client.close()
119
119
 
120
120
 
121
+ @app.command("history-stats")
122
+ def history_stats_cmd(
123
+ query: str = typer.Argument(..., help="Brand name or numeric ID"),
124
+ channel: int | None = typer.Option(None, "--channel", "-c", help="Restrict the roll-up to a specific channel"),
125
+ top_channels: int = typer.Option(10, "--top-channels", help="How many top-by-count channels to include in the roll-up (1-50)"),
126
+ json_output: bool = typer.Option(False, "--json", help="JSON output"),
127
+ csv_output: bool = typer.Option(False, "--csv", help="CSV output"),
128
+ md_output: bool = typer.Option(False, "--md", help="Markdown output"),
129
+ toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
130
+ ) -> None:
131
+ """Aggregate roll-up of a brand's sponsorship history (no per-row output).
132
+
133
+ Same scope as `tl brands history` (videos where the brand is in
134
+ `sponsored_brand_mentions`), but returned as a single summary
135
+ record: total sponsored videos, view sums/avg/median, first/last
136
+ seen dates, per-year buckets, top channels by count, and
137
+ TL-brokered adlink counts. Computed via one ES aggregation +
138
+ one PG count — cost is flat regardless of how prolific the
139
+ brand is.
140
+
141
+ Requires an Intelligence plan. Costs 5 credits flat.
142
+
143
+ Examples:
144
+ tl brands history-stats Nike
145
+ tl brands history-stats 21416 --top-channels 25
146
+ tl brands history-stats Nike --channel 12345 --json
147
+ """
148
+ fmt = detect_format(json_output, csv_output, md_output, toon_output)
149
+
150
+ params: dict[str, str] = {"top-channels": str(top_channels)}
151
+ if channel is not None:
152
+ params["channel_id"] = str(channel)
153
+
154
+ encoded_query = urllib.parse.quote(query, safe="")
155
+ client = get_client()
156
+ try:
157
+ data = client.get(f"/brands/{encoded_query}/history-stats", params=params)
158
+ output_single(data, fmt)
159
+ except ApiError as e:
160
+ handle_api_error(e)
161
+ finally:
162
+ client.close()
163
+
164
+
121
165
  SIMILAR_COLUMNS = ["score", "brand_id", "brand_name", "website", "mbn"]
122
166
  SIMILAR_COLUMN_CONFIG = {
123
167
  "score": {"justify": "right"},
@@ -125,7 +169,7 @@ SIMILAR_COLUMN_CONFIG = {
125
169
 
126
170
 
127
171
  def _format_score(results: list[dict]) -> list[dict]:
128
- """Convert raw cosine score (0.0-1.0) to percentage string."""
172
+ """Convert raw similarity score (0.0-1.0) to percentage string."""
129
173
  for row in results:
130
174
  score = row.get("score")
131
175
  if isinstance(score, (int, float)):
@@ -144,7 +188,7 @@ def similar_cmd(
144
188
  ) -> None:
145
189
  """Find brands similar to a given one (by ID or name).
146
190
 
147
- Costs 50 credits per call. Intelligence plan required.
191
+ Costs 25 credits per call. Intelligence plan required.
148
192
 
149
193
  Examples:
150
194
  tl brands similar Nike
@@ -93,7 +93,7 @@ def _handle_channel_api_error(e: ApiError) -> None:
93
93
 
94
94
 
95
95
  def _format_score(results: list[dict]) -> list[dict]:
96
- """Convert raw cosine score (0.0-1.0) to percentage string for table/csv/md."""
96
+ """Convert raw similarity score (0.0-1.0) to percentage string for table/csv/md."""
97
97
  for row in results:
98
98
  score = row.get("score")
99
99
  if isinstance(score, (int, float)):
@@ -171,14 +171,14 @@ def similar_cmd(
171
171
  ) -> None:
172
172
  """Find channels similar to a given one (by id or name).
173
173
 
174
- Costs 50 credits per call. Intelligence plan required. Results are
175
- ranked by cosine similarity and enriched with subscribers, impression,
174
+ Costs 25 credits per call. Intelligence plan required. Results are
175
+ ranked by similarity and enriched with subscribers, impression,
176
176
  total_views, category, and the channel's representative CPM.
177
177
 
178
178
  Server-side filters (pushed to the recommender):
179
179
  language:<iso> Restrict to a content language (default: en)
180
180
  msn:<true|false> Restrict to Media Selling Network (default: true)
181
- min-score:<0-1> Minimum cosine similarity (default: 0.5)
181
+ min-score:<0-1> Minimum similarity (default: 0.5)
182
182
 
183
183
  Client-side post-filters (applied after fetch):
184
184
  category:<code> Keep only rows matching this content_category
@@ -1,10 +1,11 @@
1
- """tl recommender — Vector-recommender introspection and discovery.
1
+ """tl recommender — Recommender introspection and discovery.
2
2
 
3
- Surfaces the channel/profile feature-vector machinery that powers the
4
- "Recommender Insights" web view: list the vector tags (categories,
5
- demographics, formats, etc.), find the top channels and profiles loaded
6
- on a given tag, inspect a single channel or brand vector, or fetch
7
- channels similar to a brand profile's ideal vector.
3
+ Surfaces the channel/profile similarity machinery that powers the
4
+ "Recommender Insights" web view: list the similarity tags (categories,
5
+ demographics, formats, etc.), find the top channels and profiles
6
+ scoring high on a given tag, inspect a single channel or brand
7
+ similarity profile, fetch channels similar to a brand's ideal profile,
8
+ or fetch brands likely to sponsor a given channel.
8
9
 
9
10
  For 1:1 similarity use `tl channels similar` and `tl brands similar`.
10
11
  """
@@ -19,7 +20,7 @@ from tl_cli.client.http import get_client
19
20
  from tl_cli.filters import parse_filters
20
21
  from tl_cli.output.formatter import detect_format, output, output_single
21
22
 
22
- app = typer.Typer(help="Vector recommender (tags, top-channels/profiles/brands, vector inspection, profile→channel similarity)")
23
+ app = typer.Typer(help="Recommender (similarity tags, top-channels/profiles/brands, similarity-profile inspection, profile→channel and channel→brand similarity)")
23
24
 
24
25
 
25
26
  TOP_CHANNEL_COLUMNS = ["value", "channel_id", "channel_name", "slug"]
@@ -46,7 +47,7 @@ def _handle_recommender_error(e: ApiError) -> None:
46
47
 
47
48
  @app.callback(invoke_without_command=True)
48
49
  def recommender(ctx: typer.Context) -> None:
49
- """Vector recommender."""
50
+ """Recommender."""
50
51
  if ctx.invoked_subcommand is None:
51
52
  typer.echo(ctx.get_help())
52
53
 
@@ -59,10 +60,10 @@ def tags_cmd(
59
60
  md_output: bool = typer.Option(False, "--md", help="Markdown output"),
60
61
  toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
61
62
  ) -> None:
62
- """List vector tag names (free).
63
+ """List similarity tag names (free).
63
64
 
64
65
  Use this to discover the tag names accepted by `tl recommender top`.
65
- Each tag is one dimension of a channel/profile feature vector
66
+ Each tag is one signal in a channel or brand similarity profile
66
67
  e.g. content categories like "Cooking", demographic buckets like
67
68
  "Age 18-24", device shares, country shares.
68
69
 
@@ -81,7 +82,7 @@ def tags_cmd(
81
82
  data,
82
83
  fmt,
83
84
  columns=["group", "field_name"],
84
- title="Recommender vector tags",
85
+ title="Similarity tags",
85
86
  )
86
87
  except ApiError as e:
87
88
  handle_api_error(e)
@@ -127,7 +128,7 @@ def _do_top(kind: str, tag: str, args: list[str], fmt: str, limit: int, columns:
127
128
 
128
129
  @app.command("top-channels")
129
130
  def top_channels_cmd(
130
- tag: str = typer.Argument(..., help='Vector tag name (e.g. "Cooking", "Age 18-24"). Run `tl recommender tags` to discover valid names.'),
131
+ tag: str = typer.Argument(..., help='Similarity tag name (e.g. "Cooking", "Age 18-24"). Run `tl recommender tags` to discover valid names.'),
131
132
  args: list[str] = typer.Argument(None, help="Filters (key:value pairs)."),
132
133
  json_output: bool = typer.Option(False, "--json", help="JSON output"),
133
134
  csv_output: bool = typer.Option(False, "--csv", help="CSV output"),
@@ -135,9 +136,9 @@ def top_channels_cmd(
135
136
  toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
136
137
  limit: int = typer.Option(50, "--limit", "-l", help="Max results (1-100)"),
137
138
  ) -> None:
138
- """Top channels loaded on a single vector tag.
139
+ """Top channels scoring high on a single similarity tag.
139
140
 
140
- Costs 50 credits per call. Intelligence plan required.
141
+ Costs 25 credits per call. Intelligence plan required.
141
142
 
142
143
  Filters:
143
144
  msn:<yes|no|all> MSN membership (default: all)
@@ -154,7 +155,7 @@ def top_channels_cmd(
154
155
 
155
156
  @app.command("top-profiles")
156
157
  def top_profiles_cmd(
157
- tag: str = typer.Argument(..., help='Vector tag name (e.g. "Cooking", "Age 18-24"). Run `tl recommender tags` to discover valid names.'),
158
+ tag: str = typer.Argument(..., help='Similarity tag name (e.g. "Cooking", "Age 18-24"). Run `tl recommender tags` to discover valid names.'),
158
159
  args: list[str] = typer.Argument(None, help="Filters (key:value pairs)."),
159
160
  json_output: bool = typer.Option(False, "--json", help="JSON output"),
160
161
  csv_output: bool = typer.Option(False, "--csv", help="CSV output"),
@@ -162,9 +163,9 @@ def top_profiles_cmd(
162
163
  toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
163
164
  limit: int = typer.Option(50, "--limit", "-l", help="Max results (1-100)"),
164
165
  ) -> None:
165
- """Top brand profiles loaded on a single vector tag.
166
+ """Top brand profiles scoring high on a single similarity tag.
166
167
 
167
- Costs 50 credits per call. Intelligence plan required. Profiles can
168
+ Costs 25 credits per call. Intelligence plan required. Profiles can
168
169
  represent the same brand more than once (one brand → multiple
169
170
  profiles); use `top-brands` for brand-deduplicated results.
170
171
 
@@ -182,7 +183,7 @@ def top_profiles_cmd(
182
183
 
183
184
  @app.command("top-brands")
184
185
  def top_brands_cmd(
185
- tag: str = typer.Argument(..., help='Vector tag name (e.g. "Cooking", "Age 18-24"). Run `tl recommender tags` to discover valid names.'),
186
+ tag: str = typer.Argument(..., help='Similarity tag name (e.g. "Cooking", "Age 18-24"). Run `tl recommender tags` to discover valid names.'),
186
187
  args: list[str] = typer.Argument(None, help="Filters (key:value pairs)."),
187
188
  json_output: bool = typer.Option(False, "--json", help="JSON output"),
188
189
  csv_output: bool = typer.Option(False, "--csv", help="CSV output"),
@@ -190,9 +191,9 @@ def top_brands_cmd(
190
191
  toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
191
192
  limit: int = typer.Option(50, "--limit", "-l", help="Max results (1-100)"),
192
193
  ) -> None:
193
- """Top brands loaded on a single vector tag (deduplicated from profiles).
194
+ """Top brands scoring high on a single similarity tag (deduplicated from profiles).
194
195
 
195
- Costs 50 credits per call. Intelligence plan required. Server-side
196
+ Costs 25 credits per call. Intelligence plan required. Server-side
196
197
  aggregates the underlying profile rows by brand, keeping the
197
198
  highest-scoring profile per brand.
198
199
 
@@ -216,10 +217,10 @@ def inspect_channel_cmd(
216
217
  md_output: bool = typer.Option(False, "--md", help="Markdown output"),
217
218
  toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
218
219
  ) -> None:
219
- """Show a channel's feature vector grouped by category.
220
+ """Show a channel's similarity profile grouped by category.
220
221
 
221
- Costs 50 credits per call. Intelligence plan required. Returns the
222
- grouped sparse vector (active dimensions only) and the magnitude.
222
+ Costs 25 credits per call. Intelligence plan required. Returns the
223
+ active similarity tags grouped by category, plus the overall strength.
223
224
 
224
225
  Examples:
225
226
  tl recommender inspect-channel 12345
@@ -245,11 +246,11 @@ def inspect_brand_cmd(
245
246
  md_output: bool = typer.Option(False, "--md", help="Markdown output"),
246
247
  toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
247
248
  ) -> None:
248
- """Show a brand profile's ideal feature vector grouped by category.
249
+ """Show a brand profile's ideal similarity profile grouped by category.
249
250
 
250
- Costs 50 credits per call. Intelligence plan required. Resolves the
251
+ Costs 25 credits per call. Intelligence plan required. Resolves the
251
252
  brand to its (preferred MBN) profile and inspects that profile's
252
- aggregated vector.
253
+ aggregated similarity tags.
253
254
 
254
255
  Examples:
255
256
  tl recommender inspect-brand 287
@@ -277,9 +278,9 @@ def similar_to_profile_cmd(
277
278
  toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
278
279
  limit: int = typer.Option(20, "--limit", "-l", help="Max results (1-100)"),
279
280
  ) -> None:
280
- """Channels closest to a brand profile's ideal vector.
281
+ """Channels closest to a brand profile's ideal similarity profile.
281
282
 
282
- Costs 50 credits per call. Intelligence plan required. Channels the
283
+ Costs 25 credits per call. Intelligence plan required. Channels the
283
284
  brand has already worked with or been proposed are excluded.
284
285
 
285
286
  Filters:
@@ -312,3 +313,51 @@ def similar_to_profile_cmd(
312
313
  handle_api_error(e)
313
314
  finally:
314
315
  client.close()
316
+
317
+
318
+ @app.command("similar-brands-to-channel")
319
+ def similar_brands_to_channel_cmd(
320
+ channel_ref: str = typer.Argument(..., help="Channel ID (numeric) or name (partial match, must be unique)"),
321
+ args: list[str] = typer.Argument(None, help="Filters (key:value pairs)."),
322
+ json_output: bool = typer.Option(False, "--json", help="JSON output"),
323
+ csv_output: bool = typer.Option(False, "--csv", help="CSV output"),
324
+ md_output: bool = typer.Option(False, "--md", help="Markdown output"),
325
+ toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
326
+ limit: int = typer.Option(20, "--limit", "-l", help="Max results (1-100)"),
327
+ ) -> None:
328
+ """Brands most likely to sponsor a given channel.
329
+
330
+ Compares the channel's similarity profile against brand similarity
331
+ profiles and dedupes the results by brand. Costs 25 credits per call.
332
+ Intelligence plan required.
333
+
334
+ Filters:
335
+ mbn:<yes|no|all> MBN membership of the underlying profile (default: all)
336
+
337
+ Examples:
338
+ tl recommender similar-brands-to-channel 12345
339
+ tl recommender similar-brands-to-channel "MrBeast" mbn:yes --limit 30
340
+ """
341
+ fmt = detect_format(json_output, csv_output, md_output, toon_output)
342
+ filters = parse_filters(args or [])
343
+ params = {k: v for k, v in filters.items() if k in {"mbn"}}
344
+ params["limit"] = str(limit)
345
+ encoded = urllib.parse.quote(channel_ref, safe="")
346
+ client = get_client()
347
+ try:
348
+ data = client.get(f"/recommender/channels/{encoded}/similar-brands", params=params)
349
+ for r in data.get("results", []):
350
+ score = r.get("score")
351
+ if isinstance(score, (int, float)) and fmt in ("table", "md"):
352
+ r["score"] = f"{score * 100:.1f}%"
353
+ output(
354
+ data,
355
+ fmt,
356
+ columns=["score", "brand_id", "brand_name", "website", "mbn", "profile_id"],
357
+ title=f"Brands likely to sponsor channel {channel_ref}",
358
+ column_config={"score": {"justify": "right"}},
359
+ )
360
+ except ApiError as e:
361
+ _handle_recommender_error(e)
362
+ finally:
363
+ client.close()
@@ -0,0 +1,83 @@
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, table: str | None = None) -> None:
17
+ client = get_client()
18
+ try:
19
+ params = {"table": table} if table else {}
20
+ data = client.get(f"/raw/{db}/schema", params=params)
21
+ if json_output:
22
+ print(json.dumps(data, indent=2, default=str))
23
+ return
24
+ content = data.get("content", "")
25
+ if console.is_terminal:
26
+ console.print(Markdown(content))
27
+ else:
28
+ print(content)
29
+ except ApiError as e:
30
+ handle_api_error(e)
31
+ finally:
32
+ client.close()
33
+
34
+
35
+ @app.command("pg")
36
+ def pg_cmd(
37
+ table: str = typer.Argument(None, help="Optional table name. When given, prints only that table's section in the same markdown format."),
38
+ json_output: bool = typer.Option(False, "--json", help="JSON output"),
39
+ ) -> None:
40
+ """Show PostgreSQL schema reference (for `tl db pg`).
41
+
42
+ With no argument: lists every table visible to your role.
43
+ With a table name: prints only that table's column listing.
44
+
45
+ **Strongly preferred for single-table lookups.** Listing every
46
+ table just to read one is wasteful — pass the table name and the
47
+ server returns only that section.
48
+
49
+ Examples:
50
+ tl schema pg
51
+ tl schema pg thoughtleaders_channel
52
+ tl schema pg thoughtleaders_adlink --json
53
+ """
54
+ _show("pg", json_output, table=table)
55
+
56
+
57
+ @app.command("fb")
58
+ def fb_cmd(
59
+ table: str = typer.Argument(None, help="Optional table name (`article_metrics` or `channel_metrics`). When given, prints only that table's section."),
60
+ json_output: bool = typer.Option(False, "--json", help="JSON output"),
61
+ ) -> None:
62
+ """Show Firebolt schema (live: tables and column types) for `tl db fb`.
63
+
64
+ With no argument: lists both accepted tables.
65
+ With a table name: prints only that table's columns + primary index.
66
+
67
+ **Strongly preferred for single-table lookups.** Pass the table
68
+ name to skip the other one.
69
+
70
+ Examples:
71
+ tl schema fb
72
+ tl schema fb article_metrics
73
+ tl schema fb channel_metrics --json
74
+ """
75
+ _show("fb", json_output, table=table)
76
+
77
+
78
+ @app.command("es")
79
+ def es_cmd(
80
+ json_output: bool = typer.Option(False, "--json", help="JSON output"),
81
+ ) -> None:
82
+ """Show Elasticsearch document shape for `tl db es`."""
83
+ _show("es", json_output)
@@ -3,16 +3,11 @@
3
3
  Query sponsorship data, channels, brands, and intelligence.
4
4
  """
5
5
 
6
- import re
7
6
  import sys
8
7
  import traceback
9
- from pathlib import Path
10
- from typing import Optional
11
8
 
12
- import click
13
9
  import typer
14
10
  from rich.console import Console
15
- from rich.markdown import Markdown
16
11
 
17
12
  from tl_cli import __version__
18
13
  from tl_cli import config as tl_config
@@ -128,61 +123,6 @@ def update_command() -> None:
128
123
  raise typer.Exit()
129
124
 
130
125
 
131
- def _get_terminology() -> str | None:
132
- """Extract the Terminology section from README.md.
133
-
134
- Tries to locate README.md relative to the package source first,
135
- then falls back to importlib.metadata.
136
- """
137
- try:
138
- text = None
139
- readme = Path(__file__).resolve().parent.parent.parent / "README.md"
140
- if readme.is_file():
141
- text = readme.read_text()
142
- else:
143
- from importlib.metadata import metadata
144
- text = metadata("thoughtleaders-cli").get_payload()
145
- if not text:
146
- return None
147
- match = re.search(r"^# Terminology\s*\n(.+?)(?=\n# |\Z)", text, re.DOTALL | re.MULTILINE)
148
- if not match:
149
- return None
150
- return match.group(1).strip()
151
- except Exception:
152
- return None
153
-
154
-
155
- @app.command(name="help", hidden=True)
156
- def help_command(
157
- ctx: typer.Context,
158
- command: Optional[str] = typer.Argument(None, help="Command to show help for"),
159
- ) -> None:
160
- """Show help for the CLI or a specific command."""
161
- root_ctx = ctx.parent
162
- root_cmd = root_ctx.command
163
-
164
- if command is None:
165
- click.echo(root_cmd.get_help(root_ctx))
166
- terminology = _get_terminology()
167
- if terminology:
168
- import shutil
169
- term_width = shutil.get_terminal_size().columns
170
- console = Console(width=int(term_width * 0.9))
171
- console.print(Markdown(terminology))
172
- console.print()
173
- raise typer.Exit()
174
-
175
- # Look up the subcommand
176
- sub_cmd = root_cmd.get_command(root_ctx, command)
177
- if sub_cmd is None:
178
- click.echo(f"Unknown command: {command}", err=True)
179
- raise typer.Exit(1)
180
-
181
- sub_ctx = click.Context(sub_cmd, info_name=command, parent=root_ctx)
182
- click.echo(sub_cmd.get_help(sub_ctx))
183
- raise typer.Exit()
184
-
185
-
186
126
  def cli() -> None:
187
127
  """Entry point that wraps the Typer app with top-level error handling.
188
128
 
@@ -388,7 +388,7 @@ wheels = [
388
388
 
389
389
  [[package]]
390
390
  name = "thoughtleaders-cli"
391
- version = "0.6.5"
391
+ version = "0.6.7"
392
392
  source = { editable = "." }
393
393
  dependencies = [
394
394
  { name = "authlib" },
@@ -1,55 +0,0 @@
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)