thoughtleaders-cli 0.7.9__tar.gz → 0.7.11__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 (132) hide show
  1. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/.claude-plugin/plugin.json +1 -1
  2. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/AGENTS.md +1 -1
  3. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/PKG-INFO +1 -3
  4. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/README.md +0 -2
  5. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/pyproject.toml +1 -1
  6. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl/SKILL.md +13 -12
  7. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl/references/elasticsearch-schema.md +22 -22
  8. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl/references/firebolt-schema.md +1 -1
  9. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl/references/postgres-schema.md +2 -2
  10. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-save-report/SKILL.md +2 -2
  11. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/__init__.py +1 -1
  12. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/channels.py +6 -3
  13. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/reports.py +3 -4
  14. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/setup.py +129 -25
  15. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/self_update.py +10 -2
  16. thoughtleaders_cli-0.7.11/tests/test_setup.py +149 -0
  17. thoughtleaders_cli-0.7.9/skills/tl-import/SKILL.md +0 -289
  18. thoughtleaders_cli-0.7.9/skills/tl-report-builder/SKILL.md +0 -1455
  19. thoughtleaders_cli-0.7.9/skills/tl-report-builder/examples/e2e_findings.md +0 -269
  20. thoughtleaders_cli-0.7.9/skills/tl-report-builder/examples/golden_queries.md +0 -150
  21. thoughtleaders_cli-0.7.9/skills/tl-report-builder/references/columns_brands.md +0 -82
  22. thoughtleaders_cli-0.7.9/skills/tl-report-builder/references/columns_channels.md +0 -99
  23. thoughtleaders_cli-0.7.9/skills/tl-report-builder/references/columns_content.md +0 -78
  24. thoughtleaders_cli-0.7.9/skills/tl-report-builder/references/columns_sponsorships.md +0 -96
  25. thoughtleaders_cli-0.7.9/skills/tl-report-builder/references/intelligence_filterset_schema.json +0 -407
  26. thoughtleaders_cli-0.7.9/skills/tl-report-builder/references/intelligence_widget_schema.json +0 -194
  27. thoughtleaders_cli-0.7.9/skills/tl-report-builder/references/report_glossary.md +0 -145
  28. thoughtleaders_cli-0.7.9/skills/tl-report-builder/references/sponsorship_filterset_schema.json +0 -217
  29. thoughtleaders_cli-0.7.9/skills/tl-report-builder/references/sponsorship_widget_schema.json +0 -165
  30. thoughtleaders_cli-0.7.9/skills/tl-report-builder/references/widgets.md +0 -184
  31. thoughtleaders_cli-0.7.9/skills/tl-report-builder/tools/column_builder.md +0 -384
  32. thoughtleaders_cli-0.7.9/skills/tl-report-builder/tools/database_query.md +0 -210
  33. thoughtleaders_cli-0.7.9/skills/tl-report-builder/tools/name_resolver.md +0 -162
  34. thoughtleaders_cli-0.7.9/skills/tl-report-builder/tools/sample_judge.md +0 -223
  35. thoughtleaders_cli-0.7.9/skills/tl-report-builder/tools/similar_channels.md +0 -61
  36. thoughtleaders_cli-0.7.9/skills/tl-report-builder/tools/topic_matcher.md +0 -289
  37. thoughtleaders_cli-0.7.9/skills/tl-report-builder/tools/widget_builder.md +0 -278
  38. thoughtleaders_cli-0.7.9/skills/tl-save-report/references/sortable_columns.json +0 -64
  39. thoughtleaders_cli-0.7.9/tests/test_setup.py +0 -41
  40. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/.claude-plugin/marketplace.json +0 -0
  41. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/.github/workflows/python-publish.yml +0 -0
  42. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/.gitignore +0 -0
  43. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/API.md +0 -0
  44. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/CLAUDE.md +0 -0
  45. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/LICENSE +0 -0
  46. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/agents/tl-analyst.md +0 -0
  47. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/agents/youtube-comment-classifier.md +0 -0
  48. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/hooks/hooks.json +0 -0
  49. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/hooks/scripts/load-tl-skill.mjs +0 -0
  50. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/hooks/scripts/post-usage.sh +0 -0
  51. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/hooks/scripts/pre-check.sh +0 -0
  52. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl/references/business-glossary.md +0 -0
  53. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/.gitignore +0 -0
  54. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/SKILL.md +0 -0
  55. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/references/comment-patterns.md +0 -0
  56. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/references/peer-cohort.md +0 -0
  57. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/references/red-flags.md +0 -0
  58. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/references/scoring.md +0 -0
  59. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/scripts/_io_utf8.py +0 -0
  60. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/scripts/analyze_channel.py +0 -0
  61. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/scripts/anomaly_detector.py +0 -0
  62. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/scripts/comment_analyzer.py +0 -0
  63. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/scripts/comment_scraper.py +0 -0
  64. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/scripts/engagement_ratios.py +0 -0
  65. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/scripts/peer_cohort.py +0 -0
  66. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/scripts/report.py +0 -0
  67. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/scripts/resolve_channel.py +0 -0
  68. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/scripts/score.py +0 -0
  69. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/scripts/tl_cli.py +0 -0
  70. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/scripts/video_integrity.py +0 -0
  71. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/scripts/view_curves.py +0 -0
  72. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-keyword-research/SKILL.md +0 -0
  73. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-keyword-research/scripts/probe.py +0 -0
  74. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-save-report/references/columns_brands.md +0 -0
  75. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-save-report/references/columns_channels.md +0 -0
  76. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-save-report/references/columns_content.md +0 -0
  77. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-save-report/references/columns_sponsorships.md +0 -0
  78. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-save-report/references/intelligence_filterset_schema.json +0 -0
  79. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-save-report/references/intelligence_widget_schema.json +0 -0
  80. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-save-report/references/report_glossary.md +0 -0
  81. {thoughtleaders_cli-0.7.9/skills/tl-report-builder → thoughtleaders_cli-0.7.11/skills/tl-save-report}/references/sortable_columns.json +0 -0
  82. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-save-report/references/sponsorship_filterset_schema.json +0 -0
  83. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-save-report/references/sponsorship_widget_schema.json +0 -0
  84. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-save-report/references/widgets.md +0 -0
  85. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-top-partnerships/SKILL.md +0 -0
  86. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-top-partnerships/scripts/top_partnerships.py +0 -0
  87. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-views-guarantee/SKILL.md +0 -0
  88. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-views-guarantee/scripts/vg.py +0 -0
  89. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/_completions.py +0 -0
  90. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/_typer_utils.py +0 -0
  91. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/auth/__init__.py +0 -0
  92. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/auth/commands.py +0 -0
  93. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/auth/login.py +0 -0
  94. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/auth/pkce.py +0 -0
  95. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/auth/token_store.py +0 -0
  96. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/client/__init__.py +0 -0
  97. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/client/errors.py +0 -0
  98. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/client/http.py +0 -0
  99. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/__init__.py +0 -0
  100. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/_comments_common.py +0 -0
  101. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/balance.py +0 -0
  102. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/brands.py +0 -0
  103. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/bulk_import.py +0 -0
  104. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/changelog.py +0 -0
  105. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/credits.py +0 -0
  106. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/db.py +0 -0
  107. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/deals.py +0 -0
  108. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/describe.py +0 -0
  109. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/doctor.py +0 -0
  110. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/matches.py +0 -0
  111. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/proposals.py +0 -0
  112. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/recommender.py +0 -0
  113. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/schema.py +0 -0
  114. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/snapshots.py +0 -0
  115. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/sponsorships.py +0 -0
  116. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/uploads.py +0 -0
  117. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/whoami.py +0 -0
  118. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/config.py +0 -0
  119. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/filters.py +0 -0
  120. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/hints.py +0 -0
  121. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/main.py +0 -0
  122. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/output/__init__.py +0 -0
  123. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/output/formatter.py +0 -0
  124. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/tests/__init__.py +0 -0
  125. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/tests/test_auth.py +0 -0
  126. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/tests/test_describe.py +0 -0
  127. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/tests/test_filters.py +0 -0
  128. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/tests/test_http_auth.py +0 -0
  129. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/tests/test_output.py +0 -0
  130. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/tests/test_reports.py +0 -0
  131. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/tests/test_sponsorships.py +0 -0
  132. {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tl-cli",
3
- "version": "0.7.9",
3
+ "version": "0.7.11",
4
4
  "description": "ThoughtLeaders CLI — query sponsorship deals, channels, brands, uploads, and intelligence from the terminal",
5
5
  "author": {
6
6
  "name": "ThoughtLeaders",
@@ -53,7 +53,7 @@ This repo is also a Claude Code plugin, and can directly be installed as one.
53
53
 
54
54
  - **`tl`** — the main skill for querying ThoughtLeaders data. Default for any sponsorship / channel / brand / upload / report question.
55
55
  - **`tl-keyword-research`** — invoke whenever the user wants to find videos or channels by **content keywords** (topics, concepts, niches) that aren't covered by a curated recommender tag, OR to validate that a candidate channel's content actually touches a given topic. Returns `{operator, keywords:[{keyword,count}]}` from a ranked ES probe over `title` / `summary` / `transcript`; the caller then runs the actual content search with the surviving high-count terms. **Do not compose keyword sets by hand for `tl db es` content searches — delegate to this skill first.** See `skills/tl/SKILL.md` → *Channel & video discovery* for the four-path decision tree and when to use this vs the recommender / raw SQL.
56
- - **`tl-import`**, **`tl-save-report`**, **`adapt-tl-data`**, **`tl-views-guarantee`**, **`tl-top-partnerships`** — narrower workflows; the skill files document their own triggers. `tl-top-partnerships` is brand-user-facing: ranks a brand's sold sponsorships by live eCPM vs the sold-date projection and delivers a two-tab Google Sheet via `gws`.
56
+ - **`tl-save-report`**, **`adapt-tl-data`**, **`tl-views-guarantee`**, **`tl-top-partnerships`** — narrower workflows; the skill files document their own triggers. `tl-top-partnerships` is brand-user-facing: ranks a brand's sold sponsorships by live eCPM vs the sold-date projection and delivers a two-tab Google Sheet via `gws`.
57
57
 
58
58
  ### Skill content boundaries
59
59
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: thoughtleaders-cli
3
- Version: 0.7.9
3
+ Version: 0.7.11
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
@@ -280,8 +280,6 @@ The plugin ships several focused skills (installed by all the `tl setup *` comma
280
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/`.
281
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
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.
284
- - **`tl-import`** / **`bulk-import`** — superuser-only; bulk-add or exclude lists of channels, brands, videos, or sponsorships against a report.
285
283
  - **`tl-channel-authenticity`** — vets a YouTube channel for non-organic views and bot/spam comments before booking (or after delivering) a sponsorship.
286
284
  - **`tl-views-guarantee`** — sizes a multi-video sponsorship buy for a channel, returning the video bundle size, views guarantee, and likelihood to hit.
287
285
  - **`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`).
@@ -252,8 +252,6 @@ The plugin ships several focused skills (installed by all the `tl setup *` comma
252
252
  - **`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/`.
253
253
  - **`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.
254
254
  - **`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").
255
- - **`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.
256
- - **`tl-import`** / **`bulk-import`** — superuser-only; bulk-add or exclude lists of channels, brands, videos, or sponsorships against a report.
257
255
  - **`tl-channel-authenticity`** — vets a YouTube channel for non-organic views and bot/spam comments before booking (or after delivering) a sponsorship.
258
256
  - **`tl-views-guarantee`** — sizes a multi-video sponsorship buy for a channel, returning the video bundle size, views guarantee, and likelihood to hit.
259
257
  - **`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`).
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "thoughtleaders-cli"
7
- version = "0.7.9"
7
+ version = "0.7.11"
8
8
  description = "ThoughtLeaders CLI — query sponsorship data, channels, brands, and intelligence"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -9,7 +9,7 @@ description: |
9
9
 
10
10
  ## Core Principles
11
11
 
12
- Run the `tl` CLI to query ThoughtLeaders' sponsorship platform data. Use it to answer questions about deals, channels, brands, uploads, metrics, etc. Use raw database queries via `tl db pg|fb|es` for everything.
12
+ Run the `tl` CLI to query ThoughtLeaders' sponsorship platform data. Use it to answer questions about deals, channels, brands, uploads, metrics, etc. Use raw database queries via `tl db pg|fb|es` for everything. One exception: resolving a named channel or brand (name, YouTube URL, @handle, video URL) to an ID is always `tl channels find` / `tl brands find` — never `ILIKE` on names.
13
13
 
14
14
  If doing a database query, follow this recipe:
15
15
 
@@ -25,13 +25,13 @@ If doing a database query, follow this recipe:
25
25
  ```bash
26
26
  tl db pg "SELECT id, weighted_price FROM thoughtleaders_adlink
27
27
  WHERE publish_status = 3 AND price > 5000
28
- LIMIT 500 OFFSET 0" --json \
28
+ LIMIT 10000 OFFSET 0" --json \
29
29
  | jq '.results[] | {id, price: .weighted_price}'
30
30
  ```
31
31
  - **`yq`** — same idea for YAML/TOML, useful when reading config files or `--md` blocks.
32
32
  - **`rg`** — fast text search across CLI output, transcripts, and the codebase. Better than `grep` for searching large `--csv` exports or transcript dumps from ES.
33
33
  ```bash
34
- tl db es '{"size":500,"query":{"term":{"channel.id":5607}},"_source":["id","transcript"]}' --json | rg -o "NordVPN[^.]*"
34
+ tl db es '{"size":10000,"query":{"term":{"channel.id":5607}},"_source":["id","transcript"]}' --json | rg -o "NordVPN[^.]*"
35
35
  ```
36
36
  - **`duckdb`** — embedded analytical SQL over CSV/JSON files. Use when you need joins, aggregations, or window functions across multiple `tl` exports without spinning up a database.
37
37
  ```bash
@@ -42,13 +42,13 @@ If doing a database query, follow this recipe:
42
42
  JOIN thoughtleaders_brand b ON b.id = pb.brand_id
43
43
  WHERE al.publish_status = 3
44
44
  AND al.purchase_date >= '2026-01-01'
45
- LIMIT 500 OFFSET 0" --csv > deals.csv
45
+ LIMIT 10000 OFFSET 0" --csv > deals.csv
46
46
  duckdb -c "SELECT brand, SUM(price) AS revenue FROM 'deals.csv' GROUP BY brand ORDER BY revenue DESC LIMIT 10"
47
47
  ```
48
48
 
49
49
  The pattern is always: server-side narrowing first (usually by filters in the `tl db` query, but could be from similarity / recommender searches), then shell tool to shape the result, then read only the final summary into context. If `tl doctor` reports any of these as missing, ask the user to install them.
50
50
 
51
- Always assume there will be more than 1 page of results. You MUST always pass `LIMIT` and `OFFSET` to every `tl db pg|fb|es` query (and use the response envelope's `next_offset` / breadcrumbs to walk forward) so the entire data set is retrieved. The maximum number of rows per page is present in the output of `whoami`.
51
+ Always assume there will be more than 1 page of results. You MUST always pass `LIMIT` and `OFFSET` to every `tl db pg|fb|es` query (and use the response envelope's `next_offset` / breadcrumbs to walk forward) so the entire data set is retrieved. Prefer large pages (up to the engine's cap) to minimize round-trips; the per-engine page-size caps are documented in each engine's schema reference under `references/`.
52
52
 
53
53
  **Counts, totals, and breakdowns: aggregate in the query engine — never page through records to count them.** A "how many / total / average / per-X" question is ONE aggregation query, not N pages of rows summed in your head:
54
54
  - `tl db pg` — `SELECT COUNT(*) …`, or `SELECT col, COUNT(*) AS n … GROUP BY col ORDER BY n DESC`. Also `SUM`/`AVG`/`MIN`/`MAX`/`date_trunc`. Returns one/few rows regardless of table size. (`LIMIT`/`OFFSET` still required — an aggregate is one row, so `LIMIT 1 OFFSET 0` is fine.)
@@ -95,7 +95,8 @@ Other key concepts:
95
95
  - **MBN** (Media Buying Network) — the brand-side counterpart to MSN: brand profiles that have opted in to receive proposed sponsorships. A profile is in the MBN group if the `profile.media_buying_network_join_date` field is not null.
96
96
  - **TPP** (ThoughtLeaders Partner Program, a.k.a. "TL channels") — the ~170 channels TL has the closest working relationship with. A channel is in the TPP group if `channel.is_tl_channel` is True. **Prefer TPP channels when booking**: they respond fastest, are the easiest to close, and don't need an outreach round-trip — treat them as immediately bookable. TPP is a strict subset of MSN, so the same booking rules (one active mention adspot, etc.) apply.
97
97
  - **`demographics_updated_at`** (on channels) — If non-null, the channel has demographics screenshots on file. If null, no demographics screenshots have been uploaded. Use this to check whether a channel has demographics data from screenshots.
98
- - **`impression`** (on channels) — projected views per video on that channel. Forward-looking estimate. May be null when not yet computed.
98
+ - **`reach`** (on channels) — subscriber count. ⚠️ Despite the name, this is NOT ad-industry "reach" (unique audience exposed). There is no `subscribers` field `reach` is it.
99
+ - **`impression`** (on channels) — projected views per video on that channel. Forward-looking estimate. May be null when not yet computed. ⚠️ NOT actual views and NOT ad-industry "impressions" (ads served).
99
100
  - **`views`** (on sponsorships) — actual view count of the sold and published sponsored video, accessible when `article_id` is set.
100
101
  - **`impressions_guarantee`** (on sponsorships) — projected/guaranteed impressions for the sponsorship. Numeric.
101
102
  - **Sponsorship detail fields** (returned by `tl sponsorships show <id> --json`) — 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.
@@ -147,7 +148,7 @@ Unless the user specifically asks for running a specific report or showing the r
147
148
 
148
149
  1. **Discover first**: 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).
149
150
  2. **Check credits**: Run `tl balance --json` before expensive queries. Warn the user if a query will cost many credits.
150
- 3. **Decide the method of discovery**: If the user want to explore certain topics, use the recommender commands. If it's more about filtering, construct a query for PG or ES.
151
+ 3. **Decide the method of discovery**: If the user named a specific channel, brand, or creator (a name, YouTube URL, @handle, or video URL), resolve it to an ID with `tl channels find` / `tl brands find` before anything else. If the user wants to explore certain topics, use the recommender commands. If it's more about filtering, construct a query for PG or ES.
151
152
  4. **Always use --json**: Parse JSON output for multi-step analysis.
152
153
  5. **Chain commands**: For complex questions, chain multiple `tl` commands, shell commands, and other tools.
153
154
  6. **Format results**: When the user asks for a list or tabular data, present the results as a well-formatted markdown table. Pick the most relevant columns and use clear headers. Sort the result by relevant criteria - if the user asked for "top performers", order by the performance metric; if the user asked for "most recent", sort by the pertinent date desc.
@@ -445,7 +446,7 @@ If unsure about what information to find where, read the [references/postgresql-
445
446
  | 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`). |
446
447
  | 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. |
447
448
  | ES `query_string`, `regexp`, `wildcard`, `fuzzy`, `more_like_this`, parent/child joins; any `script_*`; multiple aggregations in one body | **Unavailable** — not accepted. | Rewrite using `term`/`terms`/`match`/`bool`/`nested`. For multi-agg dashboards, run multiple `tl db es` calls and combine client-side. For "similar"-style queries, try `tl channels similar` / `tl brands similar` (server-implemented similarity search). |
448
- | 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. |
449
+ | ES deep pagination beyond `from+size = 10,000` | **Unavailable** — `scroll`, `pit`, and `search_after` aren't allowlisted; hits past the first 10,000 of a query are unreachable. | A single `size: 10000` page covers everything reachable. For bigger sweeps, slice into `publication_date` range windows of <10k hits each. |
449
450
  | ES index introspection (`_cat/indices`, mappings) | **Unavailable** — only `_search` is wired. | Read [references/elasticsearch-schema.md](references/elasticsearch-schema.md). It's manually maintained — update it when you discover new fields. |
450
451
  | Schema introspection on Postgres (`information_schema.columns`, `pg_class`, …) | **Partial** — catalog-resolving casts and many `pg_*` helpers are blocked. | Use `tl schema pg` for the live table/column listing, or read [references/postgres-schema.md](references/postgres-schema.md). |
451
452
 
@@ -485,7 +486,7 @@ tl channels find "MrBeast"
485
486
  tl brands find "NordVPN"
486
487
  ```
487
488
 
488
- If finding channels and brands fail, try variation on the name with or without whitespace.
489
+ `tl channels find` resolves spacing/typo variants on its own ("Deco Destiny" → "DecoDestiny") via YouTube lookups and fuzzy similarity matching no need to retry with hand-made name variations. A real channel that isn't in the index yet gets queued for analysis automatically (the response says to check back in ~24 hours). A plain "Not found" means even YouTube couldn't find it — treat that as the answer.
489
490
 
490
491
  **Path 2. Curated tag / category / demographic** — user named a topic that maps cleanly to a recommender tag (`"Cooking"`, `"Tech"`, `"USA share"`, content categories, format hints). Use the recommender — it ranks channels by how strongly they load on a tag, returning ranked similarity scores instead of forcing exact equality. It also returns matching brand profiles alongside the channels — useful when the user wants to know "who buys this kind of inventory."
491
492
 
@@ -559,7 +560,7 @@ For per-country share beyond the recommender's "USA share" tag, use the `demogra
559
560
 
560
561
  **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. For MSN-only / non-MSN lookups, run the same raw SQL with `media_selling_network_join_date IS [NOT] NULL` from a context that has access to it (full-access role), or rely on the recommender's MSN-aware filters: `tl recommender top-channels "<tag>" msn:yes|no|all`.
561
562
 
562
- **Anti-pattern: defaulting to `ILIKE` on `channel_name` for off-tag topic queries.** If the question is "channels about X" where X is a topic / concept / niche (not a literal substring you expect in channel names), reach for path 3 (`tl-keyword-research`), not `WHERE channel_name ILIKE '%X%'`. Channel-name `ILIKE` misses channels whose name doesn't literally contain X but whose content does; the keyword-research skill catches them via `title` / `summary` / `transcript`. Use `channel_name ILIKE` only when you actually expect the channel's name to contain the term (e.g. `"Crypto"` in `"My Happy Crypto"`) as a supplementary signal alongside path 3, not as a replacement for it.
563
+ **Anti-pattern: defaulting to `ILIKE` on `channel_name` for off-tag topic queries.** If the question is "channels about X" where X is a topic / concept / niche (not a literal substring you expect in channel names), reach for path 3 (`tl-keyword-research`), not `WHERE channel_name ILIKE '%X%'`. Channel-name `ILIKE` misses channels whose name doesn't literally contain X but whose content does; the keyword-research skill catches them via `title` / `summary` / `transcript`. Use `channel_name ILIKE` only when you actually expect the channel's name to contain the term (e.g. `"Crypto"` in `"My Happy Crypto"`) as a supplementary signal alongside path 3, not as a replacement for it. And for a *named entity* — a specific creator or channel — don't start with `ILIKE` at all: run `tl channels find "<name>"` first (path 1). Fall back to `ILIKE` name variations only if the resolver finds nothing, and treat a clean "Not found" from the resolver as the likely answer (the channel probably isn't in the index) rather than a cue for ever-broader scans.
563
564
 
564
565
  ### Output flags
565
566
  - `--json` — structured JSON output format (use this for parsing)
@@ -617,7 +618,7 @@ tl db pg "SELECT al.id, al.weighted_price, al.purchase_date, b.name AS brand
617
618
  WHERE al.publish_status = 3
618
619
  AND al.purchase_date >= '2026-01-01'
619
620
  ORDER BY al.purchase_date DESC
620
- LIMIT 500 OFFSET 0" --json
621
+ LIMIT 10000 OFFSET 0" --json
621
622
  ```
622
623
 
623
624
  ### Brand sponsorship history — what channels does Nike sponsor?
@@ -769,7 +770,7 @@ tl db pg "SELECT al.id, c.channel_name, c.demographic_device_primary, c.demograp
769
770
  WHERE al.publish_status = 3
770
771
  AND c.demographic_device_primary = 'mobile'
771
772
  AND c.demographic_usa_share >= 60
772
- LIMIT 500 OFFSET 0" --json
773
+ LIMIT 10000 OFFSET 0" --json
773
774
  ```
774
775
 
775
776
  ### "Find channels similar to one I know" (similarity recommender):
@@ -21,19 +21,19 @@ Output flags: `--json`, `--csv`, `--md`, `--toon`. The CLI flattens hits into ro
21
21
 
22
22
  See the output of `tl db es`" for the object schema. Highlights:
23
23
 
24
- - **Top-level keys** accepted: `query`, `aggs`/`aggregations`, `sort`, `_source`, `size`, `from`, `track_total_hits`, `highlight`, `fields`, `min_score`, `search_after`, `timeout`, `collapse`, `post_filter`. Anything else (incl. `scroll`, `pit`, `runtime_mappings`, `knn`) is not accepted.
25
- - `size` ≤ 500. `from + size` ≤ 10,000. Use `search_after` to page deeper.
24
+ - **Top-level keys** accepted: `query`, `aggs`/`aggregations`, `sort`, `_source`, `size`, `from`, `track_total_hits`, `highlight`, `fields`, `min_score`, `timeout`, `collapse`, `post_filter`. Anything else (incl. `scroll`, `pit`, `search_after`, `runtime_mappings`, `knn`) is not accepted.
25
+ - `size` ≤ 10,000. `from + size` ≤ 10,000 — hits beyond the first 10,000 of a query are unreachable; narrow the query (e.g. `publication_date` ranges) instead of paging deeper.
26
26
  - **Accepted query types** include `term`/`terms`/`match`/`bool`/`nested`/`range`/`exists`/`match_phrase`. `query_string`, `regexp`, `wildcard`, `fuzzy`, `more_like_this`, `has_child`, `has_parent`, `parent_id` are not accepted.
27
27
  - **No scripts** — any key whose name contains `script` is not accepted.
28
28
  - **At most one aggregation total** counted recursively (top-level + sub-agg = 2 = not accepted). Run multiple calls for multi-metric work.
29
29
 
30
30
  ### ElasticSearch document structure ("articles")
31
31
 
32
- The `doc_type.name` field in ES objects determins between records for video uploads and for channel data.
32
+ The `doc_type` join field distinguishes video uploads ("articles") from channel data channel docs are parents, article docs are their children. Filter with `{"term": {"doc_type": "article"}}` or `{"term": {"doc_type": "channel"}}`. ⚠️ Term-querying `doc_type.name` matches nothing — even though article docs' `_source` shows `doc_type` as an object with a `name` key, that's join-field syntax, not a queryable subfield.
33
33
 
34
34
  #### Upload/video Fields (selected — 73 total)
35
35
 
36
- Distinguished by `doc_type.name="article"`.
36
+ Filter with `{"term": {"doc_type": "article"}}`.
37
37
 
38
38
  | Field | Type | Description |
39
39
  |-------|------|-------------|
@@ -80,7 +80,7 @@ Distinguished by `doc_type.name="article"`.
80
80
 
81
81
  #### Channel Fields
82
82
 
83
- Distinguished by `doc_type.name="channel"`.
83
+ Filter with `{"term": {"doc_type": "channel"}}`.
84
84
 
85
85
  Contains a denormalized subset of the PostgreSQL channel data.
86
86
 
@@ -90,10 +90,10 @@ Contains a denormalized subset of the PostgreSQL channel data.
90
90
  |-------|------|-------------|
91
91
  | `name` | text | Channel name |
92
92
  | `channel` | object | Channel metadata (nested on article docs) |
93
- | `reach` | long | Subscriber count |
94
- | `impression` | long | View count |
95
- | `impression_live` | long | Live view count |
96
- | `impression_shorts` | long | Shorts view count |
93
+ | `reach` | long | Subscriber count. ⚠️ NOT ad-industry "reach" (unique audience exposed) — this is the channel's subscriber count. |
94
+ | `impression` | long | Projected views per longform video — forward-looking estimate. ⚠️ NOT actual views and NOT ad-industry "impressions"; for actual views see `total_views` / the video docs. |
95
+ | `impression_live` | long | Projected views per live stream (forward-looking estimate) |
96
+ | `impression_shorts` | long | Projected views per short (forward-looking estimate) |
97
97
  | `is_tl_channel` | boolean | TPP partner channel |
98
98
  | `is_active` | boolean | Channel is active |
99
99
  | `media_selling_network_join_date` | date | MSN join date |
@@ -215,25 +215,25 @@ tl db es '{
215
215
 
216
216
  For more dimensions, run multiple `tl db es` calls and join client-side.
217
217
 
218
- ### Deep pagination via `search_after`
218
+ ### Deep sweeps window by date, don't page past 10k
219
219
 
220
- ```bash
221
- # First page — sort must include a tiebreaker on _id for stability
222
- tl db es '{
223
- "size": 500,
224
- "query": {"term": {"channel.id": 12345}},
225
- "sort": [{"publication_date": "desc"}, {"_id": "asc"}]
226
- }'
220
+ `from + size` is capped at 10,000 and cursor keys (`search_after`, `scroll`, `pit`) are not accepted, so hits beyond the first 10,000 of any one query are unreachable. For result sets bigger than that, slice the query into non-overlapping `publication_date` (or other range-field) windows, each under 10,000 hits, and sweep window by window:
227
221
 
228
- # Subsequent pages — pass the last hit's sort values as search_after
222
+ ```bash
223
+ # One window — repeat with shifted date ranges until the full period is covered
229
224
  tl db es '{
230
- "size": 500,
231
- "query": {"term": {"channel.id": 12345}},
232
- "sort": [{"publication_date": "desc"}, {"_id": "asc"}],
233
- "search_after": ["2025-09-14", "12345:abc123"]
225
+ "size": 10000,
226
+ "track_total_hits": true,
227
+ "query": {"bool": {"filter": [
228
+ {"term": {"channel.id": 12345}},
229
+ {"range": {"publication_date": {"gte": "2025-01-01", "lt": "2025-04-01"}}}
230
+ ]}},
231
+ "sort": [{"publication_date": "asc"}]
234
232
  }'
235
233
  ```
236
234
 
235
+ Check `total` per window — if a window exceeds 10,000, split it further.
236
+
237
237
  ## Text analyzer behavior
238
238
 
239
239
  `text` fields on article docs (`title`, `summary`, `transcript`) appear to use the `standard` analyzer (tokenize + lowercase, no stemmer, no English-possessive filter), so inflections, plurals, and possessives are each indexed as distinct terms. For example: `bitcoin` (4,466,300) vs `bitcoins` (489,262). For stemming-style recall, expand the query side with a `bool.should` over the variants.
@@ -130,7 +130,7 @@ tl db pg "SELECT al.id, al.article_id, s.channel_id
130
130
  WHERE al.publish_status = 3
131
131
  AND b.name = 'Nike'
132
132
  AND al.article_id IS NOT NULL
133
- LIMIT 500 OFFSET 0" --json \
133
+ LIMIT 10000 OFFSET 0" --json \
134
134
  | jq -r '.results[] | "\(.channel_id):\(.article_id)"'
135
135
 
136
136
  # Or videos via Elasticsearch content search
@@ -315,7 +315,7 @@ FROM thoughtleaders_adlink
315
315
  WHERE publish_status = 3
316
316
  AND purchase_date >= date_trunc('month', CURRENT_DATE)
317
317
  ORDER BY purchase_date DESC
318
- LIMIT 500 OFFSET 0
318
+ LIMIT 10000 OFFSET 0
319
319
  ```
320
320
 
321
321
  **MSN channel joins this month:**
@@ -324,7 +324,7 @@ SELECT id, channel_name, media_selling_network_join_date
324
324
  FROM thoughtleaders_channel
325
325
  WHERE media_selling_network_join_date >= date_trunc('month', CURRENT_DATE)
326
326
  ORDER BY media_selling_network_join_date DESC
327
- LIMIT 500 OFFSET 0
327
+ LIMIT 10000 OFFSET 0
328
328
  ```
329
329
 
330
330
  **A specific sponsorship info with brand and channel name:**
@@ -56,7 +56,7 @@ The entity being saved must be one of: **channels**, **brands**, **videos / uplo
56
56
 
57
57
  **Skip when**:
58
58
 
59
- - The user wants to **add to an existing report** (`"add these channels to report 1234"`) → hand off to `tl-import`.
59
+ - The user wants to **add to an existing report** (`"add these channels to report 1234"`) → use the `tl bulk-import` command, not this skill.
60
60
  - The user only wants the data **shown / counted / analysed in chat** without saving → stay in `tl`; don't invoke this skill.
61
61
  - The user wants to build a report **from scratch** with no prior session exploration to capture — that's a different shape of request (the user has a goal, not a result set). Run the appropriate `tl db pg|fb|es` queries to produce a result set first; then this skill takes over for the save.
62
62
 
@@ -524,4 +524,4 @@ The above maps the visible CLI output to the underlying cause — match on a sub
524
524
 
525
525
  - **No discovery-side work** — no keyword research, no live-data sample validation, no result-set re-evaluation. The session already produced the data; re-running discovery would be wasted effort. Name resolution (`tl brands find` / `tl channels find` to turn names into IDs before they land in the FilterSet) is the one exception — it's required by the FilterSet schema, not discovery. If the user comes in with no prior session, run the relevant `tl db pg|fb|es` queries first to produce a result set, then invoke this skill on the result.
526
526
  - **No editing of existing reports.** If the user wants to refine an already-saved report's columns, widgets, title, or description, run `tl reports update <id>` directly. For FilterSet refinements, the platform requires saving a new variant.
527
- - **No bulk-importing into an existing report.** That's `tl-import`'s role. Save-report only creates new reports.
527
+ - **No bulk-importing into an existing report.** Use the `tl bulk-import` command for that. Save-report only creates new reports.
@@ -1,3 +1,3 @@
1
1
  """ThoughtLeaders CLI — query sponsorship data, channels, brands, and intelligence."""
2
2
 
3
- __version__ = "0.7.9"
3
+ __version__ = "0.7.11"
@@ -296,7 +296,9 @@ def find_cmd(
296
296
  """Resolve a string to a single channel.
297
297
 
298
298
  Accepts:
299
- - A partial channel name or slug (ILIKE match)
299
+ - A partial channel name or slug (substring match, falling back to
300
+ fuzzy similarity — spacing/typo variants like "Deco Destiny" still
301
+ resolve a channel named "DecoDestiny")
300
302
  - A YouTube channel URL (https://youtube.com/channel/UC...,
301
303
  https://youtube.com/@handle, /c/<name>, /user/<name>)
302
304
  - A raw YouTube channel ID (UC...) or @handle
@@ -308,8 +310,9 @@ def find_cmd(
308
310
  `{"id": ..., "name": ...}`).
309
311
 
310
312
  Ambiguous matches return an error with candidate IDs and names.
311
- If the input is a YouTube URL and no channel matches, the URL is
312
- queued for scraping; retry the command later.
313
+ If the input is a YouTube URL or a name that YouTube resolves to
314
+ a channel not yet in the index — it is queued for analysis; check
315
+ back in about 24 hours.
313
316
 
314
317
  Examples:
315
318
  tl channels find "MrBeast"
@@ -404,8 +404,8 @@ def create_report(
404
404
 
405
405
  With --config '<json>' or --config-file <path>, skips the orchestration
406
406
  pipeline and saves the provided config directly. Useful when an external
407
- agent (e.g. the tl-report-builder Claude Code skill) has already produced a
408
- validated config and you just want to persist it. Prefer --config-file when
407
+ agent has already produced a validated config and you just want to persist
408
+ it. Prefer --config-file when
409
409
  the config might contain apostrophes, dollar signs, or backticks — file
410
410
  transport sidesteps shell quoting entirely.
411
411
 
@@ -518,8 +518,7 @@ ENTITY_TO_REPORT_TYPE = {
518
518
 
519
519
  # FilterSet M2M field per entity is identical to the entity name, except
520
520
  # article IDs are composite strings (`<channel_id>:<youtube_id>`) and the
521
- # others are integers. See the FilterSet schema references in
522
- # skills/tl-report-builder/references/ for the catalogue.
521
+ # others are integers.
523
522
 
524
523
 
525
524
  def _read_ids(path: str, entity: str) -> list:
@@ -7,9 +7,12 @@ whenever either `gemini` or `codex` is on PATH. Behaviour follows the
7
7
  OpenCode pattern (full per-skill tree copy, .tl-version stamp).
8
8
  """
9
9
 
10
+ import filecmp
10
11
  import json
12
+ import os
11
13
  import shutil
12
14
  import subprocess
15
+ import sys
13
16
  from pathlib import Path
14
17
 
15
18
  import typer
@@ -41,6 +44,19 @@ OPENCODE_SKILLS_DIR = Path.home() / ".config" / "opencode" / "skills"
41
44
  AGENTS_SKILLS_DIR = Path.home() / ".agents" / "skills"
42
45
  AGENTS_SKILLS_BINARIES = ("gemini", "codex")
43
46
 
47
+ # Personal-command shim that keeps the short `/tl` invocation working when the
48
+ # skills are provided (namespaced) by the installed plugin. Plugin skills and
49
+ # commands are always invoked as `/tl-cli:<name>`; this one-file pointer in
50
+ # ~/.claude/commands/ restores plain `/tl` without duplicating any skill
51
+ # content, so plugin updates flow through automatically.
52
+ TL_COMMAND_SHIM = """\
53
+ ---
54
+ description: ThoughtLeaders data analyst — shortcut for the tl-cli plugin's tl skill
55
+ ---
56
+
57
+ Invoke the `tl-cli:tl` skill with this request: $ARGUMENTS
58
+ """
59
+
44
60
 
45
61
  def _find_plugin_root() -> Path | None:
46
62
  """Locate the plugin assets directory.
@@ -61,8 +77,31 @@ def _find_plugin_root() -> Path | None:
61
77
 
62
78
 
63
79
  def _find_claude_binary() -> str | None:
64
- """Find the claude binary on PATH."""
65
- return shutil.which("claude")
80
+ """Find the claude binary on PATH, falling back to known install locations.
81
+
82
+ On Windows the Claude Code installers often don't end up on the PATH of
83
+ the shell running `tl` (stale PATH, PowerShell-only profile changes), so
84
+ after `shutil.which` we probe the documented install targets directly:
85
+ the native installer (`~/.local/bin`) and the npm global prefix.
86
+ """
87
+ found = shutil.which("claude")
88
+ if found:
89
+ return found
90
+ home = Path.home()
91
+ if sys.platform == "win32":
92
+ candidates = [
93
+ home / ".local" / "bin" / "claude.exe",
94
+ Path(os.environ.get("APPDATA", str(home / "AppData" / "Roaming"))) / "npm" / "claude.cmd",
95
+ ]
96
+ else:
97
+ candidates = [
98
+ home / ".local" / "bin" / "claude",
99
+ home / ".claude" / "local" / "claude",
100
+ ]
101
+ for candidate in candidates:
102
+ if candidate.is_file():
103
+ return str(candidate)
104
+ return None
66
105
 
67
106
 
68
107
  def _run_claude(args: list[str], claude_bin: str) -> tuple[bool, str]:
@@ -156,6 +195,50 @@ def _install_standalone_skills(plugin_root: Path) -> int:
156
195
  return count
157
196
 
158
197
 
198
+ def _install_command_shim() -> Path:
199
+ """Write the `/tl` shim command to ~/.claude/commands/tl.md."""
200
+ CLAUDE_COMMANDS_DIR.mkdir(parents=True, exist_ok=True)
201
+ dst = CLAUDE_COMMANDS_DIR / "tl.md"
202
+ dst.write_text(TL_COMMAND_SHIM, encoding="utf-8")
203
+ return dst
204
+
205
+
206
+ def _trees_identical(a: Path, b: Path) -> bool:
207
+ """True if two directory trees contain the same files with the same contents."""
208
+ a_files = sorted(p.relative_to(a) for p in a.rglob("*") if p.is_file())
209
+ b_files = sorted(p.relative_to(b) for p in b.rglob("*") if p.is_file())
210
+ if a_files != b_files:
211
+ return False
212
+ return all(filecmp.cmp(a / rel, b / rel, shallow=False) for rel in a_files)
213
+
214
+
215
+ def _remove_matching_standalone_skills(plugin_root: Path) -> tuple[int, int]:
216
+ """Remove standalone copies in ~/.claude/skills/ that match the plugin's skills.
217
+
218
+ Earlier versions of `tl setup claude` copied every bundled skill into
219
+ ~/.claude/skills/. Now that the plugin provides them, those copies are
220
+ redundant — but a copy is only deleted when its tree is byte-identical
221
+ to the bundled skill, so user-modified copies are never touched.
222
+ Returns (removed, kept_modified).
223
+ """
224
+ removed = kept = 0
225
+ skills_src = plugin_root / "skills"
226
+ if not skills_src.is_dir():
227
+ return removed, kept
228
+ for skill_dir in skills_src.iterdir():
229
+ if not (skill_dir.is_dir() and (skill_dir / "SKILL.md").is_file()):
230
+ continue
231
+ standalone = CLAUDE_SKILLS_DIR / skill_dir.name
232
+ if not standalone.is_dir():
233
+ continue
234
+ if _trees_identical(skill_dir, standalone):
235
+ shutil.rmtree(standalone)
236
+ removed += 1
237
+ else:
238
+ kept += 1
239
+ return removed, kept
240
+
241
+
159
242
  def _bundled_skill_blurbs(plugin_root: Path) -> list[tuple[str, str]]:
160
243
  """Read (name, tl-blurb) for each bundled skill, for the setup summary.
161
244
 
@@ -192,18 +275,19 @@ def _bundled_skill_blurbs(plugin_root: Path) -> list[tuple[str, str]]:
192
275
 
193
276
 
194
277
  def _print_manual_instructions() -> None:
195
- """Print manual install instructions when claude binary is not found."""
278
+ """Print manual install instructions when the plugin couldn't be installed."""
196
279
  console.print()
197
- console.print("[yellow]Claude Code binary not found on PATH.[/yellow]")
280
+ console.print("[yellow]The Claude Code plugin could not be installed automatically.[/yellow]")
198
281
  console.print()
199
- console.print("Install Claude Code first, then run these commands inside Claude Code:")
282
+ console.print(f"The skills were installed to {CLAUDE_SKILLS_DIR} instead restart")
283
+ console.print("Claude Code and they will be available (e.g. [cyan]/tl[/cyan]).")
284
+ console.print()
285
+ console.print("To install the full plugin, run these commands inside Claude Code:")
200
286
  console.print()
201
287
  console.print(f" [cyan]/plugin marketplace add {MARKETPLACE_SOURCE}[/cyan]")
202
288
  console.print(f" [cyan]/plugin install {PLUGIN_KEY}[/cyan]")
203
289
  console.print()
204
- console.print("Or start Claude Code with the plugin loaded directly:")
205
- console.print()
206
- console.print(f" [cyan]claude --plugin-dir /path/to/tl-cli[/cyan]")
290
+ console.print("then re-run [cyan]tl setup claude[/cyan] to clean up the standalone copies.")
207
291
 
208
292
 
209
293
  @app.command("claude")
@@ -214,8 +298,10 @@ def setup_claude(
214
298
  """Install the TL CLI plugin for Claude Code.
215
299
 
216
300
  Registers the ThoughtLeaders marketplace, installs the tl-cli plugin,
217
- and copies skills/commands to ~/.claude/ for short /tl invocation.
218
- If the claude binary is not on PATH, prints manual instructions.
301
+ and adds a /tl shim command so the plugin's tl skill can be invoked
302
+ without the plugin namespace. Standalone skill copies in ~/.claude/skills
303
+ are only installed as a fallback when the plugin can't be installed;
304
+ unmodified copies left by earlier versions are removed.
219
305
 
220
306
  Examples:
221
307
  tl setup claude
@@ -249,10 +335,9 @@ def setup_claude(
249
335
  # Check claude binary
250
336
  claude_bin = _find_claude_binary()
251
337
  if not claude_bin:
252
- # Still install standalone skills even without claude binary
253
- console.print(" [yellow]![/yellow] claude binary not found on PATH")
338
+ # Fall back to standalone skill copies when the plugin can't be installed
339
+ console.print(" [yellow]![/yellow] claude binary not found")
254
340
  _install_standalone_skills_step(plugin_root)
255
- console.print()
256
341
  _print_manual_instructions()
257
342
  raise SystemExit(1)
258
343
 
@@ -271,6 +356,7 @@ def setup_claude(
271
356
  _run_claude(["plugin", "marketplace", "update", MARKETPLACE_NAME], claude_bin)
272
357
  else:
273
358
  console.print(f" [red]✗[/red] Marketplace registration failed: {output}")
359
+ _install_standalone_skills_step(plugin_root)
274
360
  _print_manual_instructions()
275
361
  raise SystemExit(1)
276
362
 
@@ -284,12 +370,20 @@ def setup_claude(
284
370
  console.print(f" [green]✓[/green] Plugin already installed: {PLUGIN_KEY}")
285
371
  else:
286
372
  console.print(f" [red]✗[/red] Plugin installation failed: {output}")
287
- console.print(" Try running inside Claude Code:")
288
- console.print(f" [cyan]/plugin install {PLUGIN_KEY}[/cyan]")
373
+ _install_standalone_skills_step(plugin_root)
374
+ _print_manual_instructions()
289
375
  raise SystemExit(1)
290
376
 
291
- # Step 3: Install standalone skills for short /tl invocation
292
- _install_standalone_skills_step(plugin_root)
377
+ # Step 3: /tl shim command + cleanup of standalone copies from older versions
378
+ console.print("[bold]Installing /tl shortcut...[/bold]")
379
+ shim = _install_command_shim()
380
+ console.print(f" [green]✓[/green] /tl command installed: {shim}")
381
+ removed, kept = _remove_matching_standalone_skills(plugin_root)
382
+ if removed:
383
+ console.print(f" [green]✓[/green] Removed {removed} standalone skill(s) now provided by the plugin")
384
+ if kept:
385
+ console.print(f" [yellow]![/yellow] Kept {kept} modified standalone skill(s) in {CLAUDE_SKILLS_DIR}")
386
+ console.print(" These differ from the plugin's versions and shadow nothing — remove manually if unwanted.")
293
387
 
294
388
  # Write version stamp
295
389
  version_dir = CLAUDE_PLUGINS_DIR / "tl-cli"
@@ -303,20 +397,20 @@ def setup_claude(
303
397
  blurbs = _bundled_skill_blurbs(plugin_root)
304
398
  width = max((len(name) for name, _ in blurbs), default=0)
305
399
  for name, blurb in blurbs:
306
- console.print(f" [cyan]/{name}[/cyan]{' ' * (width - len(name))} — {blurb}")
400
+ console.print(f" [cyan]/{PLUGIN_NAME}:{name}[/cyan]{' ' * (width - len(name))} — {blurb}")
307
401
  console.print()
308
- console.print("Try it:")
402
+ console.print("Try it (restart Claude Code first):")
309
403
  console.print(" [cyan]/tl Which channels did we sponsor in Q1?[/cyan]")
310
404
  console.print()
311
405
  console.print("[dim]To update, run: tl setup claude[/dim]")
312
406
 
313
407
 
314
408
  def _install_standalone_skills_step(plugin_root: Path) -> None:
315
- """Install standalone skills and print status."""
316
- console.print("[bold]Installing skills for /tl shortcut...[/bold]")
409
+ """Install standalone skills (plugin-less fallback) and print status."""
410
+ console.print("[bold]Installing standalone skills (plugin fallback)...[/bold]")
317
411
  count = _install_standalone_skills(plugin_root)
318
412
  if count > 0:
319
- console.print(f" [green]✓[/green] Installed {count} skills/commands to ~/.claude/")
413
+ console.print(f" [green]✓[/green] Installed {count} skills/commands to {CLAUDE_HOME}")
320
414
  else:
321
415
  console.print(" [yellow]![/yellow] No skills found to install")
322
416
 
@@ -362,9 +456,19 @@ def _setup_noninteractive(fmt: str = "json") -> None:
362
456
  result["marketplace_registered"] = False
363
457
  result["plugin_installed"] = False
364
458
 
365
- # Always install standalone skills
366
- count = _install_standalone_skills(plugin_root)
367
- result["standalone_skills_installed"] = count
459
+ if result["plugin_installed"]:
460
+ # Plugin provides the skills; install the /tl shim and clean up
461
+ # unmodified standalone copies left by earlier versions.
462
+ _install_command_shim()
463
+ removed, kept = _remove_matching_standalone_skills(plugin_root)
464
+ result["command_shim_installed"] = True
465
+ result["standalone_skills_installed"] = 0
466
+ result["standalone_skills_removed"] = removed
467
+ result["standalone_skills_kept_modified"] = kept
468
+ else:
469
+ # Fallback: standalone skill copies so Claude Code still gets /tl
470
+ result["command_shim_installed"] = False
471
+ result["standalone_skills_installed"] = _install_standalone_skills(plugin_root)
368
472
 
369
473
  # Write version stamp
370
474
  version_dir = CLAUDE_PLUGINS_DIR / "tl-cli"