thoughtleaders-cli 0.9.3__tar.gz → 0.9.7__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.3 → thoughtleaders_cli-0.9.7}/.claude-plugin/plugin.json +1 -1
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/.github/workflows/ci.yml +2 -2
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/.github/workflows/cli-integration.yml +2 -2
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/.github/workflows/python-publish.yml +1 -1
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/API.md +4 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/PKG-INFO +16 -1
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/README.md +15 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/pyproject.toml +1 -1
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl/SKILL.md +79 -3
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl/references/postgres-schema.md +3 -1
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/src/tl_cli/__init__.py +1 -1
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/src/tl_cli/client/http.py +14 -10
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/src/tl_cli/commands/balance.py +6 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/src/tl_cli/commands/db.py +5 -3
- thoughtleaders_cli-0.9.7/src/tl_cli/commands/memory.py +149 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/src/tl_cli/commands/reports.py +106 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/src/tl_cli/commands/setup.py +30 -1
- thoughtleaders_cli-0.9.7/src/tl_cli/commands/skills.py +437 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/src/tl_cli/commands/whoami.py +25 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/src/tl_cli/main.py +11 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/src/tl_cli/output/formatter.py +18 -6
- thoughtleaders_cli-0.9.7/src/tl_cli/skill_registry.py +312 -0
- thoughtleaders_cli-0.9.7/tests/test_memory.py +213 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/tests/test_output.py +46 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/tests/test_reports.py +51 -0
- thoughtleaders_cli-0.9.7/tests/test_setup_marker_respect.py +214 -0
- thoughtleaders_cli-0.9.7/tests/test_skill_registry.py +376 -0
- thoughtleaders_cli-0.9.7/tests/test_skills_command.py +420 -0
- thoughtleaders_cli-0.9.7/tests/test_whoami_newsletter.py +77 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/uv.lock +7 -19
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/.claude-plugin/marketplace.json +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/.github/dependabot.yml +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/.gitignore +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/AGENTS.md +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/CLAUDE.md +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/LICENSE +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/agents/keyword-context-classifier.md +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/agents/keyword-entity-resolver.md +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/agents/keyword-relevance-validator.md +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/agents/tl-analyst.md +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/agents/youtube-comment-classifier.md +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/hooks/hooks.json +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/hooks/scripts/load-tl-skill.mjs +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/hooks/scripts/post-usage.sh +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/hooks/scripts/pre-check.sh +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl/references/business-glossary.md +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl/references/elasticsearch-schema.md +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl/references/firebolt-schema.md +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-channel-authenticity/.gitignore +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-channel-authenticity/SKILL.md +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-channel-authenticity/references/comment-patterns.md +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-channel-authenticity/references/peer-cohort.md +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-channel-authenticity/references/red-flags.md +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-channel-authenticity/references/scoring.md +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-channel-authenticity/scripts/_io_utf8.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-channel-authenticity/scripts/analyze_channel.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-channel-authenticity/scripts/anomaly_detector.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-channel-authenticity/scripts/comment_analyzer.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-channel-authenticity/scripts/comment_scraper.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-channel-authenticity/scripts/engagement_ratios.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-channel-authenticity/scripts/peer_cohort.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-channel-authenticity/scripts/report.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-channel-authenticity/scripts/resolve_channel.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-channel-authenticity/scripts/score.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-channel-authenticity/scripts/tl_cli.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-channel-authenticity/scripts/video_integrity.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-channel-authenticity/scripts/view_curves.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-create-workflow/SKILL.md +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-create-workflow/references/creating-in-app.md +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-create-workflow/references/methodology.md +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-create-workflow/references/pitfalls.md +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-create-workflow/references/workflow-model.md +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-keyword-research/SKILL.md +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-keyword-research/references/elasticsearch-content-search.md +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-keyword-research/references/help.md +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-keyword-research/scripts/build_report.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-keyword-research/scripts/expand_entities.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-keyword-research/scripts/fetch_context.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-keyword-research/scripts/probe.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-keyword-research/scripts/search_channels.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-keyword-research/scripts/search_videos.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-keyword-research/scripts/select_keywords.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-save-report/SKILL.md +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-save-report/references/columns_brands.md +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-save-report/references/columns_channels.md +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-save-report/references/columns_content.md +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-save-report/references/columns_sponsorships.md +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-save-report/references/intelligence_filterset_schema.json +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-save-report/references/intelligence_widget_schema.json +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-save-report/references/report_glossary.md +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-save-report/references/sortable_columns.json +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-save-report/references/sponsorship_filterset_schema.json +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-save-report/references/sponsorship_widget_schema.json +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-save-report/references/widgets.md +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-top-partnerships/SKILL.md +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-top-partnerships/scripts/top_partnerships.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-views-guarantee/SKILL.md +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl-views-guarantee/scripts/vg.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/src/tl_cli/_completions.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/src/tl_cli/_typer_utils.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/src/tl_cli/auth/__init__.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/src/tl_cli/auth/commands.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/src/tl_cli/auth/login.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/src/tl_cli/auth/pkce.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/src/tl_cli/auth/token_store.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/src/tl_cli/client/__init__.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/src/tl_cli/client/errors.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/src/tl_cli/commands/__init__.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/src/tl_cli/commands/_comments_common.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/src/tl_cli/commands/brands.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/src/tl_cli/commands/bulk_import.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/src/tl_cli/commands/changelog.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/src/tl_cli/commands/channels.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/src/tl_cli/commands/credits.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/src/tl_cli/commands/deals.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/src/tl_cli/commands/describe.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/src/tl_cli/commands/doctor.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/src/tl_cli/commands/matches.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/src/tl_cli/commands/profiles.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/src/tl_cli/commands/proposals.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/src/tl_cli/commands/recommender.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/src/tl_cli/commands/schema.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/src/tl_cli/commands/snapshots.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/src/tl_cli/commands/sponsorships.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/src/tl_cli/commands/uploads.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/src/tl_cli/commands/workflows.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/src/tl_cli/config.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/src/tl_cli/filters.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/src/tl_cli/hints.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/src/tl_cli/output/__init__.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/src/tl_cli/query_history.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/src/tl_cli/self_update.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/tests/__init__.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/tests/test_auth.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/tests/test_brands_winner_channels.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/tests/test_build_report.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/tests/test_channels_lookalike.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/tests/test_db_repeat_warning.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/tests/test_describe.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/tests/test_error_hints.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/tests/test_expand_entities.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/tests/test_filters.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/tests/test_http_auth.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/tests/test_probe.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/tests/test_profiles.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/tests/test_scripts_executable.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/tests/test_search_channels.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/tests/test_search_videos.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/tests/test_select_keywords.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/tests/test_setup.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/tests/test_sponsorships.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/tests/test_workflows.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/tests_cli/AGENTS.md +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/tests_cli/conftest.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/tests_cli/test_balance.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/tests_cli/test_db_es.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/tests_cli/test_db_fb.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/tests_cli/test_db_pg.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/tests_cli/test_schema.py +0 -0
- {thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/tests_cli/test_whoami.py +0 -0
|
@@ -26,8 +26,8 @@ jobs:
|
|
|
26
26
|
# Matches `requires-python = ">=3.12"`.
|
|
27
27
|
python-version: ["3.12", "3.13", "3.14"]
|
|
28
28
|
steps:
|
|
29
|
-
- uses: actions/checkout@
|
|
30
|
-
- uses: astral-sh/setup-uv@
|
|
29
|
+
- uses: actions/checkout@v7
|
|
30
|
+
- uses: astral-sh/setup-uv@v7
|
|
31
31
|
with:
|
|
32
32
|
enable-cache: true
|
|
33
33
|
- name: Sync locked dependencies
|
|
@@ -29,8 +29,8 @@ jobs:
|
|
|
29
29
|
name: Live read-only CLI tests
|
|
30
30
|
runs-on: ubuntu-latest
|
|
31
31
|
steps:
|
|
32
|
-
- uses: actions/checkout@
|
|
33
|
-
- uses: astral-sh/setup-uv@
|
|
32
|
+
- uses: actions/checkout@v7
|
|
33
|
+
- uses: astral-sh/setup-uv@v7
|
|
34
34
|
with:
|
|
35
35
|
enable-cache: true
|
|
36
36
|
- name: Sync locked dependencies (installs the `tl` CLI)
|
|
@@ -142,6 +142,8 @@ Useful for verifying the API key resolves to the user you expect before kicking
|
|
|
142
142
|
|
|
143
143
|
`GET /balance` — credit balance plus the last 10 metered calls for the org. Free.
|
|
144
144
|
|
|
145
|
+
`balance` is the combined total of two pools: `topup_balance` (plan-granted credits, reset to the plan allowance each top-up period, spent first) and `purchased_balance` (bought credits — they survive top-up resets, are spent last, and keep working after the per-user session quota is exhausted).
|
|
146
|
+
|
|
145
147
|
```bash
|
|
146
148
|
curl -sS "$TL_API_BASE/balance" \
|
|
147
149
|
-H "Authorization: Bearer $TL_API_KEY" \
|
|
@@ -155,6 +157,8 @@ print(get('/balance'))
|
|
|
155
157
|
```json
|
|
156
158
|
{
|
|
157
159
|
"balance": 9995.88,
|
|
160
|
+
"topup_balance": 8995.88,
|
|
161
|
+
"purchased_balance": 1000.00,
|
|
158
162
|
"allow_overage": false,
|
|
159
163
|
"recent_usage": [
|
|
160
164
|
{
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: thoughtleaders-cli
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.7
|
|
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
|
|
@@ -284,6 +284,21 @@ The plugin ships several focused skills (installed by all the `tl setup *` comma
|
|
|
284
284
|
- **`tl-views-guarantee`** — sizes a multi-video sponsorship buy for a channel, returning the video bundle size, views guarantee, and likelihood to hit.
|
|
285
285
|
- **`tl-top-partnerships`** — brand-user performance report. Ranks a brand's sold sponsorships by live eCPM vs the sold-date projection, aggregates per channel, and delivers a two-tab Google Sheet ("By Deal" / "By Channel") via `gws`. Uses only public CLI commands (`tl whoami`, `tl sponsorships list`).
|
|
286
286
|
|
|
287
|
+
## Distributed skills
|
|
288
|
+
|
|
289
|
+
Beyond the bundled set above, an organization can be granted additional skills that aren't part of a CLI release — `tl skill` fetches and installs them on demand into the same directories `tl setup` uses (Claude Code's standalone skills directory, OpenCode's skills directory, and the directory shared by Gemini and Codex), so every supported agent picks them up automatically.
|
|
290
|
+
|
|
291
|
+
```bash
|
|
292
|
+
tl skill list # skills available to your organization, with installed/latest versions
|
|
293
|
+
tl skill list --all # full catalog (full-access accounts only)
|
|
294
|
+
tl skill download my-skill # fetch and install into every AI-agent skill directory
|
|
295
|
+
tl skill download my-skill --force # overwrite a directory tl doesn't already manage
|
|
296
|
+
tl skill update # refresh every downloaded skill to its latest version
|
|
297
|
+
tl skill remove my-skill # uninstall a downloaded skill
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
A directory `tl skill download` doesn't already manage (no prior `tl`-managed install there) is left alone unless `--force` is passed — it never overwrites a skill you installed some other way. `tl` warns once a day, on any command, when a downloaded skill has a newer version available. `tl setup` and self-update re-syncs also respect these directories: a downloaded skill is never rmtree'd or overwritten, even if its name collides with a bundled skill.
|
|
301
|
+
|
|
287
302
|
## Output Formats
|
|
288
303
|
|
|
289
304
|
By default, output is a styled table in the terminal and JSON when piped.
|
|
@@ -256,6 +256,21 @@ The plugin ships several focused skills (installed by all the `tl setup *` comma
|
|
|
256
256
|
- **`tl-views-guarantee`** — sizes a multi-video sponsorship buy for a channel, returning the video bundle size, views guarantee, and likelihood to hit.
|
|
257
257
|
- **`tl-top-partnerships`** — brand-user performance report. Ranks a brand's sold sponsorships by live eCPM vs the sold-date projection, aggregates per channel, and delivers a two-tab Google Sheet ("By Deal" / "By Channel") via `gws`. Uses only public CLI commands (`tl whoami`, `tl sponsorships list`).
|
|
258
258
|
|
|
259
|
+
## Distributed skills
|
|
260
|
+
|
|
261
|
+
Beyond the bundled set above, an organization can be granted additional skills that aren't part of a CLI release — `tl skill` fetches and installs them on demand into the same directories `tl setup` uses (Claude Code's standalone skills directory, OpenCode's skills directory, and the directory shared by Gemini and Codex), so every supported agent picks them up automatically.
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
tl skill list # skills available to your organization, with installed/latest versions
|
|
265
|
+
tl skill list --all # full catalog (full-access accounts only)
|
|
266
|
+
tl skill download my-skill # fetch and install into every AI-agent skill directory
|
|
267
|
+
tl skill download my-skill --force # overwrite a directory tl doesn't already manage
|
|
268
|
+
tl skill update # refresh every downloaded skill to its latest version
|
|
269
|
+
tl skill remove my-skill # uninstall a downloaded skill
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
A directory `tl skill download` doesn't already manage (no prior `tl`-managed install there) is left alone unless `--force` is passed — it never overwrites a skill you installed some other way. `tl` warns once a day, on any command, when a downloaded skill has a newer version available. `tl setup` and self-update re-syncs also respect these directories: a downloaded skill is never rmtree'd or overwritten, even if its name collides with a bundled skill.
|
|
273
|
+
|
|
259
274
|
## Output Formats
|
|
260
275
|
|
|
261
276
|
By default, output is a styled table in the terminal and JSON when piped.
|
|
@@ -217,6 +217,8 @@ tl snapshots channel <id> # Channel metrics over time (Firebolt-bac
|
|
|
217
217
|
tl snapshots video <id> --channel <id> # Video view curve (--channel required!)
|
|
218
218
|
tl reports # List saved reports
|
|
219
219
|
tl reports run <id> # Run a saved report
|
|
220
|
+
tl reports link <id> --source <id> --entity <e> [--type include|exclude] [--source-side included|excluded] # Link another report in as a live entity source
|
|
221
|
+
tl reports unlink <id> --source <id> [--entity <e>] [--type include|exclude] # Remove report links (all from that source unless narrowed)
|
|
220
222
|
tl <entity> comment-list <id> # List comments on a sponsorship/channel/brand/upload
|
|
221
223
|
tl <entity> comment-add <id> "msg" # Add a comment
|
|
222
224
|
tl <entity> comment-edit <comment-id> "msg" # Edit own comment (author or superuser)
|
|
@@ -251,9 +253,8 @@ contact field:
|
|
|
251
253
|
|
|
252
254
|
The channel's full email archive (`all_emails` — a JSON object keyed by email address,
|
|
253
255
|
each value recording when and where the address was found) is **not editable here**;
|
|
254
|
-
sending it returns a 400. It is append-only
|
|
255
|
-
|
|
256
|
-
modifies or removes existing entries.
|
|
256
|
+
sending it returns a 400. It is append-only and managed through an internal-only
|
|
257
|
+
process that never modifies or removes existing entries — not available via this CLI.
|
|
257
258
|
|
|
258
259
|
**Profile notes.** `tl profiles update` (superuser only) edits a brand/publisher profile's
|
|
259
260
|
**`superuser_notes`** — the internal free-text notes field on the customer record
|
|
@@ -262,6 +263,66 @@ clear. This is currently the only editable profile field; anything else returns
|
|
|
262
263
|
the editable surface. Use it for account-handling notes meant for the internal team — it is
|
|
263
264
|
never shown to the customer.
|
|
264
265
|
|
|
266
|
+
**Report linking.** `tl reports link` attaches another report to a report as a **live**
|
|
267
|
+
include/exclude entity source — the target report's results are filtered by the source
|
|
268
|
+
report's entities (channels, brands, articles, or sponsorships) and stay in sync as the
|
|
269
|
+
source changes. This is the "include/exclude entities from another report" feature the
|
|
270
|
+
web UI offers, and it differs from `tl bulk-import`, which copies a **frozen snapshot**
|
|
271
|
+
of IDs. `--type` defaults to `include`; `--source-side` defaults to `included` (pass
|
|
272
|
+
`excluded` to pull the source report's excluded entities instead of its results).
|
|
273
|
+
Requires owning the target report (or full access), and the source report must be
|
|
274
|
+
visible to you. Linking a report to itself, an exact duplicate link, or an unknown key
|
|
275
|
+
comes back as a 400.
|
|
276
|
+
|
|
277
|
+
`tl reports unlink` removes links: with only `--source`, every link pulling from that
|
|
278
|
+
source report is removed; add `--entity` and/or `--type` to remove just the matching
|
|
279
|
+
one. No matching link comes back as a 404, never a silent no-op.
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
tl reports link 1234 --source 5678 --entity channels # include source's channels
|
|
283
|
+
tl reports link 1234 --source 5678 --entity brands --type exclude # exclude source's brands
|
|
284
|
+
tl reports unlink 1234 --source 5678 # remove all links from 5678
|
|
285
|
+
tl reports unlink 1234 --source 5678 --entity brands --type exclude # remove one specific link
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Profile memory
|
|
289
|
+
|
|
290
|
+
Profile memory is the free-text summary of who a user is and what they want from
|
|
291
|
+
sponsorships. The onboarding interview writes it first; `tl memory` keeps it current
|
|
292
|
+
afterwards. **This is the standard way to update user data** — prefer it over any
|
|
293
|
+
direct database write.
|
|
294
|
+
|
|
295
|
+
```bash
|
|
296
|
+
tl memory show # Your memory + the preferences derived from it (free)
|
|
297
|
+
tl memory show --profile-id <id> # Someone else's memory (full-access only) (free)
|
|
298
|
+
tl memory add "<fact>" # Fold one new fact into the existing memory (free)
|
|
299
|
+
tl memory set "<blob>" # Replace the memory wholesale (free)
|
|
300
|
+
tl memory set --from-file <path> # Same, reading the text from a file (free)
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Examples:
|
|
304
|
+
```bash
|
|
305
|
+
tl memory show --profile-id 8871
|
|
306
|
+
tl memory add "Stopped doing finance ads. Now targeting 18-24 in the US."
|
|
307
|
+
tl memory set --from-file ./memory.txt
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
`add` is the verb to reach for almost every time: the new fact is folded into the
|
|
311
|
+
existing memory, keeping what's still true and replacing only what the fact
|
|
312
|
+
contradicts. Never read the memory, edit it yourself and write it back with `set` when
|
|
313
|
+
`add` would do — one `add` per fact is the whole interface. `add` takes noticeably
|
|
314
|
+
longer than the other two; that is expected, so let it finish rather than retrying,
|
|
315
|
+
because a repeated `add` can land twice. A fact is one statement, not a document:
|
|
316
|
+
anything past 4,000 characters is rejected, so split a long update into several `add`
|
|
317
|
+
calls rather than sending it as one.
|
|
318
|
+
|
|
319
|
+
`set` replaces the memory wholesale and, unlike `add`, accepts a short replacement for
|
|
320
|
+
a long memory — treat it as the repair hatch for when a merge has damaged a memory,
|
|
321
|
+
not as a routine update path. Blank replacement text is refused rather than erasing the
|
|
322
|
+
memory, and text beyond 50,000 characters is not kept. `add` and `set` always write to
|
|
323
|
+
the **caller's own** profile regardless of permissions; only `show --profile-id` can
|
|
324
|
+
reach someone else's, and that's full-access only.
|
|
325
|
+
|
|
265
326
|
### Creating and vetting sponsorships
|
|
266
327
|
|
|
267
328
|
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.
|
|
@@ -503,6 +564,21 @@ tl changelog since v0.4.10 # Notes from v0.4.10 to latest
|
|
|
503
564
|
tl changelog --md > CHANGELOG.md # Capture for a doc
|
|
504
565
|
```
|
|
505
566
|
|
|
567
|
+
#### Distributed skills — beyond the bundled set
|
|
568
|
+
|
|
569
|
+
An organization can be granted additional skills that aren't part of a CLI release. `tl skill` fetches and installs them on demand into the same directories `tl setup` uses (Claude Code's standalone skills directory, OpenCode's skills directory, and the directory shared by Gemini and Codex), so every supported agent picks them up automatically — no restart or reconfiguration needed beyond re-reading the skill listing.
|
|
570
|
+
|
|
571
|
+
```bash
|
|
572
|
+
tl skill list # Skills available to your org, with installed/latest versions (free)
|
|
573
|
+
tl skill list --all # Full catalog (full-access accounts only)
|
|
574
|
+
tl skill download <name> # Fetch and install into every AI-agent skill directory on this machine
|
|
575
|
+
tl skill download <name> --force # Overwrite a directory tl doesn't already manage
|
|
576
|
+
tl skill update # Refresh every downloaded skill to its latest version
|
|
577
|
+
tl skill remove <name> # Uninstall a downloaded skill
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
If a user asks to find, add, or refresh a skill by capability ("is there a skill for X", "get me the latest Y skill"), check `tl skill list` before saying no — the bundled set isn't the whole catalog. `tl skill download` never overwrites a directory it doesn't already manage unless `--force` is passed, so it's safe to run without clobbering a manually-installed skill of the same name. `tl` warns once a day, on any command, when a downloaded skill has a newer version available — surface that nag to the user rather than silently ignoring it, and offer to run `tl skill update`.
|
|
581
|
+
|
|
506
582
|
#### Channel & video discovery — pick the path for the question shape
|
|
507
583
|
|
|
508
584
|
Four first-class paths, each with a different signal. **Pick by the SHAPE of the user's question, not by habit.** "Recommender first" is the right default only for path 2 — for paths 1, 3, and 4 the recommender is the wrong tool.
|
{thoughtleaders_cli-0.9.3 → thoughtleaders_cli-0.9.7}/skills/tl/references/postgres-schema.md
RENAMED
|
@@ -25,6 +25,7 @@ The profile table is tightly coupled with the brand table for media buyers, so m
|
|
|
25
25
|
> - ❌ `youtube_id` (on channel) — use `external_channel_id`.
|
|
26
26
|
> - ❌ `msn_join_date` (on channel) — use `media_selling_network_join_date`.
|
|
27
27
|
> - ❌ `mbn_join_date` (on profile) — use `media_buying_network_join_date`.
|
|
28
|
+
> - ❌ `url` — renamed to `urls` (array) by migration 0010 (2026-07-16, "AdLink.url becomes urls"). A sponsorship can carry several URLs now; use `urls` — e.g. `urls[1]` for the first entry, or `array_to_string(urls, ', ')` to flatten.
|
|
28
29
|
|
|
29
30
|
#### Key Columns for the thoughtleaders_adlink table
|
|
30
31
|
|
|
@@ -58,6 +59,7 @@ The profile table is tightly coupled with the brand table for media buyers, so m
|
|
|
58
59
|
| `performance_grade` | int | Performance rating (see business-glossary) |
|
|
59
60
|
| `article_id` | varchar | Compound `<channel_id>:<youtube_id>` — links to ES `_id` and ES `id` field |
|
|
60
61
|
| `created_where` | varchar | What mechanism / software / agent created the record |
|
|
62
|
+
| `urls` | varchar(600)[] (array) | Destination/reference URLs for the sponsorship. ⚠️ Renamed from the old singular `url` column by migration 0010 (2026-07-16) — a sponsorship can carry several URLs now. Never NULL (empty array `{}` when none set). Use `urls[1]` for "the" URL when a single value is needed, or `array_to_string(urls, ', ')` to flatten for display. |
|
|
61
63
|
|
|
62
64
|
#### `publish_status` Constants
|
|
63
65
|
|
|
@@ -204,7 +206,7 @@ A channel can have multiple adspots (different sellers: talent manager, direct,
|
|
|
204
206
|
|
|
205
207
|
When composing `SELECT ... FROM thoughtleaders_channel ...`, do not improvise column names from semantic intuition — consult the output of `tl schema pg thoughtleaders_channel` or the column table above. The `tl schema` command is authoritative. Failed guesses return *"column '\<name\>' does not exist"* and cost a round-trip. Recurring problems:
|
|
206
208
|
|
|
207
|
-
-
|
|
209
|
+
- ❌ `reach` — this column does NOT exist on Postgres. Use `subscribers`. (The Elasticsearch channel doc uses `reach` as a legacy alias for the same value — see [elasticsearch-schema.md](elasticsearch-schema.md) — but Postgres never had that name; don't carry the ES field name over when switching to a `tl db pg` query.)
|
|
208
210
|
- Projected views is in the field named `projected_views`
|
|
209
211
|
- ❌ **Suffix/qualifier variants of date columns** (e.g. an `_max` / `latest_` / `_date` form when the canonical column has neither). Date columns use bare names.
|
|
210
212
|
- ❌ **Platform-name-prefixed ID forms** (e.g. a platform-name prefix when the canonical column uses a neutral `external_` prefix). See the column table for the actual ID column.
|
|
@@ -23,11 +23,11 @@ class TLClient:
|
|
|
23
23
|
},
|
|
24
24
|
)
|
|
25
25
|
|
|
26
|
-
def get(self, path: str, params: dict | None = None) -> dict:
|
|
27
|
-
return self._request("GET", path, params=params)
|
|
26
|
+
def get(self, path: str, params: dict | None = None, timeout: float | None = None) -> dict:
|
|
27
|
+
return self._request("GET", path, params=params, timeout=timeout)
|
|
28
28
|
|
|
29
|
-
def post(self, path: str, json_body: dict | None = None) -> dict:
|
|
30
|
-
return self._request("POST", path, json_body=json_body)
|
|
29
|
+
def post(self, path: str, json_body: dict | None = None, timeout: float | None = None) -> dict:
|
|
30
|
+
return self._request("POST", path, json_body=json_body, timeout=timeout)
|
|
31
31
|
|
|
32
32
|
def patch(self, path: str, json_body: dict | None = None) -> dict:
|
|
33
33
|
return self._request("PATCH", path, json_body=json_body)
|
|
@@ -38,20 +38,24 @@ class TLClient:
|
|
|
38
38
|
path: str,
|
|
39
39
|
params: dict | None = None,
|
|
40
40
|
json_body: dict | None = None,
|
|
41
|
+
timeout: float | None = None,
|
|
41
42
|
) -> dict:
|
|
42
43
|
headers = self._auth_headers()
|
|
44
|
+
# Only overrides the client's default timeout when the caller asks
|
|
45
|
+
# for one — passing `timeout=None` through to httpx means "no
|
|
46
|
+
# timeout", not "use the default", so it's omitted entirely here.
|
|
47
|
+
request_kwargs = {"params": params, "json": json_body, "headers": headers}
|
|
48
|
+
if timeout is not None:
|
|
49
|
+
request_kwargs["timeout"] = timeout
|
|
43
50
|
|
|
44
|
-
response = self._client.request(
|
|
45
|
-
method, path, params=params, json=json_body, headers=headers
|
|
46
|
-
)
|
|
51
|
+
response = self._client.request(method, path, **request_kwargs)
|
|
47
52
|
|
|
48
53
|
# On 401, try refreshing the token once
|
|
49
54
|
if response.status_code == 401:
|
|
50
55
|
headers = self._refresh_and_get_headers()
|
|
51
56
|
if headers:
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
)
|
|
57
|
+
request_kwargs["headers"] = headers
|
|
58
|
+
response = self._client.request(method, path, **request_kwargs)
|
|
55
59
|
|
|
56
60
|
if response.status_code >= 400:
|
|
57
61
|
detail = self._extract_detail(response)
|
|
@@ -44,6 +44,12 @@ def balance(
|
|
|
44
44
|
allow_overage = data.get("allow_overage", False)
|
|
45
45
|
|
|
46
46
|
console.print(f"\n[bold]Credit Balance:[/bold] [cyan]{balance_val}[/cyan] credits")
|
|
47
|
+
# Older servers return only the combined balance; newer ones split it
|
|
48
|
+
# into the plan-granted (top-up) pool and the purchased pool.
|
|
49
|
+
topup_val = data.get("topup_balance")
|
|
50
|
+
purchased_val = data.get("purchased_balance")
|
|
51
|
+
if topup_val is not None and purchased_val is not None:
|
|
52
|
+
console.print(f"[dim]Plan (top-up) credits: {topup_val} · Purchased: {purchased_val}[/dim]")
|
|
47
53
|
if allow_overage:
|
|
48
54
|
console.print("[dim]Overage: enabled[/dim]")
|
|
49
55
|
|
|
@@ -99,9 +99,11 @@ def pg_cmd(
|
|
|
99
99
|
Common traps — check before writing SQL:
|
|
100
100
|
|
|
101
101
|
- Column names follow the current schema: subscribers, projected_views,
|
|
102
|
-
scheduled_date, is_tpp, advertiser_profile_id, views_guarantee
|
|
103
|
-
older reach / impression / send_date /
|
|
104
|
-
/ impressions_guarantee.
|
|
102
|
+
scheduled_date, is_tpp, advertiser_profile_id, views_guarantee, url (the
|
|
103
|
+
channel's own URL) — NOT the older reach / impression / send_date /
|
|
104
|
+
is_tl_channel / creator_profile_id / impressions_guarantee / channel_url.
|
|
105
|
+
- thoughtleaders_adlink has no bare `url` column — the live sponsored-video
|
|
106
|
+
URL is `media_url`.
|
|
105
107
|
- The sponsorship table (thoughtleaders_adlink) hides its relations behind
|
|
106
108
|
joins: no channel_id/channel_name (join thoughtleaders_adspot via
|
|
107
109
|
ad_spot_id, then thoughtleaders_channel) and no brand_id (join
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""tl memory — read and update your profile memory.
|
|
2
|
+
|
|
3
|
+
Your profile memory is the free-text summary of who you are and what you want from
|
|
4
|
+
sponsorships. The onboarding interview writes it first; these commands are how it
|
|
5
|
+
stays current afterwards.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.markup import escape as rich_escape
|
|
13
|
+
|
|
14
|
+
from tl_cli._typer_utils import AlphaSortedTyperGroup
|
|
15
|
+
from tl_cli.client.errors import ApiError, handle_api_error
|
|
16
|
+
from tl_cli.client.http import get_client
|
|
17
|
+
from tl_cli.output.formatter import detect_format, output_single
|
|
18
|
+
|
|
19
|
+
app = typer.Typer(cls=AlphaSortedTyperGroup, help="Your profile memory (show; add a fact; replace)")
|
|
20
|
+
|
|
21
|
+
# `add` folds the fact into the existing memory with a model call whose duration
|
|
22
|
+
# scales with how much memory there is to rewrite, so it can run far past the
|
|
23
|
+
# client's 30s default. `set` is a plain replace and needs no extra room.
|
|
24
|
+
_ADD_TIMEOUT_SECONDS = 180.0
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@app.command("show")
|
|
28
|
+
def show_cmd(
|
|
29
|
+
profile_id: int | None = typer.Option(None, "--profile-id", help="Another profile's memory (full-access only)"),
|
|
30
|
+
json_output: bool = typer.Option(False, "--json", help="JSON output"),
|
|
31
|
+
toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
|
|
32
|
+
) -> None:
|
|
33
|
+
"""Show your profile memory and the preferences derived from it.
|
|
34
|
+
|
|
35
|
+
Examples:
|
|
36
|
+
tl memory show
|
|
37
|
+
tl memory show --json
|
|
38
|
+
tl memory show --profile-id 8871
|
|
39
|
+
"""
|
|
40
|
+
fmt = detect_format(json_output, False, False, toon_output)
|
|
41
|
+
params = {"profile_id": profile_id} if profile_id is not None else {}
|
|
42
|
+
|
|
43
|
+
client = get_client()
|
|
44
|
+
try:
|
|
45
|
+
data = client.get("/memory", params=params)
|
|
46
|
+
output_single(data, fmt)
|
|
47
|
+
except ApiError as e:
|
|
48
|
+
handle_api_error(e)
|
|
49
|
+
finally:
|
|
50
|
+
client.close()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@app.command("add")
|
|
54
|
+
def add_cmd(
|
|
55
|
+
fact: str = typer.Argument(..., help="One new fact about you"),
|
|
56
|
+
json_output: bool = typer.Option(False, "--json", help="JSON output"),
|
|
57
|
+
toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
|
|
58
|
+
) -> None:
|
|
59
|
+
"""Fold one new fact into your profile memory.
|
|
60
|
+
|
|
61
|
+
The fact is merged into what's already there — everything still true is kept,
|
|
62
|
+
and only what the new fact contradicts is replaced. Always writes to your own
|
|
63
|
+
profile.
|
|
64
|
+
|
|
65
|
+
Examples:
|
|
66
|
+
tl memory add "We stopped doing finance sponsorships."
|
|
67
|
+
tl memory add "Now targeting 18-24 in the US."
|
|
68
|
+
"""
|
|
69
|
+
fmt = detect_format(json_output, False, False, toon_output)
|
|
70
|
+
if not fact.strip():
|
|
71
|
+
Console(stderr=True).print("[red]Error:[/red] fact cannot be empty.")
|
|
72
|
+
raise typer.Exit(1)
|
|
73
|
+
|
|
74
|
+
client = get_client()
|
|
75
|
+
try:
|
|
76
|
+
data = client.post("/memory", json_body={"fact": fact.strip()}, timeout=_ADD_TIMEOUT_SECONDS)
|
|
77
|
+
output_single(data, fmt)
|
|
78
|
+
except ApiError as e:
|
|
79
|
+
handle_api_error(e)
|
|
80
|
+
finally:
|
|
81
|
+
client.close()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@app.command("set")
|
|
85
|
+
def set_cmd(
|
|
86
|
+
memory: str | None = typer.Argument(None, help="The full replacement text"),
|
|
87
|
+
from_file: Path | None = typer.Option(None, "--from-file", help="Read the replacement text from a file"),
|
|
88
|
+
json_output: bool = typer.Option(False, "--json", help="JSON output"),
|
|
89
|
+
toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
|
|
90
|
+
) -> None:
|
|
91
|
+
"""Replace your profile memory wholesale.
|
|
92
|
+
|
|
93
|
+
The repair hatch for when a merge has gone wrong — unlike `add`, this is not
|
|
94
|
+
length-guarded, so it will happily replace a long blob with a short one.
|
|
95
|
+
Prefer `add` for ordinary updates.
|
|
96
|
+
|
|
97
|
+
Examples:
|
|
98
|
+
tl memory set "Runs a finance channel. Avoids crypto."
|
|
99
|
+
tl memory set --from-file ./memory.txt
|
|
100
|
+
"""
|
|
101
|
+
fmt = detect_format(json_output, False, False, toon_output)
|
|
102
|
+
err = Console(stderr=True)
|
|
103
|
+
|
|
104
|
+
if memory is not None and from_file is not None:
|
|
105
|
+
err.print("[red]Error:[/red] pass either the replacement text or --from-file, not both.")
|
|
106
|
+
raise typer.Exit(1)
|
|
107
|
+
if memory is None and from_file is None:
|
|
108
|
+
err.print("[red]Error:[/red] pass the replacement text, or --from-file to read it from a file.")
|
|
109
|
+
raise typer.Exit(1)
|
|
110
|
+
|
|
111
|
+
if from_file is not None:
|
|
112
|
+
try:
|
|
113
|
+
# utf-8-sig, not utf-8: an editor that writes a byte-order mark leaves it
|
|
114
|
+
# as the first character of the text, where it is not whitespace, survives
|
|
115
|
+
# the strip below, and is stored invisibly at the head of the memory.
|
|
116
|
+
memory = from_file.read_text(encoding="utf-8-sig")
|
|
117
|
+
# A non-text file raises UnicodeDecodeError, a ValueError rather than an
|
|
118
|
+
# OSError. The path and the error text are escaped because a bracket in
|
|
119
|
+
# either would abort on a markup error and lose the message itself.
|
|
120
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
121
|
+
err.print(f"[red]Error:[/red] could not read {rich_escape(str(from_file))}: {rich_escape(str(exc))}")
|
|
122
|
+
raise typer.Exit(1)
|
|
123
|
+
|
|
124
|
+
# A blank replacement erases the whole memory, and the two ways to arrive at one
|
|
125
|
+
# — a stray empty argument, or a --from-file that was never written — are both
|
|
126
|
+
# mistakes rather than an intent to erase.
|
|
127
|
+
memory = (memory or "").strip()
|
|
128
|
+
if not memory:
|
|
129
|
+
source = f"{rich_escape(str(from_file))} is empty" if from_file is not None else "the replacement text is empty"
|
|
130
|
+
err.print(f"[red]Error:[/red] {source}; that would erase your whole memory.")
|
|
131
|
+
err.print("Pass the text you want stored, or use [bold]tl memory add[/bold] to change one thing at a time.")
|
|
132
|
+
raise typer.Exit(1)
|
|
133
|
+
|
|
134
|
+
client = get_client()
|
|
135
|
+
try:
|
|
136
|
+
data = client.post("/memory", json_body={"memory": memory})
|
|
137
|
+
output_single(data, fmt)
|
|
138
|
+
# A replacement past the size limit is kept only up to it. The stored length
|
|
139
|
+
# is in the output either way, but nothing there says text went missing.
|
|
140
|
+
stored = data.get("profile_memory")
|
|
141
|
+
if isinstance(stored, str) and len(stored) < len(memory):
|
|
142
|
+
err.print(
|
|
143
|
+
f"[bold yellow]Warning:[/bold yellow] [yellow]stored {len(stored):,} of "
|
|
144
|
+
f"{len(memory):,} characters — the rest was past the size limit.[/yellow]"
|
|
145
|
+
)
|
|
146
|
+
except ApiError as e:
|
|
147
|
+
handle_api_error(e)
|
|
148
|
+
finally:
|
|
149
|
+
client.close()
|
|
@@ -672,3 +672,109 @@ def update_report(
|
|
|
672
672
|
handle_api_error(e)
|
|
673
673
|
finally:
|
|
674
674
|
client.close()
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
LINK_ENTITIES = ("channels", "brands", "articles", "sponsorships")
|
|
678
|
+
LINK_TYPES = ("include", "exclude")
|
|
679
|
+
LINK_SOURCE_SIDES = ("included", "excluded")
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
@app.command("link")
|
|
683
|
+
def link_report(
|
|
684
|
+
report_id: int = typer.Argument(..., help="Target report ID (the report being filtered)"),
|
|
685
|
+
source: int = typer.Option(..., "--source", "-s", help="Source report ID whose entities are pulled in"),
|
|
686
|
+
entity: str = typer.Option(..., "--entity", "-e", help=f"Entity type: one of {', '.join(LINK_ENTITIES)}"),
|
|
687
|
+
filter_type: str = typer.Option(
|
|
688
|
+
"include", "--type", "-t", help="How the source acts on the target: include or exclude"
|
|
689
|
+
),
|
|
690
|
+
source_side: str = typer.Option(
|
|
691
|
+
"included",
|
|
692
|
+
"--source-side",
|
|
693
|
+
help="Pull the source report's results ('included', default) or its excluded entities ('excluded')",
|
|
694
|
+
),
|
|
695
|
+
json_output: bool = typer.Option(False, "--json", help="JSON output"),
|
|
696
|
+
toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
|
|
697
|
+
) -> None:
|
|
698
|
+
"""Link another report into this report as an include/exclude entity source.
|
|
699
|
+
|
|
700
|
+
The target report's results are then filtered by the source report's
|
|
701
|
+
entities of the given type — the same linking the web UI offers, kept
|
|
702
|
+
in sync automatically (unlike a bulk-import snapshot).
|
|
703
|
+
|
|
704
|
+
Examples:
|
|
705
|
+
tl reports link 1234 --source 5678 --entity channels --type include
|
|
706
|
+
tl reports link 1234 --source 5678 --entity brands --type exclude
|
|
707
|
+
"""
|
|
708
|
+
if entity not in LINK_ENTITIES:
|
|
709
|
+
err.print(f"[red]--entity must be one of: {', '.join(LINK_ENTITIES)}[/red]")
|
|
710
|
+
raise typer.Exit(2)
|
|
711
|
+
if filter_type not in LINK_TYPES:
|
|
712
|
+
err.print(f"[red]--type must be one of: {', '.join(LINK_TYPES)}[/red]")
|
|
713
|
+
raise typer.Exit(2)
|
|
714
|
+
if source_side not in LINK_SOURCE_SIDES:
|
|
715
|
+
err.print(f"[red]--source-side must be one of: {', '.join(LINK_SOURCE_SIDES)}[/red]")
|
|
716
|
+
raise typer.Exit(2)
|
|
717
|
+
|
|
718
|
+
fmt = detect_format(json_output, False, False, toon_output)
|
|
719
|
+
client = get_client()
|
|
720
|
+
try:
|
|
721
|
+
data = client.post(
|
|
722
|
+
f"/reports/{report_id}/link",
|
|
723
|
+
json_body={
|
|
724
|
+
"source_report_id": source,
|
|
725
|
+
"entity": entity,
|
|
726
|
+
"filter_type": filter_type,
|
|
727
|
+
"campaign_entity_type": source_side,
|
|
728
|
+
},
|
|
729
|
+
)
|
|
730
|
+
output_single(data, fmt)
|
|
731
|
+
except ApiError as e:
|
|
732
|
+
handle_api_error(e)
|
|
733
|
+
finally:
|
|
734
|
+
client.close()
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
@app.command("unlink")
|
|
738
|
+
def unlink_report(
|
|
739
|
+
report_id: int = typer.Argument(..., help="Target report ID (the report holding the link)"),
|
|
740
|
+
source: int = typer.Option(..., "--source", "-s", help="Source report ID to unlink"),
|
|
741
|
+
entity: str | None = typer.Option(
|
|
742
|
+
None, "--entity", "-e", help=f"Only remove links of this entity type: one of {', '.join(LINK_ENTITIES)}"
|
|
743
|
+
),
|
|
744
|
+
filter_type: str | None = typer.Option(
|
|
745
|
+
None, "--type", "-t", help="Only remove links of this type: include or exclude"
|
|
746
|
+
),
|
|
747
|
+
json_output: bool = typer.Option(False, "--json", help="JSON output"),
|
|
748
|
+
toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
|
|
749
|
+
) -> None:
|
|
750
|
+
"""Remove a report link created with `tl reports link`.
|
|
751
|
+
|
|
752
|
+
Without --entity/--type, removes every link pulling from the source
|
|
753
|
+
report. Narrow with --entity and/or --type to remove a specific one.
|
|
754
|
+
|
|
755
|
+
Examples:
|
|
756
|
+
tl reports unlink 1234 --source 5678
|
|
757
|
+
tl reports unlink 1234 --source 5678 --entity channels --type exclude
|
|
758
|
+
"""
|
|
759
|
+
if entity is not None and entity not in LINK_ENTITIES:
|
|
760
|
+
err.print(f"[red]--entity must be one of: {', '.join(LINK_ENTITIES)}[/red]")
|
|
761
|
+
raise typer.Exit(2)
|
|
762
|
+
if filter_type is not None and filter_type not in LINK_TYPES:
|
|
763
|
+
err.print(f"[red]--type must be one of: {', '.join(LINK_TYPES)}[/red]")
|
|
764
|
+
raise typer.Exit(2)
|
|
765
|
+
|
|
766
|
+
body: dict = {"source_report_id": source}
|
|
767
|
+
if entity is not None:
|
|
768
|
+
body["entity"] = entity
|
|
769
|
+
if filter_type is not None:
|
|
770
|
+
body["filter_type"] = filter_type
|
|
771
|
+
|
|
772
|
+
fmt = detect_format(json_output, False, False, toon_output)
|
|
773
|
+
client = get_client()
|
|
774
|
+
try:
|
|
775
|
+
data = client.post(f"/reports/{report_id}/unlink", json_body=body)
|
|
776
|
+
output_single(data, fmt)
|
|
777
|
+
except ApiError as e:
|
|
778
|
+
handle_api_error(e)
|
|
779
|
+
finally:
|
|
780
|
+
client.close()
|