thoughtleaders-cli 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. thoughtleaders_cli-0.5.0.dist-info/METADATA +215 -0
  2. thoughtleaders_cli-0.5.0.dist-info/RECORD +59 -0
  3. thoughtleaders_cli-0.5.0.dist-info/WHEEL +4 -0
  4. thoughtleaders_cli-0.5.0.dist-info/entry_points.txt +2 -0
  5. thoughtleaders_cli-0.5.0.dist-info/licenses/LICENSE +21 -0
  6. tl_cli/__init__.py +3 -0
  7. tl_cli/_completions.py +4 -0
  8. tl_cli/_plugin/.claude-plugin/marketplace.json +17 -0
  9. tl_cli/_plugin/.claude-plugin/plugin.json +12 -0
  10. tl_cli/_plugin/agents/tl-analyst.md +66 -0
  11. tl_cli/_plugin/commands/tl-balance.md +10 -0
  12. tl_cli/_plugin/commands/tl-brands.md +16 -0
  13. tl_cli/_plugin/commands/tl-channels.md +31 -0
  14. tl_cli/_plugin/commands/tl-reports.md +16 -0
  15. tl_cli/_plugin/commands/tl-sponsorships.md +23 -0
  16. tl_cli/_plugin/commands/tl.md +28 -0
  17. tl_cli/_plugin/hooks/hooks.json +26 -0
  18. tl_cli/_plugin/hooks/scripts/post-usage.sh +26 -0
  19. tl_cli/_plugin/hooks/scripts/pre-check.sh +30 -0
  20. tl_cli/_plugin/skills/tl/SKILL.md +413 -0
  21. tl_cli/_plugin/skills/tl/references/business-glossary.md +159 -0
  22. tl_cli/_plugin/skills/tl/references/elasticsearch-schema.md +259 -0
  23. tl_cli/_plugin/skills/tl/references/firebolt-schema.md +208 -0
  24. tl_cli/_plugin/skills/tl/references/postgres-schema.md +269 -0
  25. tl_cli/auth/__init__.py +0 -0
  26. tl_cli/auth/commands.py +49 -0
  27. tl_cli/auth/login.py +328 -0
  28. tl_cli/auth/pkce.py +21 -0
  29. tl_cli/auth/token_store.py +98 -0
  30. tl_cli/client/__init__.py +0 -0
  31. tl_cli/client/errors.py +72 -0
  32. tl_cli/client/http.py +109 -0
  33. tl_cli/commands/__init__.py +0 -0
  34. tl_cli/commands/ask.py +54 -0
  35. tl_cli/commands/balance.py +68 -0
  36. tl_cli/commands/brands.py +174 -0
  37. tl_cli/commands/changelog.py +119 -0
  38. tl_cli/commands/channels.py +291 -0
  39. tl_cli/commands/comments.py +63 -0
  40. tl_cli/commands/db.py +104 -0
  41. tl_cli/commands/deals.py +52 -0
  42. tl_cli/commands/describe.py +166 -0
  43. tl_cli/commands/doctor.py +70 -0
  44. tl_cli/commands/matches.py +69 -0
  45. tl_cli/commands/proposals.py +69 -0
  46. tl_cli/commands/reports.py +346 -0
  47. tl_cli/commands/schema.py +55 -0
  48. tl_cli/commands/setup.py +401 -0
  49. tl_cli/commands/snapshots.py +93 -0
  50. tl_cli/commands/sponsorships.py +193 -0
  51. tl_cli/commands/uploads.py +84 -0
  52. tl_cli/commands/whoami.py +206 -0
  53. tl_cli/config.py +55 -0
  54. tl_cli/filters.py +88 -0
  55. tl_cli/hints.py +53 -0
  56. tl_cli/main.py +209 -0
  57. tl_cli/output/__init__.py +0 -0
  58. tl_cli/output/formatter.py +436 -0
  59. tl_cli/self_update.py +173 -0
