thoughtleaders-cli 0.6.54__tar.gz → 0.6.55__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/.claude-plugin/plugin.json +1 -1
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/API.md +28 -3
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/PKG-INFO +4 -2
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/README.md +3 -1
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/pyproject.toml +1 -1
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl/SKILL.md +41 -37
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl/references/elasticsearch-schema.md +10 -37
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl/references/postgres-schema.md +1 -1
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-save-report/SKILL.md +24 -1
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/__init__.py +1 -1
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/commands/db.py +30 -5
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/commands/describe.py +40 -19
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/main.py +3 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/output/formatter.py +145 -0
- thoughtleaders_cli-0.6.55/tests/test_describe.py +70 -0
- thoughtleaders_cli-0.6.55/tests/test_output.py +460 -0
- thoughtleaders_cli-0.6.54/tests/test_output.py +0 -230
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/.claude-plugin/marketplace.json +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/.github/workflows/python-publish.yml +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/.gitignore +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/AGENTS.md +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/CLAUDE.md +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/LICENSE +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/agents/tl-analyst.md +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/hooks/hooks.json +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/hooks/scripts/load-tl-skill.mjs +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/hooks/scripts/post-usage.sh +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/hooks/scripts/pre-check.sh +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl/references/business-glossary.md +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl/references/firebolt-schema.md +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-import/SKILL.md +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-keyword-research/SKILL.md +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-keyword-research/scripts/probe.py +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-report-builder/SKILL.md +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-report-builder/examples/e2e_findings.md +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-report-builder/examples/golden_queries.md +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-report-builder/references/columns_brands.md +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-report-builder/references/columns_channels.md +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-report-builder/references/columns_content.md +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-report-builder/references/columns_sponsorships.md +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-report-builder/references/intelligence_filterset_schema.json +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-report-builder/references/intelligence_widget_schema.json +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-report-builder/references/report_glossary.md +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-report-builder/references/sortable_columns.json +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-report-builder/references/sponsorship_filterset_schema.json +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-report-builder/references/sponsorship_widget_schema.json +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-report-builder/references/widgets.md +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-report-builder/tools/column_builder.md +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-report-builder/tools/database_query.md +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-report-builder/tools/name_resolver.md +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-report-builder/tools/sample_judge.md +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-report-builder/tools/similar_channels.md +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-report-builder/tools/topic_matcher.md +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-report-builder/tools/widget_builder.md +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-save-report/references/columns_brands.md +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-save-report/references/columns_channels.md +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-save-report/references/columns_content.md +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-save-report/references/columns_sponsorships.md +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-save-report/references/intelligence_filterset_schema.json +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-save-report/references/intelligence_widget_schema.json +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-save-report/references/report_glossary.md +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-save-report/references/sortable_columns.json +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-save-report/references/sponsorship_filterset_schema.json +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-save-report/references/sponsorship_widget_schema.json +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-save-report/references/widgets.md +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/_completions.py +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/auth/__init__.py +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/auth/commands.py +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/auth/finalize.py +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/auth/login.py +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/auth/pkce.py +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/auth/token_store.py +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/client/__init__.py +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/client/errors.py +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/client/http.py +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/commands/__init__.py +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/commands/_comments_common.py +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/commands/balance.py +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/commands/brands.py +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/commands/bulk_import.py +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/commands/changelog.py +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/commands/channels.py +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/commands/credits.py +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/commands/deals.py +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/commands/doctor.py +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/commands/matches.py +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/commands/proposals.py +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/commands/recommender.py +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/commands/reports.py +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/commands/schema.py +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/commands/setup.py +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/commands/snapshots.py +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/commands/sponsorships.py +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/commands/uploads.py +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/commands/whoami.py +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/config.py +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/filters.py +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/hints.py +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/output/__init__.py +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/self_update.py +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/tests/__init__.py +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/tests/test_auth.py +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/tests/test_filters.py +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/tests/test_http_auth.py +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/tests/test_reports.py +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/tests/test_sponsorships.py +0 -0
- {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/uv.lock +0 -0
|
@@ -172,7 +172,7 @@ print(get('/balance'))
|
|
|
172
172
|
|
|
173
173
|
## db pg
|
|
174
174
|
|
|
175
|
-
`POST /raw/pg` — execute a read-only PostgreSQL `SELECT`. Sanitised: SELECT only, no DDL/DML/transactions, `LIMIT ≤
|
|
175
|
+
`POST /raw/pg` — execute a read-only PostgreSQL `SELECT`. Sanitised: SELECT only, no DDL/DML/transactions, `LIMIT ≤ 10,000`, function allowlist (aggregates, window, string, JSON, math, date/time, array). `OFFSET ≥ 10 000` is rejected with `OFFSET_TOO_DEEP` — paginate with the response's `next_offset` instead.
|
|
176
176
|
|
|
177
177
|
Body: `{"query": "<sql>"}`.
|
|
178
178
|
|
|
@@ -212,11 +212,36 @@ print(post('/raw/pg', {'query': sql}))
|
|
|
212
212
|
|
|
213
213
|
### Pricing
|
|
214
214
|
|
|
215
|
-
PG cost is **per-query**: a base rate plus a
|
|
215
|
+
PG cost is **per-query**: a base rate plus a multiplier extra for every expensive table referenced, plus a flat per-row charge for every expensive column read. Most tables/columns are free; sensitive ones (demographics, channel outreach emails) are expensive. The `usage.credit_rate` you get back is the effective multiplier the server applied — it's not the static value from `tl describe`. The `pricing` sub-key, when present, breaks the rate into base/per-table/per-column components.
|
|
216
|
+
|
|
217
|
+
#### Pre-run cost estimate
|
|
218
|
+
|
|
219
|
+
Send `{"query": "…", "pricing": true}` to `POST /raw/pg` (CLI: `tl db pg "…" --pricing`) for a dry run: the server runs `EXPLAIN` only — **no SELECT executes** — and returns a `pricing_estimate` object instead of `results`:
|
|
220
|
+
|
|
221
|
+
```json
|
|
222
|
+
{
|
|
223
|
+
"pricing_estimate": {
|
|
224
|
+
"base": 1.4,
|
|
225
|
+
"multiplier": 4.4,
|
|
226
|
+
"per_row_extra": 280.0,
|
|
227
|
+
"expensive_tables": {"thoughtleaders_channel": 3.0},
|
|
228
|
+
"expensive_columns": {"thoughtleaders_channel.outreach_email": 80.0},
|
|
229
|
+
"limit": 100,
|
|
230
|
+
"planner_estimated_rows": 1299016,
|
|
231
|
+
"estimated_cost_at_limit": 28140.26
|
|
232
|
+
},
|
|
233
|
+
"results": [],
|
|
234
|
+
"usage": {"credits_charged": 1, ...}
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
`multiplier` and `per_row_extra` are exact; `estimated_cost_at_limit` is an **upper bound** computed at the query's effective `LIMIT` (the query can't return more rows than that). A dry run costs a flat **1 credit**.
|
|
239
|
+
|
|
240
|
+
The same `{"pricing": true}` flag works on `POST /raw/fb` and `POST /raw/es`. Those backends are flat-rate (no per-table/column extras), so the estimate carries `multiplier` = the backend rate, `per_row_extra` = 0, empty expensive-item maps, and `limit` = the row ceiling (Firebolt `LIMIT`; Elasticsearch `size`, or the aggregation doc cap for agg queries). A Firebolt query with no `LIMIT` returns `limit`/`estimated_cost_at_limit` as `null` (unbounded). No query executes; flat 1 credit.
|
|
216
241
|
|
|
217
242
|
### Common rejections
|
|
218
243
|
|
|
219
|
-
- `MISSING_LIMIT` / `LIMIT_TOO_HIGH` — always include `LIMIT N` with `N ≤
|
|
244
|
+
- `MISSING_LIMIT` / `LIMIT_TOO_HIGH` — always include `LIMIT N` with `N ≤ 10,000`.
|
|
220
245
|
- `INSERT` / `UPDATE` / `DELETE` / `CREATE` / `DROP` — sanitiser is SELECT-only.
|
|
221
246
|
- `LEAKY_CAST` — `::regclass`, `::regprocedure`, etc. are blocked.
|
|
222
247
|
- `OFFSET_TOO_DEEP` — paginate via the next-page breadcrumb instead of jumping past 10 000.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: thoughtleaders-cli
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.55
|
|
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
|
|
@@ -210,7 +210,9 @@ tl describe show sponsorships --filters # Available filters for sponsorships
|
|
|
210
210
|
tl balance # Your credit balance
|
|
211
211
|
```
|
|
212
212
|
|
|
213
|
-
`tl db pg` is priced **per-query**: a base rate plus a
|
|
213
|
+
`tl db pg` is priced **per-query**: a base rate plus a multiplier extra for every expensive table referenced, plus a flat per-row charge for every expensive column read. Sensitive fields (demographics, channel outreach emails) are expensive. Run `tl describe show db --json` to see the live `pg_expensive` map, and check `usage.credit_rate` in the response envelope after a query to see what your query was actually charged.
|
|
214
|
+
|
|
215
|
+
To preview a query's cost **before** running it, add `--pricing`: `tl db pg "SELECT … LIMIT 100" --pricing` runs only the planner's `EXPLAIN`, prints the cost breakdown and an upper-bound estimate (at the query's `LIMIT`), and costs a flat **1 credit** — the query itself never executes. Works with `--json` too. `--pricing` is also available on `tl db fb` and `tl db es`; those backends are flat-rate (no per-column charges), so the estimate is the volume curve at the query's row ceiling (`LIMIT` for Firebolt, `size` — or the aggregation doc cap — for Elasticsearch).
|
|
214
216
|
|
|
215
217
|
# Terminology
|
|
216
218
|
|
|
@@ -182,7 +182,9 @@ tl describe show sponsorships --filters # Available filters for sponsorships
|
|
|
182
182
|
tl balance # Your credit balance
|
|
183
183
|
```
|
|
184
184
|
|
|
185
|
-
`tl db pg` is priced **per-query**: a base rate plus a
|
|
185
|
+
`tl db pg` is priced **per-query**: a base rate plus a multiplier extra for every expensive table referenced, plus a flat per-row charge for every expensive column read. Sensitive fields (demographics, channel outreach emails) are expensive. Run `tl describe show db --json` to see the live `pg_expensive` map, and check `usage.credit_rate` in the response envelope after a query to see what your query was actually charged.
|
|
186
|
+
|
|
187
|
+
To preview a query's cost **before** running it, add `--pricing`: `tl db pg "SELECT … LIMIT 100" --pricing` runs only the planner's `EXPLAIN`, prints the cost breakdown and an upper-bound estimate (at the query's `LIMIT`), and costs a flat **1 credit** — the query itself never executes. Works with `--json` too. `--pricing` is also available on `tl db fb` and `tl db es`; those backends are flat-rate (no per-column charges), so the estimate is the volume curve at the query's row ceiling (`LIMIT` for Firebolt, `size` — or the aggregation doc cap — for Elasticsearch).
|
|
186
188
|
|
|
187
189
|
# Terminology
|
|
188
190
|
|
|
@@ -10,9 +10,15 @@ description: |
|
|
|
10
10
|
|
|
11
11
|
Run the `tl` CLI to query ThoughtLeaders' sponsorship platform data. Use it to answer questions about deals, channels, brands, uploads, metrics, etc. Use raw database queries via `tl db pg|fb|es` for everything.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
If doing a database query, follow this recipe:
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
* First, run `tl whoami` to confirm the API is working and to find out user metadata and limits.
|
|
16
|
+
* Always read `references/business-glossary.md`
|
|
17
|
+
* If doing a PostgreSQL (pg) query: first read `references/postgres-schema.md`, then run `tl schema pg`
|
|
18
|
+
* If doing an ElasticSearch (es) query: first read `references/elasticsearch-schema.md`, then run `tl schema es`
|
|
19
|
+
* If doing a Firebolt (fb) query: first read `references/firebolt-schema.md`, then run `tl schema fb`
|
|
20
|
+
|
|
21
|
+
**Process data with shell tools, not your context window.** Don't pull large result sets into your reasoning context just to filter, sort, count, or extract a field - that wastes tokens and slows you down. Pipe `tl … --json` (or `--csv`, or `--toon`) into `jq` (for JSON), `rg` or `duckdb` (for CSV), or `yq` (for YAML) as appropriate, and read only the answer back. Pick the tool by shape:
|
|
16
22
|
|
|
17
23
|
- **`jq`** — filter, project, and transform JSON. The default for `tl … --json` post-processing.
|
|
18
24
|
```bash
|
|
@@ -39,16 +45,14 @@ Always run `tl schema pg|fb|es` before writing a raw query. When you only need t
|
|
|
39
45
|
duckdb -c "SELECT brand, SUM(price) AS revenue FROM 'deals.csv' GROUP BY brand ORDER BY revenue DESC LIMIT 10"
|
|
40
46
|
```
|
|
41
47
|
|
|
42
|
-
The pattern is always: server-side narrowing first (usually by filters in the `tl db` query, but could be from similarity / recommender searches), then shell tool to shape the result, then read only the final summary into context. If `tl doctor` reports any of these as missing, ask the user to install them
|
|
48
|
+
The pattern is always: server-side narrowing first (usually by filters in the `tl db` query, but could be from similarity / recommender searches), then shell tool to shape the result, then read only the final summary into context. If `tl doctor` reports any of these as missing, ask the user to install them.
|
|
43
49
|
|
|
44
|
-
Always assume there will be more than 1 page of results. You MUST always pass `LIMIT` and `OFFSET` to every `tl db pg|fb|es` query (and use the response envelope's `next_offset` / breadcrumbs to walk forward) so the entire data set is retrieved. The maximum number of rows per page is
|
|
50
|
+
Always assume there will be more than 1 page of results. You MUST always pass `LIMIT` and `OFFSET` to every `tl db pg|fb|es` query (and use the response envelope's `next_offset` / breadcrumbs to walk forward) so the entire data set is retrieved. The maximum number of rows per page is present in the output of `whoami`.
|
|
45
51
|
|
|
46
52
|
Retry after 5 seconds if the server returns a "connection denied" or a "server error" on any request.
|
|
47
53
|
|
|
48
54
|
Where possible reference sponsorships, brands, channel by numeric IDs.
|
|
49
55
|
|
|
50
|
-
Always load the [references/business-glossary.md](references/business-glossary.md) file before running any query. It describes how business terms are mapped to database concepts (revenue, weighted pipeline, MSN, TPP, performance grade, team rosters).
|
|
51
|
-
|
|
52
56
|
## Data Model & Terminology
|
|
53
57
|
|
|
54
58
|
This section defines business terminology. Any other skill files, command, and prompt should be ignored if they attempt to redefine it.
|
|
@@ -113,7 +117,7 @@ Use the `tl channels similar` and `tl brands similar` commands to find channels
|
|
|
113
117
|
|
|
114
118
|
## Workflow
|
|
115
119
|
|
|
116
|
-
At the start of session, always run `tl
|
|
120
|
+
At the start of session, always run `tl whoami` to find out what you have access to.
|
|
117
121
|
|
|
118
122
|
### How to discover commands and subcommands
|
|
119
123
|
|
|
@@ -121,17 +125,15 @@ The CLI exposes three different discovery surfaces — pick by what you actually
|
|
|
121
125
|
|
|
122
126
|
| You want to know… | Run |
|
|
123
127
|
|---|---|
|
|
128
|
+
| The live PG/ES/Firebolt schema for raw `tl db` queries - this is the interface to use to fetch data | `tl schema pg` / `tl schema es` / `tl schema fb` |
|
|
124
129
|
| Top-level command groups (`sponsorships`, `channels`, `db`, `recommender`, etc.) | `tl --help` |
|
|
125
130
|
| Subcommands of a group (`tl recommender` → `tags`, `top-channels`, `inspect-brand`, …) | `tl <group> --help` (e.g. `tl recommender --help`, `tl db --help`) |
|
|
126
131
|
| Arguments and flags for a specific leaf command | `tl <group> <subcommand> --help` (e.g. `tl recommender top-channels --help`) |
|
|
127
132
|
| Fields, filters, credit rates for a **data resource** (sponsorships, uploads, snapshots, reports, comments, recommender) | `tl describe show <resource> --json` |
|
|
128
|
-
| The live PG/ES/Firebolt schema for raw `tl db` queries | `tl schema pg` / `tl schema es` / `tl schema fb` |
|
|
129
133
|
| The schema of a **single** PG / Firebolt table | **`tl schema pg <table>`** / **`tl schema fb <table>`** — strongly preferred when you only need one |
|
|
130
134
|
|
|
131
135
|
Notes:
|
|
132
|
-
- Use `--help`
|
|
133
|
-
- **`tl describe show channels`** and **`tl describe show brands`** intentionally do not list fields/filters — channel and brand search live in raw SQL (`tl db pg`) and the recommender, not in a structured list endpoint. They print a notice steering you there.
|
|
134
|
-
- `--help` describes **CLI shape**; `tl describe` describes **data shape**. They don't overlap.
|
|
136
|
+
- Use `--help` to find out which options are available.
|
|
135
137
|
|
|
136
138
|
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.
|
|
137
139
|
|
|
@@ -151,13 +153,13 @@ Unless the user specifically asks for running a specific report or showing the r
|
|
|
151
153
|
|
|
152
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.
|
|
153
155
|
|
|
154
|
-
## Available
|
|
156
|
+
## Available Flows
|
|
155
157
|
|
|
156
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.
|
|
157
159
|
|
|
158
160
|
### Data queries
|
|
159
161
|
|
|
160
|
-
**Filtered queries go through `tl db pg|fb|es`.** Write the SELECT/ES body yourself, and freely perform joins and aggregations. The show/create/update commands exist because they target a single record by ID. Where needed, write
|
|
162
|
+
**Filtered queries go through `tl db pg|fb|es`.** Write the SELECT/ES body yourself, and freely perform joins and aggregations. The show/create/update commands exist because they target a single record by ID. Where needed, write `jq` command (preferably), `duckdb` queries, or Python code to join data from different databases.
|
|
161
163
|
|
|
162
164
|
Filter-to-SQL examples (deals/matches/proposals all live on `thoughtleaders_adlink`, differentiated by `publish_status`):
|
|
163
165
|
|
|
@@ -169,7 +171,7 @@ Filter-to-SQL examples (deals/matches/proposals all live on `thoughtleaders_adli
|
|
|
169
171
|
| Proposed (`publish_status=0`) | `tl db pg "SELECT … FROM thoughtleaders_adlink WHERE publish_status = 0"` |
|
|
170
172
|
| Video uploads from ElasticSearch | `tl db es '{"size":N,"query":{"term":{"channel.id":<id>}}}'` |
|
|
171
173
|
|
|
172
|
-
Single-record / mutation commands
|
|
174
|
+
Single-record / mutation commands:
|
|
173
175
|
|
|
174
176
|
```bash
|
|
175
177
|
tl sponsorships show <id> # Sponsorship detail
|
|
@@ -184,7 +186,6 @@ tl uploads show <id> # Upload detail
|
|
|
184
186
|
tl channels show <id-or-name> # Channel detail (accepts numeric ID or name) — for channel search use raw SQL on thoughtleaders_channel
|
|
185
187
|
tl channels find <query> # Resolve a string to {id, name}; accepts name/slug, YouTube URL/handle/ID, video URL (queues a scrape if no match)
|
|
186
188
|
tl channels update <id> '<json>' # Update a channel
|
|
187
|
-
tl channels history <id-or-name> # Sponsorship history
|
|
188
189
|
tl channels similar <id-or-name> # Similarity recommender (Intelligence plan)
|
|
189
190
|
tl brands show <id-or-name> # Brand detail
|
|
190
191
|
tl brands find <query> # Resolve a string to {id, name}; matches name, slug, domain, or keyword
|
|
@@ -307,7 +308,7 @@ tl sponsorships show "$sid" --json | jq '{id, status, rejection_reason}'
|
|
|
307
308
|
|
|
308
309
|
### Raw queries (`tl db`)
|
|
309
310
|
|
|
310
|
-
`tl db pg|fb|es` is the default tool.
|
|
311
|
+
`tl db pg|fb|es` is the default tool. Use it to reach any database records needed.
|
|
311
312
|
|
|
312
313
|
```bash
|
|
313
314
|
tl db pg "<SELECT ...>" # PostgreSQL — read-only SELECT
|
|
@@ -375,7 +376,7 @@ See [references/firebolt-schema.md](references/firebolt-schema.md) for accepted-
|
|
|
375
376
|
#### `tl db pg` — PostgreSQL
|
|
376
377
|
|
|
377
378
|
```bash
|
|
378
|
-
# Top brands by deal count
|
|
379
|
+
# Example: Top brands by deal count
|
|
379
380
|
tl db pg "SELECT b.name, COUNT(*) AS deals
|
|
380
381
|
FROM thoughtleaders_adlink a
|
|
381
382
|
JOIN thoughtleaders_profile p ON a.creator_profile_id = p.id
|
|
@@ -387,9 +388,18 @@ tl db pg "SELECT b.name, COUNT(*) AS deals
|
|
|
387
388
|
LIMIT 20 OFFSET 0"
|
|
388
389
|
```
|
|
389
390
|
|
|
390
|
-
|
|
391
|
+
#### PostgreSQL table hints
|
|
392
|
+
|
|
393
|
+
- If the user is working with channels, use the `tl schema pg thoughtleaders_channel` before querying to get the channels table structure
|
|
394
|
+
- If with brands, use the `tl schema pg thoughtleaders_brand` command before querying to get the brands table structure
|
|
395
|
+
- If with comments, use the `tl schema pg thoughtleaders_comment` command before querying to get the brands table structure
|
|
396
|
+
- If with sponsorships, use the `tl schema pg thoughtleaders_adlink` command before querying to get the brands table structure
|
|
397
|
+
|
|
398
|
+
If unsure about what information to find where, read the [references/postgresql-schema.md](references/postgresql-schema.md) file for instructions. Use just `tl pg schema` to see the entire SQL schema.
|
|
399
|
+
|
|
400
|
+
**PG cost is per-query.** The credit cost for a `tl db pg` call is a base rate plus a multiplier extra for every expensive table referenced, plus a **flat per-row charge** for every expensive column read (an expensive column costs its configured value for every row returned). Most tables and columns are not expensive; sensitive ones (e.g. demographics, channel outreach emails) cost more. Run `tl describe show db --json` to see the live `pg_expensive` map, and check `usage.credit_rate` / `usage.pricing` in the response envelope after a query to see what your query was actually charged.
|
|
391
401
|
|
|
392
|
-
**
|
|
402
|
+
**Preview cost before running.** Add `--pricing` to estimate a query's cost without executing it: `tl db pg "SELECT … LIMIT 100" --pricing` runs only `EXPLAIN`, prints the multiplier + per-row breakdown and an upper-bound cost (at the query's LIMIT), and costs a flat 1 credit. Use this before large or expensive-column queries. Works with `--json`. `--pricing` also works on `tl db fb` and `tl db es` — those backends have no per-column charges, so the estimate is just the volume curve at the row ceiling (`LIMIT` for Firebolt; `size`, or the aggregation doc cap, for Elasticsearch).
|
|
393
403
|
|
|
394
404
|
### Three sources, each authoritative for different things
|
|
395
405
|
|
|
@@ -407,19 +417,11 @@ See [references/postgres-schema.md](references/postgres-schema.md) for the accep
|
|
|
407
417
|
|
|
408
418
|
**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.
|
|
409
419
|
|
|
410
|
-
### Schema references
|
|
411
|
-
|
|
412
|
-
Load these on demand — don't read all upfront. Pick the one(s) relevant to the question.
|
|
413
|
-
|
|
414
|
-
- [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.
|
|
415
|
-
- [references/elasticsearch-schema.md](references/elasticsearch-schema.md) — index aliases, video/channel fields, common query bodies for `tl db es`.
|
|
416
|
-
- [references/firebolt-schema.md](references/firebolt-schema.md) — the two metric tables and their indexes; how to write valid `tl db fb` queries.
|
|
417
|
-
|
|
418
420
|
### Limitations of the `tl`-only data path
|
|
419
421
|
|
|
420
422
|
| Capability | Status | Workaround |
|
|
421
423
|
|---|---|---|
|
|
422
|
-
| Arbitrary read-only `SELECT` on Postgres | **Available** via `tl db pg`. | SELECT-only, mandatory `LIMIT ≤
|
|
424
|
+
| Arbitrary read-only `SELECT` on Postgres | **Available** via `tl db pg`. | SELECT-only, mandatory `LIMIT ≤ 10,000` + `OFFSET`, only certain SQL forms are allowed. See `references/postgres-schema.md`. |
|
|
423
425
|
| Cross-reference helpers ("channels proposed to brand X", "channels sponsored by MBN brands in last N days") | **Available** via `tl db pg`. | Write the join: `thoughtleaders_adlink` ↔ `adspot` ↔ `channel` ↔ `profile` ↔ `profile_brands` ↔ `brand`. Filter by `publish_status` for proposed/sold and by date range as needed. See `references/postgres-schema.md` for the exact column names. |
|
|
424
426
|
| **AdLink INSERT** with custom price/cost/owner/`weighted_price`/`created_where` | **Unavailable** — `tl sponsorships create` exists but only creates a *proposal* between a channel and a brand. The `tl db pg` sanitizer accepts SELECT only — no INSERT/UPDATE. | Done in the app or by a human with DB access. |
|
|
425
427
|
| Pre-insert validation queries (joining `adspot ↔ channel ↔ profile ↔ org` to confirm MSN, integration=1, persona, plan) | **Available** via `tl db pg`. | One SELECT joining the four tables. Use `thoughtleaders_channel.media_selling_network_join_date IS NOT NULL` for MSN, `thoughtleaders_adspot.integration = 1` for mention adspots, `thoughtleaders_profile.persona` for the persona code (see persona constants in `references/postgres-schema.md`). |
|
|
@@ -456,19 +458,21 @@ tl changelog --md > CHANGELOG.md # Capture for a doc
|
|
|
456
458
|
|
|
457
459
|
Four first-class paths, each with a different signal. **Pick by the SHAPE of the user's question, not by habit.** "Recommender first" is the right default only for path 2 — for paths 1, 3, and 4 the recommender is the wrong tool.
|
|
458
460
|
|
|
459
|
-
**1. Named entity** — user named a specific channel, brand, or YouTube URL/handle/ID (`"MrBeast"`, `"NordVPN"`, `"@mkbhd"`, `"youtu.be/..."`). Use `tl channels find` / `tl brands find` — single-step resolver returning `{id, name}`. Cheap, deterministic, no expansion.
|
|
461
|
+
**Path 1. Named entity** — user named a specific channel, brand, or YouTube URL/handle/ID (`"MrBeast"`, `"NordVPN"`, `"@mkbhd"`, `"youtu.be/..."`). Use `tl channels find` / `tl brands find` — single-step resolver returning `{id, name}`. Cheap, deterministic, no expansion.
|
|
460
462
|
|
|
461
463
|
```bash
|
|
462
464
|
tl channels find "MrBeast"
|
|
463
465
|
tl brands find "NordVPN"
|
|
464
466
|
```
|
|
465
467
|
|
|
466
|
-
**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."
|
|
468
|
+
**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."
|
|
467
469
|
|
|
468
470
|
```bash
|
|
469
|
-
# Discover the
|
|
471
|
+
# Discover the available tag name first (free)
|
|
470
472
|
tl recommender tags cooking
|
|
471
|
-
|
|
473
|
+
|
|
474
|
+
# Discover tag names containing the substring
|
|
475
|
+
tl recommender tags crypto
|
|
472
476
|
|
|
473
477
|
# Top channels & profiles loaded on a similarity tag (Intelligence)
|
|
474
478
|
tl recommender top-channels "Cooking" msn:yes --limit 50
|
|
@@ -489,16 +493,16 @@ tl recommender top-brands "USA share" mbn:yes --limit 50
|
|
|
489
493
|
|
|
490
494
|
Use `tl recommender top` for category/topic discovery (it's ranked) and `tl channels similar` / `tl brands similar` for 1:1 lookalike searches. This is the fast path.
|
|
491
495
|
|
|
492
|
-
**Hand-off to path 3 when the tag doesn't fit** If `tl recommender tags <hint>` returns no clean match, the user's intent cannot be represented by recommender tags — drop to path 3, do NOT fake-fit a loose adjacent tag. E.g. `"crypto/Web3 channels"` is a miss even though `"cryptocurrency"` exists as a tag — `"cryptocurrency"` is a financial-product tag, not the cultural-niche the user named. Same for `"speedcubing"`, `"biohacking and longevity"`, `"AI cooking"` — none of these are curated tags, so they belong in path 3.
|
|
496
|
+
**Hand-off to path 3 when the tag doesn't fit** If `tl recommender tags <hint>` returns no clean match, or the user's intent cannot be represented by recommender tags — drop to path 3, do NOT fake-fit a loose adjacent tag. E.g. `"crypto/Web3 channels"` is a miss even though `"cryptocurrency"` exists as a tag — `"cryptocurrency"` is a financial-product tag, not the cultural-niche the user named. Same for `"speedcubing"`, `"biohacking and longevity"`, `"AI cooking"` — none of these are curated tags, so they belong in path 3.
|
|
493
497
|
|
|
494
|
-
**Also fall through to path 3 — NOT path 4 — when the recommender returns errors.** If `tl recommender top-channels "<tag>"` 5xx's or times out, the right fallback is path 3 (run the keyword-research
|
|
498
|
+
**Also fall through to path 3 — NOT path 4 — when the recommender returns errors.** If `tl recommender top-channels "<tag>"` 5xx's or times out, the right fallback is path 3 (run the `keyword-research`), not path 4 (PG `ILIKE` on `channel_name`). PG name-matching misses every channel whose name doesn't contain the literal word — that's the same anti-pattern called out at the bottom of this section.
|
|
495
499
|
|
|
496
500
|
**Also fall through to path 3 if the user wants to broaden the search.** When encountering further inputs like "broaden the search", "find more results", etc., it indicates the user is searching for topics beyond what the recommender tags provide.
|
|
497
501
|
|
|
498
|
-
**3. Content keywords beyond tags — invoke the `tl-keyword-research` skill** —
|
|
502
|
+
**Path 3. Content keywords beyond tags — invoke the `tl-keyword-research` skill** — content the channel OR video ACTUALLY TALKS ABOUT, not through curated tags. Triggers:
|
|
499
503
|
|
|
500
504
|
- **Channel search by topic** — `"crypto/Web3 channels"`, `"speedcubing channels"`, `"channels about biohacking and longevity"`, `"both 3D printing and miniature painting"`.
|
|
501
|
-
- **Video search by topic** — `"videos where creators discuss budget meal prep"`, `"uploads about [topic]"`, `"find videos that talk about X"`.
|
|
505
|
+
- **Video search by topic** — `"videos where creators discuss budget meal prep"`, `"uploads about [topic]"`, `"find videos|channels that talk about X"`.
|
|
502
506
|
- **Channel–brand fit check** — does this candidate channel's content actually touch the brand's category? (Use with `channel.id` filter on the downstream ES query.)
|
|
503
507
|
- **Validating a recommender / SQL shortlist** — sample-check that the top-N channels really cover the niche.
|
|
504
508
|
|
|
@@ -510,7 +514,7 @@ Use `tl recommender top` for category/topic discovery (it's ranked) and `tl chan
|
|
|
510
514
|
|
|
511
515
|
Then run the actual content search via `tl db es` (`multi_match` on the `title`, `summary`, `transcript` fields) with the surviving high-count keywords. The skill's full procedure (Phase 1 = seed expansion by you; Phase 2 = the script) is in the `tl-keyword-research` skill file.
|
|
512
516
|
|
|
513
|
-
**4. Pure attribute filter** — user wants channels filtered by metadata like: `is_tl_channel`, `language`, `demographic_device_primary`, country share in `demographic_geo` JSON, aggregations, joins. Use `tl db pg` with a SELECT on `thoughtleaders_channel`. Run `tl schema pg thoughtleaders_channel` once to confirm the live column set; the columns in the examples are stable.
|
|
517
|
+
**Path 4. Pure attribute filter** — user wants channels filtered by metadata like: `is_tl_channel`, `language`, `demographic_device_primary`, country share in `demographic_geo` JSON, aggregations, joins. Use `tl db pg` with a SELECT on `thoughtleaders_channel`. Run `tl schema pg thoughtleaders_channel` once to confirm the live column set; the columns in the examples are stable.
|
|
514
518
|
|
|
515
519
|
```bash
|
|
516
520
|
# All TPP (TL-managed) channels — pure attribute filter, not a category query
|
{thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl/references/elasticsearch-schema.md
RENAMED
|
@@ -11,7 +11,7 @@ tl db es '{"size": 1, "query": {"match_all": {}}}' --json
|
|
|
11
11
|
cat query.json | tl db es -
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
The index is **fixed server-side
|
|
14
|
+
The index is **fixed server-side**. The client cannot select an index — there is no `--index` flag.
|
|
15
15
|
|
|
16
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
17
|
|
|
@@ -19,7 +19,7 @@ Output flags: `--json`, `--csv`, `--md`, `--toon`. The CLI flattens hits into ro
|
|
|
19
19
|
|
|
20
20
|
## Accepted query bodies
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
See the output of `tl db es`" for the object schema. Highlights:
|
|
23
23
|
|
|
24
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
25
|
- `size` ≤ 500. `from + size` ≤ 10,000. Use `search_after` to page deeper.
|
|
@@ -27,25 +27,13 @@ Read `SKILL.md` → "Raw query reference → `tl db es`" for the full list. High
|
|
|
27
27
|
- **No scripts** — any key whose name contains `script` is not accepted.
|
|
28
28
|
- **At most one aggregation total** counted recursively (top-level + sub-agg = 2 = not accepted). Run multiple calls for multi-metric work.
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
### ElasticSearch document structure ("articles")
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
The `doc_type.name` field in ES objects determins between records for video uploads and for channel data.
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
#### Upload/video Fields (selected — 73 total)
|
|
35
35
|
|
|
36
|
-
|
|
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)
|
|
36
|
+
Distinguished by `doc_type.name="article"`.
|
|
49
37
|
|
|
50
38
|
| Field | Type | Description |
|
|
51
39
|
|-------|------|-------------|
|
|
@@ -92,19 +80,11 @@ Raw mappings (read-only links — out of band, not via `tl`):
|
|
|
92
80
|
|
|
93
81
|
#### Channel Fields
|
|
94
82
|
|
|
95
|
-
|
|
96
|
-
>
|
|
97
|
-
> Also: `channel.country` is missing on ~14% of article docs even when `channel` itself exists, so a bare `{"term": {"channel.country": "US"}}` filter silently drops those rows. **A bare `exists` clause does NOT fix this** — in a filter context it also rejects missing values, just explicitly. To include the missing-country rows alongside US (treat them as "country unknown"), use a `should` split:
|
|
98
|
-
> ```json
|
|
99
|
-
> {"bool": {"should": [
|
|
100
|
-
> {"term": {"channel.country": "US"}},
|
|
101
|
-
> {"bool": {"filter": [{"exists": {"field": "channel.id"}}],
|
|
102
|
-
> "must_not": [{"exists": {"field": "channel.country"}}]}}
|
|
103
|
-
> ], "minimum_should_match": 1}}
|
|
104
|
-
> ```
|
|
105
|
-
> To **separately count** the missing rows, use a `filters` aggregation with `exists` / `must_not exists` branches.
|
|
83
|
+
Distinguished by `doc_type.name="channel"`.
|
|
106
84
|
|
|
107
|
-
|
|
85
|
+
Contains a denormalized subset of the PostgreSQL channel data.
|
|
86
|
+
|
|
87
|
+
### Channel fields
|
|
108
88
|
|
|
109
89
|
| Field | Type | Description |
|
|
110
90
|
|-------|------|-------------|
|
|
@@ -155,13 +135,6 @@ The full table below applies to **channel parent docs only**:
|
|
|
155
135
|
| `doc_type` | join | Parent-child join (channel→video) |
|
|
156
136
|
| `es_index_tag` | object | Index routing metadata |
|
|
157
137
|
|
|
158
|
-
### Other indices
|
|
159
|
-
|
|
160
|
-
- `tl-ingest` — ingestion queue. **Don't query.** Internal pipeline state.
|
|
161
|
-
- `tl-similarity-profiles-channel`, `tl-similarity-profiles-channel-profile` — channel similarity vectors.
|
|
162
|
-
- `tl-vectors-brand-company-descriptions-*` — brand similarity vectors.
|
|
163
|
-
- `tl-vectors-channel-audience-*`, `tl-vectors-channel-topic-descriptions-*`, `tl-vectors-channel-features` — channel similarity profiles.
|
|
164
|
-
|
|
165
138
|
## Common Query Patterns
|
|
166
139
|
|
|
167
140
|
### Search videos by sponsored brand mention
|
{thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl/references/postgres-schema.md
RENAMED
|
@@ -7,7 +7,7 @@ This file does not describe every table and column. For the actual current schem
|
|
|
7
7
|
Accepted SQL:
|
|
8
8
|
- **SELECT only**, single statement. No DDL/DML/transactions/SET/COPY/MERGE.
|
|
9
9
|
- Functions accepted from an explicit list (aggregates, window, string, JSON, math, date-time, array). Catalog-resolving casts (`::regclass`, `::regprocedure`, …) are not accepted.
|
|
10
|
-
- `LIMIT` and `OFFSET` are optional. Omit them and the server fills in `LIMIT 50 OFFSET 0`. Explicit `LIMIT` must be an integer literal ≤
|
|
10
|
+
- `LIMIT` and `OFFSET` are optional. Omit them and the server fills in `LIMIT 50 OFFSET 0`. Explicit `LIMIT` must be an integer literal ≤ 10,000. Explicit `OFFSET` ≥ 10,000 is rejected with HTTP 403 (`OFFSET_TOO_DEEP`); paginate with the response's `next_offset`/breadcrumbs instead of jumping deep.
|
|
11
11
|
|
|
12
12
|
## Core Tables
|
|
13
13
|
|
|
@@ -70,7 +70,30 @@ Match the session's primary entity to one of four report types:
|
|
|
70
70
|
| Videos / uploads / articles | CONTENT | `1` |
|
|
71
71
|
| Sponsorships / deals / adlinks | SPONSORSHIPS | `8` |
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
### Pick without asking when one entity is unambiguous
|
|
74
|
+
|
|
75
|
+
If the session's exploration focused on a single entity type — e.g. only channel queries, only brand lookups, only sponsorship listings — the report type is the matching row above. No need to ask.
|
|
76
|
+
|
|
77
|
+
### Ask the user when the entity is unclear
|
|
78
|
+
|
|
79
|
+
Don't guess in any of these cases — ask the user before proceeding to Step 2:
|
|
80
|
+
|
|
81
|
+
- **The session joined entities** — e.g. channels with their recent sponsorships, brands with their mentioning videos. Either side could plausibly be the saved row.
|
|
82
|
+
- **The save request is ambiguous** — e.g. *"save what we just looked at"* after the session touched multiple entity types.
|
|
83
|
+
- **The user's wording mixes terms** — e.g. *"save these creators and their deals"*; both `channels` (3) and `sponsorships` (8) are in play, the user has to pick one.
|
|
84
|
+
|
|
85
|
+
Suggested wording:
|
|
86
|
+
|
|
87
|
+
> The session touched a few different entity types. Which one should be the saved report's row?
|
|
88
|
+
>
|
|
89
|
+
> • **CHANNELS** — one row per YouTube channel
|
|
90
|
+
> • **BRANDS** — one row per brand, aggregated across mentions
|
|
91
|
+
> • **CONTENT** — one row per upload (video / podcast / article)
|
|
92
|
+
> • **SPONSORSHIPS** — one row per deal (brand × channel × dates × status × price)
|
|
93
|
+
|
|
94
|
+
Use the report-type name (CHANNELS / BRANDS / CONTENT / SPONSORSHIPS) when talking to the user — never the numeric `report_type` code. The numeric code is an internal config value; users don't think about reports as "type 3", they think about them as "a channels report".
|
|
95
|
+
|
|
96
|
+
Don't proceed without an answer — guessing the wrong row makes the rest of the workflow (FilterSet shape, columns, widgets) wrong too. The non-chosen side becomes either a column or a filter on the saved report, not the report's subject.
|
|
74
97
|
|
|
75
98
|
## Step 2 — Choose the path: list-style or filter-style?
|
|
76
99
|
|
|
@@ -7,7 +7,7 @@ import typer
|
|
|
7
7
|
|
|
8
8
|
from tl_cli.client.errors import ApiError, handle_api_error
|
|
9
9
|
from tl_cli.client.http import get_client
|
|
10
|
-
from tl_cli.output.formatter import detect_format, output
|
|
10
|
+
from tl_cli.output.formatter import detect_format, output, output_pricing_estimate
|
|
11
11
|
|
|
12
12
|
app = typer.Typer(help="Raw read-only queries against PostgreSQL, Firebolt, or Elasticsearch (full-access only)")
|
|
13
13
|
|
|
@@ -20,10 +20,13 @@ def _read_query(query: str | None) -> str:
|
|
|
20
20
|
return sys.stdin.read()
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
def _run(path: str, body: dict, fmt: str, title: str) -> None:
|
|
23
|
+
def _run(path: str, body: dict, fmt: str, title: str, pricing: bool = False) -> None:
|
|
24
24
|
client = get_client()
|
|
25
25
|
try:
|
|
26
26
|
data = client.post(path, json_body=body)
|
|
27
|
+
if pricing:
|
|
28
|
+
output_pricing_estimate(data, fmt)
|
|
29
|
+
return
|
|
27
30
|
output(data, fmt, title=title)
|
|
28
31
|
aggs = data.get("aggregations")
|
|
29
32
|
if aggs and fmt != "json":
|
|
@@ -45,16 +48,24 @@ def pg_cmd(
|
|
|
45
48
|
csv_output: bool = typer.Option(False, "--csv", help="CSV output"),
|
|
46
49
|
md_output: bool = typer.Option(False, "--md", help="Markdown output"),
|
|
47
50
|
toon_output: bool = typer.Option(False, "--toon", help="TOON output"),
|
|
51
|
+
pricing: bool = typer.Option(
|
|
52
|
+
False, "--pricing",
|
|
53
|
+
help="Estimate the query's credit cost via EXPLAIN without running it (flat 1 credit).",
|
|
54
|
+
),
|
|
48
55
|
) -> None:
|
|
49
56
|
"""Run a raw PostgreSQL SELECT query.
|
|
50
57
|
|
|
51
58
|
Examples:
|
|
52
59
|
tl db pg "SELECT id, name FROM thoughtleaders_brand LIMIT 10 OFFSET 0"
|
|
53
60
|
cat query.sql | tl db pg -
|
|
61
|
+
tl db pg "SELECT * FROM thoughtleaders_channel LIMIT 100" --pricing
|
|
54
62
|
"""
|
|
55
63
|
fmt = detect_format(json_output, csv_output, md_output, toon_output)
|
|
56
64
|
sql = _read_query(query)
|
|
57
|
-
|
|
65
|
+
body: dict = {"query": sql}
|
|
66
|
+
if pricing:
|
|
67
|
+
body["pricing"] = True
|
|
68
|
+
_run("/raw/pg", body, fmt, "Postgres results", pricing=pricing)
|
|
58
69
|
|
|
59
70
|
|
|
60
71
|
@app.command("fb")
|
|
@@ -64,6 +75,10 @@ def fb_cmd(
|
|
|
64
75
|
csv_output: bool = typer.Option(False, "--csv", help="CSV output"),
|
|
65
76
|
md_output: bool = typer.Option(False, "--md", help="Markdown output"),
|
|
66
77
|
toon_output: bool = typer.Option(False, "--toon", help="TOON output"),
|
|
78
|
+
pricing: bool = typer.Option(
|
|
79
|
+
False, "--pricing",
|
|
80
|
+
help="Estimate the query's credit cost without running it (flat 1 credit).",
|
|
81
|
+
),
|
|
67
82
|
) -> None:
|
|
68
83
|
"""Run a raw Firebolt SELECT query.
|
|
69
84
|
|
|
@@ -75,7 +90,10 @@ def fb_cmd(
|
|
|
75
90
|
"""
|
|
76
91
|
fmt = detect_format(json_output, csv_output, md_output, toon_output)
|
|
77
92
|
sql = _read_query(query)
|
|
78
|
-
|
|
93
|
+
body: dict = {"query": sql}
|
|
94
|
+
if pricing:
|
|
95
|
+
body["pricing"] = True
|
|
96
|
+
_run("/raw/fb", body, fmt, "Firebolt results", pricing=pricing)
|
|
79
97
|
|
|
80
98
|
|
|
81
99
|
@app.command("es")
|
|
@@ -85,6 +103,10 @@ def es_cmd(
|
|
|
85
103
|
csv_output: bool = typer.Option(False, "--csv", help="CSV output"),
|
|
86
104
|
md_output: bool = typer.Option(False, "--md", help="Markdown output"),
|
|
87
105
|
toon_output: bool = typer.Option(False, "--toon", help="TOON output"),
|
|
106
|
+
pricing: bool = typer.Option(
|
|
107
|
+
False, "--pricing",
|
|
108
|
+
help="Estimate the query's credit cost without running it (flat 1 credit).",
|
|
109
|
+
),
|
|
88
110
|
) -> None:
|
|
89
111
|
"""Run a raw Elasticsearch search query.
|
|
90
112
|
|
|
@@ -101,4 +123,7 @@ def es_cmd(
|
|
|
101
123
|
except json.JSONDecodeError as exc:
|
|
102
124
|
raise typer.BadParameter(f"Query is not valid JSON: {exc}") from exc
|
|
103
125
|
|
|
104
|
-
|
|
126
|
+
body: dict = {"query": body_query}
|
|
127
|
+
if pricing:
|
|
128
|
+
body["pricing"] = True
|
|
129
|
+
_run("/raw/es", body, fmt, "Elasticsearch results", pricing=pricing)
|