thoughtleaders-cli 0.6.0__tar.gz → 0.6.1__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.6.0 → thoughtleaders_cli-0.6.1}/.claude-plugin/plugin.json +1 -1
  2. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/AGENTS.md +1 -1
  3. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/PKG-INFO +4 -3
  4. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/README.md +3 -2
  5. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/pyproject.toml +1 -1
  6. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/skills/tl/SKILL.md +13 -10
  7. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/skills/tl/references/firebolt-schema.md +2 -2
  8. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/src/tl_cli/__init__.py +1 -1
  9. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/src/tl_cli/commands/recommender.py +103 -36
  10. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/.claude-plugin/marketplace.json +0 -0
  11. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/.github/workflows/python-publish.yml +0 -0
  12. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/.gitignore +0 -0
  13. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/CLAUDE.md +0 -0
  14. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/LICENSE +0 -0
  15. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/agents/tl-analyst.md +0 -0
  16. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/commands/tl-balance.md +0 -0
  17. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/commands/tl-reports.md +0 -0
  18. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/commands/tl-sponsorships.md +0 -0
  19. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/commands/tl.md +0 -0
  20. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/docs/architecture.md +0 -0
  21. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/hooks/hooks.json +0 -0
  22. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/hooks/scripts/post-usage.sh +0 -0
  23. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/hooks/scripts/pre-check.sh +0 -0
  24. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/skills/tl/references/business-glossary.md +0 -0
  25. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/skills/tl/references/elasticsearch-schema.md +0 -0
  26. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/skills/tl/references/postgres-schema.md +0 -0
  27. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/src/tl_cli/_completions.py +0 -0
  28. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/src/tl_cli/auth/__init__.py +0 -0
  29. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/src/tl_cli/auth/commands.py +0 -0
  30. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/src/tl_cli/auth/login.py +0 -0
  31. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/src/tl_cli/auth/pkce.py +0 -0
  32. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/src/tl_cli/auth/token_store.py +0 -0
  33. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/src/tl_cli/client/__init__.py +0 -0
  34. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/src/tl_cli/client/errors.py +0 -0
  35. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/src/tl_cli/client/http.py +0 -0
  36. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/src/tl_cli/commands/__init__.py +0 -0
  37. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/src/tl_cli/commands/ask.py +0 -0
  38. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/src/tl_cli/commands/balance.py +0 -0
  39. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/src/tl_cli/commands/brands.py +0 -0
  40. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/src/tl_cli/commands/changelog.py +0 -0
  41. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/src/tl_cli/commands/channels.py +0 -0
  42. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/src/tl_cli/commands/comments.py +0 -0
  43. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/src/tl_cli/commands/db.py +0 -0
  44. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/src/tl_cli/commands/deals.py +0 -0
  45. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/src/tl_cli/commands/describe.py +0 -0
  46. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/src/tl_cli/commands/doctor.py +0 -0
  47. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/src/tl_cli/commands/matches.py +0 -0
  48. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/src/tl_cli/commands/proposals.py +0 -0
  49. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/src/tl_cli/commands/reports.py +0 -0
  50. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/src/tl_cli/commands/schema.py +0 -0
  51. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/src/tl_cli/commands/setup.py +0 -0
  52. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/src/tl_cli/commands/snapshots.py +0 -0
  53. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/src/tl_cli/commands/sponsorships.py +0 -0
  54. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/src/tl_cli/commands/uploads.py +0 -0
  55. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/src/tl_cli/commands/whoami.py +0 -0
  56. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/src/tl_cli/config.py +0 -0
  57. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/src/tl_cli/filters.py +0 -0
  58. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/src/tl_cli/hints.py +0 -0
  59. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/src/tl_cli/main.py +0 -0
  60. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/src/tl_cli/output/__init__.py +0 -0
  61. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/src/tl_cli/output/formatter.py +0 -0
  62. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/src/tl_cli/self_update.py +0 -0
  63. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/tests/__init__.py +0 -0
  64. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/tests/test_auth.py +0 -0
  65. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/tests/test_filters.py +0 -0
  66. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/tests/test_output.py +0 -0
  67. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/tests/test_sponsorships.py +0 -0
  68. {thoughtleaders_cli-0.6.0 → thoughtleaders_cli-0.6.1}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tl-cli",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "ThoughtLeaders CLI — query sponsorship deals, channels, brands, uploads, and intelligence from the terminal",
