thoughtleaders-cli 0.6.51__tar.gz → 0.6.53__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.51 → thoughtleaders_cli-0.6.53}/.claude-plugin/plugin.json +1 -1
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/AGENTS.md +1 -1
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/PKG-INFO +1 -1
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/pyproject.toml +1 -1
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/skills/tl/SKILL.md +104 -8
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/skills/tl-report-builder/SKILL.md +3 -1
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/skills/tl-report-builder/references/intelligence_filterset_schema.json +15 -0
- thoughtleaders_cli-0.6.53/skills/tl-save-report/SKILL.md +262 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/src/tl_cli/__init__.py +1 -1
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/src/tl_cli/commands/bulk_import.py +5 -1
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/src/tl_cli/commands/credits.py +12 -1
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/src/tl_cli/commands/reports.py +145 -6
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/src/tl_cli/commands/schema.py +11 -4
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/src/tl_cli/commands/setup.py +27 -9
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/src/tl_cli/commands/whoami.py +2 -2
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/src/tl_cli/output/formatter.py +3 -4
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/.claude-plugin/marketplace.json +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/.github/workflows/python-publish.yml +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/.gitignore +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/API.md +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/CLAUDE.md +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/LICENSE +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/README.md +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/agents/tl-analyst.md +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/hooks/hooks.json +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/hooks/scripts/load-tl-skill.mjs +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/hooks/scripts/post-usage.sh +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/hooks/scripts/pre-check.sh +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/skills/tl/references/business-glossary.md +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/skills/tl/references/elasticsearch-schema.md +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/skills/tl/references/firebolt-schema.md +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/skills/tl/references/postgres-schema.md +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/skills/tl-import/SKILL.md +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/skills/tl-keyword-research/SKILL.md +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/skills/tl-keyword-research/scripts/probe.py +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/skills/tl-report-builder/examples/e2e_findings.md +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/skills/tl-report-builder/examples/golden_queries.md +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/skills/tl-report-builder/references/columns_brands.md +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/skills/tl-report-builder/references/columns_channels.md +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/skills/tl-report-builder/references/columns_content.md +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/skills/tl-report-builder/references/columns_sponsorships.md +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/skills/tl-report-builder/references/intelligence_widget_schema.json +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/skills/tl-report-builder/references/report_glossary.md +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/skills/tl-report-builder/references/sortable_columns.json +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/skills/tl-report-builder/references/sponsorship_filterset_schema.json +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/skills/tl-report-builder/references/sponsorship_widget_schema.json +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/skills/tl-report-builder/references/widgets.md +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/skills/tl-report-builder/tools/column_builder.md +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/skills/tl-report-builder/tools/database_query.md +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/skills/tl-report-builder/tools/name_resolver.md +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/skills/tl-report-builder/tools/sample_judge.md +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/skills/tl-report-builder/tools/similar_channels.md +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/skills/tl-report-builder/tools/topic_matcher.md +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/skills/tl-report-builder/tools/widget_builder.md +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/src/tl_cli/_completions.py +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/src/tl_cli/auth/__init__.py +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/src/tl_cli/auth/commands.py +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/src/tl_cli/auth/finalize.py +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/src/tl_cli/auth/login.py +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/src/tl_cli/auth/pkce.py +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/src/tl_cli/auth/token_store.py +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/src/tl_cli/client/__init__.py +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/src/tl_cli/client/errors.py +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/src/tl_cli/client/http.py +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/src/tl_cli/commands/__init__.py +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/src/tl_cli/commands/_comments_common.py +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/src/tl_cli/commands/balance.py +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/src/tl_cli/commands/brands.py +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/src/tl_cli/commands/changelog.py +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/src/tl_cli/commands/channels.py +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/src/tl_cli/commands/db.py +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/src/tl_cli/commands/deals.py +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/src/tl_cli/commands/describe.py +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/src/tl_cli/commands/doctor.py +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/src/tl_cli/commands/matches.py +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/src/tl_cli/commands/proposals.py +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/src/tl_cli/commands/recommender.py +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/src/tl_cli/commands/snapshots.py +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/src/tl_cli/commands/sponsorships.py +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/src/tl_cli/commands/uploads.py +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/src/tl_cli/config.py +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/src/tl_cli/filters.py +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/src/tl_cli/hints.py +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/src/tl_cli/main.py +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/src/tl_cli/output/__init__.py +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/src/tl_cli/self_update.py +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/tests/__init__.py +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/tests/test_auth.py +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/tests/test_filters.py +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/tests/test_http_auth.py +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/tests/test_output.py +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/tests/test_reports.py +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/tests/test_sponsorships.py +0 -0
- {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.53}/uv.lock +0 -0
|
@@ -117,7 +117,7 @@ The version string is defined in three files and all three must be updated toget
|
|
|
117
117
|
|
|
118
118
|
* Do not reference internal architecture of the ThoughtLeaders app in comments or skills. Specifially: do not reference internal table names, field names, API endpoints, Python modules or functions (including the sanitizer).
|
|
119
119
|
* Do not let server implementation details into skill files (anything under `skills/`). Skills describe *what the CLI does* from the user's seat — observable command surface, inputs, outputs, examples. Do not say "the server enforces X", "the API validates Y on its side", "the backend rejects Z" — those are mechanism notes that drift the moment the server changes. State the user-visible behaviour ("unknown keys come back as 400") without naming where it's enforced.
|
|
120
|
-
*
|
|
120
|
+
* **All `import` and `from X import Y` statements live at the top of the Python module file** — after the module docstring, before any code. No inline imports inside function bodies, no lazy imports for "speed" or "optional dependency" reasons. `from __future__ import …` goes at the very top (Python requires that). The only legitimate inline-import exception is **platform-conditional imports** that cannot succeed on the other platform (e.g. `import msvcrt` on Linux, `import termios`/`tty` on Windows) — those stay inside their `if sys.platform == …:` guard. If a circular-import problem makes a top-level import impossible, fix the circular dependency rather than working around it with an inline import.
|
|
121
121
|
|
|
122
122
|
# Git commit rules
|
|
123
123
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: thoughtleaders-cli
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.53
|
|
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
|
|
@@ -140,7 +140,14 @@ Unless the user specifically asks for running a specific report or showing the r
|
|
|
140
140
|
3. **Decide the method of discovery**: If the user want to explore certain topics, use the recommender commands. If it's more about filtering, construct a query for PG or ES.
|
|
141
141
|
4. **Always use --json**: Parse JSON output for multi-step analysis.
|
|
142
142
|
5. **Chain commands**: For complex questions, chain multiple `tl` commands, shell commands, and other tools.
|
|
143
|
-
6. **Format results**: When the user asks for a list or tabular data, present the results as a well-formatted markdown table. Pick the most relevant columns and use clear headers. Sort the result by relevant criteria - if the user asked for "top performers", order by the performance metric; if the user asked for "most recent", sort by the pertinent date desc.
|
|
143
|
+
6. **Format results**: When the user asks for a list or tabular data, present the results as a well-formatted markdown table. Pick the most relevant columns and use clear headers. Sort the result by relevant criteria - if the user asked for "top performers", order by the performance metric; if the user asked for "most recent", sort by the pertinent date desc.
|
|
144
|
+
7. **Always offer to save the result as a report — if the rows fit a report type.** A "fits a report type" result is a table whose rows are **channels**, **brands**, **videos / uploads**, or **sponsorships / deals**. When that's the case, after the table close the reply with a save offer — don't wait for the user to ask. Suggested phrasing (adapt the noun to the entity):
|
|
145
|
+
|
|
146
|
+
> *Want me to save this as a saved TL report you can come back to? Say "save it as a report" and I'll ask whether you want a filter-style report (predicates re-evaluated on every run) or a list-style report (these exact IDs frozen).*
|
|
147
|
+
|
|
148
|
+
If the user says yes (or uses any of the save-trigger phrases, like `save it`, `save the list`, `make a report`, `persist this`, `turn this into a campaign`, `I want to come back to this`), invoke the `tl-save-report` skill — it owns the filter-vs-list decision flow, the FilterSet mapping, and the `tl reports create` / `tl reports save-list` save call. **Don't try to compose the report config yourself**; hand off to the skill.
|
|
149
|
+
|
|
150
|
+
Skip the save offer when the result clearly doesn't fit a report type — a single scalar count, an aggregate roll-up across entity types, view-curve time series, schema introspection output, or anything that isn't a list of channels / brands / videos / sponsorships. A trailing offer on those would just be noise.
|
|
144
151
|
|
|
145
152
|
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.
|
|
146
153
|
|
|
@@ -181,10 +188,6 @@ tl channels history <id-or-name> # Sponsorship history
|
|
|
181
188
|
tl channels similar <id-or-name> # Similarity recommender (Intelligence plan)
|
|
182
189
|
tl brands show <id-or-name> # Brand detail
|
|
183
190
|
tl brands find <query> # Resolve a string to {id, name}; matches name, slug, domain, or keyword
|
|
184
|
-
tl brands history <id-or-name> # Sponsorship history
|
|
185
|
-
tl brands history <query> --channel <id> # Brand mentions on specific channel
|
|
186
|
-
tl brands history-stats <id-or-name> # Aggregate roll-up: counts, total/avg/median views, first/last seen, by-year, top channels
|
|
187
|
-
tl brands history-stats <q> --channel <id> # Same roll-up, narrowed to one channel
|
|
188
191
|
tl brands similar <id-or-name> # Find similar brands via similarity search
|
|
189
192
|
tl recommender tags [query] # List similarity tag names — categories, demographics, formats
|
|
190
193
|
tl recommender top-channels "<tag>" # Top channels loaded on a similarity tag (Intelligence)
|
|
@@ -418,7 +421,7 @@ Load these on demand — don't read all upfront. Pick the one(s) relevant to the
|
|
|
418
421
|
|---|---|---|
|
|
419
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`. |
|
|
420
423
|
| 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. |
|
|
421
|
-
| **AdLink INSERT** with custom price/cost/owner/`weighted_price`/`created_where` | **Unavailable** — `tl sponsorships create` exists but only creates a
|
|
424
|
+
| **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. |
|
|
422
425
|
| 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`). |
|
|
423
426
|
| Firebolt cross-table or join queries; filtering on non-indexed columns in WHERE | **Unavailable** — not accepted. | Fetch a wider slice keyed on `channel_id` (and optionally `id`), filter the rest in `jq`/Python. |
|
|
424
427
|
| ES `query_string`, `regexp`, `wildcard`, `fuzzy`, `more_like_this`, parent/child joins; any `script_*`; multiple aggregations in one body | **Unavailable** — not accepted. | Rewrite using `term`/`terms`/`match`/`bool`/`nested`. For multi-agg dashboards, run multiple `tl db es` calls and combine client-side. For "similar"-style queries, try `tl channels similar` / `tl brands similar` (server-implemented similarity search). |
|
|
@@ -595,9 +598,102 @@ tl db pg "SELECT al.id, al.weighted_price, al.purchase_date, b.name AS brand
|
|
|
595
598
|
LIMIT 500 OFFSET 0" --json
|
|
596
599
|
```
|
|
597
600
|
|
|
598
|
-
###
|
|
601
|
+
### Brand sponsorship history — what channels does Nike sponsor?
|
|
602
|
+
|
|
603
|
+
Resolve the brand to an ID, then probe ES for articles where the brand appears in `sponsored_brand_mentions`. Channel names live in PG (the ES article doc only carries `channel.id`), so the third call joins them in.
|
|
604
|
+
|
|
605
|
+
```bash
|
|
606
|
+
# 1. Resolve "Nike" → brand ID
|
|
607
|
+
tl brands find Nike --json # → results[0].id, say 21416
|
|
608
|
+
|
|
609
|
+
# 2. Recent sponsored videos for that brand (sorted by publication_date desc)
|
|
610
|
+
tl db es '{
|
|
611
|
+
"size": 50,
|
|
612
|
+
"track_total_hits": true,
|
|
613
|
+
"query": {"bool": {"filter": [
|
|
614
|
+
{"term": {"doc_type": "article"}},
|
|
615
|
+
{"term": {"sponsored_brand_mentions": "21416"}}
|
|
616
|
+
]}},
|
|
617
|
+
"sort": [{"publication_date": "desc"}],
|
|
618
|
+
"_source": ["title", "channel.id", "publication_date", "views"]
|
|
619
|
+
}' --json > /tmp/nike_history.json
|
|
620
|
+
|
|
621
|
+
# 3. Resolve channel.id → channel_name (one PG round-trip for the whole page)
|
|
622
|
+
jq -r '[.results[].channel.id] | unique | map(tostring) | join(",")' /tmp/nike_history.json \
|
|
623
|
+
| xargs -I CH_IDS tl db pg "SELECT id, channel_name FROM thoughtleaders_channel WHERE id IN (CH_IDS)" --json
|
|
624
|
+
|
|
625
|
+
# Narrow to a single channel:
|
|
626
|
+
tl db es '{
|
|
627
|
+
"size": 50,
|
|
628
|
+
"track_total_hits": true,
|
|
629
|
+
"query": {"bool": {"filter": [
|
|
630
|
+
{"term": {"doc_type": "article"}},
|
|
631
|
+
{"term": {"sponsored_brand_mentions": "21416"}},
|
|
632
|
+
{"term": {"channel.id": 5607}}
|
|
633
|
+
]}},
|
|
634
|
+
"sort": [{"publication_date": "desc"}],
|
|
635
|
+
"_source": ["title", "publication_date", "views"]
|
|
636
|
+
}'
|
|
637
|
+
|
|
638
|
+
# Was the video a TL-brokered deal? Cross-check ES video_id against AdLink.article_id:
|
|
639
|
+
tl db pg "SELECT article_id FROM thoughtleaders_adlink
|
|
640
|
+
WHERE article_id IN ('1247603:8LskGvKUA9I', '1247603:abc123')"
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
### Brand sponsorship roll-up — totals, first/last seen, top channels, by-year
|
|
644
|
+
|
|
645
|
+
The same ES filter (`doc_type=article` + `sponsored_brand_mentions=<id>`) with `size:0` + aggregations replaces a roll-up call. ES accepts **one aggregation total per request** (top-level + sub-aggs all count), so what would be a single server-side roll-up here splits into a few `tl db es` calls and one client-side join.
|
|
646
|
+
|
|
599
647
|
```bash
|
|
600
|
-
|
|
648
|
+
# Totals + time range (one aggregation total — the four metric aggs are siblings under aggs and bill as a single body)
|
|
649
|
+
tl db es '{
|
|
650
|
+
"size": 0,
|
|
651
|
+
"track_total_hits": true,
|
|
652
|
+
"query": {"bool": {"filter": [
|
|
653
|
+
{"term": {"doc_type": "article"}},
|
|
654
|
+
{"term": {"sponsored_brand_mentions": "21416"}}
|
|
655
|
+
]}},
|
|
656
|
+
"aggs": {
|
|
657
|
+
"views_sum": {"sum": {"field": "views"}},
|
|
658
|
+
"views_avg": {"avg": {"field": "views"}},
|
|
659
|
+
"first_seen": {"min": {"field": "publication_date"}},
|
|
660
|
+
"last_seen": {"max": {"field": "publication_date"}}
|
|
661
|
+
}
|
|
662
|
+
}'
|
|
663
|
+
|
|
664
|
+
# By-year breakdown (date_histogram only — no sub-agg, that would push over the one-agg cap)
|
|
665
|
+
tl db es '{
|
|
666
|
+
"size": 0,
|
|
667
|
+
"query": {"bool": {"filter": [
|
|
668
|
+
{"term": {"doc_type": "article"}},
|
|
669
|
+
{"term": {"sponsored_brand_mentions": "21416"}}
|
|
670
|
+
]}},
|
|
671
|
+
"aggs": {
|
|
672
|
+
"by_year": {"date_histogram": {
|
|
673
|
+
"field": "publication_date", "calendar_interval": "year",
|
|
674
|
+
"format": "yyyy", "min_doc_count": 1
|
|
675
|
+
}}
|
|
676
|
+
}
|
|
677
|
+
}'
|
|
678
|
+
|
|
679
|
+
# Top channels by sponsored-video count (terms agg only — for views per channel, run a second call per channel)
|
|
680
|
+
tl db es '{
|
|
681
|
+
"size": 0,
|
|
682
|
+
"query": {"bool": {"filter": [
|
|
683
|
+
{"term": {"doc_type": "article"}},
|
|
684
|
+
{"term": {"sponsored_brand_mentions": "21416"}}
|
|
685
|
+
]}},
|
|
686
|
+
"aggs": {
|
|
687
|
+
"by_channel": {"terms": {"field": "channel.id", "size": 10, "order": {"_count": "desc"}}}
|
|
688
|
+
}
|
|
689
|
+
}'
|
|
690
|
+
|
|
691
|
+
# TL-brokered deal count for the brand (PG, not ES — adlinks where the brand is on the creator profile)
|
|
692
|
+
tl db pg "SELECT COUNT(*) AS tl_brokered
|
|
693
|
+
FROM thoughtleaders_adlink al
|
|
694
|
+
JOIN thoughtleaders_profile p ON p.id = al.creator_profile_id
|
|
695
|
+
JOIN thoughtleaders_profile_brands pb ON pb.profile_id = p.id
|
|
696
|
+
WHERE pb.brand_id = 21416 AND al.article_id IS NOT NULL"
|
|
601
697
|
```
|
|
602
698
|
|
|
603
699
|
### "Compare view curves for two videos":
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: tl-report-builder
|
|
3
3
|
description: |
|
|
4
|
-
|
|
4
|
+
**MANUAL-INVOCATION-ONLY skill — do NOT auto-trigger on natural-language report requests.** This skill is invoked explicitly by the user (via the `/tl-report-builder` slash command, by naming the skill directly, or via some other unambiguous indication). Phrases like "build me a report", "make a campaign", "find me channels with filters Y", or "save this as a report" do NOT route here automatically. Those phrases go to other skills: the `tl` skill (for analysis / exploration of channels / brands / videos / sponsorships), `tl-save-report` (for persisting an in-chat session's result set as a saved report — filter-style or list-style), or `tl-import` (for adding identifiers to an existing report). This skill itself builds a brand-new TL report config from scratch through a heavy four-phase orchestration (routing → schema + validation → columns → widgets), covering the four report types: content/videos (1), brands (2), channels (3), sponsorships/deals (8). Only fire it when the user has explicitly chosen this path — there's almost always a lighter skill that's the right answer.
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# TL Report Builder Skill
|
|
9
9
|
|
|
10
|
+
> **Manual invocation only.** This skill is heavy (four phases, multiple tool fires, sample validation against live data, FilterSet schema mapping). It is not the default for "I want a list of channels" or "save this as a report" — those are `tl` and `tl-save-report` respectively. Reach this skill only when the user has explicitly invoked it.
|
|
11
|
+
|
|
10
12
|
Translate natural-language report requests into the campaign config JSON the TL dashboard accepts (a `Campaign` + `FilterSet` payload, ready to commit). The skill owns the orchestration end-to-end; sub-tools are invoked conditionally from within the Schema phase based on explicit criteria. Every phase may pause for follow-up interaction with the user when input is ambiguous, incomplete, or invalid.
|
|
11
13
|
|
|
12
14
|
## Core Objective
|
|
@@ -349,6 +349,21 @@
|
|
|
349
349
|
"_tl_django_m2m": "FilterSet.exclude_brands via FilterSetExcludeBrand"
|
|
350
350
|
},
|
|
351
351
|
|
|
352
|
+
"articles": {
|
|
353
|
+
"type": ["array", "null"],
|
|
354
|
+
"items": { "type": "string", "minLength": 3, "pattern": "^[0-9]+:[A-Za-z0-9_-]+$" },
|
|
355
|
+
"uniqueItems": true,
|
|
356
|
+
"description": "Resolved article (video / upload) IDs in the composite form `<channel_id>:<youtube_id>` (matches ES `_id`). M2M list — when populated, the report returns exactly these IDs, no filter logic. Used for list-style report-type-1 (CONTENT) reports.",
|
|
357
|
+
"_tl_django_m2m": "FilterSet.articles via FiltersetArticles"
|
|
358
|
+
},
|
|
359
|
+
"exclude_articles": {
|
|
360
|
+
"type": ["array", "null"],
|
|
361
|
+
"items": { "type": "string", "minLength": 3, "pattern": "^[0-9]+:[A-Za-z0-9_-]+$" },
|
|
362
|
+
"uniqueItems": true,
|
|
363
|
+
"description": "Article IDs to exclude (composite `<channel_id>:<youtube_id>` form). Pairs with a predicate-style FilterSet — useful for 'videos matching X, except these specific ones'.",
|
|
364
|
+
"_tl_django_m2m": "FilterSet.exclude_articles via FiltersetExcludeArticles"
|
|
365
|
+
},
|
|
366
|
+
|
|
352
367
|
"networks": {
|
|
353
368
|
"type": ["array", "null"],
|
|
354
369
|
"items": { "type": "integer", "minimum": 1 },
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tl-save-report
|
|
3
|
+
description: |
|
|
4
|
+
Save the results of an in-chat data-exploration session as a TL report. Triggers when the user wants to persist a channels / brands / videos (uploads) / sponsorships list or filtered set they've been working with — phrases like "save this as a report", "save the list", "turn this into a campaign", "persist this", "make a report from what you found", "save the result", "I want to come back to this". Asks the user up front whether to save it as a filter-style report (predicates re-evaluated against live data each run) or a list-style report (a frozen snapshot of the exact entity IDs from the session).
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# tl-save-report
|
|
8
|
+
|
|
9
|
+
Persist what the user has been exploring as a saved TL report. The skill assumes the data-exploration phase has already happened — the agent doesn't re-run queries, doesn't re-validate the result set, doesn't ask the user what they were looking for. Its single job is **config-from-session**: build a campaign config that captures the user's intent, post it via `tl reports create --config-file`.
|
|
10
|
+
|
|
11
|
+
This is intentionally lighter than `tl-report-builder`. Report-builder runs a four-phase orchestration to TURN a natural-language request INTO a config; save-report TAKES a session that already produced data and writes that data out as a saved report. If the user is starting from scratch ("build me a list of …"), hand off to `tl-report-builder` — don't run save-report.
|
|
12
|
+
|
|
13
|
+
## When to invoke
|
|
14
|
+
|
|
15
|
+
**Invoke when** the user has been exploring data in the current session (running `tl db pg|fb|es` queries, structured `tl` commands, or both) and now wants to **save the result** as a report they can come back to. Trigger phrases include:
|
|
16
|
+
|
|
17
|
+
- "save this as a report" / "save the list" / "save the result"
|
|
18
|
+
- "turn this into a campaign" / "persist this"
|
|
19
|
+
- "make a report from what you found"
|
|
20
|
+
- "I want to come back to this" / "set up a report for these"
|
|
21
|
+
|
|
22
|
+
The entity being saved must be one of: **channels**, **brands**, **videos / uploads / articles**, or **sponsorships / deals**.
|
|
23
|
+
|
|
24
|
+
**Skip when**:
|
|
25
|
+
|
|
26
|
+
- The user wants the report **built from scratch** from a natural-language request (no prior session exploration to capture) → hand off to `tl-report-builder`.
|
|
27
|
+
- The user wants to **add to an existing report** (`"add these channels to report 1234"`) → hand off to `tl-import`.
|
|
28
|
+
- The user only wants the data **shown / counted / analysed in chat** without saving → stay in `tl`; don't invoke this skill.
|
|
29
|
+
|
|
30
|
+
## Step 1 — Detect the report type
|
|
31
|
+
|
|
32
|
+
Match the session's primary entity to one of four report types:
|
|
33
|
+
|
|
34
|
+
| Session entity | Report type | `report_type` code |
|
|
35
|
+
| --- | --- | --- |
|
|
36
|
+
| Channels | CHANNELS | `3` |
|
|
37
|
+
| Brands | BRANDS | `2` |
|
|
38
|
+
| Videos / uploads / articles | CONTENT | `1` |
|
|
39
|
+
| Sponsorships / deals / adlinks | SPONSORSHIPS | `8` |
|
|
40
|
+
|
|
41
|
+
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.
|
|
42
|
+
|
|
43
|
+
## Step 2 — Ask: filter-style or list-style?
|
|
44
|
+
|
|
45
|
+
This is the single most important decision; ask the user before assembling anything. Don't pick silently.
|
|
46
|
+
|
|
47
|
+
**Suggested wording**:
|
|
48
|
+
|
|
49
|
+
> Two ways to save this:
|
|
50
|
+
>
|
|
51
|
+
> • **Filter-style** — I map the criteria from this session (subscriber floor, content categories, keywords, date range, etc.) into the report's filters. The report stays live: every time someone re-runs it, the filters re-evaluate against current data and the result set refreshes.
|
|
52
|
+
>
|
|
53
|
+
> • **List-style** — I snapshot the exact entity IDs we found in this session. The list is frozen — it always shows these IDs, no filter logic. Useful when you've curated the set and don't want re-evaluation.
|
|
54
|
+
>
|
|
55
|
+
> Which do you want?
|
|
56
|
+
|
|
57
|
+
The two styles differ in **which part of the FilterSet you populate**:
|
|
58
|
+
|
|
59
|
+
- **Filter-style → predicate fields populated** (`keywords`, `min_reach`, `country`, dates, demographics, etc.). Through-table M2M fields stay empty.
|
|
60
|
+
- **List-style → through-table M2M fields populated** (`channels` / `brands` / `articles` / `sponsorships`). Predicate fields stay empty.
|
|
61
|
+
|
|
62
|
+
A hybrid (some predicates + some M2M IDs) is *legal* but rarely what the user asked for — confirm before mixing them.
|
|
63
|
+
|
|
64
|
+
### When filter-style is the right answer
|
|
65
|
+
|
|
66
|
+
Pick filter-style when the user's session was driven by criteria the platform's FilterSet can express directly — keyword searches over `title` / `summary` / `transcript` / channel description, attribute thresholds (`min_reach`, `min_views`, country / language), categorical scoping (content categories, demographics, MSN status), date ranges, similar-to-channels.
|
|
67
|
+
|
|
68
|
+
When the user said something like *"channels in the cooking niche with >100K subs, all-time"* — the criteria map cleanly into a FilterSet, and the user almost certainly wants the report to keep refreshing as new channels meet the bar.
|
|
69
|
+
|
|
70
|
+
### When list-style is the right answer
|
|
71
|
+
|
|
72
|
+
Pick list-style when:
|
|
73
|
+
|
|
74
|
+
- The session produced a **specifically curated set** the user wants frozen (manual review, similar-channel walks, cross-reference subtraction, sponsorship-history dedup) — *"these 14 channels are the ones we're pitching, save this exact list"*.
|
|
75
|
+
- The session's **filters can't be mapped** into FilterSet fields cleanly (custom raw-SQL joins, multi-source aggregation in `jq`/`duckdb`, anything where the filter logic lived in the shell pipeline rather than in the platform schema). The honest move is list-style.
|
|
76
|
+
- The user explicitly said *"snapshot"*, *"freeze"*, *"this exact list"*, *"don't re-evaluate"*.
|
|
77
|
+
|
|
78
|
+
### Filter-style — mapping session criteria into the FilterSet
|
|
79
|
+
|
|
80
|
+
The authoritative field catalogues live in the report-builder's references:
|
|
81
|
+
|
|
82
|
+
- **Types 1 / 2 / 3** (CONTENT, BRANDS, CHANNELS): [`../tl-report-builder/references/intelligence_filterset_schema.json`](../tl-report-builder/references/intelligence_filterset_schema.json)
|
|
83
|
+
- **Type 8** (SPONSORSHIPS): [`../tl-report-builder/references/sponsorship_filterset_schema.json`](../tl-report-builder/references/sponsorship_filterset_schema.json)
|
|
84
|
+
|
|
85
|
+
Don't invent fields. The schema's keys are the only ones the platform accepts; unknown keys come back as `400 Invalid filterset.<field>`.
|
|
86
|
+
|
|
87
|
+
Common mappings (use the schema file for the full list):
|
|
88
|
+
|
|
89
|
+
| Session criterion | FilterSet field |
|
|
90
|
+
| --- | --- |
|
|
91
|
+
| Topic keywords (`"crypto"`, `"biohacking"`) | `keywords[]` + `keyword_operator` (`AND`/`OR`) + `content_fields[]` |
|
|
92
|
+
| Subscriber floor | `min_reach` |
|
|
93
|
+
| Views / impression floor | `min_views`, `min_impression` |
|
|
94
|
+
| Content category | `content_categories[]` |
|
|
95
|
+
| Country / language | `country`, `language` |
|
|
96
|
+
| MSN-only | `msn_channels_only: true` |
|
|
97
|
+
| Demographics (age / gender / geo) | `demographic_male_share`, `demographic_usa_share`, `demographic_geo`, etc. |
|
|
98
|
+
| Publication date range | `start_date`, `end_date`, or `days_ago` |
|
|
99
|
+
| Sponsorship date range (type 8) | `start_date` / `end_date` (send axis), `createdat_from` / `createdat_to` (created axis) |
|
|
100
|
+
| Cross-reference (`"not pitched to brand X"`) | `cross_references[]` |
|
|
101
|
+
| Similar-to-channels | `filters_json.similar_to_channels[]` |
|
|
102
|
+
| Brand mention filter | `sponsored_brand_mentions[]` (via `filters_json`) |
|
|
103
|
+
| Publish-status (sponsorships) | `publish_status` |
|
|
104
|
+
|
|
105
|
+
If the session used filters that don't map cleanly, tell the user: *"I can't map [the specific predicate] into a FilterSet — the platform doesn't expose that field directly. Want to fall back to list-style for this report?"*
|
|
106
|
+
|
|
107
|
+
### List-style — populating the M2M
|
|
108
|
+
|
|
109
|
+
Collect the entity IDs from the session results into a single array and place them in the corresponding through-table M2M field:
|
|
110
|
+
|
|
111
|
+
| Entity | FilterSet M2M field | ID shape | Exclude variant |
|
|
112
|
+
| --- | --- | --- | --- |
|
|
113
|
+
| Channels | `channels` | integer IDs | `exclude_channels` |
|
|
114
|
+
| Brands | `brands` | integer IDs | `exclude_brands` |
|
|
115
|
+
| Videos / uploads / articles | `articles` | composite string `<channel_id>:<youtube_id>` (matches ES `_id`) | `exclude_articles` |
|
|
116
|
+
| Sponsorships | `sponsorships` | integer IDs (AdLink IDs) | `exclude_sponsorships` |
|
|
117
|
+
|
|
118
|
+
**Article IDs are the composite string form**, not bare YouTube video IDs. If the session has YouTube IDs (`dQw4w9WgXcQ`) without channel prefixes, fetch `channel.id` for each via `tl db es` and rebuild the composite form before saving.
|
|
119
|
+
|
|
120
|
+
All other FilterSet predicate fields stay empty (`null` or omitted). Populating both a predicate AND the M2M creates a hybrid filter — the platform will still accept it, but the result set then becomes "IDs in the M2M that ALSO pass the predicate", which is almost never what the user said they wanted. Confirm before mixing.
|
|
121
|
+
|
|
122
|
+
The `exclude_*` variants pair with a separate predicate-style FilterSet — useful when the user said *"channels matching X, except these specific IDs"*. That's a hybrid by design; both halves get populated.
|
|
123
|
+
|
|
124
|
+
## Step 3 — Title and description (mandatory)
|
|
125
|
+
|
|
126
|
+
`tl reports create` rejects with HTTP 400 if either is missing — the validation regression has happened before. Always generate both:
|
|
127
|
+
|
|
128
|
+
- **`report_title`** — ≤ 60 chars. Capture the niche or intent: *"TPP fintech channels — May 2026"*, *"Speedcubing channels — curated list"*, *"Q1 2026 sold sponsorships, beauty brands"*.
|
|
129
|
+
- **`report_description`** — 1–3 sentences. Summarise what's in the report and how it was assembled. **Mention "filter-style" or "list-style" explicitly** so future readers know what they're looking at (list-style reports can look identical to filter-style ones from the dashboard if nobody documents the choice).
|
|
130
|
+
|
|
131
|
+
Propose values and let the user edit. Don't ship blank strings.
|
|
132
|
+
|
|
133
|
+
## Step 4 — Pick columns
|
|
134
|
+
|
|
135
|
+
Use the type's default column set; agents shouldn't compose columns from scratch when the session didn't specify any. Defaults live in:
|
|
136
|
+
|
|
137
|
+
- Type 1: [`../tl-report-builder/references/columns_content.md`](../tl-report-builder/references/columns_content.md)
|
|
138
|
+
- Type 2: [`../tl-report-builder/references/columns_brands.md`](../tl-report-builder/references/columns_brands.md)
|
|
139
|
+
- Type 3: [`../tl-report-builder/references/columns_channels.md`](../tl-report-builder/references/columns_channels.md)
|
|
140
|
+
- Type 8: [`../tl-report-builder/references/columns_sponsorships.md`](../tl-report-builder/references/columns_sponsorships.md)
|
|
141
|
+
|
|
142
|
+
If the session showed the user specific columns (`"show reach, subscribers, country"`), include those PLUS the type's required defaults. Validate that the `sort` value references a column that's actually present in the emitted `columns` dict — otherwise the report fails to render.
|
|
143
|
+
|
|
144
|
+
For **custom columns** (computed formulas the user defined inline during the session), include them under `columns._custom` per the column-builder convention; consult the type's `columns_<type>.md` for the custom-column shape.
|
|
145
|
+
|
|
146
|
+
## Step 5 — Pick widgets
|
|
147
|
+
|
|
148
|
+
Use a default set per report type. Don't over-engineer — the user can refine via `tl reports update` after saving. Widget catalogues:
|
|
149
|
+
|
|
150
|
+
- Types 1 / 2 / 3: [`../tl-report-builder/references/intelligence_widget_schema.json`](../tl-report-builder/references/intelligence_widget_schema.json)
|
|
151
|
+
- Type 8: [`../tl-report-builder/references/sponsorship_widget_schema.json`](../tl-report-builder/references/sponsorship_widget_schema.json)
|
|
152
|
+
|
|
153
|
+
Pick 4–6 widgets. For type 8 specifically, the schema's `_tl_axis_branching` rules pick the correct axis based on which date field the FilterSet populates (`send_date` for proposals, `purchase_date` for sold).
|
|
154
|
+
|
|
155
|
+
For **list-style** reports the widgets still render — they aggregate over the frozen ID list. Pick the same defaults as filter-style; the user reading the saved report wants the dashboard view either way.
|
|
156
|
+
|
|
157
|
+
## Step 6 — Assemble the config
|
|
158
|
+
|
|
159
|
+
Final config shape (`Campaign` + `FilterSet` + columns + widgets):
|
|
160
|
+
|
|
161
|
+
```json
|
|
162
|
+
{
|
|
163
|
+
"type": 2,
|
|
164
|
+
"report_type": 1 | 2 | 3 | 8,
|
|
165
|
+
"report_title": "...",
|
|
166
|
+
"report_description": "...",
|
|
167
|
+
"filterset": { ... },
|
|
168
|
+
"columns": { ... },
|
|
169
|
+
"widgets": [ ... ],
|
|
170
|
+
"histogram_bucket_size": "month",
|
|
171
|
+
"sort": "-reach"
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
`type=2` (DYNAMIC) is the campaign-model contract; don't change it.
|
|
176
|
+
|
|
177
|
+
Write to a portable temp file and verify the file exists before saving:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
TMP=$(mktemp -t tl-save-report-XXXX.json)
|
|
181
|
+
cat > "$TMP" <<'EOF'
|
|
182
|
+
{ ...config... }
|
|
183
|
+
EOF
|
|
184
|
+
ls -la "$TMP" # verify before save
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**Don't write the transport file under the user's project directory.** It's a transport, not a deliverable.
|
|
188
|
+
|
|
189
|
+
## Step 7 — Save
|
|
190
|
+
|
|
191
|
+
Two save paths, pick by style:
|
|
192
|
+
|
|
193
|
+
**List-style — `tl reports save-list`** is the simpler path. It accepts an entity + ID file + title + description, builds the minimal config, and POSTs in one call. Skip steps 4–6 entirely when you take this route — the command's defaults handle them, and the user can refine later via `tl reports update`.
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
# Write the IDs (one per line — integers for channels/brands/sponsorships;
|
|
197
|
+
# composite `<channel_id>:<youtube_id>` strings for articles).
|
|
198
|
+
printf '5607\n12345\n67890\n' > "$IDS"
|
|
199
|
+
|
|
200
|
+
tl reports save-list channels --ids-file "$IDS" \
|
|
201
|
+
--title "TPP fintech — May 2026 curated" \
|
|
202
|
+
--description "List-style: 3 channels hand-picked after the May 2026 review pass." \
|
|
203
|
+
--yes --json
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**Filter-style — `tl reports create --config-file`** is the path that needs the full config (columns + widgets + sort built from steps 4–6):
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
tl reports create --config-file "$TMP" --yes --json
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
- `--yes` skips the confirmation prompt (the user already chose).
|
|
213
|
+
- `--json` makes the response parseable so you can extract `report_url` and `campaign_id` cleanly.
|
|
214
|
+
- `--config-file` (not `--config`) sidesteps shell-quoting issues with apostrophes / dollar signs / backticks in titles or keywords.
|
|
215
|
+
|
|
216
|
+
On success the response envelope contains:
|
|
217
|
+
|
|
218
|
+
```json
|
|
219
|
+
{
|
|
220
|
+
"results": [{
|
|
221
|
+
"campaign_id": 12345,
|
|
222
|
+
"report_url": "/dashboard/reports/12345/",
|
|
223
|
+
"unresolved_names": []
|
|
224
|
+
}],
|
|
225
|
+
"usage": { "credits_charged": ..., "balance_remaining": ... }
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
On failure (HTTP 4xx / 5xx): **surface the error verbatim**. Do NOT silently report success. Common failure modes:
|
|
230
|
+
|
|
231
|
+
- `400 Missing required field: report_title` / `report_description` → you skipped step 3, go back.
|
|
232
|
+
- `400 Invalid filterset.<field>` → the mapping in step 2 produced an unknown FilterSet field; check against the schema and remove the offending key.
|
|
233
|
+
- `400 Invalid columns.<column>` → the chosen column isn't in the type's `columns_<type>.md` catalogue.
|
|
234
|
+
- `403 Forbidden` → the user lacks the plan required for this report type (Intelligence for 1/2/3 in some orgs; check `tl whoami`).
|
|
235
|
+
|
|
236
|
+
## Step 8 — Report back
|
|
237
|
+
|
|
238
|
+
Echo the saved URL + ID, plus a follow-up offer for refinement:
|
|
239
|
+
|
|
240
|
+
> Saved as report **12345**: https://app.thoughtleaders.io/dashboard/reports/12345/
|
|
241
|
+
>
|
|
242
|
+
> Want to refine the columns, widgets, title, or description? Tell me what to change and I'll run `tl reports update`.
|
|
243
|
+
|
|
244
|
+
The follow-up offer matters because **FilterSet changes (keywords, demographics, M2M lists) can't be patched in place** via `tl reports update` — they require saving a new variant. Surface that limitation only if the user actually asks to change FilterSet fields.
|
|
245
|
+
|
|
246
|
+
## Self-check before saving
|
|
247
|
+
|
|
248
|
+
1. `report_title` is non-empty and ≤ 60 chars.
|
|
249
|
+
2. `report_description` is non-empty, 1–3 sentences, explicitly says "filter-style" or "list-style".
|
|
250
|
+
3. `report_type` matches the session's primary entity (1 / 2 / 3 / 8).
|
|
251
|
+
4. `type` is `2` (DYNAMIC).
|
|
252
|
+
5. `sort` references a column actually present in `columns`.
|
|
253
|
+
6. **For filter-style**: no M2M `channels` / `brands` / `articles` / `sponsorships` populated unless the user explicitly asked for a narrow-to-these-IDs overlay (hybrid case).
|
|
254
|
+
7. **For list-style**: no predicate fields (`keywords`, `min_reach`, dates, etc.) populated — the M2M is the entire filter.
|
|
255
|
+
8. **For list-style with articles** (type 1): every entry in `filterset.articles` is the composite `<channel_id>:<youtube_id>` form, not a bare YouTube ID.
|
|
256
|
+
9. Transport file written to a portable temp path (not the user's working directory) and verified to exist before `tl reports create`.
|
|
257
|
+
|
|
258
|
+
## What this skill does NOT do
|
|
259
|
+
|
|
260
|
+
- **No Phase 1–4 orchestration**, no AI-driven keyword research, no name resolution, no `sample_judge` validation pass. The session already produced the data — re-running discovery would be wasted effort. If the user comes in with a natural-language request and no prior session, that's `tl-report-builder`'s job, not this skill's.
|
|
261
|
+
- **No editing of existing reports.** If the user wants to refine an already-saved report's columns, widgets, title, or description, run `tl reports update <id>` directly. For FilterSet refinements, the platform requires saving a new variant.
|
|
262
|
+
- **No bulk-importing into an existing report.** That's `tl-import`'s role. Save-report only creates new reports.
|
|
@@ -11,6 +11,7 @@ import time
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
|
|
13
13
|
import typer
|
|
14
|
+
from pytoon import encode as toon_encode
|
|
14
15
|
from rich.console import Console
|
|
15
16
|
|
|
16
17
|
from tl_cli.client.errors import ApiError, handle_api_error
|
|
@@ -59,6 +60,7 @@ def bulk_import_command(
|
|
|
59
60
|
ids_file: str | None = typer.Option(None, "--ids-file", "-f", help="Path to file with one identifier per line. Omit to read from stdin."),
|
|
60
61
|
exclude: bool = typer.Option(False, "--exclude", help="Mark these identifiers as excluded from the report instead of included"),
|
|
61
62
|
json_output: bool = typer.Option(False, "--json", help="JSON output (default)"),
|
|
63
|
+
toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
|
|
62
64
|
) -> None:
|
|
63
65
|
"""Bulk-import entities into a report.
|
|
64
66
|
|
|
@@ -113,7 +115,9 @@ def bulk_import_command(
|
|
|
113
115
|
client.close()
|
|
114
116
|
|
|
115
117
|
output = {"task_id": task_id, **result}
|
|
116
|
-
if
|
|
118
|
+
if toon_output:
|
|
119
|
+
sys.stdout.write(toon_encode(output) + "\n")
|
|
120
|
+
elif json_output or not sys.stdout.isatty():
|
|
117
121
|
json.dump(output, sys.stdout, indent=2)
|
|
118
122
|
sys.stdout.write("\n")
|
|
119
123
|
else:
|
|
@@ -18,6 +18,7 @@ import webbrowser
|
|
|
18
18
|
from decimal import Decimal, InvalidOperation
|
|
19
19
|
|
|
20
20
|
import typer
|
|
21
|
+
from pytoon import encode as toon_encode
|
|
21
22
|
from rich.console import Console
|
|
22
23
|
from rich.prompt import Prompt
|
|
23
24
|
from rich.table import Table
|
|
@@ -34,6 +35,7 @@ err = Console(stderr=True)
|
|
|
34
35
|
@app.command("pricing")
|
|
35
36
|
def pricing_cmd(
|
|
36
37
|
json_output: bool = typer.Option(False, "--json", help="JSON output"),
|
|
38
|
+
toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
|
|
37
39
|
) -> None:
|
|
38
40
|
"""Show the credit-to-USD rate, minimum purchase, and starter balance.
|
|
39
41
|
|
|
@@ -48,6 +50,10 @@ def pricing_cmd(
|
|
|
48
50
|
finally:
|
|
49
51
|
client.close()
|
|
50
52
|
|
|
53
|
+
if toon_output:
|
|
54
|
+
print(toon_encode(data))
|
|
55
|
+
return
|
|
56
|
+
|
|
51
57
|
if json_output:
|
|
52
58
|
print(json.dumps(data, indent=2, default=str))
|
|
53
59
|
return
|
|
@@ -150,9 +156,10 @@ def history_cmd(
|
|
|
150
156
|
limit: int = typer.Option(25, "--limit", help="Max rows to show"),
|
|
151
157
|
offset: int = typer.Option(0, "--offset", help="Offset for pagination"),
|
|
152
158
|
json_output: bool = typer.Option(False, "--json", help="JSON output"),
|
|
159
|
+
toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
|
|
153
160
|
) -> None:
|
|
154
161
|
"""Show recent credit top-ups for your organization (free)."""
|
|
155
|
-
fmt = detect_format(json_output, False, False,
|
|
162
|
+
fmt = detect_format(json_output, False, False, toon_output)
|
|
156
163
|
client = get_client()
|
|
157
164
|
try:
|
|
158
165
|
data = client.get("/credit-purchases", params={"limit": limit, "offset": offset})
|
|
@@ -166,6 +173,10 @@ def history_cmd(
|
|
|
166
173
|
print(json.dumps(data, indent=2, default=str))
|
|
167
174
|
return
|
|
168
175
|
|
|
176
|
+
if fmt == "toon":
|
|
177
|
+
print(toon_encode(data.get("results", [])))
|
|
178
|
+
return
|
|
179
|
+
|
|
169
180
|
rows = data.get("results", [])
|
|
170
181
|
if not rows:
|
|
171
182
|
console.print("[dim]No credit purchases yet. Run `tl credits buy --amount-usd N`.[/dim]")
|