thoughtleaders-cli 0.7.11__tar.gz → 0.7.12__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.
Files changed (109) hide show
  1. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/.claude-plugin/plugin.json +1 -1
  2. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/PKG-INFO +1 -1
  3. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/pyproject.toml +1 -1
  4. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/skills/tl/SKILL.md +1 -1
  5. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/skills/tl/references/elasticsearch-schema.md +18 -12
  6. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/src/tl_cli/__init__.py +1 -1
  7. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/src/tl_cli/commands/setup.py +55 -7
  8. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/tests/test_setup.py +44 -0
  9. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/.claude-plugin/marketplace.json +0 -0
  10. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/.github/workflows/python-publish.yml +0 -0
  11. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/.gitignore +0 -0
  12. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/AGENTS.md +0 -0
  13. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/API.md +0 -0
  14. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/CLAUDE.md +0 -0
  15. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/LICENSE +0 -0
  16. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/README.md +0 -0
  17. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/agents/tl-analyst.md +0 -0
  18. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/agents/youtube-comment-classifier.md +0 -0
  19. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/hooks/hooks.json +0 -0
  20. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/hooks/scripts/load-tl-skill.mjs +0 -0
  21. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/hooks/scripts/post-usage.sh +0 -0
  22. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/hooks/scripts/pre-check.sh +0 -0
  23. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/skills/tl/references/business-glossary.md +0 -0
  24. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/skills/tl/references/firebolt-schema.md +0 -0
  25. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/skills/tl/references/postgres-schema.md +0 -0
  26. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/skills/tl-channel-authenticity/.gitignore +0 -0
  27. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/skills/tl-channel-authenticity/SKILL.md +0 -0
  28. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/skills/tl-channel-authenticity/references/comment-patterns.md +0 -0
  29. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/skills/tl-channel-authenticity/references/peer-cohort.md +0 -0
  30. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/skills/tl-channel-authenticity/references/red-flags.md +0 -0
  31. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/skills/tl-channel-authenticity/references/scoring.md +0 -0
  32. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/skills/tl-channel-authenticity/scripts/_io_utf8.py +0 -0
  33. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/skills/tl-channel-authenticity/scripts/analyze_channel.py +0 -0
  34. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/skills/tl-channel-authenticity/scripts/anomaly_detector.py +0 -0
  35. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/skills/tl-channel-authenticity/scripts/comment_analyzer.py +0 -0
  36. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/skills/tl-channel-authenticity/scripts/comment_scraper.py +0 -0
  37. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/skills/tl-channel-authenticity/scripts/engagement_ratios.py +0 -0
  38. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/skills/tl-channel-authenticity/scripts/peer_cohort.py +0 -0
  39. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/skills/tl-channel-authenticity/scripts/report.py +0 -0
  40. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/skills/tl-channel-authenticity/scripts/resolve_channel.py +0 -0
  41. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/skills/tl-channel-authenticity/scripts/score.py +0 -0
  42. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/skills/tl-channel-authenticity/scripts/tl_cli.py +0 -0
  43. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/skills/tl-channel-authenticity/scripts/video_integrity.py +0 -0
  44. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/skills/tl-channel-authenticity/scripts/view_curves.py +0 -0
  45. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/skills/tl-keyword-research/SKILL.md +0 -0
  46. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/skills/tl-keyword-research/scripts/probe.py +0 -0
  47. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/skills/tl-save-report/SKILL.md +0 -0
  48. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/skills/tl-save-report/references/columns_brands.md +0 -0
  49. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/skills/tl-save-report/references/columns_channels.md +0 -0
  50. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/skills/tl-save-report/references/columns_content.md +0 -0
  51. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/skills/tl-save-report/references/columns_sponsorships.md +0 -0
  52. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/skills/tl-save-report/references/intelligence_filterset_schema.json +0 -0
  53. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/skills/tl-save-report/references/intelligence_widget_schema.json +0 -0
  54. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/skills/tl-save-report/references/report_glossary.md +0 -0
  55. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/skills/tl-save-report/references/sortable_columns.json +0 -0
  56. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/skills/tl-save-report/references/sponsorship_filterset_schema.json +0 -0
  57. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/skills/tl-save-report/references/sponsorship_widget_schema.json +0 -0
  58. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/skills/tl-save-report/references/widgets.md +0 -0
  59. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/skills/tl-top-partnerships/SKILL.md +0 -0
  60. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/skills/tl-top-partnerships/scripts/top_partnerships.py +0 -0
  61. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/skills/tl-views-guarantee/SKILL.md +0 -0
  62. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/skills/tl-views-guarantee/scripts/vg.py +0 -0
  63. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/src/tl_cli/_completions.py +0 -0
  64. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/src/tl_cli/_typer_utils.py +0 -0
  65. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/src/tl_cli/auth/__init__.py +0 -0
  66. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/src/tl_cli/auth/commands.py +0 -0
  67. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/src/tl_cli/auth/login.py +0 -0
  68. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/src/tl_cli/auth/pkce.py +0 -0
  69. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/src/tl_cli/auth/token_store.py +0 -0
  70. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/src/tl_cli/client/__init__.py +0 -0
  71. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/src/tl_cli/client/errors.py +0 -0
  72. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/src/tl_cli/client/http.py +0 -0
  73. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/src/tl_cli/commands/__init__.py +0 -0
  74. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/src/tl_cli/commands/_comments_common.py +0 -0
  75. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/src/tl_cli/commands/balance.py +0 -0
  76. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/src/tl_cli/commands/brands.py +0 -0
  77. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/src/tl_cli/commands/bulk_import.py +0 -0
  78. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/src/tl_cli/commands/changelog.py +0 -0
  79. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/src/tl_cli/commands/channels.py +0 -0
  80. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/src/tl_cli/commands/credits.py +0 -0
  81. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/src/tl_cli/commands/db.py +0 -0
  82. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/src/tl_cli/commands/deals.py +0 -0
  83. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/src/tl_cli/commands/describe.py +0 -0
  84. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/src/tl_cli/commands/doctor.py +0 -0
  85. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/src/tl_cli/commands/matches.py +0 -0
  86. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/src/tl_cli/commands/proposals.py +0 -0
  87. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/src/tl_cli/commands/recommender.py +0 -0
  88. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/src/tl_cli/commands/reports.py +0 -0
  89. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/src/tl_cli/commands/schema.py +0 -0
  90. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/src/tl_cli/commands/snapshots.py +0 -0
  91. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/src/tl_cli/commands/sponsorships.py +0 -0
  92. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/src/tl_cli/commands/uploads.py +0 -0
  93. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/src/tl_cli/commands/whoami.py +0 -0
  94. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/src/tl_cli/config.py +0 -0
  95. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/src/tl_cli/filters.py +0 -0
  96. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/src/tl_cli/hints.py +0 -0
  97. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/src/tl_cli/main.py +0 -0
  98. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/src/tl_cli/output/__init__.py +0 -0
  99. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/src/tl_cli/output/formatter.py +0 -0
  100. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/src/tl_cli/self_update.py +0 -0
  101. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/tests/__init__.py +0 -0
  102. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/tests/test_auth.py +0 -0
  103. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/tests/test_describe.py +0 -0
  104. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/tests/test_filters.py +0 -0
  105. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/tests/test_http_auth.py +0 -0
  106. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/tests/test_output.py +0 -0
  107. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/tests/test_reports.py +0 -0
  108. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/tests/test_sponsorships.py +0 -0
  109. {thoughtleaders_cli-0.7.11 → thoughtleaders_cli-0.7.12}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tl-cli",
