thoughtleaders-cli 0.9.23__tar.gz → 0.9.24__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 (218) hide show
  1. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/.claude-plugin/plugin.json +1 -1
  2. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/PKG-INFO +1 -1
  3. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/pyproject.toml +1 -1
  4. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-keyword-research/SKILL.md +79 -0
  5. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-keyword-research/scripts/build_report.py +48 -25
  6. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-keyword-research/scripts/expand_entities.py +50 -6
  7. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-keyword-research/scripts/probe.py +141 -82
  8. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-keyword-research/scripts/select_keywords.py +89 -14
  9. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/__init__.py +1 -1
  10. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/uv.lock +1 -1
  11. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/.claude-plugin/marketplace.json +0 -0
  12. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/.github/dependabot.yml +0 -0
  13. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/.github/workflows/ci.yml +0 -0
  14. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/.github/workflows/cli-integration.yml +0 -0
  15. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/.github/workflows/python-publish.yml +0 -0
  16. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/.gitignore +0 -0
  17. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/AGENTS.md +0 -0
  18. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/API.md +0 -0
  19. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/CLAUDE.md +0 -0
  20. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/LICENSE +0 -0
  21. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/README.md +0 -0
  22. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/agents/cast-sheet.md +0 -0
  23. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/agents/gem-classifier.md +0 -0
  24. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/agents/keyword-context-classifier.md +0 -0
  25. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/agents/keyword-entity-resolver.md +0 -0
  26. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/agents/keyword-relevance-validator.md +0 -0
  27. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/agents/merge-shard.md +0 -0
  28. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/agents/tl-analyst.md +0 -0
  29. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/agents/youtube-comment-classifier.md +0 -0
  30. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/hooks/hooks.json +0 -0
  31. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/hooks/scripts/load-tl-skill.mjs +0 -0
  32. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/hooks/scripts/post-usage.sh +0 -0
  33. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/hooks/scripts/pre-check.sh +0 -0
  34. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/_shared/tl_data.py +0 -0
  35. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/connector-impact/SKILL.md +0 -0
  36. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/connector-impact/references/help.md +0 -0
  37. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/connector-impact/references/impact-schema.md +0 -0
  38. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/connector-impact/references/impact-to-tl-mapping.md +0 -0
  39. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl/SKILL.md +0 -0
  40. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl/references/business-glossary.md +0 -0
  41. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl/references/elasticsearch-schema.md +0 -0
  42. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl/references/firebolt-schema.md +0 -0
  43. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl/references/postgres-schema.md +0 -0
  44. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/SKILL.md +0 -0
  45. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/references/cast-sheet.md +0 -0
  46. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/references/creator-brief-template.md +0 -0
  47. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/references/cue-phrases.txt +0 -0
  48. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/references/evidence-rules.md +0 -0
  49. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/references/extractor-rubric.md +0 -0
  50. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/references/identity-lane.md +0 -0
  51. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/references/profile-spec.md +0 -0
  52. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/references/transcript-mining.md +0 -0
  53. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/assemble_extracts.py +0 -0
  54. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/authenticate.py +0 -0
  55. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/bio_lane.py +0 -0
  56. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/brand_reads.py +0 -0
  57. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/build_html.py +0 -0
  58. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/cast_sheet.py +0 -0
  59. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/channel_context.py +0 -0
  60. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/cluster_gems.py +0 -0
  61. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/extractor_prompt.py +0 -0
  62. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/fetch_cues.py +0 -0
  63. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/identity_prompt.py +0 -0
  64. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/ledger_meta.py +0 -0
  65. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/merge_pass.py +0 -0
  66. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/moment_check.py +0 -0
  67. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/page.css +0 -0
  68. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/start_run.py +0 -0
  69. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/store_io.py +0 -0
  70. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/tier_hint.py +0 -0
  71. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/verify_quotes.py +0 -0
  72. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/.gitignore +0 -0
  73. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/SKILL.md +0 -0
  74. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/references/comment-patterns.md +0 -0
  75. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/references/peer-cohort.md +0 -0
  76. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/references/red-flags.md +0 -0
  77. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/references/scoring.md +0 -0
  78. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/scripts/_io_utf8.py +0 -0
  79. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/scripts/analyze_channel.py +0 -0
  80. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/scripts/anomaly_detector.py +0 -0
  81. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/scripts/channel_lookup.py +0 -0
  82. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/scripts/comment_analyzer.py +0 -0
  83. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/scripts/comment_scraper.py +0 -0
  84. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/scripts/engagement_ratios.py +0 -0
  85. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/scripts/peer_cohort.py +0 -0
  86. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/scripts/report.py +0 -0
  87. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/scripts/resolve_channel.py +0 -0
  88. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/scripts/score.py +0 -0
  89. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/scripts/video_integrity.py +0 -0
  90. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/scripts/view_curves.py +0 -0
  91. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-create-workflow/SKILL.md +0 -0
  92. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-create-workflow/references/creating-in-app.md +0 -0
  93. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-create-workflow/references/methodology.md +0 -0
  94. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-create-workflow/references/pitfalls.md +0 -0
  95. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-create-workflow/references/workflow-model.md +0 -0
  96. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-keyword-research/references/elasticsearch-content-search.md +0 -0
  97. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-keyword-research/references/help.md +0 -0
  98. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-keyword-research/scripts/fetch_context.py +0 -0
  99. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-keyword-research/scripts/search_channels.py +0 -0
  100. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-keyword-research/scripts/search_videos.py +0 -0
  101. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-save-report/SKILL.md +0 -0
  102. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-save-report/references/columns_brands.md +0 -0
  103. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-save-report/references/columns_channels.md +0 -0
  104. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-save-report/references/columns_content.md +0 -0
  105. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-save-report/references/columns_sponsorships.md +0 -0
  106. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-save-report/references/intelligence_filterset_schema.json +0 -0
  107. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-save-report/references/intelligence_widget_schema.json +0 -0
  108. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-save-report/references/report_glossary.md +0 -0
  109. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-save-report/references/sortable_columns.json +0 -0
  110. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-save-report/references/sponsorship_filterset_schema.json +0 -0
  111. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-save-report/references/sponsorship_widget_schema.json +0 -0
  112. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-save-report/references/widgets.md +0 -0
  113. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-top-partnerships/SKILL.md +0 -0
  114. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-top-partnerships/scripts/top_partnerships.py +0 -0
  115. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-views-guarantee/SKILL.md +0 -0
  116. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-views-guarantee/scripts/vg.py +0 -0
  117. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/__main__.py +0 -0
  118. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/_completions.py +0 -0
  119. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/_typer_utils.py +0 -0
  120. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/auth/__init__.py +0 -0
  121. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/auth/commands.py +0 -0
  122. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/auth/login.py +0 -0
  123. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/auth/pkce.py +0 -0
  124. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/auth/token_store.py +0 -0
  125. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/client/__init__.py +0 -0
  126. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/client/errors.py +0 -0
  127. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/client/http.py +0 -0
  128. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/__init__.py +0 -0
  129. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/_comments_common.py +0 -0
  130. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/agent.py +0 -0
  131. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/balance.py +0 -0
  132. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/brands.py +0 -0
  133. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/bulk_import.py +0 -0
  134. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/changelog.py +0 -0
  135. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/channels.py +0 -0
  136. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/credits.py +0 -0
  137. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/db.py +0 -0
  138. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/deals.py +0 -0
  139. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/describe.py +0 -0
  140. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/doctor.py +0 -0
  141. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/matches.py +0 -0
  142. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/memory.py +0 -0
  143. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/profiles.py +0 -0
  144. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/proposals.py +0 -0
  145. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/recommender.py +0 -0
  146. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/reports.py +0 -0
  147. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/schema.py +0 -0
  148. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/setup.py +0 -0
  149. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/skills.py +0 -0
  150. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/snapshots.py +0 -0
  151. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/sponsorships.py +0 -0
  152. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/uploads.py +0 -0
  153. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/whoami.py +0 -0
  154. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/workflows.py +0 -0
  155. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/config.py +0 -0
  156. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/filters.py +0 -0
  157. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/hints.py +0 -0
  158. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/main.py +0 -0
  159. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/output/__init__.py +0 -0
  160. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/output/formatter.py +0 -0
  161. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/query_history.py +0 -0
  162. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/self_update.py +0 -0
  163. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/skill_registry.py +0 -0
  164. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/__init__.py +0 -0
  165. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_agent_complete.py +0 -0
  166. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_assemble_extracts.py +0 -0
  167. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_auth.py +0 -0
  168. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_bio_lane.py +0 -0
  169. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_brands_winner_channels.py +0 -0
  170. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_build_report.py +0 -0
  171. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_cast_sheet.py +0 -0
  172. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_channels_lookalike.py +0 -0
  173. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_cluster_gems.py +0 -0
  174. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_connections_map_rules.py +0 -0
  175. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_creator_brief_accuracy.py +0 -0
  176. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_creator_brief_scripts.py +0 -0
  177. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_db_repeat_warning.py +0 -0
  178. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_describe.py +0 -0
  179. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_error_hints.py +0 -0
  180. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_expand_entities.py +0 -0
  181. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_extractor_prompt.py +0 -0
  182. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_fetch_cues.py +0 -0
  183. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_filters.py +0 -0
  184. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_http_auth.py +0 -0
  185. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_ledger_meta.py +0 -0
  186. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_memory.py +0 -0
  187. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_merge_pass.py +0 -0
  188. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_output.py +0 -0
  189. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_probe.py +0 -0
  190. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_probe_matching.py +0 -0
  191. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_profiles.py +0 -0
  192. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_quota_refusal_message.py +0 -0
  193. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_refusal_rendering.py +0 -0
  194. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_reports.py +0 -0
  195. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_scripts_executable.py +0 -0
  196. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_search_channels.py +0 -0
  197. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_search_videos.py +0 -0
  198. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_select_keywords.py +0 -0
  199. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_setup.py +0 -0
  200. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_setup_marker_respect.py +0 -0
  201. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_shared_tl_data.py +0 -0
  202. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_skill_registry.py +0 -0
  203. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_skills_command.py +0 -0
  204. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_sponsorships.py +0 -0
  205. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_start_run.py +0 -0
  206. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_whoami_newsletter.py +0 -0
  207. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_workflows.py +0 -0
  208. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests_cli/AGENTS.md +0 -0
  209. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests_cli/conftest.py +0 -0
  210. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests_cli/test_balance.py +0 -0
  211. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests_cli/test_db_es.py +0 -0
  212. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests_cli/test_db_fb.py +0 -0
  213. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests_cli/test_db_pg.py +0 -0
  214. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests_cli/test_schema.py +0 -0
  215. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests_cli/test_whoami.py +0 -0
  216. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tools/tl-brand-creator-connection/references/cue-phrases.trained-2026-09-15.txt +0 -0
  217. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tools/tl-brand-creator-connection/references/cue-training.md +0 -0
  218. {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tools/tl-brand-creator-connection/scripts/train_cues.py +0 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tl-cli",
3
- "version": "0.9.23",
3
+ "version": "0.9.24",
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.5
2
2
  Name: thoughtleaders-cli
3
- Version: 0.9.23
3
+ Version: 0.9.24
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.9.23"
7
+ version = "0.9.24"
8
8
  description = "ThoughtLeaders CLI — query sponsorship data, channels, brands, and intelligence"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -23,6 +23,33 @@ description: |
23
23
  common is X". Also invoke for HELP asks about this skill — "help", "how
24
24
  does keyword research work", "what are my options", "describe this skill" —
25
25
  answered free from the built-in guide, no queries run.
26
+ output-schema:
27
+ type: object
28
+ required: [keyword_groups, keyword_operator, assumptions]
29
+ properties:
30
+ keyword_groups:
31
+ type: array
32
+ items:
33
+ type: object
34
+ required: [text]
35
+ properties:
36
+ text: {type: string}
37
+ content_fields:
38
+ type: array
39
+ items: {type: string}
40
+ exclude: {type: boolean}
41
+ keyword_operator: {type: string, enum: [OR, AND]}
42
+ assumptions:
43
+ type: array
44
+ items: {type: string}
45
+ dropped:
46
+ type: array
47
+ items:
48
+ type: object
49
+ required: [text, reason]
50
+ properties:
51
+ text: {type: string}
52
+ reason: {type: string}
26
53
  ---
27
54
 
28
55
  # tl-keyword-research — topic → validated filter set + the results you choose
@@ -69,6 +96,33 @@ verbatim via `search_channels.py --group` / `search_videos.py --group`.
69
96
 
70
97
  `<SKILL_DIR>` below is this skill's directory (the one holding `SKILL.md`).
71
98
 
99
+ ## On the ThoughtLeaders platform
100
+
101
+ The same skill also runs on the ThoughtLeaders platform, when a user starts
102
+ keyword research from their dashboard. You are on the platform when the tools
103
+ `kw_probe` and `kw_validate` are available. The method in this file applies
104
+ unchanged, with these differences:
105
+
106
+ - **No shell and no sub-agents.** Stages that run a script or an agent carry
107
+ an *On the platform* note naming the tool to use instead.
108
+ - **Nobody to ask.** The task gives you the path (quick or deep) and the
109
+ deliverable. Make every scope call yourself and record it in `assumptions`.
110
+ Run the deep path autonomously: no checkpoint after round 3.
111
+ - **Titles and summaries only.** Search the `title` and `summary` fields,
112
+ never `transcript`.
113
+ - **Stop after Stage 4.** The platform builds the trend data, channel tiers,
114
+ report link and previews from your final groups, so skip Stage 5 and the
115
+ scripts in Stage 6.
116
+ - **Decide, don't count.** Finish with `submit_result`, shaped by this
117
+ skill's `output-schema`: the final keyword groups, the operator, your
118
+ assumptions, and the groups you dropped with a reason. Never put counts or
119
+ channel figures in the result.
120
+ - **Narrate with `report_progress`.** Call it at the start of each stage with
121
+ the stage name exactly as the pipeline table writes it (`Set up`, `Expand`,
122
+ `Probe`, `Validate keywords`, `Refine`), instead of messaging the user.
123
+ - **References.** Read `references/elasticsearch-content-search.md` with
124
+ `read_skill_file`.
125
+
72
126
  ## When to invoke / skip
73
127
 
74
128
  Invoke when the user wants **videos or channels by content** (topics, concepts,
@@ -234,6 +288,10 @@ index's samples, and every web-sourced name earns its place through the same
234
288
  probe + validation as any other candidate (no benefit of the doubt; a
235
289
  hallucinated name probes to `count: 0` and drops).
236
290
 
291
+ *On the platform:* use `web_search` (at most two searches) in place of the
292
+ `keyword-entity-resolver` agent, then add each name and its spelling variants
293
+ to your candidates yourself, following Stage 1's rules.
294
+
237
295
  ### Stage 1 — Expand: go wide, go deep, go creative
238
296
 
239
297
  A shallow synonym list is the #1 failure mode. Coverage is won or lost here.
@@ -271,6 +329,11 @@ Rules:
271
329
 
272
330
  ### Stage 2 — Probe (`probe.py`)
273
331
 
332
+ *On the platform:* call `kw_probe` with the candidates and the same options
333
+ `probe.py` takes (`level`, `mode`, `fields`, `samples`, `content_type`,
334
+ `since`, `until`). It returns the counts; the samples stay with the platform
335
+ for `kw_validate`.
336
+
274
337
  One ES query per candidate → counts + samples for validation:
275
338
 
276
339
  ```bash
@@ -322,6 +385,11 @@ surviving variants before delivery.
322
385
  This fixes "the word is there but the topic isn't" — the step that separates
323
386
  this skill from a free YouTube search.
324
387
 
388
+ *On the platform:* call `kw_validate` on the probe with the verbatim intent. It
389
+ runs the relevance check and the strict-majority rule, and returns the kept
390
+ and dropped keywords. Scope calls are yours: decide them and record them in
391
+ `assumptions`.
392
+
325
393
  **Inline (≤ ~15 candidates):** read each keyword's `samples` against the
326
394
  verbatim intent. Drop off-intent keywords, `count: 0`, and redundant
327
395
  `subsumed_by` duplicates.
@@ -448,8 +516,14 @@ then fold the answers into the groups and the validators' TOPIC/NOT lines.
448
516
  run ≥3 rounds, stop when fitness stops improving (sane cap ~6 rounds), note
449
517
  that you ran autonomously.
450
518
 
519
+ *On the platform:* always run autonomously, measuring with `kw_probe` (mode
520
+ `sqs`) and re-validating with `kw_validate`.
521
+
451
522
  ### Stage 5 — Materialize the chosen deliverables
452
523
 
524
+ *On the platform:* skip this stage. The platform builds the deliverables from
525
+ your final groups.
526
+
453
527
  Everything here takes the final filter verbatim via `--group`, so what you
454
528
  deliver is exactly what the filter selects.
455
529
 
@@ -541,6 +615,11 @@ on-topic uploads. Tell the user when one channel dominates and offer
541
615
 
542
616
  ### Stage 6 — Deliver (`build_report.py`)
543
617
 
618
+ *On the platform:* don't run `build_report.py` and don't offer to save. Call
619
+ `submit_result` with the groups in the same shape (`text`, plus
620
+ `content_fields` / `exclude` where needed); the platform prunes them,
621
+ translates them into the report grammar and builds the link.
622
+
544
623
  Hand the validated groups to the builder — start from `select_keywords.py
545
624
  --apply`'s `groups`, adding per-group `content_fields` / `exclude` as needed:
546
625
 
@@ -39,7 +39,15 @@ Input (stdin JSON):
39
39
  }
40
40
 
41
41
  Output (stdout): a single JSON object — see OUTPUT CONTRACT at the bottom.
42
+
43
+ Library use: the file is import-safe (standard library only, nothing runs at import),
44
+ so another Python program can load it by path and call `build(spec, *, app_url=...)`.
45
+ `spec` is the parsed input object above; the return value is the object the command
46
+ prints. `build` leaves `spec` untouched and returns fresh structures. A bad spec raises
47
+ `ReportSpecError` (a `ValueError`). Library functions never print, read stdin or exit.
42
48
  """
49
+ from __future__ import annotations
50
+
43
51
  import argparse
44
52
  import json
45
53
  import re
@@ -116,6 +124,10 @@ DEFAULT_FIELDS = {
116
124
  }
117
125
 
118
126
 
127
+ class ReportSpecError(ValueError):
128
+ """The spec handed to `build` is unusable."""
129
+
130
+
119
131
  def tokens(text):
120
132
  return _TOKEN_RE.findall(text.lower())
121
133
 
@@ -389,33 +401,23 @@ def build_report_config(filter_set, report_type_int, title, description):
389
401
  }
390
402
 
391
403
 
392
- def main():
393
- ap = argparse.ArgumentParser(
394
- description="Build the keyword-research deliverable (filter set + report link) from validated groups."
395
- )
396
- ap.add_argument("--app-url", default=DEFAULT_APP_URL, help=f"App base URL (default {DEFAULT_APP_URL})")
397
- args = ap.parse_args()
398
-
399
- if sys.stdin.isatty():
400
- sys.exit("pipe a JSON spec on stdin (see module docstring)")
401
- try:
402
- spec = json.loads(sys.stdin.read())
403
- except json.JSONDecodeError as exc:
404
- sys.exit(f"invalid JSON on stdin: {exc}")
405
-
406
- operator = (spec.get("operator") or "OR").upper()
404
+ def build(spec, *, app_url=DEFAULT_APP_URL):
405
+ """Build the deliverable (filter set, report link, report config) from a spec; raises ReportSpecError."""
406
+ if not isinstance(spec, dict):
407
+ raise ReportSpecError("spec must be a JSON object")
408
+ operator = str(spec.get("operator") or "OR").upper()
407
409
  if operator not in ("AND", "OR"):
408
- sys.exit("operator must be AND or OR")
409
- report_type = (spec.get("report_type") or "channels").lower()
410
+ raise ReportSpecError("operator must be AND or OR")
411
+ report_type = str(spec.get("report_type") or "channels").lower()
410
412
  if report_type not in REPORT_TYPES:
411
- sys.exit(f"report_type must be one of {sorted(REPORT_TYPES)}")
413
+ raise ReportSpecError(f"report_type must be one of {sorted(REPORT_TYPES)}")
412
414
  slug, report_type_int = REPORT_TYPES[report_type]
413
415
  default_fields = spec.get("default_content_fields") or DEFAULT_FIELDS[report_type]
414
416
 
415
417
  groups = spec.get("groups") or []
416
- groups = [g for g in groups if isinstance(g, dict) and str(g.get("text", "")).strip()]
418
+ groups = [dict(g) for g in groups if isinstance(g, dict) and str(g.get("text", "")).strip()]
417
419
  if not groups:
418
- sys.exit("provide at least one group with non-empty 'text'")
420
+ raise ReportSpecError("provide at least one group with non-empty 'text'")
419
421
  for g in groups:
420
422
  g["text"] = str(g["text"]).strip()
421
423
 
@@ -426,7 +428,7 @@ def main():
426
428
  used_fields.update(g.get("content_fields") or [])
427
429
  unknown = sorted(f for f in used_fields if f not in VALID_CONTENT_FIELDS)
428
430
  if unknown:
429
- sys.exit(
431
+ raise ReportSpecError(
430
432
  f"unknown content_fields {unknown}; use recognized content field names "
431
433
  f"(e.g. title, summary, transcript, channel_description, "
432
434
  f"channel_topic_description) — not raw ES paths. Valid: {sorted(VALID_CONTENT_FIELDS)}"
@@ -443,18 +445,18 @@ def main():
443
445
  try:
444
446
  app_text = sqs_to_app_syntax(g["text"])
445
447
  except ValueError as exc:
446
- sys.exit(f"group {g['text']!r}: {exc}")
448
+ raise ReportSpecError(f"group {g['text']!r}: {exc}")
447
449
  if app_text != g["text"]:
448
450
  translated.append({"from": g["text"], "to": app_text})
449
451
  g["text"] = app_text
450
452
 
451
453
  filter_set = build_filter_set(kept, operator, default_fields)
452
- report_link = build_inline_link(kept, operator, slug, default_fields, args.app_url)
454
+ report_link = build_inline_link(kept, operator, slug, default_fields, app_url)
453
455
  title = spec.get("title") or f"{report_type.title()} — keyword search"
454
456
  description = spec.get("description") or ""
455
457
  report_config = build_report_config(filter_set, report_type_int, title, description)
456
458
 
457
- print(json.dumps({
459
+ return {
458
460
  "report_type": report_type,
459
461
  "operator": operator,
460
462
  "expression": render_expression(kept, operator),
@@ -463,7 +465,28 @@ def main():
463
465
  "report_config": report_config,
464
466
  "pruned": pruned,
465
467
  "translated": translated,
466
- }, ensure_ascii=False, indent=2))
468
+ }
469
+
470
+
471
+ def main():
472
+ ap = argparse.ArgumentParser(
473
+ description="Build the keyword-research deliverable (filter set + report link) from validated groups."
474
+ )
475
+ ap.add_argument("--app-url", default=DEFAULT_APP_URL, help=f"App base URL (default {DEFAULT_APP_URL})")
476
+ args = ap.parse_args()
477
+
478
+ if sys.stdin.isatty():
479
+ sys.exit("pipe a JSON spec on stdin (see module docstring)")
480
+ try:
481
+ spec = json.loads(sys.stdin.read())
482
+ except json.JSONDecodeError as exc:
483
+ sys.exit(f"invalid JSON on stdin: {exc}")
484
+
485
+ try:
486
+ out = build(spec, app_url=args.app_url)
487
+ except ReportSpecError as exc:
488
+ sys.exit(str(exc))
489
+ print(json.dumps(out, ensure_ascii=False, indent=2))
467
490
 
468
491
 
469
492
  # OUTPUT CONTRACT (stdout, single JSON object):
@@ -33,8 +33,19 @@ Usage:
33
33
  python3 expand_entities.py --existing "cannes lions" "advertising awards" < resolver.json > expanded.json
34
34
  # pipe the candidates straight into probe.py:
35
35
  python3 expand_entities.py --probe-batch < resolver.json | python3 probe.py --samples 5
36
+
37
+ Library use: the file is import-safe (standard library only, nothing runs at import),
38
+ so another Python program can load it by path and call `expand(resolver_reply, *,
39
+ existing=None)` (what the default mode prints) or `probe_batch(resolver_reply, *,
40
+ existing=None)` (what `--probe-batch` prints). `resolver_reply` is the parsed resolver
41
+ JSON; `existing` lists the candidates already brainstormed. Both return fresh structures
42
+ that share nothing with the input. Bad input raises `ExpandInputError` (a `ValueError`).
43
+ Library functions never print, read stdin or exit.
36
44
  """
45
+ from __future__ import annotations
46
+
37
47
  import argparse
48
+ import copy
38
49
  import json
39
50
  import re
40
51
  import sys
@@ -80,6 +91,10 @@ def solid(text):
80
91
  return _NONWORD_RE.sub("", text)
81
92
 
82
93
 
94
+ class ExpandInputError(ValueError):
95
+ """The resolver reply or options handed to the expander are unusable."""
96
+
97
+
83
98
  def _norm(text):
84
99
  """Case/space-insensitive key for dedupe."""
85
100
  return re.sub(r"\s+", " ", text.strip().lower())
@@ -156,8 +171,25 @@ def _collect_names(resolver, max_insider):
156
171
  return fams
157
172
 
158
173
 
159
- def expand(resolver, existing, max_insider):
160
- """Build probe candidates + family detail from a resolver JSON object."""
174
+ def check_resolver(resolver_reply):
175
+ """Raise ExpandInputError unless the resolver reply is a JSON object."""
176
+ if not isinstance(resolver_reply, dict):
177
+ raise ExpandInputError("resolver JSON must be an object")
178
+
179
+
180
+ def expand(resolver_reply, *, existing=None, max_insider=MAX_INSIDER_DEFAULT):
181
+ """Build probe candidates + family detail from a resolver JSON object (what the default mode prints).
182
+
183
+ `existing` lists candidates already brainstormed; matching forms are dropped.
184
+ `max_insider` caps the insider terms folded in. Raises ExpandInputError.
185
+ """
186
+ check_resolver(resolver_reply)
187
+ resolver = resolver_reply
188
+ existing = [] if existing is None else existing
189
+ if not isinstance(existing, (list, tuple)) or not all(isinstance(x, str) for x in existing):
190
+ raise ExpandInputError("existing must be a list of strings")
191
+ if max_insider < 0:
192
+ raise ExpandInputError("--max-insider must be >= 0")
161
193
  seen = {_norm(x) for x in existing} # forms already covered → skip
162
194
  deduped = []
163
195
 
@@ -211,7 +243,7 @@ def expand(resolver, existing, max_insider):
211
243
  )
212
244
  collisions = [c for c in (resolver.get("collisions", []) or []) if isinstance(c, dict)]
213
245
 
214
- return {
246
+ result = {
215
247
  "probe_candidates": probe_candidates,
216
248
  "families": families,
217
249
  "aliases": aliases,
@@ -228,6 +260,12 @@ def expand(resolver, existing, max_insider):
228
260
  "deduped": len(deduped),
229
261
  },
230
262
  }
263
+ return copy.deepcopy(result) # the caller's input is never shared with the output
264
+
265
+
266
+ def probe_batch(resolver_reply, *, existing=None, max_insider=MAX_INSIDER_DEFAULT):
267
+ """Just the probe_candidates array (what --probe-batch prints), ready for probe.py."""
268
+ return expand(resolver_reply, existing=existing, max_insider=max_insider)["probe_candidates"]
231
269
 
232
270
 
233
271
  def load_resolver():
@@ -240,8 +278,10 @@ def load_resolver():
240
278
  data = json.loads(raw)
241
279
  except json.JSONDecodeError as exc:
242
280
  sys.exit(f"invalid resolver JSON on stdin: {exc}")
243
- if not isinstance(data, dict):
244
- sys.exit("resolver JSON must be an object")
281
+ try:
282
+ check_resolver(data)
283
+ except ExpandInputError as exc:
284
+ sys.exit(str(exc))
245
285
  return data
246
286
 
247
287
 
@@ -259,7 +299,11 @@ def main():
259
299
  if args.max_insider < 0:
260
300
  sys.exit("--max-insider must be >= 0")
261
301
 
262
- out = expand(load_resolver(), args.existing, args.max_insider)
302
+ resolver = load_resolver()
303
+ try:
304
+ out = expand(resolver, existing=args.existing, max_insider=args.max_insider)
305
+ except ExpandInputError as exc:
306
+ sys.exit(str(exc))
263
307
  if args.probe_batch:
264
308
  print(json.dumps(out["probe_candidates"], ensure_ascii=False))
265
309
  else: