thoughtleaders-cli 0.9.4__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.4 → thoughtleaders_cli-0.9.7}/.claude-plugin/plugin.json +1 -1
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/PKG-INFO +1 -1
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/pyproject.toml +1 -1
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl/SKILL.md +62 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl/references/postgres-schema.md +3 -1
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/src/tl_cli/__init__.py +1 -1
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/src/tl_cli/client/http.py +2 -2
- thoughtleaders_cli-0.9.7/src/tl_cli/commands/memory.py +149 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/src/tl_cli/commands/reports.py +106 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/src/tl_cli/commands/whoami.py +25 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/src/tl_cli/main.py +2 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/src/tl_cli/output/formatter.py +18 -6
- thoughtleaders_cli-0.9.7/tests/test_memory.py +213 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/tests/test_output.py +46 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/tests/test_reports.py +51 -0
- thoughtleaders_cli-0.9.7/tests/test_whoami_newsletter.py +77 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/.claude-plugin/marketplace.json +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/.github/dependabot.yml +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/.github/workflows/ci.yml +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/.github/workflows/cli-integration.yml +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/.github/workflows/python-publish.yml +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/.gitignore +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/AGENTS.md +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/API.md +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/CLAUDE.md +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/LICENSE +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/README.md +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/agents/keyword-context-classifier.md +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/agents/keyword-entity-resolver.md +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/agents/keyword-relevance-validator.md +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/agents/tl-analyst.md +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/agents/youtube-comment-classifier.md +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/hooks/hooks.json +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/hooks/scripts/load-tl-skill.mjs +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/hooks/scripts/post-usage.sh +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/hooks/scripts/pre-check.sh +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl/references/business-glossary.md +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl/references/elasticsearch-schema.md +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl/references/firebolt-schema.md +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-channel-authenticity/.gitignore +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-channel-authenticity/SKILL.md +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-channel-authenticity/references/comment-patterns.md +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-channel-authenticity/references/peer-cohort.md +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-channel-authenticity/references/red-flags.md +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-channel-authenticity/references/scoring.md +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-channel-authenticity/scripts/_io_utf8.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-channel-authenticity/scripts/analyze_channel.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-channel-authenticity/scripts/anomaly_detector.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-channel-authenticity/scripts/comment_analyzer.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-channel-authenticity/scripts/comment_scraper.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-channel-authenticity/scripts/engagement_ratios.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-channel-authenticity/scripts/peer_cohort.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-channel-authenticity/scripts/report.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-channel-authenticity/scripts/resolve_channel.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-channel-authenticity/scripts/score.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-channel-authenticity/scripts/tl_cli.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-channel-authenticity/scripts/video_integrity.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-channel-authenticity/scripts/view_curves.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-create-workflow/SKILL.md +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-create-workflow/references/creating-in-app.md +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-create-workflow/references/methodology.md +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-create-workflow/references/pitfalls.md +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-create-workflow/references/workflow-model.md +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-keyword-research/SKILL.md +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-keyword-research/references/elasticsearch-content-search.md +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-keyword-research/references/help.md +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-keyword-research/scripts/build_report.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-keyword-research/scripts/expand_entities.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-keyword-research/scripts/fetch_context.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-keyword-research/scripts/probe.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-keyword-research/scripts/search_channels.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-keyword-research/scripts/search_videos.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-keyword-research/scripts/select_keywords.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-save-report/SKILL.md +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-save-report/references/columns_brands.md +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-save-report/references/columns_channels.md +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-save-report/references/columns_content.md +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-save-report/references/columns_sponsorships.md +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-save-report/references/intelligence_filterset_schema.json +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-save-report/references/intelligence_widget_schema.json +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-save-report/references/report_glossary.md +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-save-report/references/sortable_columns.json +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-save-report/references/sponsorship_filterset_schema.json +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-save-report/references/sponsorship_widget_schema.json +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-save-report/references/widgets.md +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-top-partnerships/SKILL.md +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-top-partnerships/scripts/top_partnerships.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-views-guarantee/SKILL.md +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/skills/tl-views-guarantee/scripts/vg.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/src/tl_cli/_completions.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/src/tl_cli/_typer_utils.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/src/tl_cli/auth/__init__.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/src/tl_cli/auth/commands.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/src/tl_cli/auth/login.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/src/tl_cli/auth/pkce.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/src/tl_cli/auth/token_store.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/src/tl_cli/client/__init__.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/src/tl_cli/client/errors.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/src/tl_cli/commands/__init__.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/src/tl_cli/commands/_comments_common.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/src/tl_cli/commands/balance.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/src/tl_cli/commands/brands.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/src/tl_cli/commands/bulk_import.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/src/tl_cli/commands/changelog.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/src/tl_cli/commands/channels.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/src/tl_cli/commands/credits.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/src/tl_cli/commands/db.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/src/tl_cli/commands/deals.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/src/tl_cli/commands/describe.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/src/tl_cli/commands/doctor.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/src/tl_cli/commands/matches.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/src/tl_cli/commands/profiles.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/src/tl_cli/commands/proposals.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/src/tl_cli/commands/recommender.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/src/tl_cli/commands/schema.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/src/tl_cli/commands/setup.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/src/tl_cli/commands/skills.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/src/tl_cli/commands/snapshots.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/src/tl_cli/commands/sponsorships.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/src/tl_cli/commands/uploads.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/src/tl_cli/commands/workflows.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/src/tl_cli/config.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/src/tl_cli/filters.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/src/tl_cli/hints.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/src/tl_cli/output/__init__.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/src/tl_cli/query_history.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/src/tl_cli/self_update.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/src/tl_cli/skill_registry.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/tests/__init__.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/tests/test_auth.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/tests/test_brands_winner_channels.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/tests/test_build_report.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/tests/test_channels_lookalike.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/tests/test_db_repeat_warning.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/tests/test_describe.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/tests/test_error_hints.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/tests/test_expand_entities.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/tests/test_filters.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/tests/test_http_auth.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/tests/test_probe.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/tests/test_profiles.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/tests/test_scripts_executable.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/tests/test_search_channels.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/tests/test_search_videos.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/tests/test_select_keywords.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/tests/test_setup.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/tests/test_setup_marker_respect.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/tests/test_skill_registry.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/tests/test_skills_command.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/tests/test_sponsorships.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/tests/test_workflows.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/tests_cli/AGENTS.md +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/tests_cli/conftest.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/tests_cli/test_balance.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/tests_cli/test_db_es.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/tests_cli/test_db_fb.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/tests_cli/test_db_pg.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/tests_cli/test_schema.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/tests_cli/test_whoami.py +0 -0
- {thoughtleaders_cli-0.9.4 → thoughtleaders_cli-0.9.7}/uv.lock +0 -0
|
@@ -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
|
|
@@ -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)
|
|
@@ -261,6 +263,66 @@ clear. This is currently the only editable profile field; anything else returns
|
|
|
261
263
|
the editable surface. Use it for account-handling notes meant for the internal team — it is
|
|
262
264
|
never shown to the customer.
|
|
263
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
|
+
|
|
264
326
|
### Creating and vetting sponsorships
|
|
265
327
|
|
|
266
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.
|
{thoughtleaders_cli-0.9.4 → 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.
|
|
@@ -26,8 +26,8 @@ class TLClient:
|
|
|
26
26
|
def get(self, path: str, params: dict | None = None, timeout: float | None = None) -> dict:
|
|
27
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)
|
|
@@ -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()
|
|
@@ -17,6 +17,18 @@ from tl_cli.output.formatter import detect_format
|
|
|
17
17
|
app = typer.Typer(cls=AlphaSortedTyperGroup, help="Show current user, profile, org, and brands (free)")
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
def _newsletter_subscribed(data: dict) -> bool | None:
|
|
21
|
+
"""Industry Weekly membership, or None when the response doesn't say.
|
|
22
|
+
|
|
23
|
+
The field is absent whenever membership can't be determined, so None means
|
|
24
|
+
unknown — callers must not render it as a "no".
|
|
25
|
+
"""
|
|
26
|
+
newsletter = data.get("newsletter")
|
|
27
|
+
if not isinstance(newsletter, dict) or "subscribed" not in newsletter:
|
|
28
|
+
return None
|
|
29
|
+
return bool(newsletter["subscribed"])
|
|
30
|
+
|
|
31
|
+
|
|
20
32
|
def _render_whoami(data: dict) -> None:
|
|
21
33
|
"""Rich-formatted whoami output."""
|
|
22
34
|
console = Console()
|
|
@@ -53,6 +65,16 @@ def _render_whoami(data: dict) -> None:
|
|
|
53
65
|
lines.append("\n")
|
|
54
66
|
lines.append("Joined: ", style="dim")
|
|
55
67
|
lines.append(user.get("date_joined", "")[:10])
|
|
68
|
+
# Unknown membership prints no row at all, rather than a "no" the reader
|
|
69
|
+
# would take as fact.
|
|
70
|
+
subscribed = _newsletter_subscribed(data)
|
|
71
|
+
if subscribed is not None:
|
|
72
|
+
lines.append("\n")
|
|
73
|
+
lines.append("Weekly: ", style="dim")
|
|
74
|
+
lines.append(
|
|
75
|
+
"subscribed" if subscribed else "not subscribed",
|
|
76
|
+
style="green" if subscribed else "yellow",
|
|
77
|
+
)
|
|
56
78
|
|
|
57
79
|
console.print(Panel(lines, title=title, border_style="cyan"))
|
|
58
80
|
|
|
@@ -141,6 +163,9 @@ def _render_whoami_md(data: dict) -> None:
|
|
|
141
163
|
print(f"- **Flags:** {', '.join(flags)}")
|
|
142
164
|
print(f"- **Paid:** {'yes' if profile.get('is_paid') else 'no'}")
|
|
143
165
|
print(f"- **Joined:** {user.get('date_joined', '')[:10]}")
|
|
166
|
+
subscribed = _newsletter_subscribed(data)
|
|
167
|
+
if subscribed is not None:
|
|
168
|
+
print(f"- **Industry Weekly:** {'subscribed' if subscribed else 'not subscribed'}")
|
|
144
169
|
|
|
145
170
|
print(f"\n## Organization: {org.get('name', '')}\n")
|
|
146
171
|
plan = org.get("plan")
|
|
@@ -22,6 +22,7 @@ from tl_cli.commands.channels import app as channels_app
|
|
|
22
22
|
from tl_cli.commands.db import app as db_app
|
|
23
23
|
from tl_cli.commands.deals import app as deals_app
|
|
24
24
|
from tl_cli.commands.matches import app as matches_app
|
|
25
|
+
from tl_cli.commands.memory import app as memory_app
|
|
25
26
|
from tl_cli.commands.profiles import app as profiles_app
|
|
26
27
|
from tl_cli.commands.proposals import app as proposals_app
|
|
27
28
|
from tl_cli.commands.recommender import app as recommender_app
|
|
@@ -104,6 +105,7 @@ app.add_typer(deals_app, name="deals")
|
|
|
104
105
|
app.add_typer(uploads_app, name="uploads")
|
|
105
106
|
app.add_typer(channels_app, name="channels")
|
|
106
107
|
app.add_typer(brands_app, name="brands")
|
|
108
|
+
app.add_typer(memory_app, name="memory")
|
|
107
109
|
app.add_typer(profiles_app, name="profiles")
|
|
108
110
|
app.add_typer(recommender_app, name="recommender")
|
|
109
111
|
app.add_typer(snapshots_app, name="snapshots")
|
|
@@ -14,6 +14,7 @@ import sys
|
|
|
14
14
|
|
|
15
15
|
from pytoon import encode as toon_encode
|
|
16
16
|
from rich.console import Console
|
|
17
|
+
from rich.markup import escape as rich_escape
|
|
17
18
|
from rich.table import Table
|
|
18
19
|
|
|
19
20
|
# Stderr console for status messages (never pollutes piped data)
|
|
@@ -374,6 +375,12 @@ def _output_detail(record: dict) -> None:
|
|
|
374
375
|
If a value is a non-empty list of dicts, it's rendered as an indented
|
|
375
376
|
sub-table beneath its label instead of stringified. Empty lists show
|
|
376
377
|
`(none)` to signal "no entries" explicitly rather than printing `[]`.
|
|
378
|
+
|
|
379
|
+
Field names and values are both data, never markup: a square-bracketed
|
|
380
|
+
token ("[/finance]" in a free-text note, a `SELECT ... AS "[x]"` alias) is
|
|
381
|
+
otherwise read as a Rich tag and either swallows the word or aborts the
|
|
382
|
+
whole command with a markup error. Everything interpolated below is
|
|
383
|
+
escaped; only the styling this function itself adds stays live.
|
|
377
384
|
"""
|
|
378
385
|
console = Console()
|
|
379
386
|
nested_items = [(k, v) for k, v in record.items() if _is_list_of_dicts(v)]
|
|
@@ -388,30 +395,35 @@ def _output_detail(record: dict) -> None:
|
|
|
388
395
|
display = json.dumps(value, default=str)
|
|
389
396
|
else:
|
|
390
397
|
display = value
|
|
391
|
-
label
|
|
392
|
-
|
|
398
|
+
# Escape the padded label, not the bare key: the escape only adds
|
|
399
|
+
# backslashes Rich consumes, so the rendered width still lines up.
|
|
400
|
+
label = f"[bold]{rich_escape(f'{key:<{max_key_len}}')}[/bold]"
|
|
401
|
+
console.print(f" {label} {rich_escape(str(display))}")
|
|
393
402
|
|
|
394
403
|
for key, rows in nested_items:
|
|
395
|
-
console.print(f"\n [bold]{key}[/bold] ({len(rows)}):")
|
|
404
|
+
console.print(f"\n [bold]{rich_escape(key)}[/bold] ({len(rows)}):")
|
|
396
405
|
sub_cols = list(rows[0].keys())
|
|
397
406
|
sub_table = Table(show_header=True, padding=(0, 1))
|
|
398
407
|
for col in sub_cols:
|
|
399
408
|
kwargs: dict = {"overflow": "ellipsis", "max_width": 40}
|
|
400
409
|
if col in _RIGHT_ALIGN_COLS:
|
|
401
410
|
kwargs["justify"] = "right"
|
|
402
|
-
sub_table.add_column(col, **kwargs)
|
|
411
|
+
sub_table.add_column(rich_escape(col), **kwargs)
|
|
403
412
|
for row in rows:
|
|
404
413
|
sub_table.add_row(*[_format_cell(row.get(col)) for col in sub_cols])
|
|
405
414
|
console.print(sub_table)
|
|
406
415
|
|
|
407
416
|
for key in empty_list_items:
|
|
408
|
-
console.print(f"\n [bold]{key}[/bold]: [dim](none)[/dim]")
|
|
417
|
+
console.print(f"\n [bold]{rich_escape(key)}[/bold]: [dim](none)[/dim]")
|
|
409
418
|
|
|
410
419
|
|
|
411
420
|
def _format_cell(value: object) -> str:
|
|
421
|
+
"""Render one sub-table cell. Escaped for the same reason the detail rows are:
|
|
422
|
+
Rich reads markup in table cells too, so a bracketed token in free text would
|
|
423
|
+
vanish or abort the render."""
|
|
412
424
|
if value is None:
|
|
413
425
|
return ""
|
|
414
|
-
return _truncate(str(value), 40)
|
|
426
|
+
return rich_escape(_truncate(str(value), 40))
|
|
415
427
|
|
|
416
428
|
|
|
417
429
|
def _output_detail_csv(record: dict) -> None:
|