thoughtleaders-cli 0.7.6__py3-none-any.whl → 0.7.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: thoughtleaders-cli
3
- Version: 0.7.6
3
+ Version: 0.7.8
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
@@ -163,6 +163,7 @@ tl recommender tags cooking # Search tag names by substring
163
163
  tl recommender top-channels "Cooking" msn:yes --limit 50 # Top channels for a tag
164
164
  tl recommender top-profiles "Cooking" mbn:yes --limit 30 # Top brand profiles (one brand → potentially multiple profiles)
165
165
  tl recommender top-brands "Cooking" --limit 30 # Top brands (deduped from profiles)
166
+ tl recommender channels-with-tag "Cooking" # ALL channel IDs loaded on a tag (--min defaults to 0.00001; paged; 1 credit/result)
166
167
  tl recommender inspect-channel 12345 # Per-tag breakdown of a channel's vector
167
168
  tl recommender inspect-brand Nike # Per-tag breakdown of a brand's ideal profile
168
169
  tl recommender channels-for-profile 842 # Channels closest to a specific brand profile
@@ -210,9 +211,9 @@ tl describe show sponsorships --filters # Available filters for sponsorships
210
211
  tl balance # Your credit balance
211
212
  ```
212
213
 
213
- `tl db pg` is priced **per-query**: a base rate plus a multiplier extra for every expensive table referenced, plus a flat per-row charge for every expensive column read. Sensitive fields (demographics, channel outreach emails) are expensive. Run `tl describe show db --json` to see the live `pg_expensive` map, and check `usage.credit_rate` in the response envelope after a query to see what your query was actually charged.
214
+ `tl db pg` is priced **per-row**: the per-row rate is the **sum of the rates of the tables the query touches** (default 1.0/row; some tables are cheaper or dearer), plus a flat per-row charge for every expensive column read (demographics, channel outreach emails), all times the rows returned. Aggregate queries (`count`/`GROUP BY`) add a surcharge proportional to the rows they aggregate. Run `tl describe show db --json` to see the live `pg_pricing` map, and check `usage.credit_rate` in the response envelope after a query to see what your query was actually charged.
214
215
 
215
- To preview a query's cost **before** running it, add `--pricing`: `tl db pg "SELECT … LIMIT 100" --pricing` runs only the planner's `EXPLAIN`, prints the cost breakdown and an upper-bound estimate (at the query's `LIMIT`), and costs a flat **1 credit** — the query itself never executes. Works with `--json` too. `--pricing` is also available on `tl db fb` and `tl db es`; those backends are flat-rate (no per-column charges), so the estimate is the volume curve at the query's row ceiling (`LIMIT` for Firebolt, `size` — or the aggregation doc cap — for Elasticsearch).
216
+ To preview a query's cost **before** running it, add `--pricing`: `tl db pg "SELECT … LIMIT 100" --pricing` runs only the planner's `EXPLAIN`, prints the cost breakdown and an upper-bound estimate (at the query's `LIMIT`), and costs a flat **1 credit** — the query itself never executes. Works with `--json` too. `--pricing` is also available on `tl db fb` and `tl db es`; those backends have no per-table or per-column charges, so the estimate is the flat per-row rate at the query's row ceiling (`LIMIT` for Firebolt, `size` — or the aggregation doc cap — for Elasticsearch).
216
217
 
217
218
  # Terminology
218
219
 
@@ -277,8 +278,11 @@ Each agent discovers the skill automatically and uses it when you ask about spon
277
278
  The plugin ships several focused skills (installed by all the `tl setup *` commands):
278
279
 
279
280
  - **`tl`** — the data-analyst skill. Defaults to raw database queries via `tl db pg|fb|es` for anything non-trivial; uses the structured `tl <resource> show` / `find` / `similar` commands for single-record lookups and similarity / ID-resolution special cases. Comes with full schema references for Postgres, Elasticsearch, and Firebolt under `references/`.
280
- - **`tl-report-builder`** — builds TL reports (channels / brands / sponsorships / videos) from natural-language requests. Produces an in-chat preview by default; saves a real campaign when the user is explicit ("save", "create the report").
281
+ - **`tl-keyword-research`** — broadens and ranks content-search keywords by Elasticsearch document count before a `tl db es` content search, so finding videos or channels by topic isn't bottlenecked on hand-guessed terms.
282
+ - **`tl-save-report`** — persists the result set from an in-chat exploration session as a saved TL report ("save this as a report", "turn this into a campaign").
283
+ - **`tl-report-builder`** — builds a brand-new TL report config from scratch (channels / brands / sponsorships / videos) through a guided multi-phase flow. Manual-invocation-only: reach it via `/tl-report-builder` or by naming it explicitly — natural-language report requests route to `tl`, `tl-save-report`, or `tl-import` instead.
281
284
  - **`tl-import`** / **`bulk-import`** — superuser-only; bulk-add or exclude lists of channels, brands, videos, or sponsorships against a report.
285
+ - **`tl-channel-authenticity`** — vets a YouTube channel for non-organic views and bot/spam comments before booking (or after delivering) a sponsorship.
282
286
  - **`tl-views-guarantee`** — sizes a multi-video sponsorship buy for a channel, returning the video bundle size, views guarantee, and likelihood to hit.
283
287
  - **`tl-top-partnerships`** — brand-user performance report. Ranks a brand's sold sponsorships by live eCPM vs the sold-date projection, aggregates per channel, and delivers a two-tab Google Sheet ("By Deal" / "By Channel") via `gws`. Uses only public CLI commands (`tl whoami`, `tl sponsorships list`).
284
288
 
@@ -1,4 +1,4 @@
1
- tl_cli/__init__.py,sha256=Aq3mKDHNRP4nTdWfXRVP0mw3Gi2UrDf1gZYuf1CJBTM,112
1
+ tl_cli/__init__.py,sha256=P8v-VIzxtaC--QFovDaGF5D5jVNjkjwOqcucG5_5_I0,112
2
2
  tl_cli/_completions.py,sha256=kOyEUqC26vbYvyXWi513WX8fF73qQLR5WWuRSe_wqyk,164
3
3
  tl_cli/_typer_utils.py,sha256=ZiZsCVmEznPvBw-dYbr3tu3zWZ0iN6kjoQmK3gMqD28,860
4
4
  tl_cli/config.py,sha256=UV_OYTXuQnAIqbi_oVCXx0hhIdZWR678RRapVv51UwQ,1859
@@ -7,8 +7,8 @@ tl_cli/hints.py,sha256=cT8kuDtkAZqwXkc2RV0Yg_abofK-g9UiXwTTBunX78U,1557
7
7
  tl_cli/main.py,sha256=A_8b2SQjBKATxrjO7AGC5Ab1QWlP35gGo4TWzYZtlOM,5806
8
8
  tl_cli/self_update.py,sha256=akXOWYgBX2otyaVlx9CDl04gG2s_hYigE2Vkpubt0SA,18302
9
9
  tl_cli/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- tl_cli/auth/commands.py,sha256=BiT-87UbZjsOJKtjv2dKcCwbAg4-Dp4oICRM0c97TFg,7409
11
- tl_cli/auth/login.py,sha256=Rf65nhN7sjUNcaHYlsO7oYCiHqhGo8e8XwoGI2S4mgM,11487
10
+ tl_cli/auth/commands.py,sha256=CkCaKFb-xUwhCeIL92EC4-odiaSpLI1bmgg5tDCrclM,7387
11
+ tl_cli/auth/login.py,sha256=AxdQ8LOZd1uZhFXPyaiGB_Hk0RiVuX7t37gkjgoEOCM,11568
12
12
  tl_cli/auth/pkce.py,sha256=4Q6Ip-TeZFNG9c3swXNi4gH7mdMkltKa62gZZNybt8U,658
13
13
  tl_cli/auth/token_store.py,sha256=TcZnUol4-8r0jMEJhOPmABCX12_5RkAln2xfWPNdmHk,3275
14
14
  tl_cli/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -24,11 +24,11 @@ tl_cli/commands/channels.py,sha256=ALw2fgJL3w0dpYp3A41OECL0odenhSV16vFA7la7aQI,1
24
24
  tl_cli/commands/credits.py,sha256=2xCht2e420LmaFBKNdKoMz8GlTh31qSWSlJAnVzoZic,7308
25
25
  tl_cli/commands/db.py,sha256=rdIQrxT7sdrPEnBbByNHvPr2X6iIg-wb19X9bWYwDRc,5053
26
26
  tl_cli/commands/deals.py,sha256=ZK9yneInsC6DXoCPS65oyLoVR0eRW1xdRlEN7oRp1pc,2174
27
- tl_cli/commands/describe.py,sha256=Ox2B1hoVuJ6pJc_x5BJjAhEJhlae-el9zwoJKutw3X8,12232
27
+ tl_cli/commands/describe.py,sha256=3lURv4NllM5qPeMEBbejrIxiMzsyTwpJIz211-wuGCU,12362
28
28
  tl_cli/commands/doctor.py,sha256=KUKglwhMc7B26XXy_3M0LkHu7wqfFO5T0YPHO1SH1VY,9024
29
29
  tl_cli/commands/matches.py,sha256=K5o6B8FLECp7825dU4W3X8n-wuXvGJz57xpQPXeXQ-0,2886
30
30
  tl_cli/commands/proposals.py,sha256=khsjorluIfgrJ22DiwzIAFcYD4JbirjkOBz1KuQ0Sdk,2918
31
- tl_cli/commands/recommender.py,sha256=DIRvnSbV2TwvVUgA5luGJ7uQUOjHx2CULFsZVaqUYw4,18922
31
+ tl_cli/commands/recommender.py,sha256=BZfviKAPhpZah0zaSyH5RDa23fG8QAkUnG5FDhOdi4I,21324
32
32
  tl_cli/commands/reports.py,sha256=WcZWtBVRX49z-1Fw4T5iq_xZqks3QmIXlWVg5iVGs58,26532
33
33
  tl_cli/commands/schema.py,sha256=GCBEE4fDatQhVasLKKr7bkGhELRZ0scYm_hUCbDYmuA,5985
34
34
  tl_cli/commands/setup.py,sha256=QST6DCSxJHLFNX1UUJHwZ3hTDTnySATaV2Tc2JsdYYY,21920
@@ -37,16 +37,16 @@ tl_cli/commands/sponsorships.py,sha256=MWjyaReMMhmVKAbrCBCVw_J6dzkML_TIo_2kyPOYQ
37
37
  tl_cli/commands/uploads.py,sha256=Tf9tqAEm9FGe3A7sr_EDX9OzdNInCmrWNr10wWGuMUo,1526
38
38
  tl_cli/commands/whoami.py,sha256=aUXwBRwh1vAGrvz8CKGfHYtEOKJCIDfwrGesKAwYZMk,7866
39
39
  tl_cli/output/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
40
- tl_cli/output/formatter.py,sha256=pqlKmb2nZ1Z2e1A9m8l5mgVemJinVAP4in1tUzFWHno,22522
40
+ tl_cli/output/formatter.py,sha256=zWwcg4yovMXLaduxu8skpDjPVLTaGZAmwtYwjpZDg1w,22766
41
41
  tl_cli/_plugin/.claude-plugin/marketplace.json,sha256=l56PMmyjfGXNGlV30wRyOAe74B6gJNCVNCxgsBbSNxc,446
42
- tl_cli/_plugin/.claude-plugin/plugin.json,sha256=djTkN3fvnL-cnoGePHYiQbudE6-RuK4sETDz-HflKkU,466
42
+ tl_cli/_plugin/.claude-plugin/plugin.json,sha256=kSWDf14vaAKSZIa62p4eXV9VHtaO7E7gxoPf_RokUds,466
43
43
  tl_cli/_plugin/agents/tl-analyst.md,sha256=6J3X3NANkWg6OOUCvNirkN4ulIk80KSumPncDUBt75E,6761
44
44
  tl_cli/_plugin/agents/youtube-comment-classifier.md,sha256=S5lr_htA98FIX0su8FJ2ntiHfbdK8OB2NQKC4lTnQcw,2178
45
45
  tl_cli/_plugin/hooks/hooks.json,sha256=FSWibw1xAjA-suFV3fR8btIb2kQ82LQ08otTr-NpmFw,835
46
46
  tl_cli/_plugin/hooks/scripts/load-tl-skill.mjs,sha256=EBsyZ-caei-CBJsRtqzJXJs_20O3H22MuVmDpu96umo,805
47
47
  tl_cli/_plugin/hooks/scripts/post-usage.sh,sha256=WVvZLkZik6lbeZ20Kh-wgm4JkRFHFN0Uwl4C8S3Y0sY,759
48
48
  tl_cli/_plugin/hooks/scripts/pre-check.sh,sha256=E9KeuXy6yeHEBOnOFW4hDW-Et-Dbp1Oh--3WXKfOX78,898
49
- tl_cli/_plugin/skills/tl/SKILL.md,sha256=R8pU0aU-QrRoawdX-F9mT3ci9inwWBN744xFMciTrmQ,61239
49
+ tl_cli/_plugin/skills/tl/SKILL.md,sha256=B9UMrGJduF3nOxESSicKYM5K5G91NcCbmYQjQh6faVk,61661
50
50
  tl_cli/_plugin/skills/tl/references/business-glossary.md,sha256=FCS-qBOGpdJCmHdglRGRjAuTQAtzpxJNpMkEWThuvlI,17779
51
51
  tl_cli/_plugin/skills/tl/references/elasticsearch-schema.md,sha256=OpHvixZ8UcZYJd8GdwgumryFyKwPAxj3AvPkl1QreMY,9316
52
52
  tl_cli/_plugin/skills/tl/references/firebolt-schema.md,sha256=KagpSWWEWIRfsAWz271PvAqVbSPvWLoogWhCA_XFSZw,10642
@@ -110,8 +110,8 @@ tl_cli/_plugin/skills/tl-top-partnerships/SKILL.md,sha256=hvH05hIaGlc0RfTE0GLBtD
110
110
  tl_cli/_plugin/skills/tl-top-partnerships/scripts/top_partnerships.py,sha256=_13W6-HuD_jtl7AWQQcZQ0SQO9qODMymlcL-1s4-VwU,13248
111
111
  tl_cli/_plugin/skills/tl-views-guarantee/SKILL.md,sha256=IH7q1WJDWri9TWJMiga1FMGJO_GKSbWwaDS6CVNZ9c0,9270
112
112
  tl_cli/_plugin/skills/tl-views-guarantee/scripts/vg.py,sha256=Qp5poinHEqh9374anq0bLtlxj2YL6ipBicaT960-Cws,15825
113
- thoughtleaders_cli-0.7.6.dist-info/METADATA,sha256=P7oqDlNfz_pjBEWzS7pWmSnS8KTUFNEYiEV7H5D-ZYI,18452
114
- thoughtleaders_cli-0.7.6.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
115
- thoughtleaders_cli-0.7.6.dist-info/entry_points.txt,sha256=umZp-1BkGkHDG0bNZXpTXrjwW0HGf9IDFN40eAWuuvg,39
116
- thoughtleaders_cli-0.7.6.dist-info/licenses/LICENSE,sha256=RUfdfLsn6jygiyrnnVUHt6r4IPwr2rbDm9Kixgtu8fo,1071
117
- thoughtleaders_cli-0.7.6.dist-info/RECORD,,
113
+ thoughtleaders_cli-0.7.8.dist-info/METADATA,sha256=uVUM2ngztGKX4s6AY0DGVJzxnM2eEEi3ogta_oK9puQ,19387
114
+ thoughtleaders_cli-0.7.8.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
115
+ thoughtleaders_cli-0.7.8.dist-info/entry_points.txt,sha256=umZp-1BkGkHDG0bNZXpTXrjwW0HGf9IDFN40eAWuuvg,39
116
+ thoughtleaders_cli-0.7.8.dist-info/licenses/LICENSE,sha256=RUfdfLsn6jygiyrnnVUHt6r4IPwr2rbDm9Kixgtu8fo,1071
117
+ thoughtleaders_cli-0.7.8.dist-info/RECORD,,
tl_cli/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """ThoughtLeaders CLI — query sponsorship data, channels, brands, and intelligence."""
2
2
 
3
- __version__ = "0.7.6"
3
+ __version__ = "0.7.8"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tl-cli",
3
- "version": "0.7.6",
3
+ "version": "0.7.8",
4
4
  "description": "ThoughtLeaders CLI — query sponsorship deals, channels, brands, uploads, and intelligence from the terminal",
5
5
  "author": {
6
6
  "name": "ThoughtLeaders",
@@ -204,6 +204,7 @@ tl recommender tags [query] # List similarity tag names — categorie
204
204
  tl recommender top-channels "<tag>" # Top channels loaded on a similarity tag (Intelligence)
205
205
  tl recommender top-profiles "<tag>" # Top brand profiles loaded on a similarity tag
206
206
  tl recommender top-brands "<tag>" # Top brands (deduped from profiles) loaded on a similarity tag
207
+ tl recommender channels-with-tag "<tag>" [--min <v>] # ALL channel IDs scoring >= v on a tag (--min default 0.00001 drops zero-loading channels; paged, enumerates the full set; 1 credit/result; Intelligence)
207
208
  tl recommender inspect-channel <ref> # Show a channel's similarity-profile breakdown (Intelligence)
208
209
  tl recommender inspect-brand <ref> # Show a brand profile's ideal similarity-profile breakdown (Intelligence)
209
210
  tl recommender channels-for-profile <id> # Find channels closest to a brand profile's ideal profile (Intelligence)
@@ -414,9 +415,9 @@ tl db pg "SELECT b.name, COUNT(*) AS deals
414
415
 
415
416
  If unsure about what information to find where, read the [references/postgresql-schema.md](references/postgresql-schema.md) file for instructions. Use just `tl pg schema` to see the entire SQL schema.
416
417
 
417
- **PG cost is per-query.** The credit cost for a `tl db pg` call is a base rate plus a multiplier extra for every expensive table referenced, plus a **flat per-row charge** for every expensive column read (an expensive column costs its configured value for every row returned). Most tables and columns are not expensive; sensitive ones (e.g. demographics, channel outreach emails) cost more. Run `tl describe show db --json` to see the live `pg_expensive` map, and check `usage.credit_rate` / `usage.pricing` in the response envelope after a query to see what your query was actually charged.
418
+ **PG cost is per-row.** The credit cost for a `tl db pg` call is its per-row rate the **sum of the rates of the tables the query touches** (default 1.0/row; some tables are cheaper or dearer) plus a **flat per-row charge** for every expensive column read — times the rows returned. So a join pays for each table it reads, and an expensive column costs its configured value for every row returned. Aggregate queries (`count`/`GROUP BY`) add a surcharge proportional to the estimated rows aggregated. Sensitive columns (e.g. demographics, channel outreach emails) cost more per row. Run `tl describe show db --json` to see the live `pg_pricing` map, and check `usage.credit_rate` / `usage.pricing` in the response envelope after a query to see what your query was actually charged.
418
419
 
419
- **Preview cost before running.** Add `--pricing` to estimate a query's cost without executing it: `tl db pg "SELECT … LIMIT 100" --pricing` runs only `EXPLAIN`, prints the multiplier + per-row breakdown and an upper-bound cost (at the query's LIMIT), and costs a flat 1 credit. Use this before large or expensive-column queries. Works with `--json`. `--pricing` also works on `tl db fb` and `tl db es` — those backends have no per-column charges, so the estimate is just the volume curve at the row ceiling (`LIMIT` for Firebolt; `size`, or the aggregation doc cap, for Elasticsearch).
420
+ **Preview cost before running.** Add `--pricing` to estimate a query's cost without executing it: `tl db pg "SELECT … LIMIT 100" --pricing` runs only `EXPLAIN`, prints the per-row rate + per-row breakdown and an upper-bound cost (at the query's LIMIT), and costs a flat 1 credit. Use this before large or expensive-column queries. Works with `--json`. `--pricing` also works on `tl db fb` and `tl db es` — those backends have no per-table or per-column charges, so the estimate is just the flat per-row rate at the row ceiling (`LIMIT` for Firebolt; `size`, or the aggregation doc cap, for Elasticsearch).
420
421
 
421
422
  ### Three sources, each authoritative for different things
422
423
 
tl_cli/auth/commands.py CHANGED
@@ -187,9 +187,7 @@ def logout_cmd() -> None:
187
187
  # the interactive login established. Point the user at Auth0's logout
188
188
  # URL so the next `tl auth login` doesn't silently SSO straight back in.
189
189
  logout_url = f"https://{get_config().auth0_domain}/logout"
190
- console.print(
191
- f"To end your Auth0 browser session, visit: [cyan]{logout_url}[/cyan]"
192
- )
190
+ console.print(f"To end your Auth0 browser session, visit: [cyan]{logout_url}[/cyan]")
193
191
  clear_tokens()
194
192
  console.print("[green]Logged out successfully.[/green]")
195
193
 
tl_cli/auth/login.py CHANGED
@@ -129,7 +129,7 @@ def login_device_code() -> StoredTokens:
129
129
  console.print()
130
130
  console.print(f"[bold]And enter the code:[/bold] [cyan bold]{user_code}[/cyan bold]")
131
131
  console.print()
132
- console.print(f"[dim]The code expires in {expires_in // 60} minutes.[/dim]")
132
+ console.print(f"[dim]The code expires in {expires_in // 60} minutes. After you have logged in successfully, please wait until the system is notified.[/dim]")
133
133
 
134
134
  # Poll for token
135
135
  deadline = time.time() + expires_in
@@ -124,11 +124,11 @@ def _summarise_modes(credits: dict) -> tuple[str, str, bool]:
124
124
  - 'free' → "free"
125
125
  - 'flat' → "<rate> per call"
126
126
  - 'linear-per-result' (one mode) → "<rate> × n (per result)"
127
- - 'curve' (one mode, mult=R) → "curve (×R)"
127
+ - 'per-row' (one mode, rate=R) → "R/row"
128
128
  - mixed (e.g. channels has detail / history / similar at different rates)
129
129
  → per-mode "<mode> R" joined with commas
130
130
 
131
- The typical-cost column uses the n=100 example for curve/per-result and
131
+ The typical-cost column uses the n=100 example for per-row/per-result and
132
132
  the flat rate for flat. Free shows '-'.
133
133
  """
134
134
  modes = _modes_block(credits)
@@ -163,8 +163,8 @@ def _format_single_mode_label(mode_name: str, payload: dict, *, terse: bool = Fa
163
163
  return f"{_fmt_credits(rate)}/call" if terse else f"{_fmt_credits(rate)} per call"
164
164
  if model == "linear-per-result":
165
165
  return f"{_fmt_credits(rate)}×n" if terse else f"{_fmt_credits(rate)} × n (per result)"
166
- if model == "curve":
167
- return f"curve ×{rate}"
166
+ if model == "per-row":
167
+ return f"{_fmt_credits(rate)}/row"
168
168
  return f"{model} ({_fmt_credits(rate)})"
169
169
 
170
170
 
@@ -256,25 +256,25 @@ def _print_pricing_section(credits: dict) -> None:
256
256
  "Estimate using the examples above before running with a large limit."
257
257
  )
258
258
 
259
- # Surface live PG expensive-items pricing when the server included it
260
- # (db resource only).
261
- _print_pg_expensive_section(credits.get("pg_expensive"))
259
+ # Surface live PG per-table / per-column pricing when the server included
260
+ # it (db resource only).
261
+ _print_pg_pricing_section(credits.get("pg_pricing"))
262
262
 
263
263
 
264
- def _print_pg_expensive_section(expensive: object) -> None:
265
- """Render the `credits.pg_expensive` block as a flat dotted-path table.
264
+ def _print_pg_pricing_section(pricing: object) -> None:
265
+ """Render the `credits.pg_pricing` block as a flat dotted-path table.
266
266
 
267
267
  The server emits a three-level nested structure
268
268
  ``{base: {pg: float}, tables: {name: float}, columns: {"t.c": float}}``;
269
- flattening each leaf to ``<section>.<key>`` keeps the live extras
270
- visible in one sorted scan, with the base rate clearly distinguished
271
- from the per-table and per-column extras a query may or may not
272
- incur.
269
+ flattening each leaf to ``<section>.<key>`` keeps the live rates visible
270
+ in one sorted scan, with the default per-row rate (``default.pg``)
271
+ distinguished from the per-table rates and per-column extras a query
272
+ may or may not incur.
273
273
  """
274
- if not isinstance(expensive, dict) or not expensive:
274
+ if not isinstance(pricing, dict) or not pricing:
275
275
  return
276
276
  rows: list[tuple[str, float]] = []
277
- for section, body in expensive.items():
277
+ for section, body in pricing.items():
278
278
  if not isinstance(body, dict):
279
279
  # Forward-compat: an unexpected leaf type — surface as-is
280
280
  # under the section name rather than dropping it silently.
@@ -284,15 +284,16 @@ def _print_pg_expensive_section(expensive: object) -> None:
284
284
  rows.append((f"{section}.{key}", val))
285
285
  if not rows:
286
286
  return
287
- sub = Table(title="PG expensive items (live)")
287
+ sub = Table(title="PG per-row pricing (live)")
288
288
  sub.add_column("Path", style="bold")
289
- sub.add_column("Extra", justify="right")
289
+ sub.add_column("Rate", justify="right")
290
290
  for path, val in sorted(rows):
291
291
  sub.add_row(path, _fmt_credits(val))
292
292
  console.print(sub)
293
293
  console.print(
294
- "[dim]These are the rates, not a per-query total. For the actual cost "
295
- "of a specific query (before running it), use[/dim] "
294
+ "[dim]These are the per-table / per-column rates, not a per-query total. "
295
+ "A query's per-row rate is the sum of the rates of the tables it touches. "
296
+ "For the actual cost of a specific query (before running it), use[/dim] "
296
297
  "[cyan]tl db pg \"SELECT ...\" --pricing[/cyan][dim].[/dim]"
297
298
  )
298
299
 
@@ -210,6 +210,54 @@ def top_brands_cmd(
210
210
  _do_top("brands", tag, args or [], fmt, limit, TOP_BRAND_COLUMNS, f"Top brands: {tag}")
211
211
 
212
212
 
213
+ @app.command("channels-with-tag")
214
+ def channels_with_tag_cmd(
215
+ tag: str = typer.Argument(..., help='Similarity tag name (e.g. "Cooking", "Age 18-24"). Run `tl recommender tags` to discover valid names.'),
216
+ min_value: float = typer.Option(0.00001, "--min", help="Inclusive minimum tag value; only channels scoring at or above this are returned. Defaults to 0.00001, which excludes channels with no loading on the tag."),
217
+ json_output: bool = typer.Option(False, "--json", help="JSON output"),
218
+ csv_output: bool = typer.Option(False, "--csv", help="CSV output"),
219
+ md_output: bool = typer.Option(False, "--md", help="Markdown output"),
220
+ toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
221
+ limit: int = typer.Option(100, "--limit", "-l", help="Max results per page (1-1000)"),
222
+ offset: int = typer.Option(0, "--offset", help="Pagination offset"),
223
+ ) -> None:
224
+ """Every channel whose value for a similarity tag is at or above a threshold.
225
+
226
+ Unlike `top-channels` (which ranks the strongest few), this walks the
227
+ *entire* match set in pages of up to 1000 — including sets larger than
228
+ the search index's 10k window — so you can enumerate every channel above
229
+ a cutoff. Returns channel IDs only; expand them with `tl channels show`
230
+ or `tl recommender inspect-channel`.
231
+
232
+ `--min` defaults to 0.00001 — just above zero — so a bare call returns
233
+ every channel with any loading on the tag and drops the zero-fill rest.
234
+ Raise it for a stricter cutoff.
235
+
236
+ Costs 1 credit per channel ID returned. Intelligence plan required.
237
+
238
+ Examples:
239
+ tl recommender channels-with-tag "Cooking"
240
+ tl recommender channels-with-tag "Age 18-24" --min 0.3 --limit 1000
241
+ tl recommender channels-with-tag "Cooking" --min 0.5 --offset 1000 --json
242
+ """
243
+ fmt = detect_format(json_output, csv_output, md_output, toon_output)
244
+ tag = _strip_quotes(tag)
245
+ params = {"tag": tag, "min": str(min_value), "limit": str(limit), "offset": str(offset)}
246
+ client = get_client()
247
+ try:
248
+ data = client.get("/recommender/channels-with-tag", params=params)
249
+ output(
250
+ data,
251
+ fmt,
252
+ columns=["channel_id"],
253
+ title=f"Channels with {tag} >= {min_value}",
254
+ )
255
+ except ApiError as e:
256
+ handle_api_error(e)
257
+ finally:
258
+ client.close()
259
+
260
+
213
261
  @app.command("inspect-channel")
214
262
  def inspect_channel_cmd(
215
263
  channel_ref: str = typer.Argument(..., help="Channel ID (numeric) or name (partial match, must be unique)"),
@@ -540,7 +540,7 @@ def output_pricing_estimate(data: dict, fmt: str) -> None:
540
540
 
541
541
  Firebolt and Elasticsearch have no per-column extras — their estimate
542
542
  carries `per_row_extra=0` and empty expensive-item maps, so the
543
- breakdown table is skipped and only the volume-curve cost shows.
543
+ breakdown table is skipped and only the per-row cost shows.
544
544
  A `limit`/cost of `None` (e.g. a Firebolt query with no `LIMIT`) means
545
545
  the row count is unbounded and the cost can't be pinned ahead of time.
546
546
  """
@@ -567,24 +567,30 @@ def output_pricing_estimate(data: dict, fmt: str) -> None:
567
567
  else:
568
568
  console.print(
569
569
  " Estimated cost: [yellow]depends on rows returned[/yellow] "
570
- "(no row limit set — cost scales with the volume curve)"
570
+ "(no row limit set — cost is linear in rows returned)"
571
571
  )
572
- console.print(f" Multiplier (base + expensive tables): {multiplier}")
572
+ console.print(f" Per-row rate (sum of table rates): {multiplier}")
573
573
  console.print(f" Per-row extra (expensive columns): {per_row}")
574
+ agg_surcharge = est.get("agg_surcharge")
575
+ if agg_surcharge:
576
+ console.print(
577
+ f" Aggregate surcharge (flat): {agg_surcharge} "
578
+ f"[dim](≈{est.get('aggregated_rows', 0):,} rows aggregated)[/dim]"
579
+ )
574
580
  if planner_rows is not None:
575
581
  console.print(
576
582
  f" [dim]Planner row estimate (pre-LIMIT): {planner_rows:,}[/dim]"
577
583
  )
578
584
 
579
- tables = est.get("expensive_tables") or {}
585
+ tables = est.get("table_rates") or {}
580
586
  columns = est.get("expensive_columns") or {}
581
587
  if tables or columns:
582
- sub = Table(title="Expensive items this query touches")
588
+ sub = Table(title="Per-row rates this query touches")
583
589
  sub.add_column("Item", style="bold")
584
590
  sub.add_column("Kind")
585
- sub.add_column("Extra", justify="right")
591
+ sub.add_column("Rate", justify="right")
586
592
  for name, val in sorted(tables.items()):
587
- sub.add_row(name, "table (multiplier)", _fmt_credits(val))
593
+ sub.add_row(name, "table (per row)", f"{_fmt_credits(val)}/row")
588
594
  for path, val in sorted(columns.items()):
589
595
  sub.add_row(path, "column (per row)", f"{_fmt_credits(val)}/row")
590
596
  console.print(sub)