thoughtleaders-cli 0.6.11__tar.gz → 0.6.13__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 (91) hide show
  1. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/.claude-plugin/plugin.json +1 -1
  2. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/PKG-INFO +1 -1
  3. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/pyproject.toml +1 -1
  4. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/skills/tl-report-builder/SKILL.md +3 -3
  5. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/src/tl_cli/__init__.py +1 -1
  6. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/src/tl_cli/commands/reports.py +134 -58
  7. thoughtleaders_cli-0.6.13/tests/test_reports.py +79 -0
  8. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/.claude-plugin/marketplace.json +0 -0
  9. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/.github/workflows/python-publish.yml +0 -0
  10. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/.gitignore +0 -0
  11. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/AGENTS.md +0 -0
  12. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/CLAUDE.md +0 -0
  13. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/LICENSE +0 -0
  14. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/README.md +0 -0
  15. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/agents/tl-analyst.md +0 -0
  16. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/commands/tl-balance.md +0 -0
  17. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/commands/tl-reports.md +0 -0
  18. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/commands/tl-sponsorships.md +0 -0
  19. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/commands/tl.md +0 -0
  20. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/docs/architecture.md +0 -0
  21. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/hooks/hooks.json +0 -0
  22. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/hooks/scripts/post-usage.sh +0 -0
  23. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/hooks/scripts/pre-check.sh +0 -0
  24. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/skills/tl/SKILL.md +0 -0
  25. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/skills/tl/references/business-glossary.md +0 -0
  26. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/skills/tl/references/elasticsearch-schema.md +0 -0
  27. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/skills/tl/references/firebolt-schema.md +0 -0
  28. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/skills/tl/references/postgres-schema.md +0 -0
  29. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/skills/tl-report-builder/examples/e2e_findings.md +0 -0
  30. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/skills/tl-report-builder/examples/golden_queries.md +0 -0
  31. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/skills/tl-report-builder/references/columns_brands.md +0 -0
  32. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/skills/tl-report-builder/references/columns_channels.md +0 -0
  33. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/skills/tl-report-builder/references/columns_content.md +0 -0
  34. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/skills/tl-report-builder/references/columns_sponsorships.md +0 -0
  35. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/skills/tl-report-builder/references/intelligence_filterset_schema.json +0 -0
  36. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/skills/tl-report-builder/references/intelligence_widget_schema.json +0 -0
  37. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/skills/tl-report-builder/references/report_glossary.md +0 -0
  38. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/skills/tl-report-builder/references/sortable_columns.json +0 -0
  39. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/skills/tl-report-builder/references/sponsorship_filterset_schema.json +0 -0
  40. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/skills/tl-report-builder/references/sponsorship_widget_schema.json +0 -0
  41. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/skills/tl-report-builder/references/widgets.md +0 -0
  42. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/skills/tl-report-builder/tools/column_builder.md +0 -0
  43. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/skills/tl-report-builder/tools/database_query.md +0 -0
  44. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/skills/tl-report-builder/tools/keyword_research.md +0 -0
  45. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/skills/tl-report-builder/tools/name_resolver.md +0 -0
  46. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/skills/tl-report-builder/tools/sample_judge.md +0 -0
  47. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/skills/tl-report-builder/tools/similar_channels.md +0 -0
  48. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/skills/tl-report-builder/tools/topic_matcher.md +0 -0
  49. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/skills/tl-report-builder/tools/widget_builder.md +0 -0
  50. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/src/tl_cli/_completions.py +0 -0
  51. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/src/tl_cli/auth/__init__.py +0 -0
  52. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/src/tl_cli/auth/commands.py +0 -0
  53. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/src/tl_cli/auth/login.py +0 -0
  54. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/src/tl_cli/auth/pkce.py +0 -0
  55. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/src/tl_cli/auth/token_store.py +0 -0
  56. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/src/tl_cli/client/__init__.py +0 -0
  57. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/src/tl_cli/client/errors.py +0 -0
  58. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/src/tl_cli/client/http.py +0 -0
  59. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/src/tl_cli/commands/__init__.py +0 -0
  60. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/src/tl_cli/commands/_comments_common.py +0 -0
  61. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/src/tl_cli/commands/ask.py +0 -0
  62. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/src/tl_cli/commands/balance.py +0 -0
  63. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/src/tl_cli/commands/brands.py +0 -0
  64. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/src/tl_cli/commands/changelog.py +0 -0
  65. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/src/tl_cli/commands/channels.py +0 -0
  66. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/src/tl_cli/commands/db.py +0 -0
  67. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/src/tl_cli/commands/deals.py +0 -0
  68. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/src/tl_cli/commands/describe.py +0 -0
  69. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/src/tl_cli/commands/doctor.py +0 -0
  70. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/src/tl_cli/commands/matches.py +0 -0
  71. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/src/tl_cli/commands/proposals.py +0 -0
  72. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/src/tl_cli/commands/recommender.py +0 -0
  73. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/src/tl_cli/commands/schema.py +0 -0
  74. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/src/tl_cli/commands/setup.py +0 -0
  75. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/src/tl_cli/commands/snapshots.py +0 -0
  76. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/src/tl_cli/commands/sponsorships.py +0 -0
  77. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/src/tl_cli/commands/uploads.py +0 -0
  78. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/src/tl_cli/commands/whoami.py +0 -0
  79. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/src/tl_cli/config.py +0 -0
  80. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/src/tl_cli/filters.py +0 -0
  81. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/src/tl_cli/hints.py +0 -0
  82. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/src/tl_cli/main.py +0 -0
  83. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/src/tl_cli/output/__init__.py +0 -0
  84. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/src/tl_cli/output/formatter.py +0 -0
  85. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/src/tl_cli/self_update.py +0 -0
  86. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/tests/__init__.py +0 -0
  87. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/tests/test_auth.py +0 -0
  88. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/tests/test_filters.py +0 -0
  89. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/tests/test_output.py +0 -0
  90. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/tests/test_sponsorships.py +0 -0
  91. {thoughtleaders_cli-0.6.11 → thoughtleaders_cli-0.6.13}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tl-cli",
