thoughtleaders-cli 0.6.51__tar.gz → 0.6.52__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.
Files changed (93) hide show
  1. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/.claude-plugin/plugin.json +1 -1
  2. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/AGENTS.md +1 -1
  3. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/PKG-INFO +1 -1
  4. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/pyproject.toml +1 -1
  5. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/skills/tl/SKILL.md +95 -6
  6. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/src/tl_cli/__init__.py +1 -1
  7. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/src/tl_cli/commands/bulk_import.py +5 -1
  8. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/src/tl_cli/commands/credits.py +12 -1
  9. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/src/tl_cli/commands/reports.py +13 -6
  10. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/src/tl_cli/commands/schema.py +11 -4
  11. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/src/tl_cli/commands/setup.py +27 -9
  12. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/src/tl_cli/commands/whoami.py +2 -2
  13. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/src/tl_cli/output/formatter.py +3 -4
  14. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/.claude-plugin/marketplace.json +0 -0
  15. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/.github/workflows/python-publish.yml +0 -0
  16. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/.gitignore +0 -0
  17. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/API.md +0 -0
  18. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/CLAUDE.md +0 -0
  19. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/LICENSE +0 -0
  20. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/README.md +0 -0
  21. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/agents/tl-analyst.md +0 -0
  22. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/hooks/hooks.json +0 -0
  23. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/hooks/scripts/load-tl-skill.mjs +0 -0
  24. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/hooks/scripts/post-usage.sh +0 -0
  25. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/hooks/scripts/pre-check.sh +0 -0
  26. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/skills/tl/references/business-glossary.md +0 -0
  27. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/skills/tl/references/elasticsearch-schema.md +0 -0
  28. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/skills/tl/references/firebolt-schema.md +0 -0
  29. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/skills/tl/references/postgres-schema.md +0 -0
  30. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/skills/tl-import/SKILL.md +0 -0
  31. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/skills/tl-keyword-research/SKILL.md +0 -0
  32. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/skills/tl-keyword-research/scripts/probe.py +0 -0
  33. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/skills/tl-report-builder/SKILL.md +0 -0
  34. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/skills/tl-report-builder/examples/e2e_findings.md +0 -0
  35. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/skills/tl-report-builder/examples/golden_queries.md +0 -0
  36. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/skills/tl-report-builder/references/columns_brands.md +0 -0
  37. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/skills/tl-report-builder/references/columns_channels.md +0 -0
  38. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/skills/tl-report-builder/references/columns_content.md +0 -0
  39. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/skills/tl-report-builder/references/columns_sponsorships.md +0 -0
  40. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/skills/tl-report-builder/references/intelligence_filterset_schema.json +0 -0
  41. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/skills/tl-report-builder/references/intelligence_widget_schema.json +0 -0
  42. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/skills/tl-report-builder/references/report_glossary.md +0 -0
  43. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/skills/tl-report-builder/references/sortable_columns.json +0 -0
  44. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/skills/tl-report-builder/references/sponsorship_filterset_schema.json +0 -0
  45. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/skills/tl-report-builder/references/sponsorship_widget_schema.json +0 -0
  46. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/skills/tl-report-builder/references/widgets.md +0 -0
  47. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/skills/tl-report-builder/tools/column_builder.md +0 -0
  48. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/skills/tl-report-builder/tools/database_query.md +0 -0
  49. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/skills/tl-report-builder/tools/name_resolver.md +0 -0
  50. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/skills/tl-report-builder/tools/sample_judge.md +0 -0
  51. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/skills/tl-report-builder/tools/similar_channels.md +0 -0
  52. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/skills/tl-report-builder/tools/topic_matcher.md +0 -0
  53. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/skills/tl-report-builder/tools/widget_builder.md +0 -0
  54. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/src/tl_cli/_completions.py +0 -0
  55. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/src/tl_cli/auth/__init__.py +0 -0
  56. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/src/tl_cli/auth/commands.py +0 -0
  57. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/src/tl_cli/auth/finalize.py +0 -0
  58. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/src/tl_cli/auth/login.py +0 -0
  59. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/src/tl_cli/auth/pkce.py +0 -0
  60. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/src/tl_cli/auth/token_store.py +0 -0
  61. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/src/tl_cli/client/__init__.py +0 -0
  62. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/src/tl_cli/client/errors.py +0 -0
  63. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/src/tl_cli/client/http.py +0 -0
  64. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/src/tl_cli/commands/__init__.py +0 -0
  65. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/src/tl_cli/commands/_comments_common.py +0 -0
  66. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/src/tl_cli/commands/balance.py +0 -0
  67. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/src/tl_cli/commands/brands.py +0 -0
  68. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/src/tl_cli/commands/changelog.py +0 -0
  69. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/src/tl_cli/commands/channels.py +0 -0
  70. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/src/tl_cli/commands/db.py +0 -0
  71. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/src/tl_cli/commands/deals.py +0 -0
  72. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/src/tl_cli/commands/describe.py +0 -0
  73. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/src/tl_cli/commands/doctor.py +0 -0
  74. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/src/tl_cli/commands/matches.py +0 -0
  75. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/src/tl_cli/commands/proposals.py +0 -0
  76. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/src/tl_cli/commands/recommender.py +0 -0
  77. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/src/tl_cli/commands/snapshots.py +0 -0
  78. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/src/tl_cli/commands/sponsorships.py +0 -0
  79. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/src/tl_cli/commands/uploads.py +0 -0
  80. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/src/tl_cli/config.py +0 -0
  81. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/src/tl_cli/filters.py +0 -0
  82. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/src/tl_cli/hints.py +0 -0
  83. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/src/tl_cli/main.py +0 -0
  84. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/src/tl_cli/output/__init__.py +0 -0
  85. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/src/tl_cli/self_update.py +0 -0
  86. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/tests/__init__.py +0 -0
  87. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/tests/test_auth.py +0 -0
  88. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/tests/test_filters.py +0 -0
  89. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/tests/test_http_auth.py +0 -0
  90. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/tests/test_output.py +0 -0
  91. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/tests/test_reports.py +0 -0
  92. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/tests/test_sponsorships.py +0 -0
  93. {thoughtleaders_cli-0.6.51 → thoughtleaders_cli-0.6.52}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tl-cli",
