thoughtleaders-cli 0.5.2__tar.gz → 0.6.0__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 (68) hide show
  1. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/.claude-plugin/plugin.json +1 -1
  2. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/AGENTS.md +3 -1
  3. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/PKG-INFO +21 -8
  4. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/README.md +20 -7
  5. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/commands/tl.md +2 -2
  6. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/docs/architecture.md +6 -1
  7. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/pyproject.toml +1 -1
  8. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/skills/tl/SKILL.md +77 -40
  9. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/skills/tl/references/firebolt-schema.md +6 -5
  10. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/src/tl_cli/__init__.py +1 -1
  11. thoughtleaders_cli-0.6.0/src/tl_cli/commands/recommender.py +247 -0
  12. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/src/tl_cli/main.py +2 -0
  13. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/uv.lock +2 -2
  14. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/.claude-plugin/marketplace.json +0 -0
  15. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/.github/workflows/python-publish.yml +0 -0
  16. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/.gitignore +0 -0
  17. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/CLAUDE.md +0 -0
  18. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/LICENSE +0 -0
  19. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/agents/tl-analyst.md +0 -0
  20. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/commands/tl-balance.md +0 -0
  21. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/commands/tl-reports.md +0 -0
  22. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/commands/tl-sponsorships.md +0 -0
  23. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/hooks/hooks.json +0 -0
  24. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/hooks/scripts/post-usage.sh +0 -0
  25. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/hooks/scripts/pre-check.sh +0 -0
  26. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/skills/tl/references/business-glossary.md +0 -0
  27. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/skills/tl/references/elasticsearch-schema.md +0 -0
  28. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/skills/tl/references/postgres-schema.md +0 -0
  29. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/src/tl_cli/_completions.py +0 -0
  30. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/src/tl_cli/auth/__init__.py +0 -0
  31. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/src/tl_cli/auth/commands.py +0 -0
  32. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/src/tl_cli/auth/login.py +0 -0
  33. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/src/tl_cli/auth/pkce.py +0 -0
  34. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/src/tl_cli/auth/token_store.py +0 -0
  35. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/src/tl_cli/client/__init__.py +0 -0
  36. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/src/tl_cli/client/errors.py +0 -0
  37. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/src/tl_cli/client/http.py +0 -0
  38. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/src/tl_cli/commands/__init__.py +0 -0
  39. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/src/tl_cli/commands/ask.py +0 -0
  40. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/src/tl_cli/commands/balance.py +0 -0
  41. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/src/tl_cli/commands/brands.py +0 -0
  42. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/src/tl_cli/commands/changelog.py +0 -0
  43. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/src/tl_cli/commands/channels.py +0 -0
  44. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/src/tl_cli/commands/comments.py +0 -0
  45. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/src/tl_cli/commands/db.py +0 -0
  46. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/src/tl_cli/commands/deals.py +0 -0
  47. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/src/tl_cli/commands/describe.py +0 -0
  48. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/src/tl_cli/commands/doctor.py +0 -0
  49. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/src/tl_cli/commands/matches.py +0 -0
  50. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/src/tl_cli/commands/proposals.py +0 -0
  51. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/src/tl_cli/commands/reports.py +0 -0
  52. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/src/tl_cli/commands/schema.py +0 -0
  53. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/src/tl_cli/commands/setup.py +0 -0
  54. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/src/tl_cli/commands/snapshots.py +0 -0
  55. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/src/tl_cli/commands/sponsorships.py +0 -0
  56. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/src/tl_cli/commands/uploads.py +0 -0
  57. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/src/tl_cli/commands/whoami.py +0 -0
  58. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/src/tl_cli/config.py +0 -0
  59. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/src/tl_cli/filters.py +0 -0
  60. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/src/tl_cli/hints.py +0 -0
  61. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/src/tl_cli/output/__init__.py +0 -0
  62. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/src/tl_cli/output/formatter.py +0 -0
  63. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/src/tl_cli/self_update.py +0 -0
  64. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/tests/__init__.py +0 -0
  65. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/tests/test_auth.py +0 -0
  66. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/tests/test_filters.py +0 -0
  67. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/tests/test_output.py +0 -0
  68. {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.0}/tests/test_sponsorships.py +0 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tl-cli",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
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). 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, 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.
4
4
 
5
5
  # Architecture
6
6
 
@@ -20,6 +20,8 @@ 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`, `inspect-channel`, `inspect-brand`, `similar-to-profile` (all 50 credits flat, Intelligence-gated). 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 "<tag>"` rather than `WHERE content_category = <code>` SQL — the recommender is ranked, not equality-based, and returns matching brand profiles alongside channels. The underlying recommender code uses "element"/"field_name" terminology; the CLI/API layer renames these to "tag" at the boundary.
24
+
23
25
  ## Filter Parsing (`filters.py`)
24
26
 
25
27
  `parse_filters()` handles `key:value` and `key:"quoted value"` syntax. Returns `dict[str, str]` passed as query params. Date filter keys (listed in `DATE_FILTER_KEYS` — e.g. `since`, `created-at`, `created-at-start`, `publish-date-end`) accept keywords `today`, `yesterday`, `tomorrow`. Sponsorship date fields (`created-at`, `publish-date`, `purchase-date`, `send-date`) each expose three filter shapes: bare `<field>:<date>` matches within that date/period, and `<field>-start:` / `<field>-end:` give inclusive lower/upper bounds (both sides inclusive; partial dates expand to the whole period). Empty-string values result in `IS NULL` queries on the backend.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: thoughtleaders-cli
3
- Version: 0.5.2
3
+ Version: 0.6.0
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
@@ -81,13 +81,16 @@ tl uploads list q:code --csv
81
81
  # Show upload details (supports colon-containing IDs)