@@ -0,0 +1,413 @@
1
+ ---
2
+ name: tl
3
+ description: Query and analyze ThoughtLeaders business data using the `tl` CLI — structured resource commands (sponsorships, deals, channels, brands, uploads, snapshots, reports) plus raw PostgreSQL / Elasticsearch / Firebolt access via `tl db pg|fb|es`. Triggers on questions about deals, sponsorships, pipeline, revenue, brands, channels, MSN, TPP, uploads/videos, transcripts, brand mentions, view-curves, sales numbers, reports, or any cross-source business analysis ("how many deals", "pipeline report", "weighted pipeline", "channel data", "brand lookup", "view curve", "find mentions of", "investigate this video", "query the database"). Use structured tl commands — you ARE the AI layer, not tl ask.
4
+ ---
5
+
6
+ # ThoughtLeaders Data Analyst
7
+
8
+ You have access to the `tl` CLI which queries ThoughtLeaders' sponsorship platform data. Run it to answer questions about deals, channels, brands, uploads, metrics, and more.
9
+
10
+ ## Core Principles
11
+
12
+ **You are the intelligence layer.** Use structured `tl` commands, not `tl ask`. The `tl ask` command is a server-side LLM fallback for users without Claude — but the user has you. Translate their questions into the right `tl` commands.
13
+
14
+ Always run `tl describe show <resource>` before running a query against that resource. For raw-db queries, use `tl schema pg|fb|es` to get the database schemas.
15
+
16
+ Always assume there will be more than 1 page of results. You MUST always use `--limit` and `--offset` options in the `tl list` commands to retrieve the entire data set (all pages, until the total records are fetched). You must also always use pagination in scripts you write to collect results. The maximum number of results per page is 500.
17
+
18
+ Retry after 5 seconds if the server returns a "connection denied" or a "server error" on any request.
19
+
20
+ Where possible reference sponsorships, brands, channel by numeric IDs.
21
+
22
+ ## Data Model & Terminology
23
+
24
+ ThoughtLeaders is a sponsorship marketplace connecting **Brands** (advertisers / media buyers) with **Channels** (YouTube creators, podcasters / media sellers).
25
+
26
+ The centre of the data model is **Sponsorships** — business relationships between brands and channels. Sponsorships have a funnel of types, from broad to narrow:
27
+
28
+ - **Sponsorships** — the broadest category, encompassing all stages
29
+ - **Matches** — possible brand-channel pairings that ThoughtLeaders thinks could work
30
+ - **Proposals** — matches that have been proposed to both sides to consider
31
+ - **Deals** — contractually agreed-upon sponsorships (sold), either in production or published
32
+
33
+ Sponsorships are sometimes called "Ads" or "Ad campaigns".
34
+
35
+ The CLI has shortcut commands for each type: `tl matches`, `tl proposals`, `tl deals`. These filter `tl sponsorships` by status.
36
+
37
+ Other key concepts:
38
+ - **Uploads** — YouTube videos indexed from Elasticsearch
39
+ - **Snapshots** — historical time-series metrics for channels and videos (Firebolt)
40
+ - **Reports** — saved report configurations that can be re-run
41
+ - **Comments** — notes attached to sponsorships
42
+ - **Adspots** — types of ads a channel carries (e.g. mention, dedicated video, product placement). Returned by `tl channels show`; each carries price/cost.
43
+ - **MSN** (Media Selling Network) — the ~11k YouTube channels that have opted in to receive sponsorship offers. Returned as a boolean `msn` field on every channel response (list, detail, similar). Derived server-side from whether `Channel.media_selling_network_join_date` is non-null — the timestamp itself isn't exposed over the CLI, just the boolean. Filterable via `msn:` tri-state: `msn:yes` (MSN only — the default on `similar`; on `list` the default is `both`), `msn:no` (non-MSN only), `msn:both` (no filter).
44
+ - **TPP** (ThoughtLeaders Partner Program, a.k.a. "TL channels") — the smaller, exclusive ~169 channels TL manages directly. Returned as the `tpp` boolean field on every channel response (list, detail, similar). Filterable via `tpp:` with the same tri-state vocabulary: `tpp:yes` / `tpp:no` / `tpp:both` (default `both`).
45
+ - **`demographics_updated_at`** (on channel detail) — ISO timestamp of when demographic screenshots were last uploaded and processed via OCR. If non-null, the channel has demographics screenshots on file. If null, no screenshots have been uploaded. Use this to check whether a channel has demographics data from screenshots.
46
+ - **`impression`** (on channels) — projected views per video on that channel. Forward-looking estimate. May be null when not yet computed.
47
+ - **`views`** (on sponsorships) — actual view count of the sold and published sponsored video, accessible when `article_id` is set.
48
+ - **`impressions_guarantee`** (on sponsorships) — projected/guaranteed impressions for the sponsorship. Numeric; rounded to int in list output.
49
+ - **Sponsorship detail fields** (returned by `tl sponsorships show <id> --json`) — in addition to the list-view columns, 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.
50
+ - **CPM** has two distinct meanings depending on level — pick the one the user actually wants:
51
+ - **Channel CPM** = `(adspot.price / channel.impression) × 1000` — projected price per thousand projected views. Used for pricing decisions **before** a sponsorship is sold. Available for channels with active adspots via `tl channels show <channel_id>`.
52
+ - **Sponsorship CPM** = calculated in either of two ways: if `views` is present, then CPM is `(sponsorship.price / sponsorship.views) × 1000`, meaning realized cost per thousand actual views, computed post-publication. If `views` is null, Compute from the sponsorship's `price` and the channel's `impression` fields.
53
+ - **CPM does not have a range filter.** To find sponsorships in a CPM range (e.g. "around $15"), fetch the record set with other filters first, then apply the CPM range in post-processing (jq, Python, etc.) on the returned `cpm` field. Plan queries and pagination accordingly — the server cannot reduce the result count based on CPM.
54
+ - **Sponsorship dates** — each sponsorship has four distinct dates, useful for different queries:
55
+ - **`created_at`** — when the sponsorship record was created in the system
56
+ - **`purchase_date`** — when the sponsorship was purchased (i.e. when the deal was made); These make up bookings.
57
+ - **`send_date`** — the date the video is/was expected to be published (scheduled)
58
+ - **`publish_date`** — the date the video was actually published; These make up live ads.
59
+ - **Credits** — every data query costs credits; use `tl describe` to see rates
60
+
61
+ Users see data scoped by their organization and plan:
62
+ - **Media buyers** see sponsorships where their org is the brand. They see `price` but never `cost`.
63
+ - **Media sellers** see sponsorships where their org is the publisher. They see `cost` but never `price`.
64
+ - **Intelligence plan** is required for `tl brands`, full channel search, and full uploads.
65
+
66
+ When querying sponsorship bookings, query by `status:sold` and filter the the date range only by `purchase_date`. Otherwise, query for state:sold by `created_at`.
67
+
68
+ An obsolete name for "sponsorship" is an "adlink".
69
+
70
+ ## Methodology
71
+
72
+ Where possible, if searching for a sponsorship match between channels and brands, first search for what do similar brands sponsor / which brands is the channel usually sponsored by. The similarity judgement should be preferably based on similar topics, similar upload frequency, similar channel sizes, and only after all that, on demographics.
73
+
74
+ ## Workflow
75
+
76
+ At the start of session, always run a `tl help` command to find out which commands are available, and the `tl whoami` command to find out what you have access to.
77
+
78
+ Unless the user specifically asks for running a specific report or showing the result of a specific report, find the data by using other, low-level commands.
79
+
80
+ 1. **Discover first**: Run `tl describe show <resource> --json` to learn available fields, filters, and credit costs before querying
81
+ 2. **Check saved reports**: Run `tl reports --json` to see if the user has a saved report that already answers their question
82
+ 3. **Check credits**: Run `tl balance --json` before expensive queries. Warn the user if a query will cost many credits.
83
+ 4. **Query with filters**: Use `key:value` filter syntax for structured queries
84
+ 5. **Always use --json**: Parse JSON output for multi-step analysis.
85
+ 6. **Chain commands**: For complex questions, chain multiple `tl` commands
86
+ 7. **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.
87
+
88
+ Prefer writing Python code, shell code, or `jq` commands that fetche or analysise large sets of data, instead of analysing it yourself. Create temporary files in `/tmp` that can be analysed later in different ways. Before bulk data analysis by running `jq`, Python or Bash commands, first try fetching just a single result with `--limit 1` without `jq` etc, to see the shape of the data and any error messages.
89
+
90
+ ## Available Commands
91
+
92
+ ### Data queries
93
+ ```bash
94
+ tl sponsorships list [filters...] # Sponsorships — list curve, mult 1.0
95
+ tl sponsorships show <id> # Sponsorship detail (2 credits)
96
+ tl sponsorships create --channel <id> --brand <id> # Create proposal (free)
97
+ tl deals list [filters...] # Shortcut: agreed-upon sponsorships (status:deal); same curve as sponsorships list
98
+ tl deals show <id> # Deal detail (2 credits)
99
+ tl matches list [filters...] # Shortcut: possible brand-channel pairings (status:match); same curve
100
+ tl matches show <id> # Match detail (2 credits)
101
+ tl matches create --channel <id> --brand <id> # Create match (free)
102
+ tl proposals list [filters...] # Shortcut: proposed matches (status:proposal); same curve
103
+ tl proposals show <id> # Proposal detail (2 credits)
104
+ tl proposals create --channel <id> --brand <id> # Create proposal (free)
105
+ tl uploads list [filters...] # Video uploads from ES — list curve, mult 1.0
106
+ tl uploads show <id> # Upload detail (2 credits)
107
+ tl channels list [filters...] # Channel search — list curve, mult 1.0
108
+ tl channels show <id-or-name> # Channel detail (2 credits; accepts numeric ID or name)
109
+ tl channels history <id-or-name> # Sponsorship history (5 credits/result, linear)
110
+ tl channels similar <id-or-name> # Vector-similarity recommender (50 credits flat; Intelligence plan)
111
+ tl brands show <id-or-name> # Brand detail (1 credit)
112
+ tl brands history <id-or-name> # Sponsorship history (5 credits/result, linear)
113
+ tl brands history <query> --channel <id> # Brand mentions on specific channel
114
+ tl brands similar <id-or-name> # Find similar brands via profile vector KNN (50 credits flat)
115
+ tl snapshots channel <id> # Channel metrics over time — list curve, mult 1.2 (Firebolt-backed)
116
+ tl snapshots video <id> --channel <id> # Video view curve — list curve, mult 1.2 (--channel required!)
117
+ tl reports # List saved reports — list curve, mult 1.3
118
+ tl reports run <id> # Run a saved report (credits vary)
119
+ tl comments list <adlink-id> # List comments — list curve, mult 1.0
120
+ tl comments add <adlink-id> "msg" # Add comment (free)
121
+ ```
122
+
123
+ **"List curve"** above means non-linear pricing: `cost = 1 + mult × 0.126 × n^1.2`. The flat 1-credit setup applies to every list call; the `mult` reflects per-resource complexity. `tl db {pg,fb,es}` shares the same curve at mult=1.4. Concrete totals:
124
+
125
+ | Rows | mult=1.0 (channels, brands, comments, uploads, sponsorships) | mult=1.2 (snapshots) | mult=1.3 (reports) | mult=1.4 (db.pg / db.fb / db.es) |
126
+ |---:|---:|---:|---:|---:|
127
+ | 1 | 1 | 1 | 1 | 1 |
128
+ | 10 | 3 | 3 | 4 | 4 |
129
+ | 50 | 15 | 18 | 19 | 20 |
130
+ | 100 | 33 | 39 | 42 | 45 |
131
+ | 200 | 74 | 88 | 96 | 103 |
132
+ | 500 | 219 | 263 | 285 | 307 |
133
+
134
+ The marginal per-row cost is exactly proportional to `mult` — a 1.4× resource costs 1.4× the row part of a 1.0× resource at any size. Splitting a 500-row pull into ten 50-row calls saves ~30% but burns 10 setup floors instead of 1; "narrow the query" is almost always the better move than "fragment the pagination."
135
+
136
+ ### Raw queries (`tl db`)
137
+
138
+ When a structured `tl <resource>` command can answer the question, prefer it — authoritative, role-scoped, paginated, breadcrumbed. Drop down to `tl db pg|fb|es` only when the high-level command can't express what you need.
139
+
140
+ ```bash
141
+ tl db pg "<SELECT ...>" # PostgreSQL — read-only SELECT
142
+ tl db fb "<SELECT ...>" # Firebolt — single-table reads on article_metrics / channel_metrics
143
+ tl db es "<JSON body>" # Elasticsearch — search bodies against the server-fixed alias
144
+ ```
145
+
146
+ All three honour `--json`/`--csv`/`--md`/`--toon` and accept `-` to read from stdin (`cat q.sql | tl db fb -`). They share the list-curve at `mult=1.4` (raw queries, no role scoping, wider blast radius).
147
+
148
+ **Reach for raw queries — don't simulate them client-side.** The structured `list` commands are great for filtered records. The moment the question turns into **aggregation, joining, or complex multi-condition filtering**, switch to `tl db`:
149
+
150
+ - **Aggregations** (counts, sums, avgs, group-bys, percentiles, time histograms) — push them into a single `tl db es` agg query or `tl db pg` `GROUP BY`. One server-side aggregation is faster, cheaper (one call vs N pages), and avoids the `from+size=10000` deep-pagination cap in ES.
151
+ - **Joins** — "X plus the related Y" belongs in `tl db pg` once it ships. Until then, doing the join client-side via two paginated structured walks is a workaround — flag it as such.
152
+ - **Complex filtering** the structured filter vocabulary can't express (compound boolean, `NOT IN`/`EXISTS`, `WHERE col IS NULL` on hidden fields, mixed range + enum + text predicates) — write it as one query rather than over-fetching and post-filtering.
153
+
154
+ Structured commands stay best for: single-record lookups (`tl <resource> show`), role-scoped filtered lists with simple filters, and anything where breadcrumbs/role-scoping matter.
155
+
156
+ | Need | Use |
157
+ |---|---|
158
+ | Single-record detail lookup | `tl <resource> show <id>` |
159
+ | Simple filtered list of records | Structured `tl <resource> list` |
160
+ | Channel/brand similarity, history | `tl channels similar / history`, `tl brands similar / history` |
161
+ | Saved reports | `tl reports`, `tl reports run` |
162
+ | Time-series view-curve / channel growth (default shape) | `tl snapshots channel`, `tl snapshots video` |
163
+ | **Aggregations** (counts, sums, group-by, histograms, percentiles) | **`tl db es` agg query** or **`tl db pg` `GROUP BY`** — do not paginate-and-roll-up client-side |
164
+ | **Joins / cross-table data** | **`tl db pg`** — single SQL query beats two paginated structured walks |
165
+ | **Complex filtering** the structured filters can't express | **`tl db pg` / `tl db es`** rather than over-fetching and post-filtering |
166
+ | Transcript / brand-mention search inside video content | `tl db es` (no structured equivalent for content text) |
167
+ | Custom Firebolt shape (milestone-age slices, multi-channel growth comparisons) | `tl db fb` |
168
+ | Anything requiring a Postgres column the structured commands don't expose | `tl db pg` |
169
+
170
+ #### `tl db es` — Elasticsearch
171
+
172
+ The CLI sends your JSON body to the server, which validates it before forwarding to ES. The index is fixed server-side (defaults to `tl-platform`); the client cannot select it. To narrow to a quarter or year, scope inside the body with a `publication_date` range filter rather than picking a different alias.
173
+
174
+ ```bash
175
+ # Single video by composite ID
176
+ tl db es '{"size":1,"query":{"term":{"id":"1247603:8LskGvKUA9I"}}}'
177
+
178
+ # Aggregation: count sponsored mentions of brand 5612
179
+ tl db es '{"size":0,"track_total_hits":true,"query":{"term":{"sponsored_brand_mentions":"5612"}}}'
180
+
181
+ # Pipe a larger body
182
+ cat query.json | tl db es -
183
+ ```
184
+
185
+ See [references/elasticsearch-schema.md](references/elasticsearch-schema.md) for accepted top-level keys, query types, size/depth limits, scripting/aggregation rules, and the field catalogue.
186
+
187
+ #### `tl db fb` — Firebolt
188
+
189
+ ```bash
190
+ # View curve for one video (composite key)
191
+ tl db fb "SELECT age, view_count, like_count FROM article_metrics
192
+ WHERE channel_id = 12345 AND id = 'dQw4w9WgXcQ'
193
+ ORDER BY age"
194
+
195
+ # Channel reach over time
196
+ tl db fb "SELECT scrape_date, total_views, reach FROM channel_metrics
197
+ WHERE id = 12345
198
+ ORDER BY scrape_date"
199
+ ```
200
+
201
+ See [references/firebolt-schema.md](references/firebolt-schema.md) for accepted-query rules (SELECT-only, single-table, indexed-filter requirements), table schemas, and ID-format details.
202
+
203
+ #### `tl db pg` — PostgreSQL
204
+
205
+ ```bash
206
+ # Top brands by deal count
207
+ tl db pg "SELECT b.name, COUNT(*) AS deals
208
+ FROM thoughtleaders_adlink a
209
+ JOIN thoughtleaders_profile p ON a.creator_profile_id = p.id
210
+ JOIN thoughtleaders_profile_brands pb ON p.id = pb.profile_id
211
+ JOIN thoughtleaders_brand b ON pb.brand_id = b.id
212
+ WHERE a.publish_status = 3
213
+ GROUP BY b.name
214
+ ORDER BY deals DESC
215
+ LIMIT 20 OFFSET 0"
216
+ ```
217
+
218
+ See [references/postgres-schema.md](references/postgres-schema.md) for the accepted-SQL rules and the table/column catalogue. `tl schema pg` prints the live table/column listing visible to the caller.
219
+
220
+ ### Three sources, each authoritative for different things
221
+
222
+ - **Postgres** — deals, pipeline, brands, channels, users, organizations, profiles, revenue. Source of truth for deal state. Reachable via the structured `tl` commands or raw `tl db pg`.
223
+ - **Elasticsearch** — videos, transcripts, brand mentions, **current** channel/video metrics, demographics. Reachable via `tl uploads`, `tl channels`, and `tl db es`.
224
+ - **Firebolt** — **historical** time-series snapshots only (view curves over time, subscriber-growth trends). Reachable via `tl snapshots` (preferred) or `tl db fb`.
225
+
226
+ **Use Firebolt only when you need a value AT A POINT IN TIME that no longer exists in the current ES/PG snapshot.** For "current views/subs", use ES.
227
+
228
+ **Join keys across sources** (you'll be doing the join in `jq`/Python, not in SQL):
229
+ - `Postgres channel.id` ↔ `ES channel.id` (on article docs) ↔ `Firebolt article_metrics.channel_id` / `channel_metrics.id`
230
+ - `Postgres adlink.article_id` is `<channel_id>:<youtube_id>` — same as ES `_id`. Strip the prefix to get `Firebolt article_metrics.id`.
231
+ - `Postgres brand.id` ↔ ES `sponsored_brand_mentions[]` / `organic_brand_mentions[]`.
232
+ - `publication_id` is **deprecated** — don't use it.
233
+
234
+ **Snapshots are sparse**, especially for older videos. Don't assume two arbitrary dates have data points. For approximations, prefer `tl snapshots` which already implements the project's interpolation logic; falling back to raw `tl db fb` means you handle gaps yourself.
235
+
236
+ ### Schema references
237
+
238
+ Load these on demand — don't read all upfront. Pick the one(s) relevant to the question.
239
+
240
+ - [references/postgres-schema.md](references/postgres-schema.md) — tables, columns, relationships, `publish_status` constants. Required reading for `tl db pg` queries, and useful for understanding what the structured `tl` commands return.
241
+ - [references/elasticsearch-schema.md](references/elasticsearch-schema.md) — index aliases, video/channel fields, common query bodies for `tl db es`.
242
+ - [references/firebolt-schema.md](references/firebolt-schema.md) — the two metric tables and their indexes; how to write valid `tl db fb` queries.
243
+
244
+ Always load the [references/business-glossary.md](references/business-glossary.md) file. It describes how business terms are mapped to database concepts (revenue, weighted pipeline, MSN, TPP, performance grade, team rosters).
245
+
246
+ ### Key business concepts
247
+
248
+ See [references/business-glossary.md](references/business-glossary.md) for revenue/pipeline definitions, performance grades, ownership fields, MSN/TPP, and team rosters.
249
+
250
+ ### Limitations of the `tl`-only data path
251
+
252
+ | Capability | Status | Workaround |
253
+ |---|---|---|
254
+ | Arbitrary read-only `SELECT` on Postgres | **Available** via `tl db pg`. | SELECT-only, mandatory `LIMIT ≤ 500` + `OFFSET`, function allowlist, no `::reg*` casts. See `references/postgres-schema.md`. |
255
+ | Cross-reference helpers ("channels proposed to brand X", "channels sponsored by MBN brands in last N days") | **Unavailable** — these were stacked PG joins. | Approximate with `tl brands history <brand>` (videos where the brand was detected → extract channel IDs) and `tl sponsorships list brand:<name> status:<...>`. Won't perfectly match (e.g. `media_buying_network_join_date` isn't exposed). |
256
+ | **AdLink INSERT** with custom price/cost/owner/`weighted_price`/`created_where` | **Unavailable** — `tl sponsorships create` exists but only creates a free *proposal* between a channel and a brand. It does not let you set price/cost/owner_sales_id/send_date/etc. | Done in the app or by a human with DB access. |
257
+ | Pre-insert validation queries (joining `adspot ↔ channel ↔ profile ↔ org` to confirm MSN, integration=1, persona, plan) | **Unavailable** as a single query (needs PG joins). | Partial: `tl channels show <id>` exposes `msn`, `tpp`, and active adspots with `integration` codes. Persona/plan/profile-level checks aren't surfaced. |
258
+ | 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. |
259
+ | 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` (vector KNN, server-implemented). |
260
+ | ES deep pagination beyond `from+size = 10,000` | **Unavailable** via raw — `scroll` and `pit` aren't allowlisted; `search_after` is allowed but `from` is still capped. | Use `search_after` with `sort` to walk past 10k. For huge sweeps, narrow with `publication_date` ranges. |
261
+ | 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. |
262
+ | 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). |
263
+
264
+ If a user asks for one of the **Unavailable** items, say so explicitly and propose the closest `tl`-based approximation rather than silently degrading.
265
+
266
+ ### Discovery & system
267
+ ```bash
268
+ tl describe # List all resources with credit costs (free)
269
+ tl describe show <resource> --json # Fields, filters, credit rates (free)
270
+ tl schema pg # PostgreSQL schema reference for `tl db pg` (free)
271
+ tl schema fb # Live Firebolt tables and column types for `tl db fb` (free)
272
+ tl schema es # Elasticsearch document shape for `tl db es` (free)
273
+ tl balance --json # Credit balance (free)
274
+ tl whoami # Current user, org, brands (free)
275
+ tl auth status # Auth check (free)
276
+ tl changelog # Release notes — current version, or current..latest if behind (free)
277
+ tl changelog v0.4.17 v0.4.18 # Notes for explicit versions
278
+ tl changelog since v0.4.10 # Notes from v0.4.10 to latest
279
+ tl changelog --md > CHANGELOG.md # Capture for a doc
280
+ ```
281
+
282
+ `tl changelog` summaries are LLM-generated server-side from full commit messages and cached per version, so repeat calls are fast and don't re-bill the LLM. The release date and a 2–4 sentence prose summary come back per version.
283
+
284
+ ### Filter syntax
285
+ All list commands accept `key:value` filters:
286
+ ```bash
287
+ tl sponsorships list status:sold brand:"Nike" purchase-date:2026-01
288
+ tl uploads list channel:12345 type:longform
289
+ tl channels list category:cooking min-subs:100k language:en
290
+ tl channels list tpp:yes # list all TPP (TL-managed) channels
291
+ tl channels list tpp:no primary-device:mobile # mobile-first channels that aren't in TPP
292
+ tl channels list msn:yes category:tech # Media Selling Network channels in tech
293
+ tl channels list msn:no min-subs:500k # big non-MSN channels (not yet opted in)
294
+ ```
295
+
296
+ Date filters accept keywords: `today`, `yesterday`, `tomorrow`.
297
+
298
+ #### Channel demographic filters
299
+
300
+ These filters apply to both `tl channels list` and `tl sponsorships list` (the latter filters by the associated channel's demographics):
301
+
302
+ ```bash
303
+ # Primary device type
304
+ tl channels list primary-device:mobile
305
+ tl channels list primary-device:desktop
306
+ tl channels list primary-device:tablet
307
+
308
+ # Minimum device audience share (0–100)
309
+ tl channels list min-mobile-share:60
310
+ tl channels list min-desktop-share:30
311
+ tl channels list min-tablet-share:10
312
+
313
+ # Minimum geo share (0–100, ISO country codes lowercase)
314
+ tl channels list min-us-share:70
315
+ tl channels list min-gb-share:25
316
+
317
+ # Combine with other filters
318
+ tl channels list category:tech primary-device:mobile min-us-share:50 min-subs:100k
319
+ tl sponsorships list status:sold primary-device:mobile min-us-share:60
320
+ ```
321
+
322
+ ### Output flags
323
+ - `--json` — structured JSON (use this for parsing)
324
+ - `--csv` — CSV output
325
+ - `--md` — Markdown table
326
+ - `--limit N` — max results
327
+ - `--offset N` — pagination
328
+
329
+ ### Response shape
330
+ Successful `--json` responses wrap data in an envelope:
331
+
332
+ ```json
333
+ {
334
+ "results": [ { "...": "..." } ],
335
+ "total": 42,
336
+ "usage": { "credits_charged": 2, "balance_remaining": 9998 },
337
+ "_breadcrumbs": [ { "hint": "...", "command": "tl ..." } ]
338
+ }
339
+ ```
340
+
341
+ Errors return `{"detail": "..."}` with an HTTP status (400 / 401 / 403 / 404).
342
+
343
+ While analysing results, you must always examine the `results` field in the JSON.
344
+
345
+ ## Credit Awareness
346
+
347
+ Every query costs credits. Before running expensive queries:
348
+ 1. Check the credit rate: `tl describe show <resource> --json | jq '.credits'` and the user balance.
349
+ 2. **List endpoints (sponsorships/channels/uploads/snapshots/comments/reports/db) are priced non-linearly:** `cost = 1 + mult × 0.126 × n^1.2`, where `mult` is the per-resource complexity factor (1.0 for cheap reads, 1.2 for snapshots, 1.3 for reports, 1.4 for raw db). Detail/history/similar endpoints are linear (`rate × results`). See the table in the command list above.
350
+ 3. Estimate cost from the formula or the table; for non-list endpoints use `results × rate`.
351
+ 4. If estimated cost is more than 10% of the remaining balance, ask the user to confirm the operation before running.
352
+
353
+ ## Data Scoping
354
+
355
+ Users only see data their plan allows:
356
+ - **Media buyers** see deals where their org is the brand. They see `price` but never `cost`.
357
+ - **Media sellers** see deals where their org is the publisher. They see `cost` but never `price`.
358
+ - **Intelligence plan** required for `tl brands`, full `tl channels list` search, and full `tl uploads list`.
359
+ - **Paid plan** required for `tl snapshots`.
360
+
361
+ ## Important: Status Labels
362
+
363
+ 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".
364
+
365
+ ## Important: Firebolt Snapshots
366
+
367
+ `tl snapshots video` **always requires** `--channel`. Without it, the query scans 7.4 billion rows and times out. Always provide the channel ID.
368
+
369
+ ## Examples
370
+
371
+ "Show me my sold sponsorships this quarter":
372
+ ```bash
373
+ tl deals list purchase-date-start:2026-01-01 --json
374
+ ```
375
+
376
+ "What channels does Nike sponsor?":
377
+ ```bash
378
+ tl brands history Nike --json
379
+ ```
380
+
381
+ "Compare view curves for two videos":
382
+ ```bash
383
+ tl snapshots video abc123 --channel 456 --json
384
+ tl snapshots video def789 --channel 456 --json
385
+ ```
386
+
387
+ "Run my Q1 pipeline report":
388
+ ```bash
389
+ tl reports --json # Find the report ID first
390
+ tl reports run 42 --json
391
+ ```
392
+
393
+ "Find mobile-first US channels in cooking":
394
+ ```bash
395
+ tl channels list category:cooking primary-device:mobile min-us-share:50 --json
396
+ ```
397
+
398
+ "Show sold sponsorships targeting mobile US audiences":
399
+ ```bash
400
+ tl sponsorships list status:sold primary-device:mobile min-us-share:60 --json
401
+ ```
402
+
403
+ "Find channels similar to one I know" (vector-similarity recommender, 50 credits per call):
404
+ ```bash
405
+ tl channels similar 29834 --limit 10 # by ID (defaults to msn:yes, tpp:both)
406
+ tl channels similar "Tremending girls" --limit 5 # by unique name
407
+ tl channels similar 29834 min-score:0.85 --limit 20 # tighter similarity threshold
408
+ tl channels similar 29834 msn:both min-score:0.4 --limit 30 # include both MSN and non-MSN channels
409
+ tl channels similar 29834 msn:no --limit 30 # non-MSN channels only
410
+ tl channels similar 29834 tpp:yes --limit 30 # TPP (TL-managed) channels only
411
+ tl channels similar 29834 min-subs:1000000 exclude:477487 --limit 15 # client-side filters
412
+ ```
413
+ **Both `tl channels show` and `tl channels similar` accept either a numeric channel ID or a channel name.** Name arguments are case-insensitive partial matches; if more than one active channel matches, the command prints a candidates table (channel_id, subscribers, name) and exits 1 so you can retry with a specific ID. The `msn` filter on `similar` is tri-state: `yes` (only MSN channels — the default), `no` (only non-MSN channels), `both` (no MSN filter). `tl channels look-alike` is a hidden alias for `similar` that matches the internal "look-alike channels" terminology.
@@ -0,0 +1,159 @@
1
+ # ThoughtLeaders Business Glossary
2
+
3
+ Maps business terms to database concepts.
4
+
5
+ ## Revenue & Deal Lifecycle
6
+
7
+ | Business Term | DB Concept | Notes |
8
+ |--------------|------------|-------|
9
+ | **Revenue / Sold ad** | `adlink.publish_status = 3` (SOLD) | Only status=3 counts as real revenue |
10
+ | **Gross ads / Gross revenue** | `SUM(adlink.price)` where sold | Total advertiser spend |
11
+ | **Net revenue / TL profit** | `price - cost` per adlink | What TL earns as a company |
12
+ | **Cost** | `adlink.cost` | What the channel earns |
13
+ | **Price** | `adlink.price` | What the advertiser pays |
14
+ | **Closed-lost** | `publish_status IN (4, 5, 9)` | All three rejection statuses |
15
+ | **Open opportunity** | `publish_status IN (0, 2, 6, 7, 8)` | Pipeline — not revenue, not lost |
16
+ | **Proposal Approved** | `publish_status = 6` | AM approved to show to brand — NOT brand approval. Internal gate only. |
17
+ | **Pending** | `publish_status = 2` | Brand has agreed — this is the real high-intent signal |
18
+ | **Weighted pipeline** | `SUM(weighted_price)` for open opps | Pre-calculated on save |
19
+ | **Ad is live** | `publish_date IS NOT NULL` | Until publish_date is set, ad is not on YouTube |
20
+ | **Cancellation risk** | Sold but `publish_date IS NULL` | Sold deals without publish_date can still be canceled |
21
+
22
+ ## Performance Grade (`adlink.performance_grade`)
23
+
24
+ | Value | Label | Description |
25
+ |-------|-------|-------------|
26
+ | 0 | Pending | Not yet graded (treat as NULL) |
27
+ | 1 | Loser | Underperforming ad — do not renew |
28
+ | 2 | Neutral | Mixed results — test one more time, ideally at a better rate. Second test determines if channel becomes Loser or Winner |
29
+ | 3 | Winner | High-performing ad — should always be renewed |
30
+
31
+ **Renewal logic:** All Winners should be renewed. Neutrals get one more test (ideally at a lower CPM), then reclassified as Winner or Loser.
32
+
33
+ ## View Guarantees
34
+
35
+ | Field | Description |
36
+ |-------|-------------|
37
+ | `adlink.impressions_guarantee` | The number of views guaranteed for the ad (bigint). 0 or NULL = no guarantee. |
38
+ | `adlink.view_guarantee_hit_date` | Timestamp when the guarantee was met. NULL = not yet hit or no guarantee. |
39
+ | `adlink.projected_views_at_purchase_date` | Projected views at time of purchase (used for CPM estimation). |
40
+
41
+ ## Entities
42
+
43
+ | Business Term | DB Table | Notes |
44
+ |--------------|----------|-------|
45
+ | **Deal / Sponsorship** | `thoughtleaders_adlink` | One brand ↔ channel placement |
46
+ | **Brand** | `thoughtleaders_brand` | Advertiser entity (the buying-side brand) |
47
+ | **Brand profile** | `thoughtleaders_profile` | Advertiser entity / account |
48
+ | **Organization** | `thoughtleaders_organization` | Parent entity for profiles |
49
+ | **Channel** | `thoughtleaders_channel` | YouTube channel |
50
+ | **Ad Spot (Catalogue item)** | `thoughtleaders_adspot` | TL's catalogue of buyable placements. Price/cost on adspot are *list prices* only — each adlink (instance) can have completely different price/cost |
51
+ | **Campaign** | `dashboard_campaign` | Groups multiple deals |
52
+
53
+ ## Ad Spots & Channels
54
+
55
+ - A channel can have **multiple ad spots** because different people sell the same channel (talent manager, direct, multiple agencies)
56
+ - Ad spots are the **catalogue** — adlinks are **instances** of catalogue items
57
+ - Price/cost on adspot = list/catalog values; price/cost on adlink = actual deal values
58
+ - **Only one active adspot with integration=mention per channel at any time** (MSN rule)
59
+
60
+ ## Ownership & Accountability
61
+
62
+ | Field | Model | Meaning |
63
+ |-------|-------|---------|
64
+ | `owner_sales_id` | `adlink` | **Most important.** Person responsible for closing the deal and for the revenue. Final accountability. |
65
+ | `owner_advertiser_id` | `adlink` | Brand-side owner for this specific deal |
66
+ | `owner_publisher_id` | `adlink` | Channel-side owner for this specific deal |
67
+ | `owner_advertiser_id` | `profile` | **Account owner.** Who owns the brand relationship overall. Often same person as owner_sales on adlinks, but not always. |
68
+ | `owner_publisher_id` | `profile` | Channel relationship owner on the profile level |
69
+ | `owner_sales_id` | `profile` | Sales owner at profile level |
70
+
71
+ **Key insight:** Ownership exists on both `profile` (account-level) and `adlink` (deal-level). For revenue attribution, always use `adlink.owner_sales_id`.
72
+
73
+ ## MSN (Media Selling Network)
74
+
75
+ - Channels where TL has **≥80% confidence** they can buy an ad tomorrow
76
+ - Key data: **who is the contact** to buy the ad from
77
+ - `thoughtleaders_channel.media_selling_network_join_date` = when channel joined MSN
78
+ - `thoughtleaders_channel.is_tl_channel` = TPP/VIP channel (subset of MSN)
79
+ - **Rule:** Only one active adspot with `integration=mention` per channel at any time
80
+ - MSN quality depends on having current, accurate contact info
81
+
82
+ ## Teams & Ownership
83
+
84
+ ### Brand-led Revenue (Sales / Account Management)
85
+
86
+ These teams close deals and manage brand relationships. Revenue is attributed via `adlink.owner_sales_id`.
87
+
88
+ **Emma's team:**
89
+ | Person | auth_user.id | Owner field |
90
+ |--------|-------------|-------------|
91
+ | Emma | 11158 | `adlink.owner_sales_id` |
92
+ | Orli | 2042 | `adlink.owner_sales_id` |
93
+ | Eli | 20836 | `adlink.owner_sales_id` |
94
+ | Grace | 20835 | `adlink.owner_sales_id` |
95
+ | Mark | 23979 | `adlink.owner_sales_id` |
96
+ | Abbie | 23978 | `adlink.owner_sales_id` |
97
+ | Ariella | 23977 | `adlink.owner_sales_id` |
98
+
99
+ **Nicole's team:**
100
+ | Person | auth_user.id | Owner field |
101
+ |--------|-------------|-------------|
102
+ | Nicole | 9929 | `adlink.owner_sales_id` |
103
+ | Maika | 5412 | `adlink.owner_sales_id` |
104
+ | Yuval | 14252 | `adlink.owner_sales_id` |
105
+ | Revital | 14251 | `adlink.owner_sales_id` |
106
+
107
+ ### Network Growth (SDR / Partnerships) — Pauline's team
108
+
109
+ Responsible for growing the MSN (new channels) and MBN (new brands). SDR outreach on both sides.
110
+
111
+ | Person | auth_user.id | Role | Owner field |
112
+ |--------|-------------|------|-------------|
113
+ | Pauline | 218 | Team lead | `adlink.owner_publisher_id` (channel handovers) |
114
+ | Morgan | 5710 | Channel SDR | — |
115
+ | Jen | 873 | Channel SDR | — |
116
+ | Ruby Jean | 9011 | Channel SDR | — |
117
+ | Molly | 11361 | Channel SDR | — |
118
+ | Pierra | 11323 | Brand SDR | — |
119
+ | Nian | 8795 | Brand SDR | — |
120
+
121
+ ### Ad Ops — Jody's team
122
+
123
+ Manages getting sold ads published. `profile.owner_publisher_id` = ad ops manager of an account.
124
+
125
+ | Person | auth_user.id | Owner field |
126
+ |--------|-------------|-------------|
127
+ | Jody | 71 | `profile.owner_publisher_id` (account-level ad ops owner) |
128
+ | Kathleen | 9274 | `profile.owner_publisher_id` |
129
+ | Shane | 18159 | `profile.owner_publisher_id` |
130
+ | Kevin | 5799 | `profile.owner_publisher_id` |
131
+ | Airis | 5804 | `profile.owner_publisher_id` |
132
+ | Lara | 10743 | `profile.owner_publisher_id` |
133
+ | Josh | 11592 | `profile.owner_publisher_id` |
134
+
135
+ ### Querying by team
136
+
137
+ ```sql
138
+ -- Emma's team pipeline
139
+ SELECT ... FROM thoughtleaders_adlink al
140
+ WHERE al.owner_sales_id IN (11158, 2042, 20836, 20835, 23979, 23978, 23977)
141
+
142
+ -- Nicole's team pipeline
143
+ SELECT ... FROM thoughtleaders_adlink al
144
+ WHERE al.owner_sales_id IN (9929, 5412, 14252, 14251)
145
+
146
+ -- All brand-led revenue (both teams)
147
+ SELECT ... FROM thoughtleaders_adlink al
148
+ WHERE al.owner_sales_id IN (11158, 2042, 20836, 20835, 23979, 23978, 23977, 9929, 5412, 14252, 14251)
149
+
150
+ -- Pauline's network growth team (channel SDRs)
151
+ -- owner_publisher_id on adlink for channel-side work
152
+ SELECT ... FROM thoughtleaders_adlink al
153
+ WHERE al.owner_publisher_id IN (218, 5710, 873, 9011, 11361)
154
+
155
+ -- Jody's ad ops team accounts (profile-level ownership)
156
+ SELECT ... FROM thoughtleaders_profile p
157
+ WHERE p.owner_publisher_id IN (71, 9274, 18159, 5799, 5804, 10743, 11592)
158
+ ```
159
+