thoughtleaders-cli 0.6.10__tar.gz → 0.6.12__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.6.10 → thoughtleaders_cli-0.6.12}/.claude-plugin/plugin.json +1 -1
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/AGENTS.md +1 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/PKG-INFO +1 -1
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/pyproject.toml +1 -1
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/skills/tl/SKILL.md +3 -11
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/src/tl_cli/__init__.py +1 -1
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/src/tl_cli/commands/channels.py +3 -9
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/src/tl_cli/commands/reports.py +134 -58
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/src/tl_cli/commands/sponsorships.py +3 -9
- thoughtleaders_cli-0.6.12/tests/test_reports.py +79 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/.claude-plugin/marketplace.json +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/.github/workflows/python-publish.yml +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/.gitignore +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/CLAUDE.md +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/LICENSE +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/README.md +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/agents/tl-analyst.md +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/commands/tl-balance.md +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/commands/tl-reports.md +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/commands/tl-sponsorships.md +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/commands/tl.md +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/docs/architecture.md +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/hooks/hooks.json +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/hooks/scripts/post-usage.sh +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/hooks/scripts/pre-check.sh +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/skills/tl/references/business-glossary.md +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/skills/tl/references/elasticsearch-schema.md +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/skills/tl/references/firebolt-schema.md +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/skills/tl/references/postgres-schema.md +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/skills/tl-report-builder/SKILL.md +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/skills/tl-report-builder/examples/e2e_findings.md +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/skills/tl-report-builder/examples/golden_queries.md +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/skills/tl-report-builder/references/columns_brands.md +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/skills/tl-report-builder/references/columns_channels.md +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/skills/tl-report-builder/references/columns_content.md +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/skills/tl-report-builder/references/columns_sponsorships.md +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/skills/tl-report-builder/references/intelligence_filterset_schema.json +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/skills/tl-report-builder/references/intelligence_widget_schema.json +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/skills/tl-report-builder/references/report_glossary.md +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/skills/tl-report-builder/references/sortable_columns.json +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/skills/tl-report-builder/references/sponsorship_filterset_schema.json +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/skills/tl-report-builder/references/sponsorship_widget_schema.json +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/skills/tl-report-builder/references/widgets.md +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/skills/tl-report-builder/tools/column_builder.md +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/skills/tl-report-builder/tools/database_query.md +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/skills/tl-report-builder/tools/keyword_research.md +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/skills/tl-report-builder/tools/name_resolver.md +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/skills/tl-report-builder/tools/sample_judge.md +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/skills/tl-report-builder/tools/similar_channels.md +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/skills/tl-report-builder/tools/topic_matcher.md +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/skills/tl-report-builder/tools/widget_builder.md +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/src/tl_cli/_completions.py +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/src/tl_cli/auth/__init__.py +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/src/tl_cli/auth/commands.py +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/src/tl_cli/auth/login.py +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/src/tl_cli/auth/pkce.py +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/src/tl_cli/auth/token_store.py +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/src/tl_cli/client/__init__.py +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/src/tl_cli/client/errors.py +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/src/tl_cli/client/http.py +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/src/tl_cli/commands/__init__.py +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/src/tl_cli/commands/_comments_common.py +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/src/tl_cli/commands/ask.py +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/src/tl_cli/commands/balance.py +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/src/tl_cli/commands/brands.py +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/src/tl_cli/commands/changelog.py +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/src/tl_cli/commands/db.py +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/src/tl_cli/commands/deals.py +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/src/tl_cli/commands/describe.py +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/src/tl_cli/commands/doctor.py +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/src/tl_cli/commands/matches.py +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/src/tl_cli/commands/proposals.py +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/src/tl_cli/commands/recommender.py +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/src/tl_cli/commands/schema.py +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/src/tl_cli/commands/setup.py +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/src/tl_cli/commands/snapshots.py +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/src/tl_cli/commands/uploads.py +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/src/tl_cli/commands/whoami.py +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/src/tl_cli/config.py +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/src/tl_cli/filters.py +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/src/tl_cli/hints.py +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/src/tl_cli/main.py +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/src/tl_cli/output/__init__.py +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/src/tl_cli/output/formatter.py +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/src/tl_cli/self_update.py +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/tests/__init__.py +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/tests/test_auth.py +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/tests/test_filters.py +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/tests/test_output.py +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/tests/test_sponsorships.py +0 -0
- {thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/uv.lock +0 -0
|
@@ -95,6 +95,7 @@ The version string is defined in three files and all three must be updated toget
|
|
|
95
95
|
## Coding
|
|
96
96
|
|
|
97
97
|
* 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).
|
|
98
|
+
* 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.
|
|
98
99
|
* Place all imports at the start of the Python module file
|
|
99
100
|
|
|
100
101
|
# Git commit rules
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: thoughtleaders-cli
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.12
|
|
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
|
|
@@ -141,7 +141,7 @@ Prefer writing Python code, shell code, or `jq` commands that fetche or analysis
|
|
|
141
141
|
tl sponsorships list [filters...] # Sponsorships — list curve, mult 1.0
|
|
142
142
|
tl sponsorships show <id> # Sponsorship detail (2 credits)
|
|
143
143
|
tl sponsorships create --channel <id> --brand <id> # Create proposal (free)
|
|
144
|
-
tl sponsorships update <id> '<json>' # Update
|
|
144
|
+
tl sponsorships update <id> '<json>' # Update a sponsorship (2 credits)
|
|
145
145
|
tl deals list [filters...] # Shortcut: agreed-upon sponsorships (status:deal); same curve as sponsorships list
|
|
146
146
|
tl deals show <id> # Deal detail (2 credits)
|
|
147
147
|
tl matches list [filters...] # Shortcut: possible brand-channel pairings (status:match); same curve
|
|
@@ -153,7 +153,7 @@ tl proposals create --channel <id> --brand <id> # Create proposal (free)
|
|
|
153
153
|
tl uploads list [filters...] # Video uploads from ES — list curve, mult 1.0
|
|
154
154
|
tl uploads show <id> # Upload detail (2 credits)
|
|
155
155
|
tl channels show <id-or-name> # Channel detail (2 credits; accepts numeric ID or name) — for channel search use raw SQL on thoughtleaders_channel
|
|
156
|
-
tl channels update <id> '<json>' # Update
|
|
156
|
+
tl channels update <id> '<json>' # Update a channel (2 credits)
|
|
157
157
|
tl channels history <id-or-name> # Sponsorship history (5 credits/result, linear)
|
|
158
158
|
tl channels similar <id-or-name> # Similarity recommender (25 credits flat; Intelligence plan)
|
|
159
159
|
tl brands show <id-or-name> # Brand detail (1 credit)
|
|
@@ -182,17 +182,11 @@ tl <entity> comment-edit <comment-id> "msg" # Edit own comment (author or super
|
|
|
182
182
|
|
|
183
183
|
### Updating records
|
|
184
184
|
|
|
185
|
-
A narrow write surface is exposed for two resources. Each command takes the record id and a single JSON object with the fields to change; the server enforces a hard-coded field whitelist and rejects anything else with a 400. Each call costs 2 credits.
|
|
186
|
-
|
|
187
185
|
```bash
|
|
188
186
|
tl sponsorships update <id> '<json>' # Edit a sponsorship (adlink)
|
|
189
187
|
tl channels update <id> '<json>' # Edit a channel
|
|
190
188
|
```
|
|
191
189
|
|
|
192
|
-
**Sponsorships** — only `publish_status` is editable. Accepts either an int code or a status label (`proposed`, `pending`, `sold`, `matched`, `outreach`, `proposal_approved`, `advertiser_reject`, `publisher_reject`, `agency_reject`, `unavailable`). Non-full-access users may only update sponsorships tied to their own organization (either through `creator_profile` or through the channel's `publication`). Trying to edit a sponsorship outside the user's org returns 403.
|
|
193
|
-
|
|
194
|
-
**Channels** — only the demographic fields are editable: `demographic_usa_share` and `demographic_male_share` (integers 0–100), `demographic_age` / `demographic_device` / `demographic_geo` (JSON objects with numeric values). Requires full-access permission; non-full-access users get a 403. The `demographics_updated_at` timestamp is refreshed automatically when any whitelisted demographic field changes.
|
|
195
|
-
|
|
196
190
|
Examples:
|
|
197
191
|
```bash
|
|
198
192
|
tl sponsorships update 98765 '{"publish_status": "sold"}'
|
|
@@ -202,7 +196,7 @@ tl channels update 12345 '{"demographic_geo": {"US": 60, "UK": 12, "CA": 8}}'
|
|
|
202
196
|
tl channels update 12345 '{"demographic_male_share": 55, "demographic_usa_share": 70}'
|
|
203
197
|
```
|
|
204
198
|
|
|
205
|
-
|
|
199
|
+
Each call costs 2 credits. If a request is rejected with a 400, the response body names the offending key — read it and retry with a smaller body. If the user wants to edit something the API rejects, the change has to be made in the app or by a human with DB access.
|
|
206
200
|
|
|
207
201
|
### Raw queries (`tl db`)
|
|
208
202
|
|
|
@@ -326,8 +320,6 @@ See [references/business-glossary.md](references/business-glossary.md) for reven
|
|
|
326
320
|
| Arbitrary read-only `SELECT` on Postgres | **Available** via `tl db pg`. | SELECT-only, mandatory `LIMIT ≤ 500` + `OFFSET`, only certain SQL forms are allowed. See `references/postgres-schema.md`. |
|
|
327
321
|
| Cross-reference helpers ("channels proposed to brand X", "channels sponsored by MBN brands in last N days") | **Available** via `tl db pg`. | Write the join: `thoughtleaders_adlink` ↔ `adspot` ↔ `channel` ↔ `profile` ↔ `profile_brands` ↔ `brand`. Filter by `publish_status` for proposed/sold and by date range as needed. See `references/postgres-schema.md` for the exact column names. |
|
|
328
322
|
| **AdLink INSERT** with custom price/cost/owner/`weighted_price`/`created_where` | **Unavailable** — `tl sponsorships create` exists but only creates a free *proposal* between a channel and a brand. The `tl db pg` sanitizer accepts SELECT only — no INSERT/UPDATE. | Done in the app or by a human with DB access. |
|
|
329
|
-
| **AdLink UPDATE** of any field other than `publish_status` (price, cost, owner, send_date, …) | **Unavailable** — `tl sponsorships update` only accepts `publish_status` and only for sponsorships in the user's org (full-access bypasses the org check). | Done in the app or by a human with DB access. |
|
|
330
|
-
| **Channel UPDATE** of any field other than the demographic fields (`demographic_usa_share`, `demographic_male_share`, `demographic_age`, `demographic_device`, `demographic_geo`) | **Unavailable** — `tl channels update` only accepts those fields, and only for full-access users. | Done in the app or by a human with DB access. |
|
|
331
323
|
| Pre-insert validation queries (joining `adspot ↔ channel ↔ profile ↔ org` to confirm MSN, integration=1, persona, plan) | **Available** via `tl db pg`. | One SELECT joining the four tables. Use `thoughtleaders_channel.media_selling_network_join_date IS NOT NULL` for MSN, `thoughtleaders_adspot.integration = 1` for mention adspots, `thoughtleaders_profile.persona` for the persona code (see persona constants in `references/postgres-schema.md`). |
|
|
332
324
|
| Firebolt cross-table or join queries; filtering on non-indexed columns in WHERE | **Unavailable** — not accepted. | Fetch a wider slice keyed on `channel_id` (and optionally `id`), filter the rest in `jq`/Python. |
|
|
333
325
|
| ES `query_string`, `regexp`, `wildcard`, `fuzzy`, `more_like_this`, parent/child joins; any `script_*`; multiple aggregations in one body | **Unavailable** — not accepted. | Rewrite using `term`/`terms`/`match`/`bool`/`nested`. For multi-agg dashboards, run multiple `tl db es` calls and combine client-side. For "similar"-style queries, try `tl channels similar` / `tl brands similar` (server-implemented similarity search). |
|
|
@@ -238,19 +238,13 @@ def history_cmd(
|
|
|
238
238
|
@app.command("update")
|
|
239
239
|
def update_cmd(
|
|
240
240
|
channel_id: int = typer.Argument(..., help="Channel ID (numeric)"),
|
|
241
|
-
fields: str = typer.Argument(..., help='JSON object of fields to update
|
|
241
|
+
fields: str = typer.Argument(..., help='JSON object of fields to update'),
|
|
242
242
|
json_output: bool = typer.Option(False, "--json", help="JSON output"),
|
|
243
243
|
toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
|
|
244
244
|
) -> None:
|
|
245
|
-
"""Update
|
|
245
|
+
"""Update a channel.
|
|
246
246
|
|
|
247
|
-
|
|
248
|
-
demographic_age, demographic_device, demographic_geo. The
|
|
249
|
-
demographics_updated_at timestamp is refreshed automatically.
|
|
250
|
-
|
|
251
|
-
Examples:
|
|
252
|
-
tl channels update 12345 '{"demographic_male_share": 62}'
|
|
253
|
-
tl channels update 12345 '{"demographic_geo": {"US": 60, "UK": 12}}'
|
|
247
|
+
Unknown fields are rejected with a 400 listing the offending key.
|
|
254
248
|
"""
|
|
255
249
|
fmt = detect_format(json_output, False, False, toon_output)
|
|
256
250
|
try:
|
|
@@ -10,9 +10,9 @@ from rich.text import Text
|
|
|
10
10
|
|
|
11
11
|
from tl_cli.client.errors import ApiError, handle_api_error
|
|
12
12
|
from tl_cli.client.http import get_client
|
|
13
|
-
from tl_cli.output.formatter import detect_format, output
|
|
13
|
+
from tl_cli.output.formatter import detect_format, output, output_single
|
|
14
14
|
|
|
15
|
-
app = typer.Typer(help="Saved reports (list, run, create)")
|
|
15
|
+
app = typer.Typer(help="Saved reports (list, run, create, update)")
|
|
16
16
|
err = Console(stderr=True)
|
|
17
17
|
|
|
18
18
|
# Report type labels matching Django's ReportType enum
|
|
@@ -226,75 +226,120 @@ def _handle_follow_up(result: dict) -> str:
|
|
|
226
226
|
return answer
|
|
227
227
|
|
|
228
228
|
|
|
229
|
+
def _parse_config_arg(config_json: str) -> dict:
|
|
230
|
+
"""Parse the --config argument string into a dict, exiting cleanly on bad input."""
|
|
231
|
+
try:
|
|
232
|
+
config = json.loads(config_json)
|
|
233
|
+
except json.JSONDecodeError as exc:
|
|
234
|
+
err.print(f"[red]--config is not valid JSON: {exc}[/red]")
|
|
235
|
+
raise typer.Exit(1)
|
|
236
|
+
if not isinstance(config, dict):
|
|
237
|
+
err.print("[red]--config must be a JSON object.[/red]")
|
|
238
|
+
raise typer.Exit(1)
|
|
239
|
+
return config
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _orchestrate_via_server(
|
|
243
|
+
client,
|
|
244
|
+
prompt: str,
|
|
245
|
+
timeout: int,
|
|
246
|
+
) -> dict:
|
|
247
|
+
"""Run the server-side AI Report Builder loop and return the resulting config."""
|
|
248
|
+
conversation: list[dict[str, str]] = []
|
|
249
|
+
current_prompt = prompt
|
|
250
|
+
|
|
251
|
+
while True:
|
|
252
|
+
# Send prompt to server, poll for result
|
|
253
|
+
try:
|
|
254
|
+
create_data = client.post("/reports/create", json_body={
|
|
255
|
+
"prompt": current_prompt,
|
|
256
|
+
"conversation": conversation,
|
|
257
|
+
})
|
|
258
|
+
except ApiError as e:
|
|
259
|
+
if e.status_code == 503:
|
|
260
|
+
err.print("[red]AI Report Builder is temporarily unavailable. Please try again later.[/red]")
|
|
261
|
+
raise typer.Exit(1)
|
|
262
|
+
handle_api_error(e)
|
|
263
|
+
raise typer.Exit(1)
|
|
264
|
+
|
|
265
|
+
task_id = create_data.get("task_id")
|
|
266
|
+
if not task_id:
|
|
267
|
+
err.print("[red]Server did not return a task ID.[/red]")
|
|
268
|
+
raise typer.Exit(1)
|
|
269
|
+
|
|
270
|
+
result = _poll_for_result(client, task_id, timeout)
|
|
271
|
+
action = result.get("action", "")
|
|
272
|
+
|
|
273
|
+
if action == "follow_up":
|
|
274
|
+
answer = _handle_follow_up(result)
|
|
275
|
+
conversation.append({"role": "user", "content": current_prompt})
|
|
276
|
+
conversation.append({"role": "assistant", "content": result.get("question", "")})
|
|
277
|
+
current_prompt = answer
|
|
278
|
+
continue
|
|
279
|
+
|
|
280
|
+
if action in ("error", "unsupported"):
|
|
281
|
+
message = result.get("message", "Request could not be processed.")
|
|
282
|
+
err.print(f"\n[red]{message}[/red]")
|
|
283
|
+
raise typer.Exit(1)
|
|
284
|
+
|
|
285
|
+
if action == "preview":
|
|
286
|
+
return result.get("config", {})
|
|
287
|
+
if action == "create_report":
|
|
288
|
+
return result
|
|
289
|
+
|
|
290
|
+
err.print(f"[yellow]Unexpected action: {action}[/yellow]")
|
|
291
|
+
err.print(json.dumps(result, indent=2, default=str))
|
|
292
|
+
raise typer.Exit(1)
|
|
293
|
+
|
|
294
|
+
|
|
229
295
|
@app.command("create")
|
|
230
296
|
def create_report(
|
|
231
|
-
prompt: str = typer.Argument(
|
|
297
|
+
prompt: str | None = typer.Argument(
|
|
298
|
+
None,
|
|
299
|
+
help="Natural language description of the report you want. Omit when using --config.",
|
|
300
|
+
),
|
|
301
|
+
config_json: str | None = typer.Option(
|
|
302
|
+
None,
|
|
303
|
+
"--config",
|
|
304
|
+
help=(
|
|
305
|
+
"Pre-built report config as JSON. Skips the AI Report Builder "
|
|
306
|
+
"pipeline and saves the config directly. Mutually exclusive with "
|
|
307
|
+
"the prompt argument."
|
|
308
|
+
),
|
|
309
|
+
),
|
|
232
310
|
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
|
|
233
311
|
json_output: bool = typer.Option(False, "--json", help="Output raw JSON config"),
|
|
234
312
|
timeout: int = typer.Option(300, "--timeout", help="Max orchestration time in seconds"),
|
|
235
313
|
) -> None:
|
|
236
|
-
"""Create a report from a natural
|
|
314
|
+
"""Create a report from a natural-language prompt or a pre-built config.
|
|
237
315
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
316
|
+
With a prompt, runs the AI Report Builder pipeline (keyword research, config
|
|
317
|
+
generation, review) and saves the resulting campaign.
|
|
318
|
+
|
|
319
|
+
With --config '<json>', skips the orchestration pipeline and saves the
|
|
320
|
+
provided config directly. Useful when an external agent (e.g. the
|
|
321
|
+
tl-report-builder Claude Code skill) has already produced a validated
|
|
322
|
+
config and you just want to persist it.
|
|
241
323
|
|
|
242
324
|
Examples:
|
|
243
325
|
tl reports create "gaming channels sponsoring energy drinks"
|
|
244
326
|
tl reports create "tech review channels with 100K+ subscribers" --yes
|
|
245
|
-
tl reports create "
|
|
327
|
+
tl reports create --config "$(cat config.json)" --yes
|
|
246
328
|
"""
|
|
329
|
+
if (prompt is None) == (config_json is None):
|
|
330
|
+
err.print(
|
|
331
|
+
"[red]Provide either a natural-language prompt OR --config '<json>', not both.[/red]"
|
|
332
|
+
)
|
|
333
|
+
raise typer.Exit(1)
|
|
334
|
+
|
|
247
335
|
client = get_client()
|
|
248
336
|
try:
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
create_data = client.post("/reports/create", json_body={
|
|
256
|
-
"prompt": current_prompt,
|
|
257
|
-
"conversation": conversation,
|
|
258
|
-
})
|
|
259
|
-
except ApiError as e:
|
|
260
|
-
if e.status_code == 503:
|
|
261
|
-
err.print("[red]AI Report Builder is temporarily unavailable. Please try again later.[/red]")
|
|
262
|
-
raise typer.Exit(1)
|
|
263
|
-
handle_api_error(e)
|
|
264
|
-
raise typer.Exit(1)
|
|
265
|
-
|
|
266
|
-
task_id = create_data.get("task_id")
|
|
267
|
-
if not task_id:
|
|
268
|
-
err.print("[red]Server did not return a task ID.[/red]")
|
|
269
|
-
raise typer.Exit(1)
|
|
270
|
-
|
|
271
|
-
result = _poll_for_result(client, task_id, timeout)
|
|
272
|
-
action = result.get("action", "")
|
|
273
|
-
|
|
274
|
-
# Server wraps response: "preview" → config in result["config"]
|
|
275
|
-
if action == "follow_up":
|
|
276
|
-
answer = _handle_follow_up(result)
|
|
277
|
-
conversation.append({"role": "user", "content": current_prompt})
|
|
278
|
-
conversation.append({"role": "assistant", "content": result.get("question", "")})
|
|
279
|
-
current_prompt = answer
|
|
280
|
-
continue
|
|
281
|
-
|
|
282
|
-
if action in ("error", "unsupported"):
|
|
283
|
-
message = result.get("message", "Request could not be processed.")
|
|
284
|
-
err.print(f"\n[red]{message}[/red]")
|
|
285
|
-
raise typer.Exit(1)
|
|
286
|
-
|
|
287
|
-
if action == "preview":
|
|
288
|
-
config = result.get("config", {})
|
|
289
|
-
elif action == "create_report":
|
|
290
|
-
config = result
|
|
291
|
-
else:
|
|
292
|
-
err.print(f"[yellow]Unexpected action: {action}[/yellow]")
|
|
293
|
-
if json_output:
|
|
294
|
-
print(json.dumps(result, indent=2, default=str))
|
|
295
|
-
raise typer.Exit(1)
|
|
296
|
-
|
|
297
|
-
break
|
|
337
|
+
if config_json is not None:
|
|
338
|
+
config = _parse_config_arg(config_json)
|
|
339
|
+
saved_prompts: list[str] = []
|
|
340
|
+
else:
|
|
341
|
+
config = _orchestrate_via_server(client, prompt, timeout)
|
|
342
|
+
saved_prompts = [prompt]
|
|
298
343
|
|
|
299
344
|
# --- Show preview ---
|
|
300
345
|
if json_output:
|
|
@@ -315,7 +360,7 @@ def create_report(
|
|
|
315
360
|
# --- Save to server ---
|
|
316
361
|
data = client.post("/reports/confirm", json_body={
|
|
317
362
|
"config": config,
|
|
318
|
-
"prompts":
|
|
363
|
+
"prompts": saved_prompts,
|
|
319
364
|
"reasoning": "",
|
|
320
365
|
})
|
|
321
366
|
|
|
@@ -344,3 +389,34 @@ def create_report(
|
|
|
344
389
|
handle_api_error(e)
|
|
345
390
|
finally:
|
|
346
391
|
client.close()
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
@app.command("update")
|
|
395
|
+
def update_report(
|
|
396
|
+
report_id: int = typer.Argument(..., help="Report ID"),
|
|
397
|
+
fields: str = typer.Argument(..., help='JSON object of fields to update'),
|
|
398
|
+
json_output: bool = typer.Option(False, "--json", help="JSON output"),
|
|
399
|
+
toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
|
|
400
|
+
) -> None:
|
|
401
|
+
"""Update a saved report.
|
|
402
|
+
|
|
403
|
+
Unknown fields are rejected with a 400 listing the offending key.
|
|
404
|
+
"""
|
|
405
|
+
fmt = detect_format(json_output, False, False, toon_output)
|
|
406
|
+
try:
|
|
407
|
+
body = json.loads(fields)
|
|
408
|
+
except json.JSONDecodeError as exc:
|
|
409
|
+
err.print(f"[red]Error:[/red] fields argument must be a JSON object: {exc}")
|
|
410
|
+
raise typer.Exit(1)
|
|
411
|
+
if not isinstance(body, dict):
|
|
412
|
+
err.print("[red]Error:[/red] fields argument must be a JSON object.")
|
|
413
|
+
raise typer.Exit(1)
|
|
414
|
+
|
|
415
|
+
client = get_client()
|
|
416
|
+
try:
|
|
417
|
+
data = client.post(f"/reports/{report_id}/edit", json_body=body)
|
|
418
|
+
output_single(data, fmt)
|
|
419
|
+
except ApiError as e:
|
|
420
|
+
handle_api_error(e)
|
|
421
|
+
finally:
|
|
422
|
+
client.close()
|
|
@@ -198,19 +198,13 @@ def create_cmd(
|
|
|
198
198
|
@app.command("update")
|
|
199
199
|
def update_cmd(
|
|
200
200
|
sponsorship_id: int = typer.Argument(..., help="Sponsorship (adlink) ID"),
|
|
201
|
-
fields: str = typer.Argument(..., help='JSON object of fields to update
|
|
201
|
+
fields: str = typer.Argument(..., help='JSON object of fields to update'),
|
|
202
202
|
json_output: bool = typer.Option(False, "--json", help="JSON output"),
|
|
203
203
|
toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
|
|
204
204
|
) -> None:
|
|
205
|
-
"""Update
|
|
205
|
+
"""Update a sponsorship.
|
|
206
206
|
|
|
207
|
-
|
|
208
|
-
'pending', 'matched'). Non-full-access users may only update sponsorships
|
|
209
|
-
tied to their own organization.
|
|
210
|
-
|
|
211
|
-
Examples:
|
|
212
|
-
tl sponsorships update 98765 '{"publish_status": "sold"}'
|
|
213
|
-
tl sponsorships update 98765 '{"publish_status": 3}'
|
|
207
|
+
Unknown fields are rejected with a 400 listing the offending key.
|
|
214
208
|
"""
|
|
215
209
|
fmt = detect_format(json_output, False, False, toon_output)
|
|
216
210
|
try:
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Tests for `tl reports create --config` and `tl reports update`."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
import typer
|
|
5
|
+
from typer.testing import CliRunner
|
|
6
|
+
|
|
7
|
+
from tl_cli.commands.reports import _parse_config_arg, app
|
|
8
|
+
|
|
9
|
+
runner = CliRunner()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# ---------------------------------------------------------------------------
|
|
13
|
+
# _parse_config_arg
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TestParseConfigArg:
|
|
18
|
+
def test_valid_object_returns_dict(self) -> None:
|
|
19
|
+
result = _parse_config_arg('{"report_title": "Test", "report_type": 3}')
|
|
20
|
+
assert result == {"report_title": "Test", "report_type": 3}
|
|
21
|
+
|
|
22
|
+
def test_invalid_json_exits(self) -> None:
|
|
23
|
+
with pytest.raises(typer.Exit) as excinfo:
|
|
24
|
+
_parse_config_arg('{not json')
|
|
25
|
+
assert excinfo.value.exit_code == 1
|
|
26
|
+
|
|
27
|
+
def test_non_object_exits(self) -> None:
|
|
28
|
+
# JSON arrays / strings / numbers are valid JSON but not the object the
|
|
29
|
+
# endpoint accepts.
|
|
30
|
+
with pytest.raises(typer.Exit) as excinfo:
|
|
31
|
+
_parse_config_arg('[1, 2, 3]')
|
|
32
|
+
assert excinfo.value.exit_code == 1
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# tl reports create — argument validation
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class TestCreateArgValidation:
|
|
41
|
+
def test_no_prompt_and_no_config_rejected(self) -> None:
|
|
42
|
+
result = runner.invoke(app, ["create"])
|
|
43
|
+
assert result.exit_code == 1
|
|
44
|
+
assert "either" in (result.stderr or result.output).lower() and "config" in (result.stderr or result.output).lower()
|
|
45
|
+
|
|
46
|
+
def test_both_prompt_and_config_rejected(self) -> None:
|
|
47
|
+
result = runner.invoke(
|
|
48
|
+
app,
|
|
49
|
+
["create", "gaming channels", "--config", '{"report_title": "x", "report_type": 3}'],
|
|
50
|
+
)
|
|
51
|
+
assert result.exit_code == 1
|
|
52
|
+
assert "either" in (result.stderr or result.output).lower()
|
|
53
|
+
|
|
54
|
+
def test_config_invalid_json_rejected(self) -> None:
|
|
55
|
+
result = runner.invoke(app, ["create", "--config", "{not json", "--yes"])
|
|
56
|
+
assert result.exit_code == 1
|
|
57
|
+
assert "valid json" in (result.stderr or result.output).lower()
|
|
58
|
+
|
|
59
|
+
def test_config_non_object_rejected(self) -> None:
|
|
60
|
+
result = runner.invoke(app, ["create", "--config", "[1,2,3]", "--yes"])
|
|
61
|
+
assert result.exit_code == 1
|
|
62
|
+
assert "json object" in (result.stderr or result.output).lower()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
# tl reports update — argument validation
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class TestUpdateArgValidation:
|
|
71
|
+
def test_invalid_json_rejected(self) -> None:
|
|
72
|
+
result = runner.invoke(app, ["update", "12345", "{not json"])
|
|
73
|
+
assert result.exit_code == 1
|
|
74
|
+
assert "json object" in (result.stderr or result.output).lower()
|
|
75
|
+
|
|
76
|
+
def test_non_object_rejected(self) -> None:
|
|
77
|
+
result = runner.invoke(app, ["update", "12345", '"just a string"'])
|
|
78
|
+
assert result.exit_code == 1
|
|
79
|
+
assert "json object" in (result.stderr or result.output).lower()
|
|
File without changes
|
{thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/.github/workflows/python-publish.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/skills/tl/references/business-glossary.md
RENAMED
|
File without changes
|
{thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/skills/tl/references/elasticsearch-schema.md
RENAMED
|
File without changes
|
{thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/skills/tl/references/firebolt-schema.md
RENAMED
|
File without changes
|
{thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/skills/tl/references/postgres-schema.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{thoughtleaders_cli-0.6.10 → thoughtleaders_cli-0.6.12}/src/tl_cli/commands/_comments_common.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|