thoughtleaders-cli 0.9.13__tar.gz → 0.9.14__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 (166) hide show
  1. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/.claude-plugin/plugin.json +1 -1
  2. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/AGENTS.md +1 -1
  3. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/PKG-INFO +9 -4
  4. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/README.md +8 -3
  5. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/pyproject.toml +1 -1
  6. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl/SKILL.md +3 -6
  7. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl/references/firebolt-schema.md +6 -4
  8. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/src/tl_cli/__init__.py +1 -1
  9. thoughtleaders_cli-0.9.14/src/tl_cli/auth/commands.py +329 -0
  10. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/src/tl_cli/auth/login.py +111 -35
  11. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/src/tl_cli/auth/token_store.py +3 -1
  12. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/src/tl_cli/client/errors.py +25 -11
  13. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/src/tl_cli/client/http.py +44 -16
  14. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/src/tl_cli/commands/recommender.py +2 -2
  15. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/src/tl_cli/config.py +6 -1
  16. thoughtleaders_cli-0.9.14/tests/test_auth.py +636 -0
  17. thoughtleaders_cli-0.9.14/tests/test_http_auth.py +213 -0
  18. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/uv.lock +1 -1
  19. thoughtleaders_cli-0.9.13/src/tl_cli/auth/commands.py +0 -211
  20. thoughtleaders_cli-0.9.13/tests/test_auth.py +0 -145
  21. thoughtleaders_cli-0.9.13/tests/test_http_auth.py +0 -62
  22. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/.claude-plugin/marketplace.json +0 -0
  23. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/.github/dependabot.yml +0 -0
  24. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/.github/workflows/ci.yml +0 -0
  25. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/.github/workflows/cli-integration.yml +0 -0
  26. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/.github/workflows/python-publish.yml +0 -0
  27. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/.gitignore +0 -0
  28. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/API.md +0 -0
  29. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/CLAUDE.md +0 -0
  30. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/LICENSE +0 -0
  31. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/agents/keyword-context-classifier.md +0 -0
  32. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/agents/keyword-entity-resolver.md +0 -0
  33. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/agents/keyword-relevance-validator.md +0 -0
  34. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/agents/tl-analyst.md +0 -0
  35. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/agents/youtube-comment-classifier.md +0 -0
  36. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/hooks/hooks.json +0 -0
  37. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/hooks/scripts/load-tl-skill.mjs +0 -0
  38. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/hooks/scripts/post-usage.sh +0 -0
  39. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/hooks/scripts/pre-check.sh +0 -0
  40. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl/references/business-glossary.md +0 -0
  41. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl/references/elasticsearch-schema.md +0 -0
  42. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl/references/postgres-schema.md +0 -0
  43. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-channel-authenticity/.gitignore +0 -0
  44. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-channel-authenticity/SKILL.md +0 -0
  45. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-channel-authenticity/references/comment-patterns.md +0 -0
  46. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-channel-authenticity/references/peer-cohort.md +0 -0
  47. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-channel-authenticity/references/red-flags.md +0 -0
  48. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-channel-authenticity/references/scoring.md +0 -0
  49. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-channel-authenticity/scripts/_io_utf8.py +0 -0
  50. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-channel-authenticity/scripts/analyze_channel.py +0 -0
  51. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-channel-authenticity/scripts/anomaly_detector.py +0 -0
  52. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-channel-authenticity/scripts/comment_analyzer.py +0 -0
  53. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-channel-authenticity/scripts/comment_scraper.py +0 -0
  54. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-channel-authenticity/scripts/engagement_ratios.py +0 -0
  55. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-channel-authenticity/scripts/peer_cohort.py +0 -0
  56. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-channel-authenticity/scripts/report.py +0 -0
  57. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-channel-authenticity/scripts/resolve_channel.py +0 -0
  58. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-channel-authenticity/scripts/score.py +0 -0
  59. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-channel-authenticity/scripts/tl_cli.py +0 -0
  60. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-channel-authenticity/scripts/video_integrity.py +0 -0
  61. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-channel-authenticity/scripts/view_curves.py +0 -0
  62. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-create-workflow/SKILL.md +0 -0
  63. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-create-workflow/references/creating-in-app.md +0 -0
  64. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-create-workflow/references/methodology.md +0 -0
  65. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-create-workflow/references/pitfalls.md +0 -0
  66. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-create-workflow/references/workflow-model.md +0 -0
  67. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-keyword-research/SKILL.md +0 -0
  68. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-keyword-research/references/elasticsearch-content-search.md +0 -0
  69. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-keyword-research/references/help.md +0 -0
  70. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-keyword-research/scripts/build_report.py +0 -0
  71. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-keyword-research/scripts/expand_entities.py +0 -0
  72. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-keyword-research/scripts/fetch_context.py +0 -0
  73. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-keyword-research/scripts/probe.py +0 -0
  74. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-keyword-research/scripts/search_channels.py +0 -0
  75. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-keyword-research/scripts/search_videos.py +0 -0
  76. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-keyword-research/scripts/select_keywords.py +0 -0
  77. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-save-report/SKILL.md +0 -0
  78. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-save-report/references/columns_brands.md +0 -0
  79. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-save-report/references/columns_channels.md +0 -0
  80. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-save-report/references/columns_content.md +0 -0
  81. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-save-report/references/columns_sponsorships.md +0 -0
  82. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-save-report/references/intelligence_filterset_schema.json +0 -0
  83. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-save-report/references/intelligence_widget_schema.json +0 -0
  84. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-save-report/references/report_glossary.md +0 -0
  85. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-save-report/references/sortable_columns.json +0 -0
  86. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-save-report/references/sponsorship_filterset_schema.json +0 -0
  87. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-save-report/references/sponsorship_widget_schema.json +0 -0
  88. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-save-report/references/widgets.md +0 -0
  89. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-top-partnerships/SKILL.md +0 -0
  90. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-top-partnerships/scripts/top_partnerships.py +0 -0
  91. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-views-guarantee/SKILL.md +0 -0
  92. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/skills/tl-views-guarantee/scripts/vg.py +0 -0
  93. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/src/tl_cli/__main__.py +0 -0
  94. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/src/tl_cli/_completions.py +0 -0
  95. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/src/tl_cli/_typer_utils.py +0 -0
  96. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/src/tl_cli/auth/__init__.py +0 -0
  97. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/src/tl_cli/auth/pkce.py +0 -0
  98. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/src/tl_cli/client/__init__.py +0 -0
  99. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/src/tl_cli/commands/__init__.py +0 -0
  100. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/src/tl_cli/commands/_comments_common.py +0 -0
  101. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/src/tl_cli/commands/balance.py +0 -0
  102. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/src/tl_cli/commands/brands.py +0 -0
  103. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/src/tl_cli/commands/bulk_import.py +0 -0
  104. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/src/tl_cli/commands/changelog.py +0 -0
  105. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/src/tl_cli/commands/channels.py +0 -0
  106. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/src/tl_cli/commands/credits.py +0 -0
  107. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/src/tl_cli/commands/db.py +0 -0
  108. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/src/tl_cli/commands/deals.py +0 -0
  109. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/src/tl_cli/commands/describe.py +0 -0
  110. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/src/tl_cli/commands/doctor.py +0 -0
  111. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/src/tl_cli/commands/matches.py +0 -0
  112. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/src/tl_cli/commands/memory.py +0 -0
  113. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/src/tl_cli/commands/profiles.py +0 -0
  114. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/src/tl_cli/commands/proposals.py +0 -0
  115. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/src/tl_cli/commands/reports.py +0 -0
  116. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/src/tl_cli/commands/schema.py +0 -0
  117. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/src/tl_cli/commands/setup.py +0 -0
  118. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/src/tl_cli/commands/skills.py +0 -0
  119. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/src/tl_cli/commands/snapshots.py +0 -0
  120. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/src/tl_cli/commands/sponsorships.py +0 -0
  121. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/src/tl_cli/commands/uploads.py +0 -0
  122. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/src/tl_cli/commands/whoami.py +0 -0
  123. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/src/tl_cli/commands/workflows.py +0 -0
  124. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/src/tl_cli/filters.py +0 -0
  125. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/src/tl_cli/hints.py +0 -0
  126. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/src/tl_cli/main.py +0 -0
  127. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/src/tl_cli/output/__init__.py +0 -0
  128. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/src/tl_cli/output/formatter.py +0 -0
  129. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/src/tl_cli/query_history.py +0 -0
  130. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/src/tl_cli/self_update.py +0 -0
  131. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/src/tl_cli/skill_registry.py +0 -0
  132. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/tests/__init__.py +0 -0
  133. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/tests/test_brands_winner_channels.py +0 -0
  134. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/tests/test_build_report.py +0 -0
  135. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/tests/test_channels_lookalike.py +0 -0
  136. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/tests/test_db_repeat_warning.py +0 -0
  137. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/tests/test_describe.py +0 -0
  138. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/tests/test_error_hints.py +0 -0
  139. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/tests/test_expand_entities.py +0 -0
  140. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/tests/test_filters.py +0 -0
  141. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/tests/test_memory.py +0 -0
  142. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/tests/test_output.py +0 -0
  143. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/tests/test_probe.py +0 -0
  144. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/tests/test_profiles.py +0 -0
  145. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/tests/test_quota_refusal_message.py +0 -0
  146. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/tests/test_refusal_rendering.py +0 -0
  147. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/tests/test_reports.py +0 -0
  148. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/tests/test_scripts_executable.py +0 -0
  149. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/tests/test_search_channels.py +0 -0
  150. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/tests/test_search_videos.py +0 -0
  151. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/tests/test_select_keywords.py +0 -0
  152. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/tests/test_setup.py +0 -0
  153. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/tests/test_setup_marker_respect.py +0 -0
  154. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/tests/test_skill_registry.py +0 -0
  155. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/tests/test_skills_command.py +0 -0
  156. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/tests/test_sponsorships.py +0 -0
  157. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/tests/test_whoami_newsletter.py +0 -0
  158. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/tests/test_workflows.py +0 -0
  159. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/tests_cli/AGENTS.md +0 -0
  160. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/tests_cli/conftest.py +0 -0
  161. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/tests_cli/test_balance.py +0 -0
  162. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/tests_cli/test_db_es.py +0 -0
  163. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/tests_cli/test_db_fb.py +0 -0
  164. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/tests_cli/test_db_pg.py +0 -0
  165. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/tests_cli/test_schema.py +0 -0
  166. {thoughtleaders_cli-0.9.13 → thoughtleaders_cli-0.9.14}/tests_cli/test_whoami.py +0 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tl-cli",