3
- "version": "0.6.51",
3
+ "version": "0.6.52",
4
4
  "description": "ThoughtLeaders CLI — query sponsorship deals, channels, brands, uploads, and intelligence from the terminal",
5
5
  "author": {
6
6
  "name": "ThoughtLeaders",
@@ -117,7 +117,7 @@ The version string is defined in three files and all three must be updated toget
117
117
 
118
118
  * Do not reference internal architecture of the ThoughtLeaders app in comments or skills. Specifially: do not reference internal table names, field names, API endpoints, Python modules or functions (including the sanitizer).
119
119
  * Do not let server implementation details into skill files (anything under `skills/`). Skills describe *what the CLI does* from the user's seat — observable command surface, inputs, outputs, examples. Do not say "the server enforces X", "the API validates Y on its side", "the backend rejects Z" — those are mechanism notes that drift the moment the server changes. State the user-visible behaviour ("unknown keys come back as 400") without naming where it's enforced.
120
- * Place all imports at the start of the Python module file
120
+ * **All `import` and `from X import Y` statements live at the top of the Python module file** — after the module docstring, before any code. No inline imports inside function bodies, no lazy imports for "speed" or "optional dependency" reasons. `from __future__ import …` goes at the very top (Python requires that). The only legitimate inline-import exception is **platform-conditional imports** that cannot succeed on the other platform (e.g. `import msvcrt` on Linux, `import termios`/`tty` on Windows) — those stay inside their `if sys.platform == …:` guard. If a circular-import problem makes a top-level import impossible, fix the circular dependency rather than working around it with an inline import.
121
121
 
122
122
  # Git commit rules
123
123
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: thoughtleaders-cli
3
- Version: 0.6.51
3
+ Version: 0.6.52
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "thoughtleaders-cli"
7
- version = "0.6.51"
7
+ version = "0.6.52"
8
8
  description = "ThoughtLeaders CLI — query sponsorship data, channels, brands, and intelligence"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -181,10 +181,6 @@ tl channels history <id-or-name> # Sponsorship history
181
181
  tl channels similar <id-or-name> # Similarity recommender (Intelligence plan)
182
182
  tl brands show <id-or-name> # Brand detail
183
183
  tl brands find <query> # Resolve a string to {id, name}; matches name, slug, domain, or keyword
184
- tl brands history <id-or-name> # Sponsorship history
185
- tl brands history <query> --channel <id> # Brand mentions on specific channel
186
- tl brands history-stats <id-or-name> # Aggregate roll-up: counts, total/avg/median views, first/last seen, by-year, top channels
187
- tl brands history-stats <q> --channel <id> # Same roll-up, narrowed to one channel
188
184
  tl brands similar <id-or-name> # Find similar brands via similarity search
189
185
  tl recommender tags [query] # List similarity tag names — categories, demographics, formats
190
186
  tl recommender top-channels "<tag>" # Top channels loaded on a similarity tag (Intelligence)
@@ -595,9 +591,102 @@ tl db pg "SELECT al.id, al.weighted_price, al.purchase_date, b.name AS brand
595
591
  LIMIT 500 OFFSET 0" --json
596
592
  ```
597
593
 
598
- ### "What channels does Nike sponsor?":
594
+ ### Brand sponsorship history — what channels does Nike sponsor?
595
+
596
+ Resolve the brand to an ID, then probe ES for articles where the brand appears in `sponsored_brand_mentions`. Channel names live in PG (the ES article doc only carries `channel.id`), so the third call joins them in.
597
+
598
+ ```bash
599
+ # 1. Resolve "Nike" → brand ID
600
+ tl brands find Nike --json # → results[0].id, say 21416
601
+
602
+ # 2. Recent sponsored videos for that brand (sorted by publication_date desc)
603
+ tl db es '{
604
+ "size": 50,
605
+ "track_total_hits": true,
606
+ "query": {"bool": {"filter": [
607
+ {"term": {"doc_type": "article"}},
608
+ {"term": {"sponsored_brand_mentions": "21416"}}
609
+ ]}},
610
+ "sort": [{"publication_date": "desc"}],
611
+ "_source": ["title", "channel.id", "publication_date", "views"]
612
+ }' --json > /tmp/nike_history.json
613
+
614
+ # 3. Resolve channel.id → channel_name (one PG round-trip for the whole page)
615
+ jq -r '[.results[].channel.id] | unique | map(tostring) | join(",")' /tmp/nike_history.json \
616
+ | xargs -I CH_IDS tl db pg "SELECT id, channel_name FROM thoughtleaders_channel WHERE id IN (CH_IDS)" --json
617
+
618
+ # Narrow to a single channel:
619
+ tl db es '{
620
+ "size": 50,
621
+ "track_total_hits": true,
622
+ "query": {"bool": {"filter": [
623
+ {"term": {"doc_type": "article"}},
624
+ {"term": {"sponsored_brand_mentions": "21416"}},
625
+ {"term": {"channel.id": 5607}}
626
+ ]}},
627
+ "sort": [{"publication_date": "desc"}],
628
+ "_source": ["title", "publication_date", "views"]
629
+ }'
630
+
631
+ # Was the video a TL-brokered deal? Cross-check ES video_id against AdLink.article_id:
632
+ tl db pg "SELECT article_id FROM thoughtleaders_adlink
633
+ WHERE article_id IN ('1247603:8LskGvKUA9I', '1247603:abc123')"
634
+ ```
635
+
636
+ ### Brand sponsorship roll-up — totals, first/last seen, top channels, by-year
637
+
638
+ The same ES filter (`doc_type=article` + `sponsored_brand_mentions=<id>`) with `size:0` + aggregations replaces a roll-up call. ES accepts **one aggregation total per request** (top-level + sub-aggs all count), so what would be a single server-side roll-up here splits into a few `tl db es` calls and one client-side join.
639
+
599
640
  ```bash