82
82
  tl uploads show 1174310:0BehkmVa7ak
83
83
 
84
- # Search channels. msn and tpp are tri-state filters (yes/no/both; default 'both').
85
- # msn = opted-in to receive sponsorship offers; tpp = exclusively TL-managed.
86
- # Both are also returned as boolean fields on every channel response.
87
- tl channels list category:cooking min-subs:100k
88
- tl channels list msn:yes # MSN channels only (~11k)
89
- tl channels list tpp:yes # TPP channels only (~169)
90
- tl channels list msn:no min-subs:500k # big non-MSN channels
84
+ # Search channels via raw SQL `tl db pg` against thoughtleaders_channel
85
+ # (run `tl schema pg` once to confirm the live column set).
86
+ tl db pg "SELECT id, channel_name, total_views FROM thoughtleaders_channel
87
+ WHERE content_category = <COOKING_CODE> AND total_views >= 100000
88
+ ORDER BY total_views DESC LIMIT 50 OFFSET 0"
89
+ tl db pg "SELECT id, channel_name FROM thoughtleaders_channel
90
+ WHERE is_tl_channel = TRUE LIMIT 200 OFFSET 0" # all TPP channels (~169)
91
+ # MSN status (media_selling_network_join_date) is scrubbed from the
92
+ # advertiser sandbox view — for MSN-only / non-MSN lookups, the
93
+ # structured filter is the right tool: `tl channels list msn:yes|no`.
91
94
 
92
95
  # Show channel detail — accepts numeric ID or channel name.
93
96
  # Names that match more than one active channel print a candidate list
@@ -102,6 +105,16 @@ tl channels show "Economics Explained"
102
105
  tl channels similar 12345 --limit 10
103
106
  tl channels similar "Tremending girls" min-score:0.85 --limit 5
104
107
 
108
+ # Vector recommender — discovery by category/demographic tag (Intelligence plan).
109
+ # `tags` is free; `top`, `inspect-*`, and `similar-to-profile` cost 50 credits flat.
110
+ tl recommender tags # List every tag (free)
111
+ tl recommender tags cooking # Search tag names by substring
112
+ tl recommender top "Cooking" msn:yes --limit 50 # Top channels & brand profiles for a tag
113
+ tl recommender top "USA share" mbn:yes # Demographic tag, MBN brands only
114
+ tl recommender inspect-channel 12345 # Per-tag breakdown of a channel's vector
115
+ tl recommender inspect-brand Nike # Per-tag breakdown of a brand's ideal vector
116
+ tl recommender similar-to-profile 842 # Channels closest to a brand profile
117
+
105
118
  # Brand intelligence
106
119
  tl brands show Nike
107
120
 
@@ -54,13 +54,16 @@ tl uploads list q:code --csv
54
54
  # Show upload details (supports colon-containing IDs)
55
55
  tl uploads show 1174310:0BehkmVa7ak
56
56
 
57
- # Search channels. msn and tpp are tri-state filters (yes/no/both; default 'both').
58
- # msn = opted-in to receive sponsorship offers; tpp = exclusively TL-managed.
59
- # Both are also returned as boolean fields on every channel response.
60
- tl channels list category:cooking min-subs:100k
61
- tl channels list msn:yes # MSN channels only (~11k)
62
- tl channels list tpp:yes # TPP channels only (~169)
63
- tl channels list msn:no min-subs:500k # big non-MSN channels
57
+ # Search channels via raw SQL `tl db pg` against thoughtleaders_channel
58
+ # (run `tl schema pg` once to confirm the live column set).
59
+ tl db pg "SELECT id, channel_name, total_views FROM thoughtleaders_channel
60
+ WHERE content_category = <COOKING_CODE> AND total_views >= 100000
61
+ ORDER BY total_views DESC LIMIT 50 OFFSET 0"
62
+ tl db pg "SELECT id, channel_name FROM thoughtleaders_channel
63
+ WHERE is_tl_channel = TRUE LIMIT 200 OFFSET 0" # all TPP channels (~169)
64
+ # MSN status (media_selling_network_join_date) is scrubbed from the
65
+ # advertiser sandbox view — for MSN-only / non-MSN lookups, the
66
+ # structured filter is the right tool: `tl channels list msn:yes|no`.
64
67
 
65
68
  # Show channel detail — accepts numeric ID or channel name.
66
69
  # Names that match more than one active channel print a candidate list
@@ -75,6 +78,16 @@ tl channels show "Economics Explained"
75
78
  tl channels similar 12345 --limit 10
76
79
  tl channels similar "Tremending girls" min-score:0.85 --limit 5
77
80
 
81
+ # Vector recommender — discovery by category/demographic tag (Intelligence plan).
82
+ # `tags` is free; `top`, `inspect-*`, and `similar-to-profile` cost 50 credits flat.
83
+ tl recommender tags # List every tag (free)
84
+ tl recommender tags cooking # Search tag names by substring
85
+ tl recommender top "Cooking" msn:yes --limit 50 # Top channels & brand profiles for a tag
86
+ tl recommender top "USA share" mbn:yes # Demographic tag, MBN brands only
87
+ tl recommender inspect-channel 12345 # Per-tag breakdown of a channel's vector
88
+ tl recommender inspect-brand Nike # Per-tag breakdown of a brand's ideal vector
89
+ tl recommender similar-to-profile 842 # Channels closest to a brand profile
90
+
78
91
  # Brand intelligence
79
92
  tl brands show Nike
80
93
 
@@ -18,8 +18,8 @@ The user wants to query ThoughtLeaders data. Translate their request into the ri
18
18
  ## Examples
19
19
 
