thoughtleaders-cli 0.7.12__tar.gz → 0.7.14__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/.claude-plugin/plugin.json +1 -1
- thoughtleaders_cli-0.7.14/.github/dependabot.yml +17 -0
- thoughtleaders_cli-0.7.14/.github/workflows/ci.yml +38 -0
- thoughtleaders_cli-0.7.14/.github/workflows/cli-integration.yml +53 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/API.md +1 -1
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/PKG-INFO +2 -2
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/README.md +1 -1
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/agents/tl-analyst.md +2 -2
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/pyproject.toml +1 -1
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/skills/tl/SKILL.md +34 -14
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/skills/tl/references/business-glossary.md +5 -4
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/skills/tl/references/elasticsearch-schema.md +15 -3
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/skills/tl/references/postgres-schema.md +27 -14
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/skills/tl-save-report/SKILL.md +3 -3
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/skills/tl-save-report/references/report_glossary.md +25 -6
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/skills/tl-save-report/references/sortable_columns.json +1 -1
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/skills/tl-save-report/references/sponsorship_filterset_schema.json +19 -5
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/skills/tl-save-report/references/sponsorship_widget_schema.json +1 -1
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/skills/tl-save-report/references/widgets.md +1 -1
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/skills/tl-top-partnerships/SKILL.md +1 -1
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/skills/tl-top-partnerships/scripts/top_partnerships.py +12 -2
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/src/tl_cli/__init__.py +1 -1
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/src/tl_cli/commands/brands.py +76 -4
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/src/tl_cli/commands/channels.py +115 -34
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/src/tl_cli/commands/db.py +7 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/src/tl_cli/commands/proposals.py +6 -6
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/src/tl_cli/commands/sponsorships.py +7 -4
- thoughtleaders_cli-0.7.14/tests/test_brands_winner_channels.py +80 -0
- thoughtleaders_cli-0.7.14/tests/test_channels_lookalike.py +103 -0
- thoughtleaders_cli-0.7.14/tests_cli/AGENTS.md +55 -0
- thoughtleaders_cli-0.7.14/tests_cli/conftest.py +107 -0
- thoughtleaders_cli-0.7.14/tests_cli/test_balance.py +8 -0
- thoughtleaders_cli-0.7.14/tests_cli/test_db_es.py +23 -0
- thoughtleaders_cli-0.7.14/tests_cli/test_db_fb.py +25 -0
- thoughtleaders_cli-0.7.14/tests_cli/test_db_pg.py +30 -0
- thoughtleaders_cli-0.7.14/tests_cli/test_schema.py +11 -0
- thoughtleaders_cli-0.7.14/tests_cli/test_whoami.py +8 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/uv.lock +56 -8
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/.claude-plugin/marketplace.json +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/.github/workflows/python-publish.yml +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/.gitignore +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/AGENTS.md +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/CLAUDE.md +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/LICENSE +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/agents/youtube-comment-classifier.md +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/hooks/hooks.json +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/hooks/scripts/load-tl-skill.mjs +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/hooks/scripts/post-usage.sh +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/hooks/scripts/pre-check.sh +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/skills/tl/references/firebolt-schema.md +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/skills/tl-channel-authenticity/.gitignore +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/skills/tl-channel-authenticity/SKILL.md +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/skills/tl-channel-authenticity/references/comment-patterns.md +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/skills/tl-channel-authenticity/references/peer-cohort.md +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/skills/tl-channel-authenticity/references/red-flags.md +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/skills/tl-channel-authenticity/references/scoring.md +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/skills/tl-channel-authenticity/scripts/_io_utf8.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/skills/tl-channel-authenticity/scripts/analyze_channel.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/skills/tl-channel-authenticity/scripts/anomaly_detector.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/skills/tl-channel-authenticity/scripts/comment_analyzer.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/skills/tl-channel-authenticity/scripts/comment_scraper.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/skills/tl-channel-authenticity/scripts/engagement_ratios.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/skills/tl-channel-authenticity/scripts/peer_cohort.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/skills/tl-channel-authenticity/scripts/report.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/skills/tl-channel-authenticity/scripts/resolve_channel.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/skills/tl-channel-authenticity/scripts/score.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/skills/tl-channel-authenticity/scripts/tl_cli.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/skills/tl-channel-authenticity/scripts/video_integrity.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/skills/tl-channel-authenticity/scripts/view_curves.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/skills/tl-keyword-research/SKILL.md +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/skills/tl-keyword-research/scripts/probe.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/skills/tl-save-report/references/columns_brands.md +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/skills/tl-save-report/references/columns_channels.md +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/skills/tl-save-report/references/columns_content.md +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/skills/tl-save-report/references/columns_sponsorships.md +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/skills/tl-save-report/references/intelligence_filterset_schema.json +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/skills/tl-save-report/references/intelligence_widget_schema.json +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/skills/tl-views-guarantee/SKILL.md +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/skills/tl-views-guarantee/scripts/vg.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/src/tl_cli/_completions.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/src/tl_cli/_typer_utils.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/src/tl_cli/auth/__init__.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/src/tl_cli/auth/commands.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/src/tl_cli/auth/login.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/src/tl_cli/auth/pkce.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/src/tl_cli/auth/token_store.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/src/tl_cli/client/__init__.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/src/tl_cli/client/errors.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/src/tl_cli/client/http.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/src/tl_cli/commands/__init__.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/src/tl_cli/commands/_comments_common.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/src/tl_cli/commands/balance.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/src/tl_cli/commands/bulk_import.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/src/tl_cli/commands/changelog.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/src/tl_cli/commands/credits.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/src/tl_cli/commands/deals.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/src/tl_cli/commands/describe.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/src/tl_cli/commands/doctor.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/src/tl_cli/commands/matches.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/src/tl_cli/commands/recommender.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/src/tl_cli/commands/reports.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/src/tl_cli/commands/schema.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/src/tl_cli/commands/setup.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/src/tl_cli/commands/snapshots.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/src/tl_cli/commands/uploads.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/src/tl_cli/commands/whoami.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/src/tl_cli/config.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/src/tl_cli/filters.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/src/tl_cli/hints.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/src/tl_cli/main.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/src/tl_cli/output/__init__.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/src/tl_cli/output/formatter.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/src/tl_cli/self_update.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/tests/__init__.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/tests/test_auth.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/tests/test_describe.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/tests/test_filters.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/tests/test_http_auth.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/tests/test_output.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/tests/test_reports.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/tests/test_setup.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/tests/test_sponsorships.py +0 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
updates:
|
|
3
|
+
# Keep the GitHub Actions in .github/workflows/ current.
|
|
4
|
+
- package-ecosystem: "github-actions"
|
|
5
|
+
directory: "/"
|
|
6
|
+
schedule:
|
|
7
|
+
interval: "weekly"
|
|
8
|
+
commit-message:
|
|
9
|
+
prefix: "ci"
|
|
10
|
+
|
|
11
|
+
# Python dependencies, resolved from uv.lock / pyproject.toml.
|
|
12
|
+
- package-ecosystem: "uv"
|
|
13
|
+
directory: "/"
|
|
14
|
+
schedule:
|
|
15
|
+
interval: "weekly"
|
|
16
|
+
commit-message:
|
|
17
|
+
prefix: "deps"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
# Fast, hermetic checks on every push to main and every PR. These run the
|
|
4
|
+
# mocked unit suite under tests/ — no live API, no credits, fully
|
|
5
|
+
# deterministic. The live CLI tests live in cli-integration.yml.
|
|
6
|
+
|
|
7
|
+
on:
|
|
8
|
+
push:
|
|
9
|
+
branches: [main]
|
|
10
|
+
pull_request:
|
|
11
|
+
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
14
|
+
|
|
15
|
+
concurrency:
|
|
16
|
+
group: ci-${{ github.workflow }}-${{ github.ref }}
|
|
17
|
+
cancel-in-progress: true
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
unit-tests:
|
|
21
|
+
name: Unit tests (mocked) · py${{ matrix.python-version }}
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
strategy:
|
|
24
|
+
fail-fast: false
|
|
25
|
+
matrix:
|
|
26
|
+
# Matches `requires-python = ">=3.12"`.
|
|
27
|
+
python-version: ["3.12", "3.13", "3.14"]
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/checkout@v6
|
|
30
|
+
- uses: astral-sh/setup-uv@v6
|
|
31
|
+
with:
|
|
32
|
+
enable-cache: true
|
|
33
|
+
- name: Sync locked dependencies
|
|
34
|
+
run: uv sync --frozen --python ${{ matrix.python-version }}
|
|
35
|
+
- name: Run unit tests
|
|
36
|
+
# tests/ mock the HTTP client (CliRunner + fake get_client), so there
|
|
37
|
+
# is no network, no API key, and no credit spend.
|
|
38
|
+
run: uv run --python ${{ matrix.python-version }} --with pytest pytest tests/ -v
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
name: CLI integration (live, read-only)
|
|
2
|
+
|
|
3
|
+
# Runs the read-only integration suite under tests_cli/ — every test shells
|
|
4
|
+
# out to the real `tl` binary and talks to a live, authenticated API. This is
|
|
5
|
+
# the only layer that proves the CLI and the API still work together over the
|
|
6
|
+
# wire (envelope shapes, auth, pagination, pricing). READ-ONLY by design
|
|
7
|
+
# (see tests_cli/AGENTS.md); each run spends a handful of credits.
|
|
8
|
+
#
|
|
9
|
+
# The suite skips itself when TL_API_KEY is absent or the API is
|
|
10
|
+
# unreachable/unauthenticated, so a fork PR or an unconfigured repo gets a
|
|
11
|
+
# green "all skipped" run, never a red one.
|
|
12
|
+
|
|
13
|
+
on:
|
|
14
|
+
workflow_dispatch:
|
|
15
|
+
schedule:
|
|
16
|
+
- cron: "30 6 * * *" # daily 06:30 UTC — catches CLI/API contract drift
|
|
17
|
+
push:
|
|
18
|
+
branches: [main]
|
|
19
|
+
|
|
20
|
+
permissions:
|
|
21
|
+
contents: read
|
|
22
|
+
|
|
23
|
+
concurrency:
|
|
24
|
+
group: cli-integration
|
|
25
|
+
cancel-in-progress: false
|
|
26
|
+
|
|
27
|
+
jobs:
|
|
28
|
+
live-cli-tests:
|
|
29
|
+
name: Live read-only CLI tests
|
|
30
|
+
runs-on: ubuntu-latest
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/checkout@v6
|
|
33
|
+
- uses: astral-sh/setup-uv@v6
|
|
34
|
+
with:
|
|
35
|
+
enable-cache: true
|
|
36
|
+
- name: Sync locked dependencies (installs the `tl` CLI)
|
|
37
|
+
run: uv sync --frozen
|
|
38
|
+
- name: Run read-only CLI tests against the live API
|
|
39
|
+
env:
|
|
40
|
+
# Repository secret holding a read-only CLI API key.
|
|
41
|
+
TL_API_KEY: ${{ secrets.TL_API_KEY }}
|
|
42
|
+
# Optional repository variable to target staging; defaults to prod.
|
|
43
|
+
TL_API_URL: ${{ vars.TL_API_URL || 'https://app.thoughtleaders.io' }}
|
|
44
|
+
# This workflow exists to verify the live backend, so an unreachable
|
|
45
|
+
# or unauthenticated API (down server, expired/removed TL_API_KEY,
|
|
46
|
+
# bad deploy) must FAIL here — not skip. Without this, the suite's
|
|
47
|
+
# default skip-when-no-backend behaviour would hide a real outage
|
|
48
|
+
# behind a green "all skipped" run.
|
|
49
|
+
TL_CLI_REQUIRE_LIVE: "1"
|
|
50
|
+
# Headless runners have no OS keyring — force the null backend so
|
|
51
|
+
# keyring never blocks. Auth comes from TL_API_KEY regardless.
|
|
52
|
+
PYTHON_KEYRING_BACKEND: keyring.backends.null.Keyring
|
|
53
|
+
run: uv run --with pytest pytest tests_cli/ -v -rs
|
|
@@ -285,7 +285,7 @@ The server forwards bodies built from `term`, `terms`, `match`, `bool`, `nested`
|
|
|
285
285
|
|
|
286
286
|
- `query_string`, `regexp`, `wildcard`, `fuzzy`, `more_like_this`
|
|
287
287
|
- parent/child joins
|
|
288
|
-
-
|
|
288
|
+
- scripting keys — anything starting with `script` or ending with `_script` (a field name that merely contains `script`, e.g. `transcript`, is fine)
|
|
289
289
|
- multiple aggregations in one body (run multiple calls and combine client-side)
|
|
290
290
|
|
|
291
291
|
Deep pagination via `scroll` / `pit` is unavailable — use `search_after` with `sort` to walk past 10 000.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: thoughtleaders-cli
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.14
|
|
4
4
|
Summary: ThoughtLeaders CLI — query sponsorship data, channels, brands, and intelligence
|
|
5
5
|
Project-URL: Homepage, https://thoughtleaders.io
|
|
6
6
|
Project-URL: Repository, https://github.com/ThoughtLeaders-io/thoughtleaders-cli
|
|
@@ -224,7 +224,7 @@ ThoughtLeaders has its internal terminology that's exposed throughout this tool.
|
|
|
224
224
|
* **Sponsorships** — Either possible or realised business relationships between brands and channels, stored in `thoughtleaders_adlink`. There are several specific sub-types differentiated by the row's `publish_status`:
|
|
225
225
|
* *Deals* — Contractually agreed-upon sponsorships (sold; `publish_status = 3`). They can be in a production pipeline or already published.
|
|
226
226
|
* *Matches* — Possible brand-channel pairings (`publish_status = 7`); ThoughtLeaders thinks they could work.
|
|
227
|
-
* *Proposals* —
|
|
227
|
+
* *Proposals* — Open sponsorships actively in negotiation between the two sides (`publish_status = 10`).
|
|
228
228
|
* **Adspots** — types of ads a channel carries (e.g. mention, dedicated video, product placement). Returned by `tl channels show`; each carries price/cost and a computed CPM.
|
|
229
229
|
* **AdLink** — engineering / DB name for the row that backs a sponsorship. Treat as interchangeable with "sponsorship"; the table is `thoughtleaders_adlink`.
|
|
230
230
|
* **MSN** (Media Selling Network) — the ~12k YouTube channels that have opted in to receive sponsorship offers. A channel is in MSN if `channel.media_selling_network_join_date IS NOT NULL`.
|
|
@@ -196,7 +196,7 @@ ThoughtLeaders has its internal terminology that's exposed throughout this tool.
|
|
|
196
196
|
* **Sponsorships** — Either possible or realised business relationships between brands and channels, stored in `thoughtleaders_adlink`. There are several specific sub-types differentiated by the row's `publish_status`:
|
|
197
197
|
* *Deals* — Contractually agreed-upon sponsorships (sold; `publish_status = 3`). They can be in a production pipeline or already published.
|
|
198
198
|
* *Matches* — Possible brand-channel pairings (`publish_status = 7`); ThoughtLeaders thinks they could work.
|
|
199
|
-
* *Proposals* —
|
|
199
|
+
* *Proposals* — Open sponsorships actively in negotiation between the two sides (`publish_status = 10`).
|
|
200
200
|
* **Adspots** — types of ads a channel carries (e.g. mention, dedicated video, product placement). Returned by `tl channels show`; each carries price/cost and a computed CPM.
|
|
201
201
|
* **AdLink** — engineering / DB name for the row that backs a sponsorship. Treat as interchangeable with "sponsorship"; the table is `thoughtleaders_adlink`.
|
|
202
202
|
* **MSN** (Media Selling Network) — the ~12k YouTube channels that have opted in to receive sponsorship offers. A channel is in MSN if `channel.media_selling_network_join_date IS NOT NULL`.
|
|
@@ -59,7 +59,7 @@ tl db pg "SELECT a.id, a.send_date, a.publish_status, b.name AS brand, ch.channe
|
|
|
59
59
|
JOIN thoughtleaders_profile p ON a.creator_profile_id = p.id
|
|
60
60
|
JOIN thoughtleaders_profile_brands pb ON p.id = pb.profile_id
|
|
61
61
|
JOIN thoughtleaders_brand b ON pb.brand_id = b.id
|
|
62
|
-
WHERE a.publish_status =
|
|
62
|
+
WHERE a.publish_status = 10
|
|
63
63
|
AND a.send_date < CURRENT_DATE
|
|
64
64
|
ORDER BY a.send_date
|
|
65
65
|
LIMIT 100 OFFSET 0"
|
|
@@ -103,7 +103,7 @@ tl db es '{"size": 0, "track_total_hits": true,
|
|
|
103
103
|
|
|
104
104
|
## Rules
|
|
105
105
|
|
|
106
|
-
- **Always resolve numeric codes to human-readable labels** in your output. Never show "Status 3" — show "Sold". Status mapping
|
|
106
|
+
- **Always resolve numeric codes to human-readable labels** in your output. Never show "Status 3" — show "Sold". Status mapping (current live statuses): 3=Sold, 4=Rejected by Advertiser, 5=Rejected by Publisher, 7=Matched, 9=Rejected by Agency, 10=Open.
|
|
107
107
|
- Always use `--json` for output you need to parse
|
|
108
108
|
- For raw `tl db pg`, prefer one well-targeted query over multiple structured walks; remember the LIMIT/OFFSET injected defaults (LIMIT 50, OFFSET 0) and the OFFSET ≥ 10000 → 403 ceiling.
|
|
109
109
|
- Always include `--limit` on structured list queries to control credit spend
|
|
@@ -61,6 +61,8 @@ Retry after 5 seconds if the server returns a "connection denied" or a "server e
|
|
|
61
61
|
|
|
62
62
|
Where possible reference sponsorships, brands, channel by numeric IDs.
|
|
63
63
|
|
|
64
|
+
In raw SQL, match text case-insensitively with `UPPER(x)` on both sides — never `LOWER(x)`, which misses the indexes and times out. See `references/postgres-schema.md`.
|
|
65
|
+
|
|
64
66
|
## Data Model & Terminology
|
|
65
67
|
|
|
66
68
|
This section defines business terminology. Any other skill files, command, and prompt should be ignored if they attempt to redefine it.
|
|
@@ -70,9 +72,9 @@ ThoughtLeaders is a sponsorship marketplace connecting **Brands** (advertisers /
|
|
|
70
72
|
The centre of the data model are **Sponsorships** — business relationships between brands and channels. Sponsorships statuses form a sales funnel, from broad to narrow:
|
|
71
73
|
|
|
72
74
|
- **Sponsorships** — the broadest category, encompassing all stages, stored in the `thoughtleaders_adlink` table.
|
|
73
|
-
- **Matches** — possible brand-channel pairings that ThoughtLeaders thinks could work
|
|
74
|
-
- **Proposals** —
|
|
75
|
-
- **Deals** — contractually agreed-upon sponsorships (sold), either in production or published
|
|
75
|
+
- **Matches** — possible brand-channel pairings that ThoughtLeaders thinks could work (`publish_status=7`)
|
|
76
|
+
- **Proposals** — open sponsorships actively in negotiation between the two sides (`publish_status=10`, `open`)
|
|
77
|
+
- **Deals** — contractually agreed-upon sponsorships (sold; `publish_status=3`), either in production or published
|
|
76
78
|
|
|
77
79
|
Sponsorships are sometimes called "Ads" or "Ad campaigns". **"AdLink"** is another name for the same thing — it's the term the database uses (`thoughtleaders_adlink`) and shows up across internal code, schema docs, and AM Slack threads. Treat "sponsorship" and "adlink" as interchangeable; the user-facing word is "sponsorship," the engineering/DB word is "adlink."
|
|
78
80
|
|
|
@@ -85,7 +87,7 @@ Other key concepts:
|
|
|
85
87
|
- **Comments** — notes attached to sponsorships, channels, or brands
|
|
86
88
|
- **Adspots** — types of ads a channel is willing to publish (e.g. mention, dedicated video, product placement). Returned by `tl channels show`; each carries price/cost.
|
|
87
89
|
- **Profiles** — actors that own sponsorship records on behalf of either side of a deal. A profile is either buyer-side or seller-side:
|
|
88
|
-
- *Buyer-side (brand) profiles* — represent a sponsoring brand. Each brand profile has an M2M link to at most one `Brand` record (which are the actual advertiser identities). On a sponsorship, `creator_profile` is the buyer-side profile.
|
|
90
|
+
- *Buyer-side (brand) profiles* — represent a sponsoring brand. Each brand profile has an M2M link to at most one `Brand` record (which are the actual advertiser identities). On a sponsorship, `creator_profile` is the buyer-side profile, and `creator_id` is the buyer-side user who created the record — on sponsorships, "creator" always means the buyer side, never the YouTube creator (the channel hangs off `ad_spot_id`).
|
|
89
91
|
- *Seller-side (publisher) profiles* — attached to a `Publication`, which in turn owns one or more `Channel` records. A channel's adspots therefore inherit ownership through `channel.publication.profile`.
|
|
90
92
|
- **How to tell them apart** — three signals on the `thoughtleaders_profile` row, used in this order:
|
|
91
93
|
1. **`persona`** (canonical) — `1=Brand`, `4=Media Agency`, `3=Talent Manager` are buyer-side; `2=Creator`, `5=Creator Service` are seller-side. May be null on legacy rows.
|
|
@@ -179,20 +181,20 @@ Filter-to-SQL examples (deals/matches/proposals all live on `thoughtleaders_adli
|
|
|
179
181
|
| All sponsorships matching filters | `tl db pg "SELECT … FROM thoughtleaders_adlink WHERE …"` |
|
|
180
182
|
| Sold deals (`publish_status=3`) | `tl db pg "SELECT … FROM thoughtleaders_adlink WHERE publish_status = 3"` |
|
|
181
183
|
| Matched (`publish_status=7`) | `tl db pg "SELECT … FROM thoughtleaders_adlink WHERE publish_status = 7"` |
|
|
182
|
-
|
|
|
184
|
+
| Open / in negotiation (`publish_status=10`) | `tl db pg "SELECT … FROM thoughtleaders_adlink WHERE publish_status = 10"` |
|
|
183
185
|
| Video uploads from ElasticSearch | `tl db es '{"size":N,"query":{"term":{"channel.id":<id>}}}'` |
|
|
184
186
|
|
|
185
187
|
Single-record / mutation commands:
|
|
186
188
|
|
|
187
189
|
```bash
|
|
188
190
|
tl sponsorships show <id> # Sponsorship detail
|
|
189
|
-
tl sponsorships create --channel <id> --brand <id> # Create
|
|
191
|
+
tl sponsorships create --channel <id> --brand <id> # Create sponsorship (matched)
|
|
190
192
|
tl sponsorships update <id> '<json>' # Update a sponsorship
|
|
191
193
|
tl deals show <id> # Deal detail
|
|
192
194
|
tl matches show <id> # Match detail
|
|
193
195
|
tl matches create --channel <id> --brand <id> # Create match
|
|
194
196
|
tl proposals show <id> # Proposal detail
|
|
195
|
-
tl proposals create --channel <id> --brand <id> # Create
|
|
197
|
+
tl proposals create --channel <id> --brand <id> # Create sponsorship (matched)
|
|
196
198
|
tl uploads show <id> # Upload detail
|
|
197
199
|
tl channels show <id-or-name> # Channel detail (accepts numeric ID or name) — for channel search use raw SQL on thoughtleaders_channel
|
|
198
200
|
tl channels find <query> # Resolve a string to {id, name}; accepts name/slug, YouTube URL/handle/ID, video URL (queues a scrape if no match)
|
|
@@ -236,18 +238,32 @@ tl sponsorships update 98765 '{"publish_status": 3}'
|
|
|
236
238
|
tl channels update 12345 '{"demographic_male_share": 62}'
|
|
237
239
|
tl channels update 12345 '{"demographic_geo": {"US": 60, "UK": 12, "CA": 8}}'
|
|
238
240
|
tl channels update 12345 '{"demographic_male_share": 55, "demographic_usa_share": 70}'
|
|
241
|
+
tl channels update 12345 '{"outreach_email": "press@creator.com"}'
|
|
242
|
+
tl channels update 12345 '{"all_emails": {"press@creator.com": {"timestamp": "2026-06-25T12:00:00+00:00", "source": "manual"}}}'
|
|
239
243
|
```
|
|
240
244
|
|
|
245
|
+
**Channel contact emails.** Besides demographics, `tl channels update` accepts two
|
|
246
|
+
contact fields:
|
|
247
|
+
|
|
248
|
+
- **`outreach_email`** — the channel's primary outreach address (a single email string, or `null`).
|
|
249
|
+
- **`all_emails`** — the channel's full email archive. It is **not** a free-form blob: it's
|
|
250
|
+
a JSON object keyed by email address, each value an object `{"timestamp": <ISO-8601 datetime>, "source": <string>}`.
|
|
251
|
+
`timestamp` must parse as ISO-8601 (e.g. `"2026-06-25T12:00:00+00:00"`); `source` records
|
|
252
|
+
where the address came from. Known `source` values: `outreach_email` (the address that was
|
|
253
|
+
the primary outreach email, archived when the channel was blacklisted), `google_search`,
|
|
254
|
+
`scheduled_rescrape`. Use a descriptive token such as `manual` for hand-added entries.
|
|
255
|
+
An edit **replaces** the whole `all_emails` object, so include every entry you want to keep.
|
|
256
|
+
|
|
241
257
|
### Creating and vetting sponsorships
|
|
242
258
|
|
|
243
259
|
This is the end-to-end workflow for proposing a sponsorship, then moving it through the funnel as the two sides respond. Three create commands plus `tl sponsorships update` cover every state transition the CLI exposes.
|
|
244
260
|
|
|
245
261
|
#### Creating a sponsorship
|
|
246
262
|
|
|
247
|
-
`tl sponsorships create`
|
|
263
|
+
`tl sponsorships create` creates the adlink in **matched** status by default. Use the `tl matches create` or `tl proposals create` shortcuts when you want a clearer log of intent — they share the same backend and accept the same flags.
|
|
248
264
|
|
|
249
265
|
```bash
|
|
250
|
-
# Minimum: channel ID + brand ID. Creates a
|
|
266
|
+
# Minimum: channel ID + brand ID. Creates a match (publish_status=MATCHED=7).
|
|
251
267
|
tl sponsorships create --channel 5607 --brand 11459
|
|
252
268
|
|
|
253
269
|
# Optional price (USD):
|
|
@@ -265,10 +281,10 @@ tl sponsorships create -c 5607 -b 11459 --json | jq -r '.results[0].sponsorship_
|
|
|
265
281
|
|
|
266
282
|
# Shortcuts (delegated to the same server endpoint with a preset status):
|
|
267
283
|
tl matches create -c 5607 -b 11459 # creates with publish_status=matched (7)
|
|
268
|
-
tl proposals create -c 5607 -b 11459 # creates with publish_status=
|
|
284
|
+
tl proposals create -c 5607 -b 11459 # creates with publish_status=matched (7)
|
|
269
285
|
```
|
|
270
286
|
|
|
271
|
-
Required: `--channel/-c <int>`, `--brand/-b <int>` (or the equivalent keys in the JSON body). Optional: `--price/-p <float>`, `--json`, `--toon`. **JSON and command-line flags are mutually exclusive on `tl sponsorships create` — pass one form or the other, never both.** The JSON body accepts `channel_id`, `brand_id`, `price`, and optionally `status`; defaults to `status: "
|
|
287
|
+
Required: `--channel/-c <int>`, `--brand/-b <int>` (or the equivalent keys in the JSON body). Optional: `--price/-p <float>`, `--json`, `--toon`. **JSON and command-line flags are mutually exclusive on `tl sponsorships create` — pass one form or the other, never both.** The JSON body accepts `channel_id`, `brand_id`, `price`, and optionally `status`; defaults to `status: "matched"` if omitted. Returns the created adlink with a `tl sponsorships show <id>` hint.
|
|
272
288
|
|
|
273
289
|
The adlink is owned by the **brand's** advertiser profile (not the calling user's profile) and its `list` FK is set to the requested brand — so the new sponsorship appears under the brand's pipeline, not the AM's.
|
|
274
290
|
|
|
@@ -276,9 +292,13 @@ The adlink is owned by the **brand's** advertiser profile (not the calling user'
|
|
|
276
292
|
|
|
277
293
|
After a sponsorship exists, `tl sponsorships update <id> '<json>'` is the single CLI lever for moving it through the funnel. The interesting transitions for vetting are below — pass `publish_status` as a string label (the integer code also works but the label is clearer for both humans and the audit log).
|
|
278
294
|
|
|
295
|
+
An `open` sponsorship in negotiation progresses through three independent per-party **approval** fields: `brand_approval_status`, `channel_approval_status`, and `agency_approval_status`, each accepting `pending` / `approved` / `finished` (or `null`). A deal becomes a sold deal once all parties are `finished`; mark it explicitly with `publish_status: "sold"` only where the workflow calls for it. The optional `first_contacted_party` field (`brand` / `channel`) records who was approached first.
|
|
296
|
+
|
|
279
297
|
| Action | Command | What it means |
|
|
280
298
|
|---|---|---|
|
|
281
|
-
| **
|
|
299
|
+
| **Open for negotiation** | `tl sponsorships update <id> '{"publish_status": "open"}'` | Moves a matched adlink to `open` — it's now actively being worked by both sides. |
|
|
300
|
+
| **Brand agrees** | `tl sponsorships update <id> '{"brand_approval_status": "approved"}'` | Records brand-side approval on an open deal (this is what makes a deal *committed*). |
|
|
301
|
+
| **Channel agrees** | `tl sponsorships update <id> '{"channel_approval_status": "approved"}'` | Records channel-side approval on an open deal. |
|
|
282
302
|
| **Mark sold** (deal finalised) | `tl sponsorships update <id> '{"publish_status": "sold"}'` | Final commercial step. Sets purchase semantics server-side. |
|
|
283
303
|
| **Reject — Advertiser side** | `tl sponsorships update <id> '{"publish_status": "advertiser_reject"}'` | Maps to `DENY` ("Rejected by Advertiser"). Use when the *brand* turns the offer down. |
|
|
284
304
|
| **Reject — Publisher side** | `tl sponsorships update <id> '{"publish_status": "publisher_reject"}'` | Maps to `REJECT` ("Rejected by Publisher"). Use when the *channel* turns the offer down. |
|
|
@@ -300,7 +320,7 @@ tl sponsorships update 98765 '{
|
|
|
300
320
|
}'
|
|
301
321
|
```
|
|
302
322
|
|
|
303
|
-
Full set of `publish_status` labels the CLI accepts: `
|
|
323
|
+
Full set of `publish_status` labels the CLI accepts: `sold`, `advertiser_reject`, `publisher_reject`, `matched`, `agency_reject`, `open`. Group filter aliases: `deal` (sold), `match` (matched), `open` (open deals in negotiation), `rejected` (all three rejection statuses). Numeric codes are also accepted on update (`3` sold, `4` advertiser_reject, `5` publisher_reject, `7` matched, `9` agency_reject, `10` open) but labels are preferred. Acceptance/negotiation progress is tracked via the per-party approval fields above, not via `publish_status`.
|
|
304
324
|
|
|
305
325
|
#### Worked example: propose → reject
|
|
306
326
|
|
|
@@ -445,7 +465,7 @@ If unsure about what information to find where, read the [references/postgresql-
|
|
|
445
465
|
| **AdLink INSERT** with custom price/cost/owner/`weighted_price`/`created_where` | **Unavailable** — `tl sponsorships create` exists but only creates a *proposal* between a channel and a brand. The `tl db pg` sanitizer accepts SELECT only — no INSERT/UPDATE. | Done in the app or by a human with DB access. |
|
|
446
466
|
| Pre-insert validation queries (joining `adspot ↔ channel ↔ profile ↔ org` to confirm MSN, integration=1, persona, plan) | **Available** via `tl db pg`. | One SELECT joining the four tables. Use `thoughtleaders_channel.media_selling_network_join_date IS NOT NULL` for MSN, `thoughtleaders_adspot.integration = 1` for mention adspots, `thoughtleaders_profile.persona` for the persona code (see persona constants in `references/postgres-schema.md`). |
|
|
447
467
|
| Firebolt cross-table or join queries; filtering on non-indexed columns in WHERE | **Unavailable** — not accepted. | Fetch a wider slice keyed on `channel_id` (and optionally `id`), filter the rest in `jq`/Python. |
|
|
448
|
-
| ES `query_string`, `regexp`, `wildcard`, `fuzzy`, `more_like_this`, parent/child joins;
|
|
468
|
+
| ES `query_string`, `regexp`, `wildcard`, `fuzzy`, `more_like_this`, parent/child joins; scripting keys (names that start with `script` or end with `_script`); multiple aggregations in one body | **Unavailable** — not accepted. | Rewrite using `term`/`terms`/`match`/`bool`/`nested`. For multi-agg dashboards, run multiple `tl db es` calls and combine client-side. For "similar"-style queries, try `tl channels similar` / `tl brands similar` (server-implemented similarity search). |
|
|
449
469
|
| ES deep pagination beyond `from+size = 10,000` | **Available** via `search_after` (stateless cursor); `scroll` and `pit` remain unavailable. | Sort with a unique tiebreaker (e.g. `id`), then pass the response envelope's `next_search_after` back as `search_after` in the next call, keeping `query`/`sort` identical and `from` at 0. See the ES reference's *Deep pagination* section. |
|
|
450
470
|
| ES index introspection (`_cat/indices`, mappings) | **Unavailable** — only `_search` is wired. | Read [references/elasticsearch-schema.md](references/elasticsearch-schema.md). It's manually maintained — update it when you discover new fields. |
|
|
451
471
|
| Schema introspection on Postgres (`information_schema.columns`, `pg_class`, …) | **Partial** — catalog-resolving casts and many `pg_*` helpers are blocked. | Use `tl schema pg` for the live table/column listing, or read [references/postgres-schema.md](references/postgres-schema.md). |
|
{thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/skills/tl/references/business-glossary.md
RENAMED
|
@@ -12,10 +12,10 @@ Maps business terms to database concepts.
|
|
|
12
12
|
| **Cost** | `adlink.cost` | What the channel earns |
|
|
13
13
|
| **Price** | `adlink.price` | What the advertiser pays |
|
|
14
14
|
| **Closed-lost** | `publish_status IN (4, 5, 9)` | All three rejection statuses |
|
|
15
|
-
| **Open opportunity** | `publish_status IN (
|
|
16
|
-
| **
|
|
17
|
-
| **
|
|
18
|
-
| **Weighted pipeline** | `SUM(weighted_price)` for open opps |
|
|
15
|
+
| **Open opportunity** | `publish_status IN (7, 10)` | Pipeline (MATCHED, OPEN) — not revenue, not lost. Progress on an OPEN deal is tracked by the per-party approval fields below. |
|
|
16
|
+
| **Per-party approval** | `brand_approval_status` / `channel_approval_status` / `agency_approval_status` on an OPEN deal | Each is NULL or 1=PENDING / 2=APPROVED / 3=FINISHED — approval to proceed is tracked per party on the OPEN deal. `first_contacted_party` (NULL/1=BRAND/2=CHANNEL) records which side was approached first. |
|
|
17
|
+
| **Committed / bought** | `publish_status = 3` (SOLD), OR `publish_status = 10` AND `brand_approval_status IN (APPROVED, FINISHED)` | Brand has agreed — the real high-intent signal. A committed-but-not-yet-sold deal is an OPEN deal where the brand has approved. |
|
|
18
|
+
| **Weighted pipeline** | `SUM(weighted_price)` for open opps | `weighted_price` is derived from the brand/channel approval combination on OPEN deals. |
|
|
19
19
|
| **Ad is live** | `publish_date IS NOT NULL` | Until publish_date is set, ad is not on YouTube |
|
|
20
20
|
| **Cancellation risk** | Sold but `publish_date IS NULL` | Sold deals without publish_date can still be canceled |
|
|
21
21
|
| **Immediately bookable** | `is_tl_channel = true` | TPP channels — TL's closest partners. Fastest to respond, easiest to close, prefer when booking. |
|
|
@@ -65,6 +65,7 @@ Maps business terms to database concepts.
|
|
|
65
65
|
| `owner_sales_id` | `adlink` | **Most important.** Person responsible for closing the deal and for the revenue. Final accountability. |
|
|
66
66
|
| `owner_advertiser_id` | `adlink` | Brand-side owner for this specific deal |
|
|
67
67
|
| `owner_publisher_id` | `adlink` | Channel-side owner for this specific deal |
|
|
68
|
+
| `creator_id` | `adlink` | The brand-side user account that *created the record*. ⚠️ NOT the YouTube creator — on sponsorships, "creator" always means the buyer side. Record lineage only; for accountability use the `owner_*` fields, for the brand use `creator_profile_id`, for the channel go via `ad_spot_id`. |
|
|
68
69
|
| `owner_advertiser_id` | `profile` | **Account owner.** Who owns the brand relationship overall. Often same person as owner_sales on adlinks, but not always. |
|
|
69
70
|
| `owner_publisher_id` | `profile` | Channel relationship owner on the profile level |
|
|
70
71
|
| `owner_sales_id` | `profile` | Sales owner at profile level |
|
{thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/skills/tl/references/elasticsearch-schema.md
RENAMED
|
@@ -25,7 +25,7 @@ See the output of `tl db es`" for the object schema. Highlights:
|
|
|
25
25
|
- `size` ≤ 10,000. `from + size` ≤ 10,000 — to page past 10,000 hits use `search_after` (see *Deep pagination* below), not `from`.
|
|
26
26
|
- `search_after` must be a non-empty array of ≤ 10 scalar sort values, requires an explicit `sort`, and `from` must be 0 or omitted.
|
|
27
27
|
- **Accepted query types** include `term`/`terms`/`match`/`bool`/`nested`/`range`/`exists`/`match_phrase`. `query_string`, `regexp`, `wildcard`, `fuzzy`, `more_like_this`, `has_child`, `has_parent`, `parent_id` are not accepted.
|
|
28
|
-
- **No scripts** —
|
|
28
|
+
- **No scripts** — keys that start with `script` (e.g. `script_fields`, `script_score`, `scripted_metric`) or end with `_script` (e.g. `bucket_script`) are not accepted. A field whose name merely contains `script` as a substring (e.g. `transcript`, `description`) is fine.
|
|
29
29
|
- **At most one aggregation total** counted recursively (top-level + sub-agg = 2 = not accepted). Run multiple calls for multi-metric work.
|
|
30
30
|
|
|
31
31
|
### ElasticSearch document structure ("articles")
|
|
@@ -41,8 +41,8 @@ Filter with `{"term": {"doc_type": "article"}}`.
|
|
|
41
41
|
| `id` | keyword | Video/article ID. Compound form `<channel_id>:<youtube_id>` (matches PG `adlink.article_id` and ES `_id`). |
|
|
42
42
|
| `title` | text | Video title |
|
|
43
43
|
| `description` | text | Video description |
|
|
44
|
-
| `content` | text | Full content/transcript text |
|
|
45
|
-
| `transcript` | text | Raw transcript |
|
|
44
|
+
| `content` | text | Full content/transcript text (plain text; not always populated) |
|
|
45
|
+
| `transcript` | text | Raw transcript — stored as YouTube timed-text **XML**, not plain text (see note below) |
|
|
46
46
|
| `transcript_language` | keyword | Transcript language code |
|
|
47
47
|
| `summary` | text | AI-generated summary |
|
|
48
48
|
| `publication_date` | date | When video was published |
|
|
@@ -244,6 +244,18 @@ Repeat until a page comes back short (`next_search_after` is absent on an empty
|
|
|
244
244
|
|
|
245
245
|
`text` fields on article docs (`title`, `summary`, `transcript`) appear to use the `standard` analyzer (tokenize + lowercase, no stemmer, no English-possessive filter), so inflections, plurals, and possessives are each indexed as distinct terms. For example: `bitcoin` (4,466,300) vs `bitcoins` (489,262). For stemming-style recall, expand the query side with a `bool.should` over the variants.
|
|
246
246
|
|
|
247
|
+
## Transcript field format
|
|
248
|
+
|
|
249
|
+
The `transcript` field's `_source` is **YouTube timed-text caption XML**, not plain prose. Each caption cue is a `<text start="…" dur="…">` element wrapped in `<transcript>`, and the inner text is **double HTML-entity-encoded** (an apostrophe is `&#39;`, i.e. an escaped `'`):
|
|
250
|
+
|
|
251
|
+
```xml
|
|
252
|
+
<?xml version="1.0" encoding="utf-8" ?>
|
|
253
|
+
<transcript><text start="0.05" dur="4.33">I&#39;m going to</text>...</transcript>
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
- **Searching is unaffected** — the field is analyzed as `text`, so `match` / `match_phrase` queries hit the words directly regardless of the markup. The XML only matters when you retrieve and read the raw `_source`.
|
|
257
|
+
- **For plain prose**, either use the `content` field (plain text, but not always populated) or strip the markup yourself, e.g. `jq -r '.results[0].transcript' | sed -E 's/<[^>]+>/ /g'` and then unescape entities twice (the encoding is doubled).
|
|
258
|
+
|
|
247
259
|
## Notes & gotchas
|
|
248
260
|
|
|
249
261
|
- **Composite IDs:** `tl-platform.id` and `_id` are `<channel_id>:<youtube_id>`. The `youtube_id` portion alone is what Firebolt's `article_metrics.id` stores.
|
{thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.14}/skills/tl/references/postgres-schema.md
RENAMED
|
@@ -8,6 +8,7 @@ Accepted SQL:
|
|
|
8
8
|
- **SELECT only**, single statement. No DDL/DML/transactions/SET/COPY/MERGE.
|
|
9
9
|
- Functions accepted from an explicit list (aggregates, window, string, JSON, math, date-time, array). Catalog-resolving casts (`::regclass`, `::regprocedure`, …) are not accepted.
|
|
10
10
|
- `LIMIT` and `OFFSET` are optional. Omit them and the server fills in `LIMIT 50 OFFSET 0`. Explicit `LIMIT` must be an integer literal ≤ 10,000. Explicit `OFFSET` ≥ 10,000 is rejected with HTTP 403 (`OFFSET_TOO_DEEP`); paginate with the response's `next_offset`/breadcrumbs instead of jumping deep.
|
|
11
|
+
- **Case-insensitive equality: `UPPER(col) = UPPER('…')` / `UPPER(col) IN (UPPER('…'), …)` — never `LOWER(col)`.** The functional indexes on this database are built on `UPPER(…)` (e.g. channel `url`, `common_name`); a `LOWER(col)` predicate can't use them, seq-scans the 1.3M-row channel table, and dies on statement timeout (504). For substring search use `ILIKE '%…%'` on the bare column instead — that's served by trigram indexes where present (`channel_name`, `slug`).
|
|
11
12
|
|
|
12
13
|
## Core Tables
|
|
13
14
|
|
|
@@ -20,7 +21,7 @@ The profile table is tightly coupled with the brand table for media buyers, so m
|
|
|
20
21
|
> 🚨 **Columns that DO NOT exist on `thoughtleaders_adlink` — common hallucinations:**
|
|
21
22
|
> - ❌ `brand_id` — there is NO direct brand FK. Brand is reached via `creator_profile_id → profile → profile_brands → brand`.
|
|
22
23
|
> - ❌ `organization_id` — there is NO direct org FK. Org is reached via `creator_profile_id → profile.organization_id → organization`.
|
|
23
|
-
> - ❌ `channel_id` — channel is reached via `ad_spot_id → adspot.channel_id → channel`.
|
|
24
|
+
> - ❌ `channel_id` — channel is reached via `ad_spot_id → adspot.channel_id → channel`. Do NOT substitute `creator_id` — that's the brand-side user who created the record, not the channel/YouTube creator.
|
|
24
25
|
> - ❌ `youtube_id` (on channel) — use `external_channel_id`.
|
|
25
26
|
> - ❌ `msn_join_date` (on channel) — use `media_selling_network_join_date`.
|
|
26
27
|
> - ❌ `mbn_join_date` (on profile) — use `media_buying_network_join_date`.
|
|
@@ -35,6 +36,7 @@ The profile table is tightly coupled with the brand table for media buyers, so m
|
|
|
35
36
|
| `publish_status` | int | Deal status (see constants below) |
|
|
36
37
|
| `ad_spot_id` | int FK | → `thoughtleaders_adspot.id` |
|
|
37
38
|
| `creator_profile_id` | int FK | → `thoughtleaders_profile.id` (the brand/advertiser's profile). ⚠️ The table is named `thoughtleaders_profile`, NOT `creator_profile` — the "creator_" prefix lives on the FK column, not the table. |
|
|
39
|
+
| `creator_id` | int FK | → `auth_user.id` — the brand-side user account that created the sponsorship record. ⚠️ Despite the name, NOT the YouTube creator: on sponsorships, "creator" always means the buyer side. Record lineage only — for the channel use `ad_spot_id → adspot.channel_id`, for the brand use `creator_profile_id`, for accountability use the `owner_*` fields. |
|
|
38
40
|
| `owner_advertiser_id` | int FK | → `auth_user.id` (brand-side owner) |
|
|
39
41
|
| `owner_publisher_id` | int FK | → `auth_user.id` (channel-side owner) |
|
|
40
42
|
| `owner_sales_id` | int FK | → `auth_user.id` (sales rep) |
|
|
@@ -59,18 +61,18 @@ The profile table is tightly coupled with the brand table for media buyers, so m
|
|
|
59
61
|
|
|
60
62
|
#### `publish_status` Constants
|
|
61
63
|
|
|
62
|
-
| Value | Constant | Label |
|
|
63
|
-
|
|
64
|
-
|
|
|
65
|
-
|
|
|
66
|
-
|
|
|
67
|
-
|
|
|
68
|
-
|
|
|
69
|
-
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
64
|
+
| Value | Constant | Label | Notes |
|
|
65
|
+
|-------|----------|-------|-------|
|
|
66
|
+
| 3 | SOLD | Sold | Realized revenue / concluded deal |
|
|
67
|
+
| 4 | DENY | Rejected by Advertiser | Closed-lost |
|
|
68
|
+
| 5 | REJECT | Rejected by Publisher | Closed-lost |
|
|
69
|
+
| 7 | MATCHED | Matched (default) | Pre-negotiation initial stage |
|
|
70
|
+
| 9 | REJECTED_AGENCY | Rejected by Agency | Closed-lost |
|
|
71
|
+
| 10 | OPEN | Open | Active/in-negotiation deal; progress tracked via per-party approval fields |
|
|
72
|
+
|
|
73
|
+
Live open deals are a single `OPEN` (10) status driven by three independent per-party approval fields: `brand_approval_status`, `channel_approval_status`, `agency_approval_status` (each `1 PENDING` / `2 APPROVED` / `3 FINISHED`, or NULL), plus `first_contacted_party` (`1 BRAND` / `2 CHANNEL`, or NULL).
|
|
74
|
+
|
|
75
|
+
A deal is **committed** when it is SOLD, or OPEN with `brand_approval_status` in (APPROVED, FINISHED). `weighted_price` is derived from the brand/channel approval combination on OPEN deals.
|
|
74
76
|
|
|
75
77
|
#### `rejection_reason` Constants
|
|
76
78
|
|
|
@@ -247,6 +249,17 @@ thoughtleaders_adlink
|
|
|
247
249
|
|
|
248
250
|
As a special case, the `tl channels find` and `tl brands find` commands accept a name of the channel / brand (be sure to properly quote them for the shell) and will return the respective ID. Use this instead of constructing SQL for this particular case. The commands will return a list of possible choices
|
|
249
251
|
|
|
252
|
+
For bulk handle/name lookups in SQL (e.g. resolving a list of YouTube handles against `common_name`), compare case-insensitively with `UPPER(…)` on both sides — that's the form the functional indexes serve:
|
|
253
|
+
|
|
254
|
+
```sql
|
|
255
|
+
SELECT id, channel_name, common_name
|
|
256
|
+
FROM thoughtleaders_channel
|
|
257
|
+
WHERE UPPER(common_name) IN (UPPER('@TheInfographicsShow'), UPPER('@DrewDirksen'))
|
|
258
|
+
LIMIT 100 OFFSET 0
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
`LOWER(common_name)` (or `LOWER()` on any indexed column) cannot use those indexes — it seq-scans and times out.
|
|
262
|
+
|
|
250
263
|
### Common Join Paths
|
|
251
264
|
|
|
252
265
|
**Adlink → Channel name:**
|
|
@@ -302,7 +315,7 @@ Note: separate from the adspot publisher relationship. Not always in sync.
|
|
|
302
315
|
```sql
|
|
303
316
|
SELECT owner_sales_id, SUM(weighted_price) AS pipeline
|
|
304
317
|
FROM thoughtleaders_adlink
|
|
305
|
-
WHERE publish_status IN (
|
|
318
|
+
WHERE publish_status IN (7, 10)
|
|
306
319
|
GROUP BY owner_sales_id
|
|
307
320
|
ORDER BY pipeline DESC
|
|
308
321
|
LIMIT 100 OFFSET 0
|
|
@@ -254,10 +254,10 @@ Date upper bounds: `start_date` / `end_date` are date-typed and use `< next_day`
|
|
|
254
254
|
|
|
255
255
|
### `publish_status` (type 8 only) — numeric IDs, not strings
|
|
256
256
|
|
|
257
|
-
Sponsorship `publish_status` values are numeric IDs
|
|
257
|
+
Sponsorship `publish_status` values are numeric IDs from the set `{3, 4, 5, 7, 9, 10}`, **never string labels**. Don't emit `["sold"]` or `["live"]`. The canonical user-phrase → ID mapping is in [`references/report_glossary.md`](references/report_glossary.md) under "Deal-stage jargon". Quick anchors:
|
|
258
258
|
|
|
259
259
|
- `[3]` = sold
|
|
260
|
-
- `[
|
|
260
|
+
- `[7, 10]` = pipeline / pre-sale (matched / open)
|
|
261
261
|
- `[3]` + `filters_json.ad_publish_status: "0"` = sold + currently live on the channel
|
|
262
262
|
|
|
263
263
|
The `publish_status` field lives inside `filters_json`, not as a top-level FilterSet field.
|
|
@@ -408,7 +408,7 @@ For type 8 only, the `_over_<axis>` histograms (`count_sponsorships_over_send_da
|
|
|
408
408
|
|
|
409
409
|
| `filters_json.publish_status` includes | Use axis | Aggregator names |
|
|
410
410
|
| --- | --- | --- |
|
|
411
|
-
| Pre-sale (
|
|
411
|
+
| Pre-sale (7, 10) — matched / open | `send_date` (pipeline view) | `count_sponsorships_over_send_date`, `sum_price_over_send_date` |
|
|
412
412
|
| Sold only (3) | `purchase_date` (won-deals view) | `count_sponsorships_over_purchase_date`, `sum_price_over_purchase_date` |
|
|
413
413
|
| Mix of pre-sale + sold | `send_date` (pipeline view dominates) | as pipeline |
|
|
414
414
|
| Performance grades (winners/losers) | `purchase_date` | as won-deals |
|
|
@@ -60,16 +60,35 @@ Map informal descriptions → `filters_json.publish_status` IDs (integer; platfo
|
|
|
60
60
|
| User says | `publish_status` ID(s) | Other `filters_json` | Status name |
|
|
61
61
|
|---|---|---|---|
|
|
62
62
|
| "booked" / "sold" / "closed" / "won" | `3` | — | Sold |
|
|
63
|
-
| "
|
|
64
|
-
| "pending" | `2` | — | Pending |
|
|
63
|
+
| "open" / "in negotiation" | `10` | — | Open |
|
|
65
64
|
| "rejected" *(any side)* | `4, 5, 9` | — | Rejected by Brand / Creator / Agency |
|
|
66
65
|
| "matched" | `7` | — | Matched |
|
|
67
|
-
| "
|
|
68
|
-
| **"
|
|
69
|
-
| **"in progress"** / **"active"** | `0, 2, 3, 6` | — | Active incl. sold |
|
|
66
|
+
| **"pipeline"** *(default)* | `7, 10` | — | Matched + Open (pre-sale) |
|
|
67
|
+
| **"in progress"** / **"active"** | `3, 7, 10` | — | Active incl. sold |
|
|
70
68
|
| **"live"** / **"currently running"** | `3` | `ad_publish_status: "0"` | Sold AND published |
|
|
71
69
|
|
|
72
|
-
|
|
70
|
+
### Open and per-party approvals
|
|
71
|
+
|
|
72
|
+
A deal at **Open** (`publish_status` `10`) is an active, in-negotiation deal. Its progress is tracked by three independent per-party approval fields, each `PENDING`, `APPROVED`, `FINISHED`, or unset (`null`):
|
|
73
|
+
|
|
74
|
+
- `brand_approval` — the advertiser's sign-off
|
|
75
|
+
- `channel_approval` — the creator's sign-off
|
|
76
|
+
- `agency_approval` — the agency's sign-off (when an agency is involved)
|
|
77
|
+
|
|
78
|
+
These are **first-class FilterSet properties** — set directly on the report, not as keys inside `filters_json`. Each takes a comma list of `PENDING`/`APPROVED`/`FINISHED` (or ints `1`/`2`/`3`), plus `0`/`null`/`none` to match unset. They narrow the Open rows only and have no effect unless `publish_status` includes `10`.
|
|
79
|
+
|
|
80
|
+
The `committed` flag is a `filters_json` key (like `publish_status`): `filters_json.committed: "1"` selects Sold deals together with Open deals the brand has approved.
|
|
81
|
+
|
|
82
|
+
Users often name a deal by where it sits inside Open. Map those phrases to `publish_status` `10` plus the matching approval state:
|
|
83
|
+
|
|
84
|
+
| User says | `publish_status` | Approval filter (first-class) | Meaning |
|
|
85
|
+
|---|---|---|---|
|
|
86
|
+
| "reached out" / "outreach" | `10` | `channel_approval = PENDING` | Creator contacted, awaiting their reply |
|
|
87
|
+
| "proposed" / "creator approved" | `10` | `channel_approval = APPROVED` | Creator has agreed |
|
|
88
|
+
| "proposal approved" | `10` | `brand_approval = PENDING` | Brand is reviewing |
|
|
89
|
+
| "pending" | `10` | `brand_approval = APPROVED` | Brand has committed |
|
|
90
|
+
|
|
91
|
+
The `publish_status` set is `{3, 4, 5, 7, 9, 10}` (3 Sold, 4 Rejected by Brand, 5 Rejected by Creator, 7 Matched, 9 Rejected by Agency, 10 Open). The canonical schema home is [`tl/references/postgres-schema.md` → `publish_status` Constants](../../tl/references/postgres-schema.md#publish_status-constants); refer to it for the full enum.
|
|
73
92
|
|
|
74
93
|
## Field-pair disambiguation
|
|
75
94
|
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
{"name": "Scheduled Date", "backend_code": "send_date", "sortability": "both", "description": ""},
|
|
53
53
|
{"name": "Price", "backend_code": "price", "sortability": "both", "description": ""},
|
|
54
54
|
{"name": "Cost", "backend_code": "cost", "sortability": "both", "description": ""},
|
|
55
|
-
{"name": "Weighted price", "backend_code": "weighted_price", "sortability": "both", "description": "Price
|
|
55
|
+
{"name": "Weighted price", "backend_code": "weighted_price", "sortability": "both", "description": "Price weighted by the brand/channel approval combination on open deals."},
|
|
56
56
|
{"name": "Projected Views", "backend_code": "ad_spot__channel__impression", "sortability": "both", "description": ""},
|
|
57
57
|
{"name": "Status", "backend_code": "publish_status", "sortability": "both", "description": "Status of the deal."},
|
|
58
58
|
{"name": "Created", "backend_code": "created_at", "sortability": "both", "description": "Creation date of the sponsorship."},
|