thoughtleaders-cli 0.6.26__tar.gz → 0.6.28__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.26 → thoughtleaders_cli-0.6.28}/.claude-plugin/plugin.json +1 -1
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/PKG-INFO +1 -1
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/pyproject.toml +1 -1
- thoughtleaders_cli-0.6.28/skills/tl-import/SKILL.md +288 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/skills/tl-report-builder/SKILL.md +9 -1
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/src/tl_cli/__init__.py +1 -1
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/src/tl_cli/commands/reports.py +6 -1
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/tests/test_reports.py +27 -0
- thoughtleaders_cli-0.6.26/skills/tl-import/SKILL.md +0 -180
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/.claude-plugin/marketplace.json +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/.github/workflows/python-publish.yml +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/.gitignore +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/AGENTS.md +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/CLAUDE.md +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/LICENSE +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/README.md +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/agents/tl-analyst.md +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/commands/tl-balance.md +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/commands/tl-reports.md +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/commands/tl-sponsorships.md +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/commands/tl.md +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/docs/architecture.md +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/hooks/hooks.json +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/hooks/scripts/post-usage.sh +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/hooks/scripts/pre-check.sh +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/skills/tl/SKILL.md +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/skills/tl/references/business-glossary.md +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/skills/tl/references/elasticsearch-schema.md +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/skills/tl/references/firebolt-schema.md +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/skills/tl/references/postgres-schema.md +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/skills/tl-report-builder/examples/e2e_findings.md +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/skills/tl-report-builder/examples/golden_queries.md +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/skills/tl-report-builder/references/columns_brands.md +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/skills/tl-report-builder/references/columns_channels.md +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/skills/tl-report-builder/references/columns_content.md +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/skills/tl-report-builder/references/columns_sponsorships.md +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/skills/tl-report-builder/references/intelligence_filterset_schema.json +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/skills/tl-report-builder/references/intelligence_widget_schema.json +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/skills/tl-report-builder/references/report_glossary.md +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/skills/tl-report-builder/references/sortable_columns.json +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/skills/tl-report-builder/references/sponsorship_filterset_schema.json +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/skills/tl-report-builder/references/sponsorship_widget_schema.json +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/skills/tl-report-builder/references/widgets.md +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/skills/tl-report-builder/tools/column_builder.md +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/skills/tl-report-builder/tools/database_query.md +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/skills/tl-report-builder/tools/keyword_research.md +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/skills/tl-report-builder/tools/name_resolver.md +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/skills/tl-report-builder/tools/sample_judge.md +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/skills/tl-report-builder/tools/similar_channels.md +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/skills/tl-report-builder/tools/topic_matcher.md +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/skills/tl-report-builder/tools/widget_builder.md +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/src/tl_cli/_completions.py +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/src/tl_cli/auth/__init__.py +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/src/tl_cli/auth/commands.py +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/src/tl_cli/auth/finalize.py +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/src/tl_cli/auth/login.py +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/src/tl_cli/auth/pkce.py +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/src/tl_cli/auth/token_store.py +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/src/tl_cli/client/__init__.py +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/src/tl_cli/client/errors.py +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/src/tl_cli/client/http.py +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/src/tl_cli/commands/__init__.py +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/src/tl_cli/commands/_comments_common.py +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/src/tl_cli/commands/ask.py +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/src/tl_cli/commands/balance.py +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/src/tl_cli/commands/brands.py +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/src/tl_cli/commands/bulk_import.py +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/src/tl_cli/commands/changelog.py +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/src/tl_cli/commands/channels.py +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/src/tl_cli/commands/credits.py +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/src/tl_cli/commands/db.py +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/src/tl_cli/commands/deals.py +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/src/tl_cli/commands/describe.py +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/src/tl_cli/commands/doctor.py +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/src/tl_cli/commands/matches.py +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/src/tl_cli/commands/proposals.py +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/src/tl_cli/commands/recommender.py +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/src/tl_cli/commands/schema.py +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/src/tl_cli/commands/setup.py +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/src/tl_cli/commands/snapshots.py +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/src/tl_cli/commands/sponsorships.py +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/src/tl_cli/commands/uploads.py +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/src/tl_cli/commands/whoami.py +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/src/tl_cli/config.py +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/src/tl_cli/filters.py +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/src/tl_cli/hints.py +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/src/tl_cli/main.py +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/src/tl_cli/output/__init__.py +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/src/tl_cli/output/formatter.py +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/src/tl_cli/self_update.py +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/tests/__init__.py +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/tests/test_auth.py +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/tests/test_filters.py +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/tests/test_output.py +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/tests/test_sponsorships.py +0 -0
- {thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/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.28
|
|
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
|
|
@@ -0,0 +1,288 @@
|
|
|
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 **"Defaults — always include"** section — that's where the canonical column list lives per type; do NOT restate it here. The four files:
|
|
61
|
+
- channels → `../tl-report-builder/references/columns_channels.md`
|
|
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 each display name from the "Defaults — always include" list into a column entry shape **`{"display": true, "width": "default"}`** — the `width` field is required by the dashboard's column renderer; without it, columns sometimes resolve but cells render empty. Use `"wide"` for narrative columns (e.g. `TL Channel Summary`, `Channel Description`, `Topic Descriptions`); use `"narrow"` for short numeric columns (e.g. `Status`, `Country`); `"default"` everywhere else is safe.
|
|
67
|
+
|
|
68
|
+
5. **Pick `dataset_structure`.** This block tells the dashboard's data plane how to query each row's cell values. **Without it the report saves but rows render empty** — see the bottom-of-section sanity check. Shape:
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
"dataset_structure": {
|
|
72
|
+
"report_type": <same as the top-level report_type>,
|
|
73
|
+
"page_size": 50,
|
|
74
|
+
"sort": "<backend_code field, optionally -prefixed for descending>"
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Per-type default sort. **Critical invariant:** the `sort` field must reference a `backend_code` whose display-name column is in the column set you emitted in step 4. The dashboard's renderer rejects sorts pointing at columns that aren't present in the report. So pick the intersection of (a) the type's "Defaults — always include" columns from `columns_<type>.md` and (b) sortable columns from `../tl-report-builder/references/sortable_columns.json`:
|
|
79
|
+
|
|
80
|
+
| report_type | entity | default `sort` | maps to (must be in column set) |
|
|
81
|
+
|---|---|---|---|
|
|
82
|
+
| 3 | channels | `-reach` | `Subscribers` (in defaults) |
|
|
83
|
+
| 2 | brands | `-doc_count` | `Mentions` (in defaults) |
|
|
84
|
+
| 1 | articles | `-publication_date` | `Date` (in defaults) |
|
|
85
|
+
| 8 | sponsorships | `-send_date` | `Scheduled Date` (in defaults) |
|
|
86
|
+
|
|
87
|
+
If the user explicitly asked for a different sort, honor that — but if their preferred sort column isn't in the type's defaults, **add that column to the column set in step 4** before emitting the config. Sort pointing at an absent column re-creates the original render-failure bug.
|
|
88
|
+
|
|
89
|
+
6. **Compose the minimal config:**
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"report_title": "<from step 1>",
|
|
94
|
+
"report_description": "<from step 2>",
|
|
95
|
+
"report_type": <from step 3>,
|
|
96
|
+
"type": 2,
|
|
97
|
+
"filterset": {},
|
|
98
|
+
"columns": <from step 4>,
|
|
99
|
+
"dataset_structure": <from step 5>
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`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. **`dataset_structure` is what makes the rows render with actual values** — leave it out and the dashboard shows row numbers but blank cells.
|
|
104
|
+
7. **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).
|
|
105
|
+
|
|
106
|
+
Then run:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
tl reports create --config-file <path-you-just-wrote> --yes --json
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
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.
|
|
113
|
+
|
|
114
|
+
8. **Run bulk-import, capture the result — but DO NOT render the success summary yet.** Hand off to the bulk-import path with the new `campaign_id` and execute "Inputs to gather" + the bulk-import call + the JSON-envelope parse. **Stop before** rendering the per-row classification table or any "import done" message. Step 9 below must run first; only then do you render the summary. If you find yourself about to emit the success markdown straight out of the bulk-import flow, stop — you skipped step 9.
|
|
115
|
+
|
|
116
|
+
9. **Post-import render check (must execute before any success summary is emitted).** The save accepts the config; the renderer can still drop columns silently (e.g. `sort` points at a column you didn't emit, or `width` is missing on entries that needed it). Run:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
tl reports run <campaign_id> --limit 3 --json
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
- If `results` is non-empty AND each row has fields beyond just an ID → render works. Now surface the bulk-import success summary (headline + per-row classification table) plus the new report URL.
|
|
123
|
+
- If `results` is non-empty but each row is mostly null/empty fields → the config has a render bug. Surface to the user **instead of** the normal success message: *"The bulk-import succeeded but the report is rendering with empty cells. Add columns via the dashboard UI, or delete and re-run the import."* Don't hide this — the import already happened; the user needs to know they have a partially-broken report. Still include the bulk-import's `inputs` classification table so they see what landed.
|
|
124
|
+
- If `results` is unexpectedly empty (shouldn't happen post-bulk-import unless every row failed) → surface the bulk-import's `inputs` table to explain which rows failed; skip the "Created [report] and imported N" headline since N=0.
|
|
125
|
+
|
|
126
|
+
*Cost: a small report-run credit. Worth it — silently handing the user a report whose cells are blank is worse than telling them upfront.*
|
|
127
|
+
|
|
128
|
+
Once step 9 passes, 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.
|
|
129
|
+
|
|
130
|
+
## Inputs to gather
|
|
131
|
+
|
|
132
|
+
Before running, confirm:
|
|
133
|
+
|
|
134
|
+
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.
|
|
135
|
+
2. **Entity type** — one of `channels` / `brands` / `articles` / `sponsorships`. Infer from context, but translate user-facing vocabulary:
|
|
136
|
+
- YouTube URLs / handles / `UC…` IDs → `channels`
|
|
137
|
+
- Domains / brand slugs → `brands`
|
|
138
|
+
- "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)*
|
|
139
|
+
- "adlinks" / "deals" / "sponsorships" / numeric AdLink IDs → `sponsorships`
|
|
140
|
+
3. **Identifiers** — the list. Accepted shapes per entity:
|
|
141
|
+
- **channels**: numeric DB IDs, YouTube channel IDs (`UC…`), `@handles`, full YouTube URLs (`/@…`, `/channel/UC…`, `/user/…`)
|
|
142
|
+
- **brands**: numeric IDs, slugs, websites / domains (`example.com`)
|
|
143
|
+
- **articles** (uploads): video IDs or video URLs
|
|
144
|
+
- **sponsorships** (adlinks): numeric AdLink IDs only
|
|
145
|
+
4. **Include vs exclude** — default is include (add to the report). Pass `--exclude` only if the user explicitly wants to remove from the report.
|
|
146
|
+
|
|
147
|
+
## How to invoke
|
|
148
|
+
|
|
149
|
+
The command reads identifiers from a file (`--ids-file`) or stdin:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
# small list — stdin
|
|
153
|
+
echo '@mkbhd
|
|
154
|
+
@veritasium
|
|
155
|
+
@lemmino' | tl bulk-import channels --campaign 1234
|
|
156
|
+
|
|
157
|
+
# larger list — file
|
|
158
|
+
tl bulk-import channels --campaign 1234 --ids-file ./channels.txt
|
|
159
|
+
|
|
160
|
+
# exclusion
|
|
161
|
+
tl bulk-import brands --campaign 5678 -f ./brands.txt --exclude
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Short flags: `-c` for `--campaign`, `-f` for `--ids-file`.
|
|
165
|
+
|
|
166
|
+
## Output: the `inputs` envelope
|
|
167
|
+
|
|
168
|
+
`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.
|
|
169
|
+
|
|
170
|
+
```json
|
|
171
|
+
{
|
|
172
|
+
"task_id": "...",
|
|
173
|
+
"mode": "include",
|
|
174
|
+
"inputs": [
|
|
175
|
+
{"input": "@mkbhd", "resolved_id": 4587, "reason": "Success", "newly_created": false},
|
|
176
|
+
{"input": "@veritasium", "resolved_id": 1209, "reason": "Duplicate", "newly_created": false},
|
|
177
|
+
{"input": "@OfficialSaharTV", "resolved_id": 1328906, "reason": "Success", "newly_created": true},
|
|
178
|
+
{"input": "https://bad-url", "resolved_id": null, "reason": "Not found", "newly_created": false}
|
|
179
|
+
],
|
|
180
|
+
"success_ids": [4587, 1328906],
|
|
181
|
+
"newly_created_ids": [1328906],
|
|
182
|
+
"failed_ids": [...],
|
|
183
|
+
...
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
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.
|
|
188
|
+
|
|
189
|
+
**`mode` echoes back the operation mode** (`"include"` or `"exclude"`). You need this for labelling because the semantics flip:
|
|
190
|
+
|
|
191
|
+
- include + Success = identifier was just added to the report
|
|
192
|
+
- exclude + Success = identifier was just removed from the report
|
|
193
|
+
- include + Duplicate = identifier was already in the report (no-op)
|
|
194
|
+
- exclude + Duplicate = identifier was already excluded (no-op)
|
|
195
|
+
|
|
196
|
+
Don't use `success_ids` / `failed_ids` for display — they lose input mapping and miss the include/exclude direction. `inputs` is the canonical surface.
|
|
197
|
+
|
|
198
|
+
## Classify each row
|
|
199
|
+
|
|
200
|
+
| `reason` | `newly_created` | `mode` | Icon | Label |
|
|
201
|
+
|---|---|---|---|---|
|
|
202
|
+
| `Success` | `true` | `include` | 🆕 | Created in TL |
|
|
203
|
+
| `Success` | `true` | `exclude` | ⚠️ | Created in TL — unexpected for exclude, verify report state |
|
|
204
|
+
| `Success` | `false` | `include` | ✅ | Added |
|
|
205
|
+
| `Success` | `false` | `exclude` | ✂️ | Excluded |
|
|
206
|
+
| `Duplicate` | any | `include` | ↺ | Already in report |
|
|
207
|
+
| `Duplicate` | any | `exclude` | ↺ | Already excluded |
|
|
208
|
+
| `Not found` | any | any | ❌ | Not found |
|
|
209
|
+
| `Cannot parse` | any | any | ❌ | Bad format |
|
|
210
|
+
| `Multiple matches found` | any | any | ❌ | Ambiguous (multiple matches) |
|
|
211
|
+
| `Limit exceeded` | any | any | ❌ | Auto-create cap hit |
|
|
212
|
+
| starts with `Error:` | any | any | ❌ | Error (show reason verbatim) |
|
|
213
|
+
| anything else | any | any | ❌ | Failed (show reason verbatim) |
|
|
214
|
+
|
|
215
|
+
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.
|
|
216
|
+
|
|
217
|
+
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.
|
|
218
|
+
|
|
219
|
+
## Display
|
|
220
|
+
|
|
221
|
+
Per-row markdown table. **Headline first** with the gain count, then the table.
|
|
222
|
+
|
|
223
|
+
For include mode:
|
|
224
|
+
|
|
225
|
+
```markdown
|
|
226
|
+
**Bulk-import to report 23859 — done.** Report gained **2** rows; **1** was already there; **1** failed.
|
|
227
|
+
|
|
228
|
+
| # | Status | Input | ID | Reason |
|
|
229
|
+
|---|---|---|---|---|
|
|
230
|
+
| 1 | ✅ Added | `@mkbhd` | 4587 | Success |
|
|
231
|
+
| 2 | ↺ Already in report | `@veritasium` | 1209 | Duplicate |
|
|
232
|
+
| 3 | 🆕 Created in TL | `@OfficialSaharTV` | 1328906 | Success — enrichment queued |
|
|
233
|
+
| 4 | ❌ Not found | `https://bad-url` | — | Not found |
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
For exclude mode, headline uses "lost" wording:
|
|
237
|
+
|
|
238
|
+
```markdown
|
|
239
|
+
**Bulk-import (exclude) to report 23859 — done.** Report lost **N** rows; **M** were already excluded.
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Display rules:
|
|
243
|
+
|
|
244
|
+
- **Use the user's raw `input` value** in the Input column (it's `inputs[i].input` — the raw submitted string, unchanged).
|
|
245
|
+
- **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.
|
|
246
|
+
- **Small imports (≤30 rows):** render the full table.
|
|
247
|
+
- **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.
|
|
248
|
+
|
|
249
|
+
Summary table (include mode example):
|
|
250
|
+
|
|
251
|
+
```markdown
|
|
252
|
+
| Bucket | Count |
|
|
253
|
+
|---|---|
|
|
254
|
+
| ✅ Added | 142 |
|
|
255
|
+
| ↺ Already in report | 7 |
|
|
256
|
+
| 🆕 Created in TL | 3 |
|
|
257
|
+
| ❌ Failed | 2 |
|
|
258
|
+
| **Total submitted** | **154** |
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Summary table (exclude mode example):
|
|
262
|
+
|
|
263
|
+
```markdown
|
|
264
|
+
| Bucket | Count |
|
|
265
|
+
|---|---|
|
|
266
|
+
| ✂️ Excluded | 142 |
|
|
267
|
+
| ↺ Already excluded | 7 |
|
|
268
|
+
| ❌ Failed | 2 |
|
|
269
|
+
| **Total submitted** | **151** |
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
- **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.
|
|
273
|
+
|
|
274
|
+
## Errors at the command level (before the per-row results)
|
|
275
|
+
|
|
276
|
+
These are envelope-level failures, distinct from per-row `reason` values:
|
|
277
|
+
|
|
278
|
+
- **403** → caller isn't a superuser. Stop and tell the user; this command is gated.
|
|
279
|
+
- **400** → bad input shape (missing field, unknown entity, all-empty identifiers). Show the `detail` verbatim.
|
|
280
|
+
- **402** → out of credits. Tell the user to top up.
|
|
281
|
+
- **Connection failed** → transient network issue. Retry once; if it persists, surface to the user.
|
|
282
|
+
|
|
283
|
+
## What this skill does NOT do
|
|
284
|
+
|
|
285
|
+
- 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.
|
|
286
|
+
- 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.
|
|
287
|
+
- 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.
|
|
288
|
+
- 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
|
|
@@ -367,8 +367,13 @@ def create_report(
|
|
|
367
367
|
|
|
368
368
|
# --- Show preview ---
|
|
369
369
|
if json_output:
|
|
370
|
-
|
|
370
|
+
# Preview-only mode (--json without --yes): print the config and exit.
|
|
371
|
+
# With --yes, skip the preview emit entirely so the only thing on
|
|
372
|
+
# stdout is the single save-response JSON below — programmatic
|
|
373
|
+
# consumers can parse stdout with one json.loads() instead of having
|
|
374
|
+
# to split two documents.
|
|
371
375
|
if not yes:
|
|
376
|
+
print(json.dumps(config, indent=2, default=str))
|
|
372
377
|
raise typer.Exit(0)
|
|
373
378
|
else:
|
|
374
379
|
err.print()
|
|
@@ -115,6 +115,33 @@ class TestCreateArgValidation:
|
|
|
115
115
|
assert result.exit_code == 0
|
|
116
116
|
assert "McDonald's" in (result.stdout or result.output)
|
|
117
117
|
|
|
118
|
+
def test_yes_json_skips_preview_emit_on_stdout(self, tmp_path: Path) -> None:
|
|
119
|
+
# With --yes --json, the command should NOT emit the config preview
|
|
120
|
+
# JSON to stdout — only the save response JSON should land there, so
|
|
121
|
+
# programmatic consumers can parse stdout with a single json.loads().
|
|
122
|
+
# We can't reach the save step in a unit test (no live API), but we
|
|
123
|
+
# can confirm the preview JSON never hits stdout: the only way the
|
|
124
|
+
# preview would show is via the print(...) call in the json_output
|
|
125
|
+
# branch, and that's now gated on `not yes`. We assert stdout is
|
|
126
|
+
# empty up to the point where the network call would occur.
|
|
127
|
+
cfg = tmp_path / "yes.json"
|
|
128
|
+
cfg.write_text(
|
|
129
|
+
json.dumps({"report_title": "Preview-skip test", "report_type": 3}),
|
|
130
|
+
encoding="utf-8",
|
|
131
|
+
)
|
|
132
|
+
result = runner.invoke(
|
|
133
|
+
app, ["create", "--config-file", str(cfg), "--yes", "--json"]
|
|
134
|
+
)
|
|
135
|
+
# The save POST fails in unit tests (no live API), so exit_code is
|
|
136
|
+
# non-zero. The thing we're locking down is that the preview config
|
|
137
|
+
# JSON is NOT in stdout — i.e. the unique title string only appears
|
|
138
|
+
# on stdout if the preview path fired, which it shouldn't with --yes.
|
|
139
|
+
stdout = result.stdout or ""
|
|
140
|
+
assert "Preview-skip test" not in stdout, (
|
|
141
|
+
"--yes --json should skip the preview JSON emit so stdout is "
|
|
142
|
+
"single-document parseable; preview leaked into stdout"
|
|
143
|
+
)
|
|
144
|
+
|
|
118
145
|
|
|
119
146
|
# ---------------------------------------------------------------------------
|
|
120
147
|
# tl reports update — argument validation
|
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: tl-import
|
|
3
|
-
description: Bulk-add or exclude a list of channels, brands, uploads (videos), or sponsorships against a ThoughtLeaders report (campaign). Superuser-only. Use when a request asks to import / add / exclude a batch of identifiers against a specific report ID — phrasings like "import these channels into report 1234", "add brands to campaign 5678", "exclude these channels from report Z", "bulk-add these videos to report X".
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# tl-import
|
|
7
|
-
|
|
8
|
-
Wraps the `tl bulk-import` command — submits a list of identifiers against a report, polls until done, and renders a per-row result table.
|
|
9
|
-
|
|
10
|
-
## When to use
|
|
11
|
-
|
|
12
|
-
Trigger on requests like:
|
|
13
|
-
|
|
14
|
-
- "Import @mkbhd, @veritasium into report 1234"
|
|
15
|
-
- "Add these brands to campaign 5678"
|
|
16
|
-
- "Bulk-add this list of channels to report 999"
|
|
17
|
-
- "Exclude these channels from report Z"
|
|
18
|
-
- "Add these videos to report X"
|
|
19
|
-
|
|
20
|
-
Single-identifier requests still work (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.
|
|
21
|
-
|
|
22
|
-
## Inputs to gather
|
|
23
|
-
|
|
24
|
-
Before running, confirm:
|
|
25
|
-
|
|
26
|
-
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.
|
|
27
|
-
2. **Entity type** — one of `channels` / `brands` / `articles` / `sponsorships`. Infer from context, but translate user-facing vocabulary:
|
|
28
|
-
- YouTube URLs / handles / `UC…` IDs → `channels`
|
|
29
|
-
- Domains / brand slugs → `brands`
|
|
30
|
-
- "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)*
|
|
31
|
-
- "adlinks" / "deals" / "sponsorships" / numeric AdLink IDs → `sponsorships`
|
|
32
|
-
3. **Identifiers** — the list. Accepted shapes per entity:
|
|
33
|
-
- **channels**: numeric DB IDs, YouTube channel IDs (`UC…`), `@handles`, full YouTube URLs (`/@…`, `/channel/UC…`, `/user/…`)
|
|
34
|
-
- **brands**: numeric IDs, slugs, websites / domains (`example.com`)
|
|
35
|
-
- **articles** (uploads): video IDs or video URLs
|
|
36
|
-
- **sponsorships** (adlinks): numeric AdLink IDs only
|
|
37
|
-
4. **Include vs exclude** — default is include (add to the report). Pass `--exclude` only if the user explicitly wants to remove from the report.
|
|
38
|
-
|
|
39
|
-
## How to invoke
|
|
40
|
-
|
|
41
|
-
The command reads identifiers from a file (`--ids-file`) or stdin:
|
|
42
|
-
|
|
43
|
-
```bash
|
|
44
|
-
# small list — stdin
|
|
45
|
-
echo '@mkbhd
|
|
46
|
-
@veritasium
|
|
47
|
-
@lemmino' | tl bulk-import channels --campaign 1234
|
|
48
|
-
|
|
49
|
-
# larger list — file
|
|
50
|
-
tl bulk-import channels --campaign 1234 --ids-file ./channels.txt
|
|
51
|
-
|
|
52
|
-
# exclusion
|
|
53
|
-
tl bulk-import brands --campaign 5678 -f ./brands.txt --exclude
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
Short flags: `-c` for `--campaign`, `-f` for `--ids-file`.
|
|
57
|
-
|
|
58
|
-
## Output: the `inputs` envelope
|
|
59
|
-
|
|
60
|
-
`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.
|
|
61
|
-
|
|
62
|
-
```json
|
|
63
|
-
{
|
|
64
|
-
"task_id": "...",
|
|
65
|
-
"mode": "include",
|
|
66
|
-
"inputs": [
|
|
67
|
-
{"input": "@mkbhd", "resolved_id": 4587, "reason": "Success", "newly_created": false},
|
|
68
|
-
{"input": "@veritasium", "resolved_id": 1209, "reason": "Duplicate", "newly_created": false},
|
|
69
|
-
{"input": "@OfficialSaharTV", "resolved_id": 1328906, "reason": "Success", "newly_created": true},
|
|
70
|
-
{"input": "https://bad-url", "resolved_id": null, "reason": "Not found", "newly_created": false}
|
|
71
|
-
],
|
|
72
|
-
"success_ids": [4587, 1328906],
|
|
73
|
-
"newly_created_ids": [1328906],
|
|
74
|
-
"failed_ids": [...],
|
|
75
|
-
...
|
|
76
|
-
}
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
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.
|
|
80
|
-
|
|
81
|
-
**`mode` echoes back the operation mode** (`"include"` or `"exclude"`). You need this for labelling because the semantics flip:
|
|
82
|
-
|
|
83
|
-
- include + Success = identifier was just added to the report
|
|
84
|
-
- exclude + Success = identifier was just removed from the report
|
|
85
|
-
- include + Duplicate = identifier was already in the report (no-op)
|
|
86
|
-
- exclude + Duplicate = identifier was already excluded (no-op)
|
|
87
|
-
|
|
88
|
-
Don't use `success_ids` / `failed_ids` for display — they lose input mapping and miss the include/exclude direction. `inputs` is the canonical surface.
|
|
89
|
-
|
|
90
|
-
## Classify each row
|
|
91
|
-
|
|
92
|
-
| `reason` | `newly_created` | `mode` | Icon | Label |
|
|
93
|
-
|---|---|---|---|---|
|
|
94
|
-
| `Success` | `true` | `include` | 🆕 | Created in TL |
|
|
95
|
-
| `Success` | `true` | `exclude` | ⚠️ | Created in TL — unexpected for exclude, verify report state |
|
|
96
|
-
| `Success` | `false` | `include` | ✅ | Added |
|
|
97
|
-
| `Success` | `false` | `exclude` | ✂️ | Excluded |
|
|
98
|
-
| `Duplicate` | any | `include` | ↺ | Already in report |
|
|
99
|
-
| `Duplicate` | any | `exclude` | ↺ | Already excluded |
|
|
100
|
-
| `Not found` | any | any | ❌ | Not found |
|
|
101
|
-
| `Cannot parse` | any | any | ❌ | Bad format |
|
|
102
|
-
| `Multiple matches found` | any | any | ❌ | Ambiguous (multiple matches) |
|
|
103
|
-
| `Limit exceeded` | any | any | ❌ | Auto-create cap hit |
|
|
104
|
-
| starts with `Error:` | any | any | ❌ | Error (show reason verbatim) |
|
|
105
|
-
| anything else | any | any | ❌ | Failed (show reason verbatim) |
|
|
106
|
-
|
|
107
|
-
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.
|
|
108
|
-
|
|
109
|
-
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.
|
|
110
|
-
|
|
111
|
-
## Display
|
|
112
|
-
|
|
113
|
-
Per-row markdown table. **Headline first** with the gain count, then the table.
|
|
114
|
-
|
|
115
|
-
For include mode:
|
|
116
|
-
|
|
117
|
-
```markdown
|
|
118
|
-
**Bulk-import to report 23859 — done.** Report gained **2** rows; **1** was already there; **1** failed.
|
|
119
|
-
|
|
120
|
-
| # | Status | Input | ID | Reason |
|
|
121
|
-
|---|---|---|---|---|
|
|
122
|
-
| 1 | ✅ Added | `@mkbhd` | 4587 | Success |
|
|
123
|
-
| 2 | ↺ Already in report | `@veritasium` | 1209 | Duplicate |
|
|
124
|
-
| 3 | 🆕 Created in TL | `@OfficialSaharTV` | 1328906 | Success — enrichment queued |
|
|
125
|
-
| 4 | ❌ Not found | `https://bad-url` | — | Not found |
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
For exclude mode, headline uses "lost" wording:
|
|
129
|
-
|
|
130
|
-
```markdown
|
|
131
|
-
**Bulk-import (exclude) to report 23859 — done.** Report lost **N** rows; **M** were already excluded.
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
Display rules:
|
|
135
|
-
|
|
136
|
-
- **Use the user's raw `input` value** in the Input column (it's `inputs[i].input` — the raw submitted string, unchanged).
|
|
137
|
-
- **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.
|
|
138
|
-
- **Small imports (≤30 rows):** render the full table.
|
|
139
|
-
- **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.
|
|
140
|
-
|
|
141
|
-
Summary table (include mode example):
|
|
142
|
-
|
|
143
|
-
```markdown
|
|
144
|
-
| Bucket | Count |
|
|
145
|
-
|---|---|
|
|
146
|
-
| ✅ Added | 142 |
|
|
147
|
-
| ↺ Already in report | 7 |
|
|
148
|
-
| 🆕 Created in TL | 3 |
|
|
149
|
-
| ❌ Failed | 2 |
|
|
150
|
-
| **Total submitted** | **154** |
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
Summary table (exclude mode example):
|
|
154
|
-
|
|
155
|
-
```markdown
|
|
156
|
-
| Bucket | Count |
|
|
157
|
-
|---|---|
|
|
158
|
-
| ✂️ Excluded | 142 |
|
|
159
|
-
| ↺ Already excluded | 7 |
|
|
160
|
-
| ❌ Failed | 2 |
|
|
161
|
-
| **Total submitted** | **151** |
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
- **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.
|
|
165
|
-
|
|
166
|
-
## Errors at the command level (before the per-row results)
|
|
167
|
-
|
|
168
|
-
These are envelope-level failures, distinct from per-row `reason` values:
|
|
169
|
-
|
|
170
|
-
- **403** → caller isn't a superuser. Stop and tell the user; this command is gated.
|
|
171
|
-
- **400** → bad input shape (missing field, unknown entity, all-empty identifiers). Show the `detail` verbatim.
|
|
172
|
-
- **402** → out of credits. Tell the user to top up.
|
|
173
|
-
- **Connection failed** → transient network issue. Retry once; if it persists, surface to the user.
|
|
174
|
-
|
|
175
|
-
## What this skill does NOT do
|
|
176
|
-
|
|
177
|
-
- Doesn't create reports — that's `tl-report-builder`.
|
|
178
|
-
- Doesn't change report metadata (title, description, columns, filters).
|
|
179
|
-
- 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.
|
|
180
|
-
- 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.
|
|
File without changes
|
{thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/skills/tl/references/business-glossary.md
RENAMED
|
File without changes
|
{thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/skills/tl/references/elasticsearch-schema.md
RENAMED
|
File without changes
|
{thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/skills/tl/references/firebolt-schema.md
RENAMED
|
File without changes
|
{thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/skills/tl/references/postgres-schema.md
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
|
{thoughtleaders_cli-0.6.26 → thoughtleaders_cli-0.6.28}/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
|