5
5
  "author": {
6
6
  "name": "ThoughtLeaders",
@@ -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`, `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.
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.
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.0
3
+ Version: 0.6.1
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
@@ -109,8 +109,9 @@ tl channels similar "Tremending girls" min-score:0.85 --limit 5
109
109
  # `tags` is free; `top`, `inspect-*`, and `similar-to-profile` cost 50 credits flat.
110
110
  tl recommender tags # List every tag (free)
111
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
112
+ tl recommender top-channels "Cooking" msn:yes --limit 50 # Top channels for a tag
113
+ tl recommender top-profiles "Cooking" mbn:yes --limit 30 # Top brand profiles (one brand → potentially multiple profiles)
114
+ tl recommender top-brands "Cooking" --limit 30 # Top brands (deduped from profiles)
114
115
  tl recommender inspect-channel 12345 # Per-tag breakdown of a channel's vector
115
116
  tl recommender inspect-brand Nike # Per-tag breakdown of a brand's ideal vector
116
117
  tl recommender similar-to-profile 842 # Channels closest to a brand profile
@@ -82,8 +82,9 @@ tl channels similar "Tremending girls" min-score:0.85 --limit 5
82
82
  # `tags` is free; `top`, `inspect-*`, and `similar-to-profile` cost 50 credits flat.
83
83
  tl recommender tags # List every tag (free)
84
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
85
+ tl recommender top-channels "Cooking" msn:yes --limit 50 # Top channels for a tag
86
+ tl recommender top-profiles "Cooking" mbn:yes --limit 30 # Top brand profiles (one brand → potentially multiple profiles)
87
+ tl recommender top-brands "Cooking" --limit 30 # Top brands (deduped from profiles)
87
88
  tl recommender inspect-channel 12345 # Per-tag breakdown of a channel's vector
88
89
  tl recommender inspect-brand Nike # Per-tag breakdown of a brand's ideal vector
89
90
  tl recommender similar-to-profile 842 # Channels closest to a brand profile
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "thoughtleaders-cli"
7
- version = "0.6.0"
7
+ version = "0.6.1"
8
8
  description = "ThoughtLeaders CLI — query sponsorship data, channels, brands, and intelligence"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -95,7 +95,7 @@ When querying sponsorship bookings, query by `status:sold` and filter the the da
95
95
 
96
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.
97
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.
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.
99
99
 
100
100
  ## Workflow
101
101
 
@@ -139,7 +139,9 @@ tl brands history <id-or-name> # Sponsorship history (5 credits/result,
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
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)
142
+ tl recommender top-channels "<tag>" # Top channels loaded on a vector tag (50 credits; Intelligence)
143
+ tl recommender top-profiles "<tag>" # Top brand profiles loaded on a vector tag (50 credits)
144
+ tl recommender top-brands "<tag>" # Top brands (deduped from profiles) loaded on a vector tag (50 credits)
143
145
  tl recommender inspect-channel <ref> # Show a channel's feature-vector breakdown (50 credits; Intelligence)
144
146
  tl recommender inspect-brand <ref> # Show a brand profile's ideal-vector breakdown (50 credits; Intelligence)
145
147
  tl recommender similar-to-profile <id> # Channels closest to a brand profile's ideal vector (50 credits; Intelligence)
@@ -332,9 +334,9 @@ tl recommender tags cooking
332
334
  tl recommender tags "usa"
333
335
 
334
336
  # 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
337
+ tl recommender top-channels "Cooking" msn:yes --limit 50
338
+ tl recommender top-channels "Tech" --limit 30
339
+ tl recommender top-brands "USA share" mbn:yes --limit 50
338
340
  ```
339
341
 
340
342
  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.
@@ -434,8 +436,8 @@ tl reports run 42 --json
434
436
  "Find Cooking channels with US-heavy mobile audiences":
435
437
  ```bash
436
438
  # 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
+ tl recommender top-channels "Cooking" msn:yes --limit 100 --json \
440
+ | jq -r '.results[].channel_id' \
439
441
  | paste -sd, - \
440
442
  | xargs -I {} tl db pg "SELECT id, channel_name, total_views, demographic_usa_share
441
443
  FROM thoughtleaders_channel