20
20
  - "/tl sold sponsorships for Nike in Q1" → `tl sponsorships list status:sold brand:"Nike" purchase-date-start:2026-01-01 purchase-date-end:2026-03-31`
21
- - "/tl cooking channels over 100k subs" → `tl channels list category:cooking min-subs:100000`
22
- - "/tl mobile-first US cooking channels" → `tl channels list category:cooking primary-device:mobile min-us-share:50`
21
+ - "/tl cooking channels over 100k subs" → `tl db pg "SELECT id, channel_name, total_views FROM thoughtleaders_channel WHERE content_category = <COOKING_CODE> AND total_views >= 100000 ORDER BY total_views DESC LIMIT 50 OFFSET 0"`
22
+ - "/tl mobile-first US cooking channels" → `tl db pg "SELECT id, channel_name, demographic_usa_share FROM thoughtleaders_channel WHERE content_category = <COOKING_CODE> AND demographic_device_primary = 'mobile' AND demographic_usa_share >= 50 ORDER BY total_views DESC LIMIT 50 OFFSET 0"`
23
23
  - "/tl Nike's sponsorship activity" → `tl brands show Nike`
24
24
  - "/tl run my Q1 report" → `tl reports --json` then `tl reports run <id>`
25
25
  - "/tl check my balance" → `tl balance`
@@ -75,7 +75,12 @@ Filters are passed as `key:value` pairs after `list`:
75
75
  tl sponsorships list status:sold brand:"Nike" purchase-date:2026-01
76
76
  tl sponsorships list status:pending send-date:2026-03
77
77
  tl uploads list channel:12345 type:longform since:2026-03
78
- tl channels list category:cooking min-subs:100k language:en
78
+ # Channel discovery is raw SQL — the structured `tl channels list` covers
79
+ # only narrow lookups. Default to:
80
+ tl db pg "SELECT id, channel_name, total_views FROM thoughtleaders_channel
81
+ WHERE content_category = <COOKING_CODE> AND language = 'en'
82
+ AND total_views >= 1000000
83
+ ORDER BY total_views DESC LIMIT 50 OFFSET 0"
79
84
  ```
80
85
 
81
86
  Sponsorship date filtering (on `created-at`, `publish-date`, `purchase-date`, `send-date`) exposes three shapes per field:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "thoughtleaders-cli"
7
- version = "0.5.2"
7
+ version = "0.6.0"
8
8
  description = "ThoughtLeaders CLI — query sponsorship data, channels, brands, and intelligence"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -51,12 +51,12 @@ ThoughtLeaders is a sponsorship marketplace connecting **Brands** (advertisers /
51
51
 
52
52
  The centre of the data model is **Sponsorships** — business relationships between brands and channels. Sponsorships have a funnel of types, from broad to narrow:
53
53
 
54
- - **Sponsorships** — the broadest category, encompassing all stages
54
+ - **Sponsorships** — the broadest category, encompassing all stages, stored in the `thoughtleaders_adlink` table.
55
55
  - **Matches** — possible brand-channel pairings that ThoughtLeaders thinks could work
56
56
  - **Proposals** — matches that have been proposed to both sides to consider
57
57
  - **Deals** — contractually agreed-upon sponsorships (sold), either in production or published
58
58
 
59
- Sponsorships are sometimes called "Ads" or "Ad campaigns".
59
+ Sponsorships are sometimes called "Ads" or "Ad campaigns". An obsolete name for "sponsorship" is an "adlink".
60
60
 
61
61
  The CLI has shortcut commands for each type: `tl matches`, `tl proposals`, `tl deals`. These filter `tl sponsorships` by status.
62
62
 
@@ -66,15 +66,15 @@ Other key concepts:
66
66
  - **Reports** — saved report configurations that can be re-run
67
67
  - **Comments** — notes attached to sponsorships
68
68
  - **Adspots** — types of ads a channel carries (e.g. mention, dedicated video, product placement). Returned by `tl channels show`; each carries price/cost.
69
- - **MSN** (Media Selling Network) — the ~11k YouTube channels that have opted in to receive sponsorship offers. Returned as a boolean `msn` field on every channel response (list, detail, similar). Derived server-side from whether `Channel.media_selling_network_join_date` is non-null the timestamp itself isn't exposed over the CLI, just the boolean. Filterable via `msn:` tri-state: `msn:yes` (MSN only the default on `similar`; on `list` the default is `both`), `msn:no` (non-MSN only), `msn:both` (no filter).
70
- - **TPP** (ThoughtLeaders Partner Program, a.k.a. "TL channels") — the smaller, exclusive ~169 channels TL manages directly. Returned as the `tpp` boolean field on every channel response (list, detail, similar). Filterable via `tpp:` with the same tri-state vocabulary: `tpp:yes` / `tpp:no` / `tpp:both` (default `both`).
69
+ - **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.
70
+ - **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
71
  - **`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
72
  - **`impression`** (on channels) — projected views per video on that channel. Forward-looking estimate. May be null when not yet computed.
73
73
  - **`views`** (on sponsorships) — actual view count of the sold and published sponsored video, accessible when `article_id` is set.
74
74
  - **`impressions_guarantee`** (on sponsorships) — projected/guaranteed impressions for the sponsorship. Numeric; rounded to int in list output.
75
75
  - **Sponsorship detail fields** (returned by `tl sponsorships show <id> --json`) — in addition to the list-view columns, the detail payload includes `integration` (raw int), `publish_count`, `common_name`, `outreach_email`, nested `publisher` (`first_name`, `last_name`, `email`), nested `brand_contact` (`first_name`, `last_name`, `email`), and `brand.organization_name`. Use these when generating IOs, contracts, or outreach.
