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.
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/.claude-plugin/plugin.json +1 -1
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/AGENTS.md +1 -1
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/PKG-INFO +1 -3
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/README.md +0 -2
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/pyproject.toml +1 -1
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl/SKILL.md +13 -12
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl/references/elasticsearch-schema.md +22 -22
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl/references/firebolt-schema.md +1 -1
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl/references/postgres-schema.md +2 -2
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-save-report/SKILL.md +2 -2
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/__init__.py +1 -1
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/channels.py +6 -3
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/reports.py +3 -4
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/setup.py +129 -25
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/self_update.py +10 -2
- thoughtleaders_cli-0.7.11/tests/test_setup.py +149 -0
- thoughtleaders_cli-0.7.9/skills/tl-import/SKILL.md +0 -289
- thoughtleaders_cli-0.7.9/skills/tl-report-builder/SKILL.md +0 -1455
- thoughtleaders_cli-0.7.9/skills/tl-report-builder/examples/e2e_findings.md +0 -269
- thoughtleaders_cli-0.7.9/skills/tl-report-builder/examples/golden_queries.md +0 -150
- thoughtleaders_cli-0.7.9/skills/tl-report-builder/references/columns_brands.md +0 -82
- thoughtleaders_cli-0.7.9/skills/tl-report-builder/references/columns_channels.md +0 -99
- thoughtleaders_cli-0.7.9/skills/tl-report-builder/references/columns_content.md +0 -78
- thoughtleaders_cli-0.7.9/skills/tl-report-builder/references/columns_sponsorships.md +0 -96
- thoughtleaders_cli-0.7.9/skills/tl-report-builder/references/intelligence_filterset_schema.json +0 -407
- thoughtleaders_cli-0.7.9/skills/tl-report-builder/references/intelligence_widget_schema.json +0 -194
- thoughtleaders_cli-0.7.9/skills/tl-report-builder/references/report_glossary.md +0 -145
- thoughtleaders_cli-0.7.9/skills/tl-report-builder/references/sponsorship_filterset_schema.json +0 -217
- thoughtleaders_cli-0.7.9/skills/tl-report-builder/references/sponsorship_widget_schema.json +0 -165
- thoughtleaders_cli-0.7.9/skills/tl-report-builder/references/widgets.md +0 -184
- thoughtleaders_cli-0.7.9/skills/tl-report-builder/tools/column_builder.md +0 -384
- thoughtleaders_cli-0.7.9/skills/tl-report-builder/tools/database_query.md +0 -210
- thoughtleaders_cli-0.7.9/skills/tl-report-builder/tools/name_resolver.md +0 -162
- thoughtleaders_cli-0.7.9/skills/tl-report-builder/tools/sample_judge.md +0 -223
- thoughtleaders_cli-0.7.9/skills/tl-report-builder/tools/similar_channels.md +0 -61
- thoughtleaders_cli-0.7.9/skills/tl-report-builder/tools/topic_matcher.md +0 -289
- thoughtleaders_cli-0.7.9/skills/tl-report-builder/tools/widget_builder.md +0 -278
- thoughtleaders_cli-0.7.9/skills/tl-save-report/references/sortable_columns.json +0 -64
- thoughtleaders_cli-0.7.9/tests/test_setup.py +0 -41
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/.claude-plugin/marketplace.json +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/.github/workflows/python-publish.yml +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/.gitignore +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/API.md +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/CLAUDE.md +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/LICENSE +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/agents/tl-analyst.md +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/agents/youtube-comment-classifier.md +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/hooks/hooks.json +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/hooks/scripts/load-tl-skill.mjs +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/hooks/scripts/post-usage.sh +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/hooks/scripts/pre-check.sh +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl/references/business-glossary.md +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/.gitignore +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/SKILL.md +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/references/comment-patterns.md +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/references/peer-cohort.md +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/references/red-flags.md +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/references/scoring.md +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/scripts/_io_utf8.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/scripts/analyze_channel.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/scripts/anomaly_detector.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/scripts/comment_analyzer.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/scripts/comment_scraper.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/scripts/engagement_ratios.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/scripts/peer_cohort.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/scripts/report.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/scripts/resolve_channel.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/scripts/score.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/scripts/tl_cli.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/scripts/video_integrity.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/scripts/view_curves.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-keyword-research/SKILL.md +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-keyword-research/scripts/probe.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-save-report/references/columns_brands.md +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-save-report/references/columns_channels.md +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-save-report/references/columns_content.md +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-save-report/references/columns_sponsorships.md +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-save-report/references/intelligence_filterset_schema.json +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-save-report/references/intelligence_widget_schema.json +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-save-report/references/report_glossary.md +0 -0
- {thoughtleaders_cli-0.7.9/skills/tl-report-builder → thoughtleaders_cli-0.7.11/skills/tl-save-report}/references/sortable_columns.json +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-save-report/references/sponsorship_filterset_schema.json +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-save-report/references/sponsorship_widget_schema.json +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-save-report/references/widgets.md +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-top-partnerships/SKILL.md +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-top-partnerships/scripts/top_partnerships.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-views-guarantee/SKILL.md +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl-views-guarantee/scripts/vg.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/_completions.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/_typer_utils.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/auth/__init__.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/auth/commands.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/auth/login.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/auth/pkce.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/auth/token_store.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/client/__init__.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/client/errors.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/client/http.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/__init__.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/_comments_common.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/balance.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/brands.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/bulk_import.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/changelog.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/credits.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/db.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/deals.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/describe.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/doctor.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/matches.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/proposals.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/recommender.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/schema.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/snapshots.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/sponsorships.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/uploads.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/whoami.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/config.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/filters.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/hints.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/main.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/output/__init__.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/src/tl_cli/output/formatter.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/tests/__init__.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/tests/test_auth.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/tests/test_describe.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/tests/test_filters.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/tests/test_http_auth.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/tests/test_output.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/tests/test_reports.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/tests/test_sponsorships.py +0 -0
- {thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/uv.lock +0 -0
|
@@ -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-
|
|
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.
|
|
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`).
|
|
@@ -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
|
|
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":
|
|
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
|
|
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.
|
|
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
|
-
- **`
|
|
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
|
|
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**
|
|
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
|
-
|
|
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
|
|
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
|
|
773
|
+
LIMIT 10000 OFFSET 0" --json
|
|
773
774
|
```
|
|
774
775
|
|
|
775
776
|
### "Find channels similar to one I know" (similarity recommender):
|
{thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl/references/elasticsearch-schema.md
RENAMED
|
@@ -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`, `
|
|
25
|
-
- `size` ≤
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 |
|
|
95
|
-
| `impression_live` | long |
|
|
96
|
-
| `impression_shorts` | long |
|
|
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
|
|
218
|
+
### Deep sweeps — window by date, don't page past 10k
|
|
219
219
|
|
|
220
|
-
|
|
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
|
-
|
|
222
|
+
```bash
|
|
223
|
+
# One window — repeat with shifted date ranges until the full period is covered
|
|
229
224
|
tl db es '{
|
|
230
|
-
"size":
|
|
231
|
-
"
|
|
232
|
-
"
|
|
233
|
-
|
|
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.
|
{thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl/references/firebolt-schema.md
RENAMED
|
@@ -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
|
|
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
|
{thoughtleaders_cli-0.7.9 → thoughtleaders_cli-0.7.11}/skills/tl/references/postgres-schema.md
RENAMED
|
@@ -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
|
|
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
|
|
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"`) →
|
|
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.**
|
|
527
|
+
- **No bulk-importing into an existing report.** Use the `tl bulk-import` command for that. Save-report only creates new reports.
|
|
@@ -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 (
|
|
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
|
|
312
|
-
|
|
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
|
|
408
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
278
|
+
"""Print manual install instructions when the plugin couldn't be installed."""
|
|
196
279
|
console.print()
|
|
197
|
-
console.print("[yellow]Claude Code
|
|
280
|
+
console.print("[yellow]The Claude Code plugin could not be installed automatically.[/yellow]")
|
|
198
281
|
console.print()
|
|
199
|
-
console.print("
|
|
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("
|
|
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
|
|
218
|
-
|
|
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
|
-
#
|
|
253
|
-
console.print(" [yellow]![/yellow] claude binary not found
|
|
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
|
-
|
|
288
|
-
|
|
373
|
+
_install_standalone_skills_step(plugin_root)
|
|
374
|
+
_print_manual_instructions()
|
|
289
375
|
raise SystemExit(1)
|
|
290
376
|
|
|
291
|
-
# Step 3:
|
|
292
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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"
|