@@ -467,9 +469,10 @@ tl channels similar 29834 min-subs:1000000 exclude:477487 --limit 15 # client-s
467
469
  ```bash
468
470
  tl recommender tags # Full tag list (free)
469
471
  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
472
+ tl recommender top-channels "Cooking" msn:yes --limit 50 # Top channels loaded on a tag (50 credits)
473
+ tl recommender top-profiles "Cooking" --limit 30 # Top brand profiles for the tag
474
+ tl recommender top-brands "USA share" mbn:yes --limit 30 # Top brands (deduped) demographic tag, MBN only
475
+ tl recommender top-channels "Tech" exclude-for-profile:842 # Drop channels already proposed for profile 842
473
476
  tl recommender inspect-channel 29834 # Per-tag breakdown of a channel's vector
474
477
  tl recommender inspect-brand Nike # Per-tag breakdown of a brand's ideal vector
475
478
  tl recommender similar-to-profile 842 --limit 30 # Channels closest to a brand profile's ideal vector
@@ -117,8 +117,8 @@ Every Firebolt workflow has two steps:
117
117
 
118
118
  ```bash
119
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'
120
+ tl recommender top-channels "Tech" msn:yes --limit 50 --json \
121
+ | jq '.results[].channel_id'
122
122
 
123
123
  # Or videos for a specific brand's deals (Postgres side, via tl sponsorships)
124
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.6.0"
3
+ __version__ = "0.6.1"
@@ -19,10 +19,12 @@ from tl_cli.client.http import get_client
19
19
  from tl_cli.filters import parse_filters
20
20
  from tl_cli.output.formatter import detect_format, output, output_single
21
21
 
22
- app = typer.Typer(help="Vector recommender (tags, top-by-tag, vector inspection, profile→channel similarity)")
22
+ app = typer.Typer(help="Vector recommender (tags, top-channels/profiles/brands, vector inspection, profile→channel similarity)")
23
23
 
24
24
 
25
- TOP_COLUMNS = ["kind", "value", "channel_id", "profile_id", "name", "brand_name", "slug"]
25
+ TOP_CHANNEL_COLUMNS = ["value", "channel_id", "channel_name", "slug"]
26
+ TOP_PROFILE_COLUMNS = ["value", "profile_id", "profile_email", "brand_name", "brand_slug"]
27
+ TOP_BRAND_COLUMNS = ["value", "brand_slug", "brand_name", "profile_id"]
26
28
  TOP_COLUMN_CONFIG = {"value": {"justify": "right"}}
27
29
 
28
30
 
