sv-cli 0.3.0__tar.gz → 0.4.0__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 (79) hide show
  1. {sv_cli-0.3.0 → sv_cli-0.4.0}/AGENT.md +23 -17
  2. {sv_cli-0.3.0 → sv_cli-0.4.0}/PKG-INFO +12 -6
  3. {sv_cli-0.3.0 → sv_cli-0.4.0}/README.md +11 -5
  4. {sv_cli-0.3.0 → sv_cli-0.4.0}/pyproject.toml +1 -1
  5. {sv_cli-0.3.0 → sv_cli-0.4.0}/src/sv_cli/adapters.py +23 -3
  6. {sv_cli-0.3.0 → sv_cli-0.4.0}/src/sv_cli/api_client.py +1 -1
  7. {sv_cli-0.3.0 → sv_cli-0.4.0}/src/sv_cli/main.py +175 -28
  8. {sv_cli-0.3.0 → sv_cli-0.4.0}/src/sv_cli/resolver.py +34 -0
  9. sv_cli-0.4.0/tests/fixtures/ranklens_definitions.json +16 -0
  10. sv_cli-0.4.0/tests/fixtures/seogpt2_definitions.json +15 -0
  11. sv_cli-0.4.0/tests/test_executor.py +150 -0
  12. sv_cli-0.4.0/tests/test_help_filtering.py +80 -0
  13. {sv_cli-0.3.0 → sv_cli-0.4.0}/tests/test_resolver.py +48 -0
  14. sv_cli-0.3.0/tests/test_executor.py +0 -76
  15. {sv_cli-0.3.0 → sv_cli-0.4.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  16. {sv_cli-0.3.0 → sv_cli-0.4.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  17. {sv_cli-0.3.0 → sv_cli-0.4.0}/.github/workflows/publish.yml +0 -0
  18. {sv_cli-0.3.0 → sv_cli-0.4.0}/.github/workflows/release.yml +0 -0
  19. {sv_cli-0.3.0 → sv_cli-0.4.0}/.github/workflows/test.yml +0 -0
  20. {sv_cli-0.3.0 → sv_cli-0.4.0}/.gitignore +0 -0
  21. {sv_cli-0.3.0 → sv_cli-0.4.0}/CHANGELOG.md +0 -0
  22. {sv_cli-0.3.0 → sv_cli-0.4.0}/CODE_OF_CONDUCT.md +0 -0
  23. {sv_cli-0.3.0 → sv_cli-0.4.0}/CONTRIBUTING.md +0 -0
  24. {sv_cli-0.3.0 → sv_cli-0.4.0}/LICENSE +0 -0
  25. {sv_cli-0.3.0 → sv_cli-0.4.0}/PULL_REQUEST_TEMPLATE.md +0 -0
  26. {sv_cli-0.3.0 → sv_cli-0.4.0}/SECURITY.md +0 -0
  27. {sv_cli-0.3.0 → sv_cli-0.4.0}/docs/agent-usage.md +0 -0
  28. {sv_cli-0.3.0 → sv_cli-0.4.0}/docs/api-definition-format.md +0 -0
  29. {sv_cli-0.3.0 → sv_cli-0.4.0}/docs/authentication.md +0 -0
  30. {sv_cli-0.3.0 → sv_cli-0.4.0}/docs/commands.md +0 -0
  31. {sv_cli-0.3.0 → sv_cli-0.4.0}/docs/examples.md +0 -0
  32. {sv_cli-0.3.0 → sv_cli-0.4.0}/docs/installation.md +0 -0
  33. {sv_cli-0.3.0 → sv_cli-0.4.0}/docs/troubleshooting.md +0 -0
  34. {sv_cli-0.3.0 → sv_cli-0.4.0}/src/sv_cli/__init__.py +0 -0
  35. {sv_cli-0.3.0 → sv_cli-0.4.0}/src/sv_cli/commands/__init__.py +0 -0
  36. {sv_cli-0.3.0 → sv_cli-0.4.0}/src/sv_cli/commands/auth.py +0 -0
  37. {sv_cli-0.3.0 → sv_cli-0.4.0}/src/sv_cli/commands/better_keywords.py +0 -0
  38. {sv_cli-0.3.0 → sv_cli-0.4.0}/src/sv_cli/commands/call.py +0 -0
  39. {sv_cli-0.3.0 → sv_cli-0.4.0}/src/sv_cli/commands/config.py +0 -0
  40. {sv_cli-0.3.0 → sv_cli-0.4.0}/src/sv_cli/commands/content_quality.py +0 -0
  41. {sv_cli-0.3.0 → sv_cli-0.4.0}/src/sv_cli/commands/content_transformer.py +0 -0
  42. {sv_cli-0.3.0 → sv_cli-0.4.0}/src/sv_cli/commands/core_analysis.py +0 -0
  43. {sv_cli-0.3.0 → sv_cli-0.4.0}/src/sv_cli/commands/definitions.py +0 -0
  44. {sv_cli-0.3.0 → sv_cli-0.4.0}/src/sv_cli/commands/geo_audit.py +0 -0
  45. {sv_cli-0.3.0 → sv_cli-0.4.0}/src/sv_cli/commands/insight_igniter.py +0 -0
  46. {sv_cli-0.3.0 → sv_cli-0.4.0}/src/sv_cli/commands/marketplace_services.py +0 -0
  47. {sv_cli-0.3.0 → sv_cli-0.4.0}/src/sv_cli/commands/options.py +0 -0
  48. {sv_cli-0.3.0 → sv_cli-0.4.0}/src/sv_cli/commands/preliminary_audit.py +0 -0
  49. {sv_cli-0.3.0 → sv_cli-0.4.0}/src/sv_cli/commands/ranklens.py +0 -0
  50. {sv_cli-0.3.0 → sv_cli-0.4.0}/src/sv_cli/commands/seo_image.py +0 -0
  51. {sv_cli-0.3.0 → sv_cli-0.4.0}/src/sv_cli/commands/seo_mapping.py +0 -0
  52. {sv_cli-0.3.0 → sv_cli-0.4.0}/src/sv_cli/commands/seogpt.py +0 -0
  53. {sv_cli-0.3.0 → sv_cli-0.4.0}/src/sv_cli/commands/seogpt2.py +0 -0
  54. {sv_cli-0.3.0 → sv_cli-0.4.0}/src/sv_cli/commands/seogpt_compare.py +0 -0
  55. {sv_cli-0.3.0 → sv_cli-0.4.0}/src/sv_cli/commands/top_competitors.py +0 -0
  56. {sv_cli-0.3.0 → sv_cli-0.4.0}/src/sv_cli/commands/topical_authority.py +0 -0
  57. {sv_cli-0.3.0 → sv_cli-0.4.0}/src/sv_cli/config.py +0 -0
  58. {sv_cli-0.3.0 → sv_cli-0.4.0}/src/sv_cli/definitions.py +0 -0
  59. {sv_cli-0.3.0 → sv_cli-0.4.0}/src/sv_cli/errors.py +0 -0
  60. {sv_cli-0.3.0 → sv_cli-0.4.0}/src/sv_cli/executor.py +0 -0
  61. {sv_cli-0.3.0 → sv_cli-0.4.0}/src/sv_cli/formatter.py +0 -0
  62. {sv_cli-0.3.0 → sv_cli-0.4.0}/src/sv_cli/renderers/__init__.py +0 -0
  63. {sv_cli-0.3.0 → sv_cli-0.4.0}/src/sv_cli/renderers/csv_renderer.py +0 -0
  64. {sv_cli-0.3.0 → sv_cli-0.4.0}/src/sv_cli/renderers/json_renderer.py +0 -0
  65. {sv_cli-0.3.0 → sv_cli-0.4.0}/src/sv_cli/renderers/markdown_renderer.py +0 -0
  66. {sv_cli-0.3.0 → sv_cli-0.4.0}/src/sv_cli/renderers/table_renderer.py +0 -0
  67. {sv_cli-0.3.0 → sv_cli-0.4.0}/src/sv_cli/renderers/text_renderer.py +0 -0
  68. {sv_cli-0.3.0 → sv_cli-0.4.0}/src/sv_cli/schemas/__init__.py +0 -0
  69. {sv_cli-0.3.0 → sv_cli-0.4.0}/src/sv_cli/schemas/api_response.py +0 -0
  70. {sv_cli-0.3.0 → sv_cli-0.4.0}/src/sv_cli/schemas/config.py +0 -0
  71. {sv_cli-0.3.0 → sv_cli-0.4.0}/src/sv_cli/schemas/tool_definition.py +0 -0
  72. {sv_cli-0.3.0 → sv_cli-0.4.0}/src/sv_cli/tasks.py +0 -0
  73. {sv_cli-0.3.0 → sv_cli-0.4.0}/src/sv_cli/utils.py +0 -0
  74. {sv_cli-0.3.0 → sv_cli-0.4.0}/tests/fixtures/api_root.json +0 -0
  75. {sv_cli-0.3.0 → sv_cli-0.4.0}/tests/fixtures/better_keywords_response.json +0 -0
  76. {sv_cli-0.3.0 → sv_cli-0.4.0}/tests/fixtures/seogpt_definitions.json +0 -0
  77. {sv_cli-0.3.0 → sv_cli-0.4.0}/tests/test_config.py +0 -0
  78. {sv_cli-0.3.0 → sv_cli-0.4.0}/tests/test_definitions.py +0 -0
  79. {sv_cli-0.3.0 → sv_cli-0.4.0}/tests/test_formatter.py +0 -0
@@ -55,16 +55,16 @@ All 16 available tools. Use exact command names — aliases are human shortcuts.
55
55
 
56
56
  | Command | Alias | Default Action | API Required Fields | Async |
57
57
  |---|---|---|---|---|
58
- | `better-keywords` | `keywords` | `research` / `filter` | `--keyword` (`filter` also needs `--data`) | No |
58
+ | `better-keywords` | `keywords` | `research` / `filter` | `--keyword` (`filter` needs a JSON `data` array — raw call only) | No |
59
59
  | `content-transformer` | `transform` | `rewrite` | `--text` | No |
60
60
  | `core-analysis` | `core` | `analyze` | *(none required)* | No |
61
61
  | `geo-audit` | `audit` | `create-task` | `--url` `--keyword` | Yes |
62
62
  | `insight-igniter` | `insights` | `entities` | `--url` | No |
63
63
  | `preliminary-audit` | `prelim-audit` | `analyze` | `--url` | No |
64
- | `ranklens` | — | `rank` | `--keyword` `--url` | No |
64
+ | `ranklens` | — | `rank` | `--entity` `--url` | No |
65
65
  | `seo-image` | `image` | `generate` | `--keyword` | No |
66
66
  | `seogpt` | `seo-gpt` | `generate` | `--keyword` `--type` | No |
67
- | `seogpt2` | `seo-gpt2` | `create-task` | `--keyword` *(= Topic)* | Yes |
67
+ | `seogpt2` | `seo-gpt2` | `create-task` | `--topic` *(`--keyword`/`--kw` is a separate, optional field)* | Yes |
68
68
  | `seogpt-compare` | `compare` | `create-task` | `--url` `--keyword` | Yes |
69
69
  | `seo-mapping` | `mapping` | `create-task` | `--url` `--keyword` | Yes |
70
70
  | `topical-authority` | `topical` | `topics` | `--keyword` | No |
@@ -112,6 +112,7 @@ sv options seogpt type --search meta
112
112
  # Shorthand subcommands (where available)
113
113
  sv seogpt types
114
114
  sv seogpt types --search meta
115
+ sv seogpt lengths
115
116
  sv seogpt languages
116
117
  sv seogpt engines
117
118
  sv image types
@@ -128,6 +129,7 @@ sv seo-mapping types
128
129
  sv better-keywords types
129
130
  sv better-keywords languages
130
131
  sv content-transformer types
132
+ sv content-transformer lengths
131
133
  sv content-transformer languages
132
134
  sv ranklens languages
133
135
  sv ranklens engines
@@ -241,8 +243,8 @@ sv --format json task result TASK_ID --tool geo-audit
241
243
  ### Pattern 3: Direct tool polling
242
244
 
243
245
  ```bash
244
- sv geo-audit get-task-status --task_id TASK_ID --format json
245
- sv geo-audit get-result --task_id TASK_ID --format json
246
+ sv geo-audit get-task-status --task-id TASK_ID --format json
247
+ sv geo-audit get-result --task-id TASK_ID --format json
246
248
  ```
247
249
 
248
250
  Same for `seogpt2`, `seogpt-compare`, `seo-mapping` — replace `geo-audit` with the tool name.
@@ -253,14 +255,17 @@ Same for `seogpt2`, `seogpt-compare`, `seo-mapping` — replace `geo-audit` with
253
255
 
254
256
  These differ from the standard pattern — get them wrong and the call fails.
255
257
 
256
- ### seogpt2 — `--keyword` maps to `Topic`, not `kw`
258
+ ### seogpt2 — `--topic` maps to `Topic` (required), `--keyword`/`--kw` is a separate field
257
259
 
258
260
  ```bash
259
- # CORRECT — --keyword sends value as "Topic" API field
260
- sv seogpt2 create-task --keyword "White Label SEO for Agencies" --type on-page-blog-article --wait --strict --no-fuzzy --non-interactive --format json
261
+ # CORRECT — --topic sends value as the required "Topic" API field
262
+ sv seogpt2 create-task --topic "White Label SEO for Agencies" --type on-page-blog-article --wait --strict --no-fuzzy --non-interactive --format json
261
263
 
262
- # WRONG no other flag maps to Topic
263
- sv seogpt2 create-task --text "..." ... # also maps to Topic, use --keyword
264
+ # --title is an alias for --topic (same field)
265
+ sv seogpt2 create-task --title "White Label SEO for Agencies" --type on-page-blog-article --wait --strict --no-fuzzy --non-interactive --format json
266
+
267
+ # --keyword/--kw maps to the separate, optional KW field — it does NOT set Topic
268
+ sv seogpt2 create-task --topic "White Label SEO for Agencies" --keyword "white label seo" --type on-page-blog-article --wait --strict --no-fuzzy --non-interactive --format json
264
269
  ```
265
270
 
266
271
  ### better-keywords `filter` — requires `data` array from prior `research` call
@@ -288,11 +293,12 @@ No CLI flag exists for `mgptid`. Must use raw call:
288
293
 
289
294
  ```bash
290
295
  # Step 1 — get MGPTID from rank response
291
- sv ranklens rank --keyword "white label seo" --url https://example.com --format json
296
+ sv ranklens rank --entity "white label seo" --url https://example.com --format json
292
297
  # → parse response["data"]["MGPTID"]
298
+ # response["data"] always uses "entity", never "keyword" — --keyword/--kw still works as an input alias, but the response field name is always "entity"
293
299
 
294
300
  # Step 2 — pass MGPTID via raw call
295
- sv --format json call ranklens --json '{"action":"competitors","mgptid":"MGPTID_VALUE","web":"https://example.com","kw":"white label seo"}'
301
+ sv --format json call ranklens --json '{"action":"competitors","mgptid":"MGPTID_VALUE","web":"https://example.com","entity":"white label seo"}'
296
302
  ```
297
303
 
298
304
  ### `sv task status/result` — `--format` must be global
@@ -313,7 +319,7 @@ sv task status TASK_ID --tool geo-audit --format json
313
319
  |---|---|---|
314
320
  | `Could not resolve --type "X" in strict mode` | Value is not a valid slug or ID | Run `sv options TOOL type` → use `id` or `slug` column |
315
321
  | `Could not resolve --type "X"` (no strict) | No match found at any level | Run `sv options TOOL type --search X` to find closest match |
316
- | `Topic is required` | seogpt2 called without `--keyword` | Add `--keyword "your topic"` |
322
+ | `Topic is required` | seogpt2 called without `--topic` | Add `--topic "your topic"` |
317
323
  | `task_id is invalid` | Task has expired on the server | Create a new task |
318
324
  | `No local tool mapping found for task` | Task not in `~/.sv/tasks.json` | Add `--tool TOOL_NAME` explicitly |
319
325
  | `API authentication failed: HTTP 401` | Bad or missing API key | Check `SV_API_KEY` environment variable |
@@ -331,7 +337,7 @@ When friendly flags are insufficient, send the payload directly. The CLI injects
331
337
  ```bash
332
338
  sv --format json call seogpt --json '{"action":"generate","kw":"white label seo","type":18}'
333
339
  sv --format json call geo-audit --json '{"action":"createTask","kw":"white label seo","URL":"https://example.com"}'
334
- sv --format json call ranklens --json '{"action":"competitors","mgptid":"...","web":"https://example.com","kw":"white label seo"}'
340
+ sv --format json call ranklens --json '{"action":"competitors","mgptid":"...","web":"https://example.com","entity":"white label seo"}'
335
341
  ```
336
342
 
337
343
  From a file:
@@ -370,12 +376,12 @@ sv --format json call better-keywords --json '{"action":"filter","kw":"white lab
370
376
  sv content-transformer rewrite --text "SEO services for agencies" --keyword "white label seo" --strict --no-fuzzy --non-interactive --format json
371
377
  sv core-analysis analyze --url https://example.com --keyword "white label seo" --strict --no-fuzzy --non-interactive --format json
372
378
  sv geo-audit create-task --url https://example.com --keyword "white label seo" --wait --strict --no-fuzzy --non-interactive --format json
373
- sv insight-igniter entities --url https://example.com --keyword "white label seo" --strict --no-fuzzy --non-interactive --format json
379
+ sv insight-igniter entities --url https://example.com --strict --no-fuzzy --non-interactive --format json
374
380
  sv preliminary-audit analyze --url https://example.com --strict --no-fuzzy --non-interactive --format json
375
- sv ranklens rank --keyword "white label seo" --url https://example.com --strict --no-fuzzy --non-interactive --format json
381
+ sv ranklens rank --entity "white label seo" --url https://example.com --strict --no-fuzzy --non-interactive --format json
376
382
  sv seo-image generate --keyword "white label seo" --type 33 --strict --no-fuzzy --non-interactive --format json
377
383
  sv seogpt generate --keyword "white label seo" --type 18 --strict --no-fuzzy --non-interactive --format json
378
- sv seogpt2 create-task --keyword "White Label SEO for Agencies" --type 0 --wait --strict --no-fuzzy --non-interactive --format json
384
+ sv seogpt2 create-task --topic "White Label SEO for Agencies" --type 0 --wait --strict --no-fuzzy --non-interactive --format json
379
385
  sv seogpt-compare create-task --url https://example.com --keyword "white label seo" --wait --strict --no-fuzzy --non-interactive --format json
380
386
  sv seo-mapping create-task --url https://example.com --keyword "white label seo" --wait --strict --no-fuzzy --non-interactive --format json
381
387
  sv topical-authority topics --keyword "white label seo" --strict --no-fuzzy --non-interactive --format json
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sv-cli
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Open-source, definition-driven command-line client for SV AI API tools.
5
5
  Project-URL: Homepage, https://github.com/seovendorco/sv-cli
6
6
  Project-URL: Issues, https://github.com/seovendorco/sv-cli/issues
@@ -175,7 +175,7 @@ The canonical API tool keys are discovered from the live API root; local aliases
175
175
 
176
176
  ## Enum resolution
177
177
 
178
- For enum-heavy fields such as content type, language, engine, image type, theme, background, color, and size, the CLI resolves values in this order:
178
+ For enum-heavy fields such as content type, content length, language, engine, image type, theme, background, color, and size, the CLI resolves values in this order:
179
179
 
180
180
  1. Numeric ID exact match
181
181
  2. Exact slug match
@@ -196,6 +196,12 @@ Examples that all resolve to Meta Description (id 18):
196
196
  --type "meta desc"
197
197
  ```
198
198
 
199
+ `--length`/`--contentlength` (content length by ID, slug, or label — run `sv TOOL lengths` for valid values) resolves the same way:
200
+
201
+ ```bash
202
+ sv seogpt generate --keyword "white label seo" --type 18 --length 300
203
+ ```
204
+
199
205
  > **Note:** Fuzzy matching is enabled by default. Use `--strict --no-fuzzy` in scripts to require exact ID or slug and avoid unintended matches.
200
206
 
201
207
  Agent-safe mode:
@@ -246,10 +252,10 @@ sv task status TASK_ID --tool geo-audit
246
252
  sv task result TASK_ID --tool geo-audit
247
253
  ```
248
254
 
249
- `seogpt2` is another async tool. Its required field is `Topic` (a title or subject), mapped via `--keyword`:
255
+ `seogpt2` is another async tool. Its required field is `Topic` (a title or subject), mapped via `--topic` (`--title` is an alias for the same field). `--keyword`/`--kw` is a separate, optional field for additional keywords — it does not set the topic:
250
256
 
251
257
  ```bash
252
- sv seogpt2 create-task --keyword "White Label SEO for Agencies" --type on-page-blog-article --wait
258
+ sv seogpt2 create-task --topic "White Label SEO for Agencies" --type on-page-blog-article --wait
253
259
  ```
254
260
 
255
261
  See available types with `sv seogpt2 types`, lengths with `sv seogpt2 lengths`, engines with `sv seogpt2 engines`.
@@ -258,8 +264,8 @@ Manual 3-step flow (without `--wait`):
258
264
 
259
265
  ```bash
260
266
  sv geo-audit create-task --url https://example.com --keyword "seo agency,white label seo"
261
- sv geo-audit get-task-status --task_id TASK_ID
262
- sv geo-audit get-result --task_id TASK_ID
267
+ sv geo-audit get-task-status --task-id TASK_ID
268
+ sv geo-audit get-result --task-id TASK_ID
263
269
  ```
264
270
 
265
271
  > **Note:** `--format` is not available on `sv task status` or `sv task result` directly. Place it before `task` as a global flag:
@@ -140,7 +140,7 @@ The canonical API tool keys are discovered from the live API root; local aliases
140
140
 
141
141
  ## Enum resolution
142
142
 
143
- For enum-heavy fields such as content type, language, engine, image type, theme, background, color, and size, the CLI resolves values in this order:
143
+ For enum-heavy fields such as content type, content length, language, engine, image type, theme, background, color, and size, the CLI resolves values in this order:
144
144
 
145
145
  1. Numeric ID exact match
146
146
  2. Exact slug match
@@ -161,6 +161,12 @@ Examples that all resolve to Meta Description (id 18):
161
161
  --type "meta desc"
162
162
  ```
163
163
 
164
+ `--length`/`--contentlength` (content length by ID, slug, or label — run `sv TOOL lengths` for valid values) resolves the same way:
165
+
166
+ ```bash
167
+ sv seogpt generate --keyword "white label seo" --type 18 --length 300
168
+ ```
169
+
164
170
  > **Note:** Fuzzy matching is enabled by default. Use `--strict --no-fuzzy` in scripts to require exact ID or slug and avoid unintended matches.
165
171
 
166
172
  Agent-safe mode:
@@ -211,10 +217,10 @@ sv task status TASK_ID --tool geo-audit
211
217
  sv task result TASK_ID --tool geo-audit
212
218
  ```
213
219
 
214
- `seogpt2` is another async tool. Its required field is `Topic` (a title or subject), mapped via `--keyword`:
220
+ `seogpt2` is another async tool. Its required field is `Topic` (a title or subject), mapped via `--topic` (`--title` is an alias for the same field). `--keyword`/`--kw` is a separate, optional field for additional keywords — it does not set the topic:
215
221
 
216
222
  ```bash
217
- sv seogpt2 create-task --keyword "White Label SEO for Agencies" --type on-page-blog-article --wait
223
+ sv seogpt2 create-task --topic "White Label SEO for Agencies" --type on-page-blog-article --wait
218
224
  ```
219
225
 
220
226
  See available types with `sv seogpt2 types`, lengths with `sv seogpt2 lengths`, engines with `sv seogpt2 engines`.
@@ -223,8 +229,8 @@ Manual 3-step flow (without `--wait`):
223
229
 
224
230
  ```bash
225
231
  sv geo-audit create-task --url https://example.com --keyword "seo agency,white label seo"
226
- sv geo-audit get-task-status --task_id TASK_ID
227
- sv geo-audit get-result --task_id TASK_ID
232
+ sv geo-audit get-task-status --task-id TASK_ID
233
+ sv geo-audit get-result --task-id TASK_ID
228
234
  ```
229
235
 
230
236
  > **Note:** `--format` is not available on `sv task status` or `sv task result` directly. Place it before `task` as a global flag:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sv-cli"
7
- version = "0.3.0"
7
+ version = "0.4.0"
8
8
  description = "Open-source, definition-driven command-line client for SV AI API tools."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -31,11 +31,19 @@ class ToolAdapter:
31
31
  COMMON_FIELD_ALIASES: dict[str, tuple[str, ...]] = {
32
32
  "keyword": ("keyword", "kw", "query", "seed_keyword"),
33
33
  "keywords": ("keywords", "kws", "keyword_list", "kwlist"),
34
+ "topic": ("topic", "title"),
35
+ # Deliberately narrow: no "kw"/"keyword" fallback here, unlike "keyword" above.
36
+ # Those are common to many unrelated tools (better-keywords, seogpt2, ...) and
37
+ # would make --entity spuriously "relevant" (and functional, just semantically
38
+ # wrong) for all of them. RankLens overrides this below with its own aliases.
39
+ "entity": ("entity", "entities"),
40
+ "task_id": ("task_id", "taskid", "task-id"),
34
41
  "url": ("url", "domain", "page_url", "website"),
35
42
  "url_a": ("url_a", "urla", "url1", "first_url", "competitor_url"),
36
43
  "url_b": ("url_b", "urlb", "url2", "second_url", "comparison_url"),
37
44
  "brand": ("brand", "brand_name", "company", "company_name"),
38
45
  "type": ("contenttype", "content_type", "imagetype", "image_type", "type"),
46
+ "length": ("contentlength", "content_length", "length"),
39
47
  "language": ("language", "lang", "language_id"),
40
48
  "engine": ("engine", "ai_engine", "model"),
41
49
  "country": ("country", "country_code", "location", "gl"),
@@ -127,7 +135,14 @@ TOOL_ADAPTERS: dict[str, ToolAdapter] = {
127
135
  aliases=(),
128
136
  default_action="rank",
129
137
  actions=("rank", "competitors", "raw"),
130
- field_aliases=COMMON_FIELD_ALIASES,
138
+ field_aliases={
139
+ **COMMON_FIELD_ALIASES,
140
+ # "entity" is RankLens's real, documented field. --keyword/--kw remain
141
+ # accepted as aliases (the API still honors kw/keyword input too), but
142
+ # should resolve onto entity now rather than the deprecated field names.
143
+ "keyword": ("entity", "kw", "keyword", "query", "seed_keyword"),
144
+ "entity": ("entity", "entities", "kw", "keyword", "query", "seed_keyword"),
145
+ },
131
146
  option_aliases={
132
147
  "languages": ("lang", "language", "languages"),
133
148
  "engines": ("engine", "model", "engines"),
@@ -176,8 +191,13 @@ TOOL_ADAPTERS: dict[str, ToolAdapter] = {
176
191
  actions=("create-task", "get-task-status", "get-result", "raw"),
177
192
  field_aliases={
178
193
  **COMMON_FIELD_ALIASES,
179
- "keyword": ("Topic", "topic", "kw", "keyword", "query", "seed_keyword"),
180
- "text": ("Topic", "topic", "text", "content", "input", "body"),
194
+ # The real backend (SEOB's seogpt2api.php) treats these as two distinct
195
+ # fields: Topic (required, 12-200 chars, the article subject) and KW
196
+ # (optional, up to 5 keywords). --keyword/--kw/--keywords must all land
197
+ # on KW, never on Topic - --topic/--title are the only way to set Topic.
198
+ "keyword": ("KW", "kw", "keywords", "keyword", "query", "seed_keyword"),
199
+ "keywords": ("KW", "kw", "keywords", "keyword", "keyword_list", "kwlist"),
200
+ "topic": ("Topic", "topic", "title"),
181
201
  },
182
202
  async_likely=True,
183
203
  option_aliases={
@@ -35,7 +35,7 @@ class APIClient:
35
35
  payload: dict[str, Any],
36
36
  api_key: str | None,
37
37
  method: str = "POST",
38
- timeout: float = 60.0,
38
+ timeout: float = 300.0,
39
39
  ) -> APIResponse:
40
40
  final_payload = dict(payload)
41
41
  if api_key and not any(key in final_payload for key in ("k", "api_key", "apikey", "key")):
@@ -31,7 +31,7 @@ from .definitions import DefinitionsManager
31
31
  from .errors import CLIError, ConfigError, InvalidInputError
32
32
  from .executor import RuntimeOptions, WaitOptions, execute_tool, load_json_payload
33
33
  from .formatter import print_output
34
- from .resolver import extract_option_sets, resolve_api_field, search_candidates
34
+ from .resolver import extract_option_sets, is_cli_field_relevant, resolve_api_field, search_candidates
35
35
  from .tasks import get_task_tool, result_payload, status_payload
36
36
  from .utils import coerce_jsonish, coerce_mapping_values, parse_key_value_args, read_text_source
37
37
 
@@ -39,6 +39,78 @@ console = Console()
39
39
  err_console = Console(stderr=True)
40
40
  COMMON_CONTEXT_SETTINGS = {"allow_extra_args": True, "ignore_unknown_options": True}
41
41
 
42
+ # ---------------------------------------------------------------------------
43
+ # Per-tool/per-action option visibility for --help.
44
+ #
45
+ # The generic option palette in make_action_command() below is shared by every
46
+ # tool+action, but most of its ~25 flags are irrelevant to any given one (e.g.
47
+ # --price/--theme/--color have nothing to do with seogpt2). This section hides
48
+ # irrelevant flags from --help using the live, cached API definitions - the
49
+ # same live_definition-driven approach sv_mcp's schemas.py already uses.
50
+ #
51
+ # --help must stay instant and work offline, so this reads the on-disk
52
+ # definitions cache directly (DefinitionsManager.load_cache()) and never
53
+ # triggers a network fetch. Before the cache is populated (fresh install, or
54
+ # offline), every option stays visible - identical to the old fully-generic
55
+ # behavior - until the user runs a command or `sv definitions refresh`.
56
+ # ---------------------------------------------------------------------------
57
+
58
+ _DEFINITIONS_CACHE: dict[str, Any] | None = None
59
+
60
+
61
+ def _cached_definitions_cache() -> dict[str, Any]:
62
+ global _DEFINITIONS_CACHE
63
+ if _DEFINITIONS_CACHE is None:
64
+ try:
65
+ _DEFINITIONS_CACHE = DefinitionsManager().load_cache()
66
+ except Exception: # noqa: BLE001 - --help must never fail over a cache read
67
+ _DEFINITIONS_CACHE = {}
68
+ return _DEFINITIONS_CACHE
69
+
70
+
71
+ def _tool_definition(tool: str) -> dict[str, Any]:
72
+ cache = _cached_definitions_cache()
73
+ entry = cache.get("tools", {}).get(tool) or {}
74
+ return entry.get("definition") or {}
75
+
76
+
77
+ # create-task/get-task-status/get-result is the identical lifecycle used by every
78
+ # async tool (geogptaudit, seogpt2, seogptcompare, seogptmapping) - confirmed from
79
+ # each tool's index.php, not guessed. The status/result actions only ever need a
80
+ # task_id, regardless of what the create-task action itself accepts.
81
+ TASK_LOOKUP_ACTIONS = {"get-task-status", "get-result"}
82
+
83
+ # Friendly CLI field names in make_action_command()'s signature, in the order they're
84
+ # declared there. Keep in sync with that function's parameters.
85
+ GENERIC_CLI_FIELDS = (
86
+ "keyword", "keywords", "topic", "entity", "url", "url_a", "url_b", "brand", "type_value",
87
+ "length", "language", "engine", "country", "location", "text", "search", "query", "price",
88
+ "series", "category", "outline", "theme", "background", "color", "size",
89
+ )
90
+
91
+ # Maps a few typer parameter names to the field_aliases key they're actually resolved
92
+ # under (adapters.py), for the ones where they differ (e.g. "type_value" avoids
93
+ # shadowing the builtin `type`, but resolves as "type").
94
+ _CLI_FIELD_ALIAS_FOR = {"type_value": "type"}
95
+
96
+
97
+ def _visible_cli_fields(tool: str, action: str) -> set[str] | None:
98
+ """Friendly CLI field names relevant to this tool+action, or None for "show all".
99
+
100
+ None is the safe fallback: no live definition cached yet, so nothing is hidden.
101
+ """
102
+
103
+ if action in TASK_LOOKUP_ACTIONS:
104
+ return set() # every generic content field is irrelevant; task_id is separate
105
+ definition = _tool_definition(tool)
106
+ if not definition:
107
+ return None
108
+ return {
109
+ field
110
+ for field in GENERIC_CLI_FIELDS
111
+ if is_cli_field_relevant(tool, _CLI_FIELD_ALIAS_FOR.get(field, field), definition)
112
+ }
113
+
42
114
  app = typer.Typer(
43
115
  name="sv",
44
116
  help="Definition-driven CLI for SV AI API tools.",
@@ -585,11 +657,15 @@ def build_params(
585
657
  *,
586
658
  keyword: str | None,
587
659
  keywords: str | None,
660
+ topic: str | None,
661
+ entity: str | None,
662
+ task_id: str | None,
588
663
  url: str | None,
589
664
  url_a: str | None,
590
665
  url_b: str | None,
591
666
  brand: str | None,
592
667
  type_value: str | None,
668
+ length: str | None,
593
669
  language: str | None,
594
670
  engine: str | None,
595
671
  country: str | None,
@@ -626,11 +702,15 @@ def build_params(
626
702
  params: dict[str, Any] = {
627
703
  "keyword": keyword,
628
704
  "keywords": keywords,
705
+ "topic": topic,
706
+ "entity": entity,
707
+ "task_id": task_id,
629
708
  "url": url,
630
709
  "url_a": url_a,
631
710
  "url_b": url_b,
632
711
  "brand": brand,
633
712
  "type": type_value,
713
+ "length": length,
634
714
  "language": language,
635
715
  "engine": engine,
636
716
  "country": country,
@@ -672,32 +752,89 @@ def build_params(
672
752
 
673
753
 
674
754
  def make_action_command(tool: str, action: str):
755
+ visible = _visible_cli_fields(tool, action)
756
+
757
+ def hidden(field: str) -> bool:
758
+ return visible is not None and field not in visible
759
+
760
+ show_task_id = action in TASK_LOOKUP_ACTIONS
761
+
675
762
  def command(
676
763
  ctx: typer.Context,
677
- keyword: str | None = typer.Option(None, "--keyword", "--kw", help="Primary keyword."),
678
- keywords: str | None = typer.Option(None, "--keywords", help="Comma-separated keywords or path to a keyword file."),
679
- url: str | None = typer.Option(None, "--url", help="Target URL/domain."),
680
- url_a: str | None = typer.Option(None, "--url-a", "--url1", help="First URL for comparison or URL 1."),
681
- url_b: str | None = typer.Option(None, "--url-b", "--url2", help="Second URL for comparison or URL 2."),
682
- brand: str | None = typer.Option(None, "--brand", help="Brand or company name."),
683
- type_value: str | None = typer.Option(None, "--type", help="Type/content type/image type by ID, slug, label, or alias."),
684
- language: str | None = typer.Option(None, "--language", help="Language by ID, slug, label, or alias."),
685
- engine: str | None = typer.Option(None, "--engine", help="Engine/model by ID, slug, label, or alias."),
686
- country: str | None = typer.Option(None, "--country", help="Country/market code where supported."),
687
- location: str | None = typer.Option(None, "--location", help="Location/market where supported."),
688
- text: str | None = typer.Option(None, "--text", help="Text input for transformer-style tools."),
689
- file_path: str | None = typer.Option(None, "--file", help="Read text input from file."),
690
- stdin_flag: bool = typer.Option(False, "--stdin", help="Read text input from stdin."),
691
- search: str | None = typer.Option(None, "--search", "--searchterm", help="Search term for marketplace/service-style tools."),
692
- query: str | None = typer.Option(None, "--query", help="Query/search term where supported."),
693
- price: str | None = typer.Option(None, "--price", help="Price ceiling or price filter where supported."),
694
- series: str | None = typer.Option(None, "--series", help="Service series filter where supported."),
695
- category: str | None = typer.Option(None, "--category", help="Service category filter where supported."),
696
- outline: str | None = typer.Option(None, "--outline", help="Outline text or path to outline file."),
697
- theme: str | None = typer.Option(None, "--theme", help="Image theme by ID, slug, label, or alias."),
698
- background: str | None = typer.Option(None, "--background", help="Image background by ID, slug, label, or alias."),
699
- color: str | None = typer.Option(None, "--color", help="Image color/palette by ID, slug, label, or alias."),
700
- size: str | None = typer.Option(None, "--size", help="Image size by ID, slug, label, or alias."),
764
+ topic: str | None = typer.Option(None, "--topic", "--title", help="Article topic or title.", hidden=hidden("topic")),
765
+ entity: str | None = typer.Option(
766
+ None, "--entity", "--entities", help="Entity to analyze or discover.", hidden=hidden("entity")
767
+ ),
768
+ keyword: str | None = typer.Option(
769
+ None, "--keyword", "--kw", help="Keyword text.", hidden=hidden("keyword")
770
+ ),
771
+ keywords: str | None = typer.Option(
772
+ None, "--keywords", help="Comma-separated keywords or path to a keyword file.", hidden=hidden("keywords")
773
+ ),
774
+ task_id: str | None = typer.Option(
775
+ None, "--task-id", help="Task ID from a prior create-task call.", hidden=not show_task_id
776
+ ),
777
+ url: str | None = typer.Option(None, "--url", help="Target URL/domain.", hidden=hidden("url")),
778
+ url_a: str | None = typer.Option(
779
+ None, "--url-a", "--url1", help="First URL for comparison or URL 1.", hidden=hidden("url_a")
780
+ ),
781
+ url_b: str | None = typer.Option(
782
+ None, "--url-b", "--url2", help="Second URL for comparison or URL 2.", hidden=hidden("url_b")
783
+ ),
784
+ brand: str | None = typer.Option(None, "--brand", help="Brand or company name.", hidden=hidden("brand")),
785
+ type_value: str | None = typer.Option(
786
+ None, "--type", help="Type/content type/image type by ID, slug, label, or alias.", hidden=hidden("type_value")
787
+ ),
788
+ length: str | None = typer.Option(
789
+ None, "--length", "--contentlength", help="Content length by ID, slug, label, or alias.", hidden=hidden("length")
790
+ ),
791
+ language: str | None = typer.Option(
792
+ None, "--language", help="Language by ID, slug, label, or alias.", hidden=hidden("language")
793
+ ),
794
+ engine: str | None = typer.Option(
795
+ None, "--engine", help="Engine/model by ID, slug, label, or alias.", hidden=hidden("engine")
796
+ ),
797
+ country: str | None = typer.Option(
798
+ None, "--country", help="Country/market code where supported.", hidden=hidden("country")
799
+ ),
800
+ location: str | None = typer.Option(
801
+ None, "--location", help="Location/market where supported.", hidden=hidden("location")
802
+ ),
803
+ text: str | None = typer.Option(
804
+ None, "--text", help="Text input for transformer-style tools.", hidden=hidden("text")
805
+ ),
806
+ file_path: str | None = typer.Option(None, "--file", help="Read text input from file.", hidden=hidden("text")),
807
+ stdin_flag: bool = typer.Option(False, "--stdin", help="Read text input from stdin.", hidden=hidden("text")),
808
+ search: str | None = typer.Option(
809
+ None, "--search", "--searchterm", help="Search term for marketplace/service-style tools.", hidden=hidden("search")
810
+ ),
811
+ query: str | None = typer.Option(
812
+ None, "--query", help="Query/search term where supported.", hidden=hidden("query")
813
+ ),
814
+ price: str | None = typer.Option(
815
+ None, "--price", help="Price ceiling or price filter where supported.", hidden=hidden("price")
816
+ ),
817
+ series: str | None = typer.Option(
818
+ None, "--series", help="Service series filter where supported.", hidden=hidden("series")
819
+ ),
820
+ category: str | None = typer.Option(
821
+ None, "--category", help="Service category filter where supported.", hidden=hidden("category")
822
+ ),
823
+ outline: str | None = typer.Option(
824
+ None, "--outline", help="Outline text or path to outline file.", hidden=hidden("outline")
825
+ ),
826
+ theme: str | None = typer.Option(
827
+ None, "--theme", help="Image theme by ID, slug, label, or alias.", hidden=hidden("theme")
828
+ ),
829
+ background: str | None = typer.Option(
830
+ None, "--background", help="Image background by ID, slug, label, or alias.", hidden=hidden("background")
831
+ ),
832
+ color: str | None = typer.Option(
833
+ None, "--color", help="Image color/palette by ID, slug, label, or alias.", hidden=hidden("color")
834
+ ),
835
+ size: str | None = typer.Option(
836
+ None, "--size", help="Image size by ID, slug, label, or alias.", hidden=hidden("size")
837
+ ),
701
838
  wait: bool = typer.Option(False, "--wait", help="Wait for async task completion."),
702
839
  timeout: int = typer.Option(600, "--timeout", help="Task wait timeout in seconds."),
703
840
  poll_interval: int = typer.Option(5, "--poll-interval", help="Task poll interval in seconds."),
@@ -717,11 +854,15 @@ def make_action_command(tool: str, action: str):
717
854
  params = build_params(
718
855
  keyword=keyword,
719
856
  keywords=keywords,
857
+ topic=topic,
858
+ entity=entity,
859
+ task_id=task_id,
720
860
  url=url,
721
861
  url_a=url_a,
722
862
  url_b=url_b,
723
863
  brand=brand,
724
864
  type_value=type_value,
865
+ length=length,
725
866
  language=language,
726
867
  engine=engine,
727
868
  country=country,
@@ -811,9 +952,11 @@ def make_tool_app(adapter: ToolAdapter) -> typer.Typer:
811
952
  for action in adapter.actions:
812
953
  if action == "raw":
813
954
  continue
814
- tool_app.command(action, context_settings=COMMON_CONTEXT_SETTINGS)(make_action_command(adapter.canonical, action))
955
+ tool_app.command(action, context_settings=COMMON_CONTEXT_SETTINGS, rich_help_panel="Actions")(
956
+ make_action_command(adapter.canonical, action)
957
+ )
815
958
 
816
- @tool_app.command("raw")
959
+ @tool_app.command("raw", rich_help_panel="Actions")
817
960
  def raw(
818
961
  ctx: typer.Context,
819
962
  json_payload: str | None = typer.Option(None, "--json", help="Raw JSON payload."),
@@ -839,7 +982,11 @@ def make_tool_app(adapter: ToolAdapter) -> typer.Typer:
839
982
  fail(exc)
840
983
 
841
984
  for command_name, fields in adapter.option_aliases.items():
842
- tool_app.command(command_name)(make_option_alias_command(adapter.canonical, fields))
985
+ tool_app.command(
986
+ command_name,
987
+ rich_help_panel="Option lookups (list valid values for a parameter, not an action)",
988
+ help=f"List valid values for --{fields[0]}.",
989
+ )(make_option_alias_command(adapter.canonical, fields))
843
990
 
844
991
  return tool_app
845
992
 
@@ -333,6 +333,40 @@ def normalize_field(field: str) -> str:
333
333
  return str(field).strip().replace("-", "_").replace(" ", "_").lower()
334
334
 
335
335
 
336
+ def real_field_names(definition: Any) -> set[str]:
337
+ """Normalized API field names that genuinely appear in a tool's live api_input.
338
+
339
+ Used to decide which generic CLI options are relevant to a specific tool,
340
+ so --help doesn't advertise flags the tool's real API doesn't accept.
341
+ """
342
+
343
+ names: set[str] = set()
344
+ items = definition.get("api_input") if isinstance(definition, dict) else None
345
+ if not isinstance(items, list):
346
+ return names
347
+ for item in items:
348
+ if isinstance(item, dict):
349
+ field = item.get("field")
350
+ if isinstance(field, str):
351
+ names.add(normalize_field(field))
352
+ return names
353
+
354
+
355
+ def is_cli_field_relevant(tool: str, cli_field: str, definition: Any) -> bool:
356
+ """True when a generic CLI field resolves to a real field on this tool's live definition.
357
+
358
+ When no live definition is cached yet (e.g. before the first `sv definitions
359
+ refresh`, or offline), returns True for everything - never hide options based
360
+ on missing data.
361
+ """
362
+
363
+ if not definition:
364
+ return True
365
+ api_field, _candidates = resolve_api_field(tool, cli_field, definition)
366
+ target_fields = real_field_names(definition) | set(extract_option_sets(definition))
367
+ return api_field in target_fields
368
+
369
+
336
370
  def resolve_api_field(tool: str, cli_field: str, definition: Any) -> tuple[str, list[Candidate] | None]:
337
371
  """Choose the best API field name for a friendly CLI field."""
338
372
 
@@ -0,0 +1,16 @@
1
+ {
2
+ "api_input": [
3
+ {"field": "action", "type": "string", "required": "no", "description": ["rank: Generate RankLens report.", "competitors: Find RankLens competitors."]},
4
+ {"field": "web", "type": "string", "required": "yes", "description": "Website URL, max 300 characters."},
5
+ {"field": "entity", "type": "string", "required": "yes", "description": "Entity/keyword text, 4 to 80 characters."},
6
+ {"field": "kw", "type": "string", "required": "no", "description": "Deprecated alias of entity. Used only when entity is not sent."},
7
+ {"field": "keyword", "type": "string", "required": "no", "description": "Deprecated alias of entity. Used only when entity and kw are not sent."},
8
+ {"field": "brand", "type": "string", "required": "no", "description": "Brand text, 0 to 60 characters."},
9
+ {"field": "location", "type": "string", "required": "no", "description": "Location text, 0 to 100 characters."},
10
+ {"field": "lang", "type": "integer", "required": "no", "description": null},
11
+ {"field": "engine", "type": "integer", "required": "no", "description": null},
12
+ {"field": "mgptid", "type": "string", "required": "no", "description": "Encrypted MGPTID. Required for competitors action, not used for rank action."},
13
+ {"field": "k", "type": "string", "required": "yes", "description": "API key."}
14
+ ],
15
+ "api_output": []
16
+ }