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.
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/.claude-plugin/plugin.json +1 -1
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/PKG-INFO +1 -1
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/pyproject.toml +1 -1
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-keyword-research/SKILL.md +79 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-keyword-research/scripts/build_report.py +48 -25
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-keyword-research/scripts/expand_entities.py +50 -6
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-keyword-research/scripts/probe.py +141 -82
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-keyword-research/scripts/select_keywords.py +89 -14
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/__init__.py +1 -1
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/uv.lock +1 -1
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/.claude-plugin/marketplace.json +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/.github/dependabot.yml +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/.github/workflows/ci.yml +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/.github/workflows/cli-integration.yml +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/.github/workflows/python-publish.yml +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/.gitignore +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/AGENTS.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/API.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/CLAUDE.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/LICENSE +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/README.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/agents/cast-sheet.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/agents/gem-classifier.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/agents/keyword-context-classifier.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/agents/keyword-entity-resolver.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/agents/keyword-relevance-validator.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/agents/merge-shard.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/agents/tl-analyst.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/agents/youtube-comment-classifier.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/hooks/hooks.json +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/hooks/scripts/load-tl-skill.mjs +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/hooks/scripts/post-usage.sh +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/hooks/scripts/pre-check.sh +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/_shared/tl_data.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/connector-impact/SKILL.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/connector-impact/references/help.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/connector-impact/references/impact-schema.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/connector-impact/references/impact-to-tl-mapping.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl/SKILL.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl/references/business-glossary.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl/references/elasticsearch-schema.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl/references/firebolt-schema.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl/references/postgres-schema.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/SKILL.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/references/cast-sheet.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/references/creator-brief-template.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/references/cue-phrases.txt +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/references/evidence-rules.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/references/extractor-rubric.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/references/identity-lane.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/references/profile-spec.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/references/transcript-mining.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/assemble_extracts.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/authenticate.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/bio_lane.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/brand_reads.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/build_html.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/cast_sheet.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/channel_context.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/cluster_gems.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/extractor_prompt.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/fetch_cues.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/identity_prompt.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/ledger_meta.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/merge_pass.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/moment_check.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/page.css +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/start_run.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/store_io.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/tier_hint.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/verify_quotes.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/.gitignore +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/SKILL.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/references/comment-patterns.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/references/peer-cohort.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/references/red-flags.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/references/scoring.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/scripts/_io_utf8.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/scripts/analyze_channel.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/scripts/anomaly_detector.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/scripts/channel_lookup.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/scripts/comment_analyzer.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/scripts/comment_scraper.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/scripts/engagement_ratios.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/scripts/peer_cohort.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/scripts/report.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/scripts/resolve_channel.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/scripts/score.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/scripts/video_integrity.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/scripts/view_curves.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-create-workflow/SKILL.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-create-workflow/references/creating-in-app.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-create-workflow/references/methodology.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-create-workflow/references/pitfalls.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-create-workflow/references/workflow-model.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-keyword-research/references/elasticsearch-content-search.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-keyword-research/references/help.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-keyword-research/scripts/fetch_context.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-keyword-research/scripts/search_channels.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-keyword-research/scripts/search_videos.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-save-report/SKILL.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-save-report/references/columns_brands.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-save-report/references/columns_channels.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-save-report/references/columns_content.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-save-report/references/columns_sponsorships.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-save-report/references/intelligence_filterset_schema.json +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-save-report/references/intelligence_widget_schema.json +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-save-report/references/report_glossary.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-save-report/references/sortable_columns.json +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-save-report/references/sponsorship_filterset_schema.json +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-save-report/references/sponsorship_widget_schema.json +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-save-report/references/widgets.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-top-partnerships/SKILL.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-top-partnerships/scripts/top_partnerships.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-views-guarantee/SKILL.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/skills/tl-views-guarantee/scripts/vg.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/__main__.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/_completions.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/_typer_utils.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/auth/__init__.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/auth/commands.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/auth/login.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/auth/pkce.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/auth/token_store.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/client/__init__.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/client/errors.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/client/http.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/__init__.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/_comments_common.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/agent.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/balance.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/brands.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/bulk_import.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/changelog.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/channels.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/credits.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/db.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/deals.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/describe.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/doctor.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/matches.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/memory.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/profiles.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/proposals.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/recommender.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/reports.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/schema.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/setup.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/skills.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/snapshots.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/sponsorships.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/uploads.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/whoami.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/workflows.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/config.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/filters.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/hints.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/main.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/output/__init__.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/output/formatter.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/query_history.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/self_update.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/src/tl_cli/skill_registry.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/__init__.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_agent_complete.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_assemble_extracts.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_auth.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_bio_lane.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_brands_winner_channels.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_build_report.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_cast_sheet.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_channels_lookalike.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_cluster_gems.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_connections_map_rules.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_creator_brief_accuracy.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_creator_brief_scripts.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_db_repeat_warning.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_describe.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_error_hints.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_expand_entities.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_extractor_prompt.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_fetch_cues.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_filters.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_http_auth.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_ledger_meta.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_memory.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_merge_pass.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_output.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_probe.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_probe_matching.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_profiles.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_quota_refusal_message.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_refusal_rendering.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_reports.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_scripts_executable.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_search_channels.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_search_videos.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_select_keywords.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_setup.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_setup_marker_respect.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_shared_tl_data.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_skill_registry.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_skills_command.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_sponsorships.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_start_run.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_whoami_newsletter.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests/test_workflows.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests_cli/AGENTS.md +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests_cli/conftest.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests_cli/test_balance.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests_cli/test_db_es.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests_cli/test_db_fb.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests_cli/test_db_pg.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests_cli/test_schema.py +0 -0
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tests_cli/test_whoami.py +0 -0
- {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
- {thoughtleaders_cli-0.9.23 → thoughtleaders_cli-0.9.24}/tools/tl-brand-creator-connection/references/cue-training.md +0 -0
- {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
|
Metadata-Version: 2.5
|
|
2
2
|
Name: thoughtleaders-cli
|
|
3
|
-
Version: 0.9.
|
|
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
|
|
@@ -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
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
|
160
|
-
"""
|
|
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
|
-
|
|
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
|
-
|
|
244
|
-
|
|
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
|
-
|
|
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:
|