thoughtleaders-cli 0.6.12__tar.gz → 0.6.15__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.12 → thoughtleaders_cli-0.6.15}/.claude-plugin/plugin.json +1 -1
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/PKG-INFO +14 -4
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/README.md +13 -3
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/pyproject.toml +1 -1
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/skills/tl-report-builder/SKILL.md +112 -8
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/src/tl_cli/__init__.py +1 -1
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/src/tl_cli/commands/reports.py +34 -10
- thoughtleaders_cli-0.6.15/tests/test_reports.py +133 -0
- thoughtleaders_cli-0.6.12/tests/test_reports.py +0 -79
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/.claude-plugin/marketplace.json +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/.github/workflows/python-publish.yml +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/.gitignore +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/AGENTS.md +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/CLAUDE.md +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/LICENSE +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/agents/tl-analyst.md +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/commands/tl-balance.md +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/commands/tl-reports.md +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/commands/tl-sponsorships.md +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/commands/tl.md +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/docs/architecture.md +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/hooks/hooks.json +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/hooks/scripts/post-usage.sh +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/hooks/scripts/pre-check.sh +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/skills/tl/SKILL.md +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/skills/tl/references/business-glossary.md +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/skills/tl/references/elasticsearch-schema.md +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/skills/tl/references/firebolt-schema.md +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/skills/tl/references/postgres-schema.md +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/skills/tl-report-builder/examples/e2e_findings.md +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/skills/tl-report-builder/examples/golden_queries.md +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/skills/tl-report-builder/references/columns_brands.md +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/skills/tl-report-builder/references/columns_channels.md +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/skills/tl-report-builder/references/columns_content.md +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/skills/tl-report-builder/references/columns_sponsorships.md +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/skills/tl-report-builder/references/intelligence_filterset_schema.json +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/skills/tl-report-builder/references/intelligence_widget_schema.json +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/skills/tl-report-builder/references/report_glossary.md +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/skills/tl-report-builder/references/sortable_columns.json +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/skills/tl-report-builder/references/sponsorship_filterset_schema.json +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/skills/tl-report-builder/references/sponsorship_widget_schema.json +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/skills/tl-report-builder/references/widgets.md +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/skills/tl-report-builder/tools/column_builder.md +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/skills/tl-report-builder/tools/database_query.md +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/skills/tl-report-builder/tools/keyword_research.md +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/skills/tl-report-builder/tools/name_resolver.md +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/skills/tl-report-builder/tools/sample_judge.md +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/skills/tl-report-builder/tools/similar_channels.md +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/skills/tl-report-builder/tools/topic_matcher.md +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/skills/tl-report-builder/tools/widget_builder.md +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/src/tl_cli/_completions.py +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/src/tl_cli/auth/__init__.py +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/src/tl_cli/auth/commands.py +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/src/tl_cli/auth/login.py +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/src/tl_cli/auth/pkce.py +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/src/tl_cli/auth/token_store.py +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/src/tl_cli/client/__init__.py +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/src/tl_cli/client/errors.py +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/src/tl_cli/client/http.py +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/src/tl_cli/commands/__init__.py +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/src/tl_cli/commands/_comments_common.py +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/src/tl_cli/commands/ask.py +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/src/tl_cli/commands/balance.py +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/src/tl_cli/commands/brands.py +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/src/tl_cli/commands/changelog.py +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/src/tl_cli/commands/channels.py +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/src/tl_cli/commands/db.py +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/src/tl_cli/commands/deals.py +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/src/tl_cli/commands/describe.py +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/src/tl_cli/commands/doctor.py +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/src/tl_cli/commands/matches.py +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/src/tl_cli/commands/proposals.py +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/src/tl_cli/commands/recommender.py +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/src/tl_cli/commands/schema.py +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/src/tl_cli/commands/setup.py +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/src/tl_cli/commands/snapshots.py +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/src/tl_cli/commands/sponsorships.py +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/src/tl_cli/commands/uploads.py +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/src/tl_cli/commands/whoami.py +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/src/tl_cli/config.py +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/src/tl_cli/filters.py +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/src/tl_cli/hints.py +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/src/tl_cli/main.py +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/src/tl_cli/output/__init__.py +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/src/tl_cli/output/formatter.py +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/src/tl_cli/self_update.py +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/tests/__init__.py +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/tests/test_auth.py +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/tests/test_filters.py +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/tests/test_output.py +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/tests/test_sponsorships.py +0 -0
- {thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: thoughtleaders-cli
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.15
|
|
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
|
|
@@ -161,11 +161,18 @@ ThoughtLeaders has its internal terminology that's exposed throughout this tool.
|
|
|
161
161
|
|
|
162
162
|
Sponsorships are the centre of attention in ThoughtLeaders - all other analytics and operations serve to produce or optimise sponsorships.
|
|
163
163
|
Note that the term "Sponsorship" is wide, and can encompass deals that yet need to be approved by either side. There is a funnel of
|
|
164
|
-
sponsorship types: the pool of Sponsorships is large, the pool of
|
|
164
|
+
sponsorship types: the pool of Sponsorships is large, the pool of Matches (considered from either Brand or Channel side) is smaller,
|
|
165
165
|
the pool of Proposals is yet smaller, and the pool of Deals is the smallest.
|
|
166
166
|
|
|
167
167
|
# Integrations
|
|
168
168
|
|
|
169
|
+
## Requirements
|
|
170
|
+
|
|
171
|
+
- Python 3.12+
|
|
172
|
+
- [jq](https://stedolan.github.io/jq/)
|
|
173
|
+
- [ripgrep](https://github.com/BurntSushi/ripgrep)
|
|
174
|
+
- [duckdb](https://duckdb.org/)
|
|
175
|
+
|
|
169
176
|
## Claude Code Integration
|
|
170
177
|
|
|
171
178
|
If you use Claude Code, install the plugin for natural language access:
|
|
@@ -216,10 +223,13 @@ By default, output is a styled table in the terminal and JSON when piped.
|
|
|
216
223
|
```bash
|
|
217
224
|
tl sponsorships list status:sold # Pretty table
|
|
218
225
|
tl sponsorships list status:sold --json # JSON
|
|
219
|
-
tl sponsorships list status:sold --csv > sponsorships.csv # CSV
|
|
220
226
|
tl sponsorships list status:sold --json | jq '.results' # Pipe to jq
|
|
227
|
+
tl sponsorships list status:sold --csv > sponsorships.csv # CSV
|
|
228
|
+
tl sponsorships list status:sold --toon # TOON (token-efficient for LLMs)
|
|
221
229
|
```
|
|
222
230
|
|
|
231
|
+
TOON (Token-Oriented Object Notation) is a compact text format designed to encode structured data with fewer tokens than JSON when feeding output back into an LLM. See [the TOON format repository](https://github.com/toon-format/toon) for the specification.
|
|
232
|
+
|
|
223
233
|
## Documentation
|
|
224
234
|
|
|
225
235
|
- [Architecture & Design](docs/architecture.md) — full design doc covering commands, data scoping, credit metering, and server-side API
|
|
@@ -228,4 +238,4 @@ tl sponsorships list status:sold --json | jq '.results' # Pipe to jq
|
|
|
228
238
|
|
|
229
239
|
# Notes
|
|
230
240
|
|
|
231
|
-
* Tested with OpenCode and the `nemotron-cascade-2-30b-a3b-i1` local model.
|
|
241
|
+
* Tested with OpenCode and the `nemotron-cascade-2-30b-a3b-i1` local model.
|
|
@@ -134,11 +134,18 @@ ThoughtLeaders has its internal terminology that's exposed throughout this tool.
|
|
|
134
134
|
|
|
135
135
|
Sponsorships are the centre of attention in ThoughtLeaders - all other analytics and operations serve to produce or optimise sponsorships.
|
|
136
136
|
Note that the term "Sponsorship" is wide, and can encompass deals that yet need to be approved by either side. There is a funnel of
|
|
137
|
-
sponsorship types: the pool of Sponsorships is large, the pool of
|
|
137
|
+
sponsorship types: the pool of Sponsorships is large, the pool of Matches (considered from either Brand or Channel side) is smaller,
|
|
138
138
|
the pool of Proposals is yet smaller, and the pool of Deals is the smallest.
|
|
139
139
|
|
|
140
140
|
# Integrations
|
|
141
141
|
|
|
142
|
+
## Requirements
|
|
143
|
+
|
|
144
|
+
- Python 3.12+
|
|
145
|
+
- [jq](https://stedolan.github.io/jq/)
|
|
146
|
+
- [ripgrep](https://github.com/BurntSushi/ripgrep)
|
|
147
|
+
- [duckdb](https://duckdb.org/)
|
|
148
|
+
|
|
142
149
|
## Claude Code Integration
|
|
143
150
|
|
|
144
151
|
If you use Claude Code, install the plugin for natural language access:
|
|
@@ -189,10 +196,13 @@ By default, output is a styled table in the terminal and JSON when piped.
|
|
|
189
196
|
```bash
|
|
190
197
|
tl sponsorships list status:sold # Pretty table
|
|
191
198
|
tl sponsorships list status:sold --json # JSON
|
|
192
|
-
tl sponsorships list status:sold --csv > sponsorships.csv # CSV
|
|
193
199
|
tl sponsorships list status:sold --json | jq '.results' # Pipe to jq
|
|
200
|
+
tl sponsorships list status:sold --csv > sponsorships.csv # CSV
|
|
201
|
+
tl sponsorships list status:sold --toon # TOON (token-efficient for LLMs)
|
|
194
202
|
```
|
|
195
203
|
|
|
204
|
+
TOON (Token-Oriented Object Notation) is a compact text format designed to encode structured data with fewer tokens than JSON when feeding output back into an LLM. See [the TOON format repository](https://github.com/toon-format/toon) for the specification.
|
|
205
|
+
|
|
196
206
|
## Documentation
|
|
197
207
|
|
|
198
208
|
- [Architecture & Design](docs/architecture.md) — full design doc covering commands, data scoping, credit metering, and server-side API
|
|
@@ -201,4 +211,4 @@ tl sponsorships list status:sold --json | jq '.results' # Pipe to jq
|
|
|
201
211
|
|
|
202
212
|
# Notes
|
|
203
213
|
|
|
204
|
-
* Tested with OpenCode and the `nemotron-cascade-2-30b-a3b-i1` local model.
|
|
214
|
+
* Tested with OpenCode and the `nemotron-cascade-2-30b-a3b-i1` local model.
|
|
@@ -54,6 +54,12 @@ Internally this skill thinks in phases (1–4), report types (1, 2, 3, 8), tool
|
|
|
54
54
|
| Phase 3 — column builder | "Picking which columns to show in the report…" |
|
|
55
55
|
| Phase 4 — widget builder | "Choosing the charts and dashboards…" |
|
|
56
56
|
| Phase 4 — final composition | "Putting the final report together…" |
|
|
57
|
+
| Preview path (default) — show takeaways + sample table | "Here's what matches…" / "Found N channels — top by reach:" / "Top videos that match:" |
|
|
58
|
+
| Preview tail (ambiguous middle — close with this) | *"If you want this saved as a campaign you can come back to, say save."* |
|
|
59
|
+
| Save step (write JSON to `/tmp/<name>.json`, then `tl reports create --config-file /tmp/<name>.json --yes`) | "Saving the report…" |
|
|
60
|
+
| Save success (only after the CLI command returns success) | "Report saved." + link from the CLI response (do NOT echo the JSON config back; do NOT say "saved as <path>.json" — the temp file is transport, not the deliverable) |
|
|
61
|
+
| Save failure | "Couldn't save the report: <plain-English reason>" — surface the CLI's stderr verbatim if it's user-readable, otherwise summarise |
|
|
62
|
+
| User says "save" / "yes save it" / "save it" after a preview | "Saving…" — re-use the config from working memory; do NOT re-run Phases 1–4 |
|
|
57
63
|
| Mode B follow-up (looks_wrong) | "The top results don't look right — here are your options…" |
|
|
58
64
|
| Mode C (3 retries exhausted) | "I couldn't build a sensible result for this — here's what I tried…" |
|
|
59
65
|
|
|
@@ -72,9 +78,7 @@ Narrate like a colleague describing what they're doing — not like a logger ann
|
|
|
72
78
|
|
|
73
79
|
### Worked example — running a full request
|
|
74
80
|
|
|
75
|
-
Here's a real request and the gap between leaky narration (the failure mode) and clean narration (the target). Both sides describe the SAME execution; only the surface text differs.
|
|
76
|
-
|
|
77
|
-
**Request**: *"Create a campaign of channels we haven't pitched to investing.com in the last year"*
|
|
81
|
+
Here's a real request and the gap between leaky narration (the failure mode) and clean narration (the target). Both sides describe the SAME execution; only the surface text differs. **The intent here is "save" (not preview)** — the prompt says "Create a campaign", which is one of the explicit save-intent phrases listed in the Save-or-preview policy.
|
|
78
82
|
|
|
79
83
|
❌ **Leaky** (the failure mode: phase numbers, type numbers, identifier-shaped names, third-person framing). The `[bracketed]` placeholders mark where the agent leaked specific internal identifier names — the lesson is that any identifier-shaped name appeared at all:
|
|
80
84
|
|
|
@@ -102,9 +106,65 @@ Here's a real request and the gap between leaky narration (the failure mode) and
|
|
|
102
106
|
>
|
|
103
107
|
> *(...later...)* 7 of 10 are strong matches, but 2 are Pokémon channels — they're hitting on "Trading Card Game" because of a loose "trading" keyword. Let me tighten that up. *(tightening the search…)* Tried adding "portfolio" but that pulls in music labels via AI-generated topic descriptions, so I dropped it. *(trying a tighter set…)* **1,838 unpitched investing channels, 9 of 10 in the spot-check are clean** — Pushkar Raj Thakur, Mark Tilbury, warikoo, Pranjal Kamra, all on-target.
|
|
104
108
|
>
|
|
105
|
-
> Putting the final report together now — outreach-focused columns (deal history, contact info, demographic fit) and the standard channels-report charts.
|
|
109
|
+
> Putting the final report together now — outreach-focused columns (deal history, contact info, demographic fit) and the standard channels-report charts. *(saving…)*
|
|
110
|
+
>
|
|
111
|
+
> **Report saved.** [Investing — unpitched investing channels](https://app.thoughtleaders.io/...) (Campaign #12345)
|
|
112
|
+
>
|
|
113
|
+
> **Worth knowing**:
|
|
114
|
+
> - 668 channels already pitched to investing.com are excluded.
|
|
115
|
+
> - Topic anchored on the curated investing keyword set; spot-check looked clean.
|
|
116
|
+
> - Sort is most-recently-active first so dormant channels don't crowd the top.
|
|
117
|
+
|
|
118
|
+
Notice what's preserved (the brand resolution outcome, the actual exclusion count, the noise example with specific channel names like "Pokémon", the reasoning across attempts, the final sample names, the saved-report link, the takeaways) and what's stripped (every phase number, every type number, every identifier-shaped name, "The user wants…", raw IDs the user doesn't need to see, **and the campaign-config JSON itself** — the JSON is written to a temp file and handed to `tl reports create --config-file <path> --yes`, not echoed back into the chat where it's just noise once the report is saved). The clean version is also *more informative* — it tells the user what's happening to their data, not which step in the spec is firing.
|
|
119
|
+
|
|
120
|
+
### Worked example — preview-by-default (no save)
|
|
121
|
+
|
|
122
|
+
Same architecture, different intent. The prompt is exploratory; the policy says default to preview.
|
|
123
|
+
|
|
124
|
+
**Request**: *"Find me investing/finance channels we haven't pitched to investing.com in the last year"*
|
|
125
|
+
|
|
126
|
+
✅ **Clean preview output** (same Phase 1–4 execution, but no temp file, no `tl reports create`, no Campaign row):
|
|
127
|
+
|
|
128
|
+
> Pulling investing/finance channels we haven't pitched to investing.com lately.
|
|
129
|
+
>
|
|
130
|
+
> First — let me look up investing.com in our brand list… **Found it.** Pulling the list of channels we've pitched to them in the last 12 months so I can exclude them.
|
|
131
|
+
>
|
|
132
|
+
> *(...later...)* **668 channels we've already pitched** — those are out. Looking at the rest now to see how many fit an investing/finance niche…
|
|
133
|
+
>
|
|
134
|
+
> *(...later...)* **1,838 unpitched investing channels.** Spot-checked the top 10; 9 of 10 are clearly on-target.
|
|
135
|
+
>
|
|
136
|
+
> **Top by reach (sample):**
|
|
137
|
+
>
|
|
138
|
+
> | Channel | Subscribers | Last published |
|
|
139
|
+
> |---|---|---|
|
|
140
|
+
> | Pushkar Raj Thakur | 11.4M | 2 days ago |
|
|
141
|
+
> | Mark Tilbury | 4.2M | yesterday |
|
|
142
|
+
> | warikoo | 3.8M | today |
|
|
143
|
+
> | Pranjal Kamra | 6.1M | 4 days ago |
|
|
144
|
+
> | Meet Kevin | 1.9M | 6 days ago |
|
|
145
|
+
> | New Money | 870K | yesterday |
|
|
146
|
+
> | Hamish Hodder | 250K | 3 days ago |
|
|
147
|
+
> | … | … | … |
|
|
148
|
+
>
|
|
149
|
+
> **Worth knowing**:
|
|
150
|
+
> - 668 channels already pitched to investing.com are excluded.
|
|
151
|
+
> - Topic anchored on the curated investing keyword set; the spot-check looked clean (1 Pokémon channel slipped in via a "trading" keyword match — easy to filter further if needed).
|
|
152
|
+
> - Sort is most-recently-active first so dormant channels don't crowd the top.
|
|
153
|
+
>
|
|
154
|
+
> *If you want this saved as a campaign you can come back to, just say save.*
|
|
155
|
+
|
|
156
|
+
If the user replies *"yes save it"* or *"save"* → run the save step (write to `/tmp/`, invoke `tl reports create --config-file …`) using the **same config that's already in working memory**. Don't re-run Phases 1–4. The follow-up reply is just the takeaways + saved-report link.
|
|
106
157
|
|
|
107
|
-
|
|
158
|
+
What changes between save-mode and preview-mode:
|
|
159
|
+
|
|
160
|
+
| | Save (explicit intent) | Preview (default) |
|
|
161
|
+
|---|---|---|
|
|
162
|
+
| Phases 1–4 run? | Yes | Yes (identical) |
|
|
163
|
+
| Campaign row in DB? | Yes | No |
|
|
164
|
+
| What ends in chat | Takeaways + saved-report URL | Takeaways + sample table + "say save" tail |
|
|
165
|
+
| `/tmp/<slug>.json` written? | Yes (transport for `tl reports create`) | No (config stays in working memory) |
|
|
166
|
+
| `tl reports create` invoked? | Yes (`--config-file <path> --yes`) | No |
|
|
167
|
+
| Campaign-config JSON in chat? | **No** | **No** |
|
|
108
168
|
|
|
109
169
|
## Process Flow (Strictly Sequential)
|
|
110
170
|
|
|
@@ -226,9 +286,47 @@ USER_QUERY
|
|
|
226
286
|
└─────────────────────────────────────────────────────────────────────────┘
|
|
227
287
|
```
|
|
228
288
|
|
|
229
|
-
There is no fifth phase. Phase 4's output IS the deliverable
|
|
289
|
+
There is no fifth phase. Phase 4's output IS the deliverable. 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 `tl reports create --config-file <path> --yes`, which posts to the report-creation API.
|
|
230
290
|
|
|
231
|
-
|
|
291
|
+
**The deliverable can land in the chat in two shapes — pick the right one based on the user's intent:**
|
|
292
|
+
|
|
293
|
+
> **Save-or-preview policy** (READ THIS — saving has been over-eager in the past):
|
|
294
|
+
>
|
|
295
|
+
> Every request goes through Phases 1–4 and ends up with a fully-formed campaign config in working memory. The only branching is whether to **save** it (write a Campaign row to the DB) or **preview** it (show the takeaways + sample results in chat without persisting). **Default to preview.** Only save when the user's wording is **explicit and unambiguous** about wanting a saved deliverable.
|
|
296
|
+
>
|
|
297
|
+
> **Save** (auto-invoke `tl reports create --config-file`) when the prompt contains explicit save intent:
|
|
298
|
+
> - "save", "save it as a campaign", "save a report", "create the report", "create the campaign", "build me a saved report", "make a campaign for me", "publish", "persist", "store this", "I'll need to come back to this"
|
|
299
|
+
> - The user explicitly references the saved deliverable: "set up a campaign for", "make a dashboard for", "set up a report I can revisit"
|
|
300
|
+
> - The user's follow-up after a preview ("yes save it", "save", "do it", "go ahead", "create it now") — re-use the config that's already in working memory; do NOT re-run the phases
|
|
301
|
+
>
|
|
302
|
+
> **Preview** (default — show results in chat, do NOT save) when the prompt is exploratory, informational, or search-oriented:
|
|
303
|
+
> - "find me", "show me", "give me", "list", "who are", "what are", "are there any", "look up", "search for", "check"
|
|
304
|
+
> - "build me X channels / videos / brands / deals" — bare noun, no "report" / "campaign" / "save"
|
|
305
|
+
> - "tell me about", "explore", "scan", "analyse"
|
|
306
|
+
>
|
|
307
|
+
> **Ambiguous middle** ("build a report on X", "create a campaign for Y", "report on Z", "campaign for X"):
|
|
308
|
+
> - The user said "report" / "campaign" but didn't say "save" / "create the report" / "I'll come back to this".
|
|
309
|
+
> - **Default to preview**, then close the reply with one line: *"If you want to save this as a campaign you can come back to, just say save."*
|
|
310
|
+
> - Conservative move — never persist on ambiguity. If the user wanted it saved they will say so.
|
|
311
|
+
>
|
|
312
|
+
> **Save mechanics** (when save is triggered): two strict steps. **Step 1 alone is not the save** — the file write is just transport for step 2. Saying "Saved as foo.json" or "Saved to <path>" after only doing step 1 is a regression bug.
|
|
313
|
+
>
|
|
314
|
+
> 1. **Write the JSON to `/tmp/`** via the `Write` tool. The path **MUST** be under the system temp directory (`/tmp/` on Linux/macOS, `%TEMP%` / `$TMPDIR` on whatever platform the agent is running on). Use a name like `/tmp/tl-report-builder-<short-slug>.json`. **Never write to the user's current working directory or any project path** — the file is a transport, not a deliverable, and leaving `foo_report.json` in the user's repo or cwd pollutes their workspace. If the system temp dir isn't writable, fall back to another temp-shaped location, never to cwd.
|
|
315
|
+
> 2. **Invoke `tl reports create --config-file <that-same-tmp-path> --yes`** via the `Bash` tool. This is what actually saves the report. Read the CLI's response: success returns a `campaign_id` and `report_url` to echo to the user; failure returns a non-zero exit and an error message — surface that error verbatim, do NOT silently mark the report as saved.
|
|
316
|
+
>
|
|
317
|
+
> **Preview mechanics** (default): show takeaways + a small results table directly in chat. Use the `db_sample` rows Phase 2 already collected (top 10 by sort key). Format as a tight Markdown table with 2–4 type-relevant columns:
|
|
318
|
+
> - Type 3 (channels): `Channel | Subscribers | Last published`
|
|
319
|
+
> - Type 1 (videos/uploads): `Title | Channel | Views | Date`
|
|
320
|
+
> - Type 2 (brands): `Brand | Mentions | Channels`
|
|
321
|
+
> - Type 8 (deals/sponsorships): `Channel | Brand | Status | Send date`
|
|
322
|
+
>
|
|
323
|
+
> Then 2–4 takeaways (count, niche fit, noise warnings, sort note). Then a closing one-liner: *"If you want this saved as a campaign you can come back to, say save."* (Skip the line when the user's prompt was clearly purely informational like "are there any …".)
|
|
324
|
+
>
|
|
325
|
+
> **The JSON config never appears in chat in either path.** In save mode it's in the `/tmp/` file; in preview mode it stays in working memory. JSON in chat is implementation noise and a regression we already shipped a fix for once.
|
|
326
|
+
>
|
|
327
|
+
> **Edits** to a saved report use `tl reports update <id> '<json>'` — same shell-quoting caveat as save: when the patch contains apostrophes, write to a `/tmp/` file and use `tl reports update <id> "$(cat /tmp/<patch>.json)"`. Don't tell users to paste JSON into the platform UI; that's an obsolete pre-v0.6.12 fallback.
|
|
328
|
+
>
|
|
329
|
+
> **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.
|
|
232
330
|
|
|
233
331
|
## Phase 1 — Report Type Selection (detail)
|
|
234
332
|
|
|
@@ -1273,6 +1371,12 @@ Pseudo-shape (not runnable JSON — `<int>`, `|`-unions, and `/* notes */` are p
|
|
|
1273
1371
|
5. **Takeaways cite specifics.** Numbers, names, intent labels. Vague takeaways ("the report looks good") add no value.
|
|
1274
1372
|
6. **No new filters or columns in Phase 4.** Phase 4 doesn't reshape the FilterSet or add columns — it picks widgets, validates, and composes. Reshape requires looping back to Phase 2 or 3.
|
|
1275
1373
|
7. **Type-8 axis consistency.** Both `_over_<axis>` histograms in the same type-8 report use the SAME axis (per `sponsorship_widget_schema.json`'s `_tl_axis_branching`).
|
|
1374
|
+
8. **Don't echo `campaign_config_json` back to chat — ever.** In save mode the JSON lives in the `/tmp/` transport file passed to `tl reports create --config-file <path> --yes`. In preview mode it stays in working memory. **There is no flow where the campaign-config JSON belongs in the chat output.** See the Save-or-preview policy at the top of this file for the full split between save mode and preview mode.
|
|
1375
|
+
9. **When saving, use `--config-file <path>`, not `--config '<json>'`.** Passing JSON inline through a single-quoted shell argument breaks the moment any string value contains an apostrophe (which is common — "McDonald's", "L'Oréal", channel/title text). The temp-file transport sidesteps shell quoting entirely.
|
|
1376
|
+
10. **Temp file MUST be under `/tmp/`** (or `$TMPDIR` / `%TEMP%` — the system temp directory). Never write the transport file to the user's current working directory, project root, repo, or any other path they might be looking at. Pollution of cwd with `foo_report.json` is a regression bug.
|
|
1377
|
+
11. **Writing the file is NOT saving the report.** The save happens when `tl reports create --config-file <path> --yes` returns success. Until that command's exit code is read, the report does not exist. **Never tell the user "saved as <path>.json"** — that confuses the transport file (which is throwaway) with the saved Campaign (which is what they asked for). The save-success message must come from the CLI response: a `campaign_id` and `report_url`.
|
|
1378
|
+
12. **Default to preview, not save.** Phases 1–4 always run, but the chat output is takeaways + a sample-rows table by default. **Only save when the user's prompt contains explicit save intent** — see the Save-or-preview policy near the top for the trigger word lists. Ambiguous middle ("build a report on X", "create a campaign for Y") → preview + the closing "say save" tail. Save is the explicit, opt-in path; preview is the conservative default.
|
|
1379
|
+
13. **In preview mode the agent does not invoke `tl reports create`** and does not write a temp file. The campaign config stays in working memory. If the user follows up with "save" / "yes" / "go ahead", re-use that same in-memory config — do not re-run Phases 1–4.
|
|
1276
1380
|
|
|
1277
1381
|
## Follow-Up Interactions
|
|
1278
1382
|
|
|
@@ -1327,7 +1431,7 @@ USER: Build me a report of gaming channels with 100K+ subscribers in English
|
|
|
1327
1431
|
|
|
1328
1432
|
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
1433
|
|
|
1330
|
-
> **
|
|
1434
|
+
> **Save vs preview**: by default the skill runs Phases 1–4 and replies with takeaways + a sample-rows table — **no save**. Only when the user's prompt contains explicit save intent ("save", "create the report", "make a campaign for me to come back to") does the skill (1) write the JSON to a `/tmp/<slug>.json` file via the `Write` tool, then (2) run `tl reports create --config-file /tmp/<slug>.json --yes` via `Bash`. The file transport is shell-safe; passing the JSON inline as `--config '<json>'` breaks the moment any value contains an apostrophe ("McDonald's", "L'Oréal"). The user sees the takeaways and (in save mode) the resulting campaign link. **The JSON config never appears in chat in either mode.** For edits to an existing saved report, use `tl reports update <report_id> '<json patch>'` (same shell-quoting caveat — use a `/tmp/` file when the patch contains apostrophes). Do NOT tell users to paste into the platform UI — that's an obsolete fallback from before the CLI commands existed. See the Save-or-preview policy near the top for the full trigger word lists.
|
|
1331
1435
|
|
|
1332
1436
|
## Reference Files
|
|
1333
1437
|
|
|
@@ -296,7 +296,7 @@ def _orchestrate_via_server(
|
|
|
296
296
|
def create_report(
|
|
297
297
|
prompt: str | None = typer.Argument(
|
|
298
298
|
None,
|
|
299
|
-
help="Natural language description of the report you want. Omit when using --config.",
|
|
299
|
+
help="Natural language description of the report you want. Omit when using --config or --config-file.",
|
|
300
300
|
),
|
|
301
301
|
config_json: str | None = typer.Option(
|
|
302
302
|
None,
|
|
@@ -304,7 +304,18 @@ def create_report(
|
|
|
304
304
|
help=(
|
|
305
305
|
"Pre-built report config as JSON. Skips the AI Report Builder "
|
|
306
306
|
"pipeline and saves the config directly. Mutually exclusive with "
|
|
307
|
-
"the prompt argument."
|
|
307
|
+
"the prompt argument and --config-file."
|
|
308
|
+
),
|
|
309
|
+
),
|
|
310
|
+
config_file: str | None = typer.Option(
|
|
311
|
+
None,
|
|
312
|
+
"--config-file",
|
|
313
|
+
help=(
|
|
314
|
+
"Path to a file containing a pre-built report config in JSON. "
|
|
315
|
+
"Use this instead of --config when the config may contain "
|
|
316
|
+
"characters that are awkward to shell-escape (apostrophes in "
|
|
317
|
+
"titles or keywords, etc.). Mutually exclusive with --config "
|
|
318
|
+
"and the prompt argument."
|
|
308
319
|
),
|
|
309
320
|
),
|
|
310
321
|
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
|
|
@@ -316,27 +327,40 @@ def create_report(
|
|
|
316
327
|
With a prompt, runs the AI Report Builder pipeline (keyword research, config
|
|
317
328
|
generation, review) and saves the resulting campaign.
|
|
318
329
|
|
|
319
|
-
With --config '<json>'
|
|
320
|
-
provided config directly. Useful when an external
|
|
321
|
-
tl-report-builder Claude Code skill) has already produced a
|
|
322
|
-
config and you just want to persist it.
|
|
330
|
+
With --config '<json>' or --config-file <path>, skips the orchestration
|
|
331
|
+
pipeline and saves the provided config directly. Useful when an external
|
|
332
|
+
agent (e.g. the tl-report-builder Claude Code skill) has already produced a
|
|
333
|
+
validated config and you just want to persist it. Prefer --config-file when
|
|
334
|
+
the config might contain apostrophes, dollar signs, or backticks — file
|
|
335
|
+
transport sidesteps shell quoting entirely.
|
|
323
336
|
|
|
324
337
|
Examples:
|
|
325
338
|
tl reports create "gaming channels sponsoring energy drinks"
|
|
326
339
|
tl reports create "tech review channels with 100K+ subscribers" --yes
|
|
340
|
+
tl reports create --config-file /tmp/config.json --yes
|
|
327
341
|
tl reports create --config "$(cat config.json)" --yes
|
|
328
342
|
"""
|
|
329
|
-
|
|
343
|
+
sources_provided = sum(x is not None for x in (prompt, config_json, config_file))
|
|
344
|
+
if sources_provided != 1:
|
|
330
345
|
err.print(
|
|
331
|
-
"[red]Provide
|
|
346
|
+
"[red]Provide exactly one of: a natural-language prompt, --config '<json>', or --config-file <path>.[/red]"
|
|
332
347
|
)
|
|
333
348
|
raise typer.Exit(1)
|
|
334
349
|
|
|
335
350
|
client = get_client()
|
|
336
351
|
try:
|
|
337
|
-
if
|
|
338
|
-
|
|
352
|
+
if config_file is not None:
|
|
353
|
+
try:
|
|
354
|
+
with open(config_file, encoding="utf-8") as fh:
|
|
355
|
+
config_text = fh.read()
|
|
356
|
+
except OSError as exc:
|
|
357
|
+
err.print(f"[red]Could not read --config-file: {exc}[/red]")
|
|
358
|
+
raise typer.Exit(1)
|
|
359
|
+
config = _parse_config_arg(config_text)
|
|
339
360
|
saved_prompts: list[str] = []
|
|
361
|
+
elif config_json is not None:
|
|
362
|
+
config = _parse_config_arg(config_json)
|
|
363
|
+
saved_prompts = []
|
|
340
364
|
else:
|
|
341
365
|
config = _orchestrate_via_server(client, prompt, timeout)
|
|
342
366
|
saved_prompts = [prompt]
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Tests for `tl reports create --config[-file]` and `tl reports update`."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
import typer
|
|
8
|
+
from typer.testing import CliRunner
|
|
9
|
+
|
|
10
|
+
from tl_cli.commands.reports import _parse_config_arg, app
|
|
11
|
+
|
|
12
|
+
runner = CliRunner()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
# _parse_config_arg
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TestParseConfigArg:
|
|
21
|
+
def test_valid_object_returns_dict(self) -> None:
|
|
22
|
+
result = _parse_config_arg('{"report_title": "Test", "report_type": 3}')
|
|
23
|
+
assert result == {"report_title": "Test", "report_type": 3}
|
|
24
|
+
|
|
25
|
+
def test_invalid_json_exits(self) -> None:
|
|
26
|
+
with pytest.raises(typer.Exit) as excinfo:
|
|
27
|
+
_parse_config_arg('{not json')
|
|
28
|
+
assert excinfo.value.exit_code == 1
|
|
29
|
+
|
|
30
|
+
def test_non_object_exits(self) -> None:
|
|
31
|
+
# JSON arrays / strings / numbers are valid JSON but not the object the
|
|
32
|
+
# endpoint accepts.
|
|
33
|
+
with pytest.raises(typer.Exit) as excinfo:
|
|
34
|
+
_parse_config_arg('[1, 2, 3]')
|
|
35
|
+
assert excinfo.value.exit_code == 1
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
# tl reports create — argument validation
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class TestCreateArgValidation:
|
|
44
|
+
def test_no_prompt_and_no_config_rejected(self) -> None:
|
|
45
|
+
result = runner.invoke(app, ["create"])
|
|
46
|
+
assert result.exit_code == 1
|
|
47
|
+
msg = (result.stderr or result.output).lower()
|
|
48
|
+
assert "exactly one" in msg and "config" in msg
|
|
49
|
+
|
|
50
|
+
def test_both_prompt_and_config_rejected(self) -> None:
|
|
51
|
+
result = runner.invoke(
|
|
52
|
+
app,
|
|
53
|
+
["create", "gaming channels", "--config", '{"report_title": "x", "report_type": 3}'],
|
|
54
|
+
)
|
|
55
|
+
assert result.exit_code == 1
|
|
56
|
+
assert "exactly one" in (result.stderr or result.output).lower()
|
|
57
|
+
|
|
58
|
+
def test_both_config_and_config_file_rejected(self, tmp_path: Path) -> None:
|
|
59
|
+
cfg = tmp_path / "c.json"
|
|
60
|
+
cfg.write_text('{"report_title": "x", "report_type": 3}', encoding="utf-8")
|
|
61
|
+
result = runner.invoke(
|
|
62
|
+
app,
|
|
63
|
+
["create", "--config", '{"report_title": "y"}', "--config-file", str(cfg)],
|
|
64
|
+
)
|
|
65
|
+
assert result.exit_code == 1
|
|
66
|
+
assert "exactly one" in (result.stderr or result.output).lower()
|
|
67
|
+
|
|
68
|
+
def test_both_prompt_and_config_file_rejected(self, tmp_path: Path) -> None:
|
|
69
|
+
cfg = tmp_path / "c.json"
|
|
70
|
+
cfg.write_text('{"report_title": "x", "report_type": 3}', encoding="utf-8")
|
|
71
|
+
result = runner.invoke(app, ["create", "gaming", "--config-file", str(cfg)])
|
|
72
|
+
assert result.exit_code == 1
|
|
73
|
+
assert "exactly one" in (result.stderr or result.output).lower()
|
|
74
|
+
|
|
75
|
+
def test_config_invalid_json_rejected(self) -> None:
|
|
76
|
+
result = runner.invoke(app, ["create", "--config", "{not json", "--yes"])
|
|
77
|
+
assert result.exit_code == 1
|
|
78
|
+
assert "valid json" in (result.stderr or result.output).lower()
|
|
79
|
+
|
|
80
|
+
def test_config_non_object_rejected(self) -> None:
|
|
81
|
+
result = runner.invoke(app, ["create", "--config", "[1,2,3]", "--yes"])
|
|
82
|
+
assert result.exit_code == 1
|
|
83
|
+
assert "json object" in (result.stderr or result.output).lower()
|
|
84
|
+
|
|
85
|
+
def test_config_file_missing_path_rejected(self, tmp_path: Path) -> None:
|
|
86
|
+
missing = tmp_path / "does-not-exist.json"
|
|
87
|
+
result = runner.invoke(app, ["create", "--config-file", str(missing), "--yes"])
|
|
88
|
+
assert result.exit_code == 1
|
|
89
|
+
assert "could not read" in (result.stderr or result.output).lower()
|
|
90
|
+
|
|
91
|
+
def test_config_file_invalid_json_rejected(self, tmp_path: Path) -> None:
|
|
92
|
+
cfg = tmp_path / "broken.json"
|
|
93
|
+
cfg.write_text("{not json", encoding="utf-8")
|
|
94
|
+
result = runner.invoke(app, ["create", "--config-file", str(cfg), "--yes"])
|
|
95
|
+
assert result.exit_code == 1
|
|
96
|
+
assert "valid json" in (result.stderr or result.output).lower()
|
|
97
|
+
|
|
98
|
+
def test_config_file_handles_apostrophes(self, tmp_path: Path) -> None:
|
|
99
|
+
# The whole point of --config-file: shell-quoting woes don't apply when
|
|
100
|
+
# the JSON lives in a file. A title with an apostrophe must round-trip.
|
|
101
|
+
cfg = tmp_path / "quote.json"
|
|
102
|
+
cfg.write_text(
|
|
103
|
+
json.dumps({"report_title": "McDonald's gaming pipeline", "report_type": 3}),
|
|
104
|
+
encoding="utf-8",
|
|
105
|
+
)
|
|
106
|
+
# We don't have a live API to POST to in unit tests, so we stop short of
|
|
107
|
+
# a successful save. But the parse step should not blow up on the
|
|
108
|
+
# apostrophe — what we're guarding against is "valid json" complaints.
|
|
109
|
+
# Use --json so the command exits cleanly after preview without
|
|
110
|
+
# prompting (no confirm + no save).
|
|
111
|
+
result = runner.invoke(
|
|
112
|
+
app, ["create", "--config-file", str(cfg), "--json"]
|
|
113
|
+
)
|
|
114
|
+
# Command exits 0 after preview when --json is set without --yes.
|
|
115
|
+
assert result.exit_code == 0
|
|
116
|
+
assert "McDonald's" in (result.stdout or result.output)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# ---------------------------------------------------------------------------
|
|
120
|
+
# tl reports update — argument validation
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class TestUpdateArgValidation:
|
|
125
|
+
def test_invalid_json_rejected(self) -> None:
|
|
126
|
+
result = runner.invoke(app, ["update", "12345", "{not json"])
|
|
127
|
+
assert result.exit_code == 1
|
|
128
|
+
assert "json object" in (result.stderr or result.output).lower()
|
|
129
|
+
|
|
130
|
+
def test_non_object_rejected(self) -> None:
|
|
131
|
+
result = runner.invoke(app, ["update", "12345", '"just a string"'])
|
|
132
|
+
assert result.exit_code == 1
|
|
133
|
+
assert "json object" in (result.stderr or result.output).lower()
|
|
@@ -1,79 +0,0 @@
|
|
|
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.12 → thoughtleaders_cli-0.6.15}/.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
|
|
File without changes
|
{thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/skills/tl/references/business-glossary.md
RENAMED
|
File without changes
|
{thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/skills/tl/references/elasticsearch-schema.md
RENAMED
|
File without changes
|
{thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/skills/tl/references/firebolt-schema.md
RENAMED
|
File without changes
|
{thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/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
|
{thoughtleaders_cli-0.6.12 → thoughtleaders_cli-0.6.15}/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
|
|
File without changes
|
|
File without changes
|