thoughtleaders-cli 0.6.56__tar.gz → 0.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/.claude-plugin/plugin.json +1 -1
  2. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/AGENTS.md +17 -33
  3. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/PKG-INFO +7 -6
  4. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/README.md +6 -5
  5. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/pyproject.toml +1 -1
  6. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/channel-authenticity/SKILL.md +4 -4
  7. thoughtleaders_cli-0.7.0/skills/channel-authenticity/scripts/_io_utf8.py +53 -0
  8. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/channel-authenticity/scripts/analyze_channel.py +9 -7
  9. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/channel-authenticity/scripts/anomaly_detector.py +3 -1
  10. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/channel-authenticity/scripts/comment_analyzer.py +1 -0
  11. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/channel-authenticity/scripts/comment_scraper.py +2 -0
  12. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/channel-authenticity/scripts/engagement_ratios.py +1 -0
  13. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/channel-authenticity/scripts/peer_cohort.py +4 -2
  14. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/channel-authenticity/scripts/report.py +4 -1
  15. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/channel-authenticity/scripts/resolve_channel.py +2 -0
  16. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/channel-authenticity/scripts/score.py +4 -1
  17. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/channel-authenticity/scripts/tl_cli.py +5 -0
  18. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/channel-authenticity/scripts/video_integrity.py +2 -0
  19. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/channel-authenticity/scripts/view_curves.py +2 -0
  20. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/tl/SKILL.md +4 -6
  21. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/tl/references/postgres-schema.md +3 -1
  22. thoughtleaders_cli-0.7.0/skills/tl-views-guarantee/SKILL.md +117 -0
  23. thoughtleaders_cli-0.7.0/skills/tl-views-guarantee/scripts/vg.py +371 -0
  24. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/src/tl_cli/__init__.py +1 -1
  25. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/src/tl_cli/commands/brands.py +36 -27
  26. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/src/tl_cli/commands/channels.py +18 -8
  27. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/src/tl_cli/commands/doctor.py +9 -0
  28. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/.claude-plugin/marketplace.json +0 -0
  29. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/.github/workflows/python-publish.yml +0 -0
  30. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/.gitignore +0 -0
  31. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/API.md +0 -0
  32. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/CLAUDE.md +0 -0
  33. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/LICENSE +0 -0
  34. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/agents/tl-analyst.md +0 -0
  35. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/agents/youtube-comment-classifier.md +0 -0
  36. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/hooks/hooks.json +0 -0
  37. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/hooks/scripts/load-tl-skill.mjs +0 -0
  38. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/hooks/scripts/post-usage.sh +0 -0
  39. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/hooks/scripts/pre-check.sh +0 -0
  40. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/channel-authenticity/.gitignore +0 -0
  41. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/channel-authenticity/references/comment-patterns.md +0 -0
  42. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/channel-authenticity/references/peer-cohort.md +0 -0
  43. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/channel-authenticity/references/red-flags.md +0 -0
  44. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/channel-authenticity/references/scoring.md +0 -0
  45. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/tl/references/business-glossary.md +0 -0
  46. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/tl/references/elasticsearch-schema.md +0 -0
  47. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/tl/references/firebolt-schema.md +0 -0
  48. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/tl-import/SKILL.md +0 -0
  49. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/tl-keyword-research/SKILL.md +0 -0
  50. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/tl-keyword-research/scripts/probe.py +0 -0
  51. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/tl-report-builder/SKILL.md +0 -0
  52. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/tl-report-builder/examples/e2e_findings.md +0 -0
  53. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/tl-report-builder/examples/golden_queries.md +0 -0
  54. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/tl-report-builder/references/columns_brands.md +0 -0
  55. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/tl-report-builder/references/columns_channels.md +0 -0
  56. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/tl-report-builder/references/columns_content.md +0 -0
  57. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/tl-report-builder/references/columns_sponsorships.md +0 -0
  58. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/tl-report-builder/references/intelligence_filterset_schema.json +0 -0
  59. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/tl-report-builder/references/intelligence_widget_schema.json +0 -0
  60. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/tl-report-builder/references/report_glossary.md +0 -0
  61. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/tl-report-builder/references/sortable_columns.json +0 -0
  62. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/tl-report-builder/references/sponsorship_filterset_schema.json +0 -0
  63. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/tl-report-builder/references/sponsorship_widget_schema.json +0 -0
  64. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/tl-report-builder/references/widgets.md +0 -0
  65. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/tl-report-builder/tools/column_builder.md +0 -0
  66. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/tl-report-builder/tools/database_query.md +0 -0
  67. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/tl-report-builder/tools/name_resolver.md +0 -0
  68. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/tl-report-builder/tools/sample_judge.md +0 -0
  69. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/tl-report-builder/tools/similar_channels.md +0 -0
  70. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/tl-report-builder/tools/topic_matcher.md +0 -0
  71. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/tl-report-builder/tools/widget_builder.md +0 -0
  72. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/tl-save-report/SKILL.md +0 -0
  73. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/tl-save-report/references/columns_brands.md +0 -0
  74. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/tl-save-report/references/columns_channels.md +0 -0
  75. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/tl-save-report/references/columns_content.md +0 -0
  76. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/tl-save-report/references/columns_sponsorships.md +0 -0
  77. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/tl-save-report/references/intelligence_filterset_schema.json +0 -0
  78. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/tl-save-report/references/intelligence_widget_schema.json +0 -0
  79. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/tl-save-report/references/report_glossary.md +0 -0
  80. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/tl-save-report/references/sortable_columns.json +0 -0
  81. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/tl-save-report/references/sponsorship_filterset_schema.json +0 -0
  82. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/tl-save-report/references/sponsorship_widget_schema.json +0 -0
  83. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/skills/tl-save-report/references/widgets.md +0 -0
  84. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/src/tl_cli/_completions.py +0 -0
  85. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/src/tl_cli/auth/__init__.py +0 -0
  86. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/src/tl_cli/auth/commands.py +0 -0
  87. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/src/tl_cli/auth/finalize.py +0 -0
  88. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/src/tl_cli/auth/login.py +0 -0
  89. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/src/tl_cli/auth/pkce.py +0 -0
  90. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/src/tl_cli/auth/token_store.py +0 -0
  91. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/src/tl_cli/client/__init__.py +0 -0
  92. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/src/tl_cli/client/errors.py +0 -0
  93. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/src/tl_cli/client/http.py +0 -0
  94. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/src/tl_cli/commands/__init__.py +0 -0
  95. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/src/tl_cli/commands/_comments_common.py +0 -0
  96. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/src/tl_cli/commands/balance.py +0 -0
  97. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/src/tl_cli/commands/bulk_import.py +0 -0
  98. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/src/tl_cli/commands/changelog.py +0 -0
  99. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/src/tl_cli/commands/credits.py +0 -0
  100. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/src/tl_cli/commands/db.py +0 -0
  101. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/src/tl_cli/commands/deals.py +0 -0
  102. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/src/tl_cli/commands/describe.py +0 -0
  103. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/src/tl_cli/commands/matches.py +0 -0
  104. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/src/tl_cli/commands/proposals.py +0 -0
  105. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/src/tl_cli/commands/recommender.py +0 -0
  106. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/src/tl_cli/commands/reports.py +0 -0
  107. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/src/tl_cli/commands/schema.py +0 -0
  108. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/src/tl_cli/commands/setup.py +0 -0
  109. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/src/tl_cli/commands/snapshots.py +0 -0
  110. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/src/tl_cli/commands/sponsorships.py +0 -0
  111. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/src/tl_cli/commands/uploads.py +0 -0
  112. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/src/tl_cli/commands/whoami.py +0 -0
  113. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/src/tl_cli/config.py +0 -0
  114. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/src/tl_cli/filters.py +0 -0
  115. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/src/tl_cli/hints.py +0 -0
  116. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/src/tl_cli/main.py +0 -0
  117. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/src/tl_cli/output/__init__.py +0 -0
  118. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/src/tl_cli/output/formatter.py +0 -0
  119. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/src/tl_cli/self_update.py +0 -0
  120. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/tests/__init__.py +0 -0
  121. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/tests/test_auth.py +0 -0
  122. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/tests/test_describe.py +0 -0
  123. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/tests/test_filters.py +0 -0
  124. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/tests/test_http_auth.py +0 -0
  125. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/tests/test_output.py +0 -0
  126. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/tests/test_reports.py +0 -0
  127. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/tests/test_sponsorships.py +0 -0
  128. {thoughtleaders_cli-0.6.56 → thoughtleaders_cli-0.7.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tl-cli",
3
- "version": "0.6.56",
3
+ "version": "0.7.0",
4
4
  "description": "ThoughtLeaders CLI — query sponsorship deals, channels, brands, uploads, and intelligence from the terminal",
5
5
  "author": {
6
6
  "name": "ThoughtLeaders",
@@ -1,6 +1,6 @@
1
1
  # Project Overview
2
2
 
3
- **tl-cli** is a Python CLI for querying ThoughtLeaders sponsorship data (sponsorships, channels, brands, uploads, snapshots, reports, recommender). Built with Typer + Rich + httpx. Designed as an "agent-first tool" — the CLI handles structured commands and output, while the user's AI agent (Claude) provides intelligence.
3
+ **tl-cli** is a Python CLI for querying ThoughtLeaders sponsorship data (sponsorships, channels, brands, uploads, snapshots, reports, recommender). Built with Typer + Rich + httpx. Designed as an "AI agent-first tool" — the CLI handles data commands and output, while the user's AI agent (Claude) provides decision making.
4
4
 
5
5
  # Architecture
6
6
 
@@ -11,25 +11,16 @@
11
11
  ## Command Pattern (all data commands follow this)
12
12
 
13
13
  Every data command in `src/tl_cli/commands/` uses explicit Typer subcommands:
14
- - `list` — list/search with `key:value` filters as positional args
15
14
  - `show` — detail view by ID
16
15
  - `history` — historical data list
17
16
  - `create` / `add` — create new records (where applicable)
18
17
 
19
18
  When adding a new data command, follow this pattern. See `sponsorships.py` for the reference implementation.
20
19
 
21
- `deals`, `matches`, and `proposals` are shortcut commands that delegate to sponsorships' `do_list`/`do_show`/`do_create` with a pre-set status filter. They reject explicit `status:` filters — users should use `tl sponsorships list` for finer-grained status filtering.
22
-
23
- `recommender` (`commands/recommender.py`) wraps the recommender API at `/api/cli/v1/recommender/*` — `tags` (free), `top-channels` / `top-profiles` / `top-brands`, `inspect-channel`, `inspect-brand`, `similar-to-profile` (all 25 credits flat, Intelligence-gated). The three `top-*` URLs share one server resolver; `top-brands` dedupes the underlying profile rows by brand. Channel→channel and brand→brand similarity stay on `tl channels similar` / `tl brands similar`. When updating the SKILL or examples, prefer steering category/topic discovery (e.g. "Cooking channels") to `tl recommender top-channels "<tag>"` rather than `WHERE content_category = <code>` SQL — the recommender is ranked, not equality-based. The underlying recommender code uses "element"/"field_name" terminology; the CLI/API layer renames these to "tag" at the boundary.
24
-
25
- ## Filter Parsing (`filters.py`)
26
-
27
- `parse_filters()` handles `key:value` and `key:"quoted value"` syntax. Returns `dict[str, str]` passed as query params. Date filter keys (listed in `DATE_FILTER_KEYS` — e.g. `since`, `created-at`, `created-at-start`, `publish-date-end`) accept keywords `today`, `yesterday`, `tomorrow`. Sponsorship date fields (`created-at`, `publish-date`, `purchase-date`, `send-date`) each expose three filter shapes: bare `<field>:<date>` matches within that date/period, and `<field>-start:` / `<field>-end:` give inclusive lower/upper bounds (both sides inclusive; partial dates expand to the whole period). Empty-string values result in `IS NULL` queries on the backend.
28
-
29
20
  ## Auth Flow (`auth/`)
30
21
 
31
22
  - **PKCE + Auth0**: Browser-based login with localhost callback server (`login.py`)
32
- - **Token Storage** (`token_store.py`): OS keyring primary, `~/.config/tl/credentials.json` fallback (0o600)
23
+ - **Token Storage** (`token_store.py`): OS keyring primary, `~/.config/tl/credentials.json` fallback (chmod 0o600)
33
24
  - **Env override**: `TL_API_KEY` env var takes priority over keyring (for CI)
34
25
  - **Auto-refresh**: `TLClient` refreshes expired tokens on 401
35
26
 
@@ -52,6 +43,8 @@ TTY-aware: Rich tables in terminal, JSON when piped. Flags: `--json`, `--csv`, `
52
43
  The CLI integrates with AI coding agents via skills, commands, agents, and hooks.
53
44
 
54
45
  - **Claude Code** - `tl setup claude`
46
+ - **Gemini** - `tl setup gemini`
47
+ - **Codex** - `tl setup codex`
55
48
  - **OpenCode** - `tl setup opencode`
56
49
 
57
50
  This repo is also a Claude Code plugin, and can directly be installed as one.
@@ -60,8 +53,7 @@ This repo is also a Claude Code plugin, and can directly be installed as one.
60
53
 
61
54
  - **`tl`** — the main skill for querying ThoughtLeaders data. Default for any sponsorship / channel / brand / upload / report question.
62
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.
63
- - **`tl-report-builder`** — invoke when the user wants to build, refine, or save a platform report (campaign config, FilterSet, columns, widgets). Multi-phase flow: routing schema + validation columns widgets.
64
- - **`tl-import`**, **`tl-save-report`**, **`adapt-tl-data`** — narrower workflows; the skill files document their own triggers.
56
+ - **`tl-import`**, **`tl-save-report`**, **`adapt-tl-data`**, **`tl-views-guarantee`** narrower workflows; the skill files document their own triggers.
65
57
 
66
58
  ### Skill content boundaries
67
59
 
@@ -73,21 +65,6 @@ Skills under `skills/` are split into a `SKILL.md` and one or more `references/*
73
65
 
74
66
  When adding or updating skill content, place the fact in its single home and link from the others. Do not duplicate or "quick-recap" content across files — recaps are the highest drift surface.
75
67
 
76
- #### Anti-pattern: skill-local schema references
77
-
78
- When a dependent skill (e.g. `tl-report-builder`) needs to reference a schema fact (table layout, columns, fetch SQL, hallucinated-column markers), **link to the canonical home in `skills/tl/references/`** — do not create a new `<skill>/references/*.md` file that mirrors or paraphrases that content.
79
-
80
- Concrete regression marker: an earlier branch added `skills/tl-report-builder/references/data_plane.md` to consolidate the `thoughtleaders_topics` fetch query out of inline tool text. That had the right *shape* (don't restate schema in tool prose) but the wrong *home* — it forked schema facts into a parallel reference file that would silently drift from `skills/tl/references/postgres-schema.md`. The fix was to land the columns + fetch SQL + regression markers in `postgres-schema.md` (and upstream in `tl-data/references/postgres-schema.md`), delete the local file, and rewire the references via Markdown links.
81
-
82
- Rule of thumb: if you are about to write *"here's the SQL to query this table"* or *"these columns don't exist on this table"* anywhere outside `skills/tl/references/`, stop. Add the fact to the canonical reference, then link to the anchor. Same for business facts and the glossary.
83
-
84
- Skill-local `references/*.md` ARE appropriate when the content is **skill-shaped**, not schema-shaped:
85
- - Column metadata for a specific report type (sortable columns, formula templates) — `tl-report-builder/references/columns_*.md`
86
- - JSON schemas for tool-specific request/response shapes — `tl-report-builder/references/*_schema.json`
87
- - Disambiguation tables, defaults, and pitfall catalogues that exist only in this skill's flow — `tl-report-builder/references/report_glossary.md`
88
-
89
- If you are unsure whether a fact is schema-shaped or skill-shaped, ask: "would another TL skill (analyst, finance, mbn-outreach) ever need this fact?" If yes, it's schema/business-shaped — promote it to the canonical home.
90
-
91
68
  ## API Response Envelope
92
69
 
93
70
  All list endpoints return: `{ results, total, limit, offset, usage: { credits_charged, credit_rate, balance_remaining }, _breadcrumbs }`.
@@ -109,9 +86,11 @@ The version string is defined in three files and all three must be updated toget
109
86
  - `.claude-plugin/plugin.json` — `"version": "x.y.z"`
110
87
  - `src/tl_cli/__init__.py` — `__version__ = "x.y.z"`
111
88
 
112
- ## Important Constraint
89
+ ## Creating a release
113
90
 
114
- `tl snapshots video` requires `--channel` flag Firebolt queries without a channel partition are unbounded.
91
+ A "release" means using the `gh` command to create a release on GitHub, named like the current package version number.
92
+
93
+ Warn the user if they are creating a release and the latest commit didn't bump the version number, and ask for confirmation before releasing.
115
94
 
116
95
  ## Coding
117
96
 
@@ -119,14 +98,19 @@ The version string is defined in three files and all three must be updated toget
119
98
  * Do not let server implementation details into skill files (anything under `skills/`). Skills describe *what the CLI does* from the user's seat — observable command surface, inputs, outputs, examples. Do not say "the server enforces X", "the API validates Y on its side", "the backend rejects Z" — those are mechanism notes that drift the moment the server changes. State the user-visible behaviour ("unknown keys come back as 400") without naming where it's enforced.
120
99
  * **All `import` and `from X import Y` statements live at the top of the Python module file** — after the module docstring, before any code. No inline imports inside function bodies, no lazy imports for "speed" or "optional dependency" reasons. `from __future__ import …` goes at the very top (Python requires that). The only legitimate inline-import exception is **platform-conditional imports** that cannot succeed on the other platform (e.g. `import msvcrt` on Linux, `import termios`/`tty` on Windows) — those stay inside their `if sys.platform == …:` guard. If a circular-import problem makes a top-level import impossible, fix the circular dependency rather than working around it with an inline import.
121
100
 
101
+ # Updating
102
+
103
+ The `tl update --force` command will force an update of the `thoughtleaders-cli` package.
104
+ The auto-update feature keeps the package updated, by checking (cached) on each command invocation.
105
+
122
106
  # Git commit rules
123
107
 
124
108
  Do not reference internal architecture of the ThoughtLeaders app in commit messages.
125
109
 
126
- When a feature is purely server-side but changes the data the CLI receives (e.g. adding, removing, or renaming a field on a response, changing a credit rate, expanding an enum), make a forced empty commit on the tl-cli repo (`git commit --allow-empty`) describing the change. This keeps the CLI repo's history a complete log of what users see, even when no client code had to change.
110
+ When a feature is purely server-side but changes the data the CLI receives (e.g. adding, removing, or renaming a field on a response, changing a credit rate, expanding an enum), make a forced empty commit on the tl-cli repo (`git commit --allow-empty`) describing the change. This keeps the CLI repo's history a complete log of what users see, even when no client code had to change. The `tl changelog` command will read this log to show to the users.
127
111
 
128
112
  # Be aware of tests
129
113
 
130
- For every feature or change, explicitly consider whether tests need to be added or updated — new endpoint, new model field, new CLI command, new validation rule, new error path, anything that changes user-visible behaviour. Don't ship a feature without asking "what test covers this?" If no test does and the surface is non-trivial, write one. This applies across all repos involved in the change (server-side changes that ripple into the CLI need both server tests and CLI tests updated).
114
+ For every feature or change, explicitly consider whether tests need to be added or updated, on this repo or on the server repo — new endpoint, new model field, new CLI command, new validation rule, new error path, anything that changes user-visible behaviour. Don't ship a feature without asking "what test covers this?" If no test does and the surface is non-trivial, write one. This applies across all repos involved in the change (server-side changes that ripple into the CLI need both server tests and CLI tests updated).
131
115
 
132
- Be sure to check if tests need to be updated when changing any data structures or function names, in all repos involved in the change.
116
+ Be sure to check if tests need to be updated when changing any data structures or function names, in all repos involved in the change
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: thoughtleaders-cli
3
- Version: 0.6.56
3
+ Version: 0.7.0
4
4
  Summary: ThoughtLeaders CLI — query sponsorship data, channels, brands, and intelligence
5
5
  Project-URL: Homepage, https://thoughtleaders.io
6
6
  Project-URL: Repository, https://github.com/ThoughtLeaders-io/thoughtleaders-cli
@@ -37,7 +37,7 @@ ThoughtLeaders CLI — query sponsorship data, channels, brands, and intelligenc
37
37
  ### For account managers and sales
38
38
 
39
39
  - **Pipeline reporting on the fly.** *"How many deals did we close in Q1?"*, *"What's my weighted pipeline by sales owner?"*, *"Which proposals are stuck in `pending` for more than 14 days?"* — one raw SQL or one structured command, instead of waiting on a dashboard.
40
- - **Brand intelligence in seconds.** *"What channels does Nike sponsor?"*, *"Which brands sponsor `MrBeast`?"*, *"What's Holafly's sponsorship history through us vs. through everyone?"* — answers are one `tl brands history` or one `tl db es` call away.
40
+ - **Brand intelligence in seconds.** *"What channels does Nike sponsor?"*, *"Which brands sponsor `MrBeast`?"*, *"What's Holafly's sponsorship history through us vs. through everyone?"* — answers are one `tl db es` call away.
41
41
  - **Vetting candidates before a pitch.** Look up a channel by ID, name, YouTube URL, or `@handle`; pull its adspots, audience demographics, evergreenness score, and detected sponsor history before drafting the IO.
42
42
  - **Pre-flight before booking.** Confirm MSN/TPP membership, integration availability, and persona/plan eligibility for a brand profile with one SQL join.
43
43
 
@@ -66,6 +66,7 @@ ThoughtLeaders CLI — query sponsorship data, channels, brands, and intelligenc
66
66
  - [jq](https://stedolan.github.io/jq/)
67
67
  - [ripgrep](https://github.com/BurntSushi/ripgrep)
68
68
  - [duckdb](https://duckdb.org/)
69
+ - [yt-dlp](https://github.com/yt-dlp/yt-dlp)
69
70
 
70
71
  For automated installs on MacOS, prefer installing Homebrew and use the pipx package manager, but ask the user if they have the admin access (sudo) password first. If not, proceed by using `uv` and installing everything locally.
71
72
 
@@ -170,8 +171,7 @@ tl recommender brands-for-channel 12345 # Brands most likely to sponsor
170
171
 
171
172
  # Brand intelligence
172
173
  tl brands show Nike
173
- tl brands history Nike # Detected sponsorships from ES
174
- tl brands history-stats Nike # Aggregate roll-up (totals, first/last seen, top channels)
174
+ tl brands find Nike # Resolve a string → single brand id
175
175
 
176
176
  # Search videos and transcripts via Elasticsearch
177
177
  tl db es '{"size":20,"query":{"term":{"channel.id":12345}},"_source":["title","views"]}'
@@ -196,7 +196,7 @@ tl balance
196
196
 
197
197
  # Health check — auth, connectivity, version, latency, and required external tools.
198
198
  # Run this first when something feels off; it surfaces token expiry,
199
- # missing `jq`/`rg`/`duckdb`, and slow endpoints in one snapshot.
199
+ # missing `jq`/`rg`/`duckdb`/`yt-dlp`, and slow endpoints in one snapshot.
200
200
  tl doctor
201
201
  ```
202
202
 
@@ -276,9 +276,10 @@ Each agent discovers the skill automatically and uses it when you ask about spon
276
276
 
277
277
  The plugin ships several focused skills (installed by all the `tl setup *` commands):
278
278
 
279
- - **`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` / `history` commands for single-record lookups and the special cases they were built for (similarity search, ID resolution, sponsorship history). Comes with full schema references for Postgres, Elasticsearch, and Firebolt under `references/`.
279
+ - **`tl`** — the data-analyst skill. Defaults to raw database queries via `tl db pg|fb|es` for anything non-trivial; uses the structured `tl <resource> show` / `find` / `similar` commands for single-record lookups and similarity / ID-resolution special cases. Comes with full schema references for Postgres, Elasticsearch, and Firebolt under `references/`.
280
280
  - **`tl-report-builder`** — builds TL reports (channels / brands / sponsorships / videos) from natural-language requests. Produces an in-chat preview by default; saves a real campaign when the user is explicit ("save", "create the report").
281
281
  - **`tl-import`** / **`bulk-import`** — superuser-only; bulk-add or exclude lists of channels, brands, videos, or sponsorships against a report.
282
+ - **`tl-views-guarantee`** — sizes a multi-video sponsorship buy for a channel, returning the video bundle size, views guarantee, and likelihood to hit.
282
283
 
283
284
  ## Output Formats
284
285
 
@@ -9,7 +9,7 @@ ThoughtLeaders CLI — query sponsorship data, channels, brands, and intelligenc
9
9
  ### For account managers and sales
10
10
 
11
11
  - **Pipeline reporting on the fly.** *"How many deals did we close in Q1?"*, *"What's my weighted pipeline by sales owner?"*, *"Which proposals are stuck in `pending` for more than 14 days?"* — one raw SQL or one structured command, instead of waiting on a dashboard.
12
- - **Brand intelligence in seconds.** *"What channels does Nike sponsor?"*, *"Which brands sponsor `MrBeast`?"*, *"What's Holafly's sponsorship history through us vs. through everyone?"* — answers are one `tl brands history` or one `tl db es` call away.
12
+ - **Brand intelligence in seconds.** *"What channels does Nike sponsor?"*, *"Which brands sponsor `MrBeast`?"*, *"What's Holafly's sponsorship history through us vs. through everyone?"* — answers are one `tl db es` call away.
13
13
  - **Vetting candidates before a pitch.** Look up a channel by ID, name, YouTube URL, or `@handle`; pull its adspots, audience demographics, evergreenness score, and detected sponsor history before drafting the IO.
14
14
  - **Pre-flight before booking.** Confirm MSN/TPP membership, integration availability, and persona/plan eligibility for a brand profile with one SQL join.
15
15
 
@@ -38,6 +38,7 @@ ThoughtLeaders CLI — query sponsorship data, channels, brands, and intelligenc
38
38
  - [jq](https://stedolan.github.io/jq/)
39
39
  - [ripgrep](https://github.com/BurntSushi/ripgrep)
40
40
  - [duckdb](https://duckdb.org/)
41
+ - [yt-dlp](https://github.com/yt-dlp/yt-dlp)
41
42
 
42
43
  For automated installs on MacOS, prefer installing Homebrew and use the pipx package manager, but ask the user if they have the admin access (sudo) password first. If not, proceed by using `uv` and installing everything locally.
43
44
 
@@ -142,8 +143,7 @@ tl recommender brands-for-channel 12345 # Brands most likely to sponsor
142
143
 
143
144
  # Brand intelligence
144
145
  tl brands show Nike
145
- tl brands history Nike # Detected sponsorships from ES
146
- tl brands history-stats Nike # Aggregate roll-up (totals, first/last seen, top channels)
146
+ tl brands find Nike # Resolve a string → single brand id
147
147
 
148
148
  # Search videos and transcripts via Elasticsearch
149
149
  tl db es '{"size":20,"query":{"term":{"channel.id":12345}},"_source":["title","views"]}'
@@ -168,7 +168,7 @@ tl balance
168
168
 
169
169
  # Health check — auth, connectivity, version, latency, and required external tools.
170
170
  # Run this first when something feels off; it surfaces token expiry,
171
- # missing `jq`/`rg`/`duckdb`, and slow endpoints in one snapshot.
171
+ # missing `jq`/`rg`/`duckdb`/`yt-dlp`, and slow endpoints in one snapshot.
172
172
  tl doctor
173
173
  ```
174
174
 
@@ -248,9 +248,10 @@ Each agent discovers the skill automatically and uses it when you ask about spon
248
248
 
249
249
  The plugin ships several focused skills (installed by all the `tl setup *` commands):
250
250
 
251
- - **`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` / `history` commands for single-record lookups and the special cases they were built for (similarity search, ID resolution, sponsorship history). Comes with full schema references for Postgres, Elasticsearch, and Firebolt under `references/`.
251
+ - **`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/`.
252
252
  - **`tl-report-builder`** — builds TL reports (channels / brands / sponsorships / videos) from natural-language requests. Produces an in-chat preview by default; saves a real campaign when the user is explicit ("save", "create the report").
253
253
  - **`tl-import`** / **`bulk-import`** — superuser-only; bulk-add or exclude lists of channels, brands, videos, or sponsorships against a report.
254
+ - **`tl-views-guarantee`** — sizes a multi-video sponsorship buy for a channel, returning the video bundle size, views guarantee, and likelihood to hit.
254
255
 
255
256
  ## Output Formats
256
257
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "thoughtleaders-cli"
7
- version = "0.6.56"
7
+ version = "0.7.0"
8
8
  description = "ThoughtLeaders CLI — query sponsorship data, channels, brands, and intelligence"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -25,10 +25,10 @@ investigations.
25
25
  view-curves can be hand-waved ("the algorithm", "we ran ads"); reading what
26
26
  the audience actually says is the only direct proof. A run without it is
27
27
  invalid.
28
- - **Data access is CLI-only.** Everything goes through `tl_cli.py` the
29
- `tl` CLI (`tl db pg/fb/es`, `tl channels similar`). No database credentials
30
- are ever used. If the CLI isn't authenticated the skill fails fast with a
31
- clear message.
28
+ - **Data access is CLI-only.** Everything goes through `tl_cli.py` and the
29
+ `tl` CLI (`tl db pg/fb/es`, `tl channels similar`).
30
+ - Do all data processing with the "utf-8" encoding explicitly in all scripts
31
+ you create.
32
32
 
33
33
  ## Setup check
34
34
 
@@ -0,0 +1,53 @@
1
+ """Shared UTF-8 I/O setup for the channel-authenticity scripts.
2
+
3
+ YouTube data is full of non-ASCII: channel names, comment text, emoji.
4
+ On POSIX the default I/O encoding is already UTF-8, but on Windows the
5
+ console and the default file encoding are the legacy code page (cp1252),
6
+ so printing or writing that text raises ``UnicodeEncodeError`` (or
7
+ silently mojibakes on read-back).
8
+
9
+ Importing this module reconfigures ``stdout``/``stderr`` to UTF-8 on
10
+ Windows (a no-op elsewhere, and idempotent), so every script that does
11
+ ``import _io_utf8`` can safely print Unicode. File reads/writes must
12
+ still pass ``encoding="utf-8"`` explicitly — that's done at each call
13
+ site — and child processes get ``child_env()`` so their stdio is UTF-8
14
+ too. ``UTF8`` is exported for call sites that prefer the named constant.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import os
19
+ import sys
20
+
21
+ UTF8 = "utf-8"
22
+
23
+
24
+ def _reconfigure_std_streams() -> None:
25
+ """Force stdout/stderr to UTF-8 on Windows. No-op on POSIX."""
26
+ if sys.platform != "win32":
27
+ return
28
+ for stream in (sys.stdout, sys.stderr):
29
+ reconfigure = getattr(stream, "reconfigure", None)
30
+ if reconfigure is None:
31
+ continue
32
+ try:
33
+ reconfigure(encoding=UTF8)
34
+ except (ValueError, OSError):
35
+ # Stream already detached / not reconfigurable (e.g. piped to a
36
+ # non-text sink) — leave it as-is rather than crash on import.
37
+ pass
38
+
39
+
40
+ def child_env() -> dict[str, str]:
41
+ """``os.environ`` with UTF-8 forced, for subprocess children.
42
+
43
+ ``PYTHONUTF8=1`` enables UTF-8 mode for Python children; the
44
+ redundant ``PYTHONIOENCODING`` covers tools that read it directly.
45
+ Use as ``subprocess.run(..., env=child_env())``.
46
+ """
47
+ env = dict(os.environ)
48
+ env["PYTHONUTF8"] = "1"
49
+ env["PYTHONIOENCODING"] = UTF8
50
+ return env
51
+
52
+
53
+ _reconfigure_std_streams()
@@ -28,14 +28,16 @@ import sys
28
28
  import time
29
29
  from pathlib import Path
30
30
 
31
+ import _io_utf8 # noqa: F401 (side effect: forces UTF-8 stdout/stderr on Windows)
31
32
  import anomaly_detector
32
33
  import comment_analyzer
33
34
  import engagement_ratios
34
35
  import resolve_channel
35
36
  import score as score_mod
36
- import tl_cli
37
37
  import video_integrity
38
38
 
39
+ import tl_cli
40
+
39
41
  OUT_DIR = Path("/tmp")
40
42
 
41
43
 
@@ -74,8 +76,8 @@ def collect(ref: str) -> dict:
74
76
  }
75
77
  state_path = OUT_DIR / f"channel_authenticity_{cid}_{ts}.state.json"
76
78
  batch_path = OUT_DIR / f"channel_authenticity_{cid}_{ts}.llmbatch.json"
77
- state_path.write_text(json.dumps(state, indent=2, default=str))
78
- batch_path.write_text(json.dumps(gc.get("llm_batch", []), default=str))
79
+ state_path.write_text(json.dumps(state, indent=2, default=str), encoding="utf-8")
80
+ batch_path.write_text(json.dumps(gc.get("llm_batch", []), default=str), encoding="utf-8")
79
81
 
80
82
  return {
81
83
  "phase": "collect_done",
@@ -95,11 +97,11 @@ def collect(ref: str) -> dict:
95
97
 
96
98
 
97
99
  def finalize(state_path: str, llm_paths: list[str]) -> dict:
98
- state = json.loads(Path(state_path).read_text())
100
+ state = json.loads(Path(state_path).read_text(encoding="utf-8"))
99
101
  passes: list[list[dict]] = []
100
102
  for lp in llm_paths:
101
103
  try:
102
- c = json.loads(Path(lp).read_text())
104
+ c = json.loads(Path(lp).read_text(encoding="utf-8"))
103
105
  if isinstance(c, dict):
104
106
  c = c.get("classifications", [])
105
107
  if c:
@@ -116,12 +118,12 @@ def finalize(state_path: str, llm_paths: list[str]) -> dict:
116
118
  ts = time.strftime("%Y%m%d-%H%M%S")
117
119
  final_json = OUT_DIR / f"channel_authenticity_{cid}_{ts}.final.json"
118
120
  report_md = OUT_DIR / f"channel_authenticity_{cid}_{ts}.report.md"
119
- final_json.write_text(json.dumps(state, indent=2, default=str))
121
+ final_json.write_text(json.dumps(state, indent=2, default=str), encoding="utf-8")
120
122
 
121
123
  import report as report_mod
122
124
 
123
125
  md = report_mod.render(state)
124
- report_md.write_text(md)
126
+ report_md.write_text(md, encoding="utf-8")
125
127
  return {"final_json": str(final_json), "report_md": str(report_md), "markdown": md}
126
128
 
127
129
 
@@ -11,9 +11,11 @@ from __future__ import annotations
11
11
 
12
12
  import statistics
13
13
 
14
- import tl_cli
14
+ import _io_utf8 # noqa: F401 (side effect: forces UTF-8 stdout/stderr on Windows)
15
15
  import view_curves
16
16
 
17
+ import tl_cli
18
+
17
19
  PENALTIES = {
18
20
  "B_burst_without_engagement": (25, "critical"),
19
21
  "B_engagement_incoherence": (25, "critical"),
@@ -18,6 +18,7 @@ import re
18
18
  import statistics
19
19
  from collections import Counter
20
20
 
21
+ import _io_utf8 # noqa: F401 (side effect: forces UTF-8 stdout/stderr on Windows)
21
22
  import comment_scraper
22
23
 
23
24
  PENALTIES = {
@@ -20,6 +20,8 @@ from __future__ import annotations
20
20
 
21
21
  import sys
22
22
 
23
+ import _io_utf8 # noqa: F401 (side effect: forces UTF-8 stdout/stderr on Windows)
24
+
23
25
 
24
26
  def limit_for_views(views: int) -> int:
25
27
  if views >= 100_000:
@@ -10,6 +10,7 @@ from __future__ import annotations
10
10
 
11
11
  import statistics
12
12
 
13
+ import _io_utf8 # noqa: F401 (side effect: forces UTF-8 stdout/stderr on Windows)
13
14
  import peer_cohort
14
15
 
15
16
  # code -> (penalty, severity)
@@ -15,6 +15,8 @@ import statistics
15
15
  import time
16
16
  from pathlib import Path
17
17
 
18
+ import _io_utf8 # noqa: F401 (side effect: forces UTF-8 stdout/stderr on Windows)
19
+
18
20
  import tl_cli
19
21
 
20
22
  CACHE = Path(__file__).resolve().parent.parent / "references" / "peer-cohort-cache.json"
@@ -46,7 +48,7 @@ def _cache_key(channel: dict) -> str:
46
48
  def _load_cache() -> dict:
47
49
  if CACHE.exists():
48
50
  try:
49
- return json.loads(CACHE.read_text())
51
+ return json.loads(CACHE.read_text(encoding="utf-8"))
50
52
  except json.JSONDecodeError:
51
53
  return {}
52
54
  return {}
@@ -54,7 +56,7 @@ def _load_cache() -> dict:
54
56
 
55
57
  def _save_cache(cache: dict) -> None:
56
58
  CACHE.parent.mkdir(parents=True, exist_ok=True)
57
- CACHE.write_text(json.dumps(cache, indent=2, default=str))
59
+ CACHE.write_text(json.dumps(cache, indent=2, default=str), encoding="utf-8")
58
60
 
59
61
 
60
62
  def _peer_ids_via_cli(channel_id: int) -> list[int]:
@@ -2,6 +2,8 @@
2
2
  """Render the markdown narrative report from a finalized state dict."""
3
3
  from __future__ import annotations
4
4
 
5
+ import _io_utf8 # noqa: F401 (side effect: forces UTF-8 stdout/stderr on Windows)
6
+
5
7
  SEV_ICON = {"critical": "🔴", "warning": "🟠", "info": "🟡"}
6
8
  VERDICT_ICON = {
7
9
  "CLEAN": "✅",
@@ -132,4 +134,5 @@ if __name__ == "__main__":
132
134
  import json
133
135
  import sys
134
136
 
135
- print(render(json.load(open(sys.argv[1]))))
137
+ with open(sys.argv[1], encoding="utf-8") as fh:
138
+ print(render(json.load(fh)))
@@ -19,6 +19,8 @@ from __future__ import annotations
19
19
  import json
20
20
  import sys
21
21
 
22
+ import _io_utf8 # noqa: F401 (side effect: forces UTF-8 stdout/stderr on Windows)
23
+
22
24
  import tl_cli
23
25
 
24
26
  VIDEO_FETCH = 30 # how many recent of each content_type to pull
@@ -9,6 +9,8 @@ to "do not book" regardless of the mean.
9
9
  """
10
10
  from __future__ import annotations
11
11
 
12
+ import _io_utf8 # noqa: F401 (side effect: forces UTF-8 stdout/stderr on Windows)
13
+
12
14
  BANDS = [
13
15
  (90, "CLEAN", "Safe to book at standard rates."),
14
16
  (70, "MINOR_FLAGS", "Book but note caveats to the AM."),
@@ -72,5 +74,6 @@ if __name__ == "__main__":
72
74
  import json
73
75
  import sys
74
76
 
75
- d = json.load(open(sys.argv[1]))
77
+ with open(sys.argv[1], encoding="utf-8") as fh:
78
+ d = json.load(fh)
76
79
  print(json.dumps(composite(d["group_a"], d["group_b"], d["group_c"]), indent=2))
@@ -23,6 +23,8 @@ import os
23
23
  import shutil
24
24
  import subprocess
25
25
 
26
+ import _io_utf8
27
+
26
28
  TL_BIN = os.environ.get("TL_CLI_BIN", "tl")
27
29
 
28
30
 
@@ -67,6 +69,9 @@ def _tl(args: list[str], *, input_text: str | None = None) -> str:
67
69
  input=input_text,
68
70
  capture_output=True,
69
71
  text=True,
72
+ encoding="utf-8",
73
+ errors="replace",
74
+ env=_io_utf8.child_env(),
70
75
  timeout=180,
71
76
  )
72
77
  except FileNotFoundError as exc:
@@ -29,6 +29,8 @@ from __future__ import annotations
29
29
 
30
30
  from datetime import date
31
31
 
32
+ import _io_utf8 # noqa: F401 (side effect: forces UTF-8 stdout/stderr on Windows)
33
+
32
34
  import tl_cli
33
35
 
34
36
  PENALTIES = {
@@ -15,6 +15,8 @@ from __future__ import annotations
15
15
  import math
16
16
  from dataclasses import dataclass
17
17
 
18
+ import _io_utf8 # noqa: F401 (side effect: forces UTF-8 stdout/stderr on Windows)
19
+
18
20
  import tl_cli
19
21
 
20
22
 
@@ -151,11 +151,11 @@ Unless the user specifically asks for running a specific report or showing the r
151
151
 
152
152
  Skip the save offer when the result clearly doesn't fit a report type — a single scalar count, an aggregate roll-up across entity types, view-curve time series, schema introspection output, or anything that isn't a list of channels / brands / videos / sponsorships. A trailing offer on those would just be noise.
153
153
 
154
- Prefer writing shell code, `jq` commands, or `duckdb` commands that fetch or analysise large sets of data instead of analysing it yourself. On Mac and Linux, create temporary files in `/tmp` that can be analysed later in different ways. On Windows, create them in `%USERPROFILE%\AppData\Local\Temp`. Before analysing a potentially large result set, first try fetching just a single result with `LIMIT 1` without `jq` etc, to see the shape of the data and any error messages.
154
+ Prefer writing shell code, `jq` commands, or `duckdb` commands that fetch or analysise large sets of data instead of analysing it yourself. On Mac and Linux, create temporary files in `/tmp` that can be analysed later in different ways. On Windows, create them in the directory pointed to by the `%TEMP%` environment variable. When coding, do it in Python.
155
155
 
156
156
  ## Available Flows
157
157
 
158
- Note that if you're working on Windows, you must set up UTF-8 in the terminal with `PYTHONIOENCODING=utf-8 tl ...`, because all of these commands return UTF-8 data.
158
+ Note that if you're working on Windows, you must set up UTF-8 because all commands take UTF-8 as inputs and output UTF-8 data. If using the `bash` tool, write commands using the Bash syntax, like `export PYTHONIOENCODING=utf-8 tl db es ...`.
159
159
 
160
160
  ### Data queries
161
161
 
@@ -465,6 +465,8 @@ tl channels find "MrBeast"
465
465
  tl brands find "NordVPN"
466
466
  ```
467
467
 
468
+ If finding channels and brands fail, try variation on the name with or without whitespace.
469
+
468
470
  **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."
469
471
 
470
472
  ```bash
@@ -579,10 +581,6 @@ Users only see data their plan allows:
579
581
  - **Intelligence plan** required for `tl brands`, the full `tl recommender` surface, and `tl db es` access to full transcript / brand-mention data.
580
582
  - **Paid plan** required for `tl snapshots`.
581
583
 
582
- ## Important: Status Labels
583
-
584
- When presenting sponsorship status data, always use human-readable labels — never raw codes. The `tl` CLI returns lowercase labels (`sold`, `pending`, `matched`, etc.) — capitalize them for display. Full mapping: proposed, unavailable, pending, sold, advertiser_reject → "Rejected by Advertiser", publisher_reject → "Rejected by Publisher", proposal_approved → "Proposal Approved", matched, outreach → "Reached Out", agency_reject → "Rejected by Agency".
585
-
586
584
  ## Important: Firebolt Snapshots
587
585
 
588
586
  `tl snapshots video` **always requires** `--channel`. Without it, the query scans 7.4 billion rows and times out. Always provide the channel ID.
@@ -191,12 +191,14 @@ A channel can have multiple adspots (different sellers: talent manager, direct,
191
191
 
192
192
  When composing `SELECT ... FROM thoughtleaders_channel ...`, do not improvise column names from semantic intuition — consult the output of `tl schema pg thoughtleaders_channel` or the column table above. The `tl schema` command is authoritative. Failed guesses return *"column '\<name\>' does not exist"* and cost a round-trip. Recurring problems:
193
193
 
194
+ - Subscriber count is in the field named `reach`
195
+ - Projected views is in the field named `impression`
194
196
  - ❌ **Suffix/qualifier variants of date columns** (e.g. an `_max` / `latest_` / `_date` form when the canonical column has neither). Date columns use bare names.
195
197
  - ❌ **Platform-name-prefixed ID forms** (e.g. a platform-name prefix when the canonical column uses a neutral `external_` prefix). See the column table for the actual ID column.
196
198
  - ❌ **Bare-noun forms without the table-prefix** (e.g. `name` instead of `channel_name`). This table prefixes its display fields with `channel_` to avoid SQL keyword collisions and ambiguity in joins.
197
199
  - ❌ **User-facing-term forms used as SQL column names** (the user-facing word is sometimes different from the SQL column name; consult [business-glossary](business-glossary.md) for the canonical mapping when the two diverge).
198
200
 
199
- When the canonical column you need isn't obvious from the table above, consult `tl schema pg thoughtleaders_channel` or the above table first. Do **not** rely on a 400 to correct you, and do **not** fall back to `information_schema.columns` as the recovery path — that's a regression marker too.
201
+ When the canonical column you need isn't obvious from the previous description, consult the output of `tl schema pg thoughtleaders_channel`. Do **not** rely on a 400 to correct you, and do **not** fall back to `information_schema.columns` as the recovery path — that's a regression marker too.
200
202
 
201
203
  ### Key columns for the `auth_user` table (Django Users)
202
204