@@ -70,7 +72,7 @@ def tags_cmd(
70
72
  tl recommender tags "age 18"
71
73
  """
72
74
  fmt = detect_format(json_output, csv_output, md_output, toon_output)
73
- query = " ".join(args or []).strip()
75
+ query = _strip_quotes(" ".join(args or []).strip())
74
76
  params = {"q": query} if query else {}
75
77
  client = get_client()
76
78
  try:
@@ -78,7 +80,7 @@ def tags_cmd(
78
80
  output(
79
81
  data,
80
82
  fmt,
81
- columns=["group", "field_name", "normalized_name"],
83
+ columns=["group", "field_name"],
82
84
  title="Recommender vector tags",
83
85
  )
84
86
  except ApiError as e:
@@ -87,36 +89,21 @@ def tags_cmd(
87
89
  client.close()
88
90
 
89
91
 
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.
92
+ def _strip_quotes(value: str) -> str:
93
+ """Strip one matching pair of surrounding quotes if present.
101
94
 
102
- Costs 50 credits per call. Intelligence plan required. Returns both
103
- channels and profiles ranked by the tag's value (descending).
95
+ Lets users paste an example like `tl recommender top-channels "cooking"`
96
+ where the shell already strips quotes, but also tolerates a layer of
97
+ extra quoting from agents or scripts that re-wrap the literal.
98
+ """
99
+ if len(value) >= 2 and value[0] == value[-1] and value[0] in ('"', "'"):
100
+ return value[1:-1]
101
+ return value
104
102
 
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
103
 
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)
104
+ def _do_top(kind: str, tag: str, args: list[str], fmt: str, limit: int, columns: list[str], title: str) -> None:
105
+ tag = _strip_quotes(tag)
118
106
  filters = parse_filters(args or [])
119
-
120
107
  server_keys = {"msn", "mbn", "exclude-for-channel", "exclude-for-profile"}
121
108
  params = {k: v for k, v in filters.items() if k in server_keys}
122
109
  params["tag"] = tag
@@ -124,15 +111,12 @@ def top_cmd(
124
111
 
125
112
  client = get_client()
126
113
  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 ""
114
+ data = client.get(f"/recommender/top/{kind}", params=params)
131
115
  output(
132
116
  data,
133
117
  fmt,
134
- columns=TOP_COLUMNS,
135
- title=f"Top by tag: {tag}",
118
+ columns=columns,
119
+ title=title,
136
120
  column_config=TOP_COLUMN_CONFIG,
137
121
  )
138
122
  except ApiError as e:
@@ -141,6 +125,89 @@ def top_cmd(
141
125
  client.close()
142
126
 
143
127
 
128
+ @app.command("top-channels")
129
+ 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
+ args: list[str] = typer.Argument(None, help="Filters (key:value pairs)."),
132
+ json_output: bool = typer.Option(False, "--json", help="JSON output"),
133
+ csv_output: bool = typer.Option(False, "--csv", help="CSV output"),
134
+ md_output: bool = typer.Option(False, "--md", help="Markdown output"),
135
+ toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
136
+ limit: int = typer.Option(50, "--limit", "-l", help="Max results (1-100)"),
137
+ ) -> None:
138
+ """Top channels loaded on a single vector tag.
139
+
140
+ Costs 50 credits per call. Intelligence plan required.
141
+
142
+ Filters:
143
+ msn:<yes|no|all> MSN membership (default: all)
144
+ exclude-for-profile:<id> Drop channels already proposed for this profile
145
+
146
+ Examples:
147
+ tl recommender top-channels "Cooking"
148
+ tl recommender top-channels "Tech" msn:yes --limit 30
149
+ tl recommender top-channels "Cooking" exclude-for-profile:842
150
+ """
151
+ fmt = detect_format(json_output, csv_output, md_output, toon_output)
152
+ _do_top("channels", tag, args or [], fmt, limit, TOP_CHANNEL_COLUMNS, f"Top channels: {tag}")
153
+
154
+
155
+ @app.command("top-profiles")
156
+ 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
+ args: list[str] = typer.Argument(None, help="Filters (key:value pairs)."),
159
+ json_output: bool = typer.Option(False, "--json", help="JSON output"),
160
+ csv_output: bool = typer.Option(False, "--csv", help="CSV output"),
161
+ md_output: bool = typer.Option(False, "--md", help="Markdown output"),
162
+ toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
163
+ limit: int = typer.Option(50, "--limit", "-l", help="Max results (1-100)"),
164
+ ) -> None:
165
+ """Top brand profiles loaded on a single vector tag.
166
+
167
+ Costs 50 credits per call. Intelligence plan required. Profiles can
168
+ represent the same brand more than once (one brand → multiple
169
+ profiles); use `top-brands` for brand-deduplicated results.
170
+
171
+ Filters:
172
+ mbn:<yes|no|all> MBN membership (default: all)
173
+ exclude-for-channel:<id> Drop profiles already proposed for this channel
174
+
175
+ Examples:
176
+ tl recommender top-profiles "Cooking"
177
+ tl recommender top-profiles "USA share" mbn:yes --limit 30
178
+ """
179
+ fmt = detect_format(json_output, csv_output, md_output, toon_output)
180
+ _do_top("profiles", tag, args or [], fmt, limit, TOP_PROFILE_COLUMNS, f"Top profiles: {tag}")
181
+
182
+
183
+ @app.command("top-brands")
184
+ 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
+ args: list[str] = typer.Argument(None, help="Filters (key:value pairs)."),
187
+ json_output: bool = typer.Option(False, "--json", help="JSON output"),
188
+ csv_output: bool = typer.Option(False, "--csv", help="CSV output"),
189
+ md_output: bool = typer.Option(False, "--md", help="Markdown output"),
190
+ toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
191
+ limit: int = typer.Option(50, "--limit", "-l", help="Max results (1-100)"),
192
+ ) -> None:
193
+ """Top brands loaded on a single vector tag (deduplicated from profiles).
194
+
195
+ Costs 50 credits per call. Intelligence plan required. Server-side
196
+ aggregates the underlying profile rows by brand, keeping the
197
+ highest-scoring profile per brand.
198
+
199
+ Filters:
200
+ mbn:<yes|no|all> MBN membership of the underlying profile (default: all)
201
+ exclude-for-channel:<id> Drop brands already proposed for this channel
202
+
203
+ Examples:
204
+ tl recommender top-brands "Cooking"
205
+ tl recommender top-brands "USA share" mbn:yes --limit 30
206
+ """
207
+ fmt = detect_format(json_output, csv_output, md_output, toon_output)
208
+ _do_top("brands", tag, args or [], fmt, limit, TOP_BRAND_COLUMNS, f"Top brands: {tag}")
209
+
210
+
144
211
  @app.command("inspect-channel")
145
212
  def inspect_channel_cmd(
146
213
  channel_ref: str = typer.Argument(..., help="Channel ID (numeric) or name (partial match, must be unique)"),