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.
Files changed (107) hide show
  1. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/.claude-plugin/plugin.json +1 -1
  2. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/API.md +28 -3
  3. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/PKG-INFO +4 -2
  4. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/README.md +3 -1
  5. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/pyproject.toml +1 -1
  6. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl/SKILL.md +41 -37
  7. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl/references/elasticsearch-schema.md +10 -37
  8. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl/references/postgres-schema.md +1 -1
  9. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-save-report/SKILL.md +24 -1
  10. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/__init__.py +1 -1
  11. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/commands/db.py +30 -5
  12. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/commands/describe.py +40 -19
  13. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/main.py +3 -0
  14. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/output/formatter.py +145 -0
  15. thoughtleaders_cli-0.6.55/tests/test_describe.py +70 -0
  16. thoughtleaders_cli-0.6.55/tests/test_output.py +460 -0
  17. thoughtleaders_cli-0.6.54/tests/test_output.py +0 -230
  18. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/.claude-plugin/marketplace.json +0 -0
  19. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/.github/workflows/python-publish.yml +0 -0
  20. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/.gitignore +0 -0
  21. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/AGENTS.md +0 -0
  22. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/CLAUDE.md +0 -0
  23. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/LICENSE +0 -0
  24. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/agents/tl-analyst.md +0 -0
  25. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/hooks/hooks.json +0 -0
  26. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/hooks/scripts/load-tl-skill.mjs +0 -0
  27. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/hooks/scripts/post-usage.sh +0 -0
  28. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/hooks/scripts/pre-check.sh +0 -0
  29. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl/references/business-glossary.md +0 -0
  30. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl/references/firebolt-schema.md +0 -0
  31. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-import/SKILL.md +0 -0
  32. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-keyword-research/SKILL.md +0 -0
  33. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-keyword-research/scripts/probe.py +0 -0
  34. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-report-builder/SKILL.md +0 -0
  35. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-report-builder/examples/e2e_findings.md +0 -0
  36. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-report-builder/examples/golden_queries.md +0 -0
  37. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-report-builder/references/columns_brands.md +0 -0
  38. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-report-builder/references/columns_channels.md +0 -0
  39. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-report-builder/references/columns_content.md +0 -0
  40. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-report-builder/references/columns_sponsorships.md +0 -0
  41. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-report-builder/references/intelligence_filterset_schema.json +0 -0
  42. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-report-builder/references/intelligence_widget_schema.json +0 -0
  43. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-report-builder/references/report_glossary.md +0 -0
  44. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-report-builder/references/sortable_columns.json +0 -0
  45. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-report-builder/references/sponsorship_filterset_schema.json +0 -0
  46. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-report-builder/references/sponsorship_widget_schema.json +0 -0
  47. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-report-builder/references/widgets.md +0 -0
  48. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-report-builder/tools/column_builder.md +0 -0
  49. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-report-builder/tools/database_query.md +0 -0
  50. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-report-builder/tools/name_resolver.md +0 -0
  51. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-report-builder/tools/sample_judge.md +0 -0
  52. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-report-builder/tools/similar_channels.md +0 -0
  53. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-report-builder/tools/topic_matcher.md +0 -0
  54. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-report-builder/tools/widget_builder.md +0 -0
  55. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-save-report/references/columns_brands.md +0 -0
  56. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-save-report/references/columns_channels.md +0 -0
  57. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-save-report/references/columns_content.md +0 -0
  58. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-save-report/references/columns_sponsorships.md +0 -0
  59. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-save-report/references/intelligence_filterset_schema.json +0 -0
  60. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-save-report/references/intelligence_widget_schema.json +0 -0
  61. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-save-report/references/report_glossary.md +0 -0
  62. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-save-report/references/sortable_columns.json +0 -0
  63. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-save-report/references/sponsorship_filterset_schema.json +0 -0
  64. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-save-report/references/sponsorship_widget_schema.json +0 -0
  65. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/skills/tl-save-report/references/widgets.md +0 -0
  66. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/_completions.py +0 -0
  67. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/auth/__init__.py +0 -0
  68. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/auth/commands.py +0 -0
  69. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/auth/finalize.py +0 -0
  70. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/auth/login.py +0 -0
  71. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/auth/pkce.py +0 -0
  72. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/auth/token_store.py +0 -0
  73. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/client/__init__.py +0 -0
  74. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/client/errors.py +0 -0
  75. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/client/http.py +0 -0
  76. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/commands/__init__.py +0 -0
  77. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/commands/_comments_common.py +0 -0
  78. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/commands/balance.py +0 -0
  79. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/commands/brands.py +0 -0
  80. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/commands/bulk_import.py +0 -0
  81. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/commands/changelog.py +0 -0
  82. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/commands/channels.py +0 -0
  83. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/commands/credits.py +0 -0
  84. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/commands/deals.py +0 -0
  85. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/commands/doctor.py +0 -0
  86. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/commands/matches.py +0 -0
  87. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/commands/proposals.py +0 -0
  88. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/commands/recommender.py +0 -0
  89. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/commands/reports.py +0 -0
  90. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/commands/schema.py +0 -0
  91. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/commands/setup.py +0 -0
  92. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/commands/snapshots.py +0 -0
  93. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/commands/sponsorships.py +0 -0
  94. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/commands/uploads.py +0 -0
  95. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/commands/whoami.py +0 -0
  96. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/config.py +0 -0
  97. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/filters.py +0 -0
  98. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/hints.py +0 -0
  99. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/output/__init__.py +0 -0
  100. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/src/tl_cli/self_update.py +0 -0
  101. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/tests/__init__.py +0 -0
  102. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/tests/test_auth.py +0 -0
  103. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/tests/test_filters.py +0 -0
  104. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/tests/test_http_auth.py +0 -0
  105. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/tests/test_reports.py +0 -0
  106. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/tests/test_sponsorships.py +0 -0
  107. {thoughtleaders_cli-0.6.54 → thoughtleaders_cli-0.6.55}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tl-cli",
