thoughtleaders-cli 0.6.52__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.52 → thoughtleaders_cli-0.6.53}/.claude-plugin/plugin.json +1 -1
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/PKG-INFO +1 -1
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/pyproject.toml +1 -1
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/skills/tl/SKILL.md +9 -2
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/skills/tl-report-builder/SKILL.md +3 -1
- {thoughtleaders_cli-0.6.52 → 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.52 → thoughtleaders_cli-0.6.53}/src/tl_cli/__init__.py +1 -1
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/src/tl_cli/commands/reports.py +132 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/.claude-plugin/marketplace.json +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/.github/workflows/python-publish.yml +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/.gitignore +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/AGENTS.md +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/API.md +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/CLAUDE.md +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/LICENSE +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/README.md +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/agents/tl-analyst.md +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/hooks/hooks.json +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/hooks/scripts/load-tl-skill.mjs +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/hooks/scripts/post-usage.sh +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/hooks/scripts/pre-check.sh +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/skills/tl/references/business-glossary.md +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/skills/tl/references/elasticsearch-schema.md +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/skills/tl/references/firebolt-schema.md +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/skills/tl/references/postgres-schema.md +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/skills/tl-import/SKILL.md +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/skills/tl-keyword-research/SKILL.md +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/skills/tl-keyword-research/scripts/probe.py +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/skills/tl-report-builder/examples/e2e_findings.md +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/skills/tl-report-builder/examples/golden_queries.md +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/skills/tl-report-builder/references/columns_brands.md +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/skills/tl-report-builder/references/columns_channels.md +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/skills/tl-report-builder/references/columns_content.md +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/skills/tl-report-builder/references/columns_sponsorships.md +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/skills/tl-report-builder/references/intelligence_widget_schema.json +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/skills/tl-report-builder/references/report_glossary.md +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/skills/tl-report-builder/references/sortable_columns.json +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/skills/tl-report-builder/references/sponsorship_filterset_schema.json +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/skills/tl-report-builder/references/sponsorship_widget_schema.json +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/skills/tl-report-builder/references/widgets.md +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/skills/tl-report-builder/tools/column_builder.md +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/skills/tl-report-builder/tools/database_query.md +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/skills/tl-report-builder/tools/name_resolver.md +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/skills/tl-report-builder/tools/sample_judge.md +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/skills/tl-report-builder/tools/similar_channels.md +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/skills/tl-report-builder/tools/topic_matcher.md +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/skills/tl-report-builder/tools/widget_builder.md +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/src/tl_cli/_completions.py +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/src/tl_cli/auth/__init__.py +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/src/tl_cli/auth/commands.py +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/src/tl_cli/auth/finalize.py +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/src/tl_cli/auth/login.py +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/src/tl_cli/auth/pkce.py +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/src/tl_cli/auth/token_store.py +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/src/tl_cli/client/__init__.py +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/src/tl_cli/client/errors.py +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/src/tl_cli/client/http.py +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/src/tl_cli/commands/__init__.py +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/src/tl_cli/commands/_comments_common.py +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/src/tl_cli/commands/balance.py +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/src/tl_cli/commands/brands.py +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/src/tl_cli/commands/bulk_import.py +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/src/tl_cli/commands/changelog.py +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/src/tl_cli/commands/channels.py +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/src/tl_cli/commands/credits.py +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/src/tl_cli/commands/db.py +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/src/tl_cli/commands/deals.py +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/src/tl_cli/commands/describe.py +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/src/tl_cli/commands/doctor.py +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/src/tl_cli/commands/matches.py +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/src/tl_cli/commands/proposals.py +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/src/tl_cli/commands/recommender.py +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/src/tl_cli/commands/schema.py +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/src/tl_cli/commands/setup.py +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/src/tl_cli/commands/snapshots.py +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/src/tl_cli/commands/sponsorships.py +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/src/tl_cli/commands/uploads.py +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/src/tl_cli/commands/whoami.py +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/src/tl_cli/config.py +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/src/tl_cli/filters.py +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/src/tl_cli/hints.py +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/src/tl_cli/main.py +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/src/tl_cli/output/__init__.py +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/src/tl_cli/output/formatter.py +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/src/tl_cli/self_update.py +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/tests/__init__.py +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/tests/test_auth.py +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/tests/test_filters.py +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/tests/test_http_auth.py +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/tests/test_output.py +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/tests/test_reports.py +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/tests/test_sponsorships.py +0 -0
- {thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/uv.lock +0 -0
|
@@ -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
|
|
|
@@ -414,7 +421,7 @@ Load these on demand — don't read all upfront. Pick the one(s) relevant to the
|
|
|
414
421
|
|---|---|---|
|
|
415
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`. |
|
|
416
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. |
|
|
417
|
-
| **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. |
|
|
418
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`). |
|
|
419
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. |
|
|
420
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). |
|
|
@@ -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.
|
|
@@ -427,6 +427,138 @@ def create_report(
|
|
|
427
427
|
client.close()
|
|
428
428
|
|
|
429
429
|
|
|
430
|
+
ENTITY_TO_REPORT_TYPE = {
|
|
431
|
+
"channels": 3,
|
|
432
|
+
"brands": 2,
|
|
433
|
+
"articles": 1,
|
|
434
|
+
"sponsorships": 8,
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
# FilterSet M2M field per entity is identical to the entity name, except
|
|
438
|
+
# article IDs are composite strings (`<channel_id>:<youtube_id>`) and the
|
|
439
|
+
# others are integers. See the FilterSet schema references in
|
|
440
|
+
# skills/tl-report-builder/references/ for the catalogue.
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def _read_ids(path: str, entity: str) -> list:
|
|
444
|
+
"""Read IDs from a file, one per line. Type per entity."""
|
|
445
|
+
try:
|
|
446
|
+
with open(path, encoding="utf-8") as fh:
|
|
447
|
+
raw = [line.strip() for line in fh if line.strip()]
|
|
448
|
+
except OSError as exc:
|
|
449
|
+
err.print(f"[red]Could not read --ids-file: {exc}[/red]")
|
|
450
|
+
raise typer.Exit(1)
|
|
451
|
+
if not raw:
|
|
452
|
+
err.print(f"[red]--ids-file is empty: {path}[/red]")
|
|
453
|
+
raise typer.Exit(1)
|
|
454
|
+
if entity == "articles":
|
|
455
|
+
# Composite string IDs `<channel_id>:<youtube_id>`.
|
|
456
|
+
bad = [r for r in raw if ":" not in r]
|
|
457
|
+
if bad:
|
|
458
|
+
err.print(
|
|
459
|
+
f"[red]Article IDs must be in composite form `<channel_id>:<youtube_id>` (matches ES `_id`). "
|
|
460
|
+
f"Bare YouTube IDs are not accepted. Offending: {bad[:5]}[/red]"
|
|
461
|
+
)
|
|
462
|
+
raise typer.Exit(1)
|
|
463
|
+
return raw
|
|
464
|
+
# channels / brands / sponsorships → integer IDs.
|
|
465
|
+
try:
|
|
466
|
+
return [int(r) for r in raw]
|
|
467
|
+
except ValueError:
|
|
468
|
+
err.print(f"[red]{entity} IDs must be integers, one per line.[/red]")
|
|
469
|
+
raise typer.Exit(1)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
@app.command("save-list")
|
|
473
|
+
def save_list_cmd(
|
|
474
|
+
entity: str = typer.Argument(..., help=f"Entity type: one of {', '.join(ENTITY_TO_REPORT_TYPE)}"),
|
|
475
|
+
ids_file: str = typer.Option(..., "--ids-file", "-f", help="Path to a file with one entity ID per line"),
|
|
476
|
+
title: str = typer.Option(..., "--title", "-t", help="Report title (≤60 chars)"),
|
|
477
|
+
description: str = typer.Option(..., "--description", "-d", help="Report description (1-3 sentences)"),
|
|
478
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
|
|
479
|
+
json_output: bool = typer.Option(False, "--json", help="JSON output"),
|
|
480
|
+
toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
|
|
481
|
+
) -> None:
|
|
482
|
+
"""Save a list-style report from a curated set of entity IDs.
|
|
483
|
+
|
|
484
|
+
Sugar over `tl reports create --config-file` for the list-style flow:
|
|
485
|
+
builds a minimal campaign config with the FilterSet's M2M field
|
|
486
|
+
populated (no predicate filters), posts it to /reports/confirm, and
|
|
487
|
+
prints the new report's URL + ID.
|
|
488
|
+
|
|
489
|
+
The resulting report is a frozen snapshot — it always shows exactly
|
|
490
|
+
the IDs you supplied, with no filter re-evaluation. Refine columns,
|
|
491
|
+
widgets, title, or description via `tl reports update` afterwards.
|
|
492
|
+
|
|
493
|
+
Entity must be one of: channels, brands, articles, sponsorships.
|
|
494
|
+
Article IDs are the composite form `<channel_id>:<youtube_id>` —
|
|
495
|
+
bare YouTube IDs are not accepted.
|
|
496
|
+
|
|
497
|
+
Examples:
|
|
498
|
+
tl reports save-list channels --ids-file channels.txt \\
|
|
499
|
+
--title "TPP fintech curated set" --description "14 channels, May 2026 review"
|
|
500
|
+
tl reports save-list articles --ids-file videos.txt \\
|
|
501
|
+
--title "Speedcubing top videos" --description "Hand-picked from the long tail" --yes
|
|
502
|
+
"""
|
|
503
|
+
if entity not in ENTITY_TO_REPORT_TYPE:
|
|
504
|
+
err.print(f"[red]entity must be one of: {', '.join(ENTITY_TO_REPORT_TYPE)}[/red]")
|
|
505
|
+
raise typer.Exit(2)
|
|
506
|
+
if len(title) > 60:
|
|
507
|
+
err.print(f"[red]Title is {len(title)} chars; max is 60.[/red]")
|
|
508
|
+
raise typer.Exit(1)
|
|
509
|
+
|
|
510
|
+
ids = _read_ids(ids_file, entity)
|
|
511
|
+
report_type = ENTITY_TO_REPORT_TYPE[entity]
|
|
512
|
+
filterset = {entity: ids}
|
|
513
|
+
|
|
514
|
+
config = {
|
|
515
|
+
"type": 2,
|
|
516
|
+
"report_type": report_type,
|
|
517
|
+
"report_title": title,
|
|
518
|
+
"report_description": description,
|
|
519
|
+
"filterset": filterset,
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if not yes:
|
|
523
|
+
err.print(
|
|
524
|
+
f"\n[bold]About to save a list-style report:[/bold]"
|
|
525
|
+
f"\n Title: {title}"
|
|
526
|
+
f"\n Type: {entity} (report_type={report_type})"
|
|
527
|
+
f"\n Entity IDs: {len(ids)}"
|
|
528
|
+
)
|
|
529
|
+
if not typer.confirm("Save this report?", default=True):
|
|
530
|
+
err.print("[dim]Cancelled.[/dim]")
|
|
531
|
+
raise typer.Exit(0)
|
|
532
|
+
|
|
533
|
+
fmt = detect_format(json_output, False, False, toon_output)
|
|
534
|
+
client = get_client()
|
|
535
|
+
try:
|
|
536
|
+
data = client.post("/reports/confirm", json_body={"config": config, "prompts": [], "reasoning": ""})
|
|
537
|
+
except ApiError as e:
|
|
538
|
+
handle_api_error(e)
|
|
539
|
+
return
|
|
540
|
+
finally:
|
|
541
|
+
client.close()
|
|
542
|
+
|
|
543
|
+
if fmt == "toon":
|
|
544
|
+
print(toon_encode(data))
|
|
545
|
+
return
|
|
546
|
+
if fmt == "json":
|
|
547
|
+
print(json.dumps(data, indent=2, default=str))
|
|
548
|
+
return
|
|
549
|
+
|
|
550
|
+
results = data.get("results", [{}])
|
|
551
|
+
result = results[0] if results else {}
|
|
552
|
+
err.print()
|
|
553
|
+
err.print("[green bold]Report created![/green bold]")
|
|
554
|
+
err.print(f" Campaign ID: {result.get('campaign_id', '?')}")
|
|
555
|
+
err.print(f" URL: https://app.thoughtleaders.io{result.get('report_url', '')}")
|
|
556
|
+
err.print(
|
|
557
|
+
"\n[dim]Refine columns, widgets, title, or description with "
|
|
558
|
+
"`tl reports update <id>`.[/dim]"
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
|
|
430
562
|
@app.command("update")
|
|
431
563
|
def update_report(
|
|
432
564
|
report_id: int = typer.Argument(..., help="Report ID"),
|
|
File without changes
|
{thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/.github/workflows/python-publish.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/skills/tl/references/business-glossary.md
RENAMED
|
File without changes
|
{thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/skills/tl/references/elasticsearch-schema.md
RENAMED
|
File without changes
|
{thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/skills/tl/references/firebolt-schema.md
RENAMED
|
File without changes
|
{thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/skills/tl/references/postgres-schema.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/skills/tl-keyword-research/scripts/probe.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{thoughtleaders_cli-0.6.52 → thoughtleaders_cli-0.6.53}/src/tl_cli/commands/_comments_common.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|