3
- "version": "0.6.11",
3
+ "version": "0.6.13",
4
4
  "description": "ThoughtLeaders CLI — query sponsorship deals, channels, brands, uploads, and intelligence from the terminal",
5
5
  "author": {
6
6
  "name": "ThoughtLeaders",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: thoughtleaders-cli
3
- Version: 0.6.11
3
+ Version: 0.6.13
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.11"
7
+ version = "0.6.13"
8
8
  description = "ThoughtLeaders CLI — query sponsorship data, channels, brands, and intelligence"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -226,9 +226,9 @@ USER_QUERY
226
226
  └─────────────────────────────────────────────────────────────────────────┘
227
227
  ```
228
228
 
229
- There is no fifth phase. Phase 4's output IS the deliverable: a complete, validated campaign config + takeaways. The save step happens **outside the skill**, via a `POST` (create) or `PUT` (edit) to the report-creation API endpoint. The skill itself never writes to the database directly — reads use raw `tl db es` (intelligence reports — types 1/2/3) or raw `tl db pg` (sponsorship reports — type 8); writes go through the API.
229
+ There is no fifth phase. Phase 4's output IS the deliverable: a complete, validated campaign config + takeaways. The save step happens **outside the skill**, by handing the JSON to `tl reports create --config '<json>' --yes`. The skill itself never writes to the database directly — reads use raw `tl db es` (intelligence reports — types 1/2/3) or raw `tl db pg` (sponsorship reports — type 8); writes go through the CLI command, which posts to the report-creation API.
230
230
 
231
- > **Save-mechanism policy**: A new API endpoint is required for report creation. It will support both `POST` (initial create) and `PUT` (subsequent edits) so reports can be modified without redoing the four phases from scratch. Until the endpoint is built, the skill stops at producing the JSON config + takeaways; the calling environment handles whatever save mechanism is current. **Reads via `tl db es` / `tl db pg` (engine routed by report type — see Step 2.V1), writes via the API** is the architectural split.
231
+ > **Save-mechanism policy**: After Phase 4 emits the config, the skill instructs the user to run `tl reports create --config '<json>' --yes` to persist it. Edits to a saved report use `tl reports update <id> '<json>'`. Both commands route to the report-creation API endpoint, which delegates to the canonical campaign-update path. **Reads via `tl db es` / `tl db pg` (engine routed by report type — see Step 2.V1), writes via the CLI** is the architectural split. Do NOT instruct the user to paste the JSON into the platform UI — that's an obsolete pre-v0.6.12 fallback.
232
232
 
233
233
  ## Phase 1 — Report Type Selection (detail)
234
234
 
@@ -1327,7 +1327,7 @@ USER: Build me a report of gaming channels with 100K+ subscribers in English
1327
1327
 
1328
1328
  Claude follows this SKILL.md, executing each phase in order. No external command needed — the skill IS the orchestration; `tl db pg` is invoked from within Phase 2/3/4 as needed; tools fire conditionally per their criteria.
1329
1329
 
1330
- > **Note**: how the final config is committed (DB insert path vs. UI paste vs. another mechanism) is being addressed separately. For now Phase 4 produces the validated JSON + takeaways and stops there.
1330
+ > **Saving the config**: after Phase 4 prints the JSON, instruct the user to run `tl reports create --config '<paste the JSON>' --yes` (tl-cli v0.6.12). For edits to an existing saved report, use `tl reports update <report_id> '<json patch>'`. Do NOT tell users to paste into the platform UI that's an obsolete fallback from before the CLI commands existed.
1331
1331
 
1332
1332
  ## Reference Files
1333
1333
 
@@ -1,3 +1,3 @@
1
1
  """ThoughtLeaders CLI — query sponsorship data, channels, brands, and intelligence."""
2
2
 
3
- __version__ = "0.6.11"
3
+ __version__ = "0.6.13"
@@ -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(..., help="Natural language description of the report you want"),
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 language description.
314
+ """Create a report from a natural-language prompt or a pre-built config.
237
315
 
238
- Sends your prompt to the ThoughtLeaders server, which runs the AI Report
239
- Builder pipeline (keyword research, config generation, review). Then
240
- confirms with the server to create the campaign.
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 "beauty brands on YouTube" --json
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
- conversation: list[dict[str, str]] = []
250
- current_prompt = prompt
251
-
252
- while True:
253
- # Send prompt to server, poll for result
254
- try:
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": [prompt],
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()
@@ -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()