3
- "version": "0.6.54",
3
+ "version": "0.6.55",
4
4
  "description": "ThoughtLeaders CLI — query sponsorship deals, channels, brands, uploads, and intelligence from the terminal",
5
5
  "author": {
6
6
  "name": "ThoughtLeaders",
@@ -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 ≤ 500`, 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.
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 surcharge for every priced table and column referenced. Most tables/columns are free; sensitive ones (demographics, channel outreach emails) cost more. 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.
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 ≤ 500`.
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.54
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 surcharge for every priced table and column referenced. Sensitive fields (demographics, channel outreach emails) cost more. Run `tl describe show db --json` to see the live surcharge map, and check `usage.credit_rate` in the response envelope after a query to see what your query was actually charged.
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 surcharge for every priced table and column referenced. Sensitive fields (demographics, channel outreach emails) cost more. Run `tl describe show db --json` to see the live surcharge map, and check `usage.credit_rate` in the response envelope after a query to see what your query was actually charged.
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
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "thoughtleaders-cli"
7
- version = "0.6.54"
7
+ version = "0.6.55"
8
8
  description = "ThoughtLeaders CLI — query sponsorship data, channels, brands, and intelligence"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -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
- Always run `tl schema pg|fb|es` before writing a raw query. When you only need the schema of one table, you MUST call `tl schema pg <table>` (or `tl schema fb <table>`). Avoid calling the unscoped form, to reduce token counts. ES has no per-table form (the index is a single document shape), so `tl schema es` is the only call there.
13
+ If doing a database query, follow this recipe:
14
14
 
15
- **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`, `yq`, `rg`, or `duckdb`, as appropriate, and read only the answer back. Pick the tool by shape:
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 — `tl-internal setup` installs all four by default.
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 500.
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 --help` to find out which command groups are available, and `tl whoami` to find out what you have access to.
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` everywhere there is no separate `tl help` command. `tl help` returns "No such command '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 Commands
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 Python scripts or duckdb queries to join data from different databases.
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 remain:
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. Reach for it whenever the question is anything beyond a trivially simple lookup — and use the structured commands only for those trivial cases (single-record `show`, plain filtered `list`). Don't paginate-and-reduce in your head when one SQL or ES body would do it server-side.
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
- 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.
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
- **PG cost is per-query.** The credit rate for a `tl db pg` call equals a base rate plus a surcharge for every priced table referenced and every priced column referenced (additive on both sides). Most tables and columns carry no surcharge; sensitive ones (e.g. demographics, channel outreach emails) cost more. Run `tl describe show db --json` to see the live surcharge map, and check `usage.credit_rate` in the response envelope after a query to see what your query was actually charged.
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 ≤ 500` + `OFFSET`, only certain SQL forms are allowed. See `references/postgres-schema.md`. |
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 right tag name first (free)
471
+ # Discover the available tag name first (free)
470
472
  tl recommender tags cooking
471
- tl recommender tags "usa"
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 skill against ES), 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.
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** — user described content the channel OR video ACTUALLY TALKS ABOUT, and it isn't a curated tag. Triggers:
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
@@ -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** (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.
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
- Read `SKILL.md` "Raw query reference → `tl db es`" for the full list. Highlights:
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
- ## Index Structure
30
+ ### ElasticSearch document structure ("articles")
31
31
 
32
- ### `tl-platform-{year}-{quarter}` Main Content Index
32
+ The `doc_type.name` field in ES objects determins between records for video uploads and for channel data.
33
33
 
34
- The primary index. Contains videos AND channels as parent-child documents (`doc_type` join field).
34
+ #### Upload/video Fields (selected 73 total)
35
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)
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
- > ⚠️ **The embedded `channel.*` object on video (article) docs is a denormalized SUBSET — NOT the full channel schema.** The full field list below exists only on **channel parent docs** (`doc_type: "channel"`). On article docs, `channel.*` contains at most **6 fields**: `id`, `country`, `language`, `content_category`, `format`, `publication_id`. **`reach`, `subscribers`, `impression`, `channel_name`, `sponsorship_score`, `is_tl_channel`, etc. are NOT on the embedded object.** Filtering article docs by `channel.reach` returns zero results silently — query the parent channel doc, or join PG `thoughtleaders_channel` for those fields.
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
- The full table below applies to **channel parent docs only**:
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
@@ -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 ≤ 500. Explicit `OFFSET` ≥ 10,000 is rejected with HTTP 403 (`OFFSET_TOO_DEEP`); paginate with the response's `next_offset`/breadcrumbs instead of jumping deep.
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
- If the session joined entities (e.g. channels with their recent sponsorships), pick the **one the user actually wants to save** and ask if unclear. The other side becomes either a column or a filter, not the report subject.
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
 
@@ -1,3 +1,3 @@
1
1
  """ThoughtLeaders CLI — query sponsorship data, channels, brands, and intelligence."""
2
2
 
3
- __version__ = "0.6.54"
3
+ __version__ = "0.6.55"
@@ -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
- _run("/raw/pg", {"query": sql}, fmt, "Postgres results")
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
- _run("/raw/fb", {"query": sql}, fmt, "Firebolt results")
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
- _run("/raw/es", {"query": body_query}, fmt, "Elasticsearch results")
126
+ body: dict = {"query": body_query}
127
+ if pricing:
128
+ body["pricing"] = True
129
+ _run("/raw/es", body, fmt, "Elasticsearch results", pricing=pricing)