thoughtleaders-cli 0.6.25__tar.gz → 0.6.27__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.25 → thoughtleaders_cli-0.6.27}/.claude-plugin/plugin.json +1 -1
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/PKG-INFO +1 -1
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/pyproject.toml +1 -1
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/skills/tl/SKILL.md +5 -2
- thoughtleaders_cli-0.6.27/skills/tl-import/SKILL.md +253 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/skills/tl-report-builder/SKILL.md +9 -1
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/src/tl_cli/__init__.py +1 -1
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/src/tl_cli/auth/commands.py +9 -1
- thoughtleaders_cli-0.6.27/src/tl_cli/auth/finalize.py +88 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/src/tl_cli/client/errors.py +2 -1
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/src/tl_cli/commands/balance.py +13 -0
- thoughtleaders_cli-0.6.27/src/tl_cli/commands/credits.py +184 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/src/tl_cli/commands/reports.py +6 -1
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/src/tl_cli/commands/whoami.py +18 -6
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/src/tl_cli/main.py +2 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/src/tl_cli/self_update.py +42 -2
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/tests/test_reports.py +27 -0
- thoughtleaders_cli-0.6.25/skills/bulk-import/SKILL.md +0 -90
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/.claude-plugin/marketplace.json +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/.github/workflows/python-publish.yml +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/.gitignore +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/AGENTS.md +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/CLAUDE.md +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/LICENSE +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/README.md +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/agents/tl-analyst.md +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/commands/tl-balance.md +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/commands/tl-reports.md +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/commands/tl-sponsorships.md +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/commands/tl.md +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/docs/architecture.md +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/hooks/hooks.json +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/hooks/scripts/post-usage.sh +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/hooks/scripts/pre-check.sh +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/skills/tl/references/business-glossary.md +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/skills/tl/references/elasticsearch-schema.md +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/skills/tl/references/firebolt-schema.md +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/skills/tl/references/postgres-schema.md +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/skills/tl-report-builder/examples/e2e_findings.md +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/skills/tl-report-builder/examples/golden_queries.md +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/skills/tl-report-builder/references/columns_brands.md +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/skills/tl-report-builder/references/columns_channels.md +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/skills/tl-report-builder/references/columns_content.md +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/skills/tl-report-builder/references/columns_sponsorships.md +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/skills/tl-report-builder/references/intelligence_filterset_schema.json +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/skills/tl-report-builder/references/intelligence_widget_schema.json +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/skills/tl-report-builder/references/report_glossary.md +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/skills/tl-report-builder/references/sortable_columns.json +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/skills/tl-report-builder/references/sponsorship_filterset_schema.json +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/skills/tl-report-builder/references/sponsorship_widget_schema.json +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/skills/tl-report-builder/references/widgets.md +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/skills/tl-report-builder/tools/column_builder.md +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/skills/tl-report-builder/tools/database_query.md +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/skills/tl-report-builder/tools/keyword_research.md +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/skills/tl-report-builder/tools/name_resolver.md +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/skills/tl-report-builder/tools/sample_judge.md +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/skills/tl-report-builder/tools/similar_channels.md +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/skills/tl-report-builder/tools/topic_matcher.md +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/skills/tl-report-builder/tools/widget_builder.md +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/src/tl_cli/_completions.py +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/src/tl_cli/auth/__init__.py +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/src/tl_cli/auth/login.py +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/src/tl_cli/auth/pkce.py +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/src/tl_cli/auth/token_store.py +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/src/tl_cli/client/__init__.py +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/src/tl_cli/client/http.py +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/src/tl_cli/commands/__init__.py +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/src/tl_cli/commands/_comments_common.py +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/src/tl_cli/commands/ask.py +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/src/tl_cli/commands/brands.py +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/src/tl_cli/commands/bulk_import.py +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/src/tl_cli/commands/changelog.py +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/src/tl_cli/commands/channels.py +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/src/tl_cli/commands/db.py +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/src/tl_cli/commands/deals.py +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/src/tl_cli/commands/describe.py +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/src/tl_cli/commands/doctor.py +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/src/tl_cli/commands/matches.py +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/src/tl_cli/commands/proposals.py +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/src/tl_cli/commands/recommender.py +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/src/tl_cli/commands/schema.py +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/src/tl_cli/commands/setup.py +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/src/tl_cli/commands/snapshots.py +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/src/tl_cli/commands/sponsorships.py +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/src/tl_cli/commands/uploads.py +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/src/tl_cli/config.py +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/src/tl_cli/filters.py +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/src/tl_cli/hints.py +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/src/tl_cli/output/__init__.py +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/src/tl_cli/output/formatter.py +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/tests/__init__.py +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/tests/test_auth.py +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/tests/test_filters.py +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/tests/test_output.py +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/tests/test_sponsorships.py +0 -0
- {thoughtleaders_cli-0.6.25 → thoughtleaders_cli-0.6.27}/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.27
|
|
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
|
|
@@ -92,7 +92,7 @@ Other key concepts:
|
|
|
92
92
|
- **`purchase_date`** — when the sponsorship was purchased (i.e. when the deal was made); These make up bookings.
|
|
93
93
|
- **`send_date`** — the date the video is/was expected to be published (scheduled)
|
|
94
94
|
- **`publish_date`** — the date the video was actually published; These make up live ads.
|
|
95
|
-
- **Credits** — every data query costs credits; use `tl describe` to see rates
|
|
95
|
+
- **Credits** — every data query costs credits; use `tl describe` to see rates. Top up with `tl credits buy --amount-usd N` (free; opens a browser checkout). New accounts get a starter balance on first `tl auth login`; the rate is shown by `tl credits pricing`.
|
|
96
96
|
|
|
97
97
|
Users see data scoped by their organization and plan:
|
|
98
98
|
- **Media buyers** see sponsorships where their org is the brand. They see `price` but never `cost`.
|
|
@@ -345,7 +345,10 @@ tl schema pg <table> # PostgreSQL schema for a SINGLE table (f
|
|
|
345
345
|
tl schema fb # Live Firebolt tables and column types for `tl db fb` (free) — both tables
|
|
346
346
|
tl schema fb <table> # Firebolt schema for a SINGLE table (free) — `article_metrics` or `channel_metrics`
|
|
347
347
|
tl schema es # Elasticsearch document shape for `tl db es` (free)
|
|
348
|
-
tl balance --json # Credit balance (free)
|
|
348
|
+
tl balance --json # Credit balance + recent usage (free)
|
|
349
|
+
tl credits pricing # Current usd-per-credit rate (free, no auth)
|
|
350
|
+
tl credits buy --amount-usd 10 # Start a top-up; opens browser checkout (free)
|
|
351
|
+
tl credits history # Recent top-ups for the caller's org (free)
|
|
349
352
|
tl whoami # Current user, org, brands (free)
|
|
350
353
|
tl auth status # Auth check (free)
|
|
351
354
|
tl changelog # Release notes — current version, or current..latest if behind (free)
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tl-import
|
|
3
|
+
description: Import a list of channels, brands, uploads (videos), or sponsorships into a ThoughtLeaders report — either an existing report (caller supplies `campaign_id` or a TL report URL) or a fresh new one (skill creates a minimal container, then populates). Superuser-only. **Trigger on explicit intent to import the listed entities into a report**, NOT on the mere presence of a list (a user can paste a list and want analysis, comparison, or similar-channel discovery — those go to `tl-cli:tl-report-builder` or `tl-cli:tl`). The deciding question is: *would the user be satisfied if those exact entities ended up as the report's contents, no transformation?* If yes, this is the skill. Phrasings: "import these channels into report 1234", "add brands to campaign 5678", "exclude these channels from report Z", "bulk-add these videos to report X", "create a new report with these channels: <list>", "make a campaign containing these brands: <list>".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# tl-import
|
|
7
|
+
|
|
8
|
+
Imports a list of identifiers (channels / brands / articles / sponsorships) into a report. Two flows depending on whether the user references an existing report or wants a new one — see "Decide which flow" below. Both end in the same step: `tl bulk-import` submits the identifiers, polls until done, and the skill renders a per-row result table.
|
|
9
|
+
|
|
10
|
+
## When to use
|
|
11
|
+
|
|
12
|
+
The deciding test is the **user's intent**, not just what they pasted. The user must want the listed entities to land in a report as-given — no filtering, no analysis, no similarity expansion on top.
|
|
13
|
+
|
|
14
|
+
Trigger on:
|
|
15
|
+
|
|
16
|
+
- "Import @mkbhd, @veritasium into report 1234" → **existing-report flow**
|
|
17
|
+
- "Add these brands to campaign 5678" → **existing-report flow**
|
|
18
|
+
- "Bulk-add this list of channels to report 999" → **existing-report flow**
|
|
19
|
+
- "Exclude these channels from report Z" → **existing-report flow**
|
|
20
|
+
- "Create a new report with these channels: \<list\>" → **new-report flow**
|
|
21
|
+
- "Make a campaign containing these brands: \<list\>" → **new-report flow**
|
|
22
|
+
- "Build me a report from these adlinks: \<list\>" → **new-report flow** *(the verb "build" doesn't matter — what matters is that the user wants exactly those adlinks in the report.)*
|
|
23
|
+
|
|
24
|
+
**Do NOT trigger** when the user pastes a list but wants something other than direct import — those belong to `tl-cli:tl-report-builder` or `tl-cli:tl`:
|
|
25
|
+
|
|
26
|
+
- *"Find me channels similar to these: \<list\>"* — discovery using the list as a seed, not as the answer.
|
|
27
|
+
- *"Build a report of TPP channels in the same niche as these: \<list\>"* — discovery with filters and similarity expansion.
|
|
28
|
+
- *"Compare engagement across these channels"* — analysis on top of the list.
|
|
29
|
+
- *"Show me which of these have sponsored fintech brands"* — filtered lookup.
|
|
30
|
+
|
|
31
|
+
If you're about to do anything beyond "put these exact entities into a report", the wrong skill is running.
|
|
32
|
+
|
|
33
|
+
Single-identifier requests still work for the import intent (the command accepts one). The reason to keep this skill separate from other report-edit flows: it's the only path that auto-creates channels from YouTube URLs / handles, and brands from website domains.
|
|
34
|
+
|
|
35
|
+
## Decide which flow
|
|
36
|
+
|
|
37
|
+
Look at the user's request and pick exactly one of three responses:
|
|
38
|
+
|
|
39
|
+
| Signal | Response |
|
|
40
|
+
|---|---|
|
|
41
|
+
| User references an existing report (campaign ID number, `?campaign=<id>` in a pasted URL, "report X", "this campaign") | **Existing-report flow** — skip to "Inputs to gather" |
|
|
42
|
+
| User explicitly asks for a new report ("new report", "a new campaign", "create a report with…", "make a campaign of…") | **New-report flow** — read "Create a fresh container first" below, then continue |
|
|
43
|
+
| User provides a list with no destination cue at all (no campaign reference AND no "new" wording) | **Ambiguous — ask once** before proceeding: *"Should I add these to an existing report (give me the report ID or URL), or create a new one?"* Wait for the answer. Then dispatch to the matching flow above. |
|
|
44
|
+
|
|
45
|
+
Never silently create a new report when the destination is ambiguous; never silently use an existing report when none was referenced. The skill's only acceptable action without a clear destination is to ask.
|
|
46
|
+
|
|
47
|
+
## Create a fresh container first (new-report flow only)
|
|
48
|
+
|
|
49
|
+
The user wants the report to contain exactly the identifiers they're about to import — nothing else. No keyword research, no discovery query, no review pipeline. Just a minimal container that holds the list. **The persistence step uses the same primitive `tl-cli:tl-report-builder` calls at the end of its workflow** (`tl reports create --config-file`), but with a tiny config and none of the upstream phases.
|
|
50
|
+
|
|
51
|
+
Steps:
|
|
52
|
+
|
|
53
|
+
1. **Title.** If the user gave one (e.g. *"create a Q1 cohort report with…"* → title *"Q1 cohort"*), use it. Otherwise ask once: *"What should I name the new report?"* Title must be ≤ 60 chars, non-empty.
|
|
54
|
+
2. **Description.** Auto-generate a 1-sentence description; don't ask the user. Format: `"Bulk-imported list of <N> <entity> (<YYYY-MM-DD>)."`. Required by the platform on save (not optional).
|
|
55
|
+
3. **Map entity → `report_type`:**
|
|
56
|
+
- `channels` → **3** (THOUGHTLEADERS)
|
|
57
|
+
- `brands` → **2** (BRANDS)
|
|
58
|
+
- `articles` (uploads/videos) → **1** (CONTENT)
|
|
59
|
+
- `sponsorships` (adlinks/deals) → **8** (CAMPAIGN_MANAGEMENT)
|
|
60
|
+
4. **Pick default columns.** Read the matching columns reference file in the sibling `tl-report-builder` skill and use its **"always include"** defaults — do NOT duplicate the list inline here, that's where the canonical default lives:
|
|
61
|
+
- channels → `../tl-report-builder/references/columns_channels.md` (look for the "Defaults — always include" section)
|
|
62
|
+
- brands → `../tl-report-builder/references/columns_brands.md`
|
|
63
|
+
- articles → `../tl-report-builder/references/columns_content.md`
|
|
64
|
+
- sponsorships → `../tl-report-builder/references/columns_sponsorships.md`
|
|
65
|
+
|
|
66
|
+
Convert the column display names into the dict shape: `{"Channel": {"display": true}, "TL Channel Summary": {"display": true}, …}`.
|
|
67
|
+
5. **Compose the minimal config:**
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"report_title": "<from step 1>",
|
|
72
|
+
"report_description": "<from step 2>",
|
|
73
|
+
"report_type": <from step 3>,
|
|
74
|
+
"type": 2,
|
|
75
|
+
"filterset": {},
|
|
76
|
+
"columns": <from step 4>
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`type: 2` is DYNAMIC (the only valid campaign type for save). `filterset: {}` is intentional — no keyword/topic/demographic filters; the report's contents will come entirely from the include list bulk-import populates next.
|
|
81
|
+
6. **Persist via the same primitive `tl-report-builder` uses.** Write the config dict to a temp file using your file-writing tool — **do not use shell `echo` or heredocs**, those break on titles containing apostrophes, dollar signs, backticks, etc. The whole point of `--config-file` is to bypass shell quoting entirely. Pick any temp path the agent's filesystem tool can write to (e.g. `/tmp/tl-import-container.json` on Unix, the OS temp dir on Windows).
|
|
82
|
+
|
|
83
|
+
Then run:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
tl reports create --config-file <path-you-just-wrote> --yes --json
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
With `--yes --json` the CLI emits a single JSON document on stdout containing the save response — parse it with one `json.loads()` and pull out `campaign_id` (and `report_url` for the summary). If `tl reports create` returns HTTP 400 with `Missing required field: report_title` or `…report_description`, the config is malformed — re-check step 1/2.
|
|
90
|
+
|
|
91
|
+
7. **Hand off to bulk-import** using the new `campaign_id` as if the user had supplied it. Continue with "Inputs to gather" and the rest of this skill below.
|
|
92
|
+
|
|
93
|
+
Surface the new report URL alongside the bulk-import results in the final summary, e.g. *"Created [Q1 cohort](https://app.thoughtleaders.io/#/thoughtleaders?campaign=23859) and imported 50 channels:"* followed by the per-row table.
|
|
94
|
+
|
|
95
|
+
## Inputs to gather
|
|
96
|
+
|
|
97
|
+
Before running, confirm:
|
|
98
|
+
|
|
99
|
+
1. **Report ID** (`--campaign` / `-c`) — required. If the user pastes a TL URL (e.g. `https://app.thoughtleaders.io/#/thoughtleaders?campaign=23859&...`), the integer after `campaign=` is the ID. In the new-report flow, use the `campaign_id` returned by `tl reports create` above.
|
|
100
|
+
2. **Entity type** — one of `channels` / `brands` / `articles` / `sponsorships`. Infer from context, but translate user-facing vocabulary:
|
|
101
|
+
- YouTube URLs / handles / `UC…` IDs → `channels`
|
|
102
|
+
- Domains / brand slugs → `brands`
|
|
103
|
+
- "videos" / "uploads" / video URLs / video IDs → `articles` *(the CLI calls them uploads in `tl uploads list`, but `bulk-import` expects `articles` — same concept, legacy naming)*
|
|
104
|
+
- "adlinks" / "deals" / "sponsorships" / numeric AdLink IDs → `sponsorships`
|
|
105
|
+
3. **Identifiers** — the list. Accepted shapes per entity:
|
|
106
|
+
- **channels**: numeric DB IDs, YouTube channel IDs (`UC…`), `@handles`, full YouTube URLs (`/@…`, `/channel/UC…`, `/user/…`)
|
|
107
|
+
- **brands**: numeric IDs, slugs, websites / domains (`example.com`)
|
|
108
|
+
- **articles** (uploads): video IDs or video URLs
|
|
109
|
+
- **sponsorships** (adlinks): numeric AdLink IDs only
|
|
110
|
+
4. **Include vs exclude** — default is include (add to the report). Pass `--exclude` only if the user explicitly wants to remove from the report.
|
|
111
|
+
|
|
112
|
+
## How to invoke
|
|
113
|
+
|
|
114
|
+
The command reads identifiers from a file (`--ids-file`) or stdin:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
# small list — stdin
|
|
118
|
+
echo '@mkbhd
|
|
119
|
+
@veritasium
|
|
120
|
+
@lemmino' | tl bulk-import channels --campaign 1234
|
|
121
|
+
|
|
122
|
+
# larger list — file
|
|
123
|
+
tl bulk-import channels --campaign 1234 --ids-file ./channels.txt
|
|
124
|
+
|
|
125
|
+
# exclusion
|
|
126
|
+
tl bulk-import brands --campaign 5678 -f ./brands.txt --exclude
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Short flags: `-c` for `--campaign`, `-f` for `--ids-file`.
|
|
130
|
+
|
|
131
|
+
## Output: the `inputs` envelope
|
|
132
|
+
|
|
133
|
+
`tl bulk-import` returns a JSON envelope. Use the **`inputs`** array as the source of truth for what to render — it has one row per submitted identifier, in input order, with everything you need to classify and display.
|
|
134
|
+
|
|
135
|
+
```json
|
|
136
|
+
{
|
|
137
|
+
"task_id": "...",
|
|
138
|
+
"mode": "include",
|
|
139
|
+
"inputs": [
|
|
140
|
+
{"input": "@mkbhd", "resolved_id": 4587, "reason": "Success", "newly_created": false},
|
|
141
|
+
{"input": "@veritasium", "resolved_id": 1209, "reason": "Duplicate", "newly_created": false},
|
|
142
|
+
{"input": "@OfficialSaharTV", "resolved_id": 1328906, "reason": "Success", "newly_created": true},
|
|
143
|
+
{"input": "https://bad-url", "resolved_id": null, "reason": "Not found", "newly_created": false}
|
|
144
|
+
],
|
|
145
|
+
"success_ids": [4587, 1328906],
|
|
146
|
+
"newly_created_ids": [1328906],
|
|
147
|
+
"failed_ids": [...],
|
|
148
|
+
...
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Each `inputs` row's `input` field echoes the raw identifier the user submitted (unchanged). `resolved_id` is the entity ID it matched/created, or `null` for failures. `reason` and `newly_created` drive the row's display label below.
|
|
153
|
+
|
|
154
|
+
**`mode` echoes back the operation mode** (`"include"` or `"exclude"`). You need this for labelling because the semantics flip:
|
|
155
|
+
|
|
156
|
+
- include + Success = identifier was just added to the report
|
|
157
|
+
- exclude + Success = identifier was just removed from the report
|
|
158
|
+
- include + Duplicate = identifier was already in the report (no-op)
|
|
159
|
+
- exclude + Duplicate = identifier was already excluded (no-op)
|
|
160
|
+
|
|
161
|
+
Don't use `success_ids` / `failed_ids` for display — they lose input mapping and miss the include/exclude direction. `inputs` is the canonical surface.
|
|
162
|
+
|
|
163
|
+
## Classify each row
|
|
164
|
+
|
|
165
|
+
| `reason` | `newly_created` | `mode` | Icon | Label |
|
|
166
|
+
|---|---|---|---|---|
|
|
167
|
+
| `Success` | `true` | `include` | 🆕 | Created in TL |
|
|
168
|
+
| `Success` | `true` | `exclude` | ⚠️ | Created in TL — unexpected for exclude, verify report state |
|
|
169
|
+
| `Success` | `false` | `include` | ✅ | Added |
|
|
170
|
+
| `Success` | `false` | `exclude` | ✂️ | Excluded |
|
|
171
|
+
| `Duplicate` | any | `include` | ↺ | Already in report |
|
|
172
|
+
| `Duplicate` | any | `exclude` | ↺ | Already excluded |
|
|
173
|
+
| `Not found` | any | any | ❌ | Not found |
|
|
174
|
+
| `Cannot parse` | any | any | ❌ | Bad format |
|
|
175
|
+
| `Multiple matches found` | any | any | ❌ | Ambiguous (multiple matches) |
|
|
176
|
+
| `Limit exceeded` | any | any | ❌ | Auto-create cap hit |
|
|
177
|
+
| starts with `Error:` | any | any | ❌ | Error (show reason verbatim) |
|
|
178
|
+
| anything else | any | any | ❌ | Failed (show reason verbatim) |
|
|
179
|
+
|
|
180
|
+
For 🆕 rows: mention that enrichment (subscriber stats, AI description, demographics for channels; metadata for brands) is queued and will populate over the next few minutes — these entities just entered the database.
|
|
181
|
+
|
|
182
|
+
For ⚠️ rows: if an exclude import returns `newly_created: true`, treat it as unexpected. Tell the user the channel was created but does not appear to have been excluded — ask them to verify the report and re-submit the exclude against the returned `resolved_id` if needed.
|
|
183
|
+
|
|
184
|
+
## Display
|
|
185
|
+
|
|
186
|
+
Per-row markdown table. **Headline first** with the gain count, then the table.
|
|
187
|
+
|
|
188
|
+
For include mode:
|
|
189
|
+
|
|
190
|
+
```markdown
|
|
191
|
+
**Bulk-import to report 23859 — done.** Report gained **2** rows; **1** was already there; **1** failed.
|
|
192
|
+
|
|
193
|
+
| # | Status | Input | ID | Reason |
|
|
194
|
+
|---|---|---|---|---|
|
|
195
|
+
| 1 | ✅ Added | `@mkbhd` | 4587 | Success |
|
|
196
|
+
| 2 | ↺ Already in report | `@veritasium` | 1209 | Duplicate |
|
|
197
|
+
| 3 | 🆕 Created in TL | `@OfficialSaharTV` | 1328906 | Success — enrichment queued |
|
|
198
|
+
| 4 | ❌ Not found | `https://bad-url` | — | Not found |
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
For exclude mode, headline uses "lost" wording:
|
|
202
|
+
|
|
203
|
+
```markdown
|
|
204
|
+
**Bulk-import (exclude) to report 23859 — done.** Report lost **N** rows; **M** were already excluded.
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Display rules:
|
|
208
|
+
|
|
209
|
+
- **Use the user's raw `input` value** in the Input column (it's `inputs[i].input` — the raw submitted string, unchanged).
|
|
210
|
+
- **Omit any column that's uniformly empty** — for sponsorships, the "Input" and "ID" columns are usually identical (both numeric); the Reason column carries the signal.
|
|
211
|
+
- **Small imports (≤30 rows):** render the full table.
|
|
212
|
+
- **Large imports (>30 rows):** lead with a summary table of bucket counts; render the per-row table only for **non-bulk-success rows** — i.e. omit the dominant happy-path bucket, which is ✅ Added in include mode and ✂️ Excluded in exclude mode. The rows the user cares about (already-present, newly-created, failed, unexpected) all stay. Offer to dump the omitted rows on request.
|
|
213
|
+
|
|
214
|
+
Summary table (include mode example):
|
|
215
|
+
|
|
216
|
+
```markdown
|
|
217
|
+
| Bucket | Count |
|
|
218
|
+
|---|---|
|
|
219
|
+
| ✅ Added | 142 |
|
|
220
|
+
| ↺ Already in report | 7 |
|
|
221
|
+
| 🆕 Created in TL | 3 |
|
|
222
|
+
| ❌ Failed | 2 |
|
|
223
|
+
| **Total submitted** | **154** |
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Summary table (exclude mode example):
|
|
227
|
+
|
|
228
|
+
```markdown
|
|
229
|
+
| Bucket | Count |
|
|
230
|
+
|---|---|
|
|
231
|
+
| ✂️ Excluded | 142 |
|
|
232
|
+
| ↺ Already excluded | 7 |
|
|
233
|
+
| ❌ Failed | 2 |
|
|
234
|
+
| **Total submitted** | **151** |
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
- **Never look up entity names** with extra `tl channels show <id>` / `tl brands show <id>` calls just to populate a "Name" column — those are metered. The user's input column is enough for them to identify each row. If the user explicitly asks "what are these channels called?", then look them up.
|
|
238
|
+
|
|
239
|
+
## Errors at the command level (before the per-row results)
|
|
240
|
+
|
|
241
|
+
These are envelope-level failures, distinct from per-row `reason` values:
|
|
242
|
+
|
|
243
|
+
- **403** → caller isn't a superuser. Stop and tell the user; this command is gated.
|
|
244
|
+
- **400** → bad input shape (missing field, unknown entity, all-empty identifiers). Show the `detail` verbatim.
|
|
245
|
+
- **402** → out of credits. Tell the user to top up.
|
|
246
|
+
- **Connection failed** → transient network issue. Retry once; if it persists, surface to the user.
|
|
247
|
+
|
|
248
|
+
## What this skill does NOT do
|
|
249
|
+
|
|
250
|
+
- Doesn't run `tl-report-builder`'s discovery pipeline (keyword research, topic matching, validation cycles, review). When a user gives a fixed list of identifiers, they've already done the discovery themselves — the report is a container for their list, not a query result. Use `tl-report-builder` only when the user wants you to *find* channels/brands/etc. by criteria.
|
|
251
|
+
- Doesn't change existing report metadata (title, description, columns, filters) after creation. For that, use the platform UI or a dedicated edit flow. The new-report flow in this skill sets minimum-required metadata once at creation and never revisits it.
|
|
252
|
+
- Doesn't validate identifiers ahead of time — submit and let the per-row `reason` tell the user which ones failed. Pre-checking with `tl channels show` / etc. is wasteful (metered) and adds latency.
|
|
253
|
+
- Doesn't sweep duplicates from the user's input list — submit them as-is. The response will mark the second occurrence as `Duplicate`, which is more informative than silently deduping.
|
|
@@ -13,7 +13,15 @@ description: |
|
|
|
13
13
|
|
|
14
14
|
Save-intent variants ("save a campaign of …", "create the report …", "make a TL report for …") trigger auto-save; everything else previews. Off-taxonomy keywords ("crypto / Web3"), brand-exclusion logic ("not pitched to X"), demographic floors ("US audience ≥30%"), TPP/MSN scoping, and competitive-pitch shapes are all this skill's job — not the general `tl-cli:tl` data-analyst skill.
|
|
15
15
|
|
|
16
|
-
**Skip this skill**
|
|
16
|
+
**Skip this skill** for:
|
|
17
|
+
- counts, metrics, trends, single-record show-by-ID lookups, raw exploratory queries, or analytical questions that aren't shaped as "give me a list" → route to `tl-cli:tl`.
|
|
18
|
+
- **explicit intent to import a list of identifiers into a report — existing or new.** The routing test is the **user's import intent**, NOT the mere presence of a list. A user can paste 50 channel URLs and want analysis, comparison, similar-channel discovery, or filtered lookup — those still belong here (or in `tl-cli:tl`), not in tl-import. They can also paste 50 URLs and want exactly those channels to land in a report as-given — that is import, route to `tl-cli:tl-import`. The deciding question: *"Would the user be satisfied if the listed entities simply ended up as the report's contents exactly as-given, no transformation?"* If yes → import intent → `tl-cli:tl-import`. If they expect filtering, analysis, similarity expansion, or any other transformation on top of the list → it's not import, keep it here.
|
|
19
|
+
|
|
20
|
+
Concrete phrasings that route to `tl-cli:tl-import` (intent: import + list = report contents): *"import these channels into report 1234"*, *"add these brands to campaign 5678"*, *"create a new report with these channels: <list>"*, *"build me a campaign from these adlinks: <list>"*, *"make a report containing these uploads: <list>"*.
|
|
21
|
+
|
|
22
|
+
Phrasings that **stay here** even with a list attached (intent: discovery / analysis using the list as input, not as the answer): *"find me channels similar to these: <list>"*, *"build a report of TPP channels in the same niche as these: <list>"*, *"show me which of these have sponsored fintech brands"*, *"compare engagement across these channels"*.
|
|
23
|
+
|
|
24
|
+
If you find yourself about to resolve a URL/handle to a channel ID *as the deliverable* (no analysis, no filtering, no discovery on top), stop and hand off — that's the import shape.
|
|
17
25
|
---
|
|
18
26
|
|
|
19
27
|
# TL Report Builder Skill
|
|
@@ -4,6 +4,7 @@ import typer
|
|
|
4
4
|
from rich.console import Console
|
|
5
5
|
from rich.prompt import Prompt
|
|
6
6
|
|
|
7
|
+
from tl_cli.auth.finalize import finalize_signup
|
|
7
8
|
from tl_cli.auth.login import login_browser, login_device_code
|
|
8
9
|
from tl_cli.auth.token_store import clear_tokens, load_tokens
|
|
9
10
|
|
|
@@ -13,7 +14,12 @@ console = Console(stderr=True)
|
|
|
13
14
|
|
|
14
15
|
@app.command("login", help="Log in to ThoughtLeaders.")
|
|
15
16
|
def login_cmd() -> None:
|
|
16
|
-
"""Log in to ThoughtLeaders.
|
|
17
|
+
"""Log in to ThoughtLeaders.
|
|
18
|
+
|
|
19
|
+
After Auth0 returns, the CLI calls the server's finalize endpoint.
|
|
20
|
+
First-time users are prompted for a persona (Media Buyer or Creator)
|
|
21
|
+
and the account + starter credits are created on the server side.
|
|
22
|
+
"""
|
|
17
23
|
console.print("[bold]How would you like to authenticate?[/bold]")
|
|
18
24
|
console.print(" [cyan]1[/cyan] — Browser on this machine (default)")
|
|
19
25
|
console.print(" [cyan]2[/cyan] — Device code (use a browser on another device)")
|
|
@@ -25,6 +31,8 @@ def login_cmd() -> None:
|
|
|
25
31
|
else:
|
|
26
32
|
login_browser()
|
|
27
33
|
|
|
34
|
+
finalize_signup()
|
|
35
|
+
|
|
28
36
|
|
|
29
37
|
@app.command("logout")
|
|
30
38
|
def logout_cmd() -> None:
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Server-side signup finalize, called once per login.
|
|
2
|
+
|
|
3
|
+
After Auth0 returns a valid token the CLI asks the server whether an
|
|
4
|
+
account exists for the email. If yes, this is a no-op. If no, the CLI
|
|
5
|
+
prompts for a persona (Media Buyer or Creator) and POSTs it back; the
|
|
6
|
+
server creates the User, Organization, Profile and CreditAccount.
|
|
7
|
+
|
|
8
|
+
Errors here never abort login — the user can always retry the prompt
|
|
9
|
+
on the next call. We do print a clear message so it's obvious whether
|
|
10
|
+
the account is fully set up.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import typer
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
from rich.prompt import Prompt
|
|
18
|
+
|
|
19
|
+
from tl_cli.client.errors import ApiError
|
|
20
|
+
from tl_cli.client.http import get_client
|
|
21
|
+
|
|
22
|
+
console = Console(stderr=True)
|
|
23
|
+
|
|
24
|
+
PERSONA_LABEL_TO_KEY = {
|
|
25
|
+
"Media Buyer": "media_buyer",
|
|
26
|
+
"Creator": "creator",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def finalize_signup() -> None:
|
|
31
|
+
"""POST /auth/finalize, prompting for persona if the server asks for one."""
|
|
32
|
+
client = get_client()
|
|
33
|
+
try:
|
|
34
|
+
# First call: no body. Server tells us whether persona is required.
|
|
35
|
+
try:
|
|
36
|
+
result = client.post("/auth/finalize", {})
|
|
37
|
+
except ApiError as exc:
|
|
38
|
+
if exc.status_code == 400 and isinstance(exc.raw, dict) and exc.raw.get("code") == "persona_required":
|
|
39
|
+
result = _prompt_and_finalize(client, exc.raw.get("allowed_personas") or [])
|
|
40
|
+
elif exc.status_code == 404:
|
|
41
|
+
# Server predates this endpoint — silently skip; legacy
|
|
42
|
+
# accounts already exist and don't need provisioning.
|
|
43
|
+
return
|
|
44
|
+
else:
|
|
45
|
+
console.print(f"[yellow]Could not finalize signup: {exc.detail}[/yellow]")
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
if result.get("created"):
|
|
49
|
+
org = result.get("organization", {})
|
|
50
|
+
console.print(
|
|
51
|
+
f"[green]Account created for {org.get('name', 'your organization')}.[/green] "
|
|
52
|
+
"Run [bold]tl balance[/bold] to see your starter credits."
|
|
53
|
+
)
|
|
54
|
+
finally:
|
|
55
|
+
client.close()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _prompt_and_finalize(client, allowed: list[str]) -> dict:
|
|
59
|
+
"""Prompt the user for a persona, then retry /auth/finalize."""
|
|
60
|
+
console.print()
|
|
61
|
+
console.print("[bold]Welcome to ThoughtLeaders![/bold] We need one more detail to set up your account.")
|
|
62
|
+
console.print(" [cyan]1[/cyan] — Media Buyer (brands and agencies buying sponsorships)")
|
|
63
|
+
console.print(" [cyan]2[/cyan] — Creator (channels selling sponsorships)")
|
|
64
|
+
|
|
65
|
+
persona_key: str | None = None
|
|
66
|
+
while persona_key is None:
|
|
67
|
+
choice = Prompt.ask("I am a", choices=["1", "2"], default="1", console=console)
|
|
68
|
+
candidate = "media_buyer" if choice == "1" else "creator"
|
|
69
|
+
if allowed and candidate not in allowed:
|
|
70
|
+
console.print(f"[yellow]Server rejects persona '{candidate}'. Allowed: {', '.join(allowed)}.[/yellow]")
|
|
71
|
+
continue
|
|
72
|
+
persona_key = candidate
|
|
73
|
+
|
|
74
|
+
org_name = Prompt.ask(
|
|
75
|
+
"Organization name (optional, leave blank to use your email)",
|
|
76
|
+
default="",
|
|
77
|
+
console=console,
|
|
78
|
+
).strip()
|
|
79
|
+
|
|
80
|
+
body = {"persona": persona_key}
|
|
81
|
+
if org_name:
|
|
82
|
+
body["organization_name"] = org_name
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
return client.post("/auth/finalize", body)
|
|
86
|
+
except ApiError as exc:
|
|
87
|
+
console.print(f"[red]Signup failed:[/red] {exc.detail}")
|
|
88
|
+
raise typer.Exit(1)
|
|
@@ -46,7 +46,8 @@ def handle_api_error(error: ApiError) -> None:
|
|
|
46
46
|
sys.exit(2)
|
|
47
47
|
elif error.status_code == 402:
|
|
48
48
|
err.print("[red]Insufficient credits.[/red]")
|
|
49
|
-
err.print("
|
|
49
|
+
err.print("Top up with: [bold]tl credits buy --amount-usd 10[/bold]")
|
|
50
|
+
err.print("Or visit: https://app.thoughtleaders.io/billing/cli")
|
|
50
51
|
_print_debug(error)
|
|
51
52
|
sys.exit(4)
|
|
52
53
|
elif error.status_code == 403:
|
|
@@ -46,6 +46,19 @@ def balance(
|
|
|
46
46
|
if allow_overage:
|
|
47
47
|
console.print("[dim]Overage: enabled[/dim]")
|
|
48
48
|
|
|
49
|
+
# Top-up hint when running low. Threshold matches the hook warning
|
|
50
|
+
# that nudges the user before they hit 0.
|
|
51
|
+
try:
|
|
52
|
+
balance_decimal = float(balance_val)
|
|
53
|
+
except (TypeError, ValueError):
|
|
54
|
+
balance_decimal = None
|
|
55
|
+
if balance_decimal is not None and balance_decimal < 500:
|
|
56
|
+
console.print(
|
|
57
|
+
"[yellow]Running low.[/yellow] Top up with: "
|
|
58
|
+
"[bold]tl credits buy --amount-usd 10[/bold] "
|
|
59
|
+
"(or https://app.thoughtleaders.io/billing/cli)"
|
|
60
|
+
)
|
|
61
|
+
|
|
49
62
|
recent = data.get("recent_usage", [])
|
|
50
63
|
if recent:
|
|
51
64
|
table = Table(title="Recent Usage")
|