600
- tl brands history Nike --json
641
+ # Totals + time range (one aggregation total — the four metric aggs are siblings under aggs and bill as a single body)
642
+ tl db es '{
643
+ "size": 0,
644
+ "track_total_hits": true,
645
+ "query": {"bool": {"filter": [
646
+ {"term": {"doc_type": "article"}},
647
+ {"term": {"sponsored_brand_mentions": "21416"}}
648
+ ]}},
649
+ "aggs": {
650
+ "views_sum": {"sum": {"field": "views"}},
651
+ "views_avg": {"avg": {"field": "views"}},
652
+ "first_seen": {"min": {"field": "publication_date"}},
653
+ "last_seen": {"max": {"field": "publication_date"}}
654
+ }
655
+ }'
656
+
657
+ # By-year breakdown (date_histogram only — no sub-agg, that would push over the one-agg cap)
658
+ tl db es '{
659
+ "size": 0,
660
+ "query": {"bool": {"filter": [
661
+ {"term": {"doc_type": "article"}},
662
+ {"term": {"sponsored_brand_mentions": "21416"}}
663
+ ]}},
664
+ "aggs": {
665
+ "by_year": {"date_histogram": {
666
+ "field": "publication_date", "calendar_interval": "year",
667
+ "format": "yyyy", "min_doc_count": 1
668
+ }}
669
+ }
670
+ }'
671
+
672
+ # Top channels by sponsored-video count (terms agg only — for views per channel, run a second call per channel)
673
+ tl db es '{
674
+ "size": 0,
675
+ "query": {"bool": {"filter": [
676
+ {"term": {"doc_type": "article"}},
677
+ {"term": {"sponsored_brand_mentions": "21416"}}
678
+ ]}},
679
+ "aggs": {
680
+ "by_channel": {"terms": {"field": "channel.id", "size": 10, "order": {"_count": "desc"}}}
681
+ }
682
+ }'
683
+
684
+ # TL-brokered deal count for the brand (PG, not ES — adlinks where the brand is on the creator profile)
685
+ tl db pg "SELECT COUNT(*) AS tl_brokered
686
+ FROM thoughtleaders_adlink al
687
+ JOIN thoughtleaders_profile p ON p.id = al.creator_profile_id
688
+ JOIN thoughtleaders_profile_brands pb ON pb.profile_id = p.id
689
+ WHERE pb.brand_id = 21416 AND al.article_id IS NOT NULL"
601
690
  ```
602
691
 
603
692
  ### "Compare view curves for two videos":
@@ -1,3 +1,3 @@
1
1
  """ThoughtLeaders CLI — query sponsorship data, channels, brands, and intelligence."""
2
2
 
3
- __version__ = "0.6.51"
3
+ __version__ = "0.6.52"
@@ -11,6 +11,7 @@ import time
11
11
  from pathlib import Path
12
12
 
13
13
  import typer
14
+ from pytoon import encode as toon_encode
14
15
  from rich.console import Console
15
16
 
16
17
  from tl_cli.client.errors import ApiError, handle_api_error
@@ -59,6 +60,7 @@ def bulk_import_command(
59
60
  ids_file: str | None = typer.Option(None, "--ids-file", "-f", help="Path to file with one identifier per line. Omit to read from stdin."),
60
61
  exclude: bool = typer.Option(False, "--exclude", help="Mark these identifiers as excluded from the report instead of included"),
61
62
  json_output: bool = typer.Option(False, "--json", help="JSON output (default)"),
63
+ toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
62
64
  ) -> None:
63
65
  """Bulk-import entities into a report.
64
66
 
@@ -113,7 +115,9 @@ def bulk_import_command(
113
115
  client.close()
114
116
 
115
117
  output = {"task_id": task_id, **result}
116
- if json_output or not sys.stdout.isatty():
118
+ if toon_output:
119
+ sys.stdout.write(toon_encode(output) + "\n")
120
+ elif json_output or not sys.stdout.isatty():
117
121
  json.dump(output, sys.stdout, indent=2)
118
122
  sys.stdout.write("\n")
119
123
  else:
@@ -18,6 +18,7 @@ import webbrowser
18
18
  from decimal import Decimal, InvalidOperation
19
19
 
20
20
  import typer
21
+ from pytoon import encode as toon_encode
21
22
  from rich.console import Console
22
23
  from rich.prompt import Prompt
23
24
  from rich.table import Table
@@ -34,6 +35,7 @@ err = Console(stderr=True)
34
35
  @app.command("pricing")
35
36
  def pricing_cmd(
36
37
  json_output: bool = typer.Option(False, "--json", help="JSON output"),
38
+ toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
37
39
  ) -> None:
38
40
  """Show the credit-to-USD rate, minimum purchase, and starter balance.
39
41
 
@@ -48,6 +50,10 @@ def pricing_cmd(
48
50
  finally:
49
51
  client.close()
50
52
 
53
+ if toon_output:
54
+ print(toon_encode(data))
55
+ return
56
+
51
57
  if json_output:
52
58
  print(json.dumps(data, indent=2, default=str))
53
59
  return
@@ -150,9 +156,10 @@ def history_cmd(
150
156
  limit: int = typer.Option(25, "--limit", help="Max rows to show"),
151
157
  offset: int = typer.Option(0, "--offset", help="Offset for pagination"),
152
158
  json_output: bool = typer.Option(False, "--json", help="JSON output"),
159
+ toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
153
160
  ) -> None:
154
161
  """Show recent credit top-ups for your organization (free)."""
155
- fmt = detect_format(json_output, False, False, False)
162
+ fmt = detect_format(json_output, False, False, toon_output)
156
163
  client = get_client()
157
164
  try:
158
165
  data = client.get("/credit-purchases", params={"limit": limit, "offset": offset})
@@ -166,6 +173,10 @@ def history_cmd(
166
173
  print(json.dumps(data, indent=2, default=str))
167
174
  return
168
175
 
176
+ if fmt == "toon":
177
+ print(toon_encode(data.get("results", [])))
178
+ return
179
+
169
180
  rows = data.get("results", [])
170
181
  if not rows:
171
182
  console.print("[dim]No credit purchases yet. Run `tl credits buy --amount-usd N`.[/dim]")
@@ -4,6 +4,7 @@ import json
4
4
  import time
5
5
 
6
6
  import typer
7
+ from pytoon import encode as toon_encode
7
8
  from rich.console import Console
8
9
  from rich.panel import Panel
9
10
  from rich.text import Text
@@ -320,6 +321,7 @@ def create_report(
320
321
  ),
321
322
  yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
322
323
  json_output: bool = typer.Option(False, "--json", help="Output raw JSON config"),
324
+ toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
323
325
  timeout: int = typer.Option(300, "--timeout", help="Max orchestration time in seconds"),
324
326
  ) -> None:
325
327
  """Create a report from a natural-language prompt or a pre-built config.
@@ -366,14 +368,17 @@ def create_report(
366
368
  saved_prompts = [prompt]
367
369
 
368
370
  # --- Show preview ---
369
- if json_output:
370
- # Preview-only mode (--json without --yes): print the config and exit.
371
+ if json_output or toon_output:
372
+ # Preview-only mode (--json/--toon without --yes): print the config and exit.
371
373
  # With --yes, skip the preview emit entirely so the only thing on
372
- # stdout is the single save-response JSON below — programmatic
373
- # consumers can parse stdout with one json.loads() instead of having
374
+ # stdout is the single save-response payload below — programmatic
375
+ # consumers can parse stdout with one parser call instead of having
374
376
  # to split two documents.
375
377
  if not yes:
376
- print(json.dumps(config, indent=2, default=str))
378
+ if toon_output:
379
+ print(toon_encode(config))
380
+ else:
381
+ print(json.dumps(config, indent=2, default=str))
377
382
  raise typer.Exit(0)
378
383
  else:
379
384
  err.print()
@@ -398,7 +403,9 @@ def create_report(
398
403
  report_url = result.get("report_url", "")
399
404
  campaign_id = result.get("campaign_id", "")
400
405
 
401
- if json_output:
406
+ if toon_output:
407
+ print(toon_encode(data))
408
+ elif json_output:
402
409
  print(json.dumps(data, indent=2, default=str))
403
410
  else:
404
411
  err.print()
@@ -5,6 +5,7 @@ import re
5
5
 
6
6
  import typer
7
7
  import yaml
8
+ from pytoon import encode as toon_encode
8
9
  from rich.console import Console
9
10
  from rich.markdown import Markdown
10
11
  from rich.text import Text
@@ -79,11 +80,14 @@ def _try_render_yaml_tree(content: str) -> bool:
79
80
  return rendered
80
81
 
81
82
 
82
- def _show(db: str, json_output: bool, table: str | None = None) -> None:
83
+ def _show(db: str, json_output: bool, table: str | None = None, toon_output: bool = False) -> None:
83
84
  client = get_client()
84
85
  try:
85
86
  params = {"table": table} if table else {}
86
87
  data = client.get(f"/raw/{db}/schema", params=params)
88
+ if toon_output:
89
+ print(toon_encode(data))
90
+ return
87
91
  if json_output:
88
92
  print(json.dumps(data, indent=2, default=str))
89
93
  return
@@ -108,6 +112,7 @@ def _show(db: str, json_output: bool, table: str | None = None) -> None:
108
112
  def pg_cmd(
109
113
  table: str = typer.Argument(None, help="Optional table name. When given, prints only that table's section in the same markdown format."),
110
114
  json_output: bool = typer.Option(False, "--json", help="JSON output"),
115
+ toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
111
116
  ) -> None:
112
117
  """Show PostgreSQL schema reference (for `tl db pg`).
113
118
 
@@ -123,13 +128,14 @@ def pg_cmd(
123
128
  tl schema pg thoughtleaders_channel
124
129
  tl schema pg thoughtleaders_adlink --json
125
130
  """
126
- _show("pg", json_output, table=table)
131
+ _show("pg", json_output, table=table, toon_output=toon_output)
127
132
 
128
133
 
129
134
  @app.command("fb")
130
135
  def fb_cmd(
131
136
  table: str = typer.Argument(None, help="Optional table name (`article_metrics` or `channel_metrics`). When given, prints only that table's section."),
132
137
  json_output: bool = typer.Option(False, "--json", help="JSON output"),
138
+ toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
133
139
  ) -> None:
134
140
  """Show Firebolt schema (live: tables and column types) for `tl db fb`.
135
141
 
@@ -144,12 +150,13 @@ def fb_cmd(
144
150
  tl schema fb article_metrics
145
151
  tl schema fb channel_metrics --json
146
152
  """
147
- _show("fb", json_output, table=table)
153
+ _show("fb", json_output, table=table, toon_output=toon_output)
148
154
 
149
155
 
150
156
  @app.command("es")
151
157
  def es_cmd(
152
158
  json_output: bool = typer.Option(False, "--json", help="JSON output"),
159
+ toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
153
160
  ) -> None:
154
161
  """Show Elasticsearch document shape for `tl db es`."""
155
- _show("es", json_output)
162
+ _show("es", json_output, toon_output=toon_output)
@@ -13,6 +13,7 @@ import subprocess
13
13
  from pathlib import Path
14
14
 
15
15
  import typer
16
+ from pytoon import encode as toon_encode
16
17
  from rich.console import Console
17
18
 
18
19
  from tl_cli import __version__
@@ -172,6 +173,7 @@ def _print_manual_instructions() -> None:
172
173
  @app.command("claude")
173
174
  def setup_claude(
174
175
  json_output: bool = typer.Option(False, "--json", help="JSON output (non-interactive)"),
176
+ toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs, non-interactive)"),
175
177
  ) -> None:
176
178
  """Install the TL CLI plugin for Claude Code.
177
179
 
@@ -183,8 +185,8 @@ def setup_claude(
183
185
  tl setup claude
184
186
  tl setup claude --json
185
187
  """
186
- if json_output:
187
- _setup_noninteractive()
188
+ if json_output or toon_output:
189
+ _setup_noninteractive(fmt="toon" if toon_output else "json")
188
190
  return
189
191
 
190
192
  console.print()
@@ -283,8 +285,16 @@ def _install_standalone_skills_step(plugin_root: Path) -> None:
283
285
  console.print(" [yellow]![/yellow] No skills found to install")
284
286
 
285
287
 
286
- def _setup_noninteractive() -> None:
287
- """Non-interactive setup for --json/agent usage."""
288
+ def _emit_setup_result(result: dict, fmt: str) -> None:
289
+ """Emit a setup-status dict in JSON (default) or TOON."""
290
+ if fmt == "toon":
291
+ print(toon_encode(result))
292
+ else:
293
+ print(json.dumps(result, indent=2))
294
+
295
+
296
+ def _setup_noninteractive(fmt: str = "json") -> None:
297
+ """Non-interactive setup for --json / --toon / agent usage."""
288
298
  result = {
289
299
  "cli_version": __version__,
290
300
  "marketplace_source": MARKETPLACE_SOURCE,
@@ -296,7 +306,7 @@ def _setup_noninteractive() -> None:
296
306
  if plugin_root is None:
297
307
  result["status"] = "error"
298
308
  result["error"] = "Plugin assets not found"
299
- print(json.dumps(result, indent=2))
309
+ _emit_setup_result(result, fmt)
300
310
  raise SystemExit(1)
301
311
 
302
312
  claude_bin = _find_claude_binary()
@@ -326,7 +336,7 @@ def _setup_noninteractive() -> None:
326
336
  (version_dir / ".version").write_text(__version__)
327
337
 
328
338
  result["status"] = "ok"
329
- print(json.dumps(result, indent=2))
339
+ _emit_setup_result(result, fmt)
330
340
 
331
341
 
332
342
  # --- OpenCode setup ---
@@ -375,6 +385,7 @@ def _setup_external_agent(
375
385
  target_dir: Path,
376
386
  post_install_lines: list[str] | None,
377
387
  json_output: bool,
388
+ toon_output: bool = False,
378
389
  ) -> None:
379
390
  """Shared body for the OpenCode / Gemini / Codex setup commands.
380
391
 
@@ -388,19 +399,20 @@ def _setup_external_agent(
388
399
  """
389
400
  plugin_root = _find_plugin_root()
390
401
 
391
- if json_output:
402
+ if json_output or toon_output:
403
+ fmt = "toon" if toon_output else "json"
392
404
  result: dict = {"cli_version": __version__}
393
405
  if plugin_root is None:
394
406
  result["status"] = "error"
395
407
  result["error"] = "Plugin assets not found"
396
- print(json.dumps(result, indent=2))
408
+ _emit_setup_result(result, fmt)
397
409
  raise SystemExit(1)
398
410
  result[f"{agent_binary}_detected"] = shutil.which(agent_binary) is not None
399
411
  count = _install_skill_trees(plugin_root, target_dir)
400
412
  result["skills_installed"] = count
401
413
  result["install_dir"] = str(target_dir)
402
414
  result["status"] = "ok"
403
- print(json.dumps(result, indent=2))
415
+ _emit_setup_result(result, fmt)
404
416
  return
405
417
 
406
418
  console.print()
@@ -449,6 +461,7 @@ def _setup_external_agent(
449
461
  @app.command("opencode")
450
462
  def setup_opencode(
451
463
  json_output: bool = typer.Option(False, "--json", help="JSON output (non-interactive)"),
464
+ toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs, non-interactive)"),
452
465
  ) -> None:
453
466
  """Install the TL CLI skills for OpenCode.
454
467
 
@@ -469,6 +482,7 @@ def setup_opencode(
469
482
  "The agent can use it when you ask about sponsorships, deals, channels, or brands.",
470
483
  ],
471
484
  json_output=json_output,
485
+ toon_output=toon_output,
472
486
  )
473
487
 
474
488
 
@@ -478,6 +492,7 @@ def setup_opencode(
478
492
  @app.command("gemini")
479
493
  def setup_gemini(
480
494
  json_output: bool = typer.Option(False, "--json", help="JSON output (non-interactive)"),
495
+ toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs, non-interactive)"),
481
496
  ) -> None:
482
497
  """Install the TL CLI skills for the Gemini CLI.
483
498
 
@@ -496,12 +511,14 @@ def setup_gemini(
496
511
  target_dir=AGENTS_SKILLS_DIR,
497
512
  post_install_lines=None,
498
513
  json_output=json_output,
514
+ toon_output=toon_output,
499
515
  )
500
516
 
501
517
 
502
518
  @app.command("codex")
503
519
  def setup_codex(
504
520
  json_output: bool = typer.Option(False, "--json", help="JSON output (non-interactive)"),
521
+ toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs, non-interactive)"),
505
522
  ) -> None:
506
523
  """Install the TL CLI skills for the Codex CLI.
507
524
 
@@ -520,4 +537,5 @@ def setup_codex(
520
537
  target_dir=AGENTS_SKILLS_DIR,
521
538
  post_install_lines=None,
522
539
  json_output=json_output,
540
+ toon_output=toon_output,
523
541
  )
@@ -3,6 +3,7 @@
3
3
  import json
4
4
 
5
5
  import typer
6
+ from pytoon import encode as toon_encode
6
7
  from rich.console import Console
7
8
  from rich.panel import Panel
8
9
  from rich.table import Table
@@ -206,8 +207,7 @@ def whoami(
206
207
  if fmt == "json":
207
208
  print(json.dumps(data, indent=2, default=str))
208
209
  elif fmt == "toon":
209
- from pytoon import encode
210
- print(encode(data))
210
+ print(toon_encode(data))
211
211
  elif fmt == "md":
212
212
  _render_whoami_md(data)
213
213
  else:
@@ -11,6 +11,7 @@ import json
11
11
  import math
12
12
  import sys
13
13
 
14
+ from pytoon import encode as toon_encode
14
15
  from rich.console import Console
15
16
  from rich.table import Table
16
17
 
@@ -332,16 +333,14 @@ def _output_markdown(results: list[dict], columns: list[str], column_types: dict
332
333
 
333
334
  def _output_toon(results: list[dict], columns: list[str]) -> None:
334
335
  """TOON (Token-Oriented Object Notation) output for LLM consumption."""
335
- from pytoon import encode
336
336
  # Build column-filtered rows for uniform tabular encoding
337
337
  rows = [{col: row.get(col) for col in columns} for row in results]
338
- print(encode(rows))
338
+ print(toon_encode(rows))
339
339
 
340
340
 
341
341
  def _output_toon_single(record: dict) -> None:
342
342
  """TOON output for a single detail record."""
343
- from pytoon import encode
344
- print(encode(record))
343
+ print(toon_encode(record))
345
344
 
346
345
 
347
346
  _RIGHT_ALIGN_COLS = {"price", "cost", "cpm"}