3
- "version": "0.7.11",
3
+ "version": "0.7.12",
4
4
  "description": "ThoughtLeaders CLI — query sponsorship deals, channels, brands, uploads, and intelligence from the terminal",
5
5
  "author": {
6
6
  "name": "ThoughtLeaders",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: thoughtleaders-cli
3
- Version: 0.7.11
3
+ Version: 0.7.12
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "thoughtleaders-cli"
7
- version = "0.7.11"
7
+ version = "0.7.12"
8
8
  description = "ThoughtLeaders CLI — query sponsorship data, channels, brands, and intelligence"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -446,7 +446,7 @@ If unsure about what information to find where, read the [references/postgresql-
446
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`). |
447
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. |
448
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). |
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
+ | ES deep pagination beyond `from+size = 10,000` | **Available** via `search_after` (stateless cursor); `scroll` and `pit` remain unavailable. | Sort with a unique tiebreaker (e.g. `id`), then pass the response envelope's `next_search_after` back as `search_after` in the next call, keeping `query`/`sort` identical and `from` at 0. See the ES reference's *Deep pagination* section. |
450
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. |
451
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). |
452
452
 
@@ -21,8 +21,9 @@ 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`, `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.
24
+ - **Top-level keys** accepted: `query`, `aggs`/`aggregations`, `sort`, `_source`, `size`, `from`, `search_after`, `track_total_hits`, `highlight`, `fields`, `min_score`, `timeout`, `collapse`, `post_filter`. Anything else (incl. `scroll`, `pit`, `runtime_mappings`, `knn`) is not accepted.
25
+ - `size` ≤ 10,000. `from + size` ≤ 10,000 — to page past 10,000 hits use `search_after` (see *Deep pagination* below), not `from`.
26
+ - `search_after` must be a non-empty array of ≤ 10 scalar sort values, requires an explicit `sort`, and `from` must be 0 or omitted.
26
27
  - **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
28
  - **No scripts** — any key whose name contains `script` is not accepted.
28
29
  - **At most one aggregation total** counted recursively (top-level + sub-agg = 2 = not accepted). Run multiple calls for multi-metric work.
@@ -215,24 +216,29 @@ tl db es '{
215
216
 
216
217
  For more dimensions, run multiple `tl db es` calls and join client-side.
217
218
 
218
- ### Deep sweepswindow by date, don't page past 10k
219
+ ### Deep pagination`search_after`
219
220
 
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:
221
+ `from + size` is capped at 10,000, and the stateful cursors (`scroll`, `pit`) are not accepted. To page past 10,000 hits, use the stateless `search_after` cursor: sort deterministically with a unique tiebreaker (the `id` field not `_id`), then pass each response's `next_search_after` envelope value back as `search_after` in the next request, keeping the same `query` and `sort`:
221
222
 
222
223
  ```bash
223
- # One window — repeat with shifted date ranges until the full period is covered
224
+ # First page
224
225
  tl db es '{
225
226
  "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"}]
