thoughtleaders-cli 0.9.22__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.22 → thoughtleaders_cli-0.9.24}/.claude-plugin/plugin.json +1 -1
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/PKG-INFO +1 -1
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/pyproject.toml +1 -1
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl/SKILL.md +2 -2
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl/references/business-glossary.md +2 -2
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl/references/postgres-schema.md +3 -2
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-keyword-research/SKILL.md +79 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-keyword-research/scripts/build_report.py +48 -25
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-keyword-research/scripts/expand_entities.py +50 -6
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-keyword-research/scripts/probe.py +141 -82
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-keyword-research/scripts/select_keywords.py +89 -14
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-save-report/references/sortable_columns.json +1 -1
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/src/tl_cli/__init__.py +1 -1
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/channels.py +1 -1
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/uv.lock +1 -1
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/.claude-plugin/marketplace.json +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/.github/dependabot.yml +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/.github/workflows/ci.yml +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/.github/workflows/cli-integration.yml +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/.github/workflows/python-publish.yml +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/.gitignore +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/AGENTS.md +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/API.md +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/CLAUDE.md +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/LICENSE +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/README.md +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/agents/cast-sheet.md +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/agents/gem-classifier.md +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/agents/keyword-context-classifier.md +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/agents/keyword-entity-resolver.md +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/agents/keyword-relevance-validator.md +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/agents/merge-shard.md +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/agents/tl-analyst.md +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/agents/youtube-comment-classifier.md +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/hooks/hooks.json +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/hooks/scripts/load-tl-skill.mjs +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/hooks/scripts/post-usage.sh +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/hooks/scripts/pre-check.sh +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/_shared/tl_data.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/connector-impact/SKILL.md +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/connector-impact/references/help.md +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/connector-impact/references/impact-schema.md +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/connector-impact/references/impact-to-tl-mapping.md +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl/references/elasticsearch-schema.md +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl/references/firebolt-schema.md +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/SKILL.md +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/references/cast-sheet.md +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/references/creator-brief-template.md +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/references/cue-phrases.txt +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/references/evidence-rules.md +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/references/extractor-rubric.md +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/references/identity-lane.md +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/references/profile-spec.md +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/references/transcript-mining.md +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/assemble_extracts.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/authenticate.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/bio_lane.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/brand_reads.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/build_html.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/cast_sheet.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/channel_context.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/cluster_gems.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/extractor_prompt.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/fetch_cues.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/identity_prompt.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/ledger_meta.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/merge_pass.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/moment_check.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/page.css +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/start_run.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/store_io.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/tier_hint.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-brand-creator-connection/scripts/verify_quotes.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/.gitignore +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/SKILL.md +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/references/comment-patterns.md +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/references/peer-cohort.md +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/references/red-flags.md +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/references/scoring.md +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/scripts/_io_utf8.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/scripts/analyze_channel.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/scripts/anomaly_detector.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/scripts/channel_lookup.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/scripts/comment_analyzer.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/scripts/comment_scraper.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/scripts/engagement_ratios.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/scripts/peer_cohort.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/scripts/report.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/scripts/resolve_channel.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/scripts/score.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/scripts/video_integrity.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-channel-authenticity/scripts/view_curves.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-create-workflow/SKILL.md +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-create-workflow/references/creating-in-app.md +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-create-workflow/references/methodology.md +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-create-workflow/references/pitfalls.md +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-create-workflow/references/workflow-model.md +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-keyword-research/references/elasticsearch-content-search.md +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-keyword-research/references/help.md +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-keyword-research/scripts/fetch_context.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-keyword-research/scripts/search_channels.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-keyword-research/scripts/search_videos.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-save-report/SKILL.md +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-save-report/references/columns_brands.md +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-save-report/references/columns_channels.md +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-save-report/references/columns_content.md +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-save-report/references/columns_sponsorships.md +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-save-report/references/intelligence_filterset_schema.json +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-save-report/references/intelligence_widget_schema.json +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-save-report/references/report_glossary.md +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-save-report/references/sponsorship_filterset_schema.json +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-save-report/references/sponsorship_widget_schema.json +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-save-report/references/widgets.md +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-top-partnerships/SKILL.md +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-top-partnerships/scripts/top_partnerships.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-views-guarantee/SKILL.md +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl-views-guarantee/scripts/vg.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/src/tl_cli/__main__.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/src/tl_cli/_completions.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/src/tl_cli/_typer_utils.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/src/tl_cli/auth/__init__.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/src/tl_cli/auth/commands.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/src/tl_cli/auth/login.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/src/tl_cli/auth/pkce.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/src/tl_cli/auth/token_store.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/src/tl_cli/client/__init__.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/src/tl_cli/client/errors.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/src/tl_cli/client/http.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/__init__.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/_comments_common.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/agent.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/balance.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/brands.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/bulk_import.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/changelog.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/credits.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/db.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/deals.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/describe.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/doctor.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/matches.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/memory.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/profiles.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/proposals.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/recommender.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/reports.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/schema.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/setup.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/skills.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/snapshots.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/sponsorships.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/uploads.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/whoami.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/src/tl_cli/commands/workflows.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/src/tl_cli/config.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/src/tl_cli/filters.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/src/tl_cli/hints.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/src/tl_cli/main.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/src/tl_cli/output/__init__.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/src/tl_cli/output/formatter.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/src/tl_cli/query_history.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/src/tl_cli/self_update.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/src/tl_cli/skill_registry.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests/__init__.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests/test_agent_complete.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests/test_assemble_extracts.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests/test_auth.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests/test_bio_lane.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests/test_brands_winner_channels.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests/test_build_report.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests/test_cast_sheet.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests/test_channels_lookalike.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests/test_cluster_gems.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests/test_connections_map_rules.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests/test_creator_brief_accuracy.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests/test_creator_brief_scripts.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests/test_db_repeat_warning.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests/test_describe.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests/test_error_hints.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests/test_expand_entities.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests/test_extractor_prompt.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests/test_fetch_cues.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests/test_filters.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests/test_http_auth.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests/test_ledger_meta.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests/test_memory.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests/test_merge_pass.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests/test_output.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests/test_probe.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests/test_probe_matching.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests/test_profiles.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests/test_quota_refusal_message.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests/test_refusal_rendering.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests/test_reports.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests/test_scripts_executable.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests/test_search_channels.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests/test_search_videos.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests/test_select_keywords.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests/test_setup.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests/test_setup_marker_respect.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests/test_shared_tl_data.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests/test_skill_registry.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests/test_skills_command.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests/test_sponsorships.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests/test_start_run.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests/test_whoami_newsletter.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests/test_workflows.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests_cli/AGENTS.md +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests_cli/conftest.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests_cli/test_balance.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests_cli/test_db_es.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests_cli/test_db_fb.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests_cli/test_db_pg.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests_cli/test_schema.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tests_cli/test_whoami.py +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tools/tl-brand-creator-connection/references/cue-phrases.trained-2026-09-15.txt +0 -0
- {thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/tools/tl-brand-creator-connection/references/cue-training.md +0 -0
- {thoughtleaders_cli-0.9.22 → 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
|
|
@@ -95,7 +95,7 @@ Other key concepts:
|
|
|
95
95
|
- Org scoping for sponsorships is profile-mediated: a sponsorship belongs to your org if **either** `advertiser_profile.organization` (brand side) **or** the adspot seller's organization — `ad_spot.publisher` → that user's profile → `organization` (publisher side) — matches yours. Representing the channel does not surface its deals.
|
|
96
96
|
- **MSN** (Media Selling Network) — the ~12k YouTube channels that have opted in to receive sponsorship offers. A channels is in the MSN group if the `channel.media_selling_network_join_date` field is not null.
|
|
97
97
|
- **MBN** (Media Buying Network) — the brand-side counterpart to MSN: brand profiles that have opted in to receive proposed sponsorships. A profile is in the MBN group if the `profile.media_buying_network_join_date` field is not null.
|
|
98
|
-
- **TPP** (ThoughtLeaders Partner Program, a.k.a. "TL channels") — the ~170 channels TL has the closest working relationship with. A channel is in the TPP group if `channel.is_tpp` is True. **Prefer TPP channels when booking**: they respond fastest, are the easiest to close, and don't need an outreach round-trip — treat them as immediately bookable. TPP is a strict subset of MSN, so the same booking rules (one
|
|
98
|
+
- **TPP** (ThoughtLeaders Partner Program, a.k.a. "TL channels") — the ~170 channels TL has the closest working relationship with. A channel is in the TPP group if `channel.is_tpp` is True. **Prefer TPP channels when booking**: they respond fastest, are the easiest to close, and don't need an outreach round-trip — treat them as immediately bookable. TPP is a strict subset of MSN, so the same booking rules (one preferred mention adspot, etc.) apply.
|
|
99
99
|
- **`demographics_updated_at`** (on channels) — If non-null, the channel has demographics screenshots on file. If null, no demographics screenshots have been uploaded. Use this to check whether a channel has demographics data from screenshots.
|
|
100
100
|
- **`subscribers`** (on channels) — the channel's subscriber count.
|
|
101
101
|
- **`projected_views`** (on channels) — projected views per video on that channel. Forward-looking estimate. May be null when not yet computed. ⚠️ NOT actual views and NOT ad-industry "impressions" (ads served).
|
|
@@ -103,7 +103,7 @@ Other key concepts:
|
|
|
103
103
|
- **`views_guarantee`** (on sponsorships) — projected/guaranteed impressions for the sponsorship. Numeric.
|
|
104
104
|
- **Sponsorship detail fields** (returned by `tl sponsorships show <id> --json`) — the detail payload includes `integration` (raw int), `publish_count`, `common_name`, `outreach_email`, nested `publisher` (`first_name`, `last_name`, `email`), nested `brand_contact` (`first_name`, `last_name`, `email`), and `brand.organization_name`. Use these when generating IOs, contracts, or outreach.
|
|
105
105
|
- **CPM** has two distinct meanings depending on level — pick the one the user actually wants:
|
|
106
|
-
- **Channel CPM** = `(adspot.price / channel.projected_views) * 1000` — projected price per thousand projected views. Used for pricing decisions **before** a sponsorship is sold. Available for channels with
|
|
106
|
+
- **Channel CPM** = `(adspot.price / channel.projected_views) * 1000` — projected price per thousand projected views. Used for pricing decisions **before** a sponsorship is sold. Available for channels with preferred adspots via `tl channels show <channel_id>`.
|
|
107
107
|
- **Sponsorship CPM** = calculated in either of two ways: if `views` is present, then CPM is `(sponsorship.price / sponsorship.views) × 1000`, meaning realized cost per thousand actual views, computed post-publication. If `views` is null, Compute from the sponsorship's `price` and the channel's `projected_views` fields.
|
|
108
108
|
- Where possible, calculate the correct CPM in a SQL expression.
|
|
109
109
|
- **Sponsorship dates** — each sponsorship has four distinct dates, useful for different queries:
|
{thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl/references/business-glossary.md
RENAMED
|
@@ -56,7 +56,7 @@ Maps business terms to database concepts.
|
|
|
56
56
|
- A channel can have **multiple ad spots** because different people sell the same channel (talent manager, direct, multiple agencies)
|
|
57
57
|
- Ad spots are the **catalogue** — adlinks are **instances** of catalogue items
|
|
58
58
|
- Price/cost on adspot = list/catalog values; price/cost on adlink = actual deal values
|
|
59
|
-
- **Only one
|
|
59
|
+
- **Only one preferred adspot with integration=mention per channel at any time** (MSN rule)
|
|
60
60
|
|
|
61
61
|
## Ownership & Accountability
|
|
62
62
|
|
|
@@ -79,7 +79,7 @@ Maps business terms to database concepts.
|
|
|
79
79
|
- Key data: **who is the contact** to buy the ad from
|
|
80
80
|
- `thoughtleaders_channel.media_selling_network_join_date` = when channel joined MSN
|
|
81
81
|
- `thoughtleaders_channel.is_tpp` = TPP channel — TL's closest partners, a strict subset of MSN. **Prefer when booking**: fastest response, easiest to close. Not a gating flag; not a process barrier.
|
|
82
|
-
- **Rule:** Only one
|
|
82
|
+
- **Rule:** Only one preferred adspot with `integration=mention` per channel at any time
|
|
83
83
|
- MSN quality depends on having current, accurate contact info
|
|
84
84
|
|
|
85
85
|
## Channels & Audience
|
{thoughtleaders_cli-0.9.22 → thoughtleaders_cli-0.9.24}/skills/tl/references/postgres-schema.md
RENAMED
|
@@ -165,8 +165,9 @@ A channel can have multiple adspots (different sellers: talent manager, direct,
|
|
|
165
165
|
| `channel_id` | int FK | → `thoughtleaders_channel.id` |
|
|
166
166
|
| `price` | numeric | List/catalogue price |
|
|
167
167
|
| `cost` | numeric | List/catalogue cost |
|
|
168
|
-
| `integration` | int | 1=YouTube Mentions (live reads). Only one
|
|
169
|
-
| `
|
|
168
|
+
| `integration` | int | 1=YouTube Mentions (live reads). Only one preferred mention-type adspot per channel. |
|
|
169
|
+
| `is_preferred` | boolean | The channel's preferred spot, the one new proposals are booked on |
|
|
170
|
+
| `is_active` | boolean | False = deactivated, unusable, kept only for its AdLinks. Client sandboxes never show deactivated spots or their AdLinks |
|
|
170
171
|
| `publisher_id` | int FK | → `auth_user.id` (NOT `thoughtleaders_profile.id` — see gotcha below) |
|
|
171
172
|
|
|
172
173
|
### Key columns for the `thoughtleaders_channel` table
|
|
@@ -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:
|