thoughtleaders-cli 0.7.10__tar.gz → 0.7.11__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.7.10 → thoughtleaders_cli-0.7.11}/.claude-plugin/plugin.json +1 -1
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/PKG-INFO +1 -1
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/pyproject.toml +1 -1
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl/SKILL.md +13 -12
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl/references/elasticsearch-schema.md +22 -22
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl/references/firebolt-schema.md +1 -1
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl/references/postgres-schema.md +2 -2
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/src/tl_cli/__init__.py +1 -1
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/channels.py +6 -3
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/setup.py +129 -25
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/src/tl_cli/self_update.py +10 -2
- thoughtleaders_cli-0.7.11/tests/test_setup.py +149 -0
- thoughtleaders_cli-0.7.10/tests/test_setup.py +0 -41
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/.claude-plugin/marketplace.json +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/.github/workflows/python-publish.yml +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/.gitignore +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/AGENTS.md +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/API.md +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/CLAUDE.md +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/LICENSE +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/README.md +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/agents/tl-analyst.md +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/agents/youtube-comment-classifier.md +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/hooks/hooks.json +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/hooks/scripts/load-tl-skill.mjs +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/hooks/scripts/post-usage.sh +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/hooks/scripts/pre-check.sh +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl/references/business-glossary.md +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/.gitignore +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/SKILL.md +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/references/comment-patterns.md +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/references/peer-cohort.md +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/references/red-flags.md +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/references/scoring.md +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/scripts/_io_utf8.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/scripts/analyze_channel.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/scripts/anomaly_detector.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/scripts/comment_analyzer.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/scripts/comment_scraper.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/scripts/engagement_ratios.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/scripts/peer_cohort.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/scripts/report.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/scripts/resolve_channel.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/scripts/score.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/scripts/tl_cli.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/scripts/video_integrity.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/scripts/view_curves.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl-keyword-research/SKILL.md +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl-keyword-research/scripts/probe.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl-save-report/SKILL.md +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl-save-report/references/columns_brands.md +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl-save-report/references/columns_channels.md +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl-save-report/references/columns_content.md +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl-save-report/references/columns_sponsorships.md +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl-save-report/references/intelligence_filterset_schema.json +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl-save-report/references/intelligence_widget_schema.json +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl-save-report/references/report_glossary.md +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl-save-report/references/sortable_columns.json +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl-save-report/references/sponsorship_filterset_schema.json +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl-save-report/references/sponsorship_widget_schema.json +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl-save-report/references/widgets.md +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl-top-partnerships/SKILL.md +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl-top-partnerships/scripts/top_partnerships.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl-views-guarantee/SKILL.md +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl-views-guarantee/scripts/vg.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/src/tl_cli/_completions.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/src/tl_cli/_typer_utils.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/src/tl_cli/auth/__init__.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/src/tl_cli/auth/commands.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/src/tl_cli/auth/login.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/src/tl_cli/auth/pkce.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/src/tl_cli/auth/token_store.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/src/tl_cli/client/__init__.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/src/tl_cli/client/errors.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/src/tl_cli/client/http.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/__init__.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/_comments_common.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/balance.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/brands.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/bulk_import.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/changelog.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/credits.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/db.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/deals.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/describe.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/doctor.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/matches.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/proposals.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/recommender.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/reports.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/schema.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/snapshots.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/sponsorships.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/uploads.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/src/tl_cli/commands/whoami.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/src/tl_cli/config.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/src/tl_cli/filters.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/src/tl_cli/hints.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/src/tl_cli/main.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/src/tl_cli/output/__init__.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/src/tl_cli/output/formatter.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/tests/__init__.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/tests/test_auth.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/tests/test_describe.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/tests/test_filters.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/tests/test_http_auth.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/tests/test_output.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/tests/test_reports.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/tests/test_sponsorships.py +0 -0
- {thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: thoughtleaders-cli
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.11
|
|
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
|
|
@@ -9,7 +9,7 @@ description: |
|
|
|
9
9
|
|
|
10
10
|
## Core Principles
|
|
11
11
|
|
|
12
|
-
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.
|
|
12
|
+
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. One exception: resolving a named channel or brand (name, YouTube URL, @handle, video URL) to an ID is always `tl channels find` / `tl brands find` — never `ILIKE` on names.
|
|
13
13
|
|
|
14
14
|
If doing a database query, follow this recipe:
|
|
15
15
|
|
|
@@ -25,13 +25,13 @@ If doing a database query, follow this recipe:
|
|
|
25
25
|
```bash
|
|
26
26
|
tl db pg "SELECT id, weighted_price FROM thoughtleaders_adlink
|
|
27
27
|
WHERE publish_status = 3 AND price > 5000
|
|
28
|
-
LIMIT
|
|
28
|
+
LIMIT 10000 OFFSET 0" --json \
|
|
29
29
|
| jq '.results[] | {id, price: .weighted_price}'
|
|
30
30
|
```
|
|
31
31
|
- **`yq`** — same idea for YAML/TOML, useful when reading config files or `--md` blocks.
|
|
32
32
|
- **`rg`** — fast text search across CLI output, transcripts, and the codebase. Better than `grep` for searching large `--csv` exports or transcript dumps from ES.
|
|
33
33
|
```bash
|
|
34
|
-
tl db es '{"size":
|
|
34
|
+
tl db es '{"size":10000,"query":{"term":{"channel.id":5607}},"_source":["id","transcript"]}' --json | rg -o "NordVPN[^.]*"
|
|
35
35
|
```
|
|
36
36
|
- **`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.
|
|
37
37
|
```bash
|
|
@@ -42,13 +42,13 @@ If doing a database query, follow this recipe:
|
|
|
42
42
|
JOIN thoughtleaders_brand b ON b.id = pb.brand_id
|
|
43
43
|
WHERE al.publish_status = 3
|
|
44
44
|
AND al.purchase_date >= '2026-01-01'
|
|
45
|
-
LIMIT
|
|
45
|
+
LIMIT 10000 OFFSET 0" --csv > deals.csv
|
|
46
46
|
duckdb -c "SELECT brand, SUM(price) AS revenue FROM 'deals.csv' GROUP BY brand ORDER BY revenue DESC LIMIT 10"
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
The pattern is always: server-side narrowing first (usually by filters in the `tl db` query, but could be from similarity / recommender 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.
|
|
50
50
|
|
|
51
|
-
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.
|
|
51
|
+
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. Prefer large pages (up to the engine's cap) to minimize round-trips; the per-engine page-size caps are documented in each engine's schema reference under `references/`.
|
|
52
52
|
|
|
53
53
|
**Counts, totals, and breakdowns: aggregate in the query engine — never page through records to count them.** A "how many / total / average / per-X" question is ONE aggregation query, not N pages of rows summed in your head:
|
|
54
54
|
- `tl db pg` — `SELECT COUNT(*) …`, or `SELECT col, COUNT(*) AS n … GROUP BY col ORDER BY n DESC`. Also `SUM`/`AVG`/`MIN`/`MAX`/`date_trunc`. Returns one/few rows regardless of table size. (`LIMIT`/`OFFSET` still required — an aggregate is one row, so `LIMIT 1 OFFSET 0` is fine.)
|
|
@@ -95,7 +95,8 @@ Other key concepts:
|
|
|
95
95
|
- **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.
|
|
96
96
|
- **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.
|
|
97
97
|
- **`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.
|
|
98
|
-
- **`
|
|
98
|
+
- **`reach`** (on channels) — subscriber count. ⚠️ Despite the name, this is NOT ad-industry "reach" (unique audience exposed). There is no `subscribers` field — `reach` is it.
|
|
99
|
+
- **`impression`** (on channels) — projected views per video on that channel. Forward-looking estimate. May be null when not yet computed. ⚠️ NOT actual views and NOT ad-industry "impressions" (ads served).
|
|
99
100
|
- **`views`** (on sponsorships) — actual view count of the sold and published sponsored video, accessible when `article_id` is set.
|
|
100
101
|
- **`impressions_guarantee`** (on sponsorships) — projected/guaranteed impressions for the sponsorship. Numeric.
|
|
101
102
|
- **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.
|
|
@@ -147,7 +148,7 @@ Unless the user specifically asks for running a specific report or showing the r
|
|
|
147
148
|
|
|
148
149
|
1. **Discover first**: Use `tl schema pg`, `tl schema es`, and `tl schema fb` to find information about the main database (pg), the articles / uploads database (es), and the channel metrics database (fb).
|
|
149
150
|
2. **Check credits**: Run `tl balance --json` before expensive queries. Warn the user if a query will cost many credits.
|
|
150
|
-
3. **Decide the method of discovery**: If the user
|
|
151
|
+
3. **Decide the method of discovery**: If the user named a specific channel, brand, or creator (a name, YouTube URL, @handle, or video URL), resolve it to an ID with `tl channels find` / `tl brands find` before anything else. If the user wants to explore certain topics, use the recommender commands. If it's more about filtering, construct a query for PG or ES.
|
|
151
152
|
4. **Always use --json**: Parse JSON output for multi-step analysis.
|
|
152
153
|
5. **Chain commands**: For complex questions, chain multiple `tl` commands, shell commands, and other tools.
|
|
153
154
|
6. **Format results**: When the user asks for a list or tabular data, present the results as a well-formatted markdown table. Pick the most relevant columns and use clear headers. Sort the result by relevant criteria - if the user asked for "top performers", order by the performance metric; if the user asked for "most recent", sort by the pertinent date desc.
|
|
@@ -445,7 +446,7 @@ If unsure about what information to find where, read the [references/postgresql-
|
|
|
445
446
|
| Pre-insert validation queries (joining `adspot ↔ channel ↔ profile ↔ org` to confirm MSN, integration=1, persona, plan) | **Available** via `tl db pg`. | One SELECT joining the four tables. Use `thoughtleaders_channel.media_selling_network_join_date IS NOT NULL` for MSN, `thoughtleaders_adspot.integration = 1` for mention adspots, `thoughtleaders_profile.persona` for the persona code (see persona constants in `references/postgres-schema.md`). |
|
|
446
447
|
| Firebolt cross-table or join queries; filtering on non-indexed columns in WHERE | **Unavailable** — not accepted. | Fetch a wider slice keyed on `channel_id` (and optionally `id`), filter the rest in `jq`/Python. |
|
|
447
448
|
| ES `query_string`, `regexp`, `wildcard`, `fuzzy`, `more_like_this`, parent/child joins; any `script_*`; multiple aggregations in one body | **Unavailable** — not accepted. | Rewrite using `term`/`terms`/`match`/`bool`/`nested`. For multi-agg dashboards, run multiple `tl db es` calls and combine client-side. For "similar"-style queries, try `tl channels similar` / `tl brands similar` (server-implemented similarity search). |
|
|
448
|
-
| ES deep pagination beyond `from+size = 10,000` | **Unavailable**
|
|
449
|
+
| ES deep pagination beyond `from+size = 10,000` | **Unavailable** — `scroll`, `pit`, and `search_after` aren't allowlisted; hits past the first 10,000 of a query are unreachable. | A single `size: 10000` page covers everything reachable. For bigger sweeps, slice into `publication_date` range windows of <10k hits each. |
|
|
449
450
|
| ES index introspection (`_cat/indices`, mappings) | **Unavailable** — only `_search` is wired. | Read [references/elasticsearch-schema.md](references/elasticsearch-schema.md). It's manually maintained — update it when you discover new fields. |
|
|
450
451
|
| Schema introspection on Postgres (`information_schema.columns`, `pg_class`, …) | **Partial** — catalog-resolving casts and many `pg_*` helpers are blocked. | Use `tl schema pg` for the live table/column listing, or read [references/postgres-schema.md](references/postgres-schema.md). |
|
|
451
452
|
|
|
@@ -485,7 +486,7 @@ tl channels find "MrBeast"
|
|
|
485
486
|
tl brands find "NordVPN"
|
|
486
487
|
```
|
|
487
488
|
|
|
488
|
-
|
|
489
|
+
`tl channels find` resolves spacing/typo variants on its own ("Deco Destiny" → "DecoDestiny") via YouTube lookups and fuzzy similarity matching — no need to retry with hand-made name variations. A real channel that isn't in the index yet gets queued for analysis automatically (the response says to check back in ~24 hours). A plain "Not found" means even YouTube couldn't find it — treat that as the answer.
|
|
489
490
|
|
|
490
491
|
**Path 2. Curated tag / category / demographic** — user named a topic that maps cleanly to a recommender tag (`"Cooking"`, `"Tech"`, `"USA share"`, content categories, format hints). Use the recommender — it ranks channels by how strongly they load on a tag, returning ranked similarity scores instead of forcing exact equality. It also returns matching brand profiles alongside the channels — useful when the user wants to know "who buys this kind of inventory."
|
|
491
492
|
|
|
@@ -559,7 +560,7 @@ For per-country share beyond the recommender's "USA share" tag, use the `demogra
|
|
|
559
560
|
|
|
560
561
|
**MSN status (`media_selling_network_join_date`) is scrubbed from the advertiser sandbox view.** Raw SQL can't filter on it from an advertiser context. For MSN-only / non-MSN lookups, run the same raw SQL with `media_selling_network_join_date IS [NOT] NULL` from a context that has access to it (full-access role), or rely on the recommender's MSN-aware filters: `tl recommender top-channels "<tag>" msn:yes|no|all`.
|
|
561
562
|
|
|
562
|
-
**Anti-pattern: defaulting to `ILIKE` on `channel_name` for off-tag topic queries.** If the question is "channels about X" where X is a topic / concept / niche (not a literal substring you expect in channel names), reach for path 3 (`tl-keyword-research`), not `WHERE channel_name ILIKE '%X%'`. Channel-name `ILIKE` misses channels whose name doesn't literally contain X but whose content does; the keyword-research skill catches them via `title` / `summary` / `transcript`. Use `channel_name ILIKE` only when you actually expect the channel's name to contain the term (e.g. `"Crypto"` in `"My Happy Crypto"`) as a supplementary signal alongside path 3, not as a replacement for it.
|
|
563
|
+
**Anti-pattern: defaulting to `ILIKE` on `channel_name` for off-tag topic queries.** If the question is "channels about X" where X is a topic / concept / niche (not a literal substring you expect in channel names), reach for path 3 (`tl-keyword-research`), not `WHERE channel_name ILIKE '%X%'`. Channel-name `ILIKE` misses channels whose name doesn't literally contain X but whose content does; the keyword-research skill catches them via `title` / `summary` / `transcript`. Use `channel_name ILIKE` only when you actually expect the channel's name to contain the term (e.g. `"Crypto"` in `"My Happy Crypto"`) as a supplementary signal alongside path 3, not as a replacement for it. And for a *named entity* — a specific creator or channel — don't start with `ILIKE` at all: run `tl channels find "<name>"` first (path 1). Fall back to `ILIKE` name variations only if the resolver finds nothing, and treat a clean "Not found" from the resolver as the likely answer (the channel probably isn't in the index) rather than a cue for ever-broader scans.
|
|
563
564
|
|
|
564
565
|
### Output flags
|
|
565
566
|
- `--json` — structured JSON output format (use this for parsing)
|
|
@@ -617,7 +618,7 @@ tl db pg "SELECT al.id, al.weighted_price, al.purchase_date, b.name AS brand
|
|
|
617
618
|
WHERE al.publish_status = 3
|
|
618
619
|
AND al.purchase_date >= '2026-01-01'
|
|
619
620
|
ORDER BY al.purchase_date DESC
|
|
620
|
-
LIMIT
|
|
621
|
+
LIMIT 10000 OFFSET 0" --json
|
|
621
622
|
```
|
|
622
623
|
|
|
623
624
|
### Brand sponsorship history — what channels does Nike sponsor?
|
|
@@ -769,7 +770,7 @@ tl db pg "SELECT al.id, c.channel_name, c.demographic_device_primary, c.demograp
|
|
|
769
770
|
WHERE al.publish_status = 3
|
|
770
771
|
AND c.demographic_device_primary = 'mobile'
|
|
771
772
|
AND c.demographic_usa_share >= 60
|
|
772
|
-
LIMIT
|
|
773
|
+
LIMIT 10000 OFFSET 0" --json
|
|
773
774
|
```
|
|
774
775
|
|
|
775
776
|
### "Find channels similar to one I know" (similarity recommender):
|
{thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl/references/elasticsearch-schema.md
RENAMED
|
@@ -21,19 +21,19 @@ Output flags: `--json`, `--csv`, `--md`, `--toon`. The CLI flattens hits into ro
|
|
|
21
21
|
|
|
22
22
|
See the output of `tl db es`" for the object schema. Highlights:
|
|
23
23
|
|
|
24
|
-
- **Top-level keys** accepted: `query`, `aggs`/`aggregations`, `sort`, `_source`, `size`, `from`, `track_total_hits`, `highlight`, `fields`, `min_score`, `
|
|
25
|
-
- `size` ≤
|
|
24
|
+
- **Top-level keys** accepted: `query`, `aggs`/`aggregations`, `sort`, `_source`, `size`, `from`, `track_total_hits`, `highlight`, `fields`, `min_score`, `timeout`, `collapse`, `post_filter`. Anything else (incl. `scroll`, `pit`, `search_after`, `runtime_mappings`, `knn`) is not accepted.
|
|
25
|
+
- `size` ≤ 10,000. `from + size` ≤ 10,000 — hits beyond the first 10,000 of a query are unreachable; narrow the query (e.g. `publication_date` ranges) instead of paging deeper.
|
|
26
26
|
- **Accepted query types** include `term`/`terms`/`match`/`bool`/`nested`/`range`/`exists`/`match_phrase`. `query_string`, `regexp`, `wildcard`, `fuzzy`, `more_like_this`, `has_child`, `has_parent`, `parent_id` are not accepted.
|
|
27
27
|
- **No scripts** — any key whose name contains `script` is not accepted.
|
|
28
28
|
- **At most one aggregation total** counted recursively (top-level + sub-agg = 2 = not accepted). Run multiple calls for multi-metric work.
|
|
29
29
|
|
|
30
30
|
### ElasticSearch document structure ("articles")
|
|
31
31
|
|
|
32
|
-
The `doc_type
|
|
32
|
+
The `doc_type` join field distinguishes video uploads ("articles") from channel data — channel docs are parents, article docs are their children. Filter with `{"term": {"doc_type": "article"}}` or `{"term": {"doc_type": "channel"}}`. ⚠️ Term-querying `doc_type.name` matches nothing — even though article docs' `_source` shows `doc_type` as an object with a `name` key, that's join-field syntax, not a queryable subfield.
|
|
33
33
|
|
|
34
34
|
#### Upload/video Fields (selected — 73 total)
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
Filter with `{"term": {"doc_type": "article"}}`.
|
|
37
37
|
|
|
38
38
|
| Field | Type | Description |
|
|
39
39
|
|-------|------|-------------|
|
|
@@ -80,7 +80,7 @@ Distinguished by `doc_type.name="article"`.
|
|
|
80
80
|
|
|
81
81
|
#### Channel Fields
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
Filter with `{"term": {"doc_type": "channel"}}`.
|
|
84
84
|
|
|
85
85
|
Contains a denormalized subset of the PostgreSQL channel data.
|
|
86
86
|
|
|
@@ -90,10 +90,10 @@ Contains a denormalized subset of the PostgreSQL channel data.
|
|
|
90
90
|
|-------|------|-------------|
|
|
91
91
|
| `name` | text | Channel name |
|
|
92
92
|
| `channel` | object | Channel metadata (nested on article docs) |
|
|
93
|
-
| `reach` | long | Subscriber count |
|
|
94
|
-
| `impression` | long |
|
|
95
|
-
| `impression_live` | long |
|
|
96
|
-
| `impression_shorts` | long |
|
|
93
|
+
| `reach` | long | Subscriber count. ⚠️ NOT ad-industry "reach" (unique audience exposed) — this is the channel's subscriber count. |
|
|
94
|
+
| `impression` | long | Projected views per longform video — forward-looking estimate. ⚠️ NOT actual views and NOT ad-industry "impressions"; for actual views see `total_views` / the video docs. |
|
|
95
|
+
| `impression_live` | long | Projected views per live stream (forward-looking estimate) |
|
|
96
|
+
| `impression_shorts` | long | Projected views per short (forward-looking estimate) |
|
|
97
97
|
| `is_tl_channel` | boolean | TPP partner channel |
|
|
98
98
|
| `is_active` | boolean | Channel is active |
|
|
99
99
|
| `media_selling_network_join_date` | date | MSN join date |
|
|
@@ -215,25 +215,25 @@ tl db es '{
|
|
|
215
215
|
|
|
216
216
|
For more dimensions, run multiple `tl db es` calls and join client-side.
|
|
217
217
|
|
|
218
|
-
### Deep
|
|
218
|
+
### Deep sweeps — window by date, don't page past 10k
|
|
219
219
|
|
|
220
|
-
|
|
221
|
-
# First page — sort must include a tiebreaker on _id for stability
|
|
222
|
-
tl db es '{
|
|
223
|
-
"size": 500,
|
|
224
|
-
"query": {"term": {"channel.id": 12345}},
|
|
225
|
-
"sort": [{"publication_date": "desc"}, {"_id": "asc"}]
|
|
226
|
-
}'
|
|
220
|
+
`from + size` is capped at 10,000 and cursor keys (`search_after`, `scroll`, `pit`) are not accepted, so hits beyond the first 10,000 of any one query are unreachable. For result sets bigger than that, slice the query into non-overlapping `publication_date` (or other range-field) windows, each under 10,000 hits, and sweep window by window:
|
|
227
221
|
|
|
228
|
-
|
|
222
|
+
```bash
|
|
223
|
+
# One window — repeat with shifted date ranges until the full period is covered
|
|
229
224
|
tl db es '{
|
|
230
|
-
"size":
|
|
231
|
-
"
|
|
232
|
-
"
|
|
233
|
-
|
|
225
|
+
"size": 10000,
|
|
226
|
+
"track_total_hits": true,
|
|
227
|
+
"query": {"bool": {"filter": [
|
|
228
|
+
{"term": {"channel.id": 12345}},
|
|
229
|
+
{"range": {"publication_date": {"gte": "2025-01-01", "lt": "2025-04-01"}}}
|
|
230
|
+
]}},
|
|
231
|
+
"sort": [{"publication_date": "asc"}]
|
|
234
232
|
}'
|
|
235
233
|
```
|
|
236
234
|
|
|
235
|
+
Check `total` per window — if a window exceeds 10,000, split it further.
|
|
236
|
+
|
|
237
237
|
## Text analyzer behavior
|
|
238
238
|
|
|
239
239
|
`text` fields on article docs (`title`, `summary`, `transcript`) appear to use the `standard` analyzer (tokenize + lowercase, no stemmer, no English-possessive filter), so inflections, plurals, and possessives are each indexed as distinct terms. For example: `bitcoin` (4,466,300) vs `bitcoins` (489,262). For stemming-style recall, expand the query side with a `bool.should` over the variants.
|
{thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl/references/firebolt-schema.md
RENAMED
|
@@ -130,7 +130,7 @@ tl db pg "SELECT al.id, al.article_id, s.channel_id
|
|
|
130
130
|
WHERE al.publish_status = 3
|
|
131
131
|
AND b.name = 'Nike'
|
|
132
132
|
AND al.article_id IS NOT NULL
|
|
133
|
-
LIMIT
|
|
133
|
+
LIMIT 10000 OFFSET 0" --json \
|
|
134
134
|
| jq -r '.results[] | "\(.channel_id):\(.article_id)"'
|
|
135
135
|
|
|
136
136
|
# Or videos via Elasticsearch content search
|
{thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl/references/postgres-schema.md
RENAMED
|
@@ -315,7 +315,7 @@ FROM thoughtleaders_adlink
|
|
|
315
315
|
WHERE publish_status = 3
|
|
316
316
|
AND purchase_date >= date_trunc('month', CURRENT_DATE)
|
|
317
317
|
ORDER BY purchase_date DESC
|
|
318
|
-
LIMIT
|
|
318
|
+
LIMIT 10000 OFFSET 0
|
|
319
319
|
```
|
|
320
320
|
|
|
321
321
|
**MSN channel joins this month:**
|
|
@@ -324,7 +324,7 @@ SELECT id, channel_name, media_selling_network_join_date
|
|
|
324
324
|
FROM thoughtleaders_channel
|
|
325
325
|
WHERE media_selling_network_join_date >= date_trunc('month', CURRENT_DATE)
|
|
326
326
|
ORDER BY media_selling_network_join_date DESC
|
|
327
|
-
LIMIT
|
|
327
|
+
LIMIT 10000 OFFSET 0
|
|
328
328
|
```
|
|
329
329
|
|
|
330
330
|
**A specific sponsorship info with brand and channel name:**
|
|
@@ -296,7 +296,9 @@ def find_cmd(
|
|
|
296
296
|
"""Resolve a string to a single channel.
|
|
297
297
|
|
|
298
298
|
Accepts:
|
|
299
|
-
- A partial channel name or slug (
|
|
299
|
+
- A partial channel name or slug (substring match, falling back to
|
|
300
|
+
fuzzy similarity — spacing/typo variants like "Deco Destiny" still
|
|
301
|
+
resolve a channel named "DecoDestiny")
|
|
300
302
|
- A YouTube channel URL (https://youtube.com/channel/UC...,
|
|
301
303
|
https://youtube.com/@handle, /c/<name>, /user/<name>)
|
|
302
304
|
- A raw YouTube channel ID (UC...) or @handle
|
|
@@ -308,8 +310,9 @@ def find_cmd(
|
|
|
308
310
|
`{"id": ..., "name": ...}`).
|
|
309
311
|
|
|
310
312
|
Ambiguous matches return an error with candidate IDs and names.
|
|
311
|
-
If the input is a YouTube URL
|
|
312
|
-
|
|
313
|
+
If the input is a YouTube URL — or a name that YouTube resolves to
|
|
314
|
+
a channel not yet in the index — it is queued for analysis; check
|
|
315
|
+
back in about 24 hours.
|
|
313
316
|
|
|
314
317
|
Examples:
|
|
315
318
|
tl channels find "MrBeast"
|
|
@@ -7,9 +7,12 @@ whenever either `gemini` or `codex` is on PATH. Behaviour follows the
|
|
|
7
7
|
OpenCode pattern (full per-skill tree copy, .tl-version stamp).
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
+
import filecmp
|
|
10
11
|
import json
|
|
12
|
+
import os
|
|
11
13
|
import shutil
|
|
12
14
|
import subprocess
|
|
15
|
+
import sys
|
|
13
16
|
from pathlib import Path
|
|
14
17
|
|
|
15
18
|
import typer
|
|
@@ -41,6 +44,19 @@ OPENCODE_SKILLS_DIR = Path.home() / ".config" / "opencode" / "skills"
|
|
|
41
44
|
AGENTS_SKILLS_DIR = Path.home() / ".agents" / "skills"
|
|
42
45
|
AGENTS_SKILLS_BINARIES = ("gemini", "codex")
|
|
43
46
|
|
|
47
|
+
# Personal-command shim that keeps the short `/tl` invocation working when the
|
|
48
|
+
# skills are provided (namespaced) by the installed plugin. Plugin skills and
|
|
49
|
+
# commands are always invoked as `/tl-cli:<name>`; this one-file pointer in
|
|
50
|
+
# ~/.claude/commands/ restores plain `/tl` without duplicating any skill
|
|
51
|
+
# content, so plugin updates flow through automatically.
|
|
52
|
+
TL_COMMAND_SHIM = """\
|
|
53
|
+
---
|
|
54
|
+
description: ThoughtLeaders data analyst — shortcut for the tl-cli plugin's tl skill
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
Invoke the `tl-cli:tl` skill with this request: $ARGUMENTS
|
|
58
|
+
"""
|
|
59
|
+
|
|
44
60
|
|
|
45
61
|
def _find_plugin_root() -> Path | None:
|
|
46
62
|
"""Locate the plugin assets directory.
|
|
@@ -61,8 +77,31 @@ def _find_plugin_root() -> Path | None:
|
|
|
61
77
|
|
|
62
78
|
|
|
63
79
|
def _find_claude_binary() -> str | None:
|
|
64
|
-
"""Find the claude binary on PATH.
|
|
65
|
-
|
|
80
|
+
"""Find the claude binary on PATH, falling back to known install locations.
|
|
81
|
+
|
|
82
|
+
On Windows the Claude Code installers often don't end up on the PATH of
|
|
83
|
+
the shell running `tl` (stale PATH, PowerShell-only profile changes), so
|
|
84
|
+
after `shutil.which` we probe the documented install targets directly:
|
|
85
|
+
the native installer (`~/.local/bin`) and the npm global prefix.
|
|
86
|
+
"""
|
|
87
|
+
found = shutil.which("claude")
|
|
88
|
+
if found:
|
|
89
|
+
return found
|
|
90
|
+
home = Path.home()
|
|
91
|
+
if sys.platform == "win32":
|
|
92
|
+
candidates = [
|
|
93
|
+
home / ".local" / "bin" / "claude.exe",
|
|
94
|
+
Path(os.environ.get("APPDATA", str(home / "AppData" / "Roaming"))) / "npm" / "claude.cmd",
|
|
95
|
+
]
|
|
96
|
+
else:
|
|
97
|
+
candidates = [
|
|
98
|
+
home / ".local" / "bin" / "claude",
|
|
99
|
+
home / ".claude" / "local" / "claude",
|
|
100
|
+
]
|
|
101
|
+
for candidate in candidates:
|
|
102
|
+
if candidate.is_file():
|
|
103
|
+
return str(candidate)
|
|
104
|
+
return None
|
|
66
105
|
|
|
67
106
|
|
|
68
107
|
def _run_claude(args: list[str], claude_bin: str) -> tuple[bool, str]:
|
|
@@ -156,6 +195,50 @@ def _install_standalone_skills(plugin_root: Path) -> int:
|
|
|
156
195
|
return count
|
|
157
196
|
|
|
158
197
|
|
|
198
|
+
def _install_command_shim() -> Path:
|
|
199
|
+
"""Write the `/tl` shim command to ~/.claude/commands/tl.md."""
|
|
200
|
+
CLAUDE_COMMANDS_DIR.mkdir(parents=True, exist_ok=True)
|
|
201
|
+
dst = CLAUDE_COMMANDS_DIR / "tl.md"
|
|
202
|
+
dst.write_text(TL_COMMAND_SHIM, encoding="utf-8")
|
|
203
|
+
return dst
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _trees_identical(a: Path, b: Path) -> bool:
|
|
207
|
+
"""True if two directory trees contain the same files with the same contents."""
|
|
208
|
+
a_files = sorted(p.relative_to(a) for p in a.rglob("*") if p.is_file())
|
|
209
|
+
b_files = sorted(p.relative_to(b) for p in b.rglob("*") if p.is_file())
|
|
210
|
+
if a_files != b_files:
|
|
211
|
+
return False
|
|
212
|
+
return all(filecmp.cmp(a / rel, b / rel, shallow=False) for rel in a_files)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _remove_matching_standalone_skills(plugin_root: Path) -> tuple[int, int]:
|
|
216
|
+
"""Remove standalone copies in ~/.claude/skills/ that match the plugin's skills.
|
|
217
|
+
|
|
218
|
+
Earlier versions of `tl setup claude` copied every bundled skill into
|
|
219
|
+
~/.claude/skills/. Now that the plugin provides them, those copies are
|
|
220
|
+
redundant — but a copy is only deleted when its tree is byte-identical
|
|
221
|
+
to the bundled skill, so user-modified copies are never touched.
|
|
222
|
+
Returns (removed, kept_modified).
|
|
223
|
+
"""
|
|
224
|
+
removed = kept = 0
|
|
225
|
+
skills_src = plugin_root / "skills"
|
|
226
|
+
if not skills_src.is_dir():
|
|
227
|
+
return removed, kept
|
|
228
|
+
for skill_dir in skills_src.iterdir():
|
|
229
|
+
if not (skill_dir.is_dir() and (skill_dir / "SKILL.md").is_file()):
|
|
230
|
+
continue
|
|
231
|
+
standalone = CLAUDE_SKILLS_DIR / skill_dir.name
|
|
232
|
+
if not standalone.is_dir():
|
|
233
|
+
continue
|
|
234
|
+
if _trees_identical(skill_dir, standalone):
|
|
235
|
+
shutil.rmtree(standalone)
|
|
236
|
+
removed += 1
|
|
237
|
+
else:
|
|
238
|
+
kept += 1
|
|
239
|
+
return removed, kept
|
|
240
|
+
|
|
241
|
+
|
|
159
242
|
def _bundled_skill_blurbs(plugin_root: Path) -> list[tuple[str, str]]:
|
|
160
243
|
"""Read (name, tl-blurb) for each bundled skill, for the setup summary.
|
|
161
244
|
|
|
@@ -192,18 +275,19 @@ def _bundled_skill_blurbs(plugin_root: Path) -> list[tuple[str, str]]:
|
|
|
192
275
|
|
|
193
276
|
|
|
194
277
|
def _print_manual_instructions() -> None:
|
|
195
|
-
"""Print manual install instructions when
|
|
278
|
+
"""Print manual install instructions when the plugin couldn't be installed."""
|
|
196
279
|
console.print()
|
|
197
|
-
console.print("[yellow]Claude Code
|
|
280
|
+
console.print("[yellow]The Claude Code plugin could not be installed automatically.[/yellow]")
|
|
198
281
|
console.print()
|
|
199
|
-
console.print("
|
|
282
|
+
console.print(f"The skills were installed to {CLAUDE_SKILLS_DIR} instead — restart")
|
|
283
|
+
console.print("Claude Code and they will be available (e.g. [cyan]/tl[/cyan]).")
|
|
284
|
+
console.print()
|
|
285
|
+
console.print("To install the full plugin, run these commands inside Claude Code:")
|
|
200
286
|
console.print()
|
|
201
287
|
console.print(f" [cyan]/plugin marketplace add {MARKETPLACE_SOURCE}[/cyan]")
|
|
202
288
|
console.print(f" [cyan]/plugin install {PLUGIN_KEY}[/cyan]")
|
|
203
289
|
console.print()
|
|
204
|
-
console.print("
|
|
205
|
-
console.print()
|
|
206
|
-
console.print(f" [cyan]claude --plugin-dir /path/to/tl-cli[/cyan]")
|
|
290
|
+
console.print("then re-run [cyan]tl setup claude[/cyan] to clean up the standalone copies.")
|
|
207
291
|
|
|
208
292
|
|
|
209
293
|
@app.command("claude")
|
|
@@ -214,8 +298,10 @@ def setup_claude(
|
|
|
214
298
|
"""Install the TL CLI plugin for Claude Code.
|
|
215
299
|
|
|
216
300
|
Registers the ThoughtLeaders marketplace, installs the tl-cli plugin,
|
|
217
|
-
and
|
|
218
|
-
|
|
301
|
+
and adds a /tl shim command so the plugin's tl skill can be invoked
|
|
302
|
+
without the plugin namespace. Standalone skill copies in ~/.claude/skills
|
|
303
|
+
are only installed as a fallback when the plugin can't be installed;
|
|
304
|
+
unmodified copies left by earlier versions are removed.
|
|
219
305
|
|
|
220
306
|
Examples:
|
|
221
307
|
tl setup claude
|
|
@@ -249,10 +335,9 @@ def setup_claude(
|
|
|
249
335
|
# Check claude binary
|
|
250
336
|
claude_bin = _find_claude_binary()
|
|
251
337
|
if not claude_bin:
|
|
252
|
-
#
|
|
253
|
-
console.print(" [yellow]![/yellow] claude binary not found
|
|
338
|
+
# Fall back to standalone skill copies when the plugin can't be installed
|
|
339
|
+
console.print(" [yellow]![/yellow] claude binary not found")
|
|
254
340
|
_install_standalone_skills_step(plugin_root)
|
|
255
|
-
console.print()
|
|
256
341
|
_print_manual_instructions()
|
|
257
342
|
raise SystemExit(1)
|
|
258
343
|
|
|
@@ -271,6 +356,7 @@ def setup_claude(
|
|
|
271
356
|
_run_claude(["plugin", "marketplace", "update", MARKETPLACE_NAME], claude_bin)
|
|
272
357
|
else:
|
|
273
358
|
console.print(f" [red]✗[/red] Marketplace registration failed: {output}")
|
|
359
|
+
_install_standalone_skills_step(plugin_root)
|
|
274
360
|
_print_manual_instructions()
|
|
275
361
|
raise SystemExit(1)
|
|
276
362
|
|
|
@@ -284,12 +370,20 @@ def setup_claude(
|
|
|
284
370
|
console.print(f" [green]✓[/green] Plugin already installed: {PLUGIN_KEY}")
|
|
285
371
|
else:
|
|
286
372
|
console.print(f" [red]✗[/red] Plugin installation failed: {output}")
|
|
287
|
-
|
|
288
|
-
|
|
373
|
+
_install_standalone_skills_step(plugin_root)
|
|
374
|
+
_print_manual_instructions()
|
|
289
375
|
raise SystemExit(1)
|
|
290
376
|
|
|
291
|
-
# Step 3:
|
|
292
|
-
|
|
377
|
+
# Step 3: /tl shim command + cleanup of standalone copies from older versions
|
|
378
|
+
console.print("[bold]Installing /tl shortcut...[/bold]")
|
|
379
|
+
shim = _install_command_shim()
|
|
380
|
+
console.print(f" [green]✓[/green] /tl command installed: {shim}")
|
|
381
|
+
removed, kept = _remove_matching_standalone_skills(plugin_root)
|
|
382
|
+
if removed:
|
|
383
|
+
console.print(f" [green]✓[/green] Removed {removed} standalone skill(s) now provided by the plugin")
|
|
384
|
+
if kept:
|
|
385
|
+
console.print(f" [yellow]![/yellow] Kept {kept} modified standalone skill(s) in {CLAUDE_SKILLS_DIR}")
|
|
386
|
+
console.print(" These differ from the plugin's versions and shadow nothing — remove manually if unwanted.")
|
|
293
387
|
|
|
294
388
|
# Write version stamp
|
|
295
389
|
version_dir = CLAUDE_PLUGINS_DIR / "tl-cli"
|
|
@@ -303,20 +397,20 @@ def setup_claude(
|
|
|
303
397
|
blurbs = _bundled_skill_blurbs(plugin_root)
|
|
304
398
|
width = max((len(name) for name, _ in blurbs), default=0)
|
|
305
399
|
for name, blurb in blurbs:
|
|
306
|
-
console.print(f" [cyan]/{name}[/cyan]{' ' * (width - len(name))} — {blurb}")
|
|
400
|
+
console.print(f" [cyan]/{PLUGIN_NAME}:{name}[/cyan]{' ' * (width - len(name))} — {blurb}")
|
|
307
401
|
console.print()
|
|
308
|
-
console.print("Try it:")
|
|
402
|
+
console.print("Try it (restart Claude Code first):")
|
|
309
403
|
console.print(" [cyan]/tl Which channels did we sponsor in Q1?[/cyan]")
|
|
310
404
|
console.print()
|
|
311
405
|
console.print("[dim]To update, run: tl setup claude[/dim]")
|
|
312
406
|
|
|
313
407
|
|
|
314
408
|
def _install_standalone_skills_step(plugin_root: Path) -> None:
|
|
315
|
-
"""Install standalone skills and print status."""
|
|
316
|
-
console.print("[bold]Installing skills
|
|
409
|
+
"""Install standalone skills (plugin-less fallback) and print status."""
|
|
410
|
+
console.print("[bold]Installing standalone skills (plugin fallback)...[/bold]")
|
|
317
411
|
count = _install_standalone_skills(plugin_root)
|
|
318
412
|
if count > 0:
|
|
319
|
-
console.print(f" [green]✓[/green] Installed {count} skills/commands to
|
|
413
|
+
console.print(f" [green]✓[/green] Installed {count} skills/commands to {CLAUDE_HOME}")
|
|
320
414
|
else:
|
|
321
415
|
console.print(" [yellow]![/yellow] No skills found to install")
|
|
322
416
|
|
|
@@ -362,9 +456,19 @@ def _setup_noninteractive(fmt: str = "json") -> None:
|
|
|
362
456
|
result["marketplace_registered"] = False
|
|
363
457
|
result["plugin_installed"] = False
|
|
364
458
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
459
|
+
if result["plugin_installed"]:
|
|
460
|
+
# Plugin provides the skills; install the /tl shim and clean up
|
|
461
|
+
# unmodified standalone copies left by earlier versions.
|
|
462
|
+
_install_command_shim()
|
|
463
|
+
removed, kept = _remove_matching_standalone_skills(plugin_root)
|
|
464
|
+
result["command_shim_installed"] = True
|
|
465
|
+
result["standalone_skills_installed"] = 0
|
|
466
|
+
result["standalone_skills_removed"] = removed
|
|
467
|
+
result["standalone_skills_kept_modified"] = kept
|
|
468
|
+
else:
|
|
469
|
+
# Fallback: standalone skill copies so Claude Code still gets /tl
|
|
470
|
+
result["command_shim_installed"] = False
|
|
471
|
+
result["standalone_skills_installed"] = _install_standalone_skills(plugin_root)
|
|
368
472
|
|
|
369
473
|
# Write version stamp
|
|
370
474
|
version_dir = CLAUDE_PLUGINS_DIR / "tl-cli"
|
|
@@ -22,6 +22,7 @@ import urllib.request
|
|
|
22
22
|
from pathlib import Path
|
|
23
23
|
|
|
24
24
|
from tl_cli import __version__
|
|
25
|
+
from tl_cli.commands.setup import _find_claude_binary
|
|
25
26
|
|
|
26
27
|
CACHE_DIR = Path.home() / ".cache" / "tl-cli"
|
|
27
28
|
CACHE_PATH = CACHE_DIR / "version-check.json"
|
|
@@ -210,8 +211,12 @@ def _spawn_detached_windows_upgrade(cmd: list[str], latest: str) -> bool:
|
|
|
210
211
|
"set RC=%ERRORLEVEL%\r\n"
|
|
211
212
|
f'echo [tl-cli upgrader] exit code %RC% >> "{log_path}"\r\n'
|
|
212
213
|
"if not %RC%==0 goto end\r\n"
|
|
214
|
+
"set CLAUDE_FOUND=0\r\n"
|
|
213
215
|
"where claude >NUL 2>&1\r\n"
|
|
214
|
-
"if not errorlevel 1
|
|
216
|
+
"if not errorlevel 1 set CLAUDE_FOUND=1\r\n"
|
|
217
|
+
'if exist "%USERPROFILE%\\.local\\bin\\claude.exe" set CLAUDE_FOUND=1\r\n'
|
|
218
|
+
'if exist "%APPDATA%\\npm\\claude.cmd" set CLAUDE_FOUND=1\r\n'
|
|
219
|
+
"if %CLAUDE_FOUND%==1 (\r\n"
|
|
215
220
|
f' echo [tl-cli upgrader] re-syncing claude skills >> "{log_path}"\r\n'
|
|
216
221
|
f' tl setup claude --json >> "{log_path}" 2>&1\r\n'
|
|
217
222
|
")\r\n"
|
|
@@ -411,7 +416,10 @@ def _resync_integrations() -> None:
|
|
|
411
416
|
("gemini", "gemini"),
|
|
412
417
|
("codex", "codex"),
|
|
413
418
|
):
|
|
414
|
-
|
|
419
|
+
# claude is often installed off-PATH (native installer, npm prefix);
|
|
420
|
+
# use the same probing the setup command uses.
|
|
421
|
+
found = _find_claude_binary() if binary == "claude" else shutil.which(binary)
|
|
422
|
+
if not found:
|
|
415
423
|
continue
|
|
416
424
|
print(f"[tl-cli] re-syncing {tool} skills…", file=sys.stderr)
|
|
417
425
|
try:
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Tests for `tl setup` helpers."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from tl_cli.commands import setup
|
|
7
|
+
from tl_cli.commands.setup import (
|
|
8
|
+
_bundled_skill_blurbs,
|
|
9
|
+
_find_claude_binary,
|
|
10
|
+
_install_command_shim,
|
|
11
|
+
_remove_matching_standalone_skills,
|
|
12
|
+
_trees_identical,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _write_skill(skills_dir: Path, name: str, body: str) -> None:
|
|
17
|
+
skill_dir = skills_dir / name
|
|
18
|
+
skill_dir.mkdir(parents=True)
|
|
19
|
+
(skill_dir / "SKILL.md").write_text(body, encoding="utf-8")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TestBundledSkillBlurbs:
|
|
23
|
+
def test_reads_name_and_blurb_sorted(self, tmp_path):
|
|
24
|
+
skills = tmp_path / "skills"
|
|
25
|
+
_write_skill(skills, "tl", "---\nname: tl\ntl-blurb: data analyst\ndescription: |\n Long desc.\n---\n")
|
|
26
|
+
_write_skill(skills, "alpha", "---\nname: alpha\ntl-blurb: first thing\ndescription: x\n---\n")
|
|
27
|
+
assert _bundled_skill_blurbs(tmp_path) == [
|
|
28
|
+
("alpha", "first thing"),
|
|
29
|
+
("tl", "data analyst"),
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
def test_skips_skill_without_blurb(self, tmp_path):
|
|
33
|
+
skills = tmp_path / "skills"
|
|
34
|
+
_write_skill(skills, "tl", "---\nname: tl\ntl-blurb: has one\ndescription: x\n---\n")
|
|
35
|
+
_write_skill(skills, "other", "---\nname: other\ndescription: no blurb here\n---\n")
|
|
36
|
+
assert _bundled_skill_blurbs(tmp_path) == [("tl", "has one")]
|
|
37
|
+
|
|
38
|
+
def test_ignores_blurb_lookalike_in_body(self, tmp_path):
|
|
39
|
+
# A `tl-blurb:` line in the markdown body (after frontmatter) must not be picked up.
|
|
40
|
+
skills = tmp_path / "skills"
|
|
41
|
+
_write_skill(
|
|
42
|
+
skills,
|
|
43
|
+
"tl",
|
|
44
|
+
"---\nname: tl\ntl-blurb: real blurb\ndescription: x\n---\n\ntl-blurb: not this one\n",
|
|
45
|
+
)
|
|
46
|
+
assert _bundled_skill_blurbs(tmp_path) == [("tl", "real blurb")]
|
|
47
|
+
|
|
48
|
+
def test_missing_skills_dir_returns_empty(self, tmp_path):
|
|
49
|
+
assert _bundled_skill_blurbs(tmp_path) == []
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class TestTreesIdentical:
|
|
53
|
+
def test_identical_trees(self, tmp_path):
|
|
54
|
+
for root in ("a", "b"):
|
|
55
|
+
d = tmp_path / root / "sub"
|
|
56
|
+
d.mkdir(parents=True)
|
|
57
|
+
(d / "f.md").write_text("same", encoding="utf-8")
|
|
58
|
+
assert _trees_identical(tmp_path / "a", tmp_path / "b")
|
|
59
|
+
|
|
60
|
+
def test_different_content(self, tmp_path):
|
|
61
|
+
for root, body in (("a", "one"), ("b", "two")):
|
|
62
|
+
d = tmp_path / root
|
|
63
|
+
d.mkdir()
|
|
64
|
+
(d / "f.md").write_text(body, encoding="utf-8")
|
|
65
|
+
assert not _trees_identical(tmp_path / "a", tmp_path / "b")
|
|
66
|
+
|
|
67
|
+
def test_extra_file(self, tmp_path):
|
|
68
|
+
for root in ("a", "b"):
|
|
69
|
+
d = tmp_path / root
|
|
70
|
+
d.mkdir()
|
|
71
|
+
(d / "f.md").write_text("same", encoding="utf-8")
|
|
72
|
+
(tmp_path / "b" / "extra.md").write_text("x", encoding="utf-8")
|
|
73
|
+
assert not _trees_identical(tmp_path / "a", tmp_path / "b")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class TestRemoveMatchingStandaloneSkills:
|
|
77
|
+
def _plugin_with_skill(self, root: Path, name: str, body: str) -> Path:
|
|
78
|
+
skill = root / "skills" / name
|
|
79
|
+
skill.mkdir(parents=True)
|
|
80
|
+
(skill / "SKILL.md").write_text(body, encoding="utf-8")
|
|
81
|
+
return skill
|
|
82
|
+
|
|
83
|
+
def test_removes_identical_copy(self, tmp_path, monkeypatch):
|
|
84
|
+
plugin_root = tmp_path / "plugin"
|
|
85
|
+
self._plugin_with_skill(plugin_root, "tl", "---\nname: tl\n---\n")
|
|
86
|
+
standalone = tmp_path / "claude-skills"
|
|
87
|
+
monkeypatch.setattr(setup, "CLAUDE_SKILLS_DIR", standalone)
|
|
88
|
+
copy = standalone / "tl"
|
|
89
|
+
copy.mkdir(parents=True)
|
|
90
|
+
(copy / "SKILL.md").write_text("---\nname: tl\n---\n", encoding="utf-8")
|
|
91
|
+
|
|
92
|
+
assert _remove_matching_standalone_skills(plugin_root) == (1, 0)
|
|
93
|
+
assert not copy.exists()
|
|
94
|
+
|
|
95
|
+
def test_keeps_modified_copy(self, tmp_path, monkeypatch):
|
|
96
|
+
plugin_root = tmp_path / "plugin"
|
|
97
|
+
self._plugin_with_skill(plugin_root, "tl", "---\nname: tl\n---\n")
|
|
98
|
+
standalone = tmp_path / "claude-skills"
|
|
99
|
+
monkeypatch.setattr(setup, "CLAUDE_SKILLS_DIR", standalone)
|
|
100
|
+
copy = standalone / "tl"
|
|
101
|
+
copy.mkdir(parents=True)
|
|
102
|
+
(copy / "SKILL.md").write_text("---\nname: tl\n---\nuser edit\n", encoding="utf-8")
|
|
103
|
+
|
|
104
|
+
assert _remove_matching_standalone_skills(plugin_root) == (0, 1)
|
|
105
|
+
assert copy.exists()
|
|
106
|
+
|
|
107
|
+
def test_ignores_unrelated_personal_skills(self, tmp_path, monkeypatch):
|
|
108
|
+
plugin_root = tmp_path / "plugin"
|
|
109
|
+
self._plugin_with_skill(plugin_root, "tl", "---\nname: tl\n---\n")
|
|
110
|
+
standalone = tmp_path / "claude-skills"
|
|
111
|
+
monkeypatch.setattr(setup, "CLAUDE_SKILLS_DIR", standalone)
|
|
112
|
+
other = standalone / "my-own-skill"
|
|
113
|
+
other.mkdir(parents=True)
|
|
114
|
+
(other / "SKILL.md").write_text("mine", encoding="utf-8")
|
|
115
|
+
|
|
116
|
+
assert _remove_matching_standalone_skills(plugin_root) == (0, 0)
|
|
117
|
+
assert other.exists()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class TestInstallCommandShim:
|
|
121
|
+
def test_writes_shim_pointing_at_plugin_skill(self, tmp_path, monkeypatch):
|
|
122
|
+
monkeypatch.setattr(setup, "CLAUDE_COMMANDS_DIR", tmp_path / "commands")
|
|
123
|
+
dst = _install_command_shim()
|
|
124
|
+
assert dst == tmp_path / "commands" / "tl.md"
|
|
125
|
+
body = dst.read_text(encoding="utf-8")
|
|
126
|
+
assert "tl-cli:tl" in body
|
|
127
|
+
assert "$ARGUMENTS" in body
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class TestFindClaudeBinary:
|
|
131
|
+
def test_prefers_path(self, monkeypatch):
|
|
132
|
+
monkeypatch.setattr(setup.shutil, "which", lambda _: "/somewhere/claude")
|
|
133
|
+
assert _find_claude_binary() == "/somewhere/claude"
|
|
134
|
+
|
|
135
|
+
def test_falls_back_to_local_bin(self, tmp_path, monkeypatch):
|
|
136
|
+
monkeypatch.setattr(setup.shutil, "which", lambda _: None)
|
|
137
|
+
monkeypatch.setattr(setup.Path, "home", staticmethod(lambda: tmp_path))
|
|
138
|
+
exe = "claude.exe" if sys.platform == "win32" else "claude"
|
|
139
|
+
target = tmp_path / ".local" / "bin" / exe
|
|
140
|
+
target.parent.mkdir(parents=True)
|
|
141
|
+
target.write_text("", encoding="utf-8")
|
|
142
|
+
assert _find_claude_binary() == str(target)
|
|
143
|
+
|
|
144
|
+
def test_not_found_anywhere(self, tmp_path, monkeypatch):
|
|
145
|
+
monkeypatch.setattr(setup.shutil, "which", lambda _: None)
|
|
146
|
+
monkeypatch.setattr(setup.Path, "home", staticmethod(lambda: tmp_path))
|
|
147
|
+
if sys.platform == "win32":
|
|
148
|
+
monkeypatch.setenv("APPDATA", str(tmp_path / "AppData" / "Roaming"))
|
|
149
|
+
assert _find_claude_binary() is None
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
"""Tests for `tl setup` helpers."""
|
|
2
|
-
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
|
|
5
|
-
from tl_cli.commands.setup import _bundled_skill_blurbs
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def _write_skill(skills_dir: Path, name: str, body: str) -> None:
|
|
9
|
-
skill_dir = skills_dir / name
|
|
10
|
-
skill_dir.mkdir(parents=True)
|
|
11
|
-
(skill_dir / "SKILL.md").write_text(body, encoding="utf-8")
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class TestBundledSkillBlurbs:
|
|
15
|
-
def test_reads_name_and_blurb_sorted(self, tmp_path):
|
|
16
|
-
skills = tmp_path / "skills"
|
|
17
|
-
_write_skill(skills, "tl", "---\nname: tl\ntl-blurb: data analyst\ndescription: |\n Long desc.\n---\n")
|
|
18
|
-
_write_skill(skills, "alpha", "---\nname: alpha\ntl-blurb: first thing\ndescription: x\n---\n")
|
|
19
|
-
assert _bundled_skill_blurbs(tmp_path) == [
|
|
20
|
-
("alpha", "first thing"),
|
|
21
|
-
("tl", "data analyst"),
|
|
22
|
-
]
|
|
23
|
-
|
|
24
|
-
def test_skips_skill_without_blurb(self, tmp_path):
|
|
25
|
-
skills = tmp_path / "skills"
|
|
26
|
-
_write_skill(skills, "tl", "---\nname: tl\ntl-blurb: has one\ndescription: x\n---\n")
|
|
27
|
-
_write_skill(skills, "other", "---\nname: other\ndescription: no blurb here\n---\n")
|
|
28
|
-
assert _bundled_skill_blurbs(tmp_path) == [("tl", "has one")]
|
|
29
|
-
|
|
30
|
-
def test_ignores_blurb_lookalike_in_body(self, tmp_path):
|
|
31
|
-
# A `tl-blurb:` line in the markdown body (after frontmatter) must not be picked up.
|
|
32
|
-
skills = tmp_path / "skills"
|
|
33
|
-
_write_skill(
|
|
34
|
-
skills,
|
|
35
|
-
"tl",
|
|
36
|
-
"---\nname: tl\ntl-blurb: real blurb\ndescription: x\n---\n\ntl-blurb: not this one\n",
|
|
37
|
-
)
|
|
38
|
-
assert _bundled_skill_blurbs(tmp_path) == [("tl", "real blurb")]
|
|
39
|
-
|
|
40
|
-
def test_missing_skills_dir_returns_empty(self, tmp_path):
|
|
41
|
-
assert _bundled_skill_blurbs(tmp_path) == []
|
|
File without changes
|
{thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/.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
|
{thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/agents/youtube-comment-classifier.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl/references/business-glossary.md
RENAMED
|
File without changes
|
{thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/.gitignore
RENAMED
|
File without changes
|
{thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl-channel-authenticity/SKILL.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
|
{thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl-keyword-research/scripts/probe.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl-save-report/references/widgets.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/skills/tl-views-guarantee/scripts/vg.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
|
{thoughtleaders_cli-0.7.10 → thoughtleaders_cli-0.7.11}/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
|