76
76
  - **CPM** has two distinct meanings depending on level — pick the one the user actually wants:
77
- - **Channel CPM** = `(adspot.price / channel.impression) × 1000` — projected price per thousand projected views. Used for pricing decisions **before** a sponsorship is sold. Available for channels with active adspots via `tl channels show <channel_id>`.
77
+ - **Channel CPM** = `(adspot.price / channel.impression) * 1000` — projected price per thousand projected views. Used for pricing decisions **before** a sponsorship is sold. Available for channels with active adspots via `tl channels show <channel_id>`.
78
78
  - **Sponsorship CPM** = calculated in either of two ways: if `views` is present, then CPM is `(sponsorship.price / sponsorship.views) × 1000`, meaning realized cost per thousand actual views, computed post-publication. If `views` is null, Compute from the sponsorship's `price` and the channel's `impression` fields.
79
79
  - **CPM does not have a range filter.** To find sponsorships in a CPM range (e.g. "around $15"), fetch the record set with other filters first, then apply the CPM range in post-processing (jq, Python, etc.) on the returned `cpm` field. Plan queries and pagination accordingly — the server cannot reduce the result count based on CPM.
80
80
  - **Sponsorship dates** — each sponsorship has four distinct dates, useful for different queries:
@@ -87,23 +87,23 @@ Other key concepts:
87
87
  Users see data scoped by their organization and plan:
88
88
  - **Media buyers** see sponsorships where their org is the brand. They see `price` but never `cost`.
89
89
  - **Media sellers** see sponsorships where their org is the publisher. They see `cost` but never `price`.
90
- - **Intelligence plan** is required for `tl brands`, full channel search, and full uploads.
90
+ - **Intelligence plan** is required for accessing information not strictly related to the user's organisation.
91
91
 
92
92
  When querying sponsorship bookings, query by `status:sold` and filter the the date range only by `purchase_date`. Otherwise, query for state:sold by `created_at`.
93
93
 
94
- An obsolete name for "sponsorship" is an "adlink".
95
-
96
94
  ## Methodology
97
95
 
98
96
  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.
99
97
 
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 "<tag>"` against the vector recommender — that's faster, ranked by category-strength, and returns both channels and matching brand profiles. Run `tl recommender tags` to discover the valid tag names.
99
+
100
100
  ## Workflow
101
101
 
102
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.
103
103
 
104
104
  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
105
 
106
- 1. **Discover first**: Run `tl describe show <resource> --json` to learn available fields, filters, and credit costs before querying
106
+ 1. **Discover first**: Run `tl describe show <resource> --json` to learn available fields, filters, and credit costs before querying. Use `tl schema pg`, `tl schema es`, and `tl schema fb` to find information about the main database (pg), the articles / uploads database (es), and the channel metrics database (fb).
107
107
  2. **Check saved reports**: Run `tl reports --json` to see if the user has a saved report that already answers their question
108
108
  3. **Check credits**: Run `tl balance --json` before expensive queries. Warn the user if a query will cost many credits.
109
109
  4. **Query with filters**: Use `key:value` filter syntax for structured queries
@@ -138,6 +138,11 @@ tl brands show <id-or-name> # Brand detail (1 credit)
138
138
  tl brands history <id-or-name> # Sponsorship history (5 credits/result, linear)
139
139
  tl brands history <query> --channel <id> # Brand mentions on specific channel
140
140
  tl brands similar <id-or-name> # Find similar brands via profile vector KNN (50 credits flat)
141
+ tl recommender tags [query] # List vector tag names — categories, demographics, formats (free)
142
+ tl recommender top "<tag>" # Top channels & profiles loaded on a vector tag (50 credits; Intelligence)
143
+ tl recommender inspect-channel <ref> # Show a channel's feature-vector breakdown (50 credits; Intelligence)
144
+ tl recommender inspect-brand <ref> # Show a brand profile's ideal-vector breakdown (50 credits; Intelligence)
145
+ tl recommender similar-to-profile <id> # Channels closest to a brand profile's ideal vector (50 credits; Intelligence)
141
146
  tl snapshots channel <id> # Channel metrics over time — list curve, mult 1.2 (Firebolt-backed)
142
147
  tl snapshots video <id> --channel <id> # Video view curve — list curve, mult 1.2 (--channel required!)
143
148
  tl reports # List saved reports — list curve, mult 1.3
@@ -278,10 +283,10 @@ See [references/business-glossary.md](references/business-glossary.md) for reven
278
283
 
279
284
  | Capability | Status | Workaround |
280
285
  |---|---|---|
281
- | Arbitrary read-only `SELECT` on Postgres | **Available** via `tl db pg`. | SELECT-only, mandatory `LIMIT ≤ 500` + `OFFSET`, function allowlist, no `::reg*` casts. See `references/postgres-schema.md`. |
282
- | Cross-reference helpers ("channels proposed to brand X", "channels sponsored by MBN brands in last N days") | **Unavailable** these were stacked PG joins. | Approximate with `tl brands history <brand>` (videos where the brand was detected extract channel IDs) and `tl sponsorships list brand:<name> status:<...>`. Won't perfectly match (e.g. `media_buying_network_join_date` isn't exposed). |
283
- | **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. It does not let you set price/cost/owner_sales_id/send_date/etc. | Done in the app or by a human with DB access. |
284
- | Pre-insert validation queries (joining `adspot ↔ channel ↔ profile ↔ org` to confirm MSN, integration=1, persona, plan) | **Unavailable** as a single query (needs PG joins). | Partial: `tl channels show <id>` exposes `msn`, `tpp`, and active adspots with `integration` codes. Persona/plan/profile-level checks aren't surfaced. |
286
+ | Arbitrary read-only `SELECT` on Postgres | **Available** via `tl db pg`. | SELECT-only, mandatory `LIMIT ≤ 500` + `OFFSET`, only certain SQL forms are allowed. See `references/postgres-schema.md`. |
287
+ | Cross-reference helpers ("channels proposed to brand X", "channels sponsored by MBN brands in last N days") | **Available** via `tl db pg`. | Write the join: `thoughtleaders_adlink` `adspot` `channel` `profile` `profile_brands` `brand`. Filter by `publish_status` for proposed/sold and by date range as needed. See `references/postgres-schema.md` for the exact column names. |
288
+ | **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. |
289
+ | 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`). |
285
290
  | 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. |
