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,259 @@
1
+ # ThoughtLeaders Elasticsearch Schema Reference
2
+
3
+ ## How to query
4
+
5
+ All ES access goes through the `tl` CLI:
6
+
7
+ ```bash
8
+ tl db es '{"size": 1, "query": {"match_all": {}}}' --json
9
+
10
+ # Read body from stdin
11
+ cat query.json | tl db es -
12
+ ```
13
+
14
+ The index is **fixed server-side** (defaults to `tl-platform`). The client cannot select an index — there is no `--index` flag. To narrow a query to a smaller time window, scope it inside the body with a `publication_date` range filter rather than picking a different alias.
15
+
16
+ Cost grows non-linearly with result size (raw db queries use the list curve at `mult=1.4`). Aggregation queries bill on `min(hits.total, 200)` instead of `len(hits)`. See `SKILL.md` for the curve formula and the row-count → credits table.
17
+
18
+ Output flags: `--json`, `--csv`, `--md`, `--toon`. The CLI flattens hits into rows of `{_id, _score, ...source}`; aggregations come back in the response envelope and are rendered after the rows in TTY mode.
19
+
20
+ ## Accepted query bodies
21
+
22
+ Read `SKILL.md` → "Raw query reference → `tl db es`" for the full list. Highlights:
23
+
24
+ - **Top-level keys** accepted: `query`, `aggs`/`aggregations`, `sort`, `_source`, `size`, `from`, `track_total_hits`, `highlight`, `fields`, `min_score`, `search_after`, `timeout`, `collapse`, `post_filter`. Anything else (incl. `scroll`, `pit`, `runtime_mappings`, `knn`) is not accepted.
25
+ - `size` ≤ 500. `from + size` ≤ 10,000. Use `search_after` to page deeper.
26
+ - **Accepted query types** include `term`/`terms`/`match`/`bool`/`nested`/`range`/`exists`/`match_phrase`. `query_string`, `regexp`, `wildcard`, `fuzzy`, `more_like_this`, `has_child`, `has_parent`, `parent_id` are not accepted.
27
+ - **No scripts** — any key whose name contains `script` is not accepted.
28
+ - **At most one aggregation total** counted recursively (top-level + sub-agg = 2 = not accepted). Run multiple calls for multi-metric work.
29
+
30
+ ## Index Structure
31
+
32
+ ### `tl-platform-{year}-{quarter}` — Main Content Index
33
+
34
+ The primary index. Contains videos AND channels as parent-child documents (`doc_type` join field).
35
+
36
+ Sharded by quarter going back to 2015. **~15.6M docs in Q1 2026 alone.**
37
+
38
+ Through `tl db es`, all queries hit a server-fixed alias (typically `tl-platform`, which fans out across every quarter). **Always add `publication_date` range filters** when narrowing to a time window — that's the only knob the client has, since the alias itself isn't selectable.
39
+
40
+ The underlying physical layout (one index per quarter, e.g. `tl-platform-2026-q1`, with year and full-platform aliases on top) is for context only.
41
+
42
+ Raw mappings (read-only links — out of band, not via `tl`):
43
+ - [articles](https://github.com/ThoughtLeaders-io/elk-stack-resources/blob/main/elasticsearch/templates/_mappings_article.kibana)
44
+ - [channels](https://github.com/ThoughtLeaders-io/elk-stack-resources/blob/main/elasticsearch/templates/_mappings_channel.kibana)
45
+ - [shared configuration](https://github.com/ThoughtLeaders-io/elk-stack-resources/blob/main/elasticsearch/templates/_mappings_common.kibana)
46
+ - [vector indexes](https://github.com/ThoughtLeaders-io/elk-stack-resources/blob/main/elasticsearch/templates/vectors.kibana)
47
+
48
+ #### Video Fields (selected — 73 total)
49
+
50
+ | Field | Type | Description |
51
+ |-------|------|-------------|
52
+ | `id` | keyword | Video/article ID. Compound form `<channel_id>:<youtube_id>` (matches PG `adlink.article_id` and ES `_id`). |
53
+ | `title` | text | Video title |
54
+ | `description` | text | Video description |
55
+ | `content` | text | Full content/transcript text |
56
+ | `transcript` | text | Raw transcript |
57
+ | `transcript_language` | keyword | Transcript language code |
58
+ | `summary` | text | AI-generated summary |
59
+ | `publication_date` | date | When video was published |
60
+ | `discovery_time` | date | When TL discovered/indexed it |
61
+ | `url` | object | Video URL |
62
+ | `image_url` | object | Thumbnail URL |
63
+ | `views` | long | View count |
64
+ | `total_views` | long | Total views |
65
+ | `projected_views` | long | Projected views |
66
+ | `likes` | long | Like count |
67
+ | `comments` | integer | Comment count |
68
+ | `engagement` | long | Engagement metric |
69
+ | `duration` | integer | Duration in seconds |
70
+ | `duration_live` | integer | Live stream duration |
71
+ | `duration_longform` | integer | Long-form duration |
72
+ | `duration_shorts` | integer | Shorts duration |
73
+ | `content_type` | keyword | longform / short / live |
74
+ | `content_category` | keyword | Content category |
75
+ | `content_aspects` | keyword | Content features/aspects |
76
+ | `language` | keyword | Content language |
77
+ | `country` | keyword | Creator country |
78
+ | `format` | keyword | Platform format |
79
+ | `hashtags` | keyword | Hashtags used |
80
+ | `face_on_screen` | boolean | Whether creator shows face |
81
+
82
+ #### Brand Mention Fields
83
+
84
+ | Field | Type | Description |
85
+ |-------|------|-------------|
86
+ | `brand_mentions` | nested | Full brand mention objects |
87
+ | `all_brand_mentions` | keyword | All brand IDs mentioned |
88
+ | `sponsored_brand_mentions` | keyword | Sponsored brand IDs |
89
+ | `organic_brand_mentions` | keyword | Organic brand IDs |
90
+ | `banner_ads` | object | Banner ad data |
91
+ | `not_sponsored_by` | object | Explicitly not sponsored by |
92
+
93
+ #### Channel Fields (on video docs via `channel.id`, or on channel parent docs)
94
+
95
+ | Field | Type | Description |
96
+ |-------|------|-------------|
97
+ | `name` | text | Channel name |
98
+ | `channel` | object | Channel metadata (nested on article docs) |
99
+ | `reach` | long | Subscriber count |
100
+ | `impression` | long | View count |
101
+ | `impression_live` | long | Live view count |
102
+ | `impression_shorts` | long | Shorts view count |
103
+ | `is_tl_channel` | boolean | TPP partner channel |
104
+ | `is_active` | boolean | Channel is active |
105
+ | `media_selling_network_join_date` | date | MSN join date |
106
+ | `has_outreach_email` | boolean | Has outreach email |
107
+ | `outreach_email` | text | Contact email |
108
+ | `social_links` | text | Social media links |
109
+ | `male_share` | byte | Male audience % |
110
+ | `usa_share` | byte | US audience % |
111
+ | `sponsorship_price` | scaled_float | Sponsorship price |
112
+ | `sponsorship_score` | scaled_float | Sponsorship quality score |
113
+ | `evergreenness` | float | Evergreen score |
114
+ | `evergreenness_live` | scaled_float | Live evergreen score |
115
+ | `evergreenness_longform` | scaled_float | Longform evergreen score |
116
+ | `evergreenness_shorts` | scaled_float | Shorts evergreen score |
117
+ | `trend` | float | Growth trend |
118
+ | `trend_live` | scaled_float | Live trend |
119
+ | `trend_shorts` | scaled_float | Shorts trend |
120
+ | `posts_per_90_days` | integer | Upload frequency |
121
+ | `posts_per_90_days_live` | integer | Live frequency |
122
+ | `posts_per_90_days_shorts` | integer | Shorts frequency |
123
+ | `fulfillment_rate` | scaled_float | Fulfillment rate |
124
+ | `renewal_rate` | scaled_float | Renewal rate |
125
+ | `metrics_update_period` | byte | How often metrics update |
126
+ | `offline_since` | date | When channel went offline |
127
+
128
+ #### AI & Enrichment Fields
129
+
130
+ | Field | Type | Description |
131
+ |-------|------|-------------|
132
+ | `ai` | object | AI-generated metadata |
133
+ | `applied_enrichments` | keyword | Which enrichments have been applied |
134
+ | `article_category` | object | Categorization data |
135
+
136
+ #### System Fields
137
+
138
+ | Field | Type | Description |
139
+ |-------|------|-------------|
140
+ | `@timestamp` | date | Index timestamp |
141
+ | `doc_type` | join | Parent-child join (channel→video) |
142
+ | `es_index_tag` | object | Index routing metadata |
143
+
144
+ ### Other indices
145
+
146
+ - `tl-ingest` — ingestion queue. **Don't query.** Internal pipeline state.
147
+ - `tl-feature-vectors-channel`, `tl-feature-vectors-channel-profile` — channel similarity vectors.
148
+ - `tl-vectors-brand-company-descriptions-*` — brand similarity vectors.
149
+ - `tl-vectors-channel-audience-*`, `tl-vectors-channel-topic-descriptions-*`, `tl-vectors-channel-features` — channel feature vectors.
150
+
151
+ Note: `knn` queries against vector indices are **not currently accepted** as a top-level key. For "find similar" results, use `tl channels similar` / `tl brands similar` — they wrap the vector search server-side.
152
+
153
+ ## Common Query Patterns
154
+
155
+ ### Search videos by sponsored brand mention
156
+
157
+ ```bash
158
+ tl db es '{
159
+ "size": 50,
160
+ "query": {"term": {"sponsored_brand_mentions": "5612"}},
161
+ "_source": ["title", "channel.id", "publication_date", "views"],
162
+ "sort": [{"publication_date": "desc"}]
163
+ }'
164
+ ```
165
+
166
+ ### Search videos for a single channel
167
+
168
+ ```bash
169
+ tl db es '{
170
+ "size": 100,
171
+ "query": {"term": {"channel.id": 12345}},
172
+ "sort": [{"publication_date": "desc"}]
173
+ }'
174
+ ```
175
+
176
+ ### Count sponsored mentions for a brand (size:0 + track_total_hits)
177
+
178
+ ```bash
179
+ tl db es '{
180
+ "size": 0,
181
+ "track_total_hits": true,
182
+ "query": {"term": {"sponsored_brand_mentions": "5612"}}
183
+ }'
184
+ ```
185
+
186
+ ### Full-text search on title/description/summary/content
187
+
188
+ ```bash
189
+ tl db es '{
190
+ "size": 20,
191
+ "query": {
192
+ "multi_match": {
193
+ "query": "ergonomic keyboard review",
194
+ "fields": ["title^3", "description", "summary", "content"]
195
+ }
196
+ },
197
+ "_source": ["title", "channel.id", "publication_date"]
198
+ }'
199
+ ```
200
+
201
+ ### Filter by date range
202
+
203
+ ```bash
204
+ tl db es '{
205
+ "size": 100,
206
+ "query": {
207
+ "bool": {
208
+ "filter": [
209
+ {"term": {"channel.id": 12345}},
210
+ {"range": {"publication_date": {"gte": "2026-01-01", "lte": "2026-03-31"}}}
211
+ ]
212
+ }
213
+ }
214
+ }'
215
+ ```
216
+
217
+ ### Single top-level aggregation (only one aggregation per request is accepted)
218
+
219
+ ```bash
220
+ tl db es '{
221
+ "size": 0,
222
+ "aggs": {
223
+ "by_channel": {
224
+ "terms": {"field": "channel.id", "size": 20}
225
+ }
226
+ },
227
+ "query": {"term": {"sponsored_brand_mentions": "5612"}}
228
+ }'
229
+ ```
230
+
231
+ For more dimensions, run multiple `tl db es` calls and join client-side.
232
+
233
+ ### Deep pagination via `search_after`
234
+
235
+ ```bash
236
+ # First page — sort must include a tiebreaker on _id for stability
237
+ tl db es '{
238
+ "size": 500,
239
+ "query": {"term": {"channel.id": 12345}},
240
+ "sort": [{"publication_date": "desc"}, {"_id": "asc"}]
241
+ }'
242
+
243
+ # Subsequent pages — pass the last hit's sort values as search_after
244
+ tl db es '{
245
+ "size": 500,
246
+ "query": {"term": {"channel.id": 12345}},
247
+ "sort": [{"publication_date": "desc"}, {"_id": "asc"}],
248
+ "search_after": ["2025-09-14", "12345:abc123"]
249
+ }'
250
+ ```
251
+
252
+ ## Notes & gotchas
253
+
254
+ - **Composite IDs:** `tl-platform.id` and `_id` are `<channel_id>:<youtube_id>`. The `youtube_id` portion alone is what Firebolt's `article_metrics.id` stores.
255
+ - **Add a `publication_date` range filter** whenever the question is time-bounded — the alias is fixed, so this is the only way to narrow the search.
256
+ - `sponsored_brand_mentions` and `organic_brand_mentions` are keyword arrays — use `term` queries.
257
+ - For brand mention details (position, snippet, detection_tool), the data is in the `brand_mentions` nested field.
258
+ - **`publication_id` is deprecated** — don't use for joins.
259
+ - No write access. The CLI only exposes `_search` against `tl-platform-*`.
@@ -0,0 +1,208 @@
1
+ # ThoughtLeaders Firebolt Schema Reference
2
+
3
+ ## How to query
4
+
5
+ All Firebolt access goes through the `tl` CLI:
6
+
7
+ ```bash
8
+ tl db fb "SELECT id, age, view_count FROM article_metrics
9
+ WHERE channel_id = 12345 AND id = 'dQw4w9WgXcQ'
10
+ ORDER BY age" --json
11
+
12
+ # Read SQL from stdin
13
+ cat curve.sql | tl db fb -
14
+ ```
15
+
16
+ Cost grows non-linearly with result size (raw db queries use the list curve at `mult=1.4`). A tightly-scoped `WHERE channel_id = X AND id = Y` query rarely costs more than ~10 credits even with months of snapshots; a channel-wide `WHERE channel_id = X` over a busy channel can run into the hundreds. See `SKILL.md` for the curve formula and the row-count → credits table.
17
+
18
+ Output flags: `--json`, `--csv`, `--md`, `--toon`.
19
+
20
+ For **standard view-curve / channel-growth** questions, prefer the higher-level commands — they implement the project's interpolation/sparseness handling:
21
+
22
+ ```bash
23
+ tl snapshots channel <channel_id> --json
24
+ tl snapshots video <video_id> --channel <channel_id> --json # --channel is mandatory
25
+ ```
26
+
27
+ Drop to `tl db fb` only when you need a shape `tl snapshots` doesn't produce (custom aggregates, milestone-age slices, multi-channel growth comparisons, etc.).
28
+
29
+ ## Accepted queries
30
+
31
+ (See `SKILL.md` → "Raw query reference → `tl db fb`" for full reasoning.)
32
+
33
+ - **SELECT only.** No DDL/DML/transactions/SET/locks.
34
+ - **Single table.** No JOIN, CTE (`WITH`), subquery, set operation, or `LATERAL`.
35
+ - **Only known tables:** `article_metrics`, `channel_metrics`. Other names return `UNKNOWN_TABLE`.
36
+ - **WHERE/HAVING may only reference indexed columns** (`channel_id`/`id` for `article_metrics`; `id` for `channel_metrics`). Filtering by `age`, `publication_date`, `view_count`, `duration`, `scrape_date`, etc. in WHERE returns `NON_INDEXED_FILTER:<col>`. Apply those constraints client-side after fetching.
37
+ - **Leading index column must be equality-or-IN-filtered with literals** (`channel_id = 1` or `channel_id IN (1,2,3)`). Without it: `MISSING_INDEXED_FILTER`.
38
+ - **Trivial-aggregation exception:** a SELECT whose projected expressions are all aggregates and which has no GROUP BY / HAVING may omit WHERE entirely. Use only for tiny sanity checks.
39
+ - **No mandatory LIMIT/OFFSET** — but Firebolt will time out on bad plans, so keep the leading-index filter selective.
40
+
41
+ ## Tables
42
+
43
+ ### `article_metrics` — Video-Level Time-Series (PRIMARY TABLE)
44
+
45
+ **7.4 billion rows** | 159 GiB compressed | Data from 2022-03-04 to present.
46
+
47
+ Tracks YouTube video metrics over time. Each row = one scrape of one video on one date. Videos are scraped repeatedly, so a single video has many rows at different ages.
48
+
49
+ | Column | Type | Description |
50
+ |--------|------|-------------|
51
+ | `id` | TEXT | YouTube video ID (e.g., `'dQw4w9WgXcQ'`). **Bare YouTube ID** — NOT the compound `<channel_id>:<youtube_id>` form used in PG `adlink.article_id` and ES `_id`. |
52
+ | `channel_id` | INT | TL channel ID (matches the channel ID returned by `tl channels list`) |
53
+ | `channel_format` | INT | Platform format (4 = YouTube) |
54
+ | `publication_date` | DATE | When the video was published |
55
+ | `scrape_date` | DATE | When this data point was captured |
56
+ | `age` | INT | Days since publication (`scrape_date - publication_date`) |
57
+ | `view_count` | INT | Total view count at time of scrape |
58
+ | `like_count` | INT | Total likes at time of scrape |
59
+ | `comment_count` | INT | Total comments at time of scrape |
60
+ | `duration` | INT | Video duration in seconds |
61
+
62
+ **Primary Index: `(channel_id, id)`** — queries MUST filter by `channel_id` first.
63
+
64
+ **Shorts vs Longform:** `duration < 61` = Short. `duration >= 61` = Longform. In code: `(duration or 100) < 61`. Filter client-side after fetching — `duration` isn't an indexed column so it can't go in WHERE.
65
+
66
+ ### `channel_metrics` — Channel-Level Time-Series
67
+
68
+ **1.1 billion rows** | 6.9 GiB compressed.
69
+
70
+ | Column | Type | Description |
71
+ |--------|------|-------------|
72
+ | `id` | INT | TL channel ID |
73
+ | `total_views` | INT | Channel total views at time of scrape |
74
+ | `reach` | INT | Subscriber count at time of scrape |
75
+ | `scrape_date` | DATE | When this data point was captured |
76
+
77
+ **Primary Index: `(id)`**
78
+
79
+ ## When to use Firebolt (and when NOT to)
80
+
81
+ Firebolt's **only advantage** is historical metric snapshots. For everything else, prefer ES (current state) or the structured `tl` commands (deal/pipeline data).
82
+
83
+ | Need | Source |
84
+ |------|--------|
85
+ | Current view count on a video | **Elasticsearch** (`tl uploads show` or `tl db es`) |
86
+ | Current subscriber count | **Elasticsearch** (`tl channels show`) |
87
+ | Video metadata (title, tags, duration) | **Elasticsearch** |
88
+ | Find channels by criteria | **`tl channels list`** (Postgres) |
89
+ | Deal / pipeline / sponsorship data | **`tl sponsorships`** etc. (Postgres) |
90
+ | View curve over time (age 7→30→90→180) | **Firebolt** ✅ |
91
+ | Views at age 30 vs age 180 (evergreenness) | **Firebolt** ✅ |
92
+ | Channel subscriber growth trend | **Firebolt** ✅ |
93
+ | Detect view spikes / anomalies over time | **Firebolt** ✅ |
94
+
95
+ **Rule: only query Firebolt when you need a value AT A POINT IN TIME that no longer exists in the current ES/PG snapshot.**
96
+
97
+ ## Index rules — verified timings
98
+
99
+ `article_metrics` has 7.4B rows. The primary index is `(channel_id, id)`. Without `channel_id` in WHERE, the engine does a full table scan and times out — such queries are not accepted up front.
100
+
101
+ | Query Pattern | Performance | Result |
102
+ |--------------|-------------|--------|
103
+ | `WHERE channel_id = X AND id = Y` | ~12s | ✅ Full index match |
104
+ | `WHERE channel_id = X` | ~12s | ✅ Partial index, viable |
105
+ | `WHERE channel_id IN (X, Y, Z)` | ~12–30s | ✅ Multiple lookups, viable |
106
+ | `WHERE id = 'abc'` (no channel_id) | n/a | ❌ rejected (`MISSING_INDEXED_FILTER`) |
107
+ | `WHERE publication_date > X` | n/a | ❌ rejected (`NON_INDEXED_FILTER:publication_date`) |
108
+ | `WHERE age = 30 AND view_count > 50000` | n/a | ❌ rejected (multiple `NON_INDEXED_FILTER`) |
109
+
110
+ For `channel_metrics`, primary index is `(id)`. Same rule: always filter by `id`.
111
+
112
+ ## Workflow: resolve IDs first, then query
113
+
114
+ Every Firebolt workflow has two steps:
115
+
116
+ **Step 1 — get `channel_id` and (optionally) video IDs from PG/ES.**
117
+
118
+ ```bash
119
+ # Channels matching some criterion (PG side)
120
+ tl channels list category:tech --json --limit 50 | jq '.results[].id'
121
+
122
+ # Or videos for a specific brand's deals (Postgres side, via tl sponsorships)
123
+ tl deals list brand:"Nike" --json --limit 500 \
124
+ | jq -r '.results[] | select(.article_id != null) | "\(.channel_id):\(.article_id)"'
125
+
126
+ # Or videos via Elasticsearch content search
127
+ tl db es '{
128
+ "size":100,
129
+ "query":{"term":{"sponsored_brand_mentions":"5612"}},
130
+ "_source":["channel.id","id"]
131
+ }' --json | jq '.results[] | {channel_id: .channel.id, id: (.id | split(":")[1])}'
132
+ ```
133
+
134
+ **Step 2 — query Firebolt with those IDs.**
135
+
136
+ ```bash
137
+ # Best: full index
138
+ tl db fb "SELECT id, age, view_count FROM article_metrics
139
+ WHERE channel_id IN (123, 456, 789)
140
+ AND id IN ('abc', 'def', 'ghi')
141
+ ORDER BY id, age"
142
+
143
+ # Acceptable: channel_id only (scans all videos for that channel)
144
+ tl db fb "SELECT id, age, view_count FROM article_metrics
145
+ WHERE channel_id = 12345
146
+ ORDER BY id, age"
147
+ ```
148
+
149
+ For non-indexed filters (`age IN (30, 180)`, `duration > 60`), pull a slightly wider slice and filter in `jq`/Python.
150
+
151
+ ## Common Query Patterns
152
+
153
+ ### Get a video's full view curve
154
+
155
+ ```bash
156
+ tl db fb "SELECT id, age, view_count, like_count, comment_count, duration
157
+ FROM article_metrics
158
+ WHERE channel_id = 12345 AND id = 'dQw4w9WgXcQ'
159
+ ORDER BY age"
160
+ ```
161
+
162
+ ### Get all videos for a channel, then filter client-side to milestone ages and longform
163
+
164
+ ```bash
165
+ tl db fb "SELECT id, age, view_count, like_count, comment_count, duration, publication_date
166
+ FROM article_metrics
167
+ WHERE channel_id = 12345
168
+ ORDER BY id, age" --json \
169
+ | jq '.results[] | select(.duration > 60 and (.age == 7 or .age == 30 or .age == 60 or .age == 90 or .age == 180 or .age == 365))'
170
+ ```
171
+
172
+ ### Channel subscriber/view growth over time
173
+
174
+ ```bash
175
+ tl db fb "SELECT scrape_date, total_views, reach
176
+ FROM channel_metrics
177
+ WHERE id = 12345
178
+ ORDER BY scrape_date"
179
+ ```
180
+
181
+ ### Compare multiple channels' growth
182
+
183
+ ```bash
184
+ tl db fb "SELECT id, scrape_date, total_views, reach
185
+ FROM channel_metrics
186
+ WHERE id IN (123, 456, 789)
187
+ ORDER BY id, scrape_date"
188
+ ```
189
+
190
+ ## Sparse data warning
191
+
192
+ Snapshots are **sparse**, especially for older videos (the project's scrape cadence backs off as videos age). Channels are snapshotted daily. Do **not** assume two arbitrary dates have data points. For approximations between gaps, prefer `tl snapshots` (which implements project-internal interpolation logic) over hand-rolled raw queries.
193
+
194
+ ## How Firebolt powers other features
195
+
196
+ - **Evergreenness:** `evergreenness = (views_at_180 - views_at_30) / views_at_30`. Falls back to `(views_at_90 × 1.265 - views_at_30) / views_at_30` if 180 isn't available. Threshold `views_180 ≥ views_30 × 2`. Min views: 5,000. Stored on the ES `evergreenness` field and on the channel record returned by `tl channels show` — read those first; only recompute from Firebolt for investigations.
197
+ - **Growth/Trend stats** (initial → middle → current dynamics) are computed server-side.
198
+ - **View-curve approximation:** linear (channel metrics, gap ≤ 10 days) or logarithmic (article metrics, `views = a · log(b · age)`). Server-side; not exposed to raw `tl db fb`.
199
+
200
+ ## Use cases (mostly underexplored)
201
+
202
+ - Late viral detection: dormant videos that spike — what caused it?
203
+ - Evergreen outliers: abnormally high/low evergreen scores — why?
204
+ - Engagement shifts: like/comment ratios changing dramatically over time.
205
+ - View projection: fit logarithmic curve, project future views.
206
+ - Sponsorship timing: when in a video's lifecycle does a sponsorship deliver most views?
207
+ - Anomaly alerts: detect unusual view patterns and notify.
208
+ - Performance benchmarking: a video's curve vs its niche average.