3
- "version": "0.9.13",
3
+ "version": "0.9.14",
4
4
  "description": "ThoughtLeaders CLI — query sponsorship deals, channels, brands, uploads, and intelligence from the terminal",
5
5
  "author": {
6
6
  "name": "ThoughtLeaders",
@@ -19,7 +19,7 @@ When adding a new data command, follow this pattern. See `sponsorships.py` for t
19
19
 
20
20
  ## Auth Flow (`auth/`)
21
21
 
22
- - **PKCE + Auth0**: Browser-based login with localhost callback server (`login.py`)
22
+ - **PKCE + Auth0**: Browser-based login with a localhost callback server (`login.py`) against `auth.thoughtleaders.io`, the host the web platform and the Chrome extension sign in on, so a browser already signed in to the platform completes `tl auth login` without a password; the callback then sends that browser to the platform's `/signin?go=1&from=cli` so the web session follows. `tl auth logout` posts once to `/auth/sign-out` and ThoughtLeaders signs the user out everywhere; the CLI then revokes its refresh token and clears the local store. A 401 with `code: signed_out` means the session ended elsewhere, so the client drops its credentials instead of refreshing (`forget_session`).
23
23
  - **Token Storage** (`token_store.py`): OS keyring primary, `~/.config/tl/credentials.json` fallback (chmod 0o600)
24
24
  - **Env override**: `TL_API_KEY` env var takes priority over keyring (for CI)
25
25
  - **Auto-refresh**: `TLClient` refreshes expired tokens on 401
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: thoughtleaders-cli
3
- Version: 0.9.13
3
+ Version: 0.9.14
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
@@ -122,9 +122,15 @@ tl setup codex # install Codex CLI skill (optional)
122
122
 
123
123
  `tl auth login` offers three options:
124
124
 
125
- 1. **OAuth2 in a local browser** (default) — opens a URL on this machine.
125
+ 1. **OAuth2 in a local browser** (default) — opens a URL on this machine. If that browser is already signed in to the web platform, no password is asked; either way the browser ends up signed in to the platform too, and the Chrome extension follows. Pass `--no-browser` to print the URL instead of opening a window.
126
126
  2. **Device code** — for headless environments; complete the flow on another device.
127
- 3. **API key** — paste a pre-issued `APIKey` from Django admin. The CLI verifies it via `/whoami` and stores it tagged so every request sends `X-TL-Auth: API-KEY`.
127
+ 3. **API key** — paste an API key issued to you by ThoughtLeaders. The CLI verifies it via `/whoami` and stores it tagged so every request sends `X-TL-Auth: API-KEY`.
128
+
129
+ Pass `--method browser|device|api-key` to skip the menu; without a terminal the menu is an error, so agents must pass it. Without a terminal the browser method prints the URL instead of opening a window (`--no-browser` does the same at a terminal).
130
+
131
+ `tl auth logout` signs you out everywhere — this machine, the web platform, the Chrome extension and any other machine running `tl`. It is always global: there is no local-only logout. If ThoughtLeaders can't be reached, this machine's credentials are still cleared, but your other sessions stay signed in until you sign out on the web platform.
132
+
133
+ `tl auth status` prints who you are signed in as; `--quiet` prints nothing and just exits 0 when the next command will authenticate and 2 when it will not, which is what agent hooks check.
128
134
 
129
135
  ## Quick Start
130
136
 
@@ -180,7 +186,6 @@ tl channels similar "Tremending girls" min-score:0.85 --limit 5
180
186
  tl recommender tags # List every tag (free)
181
187
  tl recommender tags cooking # Search tag names by substring
182
188
  tl recommender top-channels "Cooking" msn:yes --limit 50 # Top channels for a tag
183
- tl recommender top-profiles "Cooking" mbn:yes --limit 30 # Top brand profiles (one brand → potentially multiple profiles)
184
189
  tl recommender top-brands "Cooking" --limit 30 # Top brands (deduped from profiles)
185
190
  tl recommender channels-with-tag "Cooking" # ALL channel IDs loaded on a tag (--min defaults to 0.00001; paged; 1 credit/result)
186
191
  tl recommender inspect-channel 12345 # Per-tag breakdown of a channel's vector
@@ -94,9 +94,15 @@ tl setup codex # install Codex CLI skill (optional)
94
94
 
95
95
  `tl auth login` offers three options:
96
96
 
97
- 1. **OAuth2 in a local browser** (default) — opens a URL on this machine.
97
+ 1. **OAuth2 in a local browser** (default) — opens a URL on this machine. If that browser is already signed in to the web platform, no password is asked; either way the browser ends up signed in to the platform too, and the Chrome extension follows. Pass `--no-browser` to print the URL instead of opening a window.
98
98
  2. **Device code** — for headless environments; complete the flow on another device.
99
- 3. **API key** — paste a pre-issued `APIKey` from Django admin. The CLI verifies it via `/whoami` and stores it tagged so every request sends `X-TL-Auth: API-KEY`.
99
+ 3. **API key** — paste an API key issued to you by ThoughtLeaders. The CLI verifies it via `/whoami` and stores it tagged so every request sends `X-TL-Auth: API-KEY`.
100
+
101
+ Pass `--method browser|device|api-key` to skip the menu; without a terminal the menu is an error, so agents must pass it. Without a terminal the browser method prints the URL instead of opening a window (`--no-browser` does the same at a terminal).
102
+
103
+ `tl auth logout` signs you out everywhere — this machine, the web platform, the Chrome extension and any other machine running `tl`. It is always global: there is no local-only logout. If ThoughtLeaders can't be reached, this machine's credentials are still cleared, but your other sessions stay signed in until you sign out on the web platform.
104
+
105
+ `tl auth status` prints who you are signed in as; `--quiet` prints nothing and just exits 0 when the next command will authenticate and 2 when it will not, which is what agent hooks check.
100
106
 
101
107
  ## Quick Start
102
108
 
@@ -152,7 +158,6 @@ tl channels similar "Tremending girls" min-score:0.85 --limit 5
152
158
  tl recommender tags # List every tag (free)
153
159
  tl recommender tags cooking # Search tag names by substring
154
160
  tl recommender top-channels "Cooking" msn:yes --limit 50 # Top channels for a tag
155
- tl recommender top-profiles "Cooking" mbn:yes --limit 30 # Top brand profiles (one brand → potentially multiple profiles)
156
161
  tl recommender top-brands "Cooking" --limit 30 # Top brands (deduped from profiles)
157
162
  tl recommender channels-with-tag "Cooking" # ALL channel IDs loaded on a tag (--min defaults to 0.00001; paged; 1 credit/result)
158
163
  tl recommender inspect-channel 12345 # Per-tag breakdown of a channel's vector
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "thoughtleaders-cli"
7
- version = "0.9.13"
7
+ version = "0.9.14"
8
8
  description = "ThoughtLeaders CLI — query sponsorship data, channels, brands, and intelligence"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -124,7 +124,7 @@ When querying sponsorship bookings, filter the rows with `publish_status = 3` (s
124
124
 
125
125
  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.
126
126
 
127
- 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.
127
+ 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`) against the recommender — that's faster, ranked by category-strength. Run `tl recommender tags` to discover the valid tag names.
128
128
 
129
129
  ## Workflow
130
130
 
@@ -205,7 +205,6 @@ tl brands find <query> # Resolve a string to {id, name}; matches
205
205
  tl brands similar <id-or-name> # Find similar brands via similarity search
206
206
  tl recommender tags [query] # List similarity tag names — categories, demographics, formats
207
207
  tl recommender top-channels "<tag>" # Top channels loaded on a similarity tag (Intelligence)
208
- tl recommender top-profiles "<tag>" # Top brand profiles loaded on a similarity tag
209
208
  tl recommender top-brands "<tag>" # Top brands (deduped from profiles) loaded on a similarity tag
210
209
  tl recommender channels-with-tag "<tag>" [--min <v>] # ALL channel IDs scoring >= v on a tag (--min default 0.00001 drops zero-loading channels; paged, enumerates the full set; 1 credit/result; Intelligence)
211
210
  tl recommender inspect-channel <ref> # Show a channel's similarity-profile breakdown (Intelligence)
@@ -591,7 +590,7 @@ tl brands find "NordVPN"
591
590
 
592
591
  `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.
593
592
 
594
- **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."
593
+ **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 ranks brands on the same tags (`top-brands`) — useful when the user wants to know "who buys this kind of inventory."
595
594
 
596
595
  ```bash
597
596
  # Discover the available tag name first (free)
@@ -600,7 +599,7 @@ tl recommender tags cooking
600
599
  # Discover tag names containing the substring
601
600
  tl recommender tags crypto
602
601
 
603
- # Top channels & profiles loaded on a similarity tag (Intelligence)
602
+ # Top channels & brands loaded on a similarity tag (Intelligence)
604
603
  tl recommender top-channels "Cooking" msn:yes --limit 50
605
604
  tl recommender top-channels "Tech" --limit 30
606
605
  tl recommender top-brands "USA share" mbn:yes --limit 50
@@ -611,7 +610,6 @@ tl recommender top-brands "USA share" mbn:yes --limit 50
611
610
  | Command | Filters |
612
611
  | --- | --- |
613
612
  | `top-channels` | `msn:<yes\|no\|all>` (default all), `exclude-for-profile:<id>` |
614
- | `top-profiles` | `mbn:<yes\|no\|all>` (default all), `exclude-for-channel:<id>` |
615
613
  | `top-brands` | `mbn:<yes\|no\|all>` (default all) |
616
614
  | `channels-for-profile` | `language:<iso>` (default `en`), `msn:<yes\|no>` (default `no`) |
617
615
  | `channels-for-brand` | same as `channels-for-profile` |
@@ -887,7 +885,6 @@ tl channels similar 29834 min-subs:1000000 exclude:477487 --limit 15 # client-s
887
885
  tl recommender tags # Full tag list (free)
888
886
  tl recommender tags cooking # Search tag names by substring
889
887
  tl recommender top-channels "Cooking" msn:yes --limit 50 # Top channels loaded on a tag (25 credits)
890
- tl recommender top-profiles "Cooking" --limit 30 # Top brand profiles for the tag
891
888
  tl recommender top-brands "USA share" mbn:yes --limit 30 # Top brands (deduped) — demographic tag, MBN only
892
889
  tl recommender top-channels "Tech" exclude-for-profile:842 # Drop channels already proposed for profile 842
893
890
  tl recommender inspect-channel 29834 # Per-tag breakdown of a channel's vector
@@ -63,7 +63,7 @@ Tracks YouTube video metrics over time. Each row = one scrape of one video on on
63
63
 
64
64
  **Primary Index: `(channel_id, id)`** — queries MUST filter by `channel_id` first.
65
65
 
66
- **Shorts vs Longform:** `duration < 61` = Short. `duration >= 61` = Longform. In code: `(duration or 100) < 61`. Filter client-side after fetching `duration` isn't an indexed column so it can't go in WHERE.
66
+ **Shorts vs Longform:** `article_metrics` has no `content_type` column, and `duration` cannot stand in for one YouTube counts uploads up to 180s as Shorts, so any duration cutoff mislabels them. Take the split from Elasticsearch's `content_type` (`longform` / `short` / `live`), matching on the document id `<channel_id>:<video_id>`.
67
67
 
68
68
  ### `channel_metrics` — Channel-Level Time-Series
69
69
 
@@ -158,7 +158,7 @@ tl db fb "SELECT id, age, view_count FROM article_metrics
158
158
  ORDER BY id, age"
159
159
  ```
160
160
 
161
- For non-indexed filters (`age IN (30, 180)`, `duration > 60`), pull a slightly wider slice and filter in `jq`/Python.
161
+ For non-indexed filters (`age IN (30, 180)`, `view_count > 1000`), pull a slightly wider slice and filter in `jq`/Python.
162
162
 
163
163
  ## Common Query Patterns
164
164
 
@@ -171,16 +171,18 @@ tl db fb "SELECT id, age, view_count, like_count, comment_count, duration
171
171
  ORDER BY age"
172
172
  ```
173
173
 
174
- ### Get all videos for a channel, then filter client-side to milestone ages and longform
174
+ ### Get all videos for a channel, then filter client-side to milestone ages
175
175
 
176
176
  ```bash
177
177
  tl db fb "SELECT id, age, view_count, like_count, comment_count, duration, publication_date
178
178
  FROM article_metrics
179
179
  WHERE channel_id = 12345
180
180
  ORDER BY id, age" --json \
181
- | jq '.results[] | select(.duration > 60 and (.age == 7 or .age == 30 or .age == 60 or .age == 90 or .age == 180 or .age == 365))'
181
+ | jq '.results[] | select(.age == 7 or .age == 30 or .age == 60 or .age == 90 or .age == 180 or .age == 365)'
182
182
  ```
183
183
 
184
+ To keep longform only, pull `content_type` from Elasticsearch and intersect on video id — Firebolt cannot tell Shorts apart.
185
+
184
186
  ### Channel subscriber/view growth over time
185
187
 
186
188
  ```bash
@@ -1,3 +1,3 @@
1
1
  """ThoughtLeaders CLI — query sponsorship data, channels, brands, and intelligence."""
2
2
 
3
- __version__ = "0.9.13"
3
+ __version__ = "0.9.14"
@@ -0,0 +1,329 @@
1
+ """Auth CLI commands: tl auth login/logout/status."""
2
+
3
+ import sys
4
+ import time
5
+
6
+ import typer
7
+ from rich.console import Console
8
+ from rich.markup import escape
9
+ from rich.prompt import Prompt
10
+
11
+ from tl_cli._typer_utils import AlphaSortedTyperGroup
12
+ from tl_cli.auth.login import login_browser, login_device_code, revoke_refresh_token
13
+ from tl_cli.auth.token_store import KIND_API_KEY, StoredTokens, clear_tokens, load_tokens, save_tokens
14
+ from tl_cli.client.errors import SIGNED_OUT_CODE, ApiError
15
+ from tl_cli.client.http import get_client
16
+ from tl_cli.config import get_config
17
+
18
+ app = typer.Typer(cls=AlphaSortedTyperGroup, help="Authentication commands")
19
+ console = Console(stderr=True)
20
+
21
+
22
+ def _read_masked(prompt: str) -> str:
23
+ """Read a line of input echoing `*` for each character.
24
+
25
+ Falls back to plain `input()` when stdin is not a TTY (piped input,
26
+ test harness). Uses stdlib `termios` on Unix and `msvcrt` on Windows
27
+ so no extra dependency is needed.
28
+ """
29
+ if not sys.stdin.isatty():
30
+ return input(prompt)
31
+
32
+ sys.stdout.write(prompt)
33
+ sys.stdout.flush()
34
+
35
+ buf: list[str] = []
36
+ if sys.platform == 'win32':
37
+ import msvcrt
38
+ while True:
39
+ ch = msvcrt.getwch()
40
+ if ch in ('\r', '\n'):
41
+ sys.stdout.write('\n')
42
+ sys.stdout.flush()
43
+ break
44
+ if ch == '\x03': # Ctrl-C
45
+ raise KeyboardInterrupt
46
+ if ch in ('\b', '\x7f'):
47
+ if buf:
48
+ buf.pop()
49
+ sys.stdout.write('\b \b')
50
+ sys.stdout.flush()
51
+ continue
52
+ buf.append(ch)
53
+ sys.stdout.write('*')
54
+ sys.stdout.flush()
55
+ else:
56
+ import termios
57
+ import tty
58
+ fd = sys.stdin.fileno()
59
+ old = termios.tcgetattr(fd)
60
+ try:
61
+ tty.setcbreak(fd)
62
+ while True:
63
+ ch = sys.stdin.read(1)
64
+ if ch in ('\r', '\n'):
65
+ sys.stdout.write('\n')
66
+ sys.stdout.flush()
67
+ break
68
+ if ch == '\x03':
69
+ raise KeyboardInterrupt
70
+ if ch in ('\b', '\x7f'):
71
+ if buf:
72
+ buf.pop()
73
+ sys.stdout.write('\b \b')
74
+ sys.stdout.flush()
75
+ continue
76
+ buf.append(ch)
77
+ sys.stdout.write('*')
78
+ sys.stdout.flush()
79
+ finally:
80
+ termios.tcsetattr(fd, termios.TCSADRAIN, old)
81
+
82
+ return ''.join(buf)
83
+
84
+
85
+ LOGIN_METHODS = {"browser": "1", "device": "2", "api-key": "3"}
86
+
87
+
88
+ def _interactive() -> bool:
89
+ """Is a person at a terminal — i.e. may we ask a question or open a browser
90
+ window? Output goes to stderr, so that is the stream that matters; stdout
91
+ may well be a file or a pipe in a perfectly interactive session."""
92
+ return sys.stdin.isatty() and sys.stderr.isatty()
93
+
94
+
95
+ @app.command("login", help="Log in to ThoughtLeaders.")
96
+ def login_cmd(
97
+ method: str | None = typer.Option(
98
+ None,
99
+ "--method",
100
+ "-m",
101
+ help="Skip the menu: browser (OAuth2 on this machine), device (code for another "
102
+ "device), or api-key. Required when there is no terminal to answer the menu.",
103
+ ),
104
+ no_browser: bool = typer.Option(
105
+ False,
106
+ "--no-browser",
107
+ help="Browser method only: print the login URL instead of opening a browser, then wait "
108
+ "up to two minutes for it to be opened on this machine. Implied when there is no "
109
+ "terminal, so a window never pops up at an agent.",
110
+ ),
111
+ ) -> None:
112
+ """Log in to ThoughtLeaders.
113
+
114
+ The default flow opens a browser on this machine for OAuth2 (Auth0). If
115
+ you are already signed in to the web platform in that browser, it
116
+ completes without a password; either way it leaves the browser signed in
117
+ to the platform too, and the Chrome extension follows. A device-code flow
118
+ is available for headless environments, and a pre-issued API key can be
119
+ configured for CI/scripts.
120
+ """
121
+ if method is not None:
122
+ if method not in LOGIN_METHODS:
123
+ console.print(
124
+ f"[red]Unknown --method {method!r}.[/red] "
125
+ f"Choose one of: {', '.join(LOGIN_METHODS)}"
126
+ )
127
+ raise typer.Exit(1)
128
+ choice = LOGIN_METHODS[method]
129
+ elif not _interactive():
130
+ console.print(
131
+ "[red]No terminal to answer the login menu.[/red] "
132
+ "Pass --method browser, --method device or --method api-key."
133
+ )
134
+ raise typer.Exit(2)
135
+ else:
136
+ console.print("[bold]How would you like to authenticate?[/bold]")
137
+ console.print(
138
+ " [cyan]1[/cyan] — OAuth2 in a browser on this machine "
139
+ "[dim](default — opens a URL in the local browser)[/dim]"
140
+ )
141
+ console.print(" [cyan]2[/cyan] — Device code (use a browser on another device)")
142
+ console.print(" [cyan]3[/cyan] — API key (paste a pre-issued key; for CI / non-interactive use)")
143
+ console.print()
144
+ choice = Prompt.ask("Choose", choices=["1", "2", "3"], default="1", console=console)
145
+
146
+ if choice == "3":
147
+ _login_api_key()
148
+ return
149
+
150
+ if choice == "2":
151
+ login_device_code()
152
+ else:
153
+ login_browser(open_browser=not no_browser and _interactive())
154
+
155
+
156
+ def _login_api_key() -> None:
157
+ """Store a user-supplied API key as the active credential.
158
+
159
+ No browser, no finalize call — the server has already issued this key
160
+ against an existing user/organization. The stored record is tagged
161
+ `kind=api_key` so the HTTP client sends `X-TL-Auth: API-KEY` on every
162
+ request. We immediately call /whoami to (a) verify the key is valid and
163
+ (b) capture the owning user's email for `tl auth status` output.
164
+ """
165
+ key = _read_masked("Paste your API key: ").strip()
166
+ if not key:
167
+ console.print("[red]No key provided.[/red]")
168
+ raise typer.Exit(1)
169
+
170
+ save_tokens(
171
+ StoredTokens(
172
+ access_token=key,
173
+ refresh_token=None,
174
+ expires_at=time.time() + 10 * 365 * 24 * 3600,
175
+ email=None,
176
+ kind=KIND_API_KEY,
177
+ )
178
+ )
179
+
180
+ client = get_client()
181
+ try:
182
+ data = client.get("/whoami")
183
+ except ApiError as e:
184
+ clear_tokens()
185
+ console.print(f"[red]API key rejected:[/red] {escape(e.detail)}")
186
+ raise typer.Exit(1)
187
+ finally:
188
+ client.close()
189
+
190
+ email = (data.get("user") or {}).get("email")
191
+ if not email:
192
+ clear_tokens()
193
+ console.print(
194
+ "[red]API key accepted but the server returned no email for the owning user.[/red] "
195
+ "This usually means the user record is incomplete — contact support."
196
+ )
197
+ raise typer.Exit(1)
198
+
199
+ save_tokens(
200
+ StoredTokens(
201
+ access_token=key,
202
+ refresh_token=None,
203
+ expires_at=time.time() + 10 * 365 * 24 * 3600,
204
+ email=email,
205
+ kind=KIND_API_KEY,
206
+ )
207
+ )
208
+ console.print(f"[green]API key stored.[/green] Authenticated as: {email}")
209
+
210
+
211
+ @app.command("logout")
212
+ def logout_cmd() -> None:
213
+ """Sign out of ThoughtLeaders everywhere.
214
+
215
+ Every sign-out is global: ThoughtLeaders ends this session on every surface
216
+ and device — the web platform, the Chrome extension and any other machine
217
+ running `tl` — and this machine then revokes and clears its own stored
218
+ credentials.
219
+ """
220
+ tokens = load_tokens()
221
+ if tokens is None and get_config().api_key:
222
+ # An API key from the environment is not a session and is not ours to
223
+ # remove; with nothing stored there is nothing to clear either.
224
+ console.print(
225
+ "[yellow]TL_API_KEY is set in your environment; it is not a session and stays in effect. "
226
+ "Nothing is stored on this machine to clear.[/yellow]"
227
+ )
228
+ return
229
+ outcome = SIGNED_OUT
230
+ if tokens and tokens.is_api_key:
231
+ # An API key is not a session — it was issued in advance and nothing
232
+ # about it ends by signing out. Clearing it here is all there is to do.
233
+ console.print("[dim]An API key is not a session; clearing this machine's stored key.[/dim]")
234
+ elif tokens:
235
+ outcome = _sign_out_everywhere()
236
+ # That call may have refreshed the session (rotating the refresh token)
237
+ # or, finding it already ended, cleared the store: revoke what is there now.
238
+ tokens = load_tokens()
239
+ # Revoke the long-lived credential so a leaked/synced copy of the local
240
+ # token store can't keep minting access tokens. Best-effort — API-key auth
241
+ # has no refresh token, and an offline revoke must not block clearing the
242
+ # local credentials.
243
+ if tokens and not tokens.is_api_key and tokens.refresh_token:
244
+ if revoke_refresh_token(tokens.refresh_token):
245
+ console.print("[dim]Refresh token revoked at Auth0.[/dim]")
246
+ else:
247
+ console.print(
248
+ "[yellow]Could not reach Auth0 to revoke the refresh token; "
249
+ "clearing local credentials anyway.[/yellow]"
250
+ )
251
+ clear_tokens()
252
+ console.print("[green]Logged out successfully.[/green]")
253
+ if outcome == UNREACHABLE:
254
+ console.print(
255
+ "[yellow]Could not reach ThoughtLeaders to sign out everywhere. This machine's credentials "
256
+ "are cleared, but your other sessions stay signed in; sign out on the web platform once "
257
+ "you are online to end them.[/yellow]"
258
+ )
259
+ elif outcome == REFUSED:
260
+ console.print("[yellow]Your other sessions stay signed in; sign out on the web platform to end them.[/yellow]")
261
+
262
+
263
+ # What telling ThoughtLeaders about a sign-out came to.
264
+ SIGNED_OUT = "signed_out" # recorded now, or already by another surface
265
+ REFUSED = "refused" # ThoughtLeaders answered, and said no
266
+ UNREACHABLE = "unreachable" # no answer at all
267
+
268
+
269
+ def _sign_out_everywhere() -> str:
270
+ """Ask ThoughtLeaders to end this session everywhere. Best effort: nothing
271
+ here may stop the local logout — not an unreachable service, and not a token
272
+ refresh giving up (`SystemExit`). Speaks for the stored session even when
273
+ `TL_API_KEY` is set: that key is not the session being ended."""
274
+ client = None
275
+ try:
276
+ client = get_client(stored_session_only=True)
277
+ client.post("/auth/sign-out", json_body={})
278
+ console.print("[dim]Signed out everywhere.[/dim]")
279
+ return SIGNED_OUT
280
+ except ApiError as e:
281
+ if isinstance(e.raw, dict) and e.raw.get("code") == SIGNED_OUT_CODE:
282
+ # Someone got there first: this session was already ended
283
+ # elsewhere, which is exactly the state we were after.
284
+ console.print("[dim]Already signed out everywhere.[/dim]")
285
+ return SIGNED_OUT
286
+ console.print(f"[yellow]ThoughtLeaders did not sign you out everywhere: {escape(e.detail)}[/yellow]")
287
+ return REFUSED
288
+ except (Exception, SystemExit): # noqa: BLE001 — declared best-effort: nothing may stop the local logout
289
+ return UNREACHABLE
290
+ finally:
291
+ if client is not None:
292
+ client.close()
293
+
294
+
295
+ @app.command("status")
296
+ def status_cmd(
297
+ quiet: bool = typer.Option(
298
+ False, "--quiet", "-q", help="Print nothing; exit 0 if logged in, 2 otherwise."
299
+ ),
300
+ ) -> None:
301
+ """Show current authentication status: exit 0 when the next command will
302
+ authenticate, 2 when it will not."""
303
+ if get_config().api_key:
304
+ # TL_API_KEY wins over anything stored (see the HTTP client), so this
305
+ # machine is authenticated whatever the keychain holds.
306
+ if not quiet:
307
+ console.print("[green]Authenticated[/green] via API key from TL_API_KEY.")
308
+ return
309
+
310
+ tokens = load_tokens()
311
+ if not tokens:
312
+ if not quiet:
313
+ console.print("[yellow]Not logged in.[/yellow] Run: tl auth login")
314
+ raise SystemExit(2)
315
+
316
+ # An expired access token with a refresh token is still a working session:
317
+ # the next command refreshes it on its own.
318
+ if tokens.is_expired and not tokens.refresh_token:
319
+ if not quiet:
320
+ console.print(f"[yellow]Token expired.[/yellow] Logged in as: {tokens.email or 'unknown'}")
321
+ console.print("Run: tl auth login")
322
+ raise SystemExit(2)
323
+
324
+ if quiet:
325
+ return
326
+ if tokens.is_api_key:
327
+ console.print("[green]Authenticated[/green] via API key.")
328
+ else:
329
+ console.print(f"[green]Authenticated[/green] as: {tokens.email or 'unknown'}")