286
291
  | 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). |
287
292
  | 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. |
@@ -309,43 +314,52 @@ tl changelog --md > CHANGELOG.md # Capture for a doc
309
314
  `tl changelog` summaries are LLM-generated server-side from full commit messages and cached per version, so repeat calls are fast and don't re-bill the LLM. The release date and a 2–4 sentence prose summary come back per version.
310
315
 
311
316
  ### Filter syntax
312
- All list commands accept `key:value` filters:
317
+ Structured list commands accept `key:value` filters (use them for trivially simple lookups):
313
318
  ```bash
314
319
  tl sponsorships list status:sold brand:"Nike" purchase-date:2026-01
315
320
  tl uploads list channel:12345 type:longform
316
- tl channels list category:cooking min-subs:100k language:en
317
- tl channels list tpp:yes # list all TPP (TL-managed) channels
318
- tl channels list tpp:no primary-device:mobile # mobile-first channels that aren't in TPP
319
- tl channels list msn:yes category:tech # Media Selling Network channels in tech
320
- tl channels list msn:no min-subs:500k # big non-MSN channels (not yet opted in)
321
321
  ```
322
322
 
323
323
  Date filters accept keywords: `today`, `yesterday`, `tomorrow`.
324
324
 
325
- #### Channel demographic filters
325
+ #### Channel discovery — recommender first, raw SQL second
326
+
327
+ 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."
328
+
329
+ ```bash
330
+ # Discover the right tag name first (free)
331
+ tl recommender tags cooking
332
+ tl recommender tags "usa"
333
+
334
+ # Top channels & profiles loaded on a vector tag (50 credits; Intelligence)
335
+ tl recommender top "Cooking" msn:yes --limit 50
336
+ tl recommender top "Tech" --limit 30
337
+ tl recommender top "USA share" mbn:yes --limit 50
338
+ ```
326
339
 