227
+ "query": {"term": {"channel.id": 12345}},
228
+ "sort": [{"publication_date": "asc"}, {"id": "asc"}]
229
+ }'
230
+ # envelope includes "next_search_after": ["2025-09-14", "12345:abc123"]
231
+
232
+ # Next page — identical query & sort, plus the cursor
233
+ tl db es '{
234
+ "size": 10000,
235
+ "query": {"term": {"channel.id": 12345}},
236
+ "sort": [{"publication_date": "asc"}, {"id": "asc"}],
237
+ "search_after": ["2025-09-14", "12345:abc123"]
232
238
  }'
233
239
  ```
234
240
 
235
- Check `total` per windowif a window exceeds 10,000, split it further.
241
+ Repeat until a page comes back short (`next_search_after` is absent on an empty page). Pages are not a consistent snapshot concurrent indexing can occasionally duplicate or skip a boundary row, which is fine for analytics sweeps. Date-range windowing (filtering by `publication_date` ranges) remains a good alternative when you want resumable, idempotent slices.
236
242
 
237
243
  ## Text analyzer behavior
238
244
 
@@ -1,3 +1,3 @@
1
1
  """ThoughtLeaders CLI — query sponsorship data, channels, brands, and intelligence."""
2
2
 
3
- __version__ = "0.7.11"
3
+ __version__ = "0.7.12"
@@ -76,30 +76,66 @@ def _find_plugin_root() -> Path | None:
76
76
  return None
77
77
 
78
78
 
79
+ def _newest_desktop_claude(base: Path, exe: str) -> Path | None:
80
+ """Pick the highest-version claude binary bundled by the Claude desktop app.
81
+
82
+ The desktop app keeps versioned copies under `<base>/<version>/<exe>`
83
+ (e.g. `%APPDATA%/Claude/claude-code/2.1.170/claude.exe`).
84
+ """
85
+ if not base.is_dir():
86
+ return None
87
+
88
+ def _version_key(d: Path) -> tuple[int, ...]:
89
+ try:
90
+ return tuple(int(part) for part in d.name.split("."))
91
+ except ValueError:
92
+ return (0,)
93
+
94
+ for version_dir in sorted(base.iterdir(), key=_version_key, reverse=True):
95
+ candidate = version_dir / exe
96
+ if candidate.is_file():
97
+ return candidate
98
+ return None
99
+
100
+
79
101
  def _find_claude_binary() -> str | None:
80
102
  """Find the claude binary on PATH, falling back to known install locations.
81
103
 
82
104
  On Windows the Claude Code installers often don't end up on the PATH of
83
105
  the shell running `tl` (stale PATH, PowerShell-only profile changes), so
84
106
  after `shutil.which` we probe the documented install targets directly:
85
- the native installer (`~/.local/bin`) and the npm global prefix.
107
+ the native installer (`~/.local/bin`), the npm global prefix, and the
108
+ binaries bundled with the Claude desktop app. When `tl` itself runs
109
+ inside a Claude Code session, `CLAUDE_CODE_EXECPATH` points straight at
110
+ the running binary and wins.
86
111
  """
112
+ env_exec = os.environ.get("CLAUDE_CODE_EXECPATH")
113
+ if env_exec and Path(env_exec).is_file():
114
+ return env_exec
87
115
  found = shutil.which("claude")
88
116
  if found:
89
117
  return found
90
118
  home = Path.home()
91
119
  if sys.platform == "win32":
120
+ appdata = Path(os.environ.get("APPDATA", str(home / "AppData" / "Roaming")))
92
121
  candidates = [
93
122
  home / ".local" / "bin" / "claude.exe",
94
- Path(os.environ.get("APPDATA", str(home / "AppData" / "Roaming"))) / "npm" / "claude.cmd",
123
+ appdata / "npm" / "claude.cmd",
124
+ _newest_desktop_claude(appdata / "Claude" / "claude-code", "claude.exe"),
95
125
  ]
96
126
  else:
127
+ desktop_base = (
128
+ home / "Library" / "Application Support" / "Claude" / "claude-code"
129
+ if sys.platform == "darwin"
130
+ else home / ".config" / "Claude" / "claude-code"
131
+ )
97
132
  candidates = [
98
133
  home / ".local" / "bin" / "claude",
99
134
  home / ".claude" / "local" / "claude",
135
+ _newest_desktop_claude(desktop_base, "claude"),
100
136
  ]
101
137
  for candidate in candidates:
102
- if candidate.is_file():
138
+ if candidate is not None and candidate.is_file():
103
139
  return str(candidate)
104
140
  return None
105
141
 
@@ -204,10 +240,22 @@ def _install_command_shim() -> Path:
204
240
 
205
241
 
206
242
  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:
243
+ """True if two directory trees contain the same files with the same contents.
244
+
245
+ Python runtime artifacts (`__pycache__/`, `*.pyc`) are ignored skills
246
+ that ship scripts grow them when the scripts run, and they shouldn't make
247
+ an otherwise-pristine copy look user-modified.
248
+ """
249
+
250
+ def _files(root: Path) -> list[Path]:
251
+ return sorted(
252
+ p.relative_to(root)
253
+ for p in root.rglob("*")
254
+ if p.is_file() and p.suffix != ".pyc" and "__pycache__" not in p.parts
255
+ )
256
+
257
+ a_files = _files(a)
258
+ if a_files != _files(b):
211
259
  return False
212
260
  return all(filecmp.cmp(a / rel, b / rel, shallow=False) for rel in a_files)
213
261
 
@@ -64,6 +64,16 @@ class TestTreesIdentical:
64
64
  (d / "f.md").write_text(body, encoding="utf-8")
65
65
  assert not _trees_identical(tmp_path / "a", tmp_path / "b")
66
66
 
67
+ def test_ignores_pycache_artifacts(self, tmp_path):
68
+ for root in ("a", "b"):
69
+ d = tmp_path / root / "scripts"
70
+ d.mkdir(parents=True)
71
+ (d / "run.py").write_text("print()", encoding="utf-8")
72
+ cache = tmp_path / "b" / "scripts" / "__pycache__"
73
+ cache.mkdir()
74
+ (cache / "run.cpython-313.pyc").write_text("bytecode", encoding="utf-8")
75
+ assert _trees_identical(tmp_path / "a", tmp_path / "b")
76
+
67
77
  def test_extra_file(self, tmp_path):
68
78
  for root in ("a", "b"):
69
79
  d = tmp_path / root
@@ -128,11 +138,44 @@ class TestInstallCommandShim:
128
138
 
129
139
 
130
140
  class TestFindClaudeBinary:
141
+ def test_prefers_execpath_env(self, tmp_path, monkeypatch):
142
+ exe = tmp_path / "claude.exe"
143
+ exe.write_text("", encoding="utf-8")
144
+ monkeypatch.setenv("CLAUDE_CODE_EXECPATH", str(exe))
145
+ assert _find_claude_binary() == str(exe)
146
+
147
+ def test_ignores_stale_execpath_env(self, tmp_path, monkeypatch):
148
+ monkeypatch.setenv("CLAUDE_CODE_EXECPATH", str(tmp_path / "gone.exe"))
149
+ monkeypatch.setattr(setup.shutil, "which", lambda _: "/somewhere/claude")
150
+ assert _find_claude_binary() == "/somewhere/claude"
151
+
131
152
  def test_prefers_path(self, monkeypatch):
153
+ monkeypatch.delenv("CLAUDE_CODE_EXECPATH", raising=False)
132
154
  monkeypatch.setattr(setup.shutil, "which", lambda _: "/somewhere/claude")
133
155
  assert _find_claude_binary() == "/somewhere/claude"
134
156
 
157
+ def test_finds_newest_desktop_app_binary(self, tmp_path, monkeypatch):
158
+ monkeypatch.delenv("CLAUDE_CODE_EXECPATH", raising=False)
159
+ monkeypatch.setattr(setup.shutil, "which", lambda _: None)
160
+ monkeypatch.setattr(setup.Path, "home", staticmethod(lambda: tmp_path))
161
+ if sys.platform == "win32":
162
+ base = tmp_path / "AppData" / "Roaming" / "Claude" / "claude-code"
163
+ monkeypatch.setenv("APPDATA", str(tmp_path / "AppData" / "Roaming"))
164
+ exe = "claude.exe"
165
+ elif sys.platform == "darwin":
166
+ base = tmp_path / "Library" / "Application Support" / "Claude" / "claude-code"
167
+ exe = "claude"
168
+ else:
169
+ base = tmp_path / ".config" / "Claude" / "claude-code"
170
+ exe = "claude"
171
+ for version in ("2.1.165", "2.1.170"):
172
+ d = base / version
173
+ d.mkdir(parents=True)
174
+ (d / exe).write_text("", encoding="utf-8")
175
+ assert _find_claude_binary() == str(base / "2.1.170" / exe)
176
+
135
177
  def test_falls_back_to_local_bin(self, tmp_path, monkeypatch):
178
+ monkeypatch.delenv("CLAUDE_CODE_EXECPATH", raising=False)
136
179
  monkeypatch.setattr(setup.shutil, "which", lambda _: None)
137
180
  monkeypatch.setattr(setup.Path, "home", staticmethod(lambda: tmp_path))
138
181
  exe = "claude.exe" if sys.platform == "win32" else "claude"
@@ -142,6 +185,7 @@ class TestFindClaudeBinary:
142
185
  assert _find_claude_binary() == str(target)
143
186
 
144
187
  def test_not_found_anywhere(self, tmp_path, monkeypatch):
188
+ monkeypatch.delenv("CLAUDE_CODE_EXECPATH", raising=False)
145
189
  monkeypatch.setattr(setup.shutil, "which", lambda _: None)
146
190
  monkeypatch.setattr(setup.Path, "home", staticmethod(lambda: tmp_path))
147
191
  if sys.platform == "win32":