sv-cli 0.3.0__py3-none-any.whl
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.
- sv_cli/__init__.py +3 -0
- sv_cli/adapters.py +323 -0
- sv_cli/api_client.py +82 -0
- sv_cli/commands/__init__.py +0 -0
- sv_cli/commands/auth.py +4 -0
- sv_cli/commands/better_keywords.py +4 -0
- sv_cli/commands/call.py +4 -0
- sv_cli/commands/config.py +4 -0
- sv_cli/commands/content_quality.py +5 -0
- sv_cli/commands/content_transformer.py +4 -0
- sv_cli/commands/core_analysis.py +4 -0
- sv_cli/commands/definitions.py +4 -0
- sv_cli/commands/geo_audit.py +4 -0
- sv_cli/commands/insight_igniter.py +4 -0
- sv_cli/commands/marketplace_services.py +5 -0
- sv_cli/commands/options.py +4 -0
- sv_cli/commands/preliminary_audit.py +4 -0
- sv_cli/commands/ranklens.py +4 -0
- sv_cli/commands/seo_image.py +4 -0
- sv_cli/commands/seo_mapping.py +4 -0
- sv_cli/commands/seogpt.py +4 -0
- sv_cli/commands/seogpt2.py +4 -0
- sv_cli/commands/seogpt_compare.py +4 -0
- sv_cli/commands/top_competitors.py +5 -0
- sv_cli/commands/topical_authority.py +4 -0
- sv_cli/config.py +216 -0
- sv_cli/definitions.py +283 -0
- sv_cli/errors.py +55 -0
- sv_cli/executor.py +188 -0
- sv_cli/formatter.py +227 -0
- sv_cli/main.py +905 -0
- sv_cli/renderers/__init__.py +0 -0
- sv_cli/renderers/csv_renderer.py +9 -0
- sv_cli/renderers/json_renderer.py +10 -0
- sv_cli/renderers/markdown_renderer.py +9 -0
- sv_cli/renderers/table_renderer.py +9 -0
- sv_cli/renderers/text_renderer.py +9 -0
- sv_cli/resolver.py +523 -0
- sv_cli/schemas/__init__.py +0 -0
- sv_cli/schemas/api_response.py +12 -0
- sv_cli/schemas/config.py +13 -0
- sv_cli/schemas/tool_definition.py +17 -0
- sv_cli/tasks.py +168 -0
- sv_cli/utils.py +204 -0
- sv_cli-0.3.0.dist-info/METADATA +338 -0
- sv_cli-0.3.0.dist-info/RECORD +49 -0
- sv_cli-0.3.0.dist-info/WHEEL +4 -0
- sv_cli-0.3.0.dist-info/entry_points.txt +2 -0
- sv_cli-0.3.0.dist-info/licenses/LICENSE +21 -0
sv_cli/main.py
ADDED
|
@@ -0,0 +1,905 @@
|
|
|
1
|
+
"""SV CLI entry point."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import copy
|
|
6
|
+
import getpass
|
|
7
|
+
import sys
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
|
|
14
|
+
from . import __version__
|
|
15
|
+
from .adapters import TOOL_ADAPTERS, ToolAdapter, adapter_as_dict, all_command_names
|
|
16
|
+
from .api_client import APIClient
|
|
17
|
+
from .config import (
|
|
18
|
+
DEFAULT_BASE_URL,
|
|
19
|
+
clear_api_key,
|
|
20
|
+
create_profile,
|
|
21
|
+
delete_profile,
|
|
22
|
+
get_config_value,
|
|
23
|
+
load_config,
|
|
24
|
+
masked_config,
|
|
25
|
+
reset_config,
|
|
26
|
+
set_api_key,
|
|
27
|
+
set_config_value,
|
|
28
|
+
use_profile,
|
|
29
|
+
)
|
|
30
|
+
from .definitions import DefinitionsManager
|
|
31
|
+
from .errors import CLIError, ConfigError, InvalidInputError
|
|
32
|
+
from .executor import RuntimeOptions, WaitOptions, execute_tool, load_json_payload
|
|
33
|
+
from .formatter import print_output
|
|
34
|
+
from .resolver import extract_option_sets, resolve_api_field, search_candidates
|
|
35
|
+
from .tasks import get_task_tool, result_payload, status_payload
|
|
36
|
+
from .utils import coerce_jsonish, coerce_mapping_values, parse_key_value_args, read_text_source
|
|
37
|
+
|
|
38
|
+
console = Console()
|
|
39
|
+
err_console = Console(stderr=True)
|
|
40
|
+
COMMON_CONTEXT_SETTINGS = {"allow_extra_args": True, "ignore_unknown_options": True}
|
|
41
|
+
|
|
42
|
+
app = typer.Typer(
|
|
43
|
+
name="sv",
|
|
44
|
+
help="Definition-driven CLI for SV AI API tools.",
|
|
45
|
+
no_args_is_help=True,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def version_callback(value: bool) -> None:
|
|
50
|
+
if value:
|
|
51
|
+
console.print(f"sv-cli {__version__}")
|
|
52
|
+
raise typer.Exit(0)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@app.callback()
|
|
56
|
+
def main_callback(
|
|
57
|
+
ctx: typer.Context,
|
|
58
|
+
version: bool = typer.Option(False, "--version", callback=version_callback, is_eager=True, help="Show version and exit."),
|
|
59
|
+
profile: str | None = typer.Option(None, "--profile", help="Configuration profile to use."),
|
|
60
|
+
api_key: str | None = typer.Option(None, "--api-key", help="SV API key. Prefer SV_API_KEY or auth set."),
|
|
61
|
+
base_url: str | None = typer.Option(None, "--base-url", help="Override API base URL."),
|
|
62
|
+
output_format: str | None = typer.Option(None, "--format", help="pretty|json|table|csv|markdown|text"),
|
|
63
|
+
output: str | None = typer.Option(None, "--output", help="Write output to a file."),
|
|
64
|
+
quiet: bool = typer.Option(False, "--quiet", help="Suppress normal output."),
|
|
65
|
+
verbose: bool = typer.Option(False, "--verbose", help="Show additional progress details."),
|
|
66
|
+
debug: bool = typer.Option(False, "--debug", help="Show masked payloads, endpoints, and HTTP timing."),
|
|
67
|
+
strict: bool = typer.Option(False, "--strict", help="Use strict enum matching for agents and CI."),
|
|
68
|
+
no_fuzzy: bool = typer.Option(False, "--no-fuzzy", help="Disable fuzzy enum matching."),
|
|
69
|
+
non_interactive: bool = typer.Option(False, "--non-interactive", help="Disable prompts and interactive choices."),
|
|
70
|
+
) -> None:
|
|
71
|
+
cfg = load_config()
|
|
72
|
+
defaults = cfg.get("defaults", {})
|
|
73
|
+
if api_key:
|
|
74
|
+
err_console.print("[yellow]Warning:[/yellow] --api-key can be stored in shell history. Prefer SV_API_KEY or `sv auth set`.")
|
|
75
|
+
ctx.obj = RuntimeOptions(
|
|
76
|
+
profile=profile,
|
|
77
|
+
api_key=api_key,
|
|
78
|
+
base_url=base_url,
|
|
79
|
+
output_format=output_format or defaults.get("format", "pretty"),
|
|
80
|
+
output=output,
|
|
81
|
+
quiet=quiet,
|
|
82
|
+
verbose=verbose,
|
|
83
|
+
debug=debug,
|
|
84
|
+
strict=strict or bool(defaults.get("strict", False)),
|
|
85
|
+
no_fuzzy=no_fuzzy or not bool(defaults.get("fuzzy", True)),
|
|
86
|
+
non_interactive=non_interactive,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def fail(exc: Exception) -> None:
|
|
91
|
+
if isinstance(exc, CLIError):
|
|
92
|
+
err_console.print(f"[red]Error:[/red] {exc.message}")
|
|
93
|
+
raise typer.Exit(exc.exit_code) from exc
|
|
94
|
+
err_console.print(f"[red]Error:[/red] {exc}")
|
|
95
|
+
raise typer.Exit(1) from exc
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def runtime_from_ctx(ctx: typer.Context) -> RuntimeOptions:
|
|
99
|
+
runtime = ctx.obj if isinstance(ctx.obj, RuntimeOptions) else RuntimeOptions()
|
|
100
|
+
return copy.copy(runtime)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def apply_runtime_overrides(runtime: RuntimeOptions, overrides: dict[str, Any]) -> RuntimeOptions:
|
|
104
|
+
runtime = copy.copy(runtime)
|
|
105
|
+
mapping = {
|
|
106
|
+
"profile": "profile",
|
|
107
|
+
"api_key": "api_key",
|
|
108
|
+
"base_url": "base_url",
|
|
109
|
+
"format": "output_format",
|
|
110
|
+
"output_format": "output_format",
|
|
111
|
+
"output": "output",
|
|
112
|
+
"quiet": "quiet",
|
|
113
|
+
"verbose": "verbose",
|
|
114
|
+
"debug": "debug",
|
|
115
|
+
"strict": "strict",
|
|
116
|
+
"no_fuzzy": "no_fuzzy",
|
|
117
|
+
"non_interactive": "non_interactive",
|
|
118
|
+
}
|
|
119
|
+
for cli_key, attr in mapping.items():
|
|
120
|
+
if cli_key in overrides and overrides[cli_key] is not None:
|
|
121
|
+
value = overrides.pop(cli_key)
|
|
122
|
+
if attr in {"quiet", "verbose", "debug", "strict", "no_fuzzy", "non_interactive"}:
|
|
123
|
+
value = bool(value)
|
|
124
|
+
setattr(runtime, attr, value)
|
|
125
|
+
return runtime
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def pop_wait_options(params: dict[str, Any]) -> WaitOptions:
|
|
129
|
+
wait = bool(params.pop("wait", False))
|
|
130
|
+
timeout = int(params.pop("timeout", 600) or 600)
|
|
131
|
+
poll_interval = int(params.pop("poll_interval", 5) or 5)
|
|
132
|
+
no_progress = bool(params.pop("no_progress", False))
|
|
133
|
+
return WaitOptions(wait=wait, timeout=timeout, poll_interval=poll_interval, no_progress=no_progress)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def run_tool_action(
|
|
137
|
+
ctx: typer.Context,
|
|
138
|
+
*,
|
|
139
|
+
tool: str,
|
|
140
|
+
action: str,
|
|
141
|
+
params: dict[str, Any],
|
|
142
|
+
method: str = "POST",
|
|
143
|
+
) -> None:
|
|
144
|
+
try:
|
|
145
|
+
runtime = runtime_from_ctx(ctx)
|
|
146
|
+
params = {key: value for key, value in params.items() if value not in (None, False)}
|
|
147
|
+
params.update(parse_key_value_args(ctx.args))
|
|
148
|
+
params = coerce_mapping_values(params)
|
|
149
|
+
runtime = apply_runtime_overrides(runtime, params)
|
|
150
|
+
wait_options = pop_wait_options(params)
|
|
151
|
+
execute_tool(
|
|
152
|
+
tool_name=tool,
|
|
153
|
+
action=action,
|
|
154
|
+
params=params,
|
|
155
|
+
runtime=runtime,
|
|
156
|
+
wait_options=wait_options,
|
|
157
|
+
method=method,
|
|
158
|
+
console=console,
|
|
159
|
+
)
|
|
160
|
+
except Exception as exc: # noqa: BLE001
|
|
161
|
+
fail(exc)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# ---------------------------------------------------------------------------
|
|
165
|
+
# Auth commands
|
|
166
|
+
# ---------------------------------------------------------------------------
|
|
167
|
+
auth_app = typer.Typer(help="Configure API-key authentication.", no_args_is_help=True)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def auth_set_impl(ctx: typer.Context, api_key: str | None, profile: str | None) -> None:
|
|
171
|
+
runtime = runtime_from_ctx(ctx)
|
|
172
|
+
target_profile = profile or runtime.profile
|
|
173
|
+
if not api_key:
|
|
174
|
+
api_key = getpass.getpass("SV API key: ").strip()
|
|
175
|
+
if not api_key:
|
|
176
|
+
raise InvalidInputError("API key cannot be empty.")
|
|
177
|
+
set_api_key(api_key, target_profile)
|
|
178
|
+
console.print("API key saved to local profile config. It will be masked in status and debug output.")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@auth_app.command("set")
|
|
182
|
+
def auth_set(
|
|
183
|
+
ctx: typer.Context,
|
|
184
|
+
api_key: str | None = typer.Option(None, "--api-key", help="API key to store. Omit to enter securely."),
|
|
185
|
+
profile: str | None = typer.Option(None, "--profile", help="Profile to update."),
|
|
186
|
+
) -> None:
|
|
187
|
+
try:
|
|
188
|
+
auth_set_impl(ctx, api_key, profile)
|
|
189
|
+
except Exception as exc: # noqa: BLE001
|
|
190
|
+
fail(exc)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@auth_app.command("login")
|
|
194
|
+
def auth_login(
|
|
195
|
+
ctx: typer.Context,
|
|
196
|
+
api_key: str | None = typer.Option(None, "--api-key", help="API key to store. Omit to enter securely."),
|
|
197
|
+
profile: str | None = typer.Option(None, "--profile", help="Profile to update."),
|
|
198
|
+
) -> None:
|
|
199
|
+
try:
|
|
200
|
+
auth_set_impl(ctx, api_key, profile)
|
|
201
|
+
except Exception as exc: # noqa: BLE001
|
|
202
|
+
fail(exc)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@auth_app.command("status")
|
|
206
|
+
def auth_status(ctx: typer.Context) -> None:
|
|
207
|
+
try:
|
|
208
|
+
cfg = masked_config()
|
|
209
|
+
runtime = runtime_from_ctx(ctx)
|
|
210
|
+
profile_name = runtime.profile or cfg.get("default_profile") or "default"
|
|
211
|
+
profile = cfg.get("profiles", {}).get(profile_name, {})
|
|
212
|
+
env = __import__("os").environ
|
|
213
|
+
env_present = "SV_API_KEY" in env
|
|
214
|
+
legacy_env_present = "SEOVENDOR_API_KEY" in env
|
|
215
|
+
table = Table("Source", "Status")
|
|
216
|
+
table.add_row("--api-key", "provided" if runtime.api_key else "not provided")
|
|
217
|
+
table.add_row("SV_API_KEY", "set" if env_present else "not set")
|
|
218
|
+
table.add_row("SEOVENDOR_API_KEY", "set (legacy)" if legacy_env_present else "not set")
|
|
219
|
+
table.add_row(f"profile:{profile_name}", "set" if profile.get("api_key") else "not set")
|
|
220
|
+
table.add_row("base_url", str(profile.get("base_url") or DEFAULT_BASE_URL))
|
|
221
|
+
console.print(table)
|
|
222
|
+
except Exception as exc: # noqa: BLE001
|
|
223
|
+
fail(exc)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@auth_app.command("clear")
|
|
227
|
+
def auth_clear(
|
|
228
|
+
ctx: typer.Context,
|
|
229
|
+
profile: str | None = typer.Option(None, "--profile", help="Profile to clear."),
|
|
230
|
+
) -> None:
|
|
231
|
+
try:
|
|
232
|
+
runtime = runtime_from_ctx(ctx)
|
|
233
|
+
clear_api_key(profile or runtime.profile)
|
|
234
|
+
console.print("API key cleared from local profile config.")
|
|
235
|
+
except Exception as exc: # noqa: BLE001
|
|
236
|
+
fail(exc)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
app.add_typer(auth_app, name="auth")
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# ---------------------------------------------------------------------------
|
|
243
|
+
# Profile commands
|
|
244
|
+
# ---------------------------------------------------------------------------
|
|
245
|
+
profile_app = typer.Typer(help="Manage profiles for separate keys/environments.", no_args_is_help=True)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@profile_app.command("list")
|
|
249
|
+
def profile_list() -> None:
|
|
250
|
+
try:
|
|
251
|
+
cfg = masked_config()
|
|
252
|
+
default_profile = cfg.get("default_profile")
|
|
253
|
+
table = Table("Profile", "Default", "Base URL", "API Key")
|
|
254
|
+
for name, prof in sorted(cfg.get("profiles", {}).items()):
|
|
255
|
+
table.add_row(name, "yes" if name == default_profile else "", str(prof.get("base_url")), "set" if prof.get("api_key") else "")
|
|
256
|
+
console.print(table)
|
|
257
|
+
except Exception as exc: # noqa: BLE001
|
|
258
|
+
fail(exc)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@profile_app.command("create")
|
|
262
|
+
def profile_create(
|
|
263
|
+
name: str = typer.Argument(..., help="Profile name."),
|
|
264
|
+
base_url: str | None = typer.Option(None, "--base-url", help="Profile base URL."),
|
|
265
|
+
api_key: str | None = typer.Option(None, "--api-key", help="Optional API key."),
|
|
266
|
+
) -> None:
|
|
267
|
+
try:
|
|
268
|
+
create_profile(name, base_url=base_url, api_key=api_key)
|
|
269
|
+
console.print(f'Profile "{name}" created.')
|
|
270
|
+
except Exception as exc: # noqa: BLE001
|
|
271
|
+
fail(exc)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
@profile_app.command("use")
|
|
275
|
+
def profile_use(name: str = typer.Argument(..., help="Profile name.")) -> None:
|
|
276
|
+
try:
|
|
277
|
+
use_profile(name)
|
|
278
|
+
console.print(f'Now using profile "{name}" by default.')
|
|
279
|
+
except Exception as exc: # noqa: BLE001
|
|
280
|
+
fail(exc)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
@profile_app.command("delete")
|
|
284
|
+
def profile_delete(name: str = typer.Argument(..., help="Profile name.")) -> None:
|
|
285
|
+
try:
|
|
286
|
+
delete_profile(name)
|
|
287
|
+
console.print(f'Profile "{name}" deleted.')
|
|
288
|
+
except Exception as exc: # noqa: BLE001
|
|
289
|
+
fail(exc)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
app.add_typer(profile_app, name="profile")
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
# ---------------------------------------------------------------------------
|
|
296
|
+
# Config commands
|
|
297
|
+
# ---------------------------------------------------------------------------
|
|
298
|
+
config_app = typer.Typer(help="Show and edit CLI configuration.", no_args_is_help=True)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
@config_app.command("show")
|
|
302
|
+
def config_show(ctx: typer.Context) -> None:
|
|
303
|
+
try:
|
|
304
|
+
runtime = runtime_from_ctx(ctx)
|
|
305
|
+
print_output(console, masked_config(), runtime.output_format, runtime.output)
|
|
306
|
+
except Exception as exc: # noqa: BLE001
|
|
307
|
+
fail(exc)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
@config_app.command("get")
|
|
311
|
+
def config_get(
|
|
312
|
+
key: str = typer.Argument(..., help="Config key, e.g. base_url or cache_ttl_seconds."),
|
|
313
|
+
profile: str | None = typer.Option(None, "--profile", help="Profile for profile-scoped keys."),
|
|
314
|
+
) -> None:
|
|
315
|
+
try:
|
|
316
|
+
console.print(get_config_value(key, profile))
|
|
317
|
+
except Exception as exc: # noqa: BLE001
|
|
318
|
+
fail(exc)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
@config_app.command("set")
|
|
322
|
+
def config_set(
|
|
323
|
+
key: str = typer.Argument(...),
|
|
324
|
+
value: str = typer.Argument(...),
|
|
325
|
+
profile: str | None = typer.Option(None, "--profile", help="Profile for profile-scoped keys."),
|
|
326
|
+
) -> None:
|
|
327
|
+
try:
|
|
328
|
+
set_config_value(key, coerce_jsonish(value), profile)
|
|
329
|
+
console.print(f"Set {key}.")
|
|
330
|
+
except Exception as exc: # noqa: BLE001
|
|
331
|
+
fail(exc)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
@config_app.command("reset")
|
|
335
|
+
def config_reset() -> None:
|
|
336
|
+
try:
|
|
337
|
+
reset_config()
|
|
338
|
+
console.print("Configuration reset.")
|
|
339
|
+
except Exception as exc: # noqa: BLE001
|
|
340
|
+
fail(exc)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
app.add_typer(config_app, name="config")
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
# ---------------------------------------------------------------------------
|
|
347
|
+
# Definition commands
|
|
348
|
+
# ---------------------------------------------------------------------------
|
|
349
|
+
definitions_app = typer.Typer(help="Fetch, inspect, refresh, and clear API definitions cache.", no_args_is_help=True)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
@definitions_app.command("refresh")
|
|
353
|
+
def definitions_refresh(
|
|
354
|
+
ctx: typer.Context,
|
|
355
|
+
tool: str | None = typer.Option(None, "--tool", help="Refresh one tool only."),
|
|
356
|
+
) -> None:
|
|
357
|
+
try:
|
|
358
|
+
runtime = runtime_from_ctx(ctx)
|
|
359
|
+
manager = DefinitionsManager(runtime.base_url)
|
|
360
|
+
if tool:
|
|
361
|
+
entry = manager.refresh_tool(tool)
|
|
362
|
+
console.print(f'Refreshed definitions for {entry["tool"]}.')
|
|
363
|
+
else:
|
|
364
|
+
cache = manager.refresh_all()
|
|
365
|
+
console.print(f"Refreshed API root and {len(cache.get('tools', {}))} tool definitions.")
|
|
366
|
+
for warning in cache.get("warnings", []):
|
|
367
|
+
err_console.print(f"[yellow]Warning:[/yellow] {warning}")
|
|
368
|
+
except Exception as exc: # noqa: BLE001
|
|
369
|
+
fail(exc)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
@definitions_app.command("list")
|
|
373
|
+
def definitions_list(ctx: typer.Context) -> None:
|
|
374
|
+
try:
|
|
375
|
+
runtime = runtime_from_ctx(ctx)
|
|
376
|
+
manager = DefinitionsManager(runtime.base_url)
|
|
377
|
+
cache = manager.get_cache(allow_stale=True)
|
|
378
|
+
rows = []
|
|
379
|
+
for tool_name, entry in sorted(cache.get("tools", {}).items()):
|
|
380
|
+
rows.append(
|
|
381
|
+
{
|
|
382
|
+
"tool": tool_name,
|
|
383
|
+
"command": entry.get("display_name") or tool_name,
|
|
384
|
+
"endpoint": entry.get("endpoint"),
|
|
385
|
+
"definitions_url": entry.get("definitions_url"),
|
|
386
|
+
"last_fetched": entry.get("last_fetched"),
|
|
387
|
+
}
|
|
388
|
+
)
|
|
389
|
+
print_output(console, rows, "table" if runtime.output_format == "pretty" else runtime.output_format, runtime.output)
|
|
390
|
+
for warning in cache.get("warnings", []):
|
|
391
|
+
err_console.print(f"[yellow]Warning:[/yellow] {warning}")
|
|
392
|
+
except Exception as exc: # noqa: BLE001
|
|
393
|
+
fail(exc)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
@definitions_app.command("show")
|
|
397
|
+
def definitions_show(
|
|
398
|
+
ctx: typer.Context,
|
|
399
|
+
tool: str = typer.Argument(..., help="Tool name or alias, e.g. seogpt or geo-audit."),
|
|
400
|
+
) -> None:
|
|
401
|
+
try:
|
|
402
|
+
runtime = runtime_from_ctx(ctx)
|
|
403
|
+
manager = DefinitionsManager(runtime.base_url)
|
|
404
|
+
entry = manager.get_tool(tool)
|
|
405
|
+
print_output(console, entry, "json" if runtime.output_format == "pretty" else runtime.output_format, runtime.output)
|
|
406
|
+
except Exception as exc: # noqa: BLE001
|
|
407
|
+
fail(exc)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
@definitions_app.command("clear")
|
|
411
|
+
def definitions_clear(ctx: typer.Context) -> None:
|
|
412
|
+
try:
|
|
413
|
+
runtime = runtime_from_ctx(ctx)
|
|
414
|
+
manager = DefinitionsManager(runtime.base_url)
|
|
415
|
+
removed = manager.clear_cache()
|
|
416
|
+
console.print("Definitions cache cleared." if removed else "Definitions cache was already empty.")
|
|
417
|
+
except Exception as exc: # noqa: BLE001
|
|
418
|
+
fail(exc)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
app.add_typer(definitions_app, name="definitions")
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
# ---------------------------------------------------------------------------
|
|
425
|
+
# Options discovery command
|
|
426
|
+
# ---------------------------------------------------------------------------
|
|
427
|
+
|
|
428
|
+
def split_positionals_and_options(args: list[str]) -> tuple[list[str], dict[str, Any]]:
|
|
429
|
+
options = parse_key_value_args(args)
|
|
430
|
+
positionals: list[str] = []
|
|
431
|
+
i = 0
|
|
432
|
+
while i < len(args):
|
|
433
|
+
token = args[i]
|
|
434
|
+
if token.startswith("--"):
|
|
435
|
+
if "=" not in token and i + 1 < len(args) and not args[i + 1].startswith("--"):
|
|
436
|
+
i += 2
|
|
437
|
+
else:
|
|
438
|
+
i += 1
|
|
439
|
+
continue
|
|
440
|
+
positionals.append(token)
|
|
441
|
+
i += 1
|
|
442
|
+
return positionals, options
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def show_options(ctx: typer.Context, tool: str | None, field: str | None, search: str | None) -> None:
|
|
446
|
+
runtime = runtime_from_ctx(ctx)
|
|
447
|
+
manager = DefinitionsManager(runtime.base_url)
|
|
448
|
+
cache = manager.get_cache(allow_stale=True)
|
|
449
|
+
if not tool:
|
|
450
|
+
rows = []
|
|
451
|
+
for tool_name, entry in sorted(cache.get("tools", {}).items()):
|
|
452
|
+
option_sets = extract_option_sets(entry.get("definition") or {})
|
|
453
|
+
rows.append({"tool": tool_name, "command": entry.get("display_name") or tool_name, "option_sets": len(option_sets)})
|
|
454
|
+
print_output(console, rows, "table" if runtime.output_format == "pretty" else runtime.output_format, runtime.output)
|
|
455
|
+
return
|
|
456
|
+
|
|
457
|
+
entry = manager.get_tool(tool)
|
|
458
|
+
canonical = entry["tool"]
|
|
459
|
+
definition = entry.get("definition") or {}
|
|
460
|
+
option_sets = extract_option_sets(definition)
|
|
461
|
+
if not field:
|
|
462
|
+
rows = [{"field": name, "count": len(candidates)} for name, candidates in sorted(option_sets.items())]
|
|
463
|
+
print_output(console, rows, "table" if runtime.output_format == "pretty" else runtime.output_format, runtime.output)
|
|
464
|
+
return
|
|
465
|
+
|
|
466
|
+
api_field, candidates = resolve_api_field(canonical, field, definition)
|
|
467
|
+
candidates = candidates or option_sets.get(field.replace("-", "_").lower())
|
|
468
|
+
if not candidates:
|
|
469
|
+
available = ", ".join(sorted(option_sets)) or "none found"
|
|
470
|
+
raise ConfigError(f'No options found for "{field}" on tool "{canonical}". Available fields: {available}')
|
|
471
|
+
candidates = search_candidates(candidates, search)
|
|
472
|
+
rows = [
|
|
473
|
+
{
|
|
474
|
+
"label": candidate.label,
|
|
475
|
+
"slug": candidate.slug,
|
|
476
|
+
"id": candidate.id,
|
|
477
|
+
"description": candidate.description or "",
|
|
478
|
+
"aliases": ", ".join(candidate.aliases),
|
|
479
|
+
}
|
|
480
|
+
for candidate in candidates
|
|
481
|
+
]
|
|
482
|
+
print_output(console, rows, "table" if runtime.output_format == "pretty" else runtime.output_format, runtime.output)
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
@app.command("options", context_settings=COMMON_CONTEXT_SETTINGS)
|
|
486
|
+
def options_command(ctx: typer.Context) -> None:
|
|
487
|
+
"""List/search dynamic enum options.
|
|
488
|
+
|
|
489
|
+
Examples:
|
|
490
|
+
sv options list
|
|
491
|
+
sv options seogpt
|
|
492
|
+
sv options seogpt contenttype --search meta
|
|
493
|
+
"""
|
|
494
|
+
try:
|
|
495
|
+
positionals, parsed_options = split_positionals_and_options(list(ctx.args))
|
|
496
|
+
runtime = runtime_from_ctx(ctx)
|
|
497
|
+
runtime = apply_runtime_overrides(runtime, parsed_options)
|
|
498
|
+
ctx.obj = runtime
|
|
499
|
+
search = parsed_options.pop("search", None)
|
|
500
|
+
if positionals and positionals[0] == "list":
|
|
501
|
+
show_options(ctx, None, None, search)
|
|
502
|
+
return
|
|
503
|
+
tool = positionals[0] if positionals else None
|
|
504
|
+
field = positionals[1] if len(positionals) > 1 else None
|
|
505
|
+
show_options(ctx, tool, field, search)
|
|
506
|
+
except Exception as exc: # noqa: BLE001
|
|
507
|
+
fail(exc)
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
# ---------------------------------------------------------------------------
|
|
511
|
+
# Raw calls and task commands
|
|
512
|
+
# ---------------------------------------------------------------------------
|
|
513
|
+
@app.command("call")
|
|
514
|
+
def raw_call(
|
|
515
|
+
ctx: typer.Context,
|
|
516
|
+
tool: str = typer.Argument(..., help="Tool name or alias."),
|
|
517
|
+
json_payload: str | None = typer.Option(None, "--json", help="Raw JSON object payload."),
|
|
518
|
+
file_path: str | None = typer.Option(None, "--file", help="Read raw JSON object payload from file."),
|
|
519
|
+
stdin_flag: bool = typer.Option(False, "--stdin", help="Read raw JSON object payload from stdin."),
|
|
520
|
+
method: str = typer.Option("POST", "--method", help="HTTP method, usually POST."),
|
|
521
|
+
) -> None:
|
|
522
|
+
try:
|
|
523
|
+
stdin_payload = sys.stdin.read() if stdin_flag else None
|
|
524
|
+
payload = load_json_payload(json_payload=json_payload, file_path=file_path, stdin_payload=stdin_payload)
|
|
525
|
+
runtime = runtime_from_ctx(ctx)
|
|
526
|
+
# Raw calls intentionally bypass fuzzy matching. Users can pass exact API payloads.
|
|
527
|
+
runtime.no_fuzzy = True
|
|
528
|
+
execute_tool(tool_name=tool, action=None, params={}, runtime=runtime, raw_payload=payload, method=method, console=console)
|
|
529
|
+
except Exception as exc: # noqa: BLE001
|
|
530
|
+
fail(exc)
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
task_app = typer.Typer(help="Check async task status/results.", no_args_is_help=True)
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def task_call(ctx: typer.Context, task_id: str, action: str, tool: str | None) -> None:
|
|
537
|
+
runtime = runtime_from_ctx(ctx)
|
|
538
|
+
manager = DefinitionsManager(runtime.base_url)
|
|
539
|
+
canonical = manager.resolve_tool(get_task_tool(task_id, tool))
|
|
540
|
+
entry = manager.get_tool(canonical)
|
|
541
|
+
api_key = __import__("sv_cli.config", fromlist=["resolve_api_key"]).resolve_api_key(
|
|
542
|
+
cli_api_key=runtime.api_key,
|
|
543
|
+
profile=runtime.profile,
|
|
544
|
+
allow_prompt=not runtime.non_interactive,
|
|
545
|
+
non_interactive=runtime.non_interactive,
|
|
546
|
+
)
|
|
547
|
+
payload = status_payload(task_id) if action == "status" else result_payload(task_id)
|
|
548
|
+
data = APIClient(debug=runtime.debug, console=err_console).request_tool(
|
|
549
|
+
endpoint=str(entry.get("endpoint")), payload=payload, api_key=api_key
|
|
550
|
+
).data
|
|
551
|
+
print_output(console, data, runtime.output_format, runtime.output)
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
@task_app.command("status")
|
|
555
|
+
def task_status(
|
|
556
|
+
ctx: typer.Context,
|
|
557
|
+
task_id: str = typer.Argument(...),
|
|
558
|
+
tool: str | None = typer.Option(None, "--tool", help="Tool that created the task, if not in local task cache."),
|
|
559
|
+
) -> None:
|
|
560
|
+
try:
|
|
561
|
+
task_call(ctx, task_id, "status", tool)
|
|
562
|
+
except Exception as exc: # noqa: BLE001
|
|
563
|
+
fail(exc)
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
@task_app.command("result")
|
|
567
|
+
def task_result(
|
|
568
|
+
ctx: typer.Context,
|
|
569
|
+
task_id: str = typer.Argument(...),
|
|
570
|
+
tool: str | None = typer.Option(None, "--tool", help="Tool that created the task, if not in local task cache."),
|
|
571
|
+
) -> None:
|
|
572
|
+
try:
|
|
573
|
+
task_call(ctx, task_id, "result", tool)
|
|
574
|
+
except Exception as exc: # noqa: BLE001
|
|
575
|
+
fail(exc)
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
app.add_typer(task_app, name="task")
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
# ---------------------------------------------------------------------------
|
|
582
|
+
# Tool command groups
|
|
583
|
+
# ---------------------------------------------------------------------------
|
|
584
|
+
def build_params(
|
|
585
|
+
*,
|
|
586
|
+
keyword: str | None,
|
|
587
|
+
keywords: str | None,
|
|
588
|
+
url: str | None,
|
|
589
|
+
url_a: str | None,
|
|
590
|
+
url_b: str | None,
|
|
591
|
+
brand: str | None,
|
|
592
|
+
type_value: str | None,
|
|
593
|
+
language: str | None,
|
|
594
|
+
engine: str | None,
|
|
595
|
+
country: str | None,
|
|
596
|
+
location: str | None,
|
|
597
|
+
text: str | None,
|
|
598
|
+
file_path: str | None,
|
|
599
|
+
stdin_flag: bool,
|
|
600
|
+
search: str | None,
|
|
601
|
+
query: str | None,
|
|
602
|
+
price: str | None,
|
|
603
|
+
series: str | None,
|
|
604
|
+
category: str | None,
|
|
605
|
+
outline: str | None,
|
|
606
|
+
theme: str | None,
|
|
607
|
+
background: str | None,
|
|
608
|
+
color: str | None,
|
|
609
|
+
size: str | None,
|
|
610
|
+
wait: bool,
|
|
611
|
+
timeout: int,
|
|
612
|
+
poll_interval: int,
|
|
613
|
+
no_progress: bool,
|
|
614
|
+
output_format: str | None,
|
|
615
|
+
output: str | None,
|
|
616
|
+
quiet: bool,
|
|
617
|
+
verbose: bool,
|
|
618
|
+
debug: bool,
|
|
619
|
+
strict: bool,
|
|
620
|
+
no_fuzzy: bool,
|
|
621
|
+
non_interactive: bool,
|
|
622
|
+
api_key: str | None,
|
|
623
|
+
base_url: str | None,
|
|
624
|
+
profile: str | None,
|
|
625
|
+
) -> dict[str, Any]:
|
|
626
|
+
params: dict[str, Any] = {
|
|
627
|
+
"keyword": keyword,
|
|
628
|
+
"keywords": keywords,
|
|
629
|
+
"url": url,
|
|
630
|
+
"url_a": url_a,
|
|
631
|
+
"url_b": url_b,
|
|
632
|
+
"brand": brand,
|
|
633
|
+
"type": type_value,
|
|
634
|
+
"language": language,
|
|
635
|
+
"engine": engine,
|
|
636
|
+
"country": country,
|
|
637
|
+
"location": location,
|
|
638
|
+
"search": search,
|
|
639
|
+
"query": query,
|
|
640
|
+
"price": price,
|
|
641
|
+
"series": series,
|
|
642
|
+
"category": category,
|
|
643
|
+
"outline": outline,
|
|
644
|
+
"theme": theme,
|
|
645
|
+
"background": background,
|
|
646
|
+
"color": color,
|
|
647
|
+
"size": size,
|
|
648
|
+
"wait": wait,
|
|
649
|
+
"timeout": timeout,
|
|
650
|
+
"poll_interval": poll_interval,
|
|
651
|
+
"no_progress": no_progress,
|
|
652
|
+
"format": output_format,
|
|
653
|
+
"output": output,
|
|
654
|
+
"quiet": quiet,
|
|
655
|
+
"verbose": verbose,
|
|
656
|
+
"debug": debug,
|
|
657
|
+
"strict": strict,
|
|
658
|
+
"no_fuzzy": no_fuzzy,
|
|
659
|
+
"non_interactive": non_interactive,
|
|
660
|
+
"api_key": api_key,
|
|
661
|
+
"base_url": base_url,
|
|
662
|
+
"profile": profile,
|
|
663
|
+
}
|
|
664
|
+
try:
|
|
665
|
+
source = read_text_source(text=text, file_path=file_path, use_stdin=stdin_flag, field_name="text")
|
|
666
|
+
except ValueError as exc:
|
|
667
|
+
raise InvalidInputError(str(exc)) from exc
|
|
668
|
+
if source:
|
|
669
|
+
field, content = source
|
|
670
|
+
params[field] = content
|
|
671
|
+
return params
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
def make_action_command(tool: str, action: str):
|
|
675
|
+
def command(
|
|
676
|
+
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."),
|
|
701
|
+
wait: bool = typer.Option(False, "--wait", help="Wait for async task completion."),
|
|
702
|
+
timeout: int = typer.Option(600, "--timeout", help="Task wait timeout in seconds."),
|
|
703
|
+
poll_interval: int = typer.Option(5, "--poll-interval", help="Task poll interval in seconds."),
|
|
704
|
+
no_progress: bool = typer.Option(False, "--no-progress", help="Disable progress display."),
|
|
705
|
+
output_format: str | None = typer.Option(None, "--format", help="pretty|json|table|csv|markdown|text"),
|
|
706
|
+
output: str | None = typer.Option(None, "--output", help="Write output to a file."),
|
|
707
|
+
quiet: bool = typer.Option(False, "--quiet", help="Suppress normal output."),
|
|
708
|
+
verbose: bool = typer.Option(False, "--verbose", help="Verbose output."),
|
|
709
|
+
debug: bool = typer.Option(False, "--debug", help="Debug output with secrets masked."),
|
|
710
|
+
strict: bool = typer.Option(False, "--strict", help="Strict enum matching."),
|
|
711
|
+
no_fuzzy: bool = typer.Option(False, "--no-fuzzy", help="Disable fuzzy enum matching."),
|
|
712
|
+
non_interactive: bool = typer.Option(False, "--non-interactive", help="Disable prompts."),
|
|
713
|
+
api_key: str | None = typer.Option(None, "--api-key", help="API key override."),
|
|
714
|
+
base_url: str | None = typer.Option(None, "--base-url", help="Base URL override."),
|
|
715
|
+
profile: str | None = typer.Option(None, "--profile", help="Profile override."),
|
|
716
|
+
) -> None:
|
|
717
|
+
params = build_params(
|
|
718
|
+
keyword=keyword,
|
|
719
|
+
keywords=keywords,
|
|
720
|
+
url=url,
|
|
721
|
+
url_a=url_a,
|
|
722
|
+
url_b=url_b,
|
|
723
|
+
brand=brand,
|
|
724
|
+
type_value=type_value,
|
|
725
|
+
language=language,
|
|
726
|
+
engine=engine,
|
|
727
|
+
country=country,
|
|
728
|
+
location=location,
|
|
729
|
+
text=text,
|
|
730
|
+
file_path=file_path,
|
|
731
|
+
stdin_flag=stdin_flag,
|
|
732
|
+
search=search,
|
|
733
|
+
query=query,
|
|
734
|
+
price=price,
|
|
735
|
+
series=series,
|
|
736
|
+
category=category,
|
|
737
|
+
outline=outline,
|
|
738
|
+
theme=theme,
|
|
739
|
+
background=background,
|
|
740
|
+
color=color,
|
|
741
|
+
size=size,
|
|
742
|
+
wait=wait,
|
|
743
|
+
timeout=timeout,
|
|
744
|
+
poll_interval=poll_interval,
|
|
745
|
+
no_progress=no_progress,
|
|
746
|
+
output_format=output_format,
|
|
747
|
+
output=output,
|
|
748
|
+
quiet=quiet,
|
|
749
|
+
verbose=verbose,
|
|
750
|
+
debug=debug,
|
|
751
|
+
strict=strict,
|
|
752
|
+
no_fuzzy=no_fuzzy,
|
|
753
|
+
non_interactive=non_interactive,
|
|
754
|
+
api_key=api_key,
|
|
755
|
+
base_url=base_url,
|
|
756
|
+
profile=profile,
|
|
757
|
+
)
|
|
758
|
+
run_tool_action(ctx, tool=tool, action=action, params=params)
|
|
759
|
+
|
|
760
|
+
command.__name__ = f"{tool.replace('-', '_')}_{action.replace('-', '_')}"
|
|
761
|
+
return command
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
def make_option_alias_command(tool: str, field_aliases: tuple[str, ...]):
|
|
765
|
+
def command(
|
|
766
|
+
ctx: typer.Context,
|
|
767
|
+
search: str | None = typer.Option(None, "--search", help="Filter options."),
|
|
768
|
+
output_format: str | None = typer.Option(None, "--format", help="Output format."),
|
|
769
|
+
output: str | None = typer.Option(None, "--output", help="Write output to file."),
|
|
770
|
+
) -> None:
|
|
771
|
+
try:
|
|
772
|
+
runtime = runtime_from_ctx(ctx)
|
|
773
|
+
overrides: dict[str, Any] = {"format": output_format, "output": output}
|
|
774
|
+
runtime = apply_runtime_overrides(runtime, overrides)
|
|
775
|
+
ctx.obj = runtime
|
|
776
|
+
show_options(ctx, tool, field_aliases[0], search)
|
|
777
|
+
except Exception as exc: # noqa: BLE001
|
|
778
|
+
fail(exc)
|
|
779
|
+
|
|
780
|
+
command.__name__ = f"{tool.replace('-', '_')}_{field_aliases[0]}_options"
|
|
781
|
+
return command
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
def make_tool_app(adapter: ToolAdapter) -> typer.Typer:
|
|
785
|
+
tool_app = typer.Typer(
|
|
786
|
+
help=f"Commands for {adapter.command} ({adapter.canonical}).",
|
|
787
|
+
invoke_without_command=True,
|
|
788
|
+
context_settings=COMMON_CONTEXT_SETTINGS,
|
|
789
|
+
)
|
|
790
|
+
|
|
791
|
+
@tool_app.callback(invoke_without_command=True, context_settings=COMMON_CONTEXT_SETTINGS)
|
|
792
|
+
def tool_callback(ctx: typer.Context) -> None:
|
|
793
|
+
if ctx.invoked_subcommand is not None:
|
|
794
|
+
return
|
|
795
|
+
try:
|
|
796
|
+
params = parse_key_value_args(ctx.args)
|
|
797
|
+
runtime = runtime_from_ctx(ctx)
|
|
798
|
+
runtime = apply_runtime_overrides(runtime, params)
|
|
799
|
+
wait_options = pop_wait_options(params)
|
|
800
|
+
execute_tool(
|
|
801
|
+
tool_name=adapter.canonical,
|
|
802
|
+
action=adapter.default_action,
|
|
803
|
+
params=params,
|
|
804
|
+
runtime=runtime,
|
|
805
|
+
wait_options=wait_options,
|
|
806
|
+
console=console,
|
|
807
|
+
)
|
|
808
|
+
except Exception as exc: # noqa: BLE001
|
|
809
|
+
fail(exc)
|
|
810
|
+
|
|
811
|
+
for action in adapter.actions:
|
|
812
|
+
if action == "raw":
|
|
813
|
+
continue
|
|
814
|
+
tool_app.command(action, context_settings=COMMON_CONTEXT_SETTINGS)(make_action_command(adapter.canonical, action))
|
|
815
|
+
|
|
816
|
+
@tool_app.command("raw")
|
|
817
|
+
def raw(
|
|
818
|
+
ctx: typer.Context,
|
|
819
|
+
json_payload: str | None = typer.Option(None, "--json", help="Raw JSON payload."),
|
|
820
|
+
file_path: str | None = typer.Option(None, "--file", help="Read raw JSON from file."),
|
|
821
|
+
stdin_flag: bool = typer.Option(False, "--stdin", help="Read raw JSON from stdin."),
|
|
822
|
+
method: str = typer.Option("POST", "--method", help="HTTP method."),
|
|
823
|
+
) -> None:
|
|
824
|
+
try:
|
|
825
|
+
stdin_payload = sys.stdin.read() if stdin_flag else None
|
|
826
|
+
payload = load_json_payload(json_payload=json_payload, file_path=file_path, stdin_payload=stdin_payload)
|
|
827
|
+
runtime = runtime_from_ctx(ctx)
|
|
828
|
+
runtime.no_fuzzy = True
|
|
829
|
+
execute_tool(
|
|
830
|
+
tool_name=adapter.canonical,
|
|
831
|
+
action=None,
|
|
832
|
+
params={},
|
|
833
|
+
runtime=runtime,
|
|
834
|
+
raw_payload=payload,
|
|
835
|
+
method=method,
|
|
836
|
+
console=console,
|
|
837
|
+
)
|
|
838
|
+
except Exception as exc: # noqa: BLE001
|
|
839
|
+
fail(exc)
|
|
840
|
+
|
|
841
|
+
for command_name, fields in adapter.option_aliases.items():
|
|
842
|
+
tool_app.command(command_name)(make_option_alias_command(adapter.canonical, fields))
|
|
843
|
+
|
|
844
|
+
return tool_app
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
registered_names: set[str] = set()
|
|
848
|
+
for adapter in TOOL_ADAPTERS.values():
|
|
849
|
+
group = make_tool_app(adapter)
|
|
850
|
+
for name in (adapter.command, *adapter.aliases):
|
|
851
|
+
if name not in registered_names:
|
|
852
|
+
app.add_typer(group, name=name)
|
|
853
|
+
registered_names.add(name)
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
# ---------------------------------------------------------------------------
|
|
857
|
+
# Beginner-friendly presets
|
|
858
|
+
# ---------------------------------------------------------------------------
|
|
859
|
+
@app.command("meta", context_settings=COMMON_CONTEXT_SETTINGS)
|
|
860
|
+
def preset_meta(
|
|
861
|
+
ctx: typer.Context,
|
|
862
|
+
keyword: str = typer.Option(..., "--keyword", "--kw"),
|
|
863
|
+
url: str | None = typer.Option(None, "--url"),
|
|
864
|
+
output_format: str | None = typer.Option(None, "--format"),
|
|
865
|
+
output: str | None = typer.Option(None, "--output"),
|
|
866
|
+
) -> None:
|
|
867
|
+
params = {"type": "meta-description", "keyword": keyword, "url": url, "format": output_format, "output": output}
|
|
868
|
+
run_tool_action(ctx, tool="seogpt", action="generate", params=params)
|
|
869
|
+
|
|
870
|
+
|
|
871
|
+
@app.command("title", context_settings=COMMON_CONTEXT_SETTINGS)
|
|
872
|
+
def preset_title(
|
|
873
|
+
ctx: typer.Context,
|
|
874
|
+
keyword: str = typer.Option(..., "--keyword", "--kw"),
|
|
875
|
+
url: str | None = typer.Option(None, "--url"),
|
|
876
|
+
brand: str | None = typer.Option(None, "--brand"),
|
|
877
|
+
output_format: str | None = typer.Option(None, "--format"),
|
|
878
|
+
output: str | None = typer.Option(None, "--output"),
|
|
879
|
+
) -> None:
|
|
880
|
+
params = {
|
|
881
|
+
"type": "page-title",
|
|
882
|
+
"keyword": keyword,
|
|
883
|
+
"url": url,
|
|
884
|
+
"brand": brand,
|
|
885
|
+
"format": output_format,
|
|
886
|
+
"output": output,
|
|
887
|
+
}
|
|
888
|
+
run_tool_action(ctx, tool="seogpt", action="generate", params=params)
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
@app.command("registry")
|
|
892
|
+
def registry(ctx: typer.Context) -> None:
|
|
893
|
+
"""Show local command adapters used on top of live definitions."""
|
|
894
|
+
|
|
895
|
+
try:
|
|
896
|
+
runtime = runtime_from_ctx(ctx)
|
|
897
|
+
data = {name: adapter_as_dict(adapter) for name, adapter in TOOL_ADAPTERS.items()}
|
|
898
|
+
data["command_aliases"] = all_command_names()
|
|
899
|
+
print_output(console, data, "json" if runtime.output_format == "pretty" else runtime.output_format, runtime.output)
|
|
900
|
+
except Exception as exc: # noqa: BLE001
|
|
901
|
+
fail(exc)
|
|
902
|
+
|
|
903
|
+
|
|
904
|
+
if __name__ == "__main__": # pragma: no cover
|
|
905
|
+
app()
|