327
- These filters apply to both `tl channels list` and `tl sponsorships list` (the latter filters by the associated channel's demographics):
340
+ Use `tl db pg` only for predicates the recommender can't express — pure attribute filters (`is_tl_channel`, `language`, `demographic_device_primary`), aggregations, and joins. Run `tl schema pg` once to confirm the live column set; the columns referenced below are stable.
328
341
 
329
342
  ```bash
330
- # Primary device type
331
- tl channels list primary-device:mobile
332
- tl channels list primary-device:desktop
333
- tl channels list primary-device:tablet
334
-
335
- # Minimum device audience share (0–100)
336
- tl channels list min-mobile-share:60
337
- tl channels list min-desktop-share:30
338
- tl channels list min-tablet-share:10
339
-
340
- # Minimum geo share (0–100, ISO country codes lowercase)
341
- tl channels list min-us-share:70
342
- tl channels list min-gb-share:25
343
-
344
- # Combine with other filters
345
- tl channels list category:tech primary-device:mobile min-us-share:50 min-subs:100k
346
- tl sponsorships list status:sold primary-device:mobile min-us-share:60
343
+ # All TPP (TL-managed) channels — pure attribute filter, not a category query
344
+ tl db pg "SELECT id, channel_name, content_category, total_views
345
+ FROM thoughtleaders_channel
346
+ WHERE is_tl_channel = TRUE
347
+ ORDER BY total_views DESC
348
+ LIMIT 200 OFFSET 0"
349
+
350
+ # Mobile-first non-TPP channels device share, not topic
351
+ tl db pg "SELECT id, channel_name, demographic_device_primary, total_views
352
+ FROM thoughtleaders_channel
353
+ WHERE is_tl_channel = FALSE
354
+ AND demographic_device_primary = 'mobile'
355
+ ORDER BY total_views DESC
356
+ LIMIT 100 OFFSET 0"
347
357
  ```
348
358
 
359
+ For per-country share beyond the recommender's "USA share" tag, use the `demographic_geo` jsonb in raw SQL: `(demographic_geo->>'gb')::int >= 25`. Same pattern with `demographic_device->>'mobile'` for non-primary device shares.
360
+
361
+ **MSN status (`media_selling_network_join_date`) is scrubbed from the advertiser sandbox view.** Raw SQL can't filter on it from an advertiser context; use the structured `tl channels list msn:yes|no|both` for MSN-specific lookups, then drop down to SQL on the resulting IDs (`WHERE id IN (...)`) for further analysis.
362
+
349
363
  ### Output flags
350
364
  - `--json` — structured JSON (use this for parsing)
351
365
  - `--csv` — CSV output
@@ -417,9 +431,19 @@ tl reports --json # Find the report ID first
417
431
  tl reports run 42 --json
418
432
  ```
419
433
 
420
- "Find mobile-first US channels in cooking":
434
+ "Find Cooking channels with US-heavy mobile audiences":
421
435
  ```bash
422
- tl channels list category:cooking primary-device:mobile min-us-share:50 --json
436
+ # Use the vector recommender for the topic, then narrow with structured filters / SQL on the IDs.
437
+ tl recommender top "Cooking" msn:yes --limit 100 --json \
438
+ | jq -r '.results[] | select(.kind=="channel") | .channel_id' \
439
+ | paste -sd, - \
440
+ | xargs -I {} tl db pg "SELECT id, channel_name, total_views, demographic_usa_share
441
+ FROM thoughtleaders_channel
442
+ WHERE id IN ({})
443
+ AND demographic_device_primary = 'mobile'
444
+ AND demographic_usa_share >= 50
445
+ ORDER BY total_views DESC
446
+ LIMIT 50 OFFSET 0" --json
423
447
  ```
424
448
 
425
449
  "Show sold sponsorships targeting mobile US audiences":
@@ -438,3 +462,16 @@ tl channels similar 29834 tpp:yes --limit 30 # TPP (TL-managed)
438
462
  tl channels similar 29834 min-subs:1000000 exclude:477487 --limit 15 # client-side filters
439
463
  ```
440
464
  **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.
465
+
466
+ "Browse the vector recommender" (categories, demographics, formats — `tl recommender tags` is free):
467
+ ```bash
468
+ tl recommender tags # Full tag list (free)
469
+ tl recommender tags cooking # Search tag names by substring
470
+ tl recommender top "Cooking" msn:yes --limit 50 # Top channels & profiles loaded on a tag (50 credits)
471
+ tl recommender top "USA share" mbn:yes --limit 30 # Demographic tag — MBN brands only
472
+ tl recommender top "Tech" exclude-for-profile:842 # Drop channels already proposed for profile 842
473
+ tl recommender inspect-channel 29834 # Per-tag breakdown of a channel's vector
474
+ tl recommender inspect-brand Nike # Per-tag breakdown of a brand's ideal vector
475
+ tl recommender similar-to-profile 842 --limit 30 # Channels closest to a brand profile's ideal vector
476
+ ```
477
+ Use `tl recommender top` for category/topic discovery (it's ranked) and `tl channels similar` / `tl brands similar` for 1:1 lookalike searches.
@@ -49,7 +49,7 @@ Tracks YouTube video metrics over time. Each row = one scrape of one video on on
49
49
  | Column | Type | Description |
50
50
  |--------|------|-------------|
51
51
  | `id` | TEXT | YouTube video ID (e.g., `'dQw4w9WgXcQ'`). **Bare YouTube ID** — NOT the compound `<channel_id>:<youtube_id>` form used in PG `adlink.article_id` and ES `_id`. |
52
- | `channel_id` | INT | TL channel ID (matches the channel ID returned by `tl channels list`) |
52
+ | `channel_id` | INT | TL channel ID (matches `thoughtleaders_channel.id` in Postgres) |
53
53
  | `channel_format` | INT | Platform format (4 = YouTube) |
54
54
  | `publication_date` | DATE | When the video was published |
55
55
  | `scrape_date` | DATE | When this data point was captured |
@@ -85,8 +85,8 @@ Firebolt's **only advantage** is historical metric snapshots. For everything els
85
85
  | Current view count on a video | **Elasticsearch** (`tl uploads show` or `tl db es`) |
86
86
  | Current subscriber count | **Elasticsearch** (`tl channels show`) |
87
87
  | Video metadata (title, tags, duration) | **Elasticsearch** |
88
- | Find channels by criteria | **`tl channels list`** (Postgres) |
89
- | Deal / pipeline / sponsorship data | **`tl sponsorships`** etc. (Postgres) |
88
+ | Find channels by criteria | **`tl db pg`** against `thoughtleaders_channel` |
89
+ | Deal / pipeline / sponsorship data | **`tl db pg`** against `thoughtleaders_adlink` (joins to `adspot`/`channel`/`profile`/`brand`) |
90
90
  | View curve over time (age 7→30→90→180) | **Firebolt** ✅ |
91
91
  | Views at age 30 vs age 180 (evergreenness) | **Firebolt** ✅ |
92
92
  | Channel subscriber growth trend | **Firebolt** ✅ |
@@ -116,8 +116,9 @@ 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 criterion (PG side)
120
- tl channels list category:tech --json --limit 50 | jq '.results[].id'
119
+ # Channels matching some category (vector recommender — preferred over content_category equality)
120
+ tl recommender top "Tech" msn:yes --limit 50 --json \
121
+ | jq '.channels[].channel_id'
121
122
 
122
123
  # Or videos for a specific brand's deals (Postgres side, via tl sponsorships)
123
124
  tl deals list brand:"Nike" --json --limit 500 \
@@ -1,3 +1,3 @@
1
1
  """ThoughtLeaders CLI — query sponsorship data, channels, brands, and intelligence."""
2
2
 
3
- __version__ = "0.5.2"
3
+ __version__ = "0.6.0"
@@ -0,0 +1,247 @@
1
+ """tl recommender — Vector-recommender introspection and discovery.
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.
8
+
9
+ For 1:1 similarity use `tl channels similar` and `tl brands similar`.
10
+ """
11
+
12
+ import urllib.parse
13
+
14
+ import typer
15
+ from rich.console import Console
16
+
17
+ from tl_cli.client.errors import ApiError, handle_api_error
18
+ from tl_cli.client.http import get_client
19
+ from tl_cli.filters import parse_filters
20
+ from tl_cli.output.formatter import detect_format, output, output_single
21
+
22
+ app = typer.Typer(help="Vector recommender (tags, top-by-tag, vector inspection, profile→channel similarity)")
23
+
24
+
25
+ TOP_COLUMNS = ["kind", "value", "channel_id", "profile_id", "name", "brand_name", "slug"]
26
+ TOP_COLUMN_CONFIG = {"value": {"justify": "right"}}
27
+
28
+
29
+ def _handle_recommender_error(e: ApiError) -> None:
30
+ """Show ambiguity candidates inline; otherwise default handler."""
31
+ if e.status_code == 400 and isinstance(e.raw, dict) and e.raw.get("candidates"):
32
+ err = Console(stderr=True)
33
+ err.print(f"[yellow]{e.detail}[/yellow]")
34
+ err.print()
35
+ err.print("[bold]Candidates:[/bold]")
36
+ for c in e.raw["candidates"]:
37
+ cid = c.get("channel_id") or c.get("brand_id") or "?"
38
+ name = c.get("name", "")
39
+ extra = c.get("website") or c.get("subscribers") or ""
40
+ err.print(f" {cid:>10} {name} [dim]{extra}[/dim]")
41
+ raise typer.Exit(1)
42
+ handle_api_error(e)
43
+
44
+
45
+ @app.callback(invoke_without_command=True)
46
+ def recommender(ctx: typer.Context) -> None:
47
+ """Vector recommender."""
48
+ if ctx.invoked_subcommand is None:
49
+ typer.echo(ctx.get_help())
50
+
51
+
52
+ @app.command("tags")
53
+ def tags_cmd(
54
+ args: list[str] = typer.Argument(None, help="Optional substring (matches tag or normalized name)"),
55
+ json_output: bool = typer.Option(False, "--json", help="JSON output"),
56
+ csv_output: bool = typer.Option(False, "--csv", help="CSV output"),
57
+ md_output: bool = typer.Option(False, "--md", help="Markdown output"),
58
+ toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
59
+ ) -> None:
60
+ """List vector tag names (free).
61
+
62
+ Use this to discover the tag names accepted by `tl recommender top`.
63
+ Each tag is one dimension of a channel/profile feature vector —
64
+ e.g. content categories like "Cooking", demographic buckets like
65
+ "Age 18-24", device shares, country shares.
66
+
67
+ Examples:
68
+ tl recommender tags
69
+ tl recommender tags cooking
70
+ tl recommender tags "age 18"
71
+ """
72
+ fmt = detect_format(json_output, csv_output, md_output, toon_output)
73
+ query = " ".join(args or []).strip()
74
+ params = {"q": query} if query else {}
75
+ client = get_client()
76
+ try:
77
+ data = client.get("/recommender/tags", params=params)
78
+ output(
79
+ data,
80
+ fmt,
81
+ columns=["group", "field_name", "normalized_name"],
82
+ title="Recommender vector tags",
83
+ )
84
+ except ApiError as e:
85
+ handle_api_error(e)
86
+ finally:
87
+ client.close()
88
+
89
+
90
+ @app.command("top")
91
+ def top_cmd(
92
+ tag: str = typer.Argument(..., help='Vector tag name (e.g. "Cooking", "Age 18-24"). Run `tl recommender tags` to discover valid names.'),
93
+ args: list[str] = typer.Argument(None, help="Filters (key:value pairs)."),
94
+ json_output: bool = typer.Option(False, "--json", help="JSON output"),
95
+ csv_output: bool = typer.Option(False, "--csv", help="CSV output"),
96
+ md_output: bool = typer.Option(False, "--md", help="Markdown output"),
97
+ toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
98
+ limit: int = typer.Option(50, "--limit", "-l", help="Max results per group (1-100)"),
99
+ ) -> None:
100
+ """Top channels and profiles loaded on a single vector tag.
101
+
102
+ Costs 50 credits per call. Intelligence plan required. Returns both
103
+ channels and profiles ranked by the tag's value (descending).
104
+
105
+ Filters:
106
+ msn:<yes|no|all> MSN membership for channel rows (default: all)
107
+ mbn:<yes|no|all> MBN membership for profile rows (default: all)
108
+ exclude-for-channel:<id> Drop profiles already proposed for this channel
109
+ exclude-for-profile:<id> Drop channels already proposed for this profile
110
+
111
+ Examples:
112
+ tl recommender top "Cooking"
113
+ tl recommender top "Tech" msn:yes --limit 30
114
+ tl recommender top "USA share" mbn:yes
115
+ tl recommender top "Cooking" exclude-for-profile:842
116
+ """
117
+ fmt = detect_format(json_output, csv_output, md_output, toon_output)
118
+ filters = parse_filters(args or [])
119
+
120
+ server_keys = {"msn", "mbn", "exclude-for-channel", "exclude-for-profile"}
121
+ params = {k: v for k, v in filters.items() if k in server_keys}
122
+ params["tag"] = tag
123
+ params["limit"] = str(limit)
124
+
125
+ client = get_client()
126
+ try:
127
+ data = client.get("/recommender/top", params=params)
128
+ rows = data.get("results", [])
129
+ for r in rows:
130
+ r["name"] = r.get("channel_name") or r.get("profile_email") or ""
131
+ output(
132
+ data,
133
+ fmt,
134
+ columns=TOP_COLUMNS,
135
+ title=f"Top by tag: {tag}",
136
+ column_config=TOP_COLUMN_CONFIG,
137
+ )
138
+ except ApiError as e:
139
+ handle_api_error(e)
140
+ finally:
141
+ client.close()
142
+
143
+
144
+ @app.command("inspect-channel")
145
+ def inspect_channel_cmd(
146
+ channel_ref: str = typer.Argument(..., help="Channel ID (numeric) or name (partial match, must be unique)"),
147
+ json_output: bool = typer.Option(False, "--json", help="JSON output"),
148
+ csv_output: bool = typer.Option(False, "--csv", help="CSV output"),
149
+ md_output: bool = typer.Option(False, "--md", help="Markdown output"),
150
+ toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
151
+ ) -> None:
152
+ """Show a channel's feature vector grouped by category.
153
+
154
+ Costs 50 credits per call. Intelligence plan required. Returns the
155
+ grouped sparse vector (active dimensions only) and the magnitude.
156
+
157
+ Examples:
158
+ tl recommender inspect-channel 12345
159
+ tl recommender inspect-channel "MrBeast"
160
+ """
161
+ fmt = detect_format(json_output, csv_output, md_output, toon_output)
162
+ encoded = urllib.parse.quote(channel_ref, safe="")
163
+ client = get_client()
164
+ try:
165
+ data = client.get(f"/recommender/channels/{encoded}/inspect")
166
+ output_single(data, fmt)
167
+ except ApiError as e:
168
+ _handle_recommender_error(e)
169
+ finally:
170
+ client.close()
171
+
172
+
173
+ @app.command("inspect-brand")
174
+ def inspect_brand_cmd(
175
+ brand_ref: str = typer.Argument(..., help="Brand ID (numeric) or name (partial match, must be unique)"),
176
+ json_output: bool = typer.Option(False, "--json", help="JSON output"),
177
+ csv_output: bool = typer.Option(False, "--csv", help="CSV output"),
178
+ md_output: bool = typer.Option(False, "--md", help="Markdown output"),
179
+ toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
180
+ ) -> None:
181
+ """Show a brand profile's ideal feature vector grouped by category.
182
+
183
+ Costs 50 credits per call. Intelligence plan required. Resolves the
184
+ brand to its (preferred MBN) profile and inspects that profile's
185
+ aggregated vector.
186
+
187
+ Examples:
188
+ tl recommender inspect-brand 287
189
+ tl recommender inspect-brand Nike
190
+ """
191
+ fmt = detect_format(json_output, csv_output, md_output, toon_output)
192
+ encoded = urllib.parse.quote(brand_ref, safe="")
193
+ client = get_client()
194
+ try:
195
+ data = client.get(f"/recommender/brands/{encoded}/inspect")
196
+ output_single(data, fmt)
197
+ except ApiError as e:
198
+ _handle_recommender_error(e)
199
+ finally:
200
+ client.close()
201
+
202
+
203
+ @app.command("similar-to-profile")
204
+ def similar_to_profile_cmd(
205
+ profile_id: int = typer.Argument(..., help="Profile ID (numeric)"),
206
+ args: list[str] = typer.Argument(None, help="Filters (key:value pairs)."),
207
+ json_output: bool = typer.Option(False, "--json", help="JSON output"),
208
+ csv_output: bool = typer.Option(False, "--csv", help="CSV output"),
209
+ md_output: bool = typer.Option(False, "--md", help="Markdown output"),
210
+ toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
211
+ limit: int = typer.Option(20, "--limit", "-l", help="Max results (1-100)"),
212
+ ) -> None:
213
+ """Channels closest to a brand profile's ideal vector.
214
+
215
+ Costs 50 credits per call. Intelligence plan required. Channels the
216
+ brand has already worked with or been proposed are excluded.
217
+
218
+ Filters:
219
+ language:<iso> Content language (default: en)
220
+ msn:<yes|no> Restrict to MSN channels (default: no)
221
+
222
+ Examples:
223
+ tl recommender similar-to-profile 842
224
+ tl recommender similar-to-profile 842 msn:yes --limit 30
225
+ """
226
+ fmt = detect_format(json_output, csv_output, md_output, toon_output)
227
+ filters = parse_filters(args or [])
228
+ params = {k: v for k, v in filters.items() if k in {"language", "msn"}}
229
+ params["limit"] = str(limit)
230
+ client = get_client()
231
+ try:
232
+ data = client.get(f"/recommender/profiles/{profile_id}/similar", params=params)
233
+ for r in data.get("results", []):
234
+ score = r.get("score")
235
+ if isinstance(score, (int, float)) and fmt in ("table", "md"):
236
+ r["score"] = f"{score * 100:.1f}%"
237
+ output(
238
+ data,
239
+ fmt,
240
+ columns=["score", "channel_id", "channel_name", "slug"],
241
+ title=f"Channels similar to profile {profile_id}",
242
+ column_config={"score": {"justify": "right"}},
243
+ )
244
+ except ApiError as e:
245
+ handle_api_error(e)
246
+ finally:
247
+ client.close()
@@ -27,6 +27,7 @@ from tl_cli.commands.db import app as db_app
27
27
  from tl_cli.commands.deals import app as deals_app
28
28
  from tl_cli.commands.matches import app as matches_app
29
29
  from tl_cli.commands.proposals import app as proposals_app
30
+ from tl_cli.commands.recommender import app as recommender_app
30
31
  from tl_cli.commands.sponsorships import app as sponsorships_app
31
32
  from tl_cli.commands.describe import app as describe_app
32
33
  from tl_cli.commands.schema import app as schema_app
@@ -95,6 +96,7 @@ app.add_typer(deals_app, name="deals")
95
96
  app.add_typer(uploads_app, name="uploads")
96
97
  app.add_typer(channels_app, name="channels")
97
98
  app.add_typer(brands_app, name="brands")
99
+ app.add_typer(recommender_app, name="recommender")
98
100
  app.add_typer(snapshots_app, name="snapshots")
99
101
  app.add_typer(reports_app, name="reports")
100
102
  app.add_typer(comments_app, name="comments")
@@ -387,8 +387,8 @@ wheels = [
387
387
  ]
388
388
 
389
389
  [[package]]
390
- name = "tl-cli"
391
- version = "0.4.19"
390
+ name = "thoughtleaders-cli"
391
+ version = "0.6.0"
392
392
  source = { editable = "." }
393
393
  dependencies = [
394
394
  { name = "authlib" },