thoughtleaders-cli 0.6.43__tar.gz → 0.6.45__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.43 → thoughtleaders_cli-0.6.45}/.claude-plugin/plugin.json +1 -1
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/PKG-INFO +1 -1
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/pyproject.toml +1 -1
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/skills/tl/SKILL.md +92 -52
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/src/tl_cli/__init__.py +1 -1
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/src/tl_cli/commands/_comments_common.py +6 -4
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/src/tl_cli/commands/brands.py +26 -4
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/src/tl_cli/commands/channels.py +28 -8
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/src/tl_cli/commands/recommender.py +77 -8
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/src/tl_cli/main.py +0 -2
- thoughtleaders_cli-0.6.43/skills/tl-feedback/SKILL.md +0 -87
- thoughtleaders_cli-0.6.43/src/tl_cli/commands/feedback.py +0 -61
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/.claude-plugin/marketplace.json +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/.github/workflows/python-publish.yml +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/.gitignore +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/AGENTS.md +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/CLAUDE.md +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/LICENSE +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/README.md +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/agents/tl-analyst.md +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/docs/architecture.md +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/hooks/hooks.json +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/hooks/scripts/load-tl-skill.mjs +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/hooks/scripts/post-usage.sh +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/hooks/scripts/pre-check.sh +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/skills/tl/references/business-glossary.md +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/skills/tl/references/elasticsearch-schema.md +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/skills/tl/references/firebolt-schema.md +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/skills/tl/references/postgres-schema.md +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/skills/tl-import/SKILL.md +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/skills/tl-report-builder/SKILL.md +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/skills/tl-report-builder/examples/e2e_findings.md +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/skills/tl-report-builder/examples/golden_queries.md +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/skills/tl-report-builder/references/columns_brands.md +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/skills/tl-report-builder/references/columns_channels.md +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/skills/tl-report-builder/references/columns_content.md +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/skills/tl-report-builder/references/columns_sponsorships.md +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/skills/tl-report-builder/references/intelligence_filterset_schema.json +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/skills/tl-report-builder/references/intelligence_widget_schema.json +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/skills/tl-report-builder/references/report_glossary.md +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/skills/tl-report-builder/references/sortable_columns.json +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/skills/tl-report-builder/references/sponsorship_filterset_schema.json +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/skills/tl-report-builder/references/sponsorship_widget_schema.json +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/skills/tl-report-builder/references/widgets.md +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/skills/tl-report-builder/tools/column_builder.md +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/skills/tl-report-builder/tools/database_query.md +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/skills/tl-report-builder/tools/keyword_research.md +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/skills/tl-report-builder/tools/name_resolver.md +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/skills/tl-report-builder/tools/sample_judge.md +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/skills/tl-report-builder/tools/similar_channels.md +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/skills/tl-report-builder/tools/topic_matcher.md +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/skills/tl-report-builder/tools/widget_builder.md +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/src/tl_cli/_completions.py +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/src/tl_cli/auth/__init__.py +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/src/tl_cli/auth/commands.py +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/src/tl_cli/auth/finalize.py +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/src/tl_cli/auth/login.py +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/src/tl_cli/auth/pkce.py +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/src/tl_cli/auth/token_store.py +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/src/tl_cli/client/__init__.py +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/src/tl_cli/client/errors.py +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/src/tl_cli/client/http.py +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/src/tl_cli/commands/__init__.py +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/src/tl_cli/commands/ask.py +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/src/tl_cli/commands/balance.py +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/src/tl_cli/commands/bulk_import.py +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/src/tl_cli/commands/changelog.py +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/src/tl_cli/commands/credits.py +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/src/tl_cli/commands/db.py +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/src/tl_cli/commands/deals.py +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/src/tl_cli/commands/describe.py +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/src/tl_cli/commands/doctor.py +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/src/tl_cli/commands/matches.py +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/src/tl_cli/commands/proposals.py +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/src/tl_cli/commands/reports.py +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/src/tl_cli/commands/schema.py +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/src/tl_cli/commands/setup.py +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/src/tl_cli/commands/snapshots.py +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/src/tl_cli/commands/sponsorships.py +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/src/tl_cli/commands/uploads.py +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/src/tl_cli/commands/whoami.py +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/src/tl_cli/config.py +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/src/tl_cli/filters.py +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/src/tl_cli/hints.py +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/src/tl_cli/output/__init__.py +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/src/tl_cli/output/formatter.py +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/src/tl_cli/self_update.py +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/tests/__init__.py +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/tests/test_auth.py +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/tests/test_filters.py +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/tests/test_http_auth.py +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/tests/test_output.py +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/tests/test_reports.py +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/tests/test_sponsorships.py +0 -0
- {thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/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.45
|
|
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
|
|
@@ -1,41 +1,47 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: tl
|
|
3
3
|
description: |
|
|
4
|
-
Query and analyze
|
|
4
|
+
Query and analyze YouTube sponsorship data using the `tl` CLI. Use this skill for data exploration and questions about channels, brands and sponsorships: counts, metrics, trends, time-series, distributions, single-record drill-downs, revenue / pipeline-weighting math, view-curve analysis, cross-source business questions. Examples: "How many deals did we close last quarter?", "What's the weighted pipeline by sales owner?", "Show me the view curve for video X", "Find mentions of Surfshark in transcripts", "Investigate this video".
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# ThoughtLeaders Data Analyst
|
|
8
8
|
|
|
9
|
-
Run the `tl` CLI to query ThoughtLeaders' sponsorship platform data. Use it to answer questions about deals, channels, brands, uploads, metrics, etc.
|
|
10
|
-
|
|
11
9
|
## Core Principles
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
Always run `tl schema pg|fb|es` before writing a raw query.
|
|
11
|
+
Run the `tl` CLI to query ThoughtLeaders' sponsorship platform data. Use it to answer questions about deals, channels, brands, uploads, metrics, etc. Use raw database queries via `tl db pg|fb|es` for everything.
|
|
16
12
|
|
|
17
|
-
|
|
13
|
+
Always run `tl schema pg|fb|es` before writing a raw query. When you only need the schema of one table, you MUST call `tl schema pg <table>` (or `tl schema fb <table>`). Avoid calling the unscoped form, to reduce token counts. ES has no per-table form (the index is a single document shape), so `tl schema es` is the only call there.
|
|
18
14
|
|
|
19
|
-
**Process data with shell tools, not your context window.** Don't pull large result sets into your reasoning context just to filter, sort, count, or extract a field
|
|
15
|
+
**Process data with shell tools, not your context window.** Don't pull large result sets into your reasoning context just to filter, sort, count, or extract a field - that wastes tokens and slows you down. Pipe `tl … --json` (or `--csv`, or `--toon`) into `jq`, `yq`, `rg`, or `duckdb`, as appropriate, and read only the answer back. Pick the tool by shape:
|
|
20
16
|
|
|
21
17
|
- **`jq`** — filter, project, and transform JSON. The default for `tl … --json` post-processing.
|
|
22
18
|
```bash
|
|
23
|
-
tl
|
|
19
|
+
tl db pg "SELECT id, weighted_price FROM thoughtleaders_adlink
|
|
20
|
+
WHERE publish_status = 3 AND price > 5000
|
|
21
|
+
LIMIT 500 OFFSET 0" --json \
|
|
22
|
+
| jq '.results[] | {id, price: .weighted_price}'
|
|
24
23
|
```
|
|
25
24
|
- **`yq`** — same idea for YAML/TOML, useful when reading config files or `--md` blocks.
|
|
26
|
-
- **`rg`** — fast text search across CLI output, transcripts, and the codebase. Better than `grep` for searching large `--csv` exports or transcript dumps.
|
|
25
|
+
- **`rg`** — fast text search across CLI output, transcripts, and the codebase. Better than `grep` for searching large `--csv` exports or transcript dumps from ES.
|
|
27
26
|
```bash
|
|
28
27
|
tl db es '{"size":500,"query":{"term":{"channel.id":5607}},"_source":["id","transcript"]}' --json | rg -o "NordVPN[^.]*"
|
|
29
28
|
```
|
|
30
29
|
- **`duckdb`** — embedded analytical SQL over CSV/JSON files. Use when you need joins, aggregations, or window functions across multiple `tl` exports without spinning up a database.
|
|
31
30
|
```bash
|
|
32
|
-
tl
|
|
31
|
+
tl db pg "SELECT al.id, b.name AS brand, al.weighted_price AS price
|
|
32
|
+
FROM thoughtleaders_adlink al
|
|
33
|
+
JOIN thoughtleaders_profile p ON p.id = al.creator_profile_id
|
|
34
|
+
JOIN thoughtleaders_profile_brands pb ON pb.profile_id = p.id
|
|
35
|
+
JOIN thoughtleaders_brand b ON b.id = pb.brand_id
|
|
36
|
+
WHERE al.publish_status = 3
|
|
37
|
+
AND al.purchase_date >= '2026-01-01'
|
|
38
|
+
LIMIT 500 OFFSET 0" --csv > deals.csv
|
|
33
39
|
duckdb -c "SELECT brand, SUM(price) AS revenue FROM 'deals.csv' GROUP BY brand ORDER BY revenue DESC LIMIT 10"
|
|
34
40
|
```
|
|
35
41
|
|
|
36
|
-
The pattern is always: server-side narrowing first (
|
|
42
|
+
The pattern is always: server-side narrowing first (usuakky by filters in the `tl db` query, but could be from similarity searches), then shell tool to shape the result, then read only the final summary into context. If `tl doctor` reports any of these as missing, ask the user to install them — `tl-internal setup` installs all four by default.
|
|
37
43
|
|
|
38
|
-
Always assume there will be more than 1 page of results. You MUST always
|
|
44
|
+
Always assume there will be more than 1 page of results. You MUST always pass `LIMIT` and `OFFSET` to every `tl db pg|fb|es` query (and use the response envelope's `next_offset` / breadcrumbs to walk forward) so the entire data set is retrieved. The maximum number of rows per page is 500.
|
|
39
45
|
|
|
40
46
|
Retry after 5 seconds if the server returns a "connection denied" or a "server error" on any request.
|
|
41
47
|
|
|
@@ -49,7 +55,7 @@ This section defines business terminology. Any other skill files, command, and p
|
|
|
49
55
|
|
|
50
56
|
ThoughtLeaders is a sponsorship marketplace connecting **Brands** (advertisers / media buyers) with **Channels** (YouTube creators, podcasters / media sellers).
|
|
51
57
|
|
|
52
|
-
The centre of the data model
|
|
58
|
+
The centre of the data model are **Sponsorships** — business relationships between brands and channels. Sponsorships statuses form a sales funnel, from broad to narrow:
|
|
53
59
|
|
|
54
60
|
- **Sponsorships** — the broadest category, encompassing all stages, stored in the `thoughtleaders_adlink` table.
|
|
55
61
|
- **Matches** — possible brand-channel pairings that ThoughtLeaders thinks could work
|
|
@@ -58,33 +64,35 @@ The centre of the data model is **Sponsorships** — business relationships betw
|
|
|
58
64
|
|
|
59
65
|
Sponsorships are sometimes called "Ads" or "Ad campaigns". **"AdLink"** is another name for the same thing — it's the term the database uses (`thoughtleaders_adlink`) and shows up across internal code, schema docs, and AM Slack threads. Treat "sponsorship" and "adlink" as interchangeable; the user-facing word is "sponsorship," the engineering/DB word is "adlink."
|
|
60
66
|
|
|
61
|
-
The CLI has shortcut commands for each type: `tl matches`, `tl proposals`, `tl deals`. These
|
|
67
|
+
The CLI has shortcut commands for each type: `tl matches`, `tl proposals`, `tl deals`. These are aliases for `tl sponsorships` with filtering by status.
|
|
62
68
|
|
|
63
69
|
Other key concepts:
|
|
70
|
+
- **Channels** — YouTube channels, but could also be podcasts
|
|
71
|
+
- **Brands** — Entities (usually companies / organizations, but could be narrowed down to individual brands of a company)
|
|
64
72
|
- **Uploads** — YouTube videos indexed from Elasticsearch
|
|
65
73
|
- **Snapshots** — historical time-series metrics for channels and videos (Firebolt)
|
|
66
74
|
- **Reports** — saved report configurations that can be re-run
|
|
67
|
-
- **Comments** — notes attached to sponsorships
|
|
68
|
-
- **Adspots** — types of ads a channel
|
|
69
|
-
- **Profiles** —
|
|
75
|
+
- **Comments** — notes attached to sponsorships, channels, or brands
|
|
76
|
+
- **Adspots** — types of ads a channel is willing to publish (e.g. mention, dedicated video, product placement). Returned by `tl channels show`; each carries price/cost.
|
|
77
|
+
- **Profiles** — actors that own sponsorship records on behalf of either side of a deal. A profile is either buyer-side or seller-side:
|
|
70
78
|
- *Buyer-side (brand) profiles* — represent a sponsoring brand. Each brand profile has an M2M link to at most one `Brand` record (which are the actual advertiser identities). On a sponsorship, `creator_profile` is the buyer-side profile.
|
|
71
79
|
- *Seller-side (publisher) profiles* — attached to a `Publication`, which in turn owns one or more `Channel` records. A channel's adspots therefore inherit ownership through `channel.publication.profile`.
|
|
72
80
|
- **How to tell them apart** — three signals on the `thoughtleaders_profile` row, used in this order:
|
|
73
81
|
1. **`persona`** (canonical) — `1=Brand`, `4=Media Agency`, `3=Talent Manager` are buyer-side; `2=Creator`, `5=Creator Service` are seller-side. May be null on legacy rows.
|
|
74
82
|
2. **`is_advertiser` / `is_publisher`** booleans — feature flags; either or both can be true for staff-style profiles, but on normal user profiles they reliably mark side.
|
|
75
83
|
- Org scoping for sponsorships is profile-mediated: a sponsorship belongs to your org if **either** `creator_profile.organization` (brand side) **or** `ad_spot.channel.publication.profile.organization` (publisher side) matches yours.
|
|
76
|
-
- **MSN** (Media Selling Network) — the ~
|
|
84
|
+
- **MSN** (Media Selling Network) — the ~12k YouTube channels that have opted in to receive sponsorship offers. A channels is in the MSN group if the `channel.media_selling_network_join_date` field is not null.
|
|
77
85
|
- **MBN** (Media Buying Network) — the brand-side counterpart to MSN: brand profiles that have opted in to receive proposed sponsorships. A profile is in the MBN group if the `profile.media_buying_network_join_date` field is not null.
|
|
78
|
-
- **TPP** (ThoughtLeaders Partner Program, a.k.a. "TL channels") — the ~
|
|
79
|
-
- **`demographics_updated_at`** (on
|
|
86
|
+
- **TPP** (ThoughtLeaders Partner Program, a.k.a. "TL channels") — the ~170 channels TL has the closest working relationship with. A channel is in the TPP group if `channel.is_tl_channel` is True. **Prefer TPP channels when booking**: they respond fastest, are the easiest to close, and don't need an outreach round-trip — treat them as immediately bookable. TPP is a strict subset of MSN, so the same booking rules (one active mention adspot, etc.) apply.
|
|
87
|
+
- **`demographics_updated_at`** (on channels) — If non-null, the channel has demographics screenshots on file. If null, no demographics screenshots have been uploaded. Use this to check whether a channel has demographics data from screenshots.
|
|
80
88
|
- **`impression`** (on channels) — projected views per video on that channel. Forward-looking estimate. May be null when not yet computed.
|
|
81
89
|
- **`views`** (on sponsorships) — actual view count of the sold and published sponsored video, accessible when `article_id` is set.
|
|
82
|
-
- **`impressions_guarantee`** (on sponsorships) — projected/guaranteed impressions for the sponsorship. Numeric
|
|
83
|
-
- **Sponsorship detail fields** (returned by `tl sponsorships show <id> --json`) —
|
|
90
|
+
- **`impressions_guarantee`** (on sponsorships) — projected/guaranteed impressions for the sponsorship. Numeric.
|
|
91
|
+
- **Sponsorship detail fields** (returned by `tl sponsorships show <id> --json`) — the detail payload includes `integration` (raw int), `publish_count`, `common_name`, `outreach_email`, nested `publisher` (`first_name`, `last_name`, `email`), nested `brand_contact` (`first_name`, `last_name`, `email`), and `brand.organization_name`. Use these when generating IOs, contracts, or outreach.
|
|
84
92
|
- **CPM** has two distinct meanings depending on level — pick the one the user actually wants:
|
|
85
93
|
- **Channel CPM** = `(adspot.price / channel.impression) * 1000` — projected price per thousand projected views. Used for pricing decisions **before** a sponsorship is sold. Available for channels with active adspots via `tl channels show <channel_id>`.
|
|
86
94
|
- **Sponsorship CPM** = calculated in either of two ways: if `views` is present, then CPM is `(sponsorship.price / sponsorship.views) × 1000`, meaning realized cost per thousand actual views, computed post-publication. If `views` is null, Compute from the sponsorship's `price` and the channel's `impression` fields.
|
|
87
|
-
-
|
|
95
|
+
- CPM does not have a range filter. To find sponsorships in a CPM range (e.g. "around $15"), fetch the record set with other filters first, then apply the CPM range in post-processing (jq, Python, etc.) on the returned `cpm` field. Plan queries and pagination accordingly — the server cannot reduce the result count based on CPM.
|
|
88
96
|
- **Sponsorship dates** — each sponsorship has four distinct dates, useful for different queries:
|
|
89
97
|
- **`created_at`** — when the sponsorship record was created in the system
|
|
90
98
|
- **`purchase_date`** — when the sponsorship was purchased (i.e. when the deal was made); These make up bookings.
|
|
@@ -97,13 +105,13 @@ Users see data scoped by their organization and plan:
|
|
|
97
105
|
- **Media sellers** see sponsorships where their org is the publisher. They see `cost` but never `price`.
|
|
98
106
|
- **Intelligence plan** is required for accessing information not strictly related to the user's organisation.
|
|
99
107
|
|
|
100
|
-
When querying sponsorship bookings, query by `status:sold` and filter the the date range only by `purchase_date`. Otherwise, query for
|
|
108
|
+
When querying sponsorship bookings, query by `status:sold` and filter the the date range only by `purchase_date`. Otherwise, query for `status:sold` and filter by `created_at`.
|
|
101
109
|
|
|
102
110
|
## Methodology
|
|
103
111
|
|
|
104
112
|
Where possible, if searching for a sponsorship match between channels and brands, first search for what do similar brands sponsor / which brands is the channel usually sponsored by. The similarity judgement should be preferably based on similar topics, similar upload frequency, similar channel sizes, and only after all that, on demographics.
|
|
105
113
|
|
|
106
|
-
Use the `tl channels similar` and `tl brands similar` commands to
|
|
114
|
+
Use the `tl channels similar` and `tl brands similar` commands to find channels or brands similar to a particular channel or brand. For category- or topic-driven discovery (e.g. "Find me Cooking channels", "Who scores high on USA share?"), use `tl recommender top-channels "<tag>"` (or `top-brands`/`top-profiles`) against the recommender — that's faster, ranked by category-strength. Run `tl recommender tags` to discover the valid tag names.
|
|
107
115
|
|
|
108
116
|
## Workflow
|
|
109
117
|
|
|
@@ -144,20 +152,30 @@ Prefer writing Python code, shell code, or `jq` commands that fetche or analysis
|
|
|
144
152
|
Note that if you're working on Windows, you need to set up UTF-8 in the console, because all of these commands return UTF-8 data.
|
|
145
153
|
|
|
146
154
|
### Data queries
|
|
155
|
+
|
|
156
|
+
**Filtered queries go through `tl db pg|fb|es`.** Write the SELECT/ES body yourself, and freely perform joins and aggregations. The show/create/update commands exist because they target a single record by ID. Where needed, write Python scripts or duckdb queries to join data from different databases.
|
|
157
|
+
|
|
158
|
+
Filter-to-SQL examples (deals/matches/proposals all live on `thoughtleaders_adlink`, differentiated by `publish_status`):
|
|
159
|
+
|
|
160
|
+
| Want | Raw-DB equivalent |
|
|
161
|
+
| --- | --- |
|
|
162
|
+
| All sponsorships matching filters | `tl db pg "SELECT … FROM thoughtleaders_adlink WHERE …"` |
|
|
163
|
+
| Sold deals (`publish_status=3`) | `tl db pg "SELECT … FROM thoughtleaders_adlink WHERE publish_status = 3"` |
|
|
164
|
+
| Matched (`publish_status=7`) | `tl db pg "SELECT … FROM thoughtleaders_adlink WHERE publish_status = 7"` |
|
|
165
|
+
| Proposed (`publish_status=0`) | `tl db pg "SELECT … FROM thoughtleaders_adlink WHERE publish_status = 0"` |
|
|
166
|
+
| Video uploads from ElasticSearch | `tl db es '{"size":N,"query":{"term":{"channel.id":<id>}}}'` |
|
|
167
|
+
|
|
168
|
+
Single-record / mutation commands remain:
|
|
169
|
+
|
|
147
170
|
```bash
|
|
148
|
-
tl sponsorships list [filters...] # Sponsorships
|
|
149
171
|
tl sponsorships show <id> # Sponsorship detail
|
|
150
172
|
tl sponsorships create --channel <id> --brand <id> # Create proposal
|
|
151
173
|
tl sponsorships update <id> '<json>' # Update a sponsorship
|
|
152
|
-
tl deals list [filters...] # Shortcut: agreed-upon sponsorships (status:deal)
|
|
153
174
|
tl deals show <id> # Deal detail
|
|
154
|
-
tl matches list [filters...] # Shortcut: possible brand-channel pairings (status:match)
|
|
155
175
|
tl matches show <id> # Match detail
|
|
156
176
|
tl matches create --channel <id> --brand <id> # Create match
|
|
157
|
-
tl proposals list [filters...] # Shortcut: proposed matches (status:proposal)
|
|
158
177
|
tl proposals show <id> # Proposal detail
|
|
159
178
|
tl proposals create --channel <id> --brand <id> # Create proposal
|
|
160
|
-
tl uploads list [filters...] # Video uploads from ES
|
|
161
179
|
tl uploads show <id> # Upload detail
|
|
162
180
|
tl channels show <id-or-name> # Channel detail (accepts numeric ID or name) — for channel search use raw SQL on thoughtleaders_channel
|
|
163
181
|
tl channels find <query> # Resolve a string to {id, name}; accepts name/slug, YouTube URL/handle/ID, video URL (queues a scrape if no match)
|
|
@@ -177,7 +195,9 @@ tl recommender top-profiles "<tag>" # Top brand profiles loaded on a similari
|
|
|
177
195
|
tl recommender top-brands "<tag>" # Top brands (deduped from profiles) loaded on a similarity tag
|
|
178
196
|
tl recommender inspect-channel <ref> # Show a channel's similarity-profile breakdown (Intelligence)
|
|
179
197
|
tl recommender inspect-brand <ref> # Show a brand profile's ideal similarity-profile breakdown (Intelligence)
|
|
180
|
-
tl recommender
|
|
198
|
+
tl recommender channels-for-profile <id> # Find channels closest to a brand profile's ideal profile (Intelligence)
|
|
199
|
+
tl recommender channels-for-brand <ref> # Same as above but takes a brand ref; uses the brand's newest profile with a vector (Intelligence)
|
|
200
|
+
tl recommender brands-for-channel <ref> # Brands most likely to sponsor a channel; runs the channel's vector against the brand-profile index (Intelligence)
|
|
181
201
|
tl snapshots channel <id> # Channel metrics over time (Firebolt-backed)
|
|
182
202
|
tl snapshots video <id> --channel <id> # Video view curve (--channel required!)
|
|
183
203
|
tl reports # List saved reports
|
|
@@ -306,7 +326,7 @@ Reasons to write a raw query (the common case):
|
|
|
306
326
|
- **Multi-condition filtering** — compound boolean, `NOT IN`/`EXISTS`, `WHERE col IS NULL` on hidden fields, mixed range + enum + text predicates: write the SQL/ES body, don't over-fetch and post-filter.
|
|
307
327
|
- **Fields the structured commands don't expose** — e.g. `media_selling_network_join_date` (only the `msn` boolean is surfaced), `weighted_price`, `tx_data`, raw `publish_status` integer, etc.
|
|
308
328
|
|
|
309
|
-
Structured commands are still the right tool for: single-record `show` by ID,
|
|
329
|
+
Structured commands are still the right tool for: single-record `show` by ID, saved `tl reports run`, and `tl snapshots channel|video` (these wrap interpolation logic you'd otherwise reimplement). Anything that would have been a "filtered list" goes through `tl db pg|fb|es`.
|
|
310
330
|
|
|
311
331
|
| Need | Use |
|
|
312
332
|
|---|---|
|
|
@@ -317,8 +337,7 @@ Structured commands are still the right tool for: single-record `show` by ID, pl
|
|
|
317
337
|
| Transcript / brand-mention search inside video content | **`tl db es`** (no structured equivalent for content text) |
|
|
318
338
|
| Custom Firebolt shape (milestone-age slices, multi-channel growth comparisons) | **`tl db fb`** |
|
|
319
339
|
| Single-record detail lookup by ID | `tl <resource> show <id>` |
|
|
320
|
-
|
|
|
321
|
-
| Channel/brand similarity (server-implemented similarity search) | `tl channels similar`, `tl brands similar` |
|
|
340
|
+
| Channel/brand similarity (server-implemented similarity search) | `tl channels similar`, `tl brands similar`, `tl recommender ...` |
|
|
322
341
|
| Saved reports | `tl reports`, `tl reports run` |
|
|
323
342
|
| Time-series view-curve / channel growth (default shape with interpolation) | `tl snapshots channel`, `tl snapshots video` |
|
|
324
343
|
|
|
@@ -435,17 +454,6 @@ tl changelog since v0.4.10 # Notes from v0.4.10 to latest
|
|
|
435
454
|
tl changelog --md > CHANGELOG.md # Capture for a doc
|
|
436
455
|
```
|
|
437
456
|
|
|
438
|
-
`tl changelog` summaries are LLM-generated server-side from full commit messages and cached per version, so repeat calls are fast and don't re-bill the LLM. The release date and a 2–4 sentence prose summary come back per version.
|
|
439
|
-
|
|
440
|
-
### Filter syntax
|
|
441
|
-
Structured list commands accept `key:value` filters (use them for trivially simple lookups):
|
|
442
|
-
```bash
|
|
443
|
-
tl sponsorships list status:sold brand:"Nike" purchase-date:2026-01
|
|
444
|
-
tl uploads list channel:12345 type:longform
|
|
445
|
-
```
|
|
446
|
-
|
|
447
|
-
Date filters accept keywords: `today`, `yesterday`, `tomorrow`.
|
|
448
|
-
|
|
449
457
|
#### Channel discovery — recommender first, raw SQL second
|
|
450
458
|
|
|
451
459
|
For category- or demographic-driven discovery, **use the recommender, not `content_category` SQL.** The recommender ranks channels by how strongly they load on a category/demographic tag (similarity scores), instead of forcing exact equality on a single integer code. It also returns the matching brand profiles alongside the channels — useful when the user actually wants to know "who buys this kind of inventory."
|
|
@@ -511,8 +519,8 @@ While analysing results, you must always examine the `results` field in the JSON
|
|
|
511
519
|
|
|
512
520
|
Every query costs credits. Before running expensive queries:
|
|
513
521
|
1. Check the credit rate: `tl describe show <resource> --json | jq '.credits'` and the user balance.
|
|
514
|
-
2. **
|
|
515
|
-
3. Estimate cost from the formula or the table; for non-
|
|
522
|
+
2. **Multi-row endpoints (snapshots, comments, reports, `tl db pg|fb|es`) are priced non-linearly:** `cost = 1 + mult × 0.126 × n^1.2`, where `mult` is the per-resource complexity factor (1.0 for cheap reads, 1.2 for snapshots, 1.3 for reports, 1.4 for raw db). Detail/history/similar endpoints are linear (`rate × results`).
|
|
523
|
+
3. Estimate cost from the formula or the table; for non-row-priced endpoints use `results × rate`.
|
|
516
524
|
4. If estimated cost is more than 10% of the remaining balance, ask the user to confirm the operation before running.
|
|
517
525
|
|
|
518
526
|
## Data Scoping
|
|
@@ -520,7 +528,7 @@ Every query costs credits. Before running expensive queries:
|
|
|
520
528
|
Users only see data their plan allows:
|
|
521
529
|
- **Media buyers** see deals where their org is the brand. They see `price` but never `cost`.
|
|
522
530
|
- **Media sellers** see deals where their org is the publisher. They see `cost` but never `price`.
|
|
523
|
-
- **Intelligence plan** required for `tl brands`, the full `tl recommender` surface, and
|
|
531
|
+
- **Intelligence plan** required for `tl brands`, the full `tl recommender` surface, and `tl db es` access to full transcript / brand-mention data.
|
|
524
532
|
- **Paid plan** required for `tl snapshots`.
|
|
525
533
|
|
|
526
534
|
## Important: Status Labels
|
|
@@ -535,7 +543,15 @@ When presenting sponsorship status data, always use human-readable labels — ne
|
|
|
535
543
|
|
|
536
544
|
"Show me my sold sponsorships this quarter":
|
|
537
545
|
```bash
|
|
538
|
-
tl
|
|
546
|
+
tl db pg "SELECT al.id, al.weighted_price, al.purchase_date, b.name AS brand
|
|
547
|
+
FROM thoughtleaders_adlink al
|
|
548
|
+
JOIN thoughtleaders_profile p ON p.id = al.creator_profile_id
|
|
549
|
+
JOIN thoughtleaders_profile_brands pb ON pb.profile_id = p.id
|
|
550
|
+
JOIN thoughtleaders_brand b ON b.id = pb.brand_id
|
|
551
|
+
WHERE al.publish_status = 3
|
|
552
|
+
AND al.purchase_date >= '2026-01-01'
|
|
553
|
+
ORDER BY al.purchase_date DESC
|
|
554
|
+
LIMIT 500 OFFSET 0" --json
|
|
539
555
|
```
|
|
540
556
|
|
|
541
557
|
"What channels does Nike sponsor?":
|
|
@@ -587,7 +603,14 @@ tl recommender top-channels "Cooking" msn:yes --limit 100 --json \
|
|
|
587
603
|
|
|
588
604
|
"Show sold sponsorships targeting mobile US audiences":
|
|
589
605
|
```bash
|
|
590
|
-
tl
|
|
606
|
+
tl db pg "SELECT al.id, c.channel_name, c.demographic_device_primary, c.demographic_usa_share, al.weighted_price
|
|
607
|
+
FROM thoughtleaders_adlink al
|
|
608
|
+
JOIN thoughtleaders_adspot s ON s.id = al.ad_spot_id
|
|
609
|
+
JOIN thoughtleaders_channel c ON c.id = s.channel_id
|
|
610
|
+
WHERE al.publish_status = 3
|
|
611
|
+
AND c.demographic_device_primary = 'mobile'
|
|
612
|
+
AND c.demographic_usa_share >= 60
|
|
613
|
+
LIMIT 500 OFFSET 0" --json
|
|
591
614
|
```
|
|
592
615
|
|
|
593
616
|
"Find channels similar to one I know" (similarity recommender, 25 credits per call):
|
|
@@ -612,6 +635,23 @@ tl recommender top-brands "USA share" mbn:yes --limit 30 # Top brands (ded
|
|
|
612
635
|
tl recommender top-channels "Tech" exclude-for-profile:842 # Drop channels already proposed for profile 842
|
|
613
636
|
tl recommender inspect-channel 29834 # Per-tag breakdown of a channel's vector
|
|
614
637
|
tl recommender inspect-brand Nike # Per-tag breakdown of a brand's ideal profile
|
|
615
|
-
tl recommender
|
|
638
|
+
tl recommender channels-for-profile 842 --limit 30 # Channels closest to a brand profile's ideal profile
|
|
639
|
+
tl recommender channels-for-profile 842 msn:yes language:en # Same, filtered to English MSN channels
|
|
640
|
+
tl recommender channels-for-brand Nike --limit 30 # Same, but takes a brand ref (uses the brand's newest profile with a vector)
|
|
641
|
+
tl recommender channels-for-brand 6037 msn:yes language:en --limit 30
|
|
642
|
+
tl recommender brands-for-channel 29834 --limit 30 # Brands likely to sponsor this channel
|
|
643
|
+
tl recommender brands-for-channel "MrBeast" mbn:yes --limit 30 # Same, restricted to MBN brand profiles
|
|
616
644
|
```
|
|
645
|
+
|
|
646
|
+
**Filters on the recommender commands:**
|
|
647
|
+
|
|
648
|
+
| Command | Filters |
|
|
649
|
+
| --- | --- |
|
|
650
|
+
| `top-channels` | `msn:<yes\|no\|all>` (default all), `exclude-for-profile:<id>` |
|
|
651
|
+
| `top-profiles` | `mbn:<yes\|no\|all>` (default all), `exclude-for-channel:<id>` |
|
|
652
|
+
| `top-brands` | `mbn:<yes\|no\|all>` (default all) |
|
|
653
|
+
| `channels-for-profile` | `language:<iso>` (default `en`), `msn:<yes\|no>` (default `no`) |
|
|
654
|
+
| `channels-for-brand` | same as `channels-for-profile` |
|
|
655
|
+
| `brands-for-channel` | `mbn:<yes\|no\|all>` (default `all`) |
|
|
656
|
+
|
|
617
657
|
Use `tl recommender top` for category/topic discovery (it's ranked) and `tl channels similar` / `tl brands similar` for 1:1 lookalike searches.
|
{thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/src/tl_cli/commands/_comments_common.py
RENAMED
|
@@ -73,23 +73,25 @@ def register_comment_commands(app: typer.Typer, entity_type: str, entity_label:
|
|
|
73
73
|
text (e.g. "sponsorship", "channel").
|
|
74
74
|
"""
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
# `help=` is passed explicitly because Typer reads the function's
|
|
77
|
+
# `__doc__` for the per-subcommand help text, and `f"""…"""` as the
|
|
78
|
+
# first statement is a runtime f-string expression — not a docstring
|
|
79
|
+
# — so `__doc__` ends up None and the help column renders blank.
|
|
80
|
+
@app.command("comment-list", help=f"List comments on a {entity_label} (free, no credits).")
|
|
77
81
|
def comment_list(
|
|
78
82
|
entity_id: str = typer.Argument(..., help=f"{entity_label.capitalize()} ID"),
|
|
79
83
|
json_output: bool = typer.Option(False, "--json", help="JSON output"),
|
|
80
84
|
toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
|
|
81
85
|
) -> None:
|
|
82
|
-
f"""List comments on a {entity_label} (free, no credits)."""
|
|
83
86
|
list_comments(entity_type, entity_id, json_output, toon_output)
|
|
84
87
|
|
|
85
|
-
@app.command("comment-add")
|
|
88
|
+
@app.command("comment-add", help=f"Add a comment to a {entity_label} (free, no credits).")
|
|
86
89
|
def comment_add(
|
|
87
90
|
entity_id: str = typer.Argument(..., help=f"{entity_label.capitalize()} ID"),
|
|
88
91
|
message: str = typer.Argument(..., help="Comment text"),
|
|
89
92
|
json_output: bool = typer.Option(False, "--json", help="JSON output"),
|
|
90
93
|
toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
|
|
91
94
|
) -> None:
|
|
92
|
-
f"""Add a comment to a {entity_label} (free, no credits)."""
|
|
93
95
|
add_comment(entity_type, entity_id, message, json_output, toon_output)
|
|
94
96
|
|
|
95
97
|
@app.command("comment-edit")
|
|
@@ -168,12 +168,20 @@ def history_stats_cmd(
|
|
|
168
168
|
@app.command("find")
|
|
169
169
|
def find_cmd(
|
|
170
170
|
query: str = typer.Argument(..., help="Brand name, slug, domain, or keyword"),
|
|
171
|
+
json_output: bool = typer.Option(False, "--json", help="JSON output"),
|
|
172
|
+
csv_output: bool = typer.Option(False, "--csv", help="CSV output"),
|
|
173
|
+
md_output: bool = typer.Option(False, "--md", help="Markdown output"),
|
|
174
|
+
toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
|
|
171
175
|
) -> None:
|
|
172
|
-
"""Resolve a string to a single brand
|
|
176
|
+
"""Resolve a string to a single brand.
|
|
173
177
|
|
|
174
178
|
Searches across name, slug, website domain, and the brand's keyword
|
|
175
|
-
fields (kw + keywords).
|
|
176
|
-
|
|
179
|
+
fields (kw + keywords). Default output is a pretty `id name` line on
|
|
180
|
+
stdout; pass --json / --csv / --md / --toon for machine-readable
|
|
181
|
+
output (the JSON shape is `{"id": ..., "name": ...}`).
|
|
182
|
+
|
|
183
|
+
Ambiguous matches return an error with the candidate IDs and names so
|
|
184
|
+
the caller can pick a better query.
|
|
177
185
|
|
|
178
186
|
Examples:
|
|
179
187
|
tl brands find Nike
|
|
@@ -181,12 +189,26 @@ def find_cmd(
|
|
|
181
189
|
tl brands find https://www.nike.com/
|
|
182
190
|
tl brands find 21416
|
|
183
191
|
"""
|
|
192
|
+
fmt = detect_format(json_output, csv_output, md_output, toon_output)
|
|
184
193
|
client = get_client()
|
|
185
194
|
try:
|
|
186
195
|
data = client.get("/brands/find", params={"q": query})
|
|
187
196
|
results = data.get("results", [])
|
|
188
197
|
record = results[0] if results else {}
|
|
189
|
-
|
|
198
|
+
if fmt == "table":
|
|
199
|
+
bid = record.get("id")
|
|
200
|
+
name = record.get("name")
|
|
201
|
+
if bid is None:
|
|
202
|
+
Console(stderr=True).print("[yellow]No match.[/yellow]")
|
|
203
|
+
raise typer.Exit(1)
|
|
204
|
+
Console().print(f"[bold yellow]{bid}[/bold yellow] {name}")
|
|
205
|
+
else:
|
|
206
|
+
output(
|
|
207
|
+
{"results": [{"id": record.get("id"), "name": record.get("name")}]},
|
|
208
|
+
fmt,
|
|
209
|
+
columns=["id", "name"],
|
|
210
|
+
title=f"Brand match for {query}",
|
|
211
|
+
)
|
|
190
212
|
except ApiError as e:
|
|
191
213
|
if e.status_code == 400 and isinstance(e.raw, dict) and e.raw.get("candidates"):
|
|
192
214
|
_print_brand_find_candidates(e.detail, e.raw["candidates"])
|
|
@@ -285,8 +285,12 @@ def update_cmd(
|
|
|
285
285
|
@app.command("find")
|
|
286
286
|
def find_cmd(
|
|
287
287
|
query: str = typer.Argument(..., help="Name, slug, YouTube URL, handle, channel ID, or video URL"),
|
|
288
|
+
json_output: bool = typer.Option(False, "--json", help="JSON output"),
|
|
289
|
+
csv_output: bool = typer.Option(False, "--csv", help="CSV output"),
|
|
290
|
+
md_output: bool = typer.Option(False, "--md", help="Markdown output"),
|
|
291
|
+
toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
|
|
288
292
|
) -> None:
|
|
289
|
-
"""Resolve a string to a single channel
|
|
293
|
+
"""Resolve a string to a single channel.
|
|
290
294
|
|
|
291
295
|
Accepts:
|
|
292
296
|
- A partial channel name or slug (ILIKE match)
|
|
@@ -296,6 +300,10 @@ def find_cmd(
|
|
|
296
300
|
- A YouTube video URL — the video's channel is resolved via the
|
|
297
301
|
platform's article index
|
|
298
302
|
|
|
303
|
+
Default output is a pretty `id name` line on stdout. Pass --json /
|
|
304
|
+
--csv / --md / --toon for machine-readable output (the JSON shape is
|
|
305
|
+
`{"id": ..., "name": ...}`).
|
|
306
|
+
|
|
299
307
|
Ambiguous matches return an error with candidate IDs and names.
|
|
300
308
|
If the input is a YouTube URL and no channel matches, the URL is
|
|
301
309
|
queued for scraping; retry the command later.
|
|
@@ -306,12 +314,26 @@ def find_cmd(
|
|
|
306
314
|
tl channels find https://www.youtube.com/watch?v=dQw4w9WgXcQ
|
|
307
315
|
tl channels find UCX6OQ3DkcsbYNE6H8uQQuVA
|
|
308
316
|
"""
|
|
317
|
+
fmt = detect_format(json_output, csv_output, md_output, toon_output)
|
|
309
318
|
client = get_client()
|
|
310
319
|
try:
|
|
311
320
|
data = client.get("/channels/find", params={"q": query})
|
|
312
321
|
results = data.get("results", [])
|
|
313
322
|
record = results[0] if results else {}
|
|
314
|
-
|
|
323
|
+
if fmt == "table":
|
|
324
|
+
cid = record.get("id")
|
|
325
|
+
name = record.get("name")
|
|
326
|
+
if cid is None:
|
|
327
|
+
Console(stderr=True).print("[yellow]No match.[/yellow]")
|
|
328
|
+
raise typer.Exit(1)
|
|
329
|
+
Console().print(f"[bold cyan]{cid}[/bold cyan] {name}")
|
|
330
|
+
else:
|
|
331
|
+
output(
|
|
332
|
+
{"results": [{"id": record.get("id"), "name": record.get("name")}]},
|
|
333
|
+
fmt,
|
|
334
|
+
columns=["id", "name"],
|
|
335
|
+
title=f"Channel match for {query}",
|
|
336
|
+
)
|
|
315
337
|
except ApiError as e:
|
|
316
338
|
if e.status_code == 400 and isinstance(e.raw, dict) and e.raw.get("candidates"):
|
|
317
339
|
_print_channel_candidates(e.detail, e.raw["candidates"])
|
|
@@ -319,12 +341,10 @@ def find_cmd(
|
|
|
319
341
|
if e.status_code == 404 and isinstance(e.raw, dict) and e.raw.get("queued"):
|
|
320
342
|
err = Console(stderr=True)
|
|
321
343
|
err.print(f"[yellow]{e.detail}[/yellow]")
|
|
322
|
-
print(
|
|
323
|
-
"
|
|
324
|
-
"
|
|
325
|
-
|
|
326
|
-
"queued_url": e.raw.get("queued_url"),
|
|
327
|
-
}, ensure_ascii=False))
|
|
344
|
+
err.print(
|
|
345
|
+
f" [dim]queued_channel_id={e.raw.get('queued_channel_id')}"
|
|
346
|
+
f" queued_url={e.raw.get('queued_url')}[/dim]"
|
|
347
|
+
)
|
|
328
348
|
raise typer.Exit(1)
|
|
329
349
|
handle_api_error(e)
|
|
330
350
|
finally:
|
|
@@ -268,8 +268,8 @@ def inspect_brand_cmd(
|
|
|
268
268
|
client.close()
|
|
269
269
|
|
|
270
270
|
|
|
271
|
-
@app.command("
|
|
272
|
-
def
|
|
271
|
+
@app.command("channels-for-profile")
|
|
272
|
+
def channels_for_profile_cmd(
|
|
273
273
|
profile_id: int = typer.Argument(..., help="Profile ID (numeric)"),
|
|
274
274
|
args: list[str] = typer.Argument(None, help="Filters (key:value pairs)."),
|
|
275
275
|
json_output: bool = typer.Option(False, "--json", help="JSON output"),
|
|
@@ -288,8 +288,8 @@ def similar_to_profile_cmd(
|
|
|
288
288
|
msn:<yes|no> Restrict to MSN channels (default: no)
|
|
289
289
|
|
|
290
290
|
Examples:
|
|
291
|
-
tl recommender
|
|
292
|
-
tl recommender
|
|
291
|
+
tl recommender channels-for-profile 842
|
|
292
|
+
tl recommender channels-for-profile 842 msn:yes --limit 30
|
|
293
293
|
"""
|
|
294
294
|
fmt = detect_format(json_output, csv_output, md_output, toon_output)
|
|
295
295
|
filters = parse_filters(args or [])
|
|
@@ -315,8 +315,63 @@ def similar_to_profile_cmd(
|
|
|
315
315
|
client.close()
|
|
316
316
|
|
|
317
317
|
|
|
318
|
-
@app.command("
|
|
319
|
-
def
|
|
318
|
+
@app.command("channels-for-brand")
|
|
319
|
+
def channels_for_brand_cmd(
|
|
320
|
+
brand_ref: str = typer.Argument(..., help="Brand ID, name, slug, or domain (resolved via tl brands find)"),
|
|
321
|
+
args: list[str] = typer.Argument(None, help="Filters (key:value pairs)."),
|
|
322
|
+
json_output: bool = typer.Option(False, "--json", help="JSON output"),
|
|
323
|
+
csv_output: bool = typer.Option(False, "--csv", help="CSV output"),
|
|
324
|
+
md_output: bool = typer.Option(False, "--md", help="Markdown output"),
|
|
325
|
+
toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
|
|
326
|
+
limit: int = typer.Option(20, "--limit", "-l", help="Max results (1-100)"),
|
|
327
|
+
) -> None:
|
|
328
|
+
"""Channels closest to a brand's ideal similarity profile.
|
|
329
|
+
|
|
330
|
+
Resolves the brand to its most-recently-created profile that has an
|
|
331
|
+
indexed search vector and runs the same KNN that
|
|
332
|
+
`channels-for-profile` uses. Costs 25 credits per call. Intelligence
|
|
333
|
+
plan required. Channels the brand has already worked with or been
|
|
334
|
+
proposed are excluded.
|
|
335
|
+
|
|
336
|
+
Filters:
|
|
337
|
+
language:<iso> Content language (default: en)
|
|
338
|
+
msn:<yes|no> Restrict to MSN channels (default: no)
|
|
339
|
+
|
|
340
|
+
Examples:
|
|
341
|
+
tl recommender channels-for-brand 6037
|
|
342
|
+
tl recommender channels-for-brand Nike msn:yes --limit 30
|
|
343
|
+
"""
|
|
344
|
+
fmt = detect_format(json_output, csv_output, md_output, toon_output)
|
|
345
|
+
filters = parse_filters(args or [])
|
|
346
|
+
params = {k: v for k, v in filters.items() if k in {"language", "msn"}}
|
|
347
|
+
params["limit"] = str(limit)
|
|
348
|
+
encoded = urllib.parse.quote(brand_ref, safe="")
|
|
349
|
+
client = get_client()
|
|
350
|
+
try:
|
|
351
|
+
data = client.get(f"/recommender/brands/{encoded}/channels-for-profile", params=params)
|
|
352
|
+
for r in data.get("results", []):
|
|
353
|
+
score = r.get("score")
|
|
354
|
+
if isinstance(score, (int, float)) and fmt in ("table", "md"):
|
|
355
|
+
r["score"] = f"{score * 100:.1f}%"
|
|
356
|
+
title = f"Channels for brand {brand_ref}"
|
|
357
|
+
prof = data.get("profile") or {}
|
|
358
|
+
if prof.get("brand_name") and prof.get("id"):
|
|
359
|
+
title = f"Channels for {prof['brand_name']} (via profile {prof['id']})"
|
|
360
|
+
output(
|
|
361
|
+
data,
|
|
362
|
+
fmt,
|
|
363
|
+
columns=["score", "channel_id", "channel_name", "slug"],
|
|
364
|
+
title=title,
|
|
365
|
+
column_config={"score": {"justify": "right"}},
|
|
366
|
+
)
|
|
367
|
+
except ApiError as e:
|
|
368
|
+
handle_api_error(e)
|
|
369
|
+
finally:
|
|
370
|
+
client.close()
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
@app.command("brands-for-channel")
|
|
374
|
+
def brands_for_channel_cmd(
|
|
320
375
|
channel_ref: str = typer.Argument(..., help="Channel ID (numeric) or name (partial match, must be unique)"),
|
|
321
376
|
args: list[str] = typer.Argument(None, help="Filters (key:value pairs)."),
|
|
322
377
|
json_output: bool = typer.Option(False, "--json", help="JSON output"),
|
|
@@ -335,8 +390,8 @@ def similar_brands_to_channel_cmd(
|
|
|
335
390
|
mbn:<yes|no|all> MBN membership of the underlying profile (default: all)
|
|
336
391
|
|
|
337
392
|
Examples:
|
|
338
|
-
tl recommender
|
|
339
|
-
tl recommender
|
|
393
|
+
tl recommender brands-for-channel 12345
|
|
394
|
+
tl recommender brands-for-channel "MrBeast" mbn:yes --limit 30
|
|
340
395
|
"""
|
|
341
396
|
fmt = detect_format(json_output, csv_output, md_output, toon_output)
|
|
342
397
|
filters = parse_filters(args or [])
|
|
@@ -361,3 +416,17 @@ def similar_brands_to_channel_cmd(
|
|
|
361
416
|
_handle_recommender_error(e)
|
|
362
417
|
finally:
|
|
363
418
|
client.close()
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
@app.command("similar-brands-to-channel", hidden=True)
|
|
422
|
+
def similar_brands_to_channel_cmd(
|
|
423
|
+
channel_ref: str = typer.Argument(..., help="Channel ID (numeric) or name (partial match, must be unique)"),
|
|
424
|
+
args: list[str] = typer.Argument(None, help="Filters (key:value pairs)."),
|
|
425
|
+
json_output: bool = typer.Option(False, "--json", help="JSON output"),
|
|
426
|
+
csv_output: bool = typer.Option(False, "--csv", help="CSV output"),
|
|
427
|
+
md_output: bool = typer.Option(False, "--md", help="Markdown output"),
|
|
428
|
+
toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
|
|
429
|
+
limit: int = typer.Option(20, "--limit", "-l", help="Max results (1-100)"),
|
|
430
|
+
) -> None:
|
|
431
|
+
"""Hidden alias for `brands-for-channel` (older name kept for back-compat)."""
|
|
432
|
+
brands_for_channel_cmd(channel_ref, args, json_output, csv_output, md_output, toon_output, limit)
|
|
@@ -28,7 +28,6 @@ from tl_cli.commands.sponsorships import app as sponsorships_app
|
|
|
28
28
|
from tl_cli.commands.describe import app as describe_app
|
|
29
29
|
from tl_cli.commands.schema import app as schema_app
|
|
30
30
|
from tl_cli.commands.doctor import app as doctor_app
|
|
31
|
-
from tl_cli.commands.feedback import app as feedback_app
|
|
32
31
|
from tl_cli.commands.reports import app as reports_app
|
|
33
32
|
from tl_cli.commands.setup import app as setup_app
|
|
34
33
|
from tl_cli.commands.snapshots import app as snapshots_app
|
|
@@ -109,7 +108,6 @@ app.add_typer(balance_app, name="balance")
|
|
|
109
108
|
app.add_typer(credits_app, name="credits")
|
|
110
109
|
app.add_typer(doctor_app, name="doctor")
|
|
111
110
|
app.add_typer(whoami_app, name="whoami")
|
|
112
|
-
app.add_typer(feedback_app, name="feedback")
|
|
113
111
|
|
|
114
112
|
# `changelog` is a single command (not a sub-typer) so positional version args
|
|
115
113
|
# don't get interpreted as subcommand names.
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: tl-feedback
|
|
3
|
-
description: Compose and submit feedback about the current AI Agent session that used the `tl` CLI. Triggers on phrases like "send feedback", "report a problem with the session", "tell the team how this went", "submit session feedback", "feedback about tl", "this session was frustrating, send feedback", "tl feedback", "share this session with the team", "log issues from this run". Use it ONLY when the user wants to send a written report about the session to the ThoughtLeaders team — never on requests that just *use* `tl` to query data.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Send a session-feedback report via `tl feedback`
|
|
7
|
-
|
|
8
|
-
The `tl feedback` command POSTs a markdown-formatted message to the ThoughtLeaders team's `#ai-feedback` Slack channel. The server-side endpoint prepends the user/org context (user, email, organisation, links into Django admin), so the body you send should contain **only the session-specific report** — do not try to restate the user's name or org yourself.
|
|
9
|
-
|
|
10
|
-
This skill produces that body. Do not invoke `tl feedback` until every section below is written.
|
|
11
|
-
|
|
12
|
-
## When to invoke
|
|
13
|
-
|
|
14
|
-
Trigger this skill **only** when the user is explicitly asking for feedback to be sent — phrases like "send feedback", "log a problem with this session", "share these issues with the team", "submit session feedback", "tl feedback". Do **not** trigger it when the user is asking `tl` to fetch data, build a report, or perform any other analytical task; in those cases the `tl-cli:tl` or `tl-cli:tl-report-builder` skills are the right ones.
|
|
15
|
-
|
|
16
|
-
If you are not sure whether the user wants the feedback sent now or is just venting, ask one clarifying question before composing the body. Otherwise proceed.
|
|
17
|
-
|
|
18
|
-
## What the body must contain
|
|
19
|
-
|
|
20
|
-
Write the body to a scratch buffer (do not stream it directly to the shell) and only send it once all four sections are present. Use the **Slack mrkdwn subset** of markdown — Slack does not render standard markdown:
|
|
21
|
-
|
|
22
|
-
| Want | Slack mrkdwn |
|
|
23
|
-
| --- | --- |
|
|
24
|
-
| Bold | `*bold*` (single asterisk) |
|
|
25
|
-
| Italic | `_italic_` |
|
|
26
|
-
| Strike | `~strike~` |
|
|
27
|
-
| Inline code | `` `code` `` |
|
|
28
|
-
| Code block | triple backticks |
|
|
29
|
-
| Block quote | `> text` |
|
|
30
|
-
| Bullets | leading `• ` (or `- `) |
|
|
31
|
-
| Link | `<https://example.com|label>` |
|
|
32
|
-
|
|
33
|
-
`**double-asterisk bold**`, `[text](url)` links, and `#` headers render as **plain characters** in Slack — do not use them.
|
|
34
|
-
|
|
35
|
-
### Required sections, in this exact order
|
|
36
|
-
|
|
37
|
-
1. **One-sentence summary of the session.**
|
|
38
|
-
Lead with one sentence (no header, no bullets) describing what the user was trying to accomplish in this session and how it went overall (e.g. *"Pulled Q1 sponsored-channel rosters for three brands; ran into a sanitizer rejection on the third query and resolved it via `tl db pg`."*).
|
|
39
|
-
|
|
40
|
-
2. **User prompts and the work done.**
|
|
41
|
-
Section header `*User prompts and actions taken*`. Then a bulleted list — one bullet per distinct prompt the user issued in the session, in order. Each prompt is the parent bullet (a short paraphrase of what they asked, in quotes). Under each prompt, indent sub-bullets listing every concrete tool call or shell command you ran to handle it. Examples:
|
|
42
|
-
|
|
43
|
-
```
|
|
44
|
-
• _"Find me Holafly's sponsored channels in the last 6 months"_
|
|
45
|
-
• `tl db pg "SELECT DISTINCT c.id, c.channel_name FROM …"`
|
|
46
|
-
• `tl brands find Holafly`
|
|
47
|
-
• _"Now show their evergreenness scores"_
|
|
48
|
-
• `tl db es '{"aggs":{"channels":{"terms":{"field":"channel.id"}}}}'`
|
|
49
|
-
• `tl db pg "SELECT id, channel_name, evergreenness FROM thoughtleaders_channel WHERE id IN (…)"`
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
Include every prompt — even ones that were short clarifications. If you used a non-`tl` tool (`jq`, `duckdb`, Read, Edit, Bash), list it too. Do not dump full command output; one line per command is enough.
|
|
53
|
-
|
|
54
|
-
3. **Analysis section.**
|
|
55
|
-
Section header `*Analysis: problems, frustrations, weaknesses*`. Write a few short paragraphs (no bullets needed) covering, in this order:
|
|
56
|
-
- **Problems / data errors** encountered (e.g. sanitizer rejections, schema mismatches, missing fields, wrong field names, 5xx responses, slow endpoints). Quote the exact error text or command when relevant.
|
|
57
|
-
- **User frustrations** signalled in their messages (impatience, asking the same question twice, "this isn't what I asked for", profanity, abandoning a thread). Be honest — the team needs the actual signal, not a sanitised version. Do not editorialise about whether the frustration was justified.
|
|
58
|
-
- **Weaknesses / gaps in the `tl` CLI or skills** that surfaced. Concrete examples: missing filters, output formats that needed jq post-processing, schema docs that were wrong or out of date, commands that ought to exist but don't, places where you (the agent) had to guess.
|
|
59
|
-
- **What went well** — keep this short (one or two sentences). Skip the section if nothing notably went well.
|
|
60
|
-
|
|
61
|
-
If a category has nothing to report, write *"None observed."* under the heading rather than omitting the heading entirely. Do not invent problems to fill space.
|
|
62
|
-
|
|
63
|
-
4. **Anything else the user explicitly asked you to include.**
|
|
64
|
-
If the user's "send feedback" request contained a specific message ("tell them the new find command is great"), append it verbatim under a final `*From the user*` section as a block quote.
|
|
65
|
-
|
|
66
|
-
## After the body is ready
|
|
67
|
-
|
|
68
|
-
Send it with `tl feedback` — pass the body as a single argument (heredoc / `$(cat <<EOF …)` works well for multi-line content):
|
|
69
|
-
|
|
70
|
-
```bash
|
|
71
|
-
tl feedback "$(cat <<'EOF'
|
|
72
|
-
<the body you composed>
|
|
73
|
-
EOF
|
|
74
|
-
)"
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
On success the CLI prints `Thanks! Your feedback was sent to the team.` Confirm to the user with a brief, neutral one-liner ("Sent."). Do not echo the full body back to the user — they wrote it with you, they don't need to re-read it.
|
|
78
|
-
|
|
79
|
-
If `tl feedback` exits non-zero (network failure, server error), show the user the raw error and offer to retry. Do not silently swallow failures — feedback that didn't reach Slack is the worst possible outcome here.
|
|
80
|
-
|
|
81
|
-
## What NOT to do
|
|
82
|
-
|
|
83
|
-
- Do not prepend user name, email, organisation, or admin links to the body. The server adds those.
|
|
84
|
-
- Do not use `**bold**` or `[text](url)` markdown — Slack will print the literal characters.
|
|
85
|
-
- Do not split the report into multiple `tl feedback` calls. One message, one call.
|
|
86
|
-
- Do not invent prompts or commands you did not actually use. If you can't recall, write *"(earlier commands not captured)"*.
|
|
87
|
-
- Do not mark the task complete until `tl feedback` exits 0.
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
"""tl feedback — Send a markdown-formatted note to the #ai-feedback channel."""
|
|
2
|
-
|
|
3
|
-
import sys
|
|
4
|
-
|
|
5
|
-
import typer
|
|
6
|
-
from rich.console import Console
|
|
7
|
-
|
|
8
|
-
from tl_cli.client.errors import ApiError, handle_api_error
|
|
9
|
-
from tl_cli.client.http import get_client
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
app = typer.Typer(help="Send feedback about the CLI to the team (free)")
|
|
13
|
-
console = Console(stderr=True)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
@app.callback(invoke_without_command=True)
|
|
17
|
-
def feedback(
|
|
18
|
-
ctx: typer.Context,
|
|
19
|
-
text: str = typer.Argument(
|
|
20
|
-
None,
|
|
21
|
-
help='Markdown-formatted feedback. Omit to read from stdin (handy for piping or heredocs).',
|
|
22
|
-
),
|
|
23
|
-
) -> None:
|
|
24
|
-
"""Send a markdown-formatted note to the ThoughtLeaders team.
|
|
25
|
-
|
|
26
|
-
The server prepends your user/org context and posts everything to the
|
|
27
|
-
#ai-feedback Slack channel. Slack supports a subset of markdown —
|
|
28
|
-
`*bold*`, `_italic_`, `~strike~`, ``code``, ```fences```, `> quotes`,
|
|
29
|
-
and `<url|label>` links. Standard `**bold**`, `[text](url)` links,
|
|
30
|
-
and `#` headers render as plain text in Slack.
|
|
31
|
-
|
|
32
|
-
Examples:
|
|
33
|
-
tl feedback "The new *find* command is great, but it should also accept channel IDs from URLs."
|
|
34
|
-
echo "long note here" | tl feedback
|
|
35
|
-
tl feedback <<< "$(cat note.md)"
|
|
36
|
-
"""
|
|
37
|
-
if ctx.invoked_subcommand is not None:
|
|
38
|
-
return
|
|
39
|
-
|
|
40
|
-
body = text
|
|
41
|
-
if body is None:
|
|
42
|
-
if sys.stdin.isatty():
|
|
43
|
-
console.print('[red]Error:[/red] no feedback text provided.')
|
|
44
|
-
console.print('Pass the text as an argument, or pipe it via stdin.')
|
|
45
|
-
raise typer.Exit(1)
|
|
46
|
-
body = sys.stdin.read()
|
|
47
|
-
|
|
48
|
-
body = (body or '').strip()
|
|
49
|
-
if not body:
|
|
50
|
-
console.print('[red]Error:[/red] feedback text is empty.')
|
|
51
|
-
raise typer.Exit(1)
|
|
52
|
-
|
|
53
|
-
client = get_client()
|
|
54
|
-
try:
|
|
55
|
-
client.post('/feedback', json_body={'text': body})
|
|
56
|
-
except ApiError as e:
|
|
57
|
-
handle_api_error(e)
|
|
58
|
-
finally:
|
|
59
|
-
client.close()
|
|
60
|
-
|
|
61
|
-
console.print('[green]Thanks![/green] Your feedback was sent to the team.')
|
|
File without changes
|
{thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/.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.43 → thoughtleaders_cli-0.6.45}/skills/tl/references/business-glossary.md
RENAMED
|
File without changes
|
{thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/skills/tl/references/elasticsearch-schema.md
RENAMED
|
File without changes
|
{thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/skills/tl/references/firebolt-schema.md
RENAMED
|
File without changes
|
{thoughtleaders_cli-0.6.43 → thoughtleaders_cli-0.6.45}/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
|
|
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
|