kaiten-cli 0.3.3__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.
- kaiten_cli/__init__.py +5 -0
- kaiten_cli/__main__.py +9 -0
- kaiten_cli/agent_gateway.py +577 -0
- kaiten_cli/app.py +1636 -0
- kaiten_cli/completion.py +570 -0
- kaiten_cli/discovery.py +39 -0
- kaiten_cli/errors.py +79 -0
- kaiten_cli/i18n.py +92 -0
- kaiten_cli/locales/ru.json +2967 -0
- kaiten_cli/localized_click.py +160 -0
- kaiten_cli/models.py +202 -0
- kaiten_cli/profiles.py +316 -0
- kaiten_cli/registry/__init__.py +260 -0
- kaiten_cli/registry/addons.py +392 -0
- kaiten_cli/registry/audit_and_analytics.py +451 -0
- kaiten_cli/registry/automation_schema.py +44 -0
- kaiten_cli/registry/automation_schemas.json +7417 -0
- kaiten_cli/registry/automations.py +342 -0
- kaiten_cli/registry/base.py +62 -0
- kaiten_cli/registry/blockers.py +320 -0
- kaiten_cli/registry/boards.py +520 -0
- kaiten_cli/registry/card_relations.py +376 -0
- kaiten_cli/registry/card_types.py +363 -0
- kaiten_cli/registry/cards.py +1329 -0
- kaiten_cli/registry/charts.py +539 -0
- kaiten_cli/registry/checklists.py +600 -0
- kaiten_cli/registry/columns.py +558 -0
- kaiten_cli/registry/comments.py +224 -0
- kaiten_cli/registry/custom_directories.py +980 -0
- kaiten_cli/registry/custom_properties.py +1460 -0
- kaiten_cli/registry/dashboards.py +564 -0
- kaiten_cli/registry/documents.py +761 -0
- kaiten_cli/registry/external_links.py +128 -0
- kaiten_cli/registry/files.py +738 -0
- kaiten_cli/registry/github_addon.py +613 -0
- kaiten_cli/registry/iterations.py +397 -0
- kaiten_cli/registry/lanes.py +205 -0
- kaiten_cli/registry/live_contracts.py +361 -0
- kaiten_cli/registry/members.py +336 -0
- kaiten_cli/registry/module_docs.py +108 -0
- kaiten_cli/registry/projects.py +369 -0
- kaiten_cli/registry/query.py +129 -0
- kaiten_cli/registry/restrictions.py +249 -0
- kaiten_cli/registry/roles_and_groups.py +1102 -0
- kaiten_cli/registry/scim.py +263 -0
- kaiten_cli/registry/service_desk.py +1270 -0
- kaiten_cli/registry/snapshot.py +187 -0
- kaiten_cli/registry/spaces.py +272 -0
- kaiten_cli/registry/subscribers.py +168 -0
- kaiten_cli/registry/tags.py +193 -0
- kaiten_cli/registry/time_logs.py +353 -0
- kaiten_cli/registry/tree.py +352 -0
- kaiten_cli/registry/utilities.py +287 -0
- kaiten_cli/registry/webhooks.py +265 -0
- kaiten_cli/runtime/__init__.py +1 -0
- kaiten_cli/runtime/automation_validation.py +115 -0
- kaiten_cli/runtime/behaviors.py +787 -0
- kaiten_cli/runtime/cache.py +733 -0
- kaiten_cli/runtime/client.py +387 -0
- kaiten_cli/runtime/endpoints.py +59 -0
- kaiten_cli/runtime/executor.py +269 -0
- kaiten_cli/runtime/fs_security.py +168 -0
- kaiten_cli/runtime/input.py +310 -0
- kaiten_cli/runtime/output.py +59 -0
- kaiten_cli/runtime/public_validation.py +35 -0
- kaiten_cli/runtime/snapshot_build.py +527 -0
- kaiten_cli/runtime/snapshot_common.py +363 -0
- kaiten_cli/runtime/snapshot_query.py +306 -0
- kaiten_cli/runtime/snapshot_store.py +1192 -0
- kaiten_cli/runtime/snapshots.py +62 -0
- kaiten_cli/runtime/sqlite_errors.py +23 -0
- kaiten_cli/runtime/support/__init__.py +1 -0
- kaiten_cli/runtime/support/addons.py +953 -0
- kaiten_cli/runtime/support/audit.py +70 -0
- kaiten_cli/runtime/support/batch.py +234 -0
- kaiten_cli/runtime/support/card_move_url.py +213 -0
- kaiten_cli/runtime/support/cards.py +174 -0
- kaiten_cli/runtime/support/checklists.py +21 -0
- kaiten_cli/runtime/support/company_users.py +117 -0
- kaiten_cli/runtime/support/dashboards.py +35 -0
- kaiten_cli/runtime/support/documents.py +175 -0
- kaiten_cli/runtime/support/files.py +561 -0
- kaiten_cli/runtime/support/markdown_export.py +525 -0
- kaiten_cli/runtime/support/pagination.py +167 -0
- kaiten_cli/runtime/support/projects.py +66 -0
- kaiten_cli/runtime/support/relations.py +86 -0
- kaiten_cli/runtime/support/restrictions.py +62 -0
- kaiten_cli/runtime/support/spaces.py +20 -0
- kaiten_cli/runtime/support/time_logs.py +61 -0
- kaiten_cli/runtime/support/tree.py +140 -0
- kaiten_cli/runtime/support/tree_sharing.py +447 -0
- kaiten_cli/runtime/trace.py +500 -0
- kaiten_cli/runtime/transforms.py +128 -0
- kaiten_cli/schema_docs.py +22 -0
- kaiten_cli/update_check.py +519 -0
- kaiten_cli-0.3.3.dist-info/METADATA +1110 -0
- kaiten_cli-0.3.3.dist-info/RECORD +99 -0
- kaiten_cli-0.3.3.dist-info/WHEEL +4 -0
- kaiten_cli-0.3.3.dist-info/entry_points.txt +3 -0
kaiten_cli/app.py
ADDED
|
@@ -0,0 +1,1636 @@
|
|
|
1
|
+
"""CLI entrypoint."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
from collections import defaultdict
|
|
9
|
+
from contextvars import ContextVar
|
|
10
|
+
from functools import lru_cache
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import click
|
|
15
|
+
from click.exceptions import NoArgsIsHelpError
|
|
16
|
+
|
|
17
|
+
from kaiten_cli import __version__
|
|
18
|
+
from kaiten_cli.completion import (
|
|
19
|
+
SUPPORTED_SHELLS,
|
|
20
|
+
completion_status,
|
|
21
|
+
generate_completion_source,
|
|
22
|
+
install_completion,
|
|
23
|
+
uninstall_completion,
|
|
24
|
+
)
|
|
25
|
+
from kaiten_cli.discovery import describe_tool, search_tools, tool_examples
|
|
26
|
+
from kaiten_cli.errors import (
|
|
27
|
+
BatchExecutionError,
|
|
28
|
+
CliError,
|
|
29
|
+
ConfigError,
|
|
30
|
+
InternalError,
|
|
31
|
+
ValidationError,
|
|
32
|
+
)
|
|
33
|
+
from kaiten_cli.i18n import get_locale, tr, use_locale
|
|
34
|
+
from kaiten_cli.localized_click import LocalizedCommand, LocalizedGroup, resolve_locale
|
|
35
|
+
from kaiten_cli.models import GlobalOptions, ToolSpec
|
|
36
|
+
from kaiten_cli.profiles import (
|
|
37
|
+
add_profile,
|
|
38
|
+
list_profiles,
|
|
39
|
+
remove_profile,
|
|
40
|
+
resolve_profile,
|
|
41
|
+
show_profile,
|
|
42
|
+
use_profile,
|
|
43
|
+
)
|
|
44
|
+
from kaiten_cli.registry import iter_module_tools, iter_tools, resolve_tool
|
|
45
|
+
from kaiten_cli.registry.module_docs import MODULE_SPECS_BY_KEY
|
|
46
|
+
from kaiten_cli.runtime.executor import execute_tool_sync_with_diagnostics, read_only_enabled
|
|
47
|
+
from kaiten_cli.runtime.input import merge_inputs
|
|
48
|
+
from kaiten_cli.runtime.output import render_error, render_success
|
|
49
|
+
from kaiten_cli.runtime.trace import (
|
|
50
|
+
ExecutionStats,
|
|
51
|
+
TraceRecorder,
|
|
52
|
+
bulk_trace_meta,
|
|
53
|
+
summarize_trace,
|
|
54
|
+
)
|
|
55
|
+
from kaiten_cli.update_check import maybe_offer_update
|
|
56
|
+
|
|
57
|
+
_ARGV: ContextVar[list[str] | None] = ContextVar("kaiten_argv", default=None)
|
|
58
|
+
CLICK_CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
|
|
59
|
+
REPOSITORY_URL = "https://github.com/ViktorOgnev/kaiten-cli"
|
|
60
|
+
README_URL = f"{REPOSITORY_URL}/blob/master/README.md"
|
|
61
|
+
COMMAND_REFERENCE_URL = f"{REPOSITORY_URL}/blob/master/COMMAND_REFERENCE.md"
|
|
62
|
+
ARCHITECTURE_URL = f"{REPOSITORY_URL}/blob/master/ARCHITECTURE.md"
|
|
63
|
+
AGENTS_URL = f"{REPOSITORY_URL}/blob/master/AGENTS.md"
|
|
64
|
+
HEAVY_DATA_SKILL_URL = f"{REPOSITORY_URL}/blob/master/skills/kaiten-cli-heavy-data/SKILL.md"
|
|
65
|
+
METRICS_SKILL_URL = f"{REPOSITORY_URL}/blob/master/skills/kaiten-cli-metrics/SKILL.md"
|
|
66
|
+
MUTATIONS_SKILL_URL = f"{REPOSITORY_URL}/blob/master/skills/kaiten-cli-mutations/SKILL.md"
|
|
67
|
+
CLI_HELP = """Kaiten API CLI optimized for humans and agents.
|
|
68
|
+
|
|
69
|
+
\b
|
|
70
|
+
Quick start:
|
|
71
|
+
kaiten search-tools "wip cards"
|
|
72
|
+
kaiten describe cards.list-all
|
|
73
|
+
kaiten examples cards.list-all
|
|
74
|
+
kaiten snapshot build --name team-basic --space-id 10 --preset basic
|
|
75
|
+
kaiten query cards --snapshot team-basic --view summary --fields id,title,state
|
|
76
|
+
kaiten --json spaces list --compact --fields id,title
|
|
77
|
+
kaiten profile add main --domain <company-subdomain-or-url> --token <api-token> --set-active
|
|
78
|
+
kaiten --json --profile main --read-only profile probe
|
|
79
|
+
kaiten --json trace summarize --file ./kaiten-trace.jsonl
|
|
80
|
+
|
|
81
|
+
\b
|
|
82
|
+
Principles:
|
|
83
|
+
- use --json for automation and LLM workflows
|
|
84
|
+
- omit --cache-mode for normal workflows: auto reuses cacheable safe reads
|
|
85
|
+
- use refresh once at a freshness boundary, off for diagnostics/privacy/polling,
|
|
86
|
+
and readwrite only with an explicit fixed TTL
|
|
87
|
+
- run search-tools -> describe -> examples once per unfamiliar family, mutation,
|
|
88
|
+
or heavy command
|
|
89
|
+
- for repeated analytics/report reads, prefer snapshot build -> query cards/query metrics
|
|
90
|
+
- keep local queries summary-first; escalate to detail/evidence only after candidate reduction
|
|
91
|
+
- use a bulk alternative for two or more IDs; never put refresh in an entity loop
|
|
92
|
+
- use --compact and --fields to shrink payloads
|
|
93
|
+
- live validation runs only when KAITEN_LIVE=1|true
|
|
94
|
+
- use --trace-file for wrappers with 3+ CLI commands, >10 expected HTTP requests,
|
|
95
|
+
or an unavoidable loop
|
|
96
|
+
|
|
97
|
+
\b
|
|
98
|
+
More guided onboarding:
|
|
99
|
+
kaiten agent-help
|
|
100
|
+
"""
|
|
101
|
+
CLI_EPILOG = f"""\b
|
|
102
|
+
Documentation:
|
|
103
|
+
Repo: {REPOSITORY_URL}
|
|
104
|
+
README: {README_URL}
|
|
105
|
+
Command reference: {COMMAND_REFERENCE_URL}
|
|
106
|
+
Architecture: {ARCHITECTURE_URL}
|
|
107
|
+
Agent guide: {AGENTS_URL}
|
|
108
|
+
Skills:
|
|
109
|
+
heavy-data: {HEAVY_DATA_SKILL_URL}
|
|
110
|
+
metrics: {METRICS_SKILL_URL}
|
|
111
|
+
mutations: {MUTATIONS_SKILL_URL}
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _ctx_options(ctx: click.Context) -> GlobalOptions:
|
|
116
|
+
return ctx.ensure_object(GlobalOptions)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _discard_result_stats(ctx: click.Context) -> None:
|
|
120
|
+
ctx.meta.pop("last_stats_payload", None)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _echo_result(ctx: click.Context, command: str, data: Any) -> None:
|
|
124
|
+
options = _ctx_options(ctx)
|
|
125
|
+
stats = ctx.meta.pop("last_stats_payload", None)
|
|
126
|
+
click.echo(render_success(command, data, options.json_mode, stats=stats))
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _echo_human_result(ctx: click.Context, text: str) -> None:
|
|
130
|
+
_discard_result_stats(ctx)
|
|
131
|
+
click.echo(text)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _render_completion_text(action: str, data: dict[str, Any]) -> str:
|
|
135
|
+
configured = _yes_no(data.get("configured"))
|
|
136
|
+
headline = (
|
|
137
|
+
tr("Shell completion dry run for {value_0}.", value_0=data["shell"])
|
|
138
|
+
if data.get("dry_run")
|
|
139
|
+
else tr(
|
|
140
|
+
"Shell completion {value_0} for {value_1}.", value_0=tr(action), value_1=data["shell"]
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
lines = [
|
|
144
|
+
headline,
|
|
145
|
+
tr("Configured: {value_0}", value_0=configured),
|
|
146
|
+
tr("Script: {value_0}", value_0=data["script_path"]),
|
|
147
|
+
tr("Shell config: {value_0}", value_0=data["config_path"]),
|
|
148
|
+
]
|
|
149
|
+
if data.get("dry_run"):
|
|
150
|
+
lines.append(tr("Dry run: no files were changed."))
|
|
151
|
+
elif action == "installed":
|
|
152
|
+
lines.append(tr("Restart the shell: {value_0}", value_0=data["restart_command"]))
|
|
153
|
+
warnings = data.get("warnings") or []
|
|
154
|
+
if warnings:
|
|
155
|
+
lines.append(tr("Warnings:"))
|
|
156
|
+
lines.extend(tr(" - {value_0}", value_0=warning) for warning in warnings)
|
|
157
|
+
return "\n".join(lines)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _echo_completion_result(
|
|
161
|
+
ctx: click.Context, command: str, action: str, data: dict[str, Any]
|
|
162
|
+
) -> None:
|
|
163
|
+
if _ctx_options(ctx).json_mode:
|
|
164
|
+
_echo_result(ctx, command, data)
|
|
165
|
+
else:
|
|
166
|
+
_echo_human_result(ctx, _render_completion_text(action, data))
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _fail(ctx: click.Context, command: str | None, error: CliError) -> None:
|
|
170
|
+
options = _ctx_options(ctx)
|
|
171
|
+
stats = ctx.meta.pop("last_stats_payload", None)
|
|
172
|
+
click.echo(
|
|
173
|
+
render_error(command, error, options.json_mode, stats=stats), err=not options.json_mode
|
|
174
|
+
)
|
|
175
|
+
ctx.exit(error.exit_code)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _emit_internal(ctx: click.Context, command: str | None, exc: Exception) -> None:
|
|
179
|
+
_fail(
|
|
180
|
+
ctx,
|
|
181
|
+
command,
|
|
182
|
+
InternalError(tr("{value_0}: {value_1}", value_0=type(exc).__name__, value_1=exc)),
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _make_debug_reporter(ctx: click.Context):
|
|
187
|
+
options = _ctx_options(ctx)
|
|
188
|
+
if not options.verbose:
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
def reporter(message: str) -> None:
|
|
192
|
+
click.echo(tr("[verbose] {value_0}", value_0=message), err=True)
|
|
193
|
+
|
|
194
|
+
return reporter
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _trace_recorder(ctx: click.Context) -> TraceRecorder | None:
|
|
198
|
+
options = _ctx_options(ctx)
|
|
199
|
+
if not options.trace_file:
|
|
200
|
+
return None
|
|
201
|
+
return TraceRecorder(options.trace_file)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _current_argv(ctx: click.Context) -> list[str]:
|
|
205
|
+
root = ctx.find_root()
|
|
206
|
+
argv = root.meta.get("argv")
|
|
207
|
+
if isinstance(argv, list):
|
|
208
|
+
return list(argv)
|
|
209
|
+
return list(_ARGV.get() or sys.argv[1:])
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _trace_bulk_meta(data: Any) -> dict[str, Any]:
|
|
213
|
+
if isinstance(data, BatchExecutionError):
|
|
214
|
+
return bulk_trace_meta(data.data)
|
|
215
|
+
return bulk_trace_meta(data)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _stats_payload(stats: ExecutionStats | None, *, duration_ms: float) -> dict[str, Any]:
|
|
219
|
+
return (stats or ExecutionStats()).to_payload(command_duration_ms=duration_ms)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _emit_stats_summary(ctx: click.Context, stats_payload: dict[str, Any]) -> None:
|
|
223
|
+
options = _ctx_options(ctx)
|
|
224
|
+
if not options.verbose:
|
|
225
|
+
return
|
|
226
|
+
cache_hits = stats_payload.get("cache_hits", {})
|
|
227
|
+
click.echo(
|
|
228
|
+
tr(
|
|
229
|
+
"[verbose] stats: duration_ms={value_0:.2f} http_requests={value_1} api_wait_ms={value_2:.2f} cache_hits={value_3}",
|
|
230
|
+
value_0=stats_payload.get("command_duration_ms", 0),
|
|
231
|
+
value_1=stats_payload.get("http_request_count", 0),
|
|
232
|
+
value_2=stats_payload.get("api_wait_ms", 0),
|
|
233
|
+
value_3=cache_hits,
|
|
234
|
+
),
|
|
235
|
+
err=True,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _cli_command_from_canonical(canonical_name: str) -> str:
|
|
240
|
+
return "kaiten " + canonical_name.replace(".", " ")
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _cli_option_name(argument_name: str) -> str:
|
|
244
|
+
return "--" + argument_name.replace("_", "-")
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _yes_no(value: Any) -> str:
|
|
248
|
+
return tr("yes") if bool(value) else tr("no")
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _format_enum(values: Any) -> str:
|
|
252
|
+
if not values:
|
|
253
|
+
return ""
|
|
254
|
+
return " enum=" + "|".join(str(value) for value in values)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _format_short_help(text: str) -> str:
|
|
258
|
+
first_line = text.strip().splitlines()[0] if text.strip() else "Kaiten command group."
|
|
259
|
+
first_sentence = first_line.split(". ")[0].rstrip(".")
|
|
260
|
+
return first_sentence + "."
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _build_namespace_help() -> dict[tuple[str, ...], tuple[str, str]]:
|
|
264
|
+
buckets: dict[tuple[str, ...], dict[str, Any]] = defaultdict(
|
|
265
|
+
lambda: {"modules": set(), "children": set(), "total": 0}
|
|
266
|
+
)
|
|
267
|
+
for module_key, tools in iter_module_tools():
|
|
268
|
+
for tool in tools:
|
|
269
|
+
namespace_segments = tool.namespace_segments
|
|
270
|
+
for index in range(1, len(namespace_segments) + 1):
|
|
271
|
+
path = namespace_segments[:index]
|
|
272
|
+
buckets[path]["modules"].add(module_key)
|
|
273
|
+
buckets[path]["total"] += 1
|
|
274
|
+
if index == len(namespace_segments):
|
|
275
|
+
buckets[path]["children"].add(tool.action)
|
|
276
|
+
else:
|
|
277
|
+
buckets[path]["children"].add(namespace_segments[index])
|
|
278
|
+
|
|
279
|
+
help_by_path: dict[tuple[str, ...], tuple[str, str]] = {}
|
|
280
|
+
for path, bucket in buckets.items():
|
|
281
|
+
modules = sorted(bucket["modules"])
|
|
282
|
+
specs = [MODULE_SPECS_BY_KEY[module] for module in modules if module in MODULE_SPECS_BY_KEY]
|
|
283
|
+
if len(specs) == 1:
|
|
284
|
+
summary = tr(specs[0].description)
|
|
285
|
+
elif specs:
|
|
286
|
+
labels = ", ".join(tr(spec.label) for spec in specs)
|
|
287
|
+
summary = tr("Commands from these Kaiten areas: {labels}.", labels=labels)
|
|
288
|
+
else:
|
|
289
|
+
summary = tr("Kaiten command group for {path}.", path=".".join(path))
|
|
290
|
+
|
|
291
|
+
children = sorted(bucket["children"])
|
|
292
|
+
child_sample = ", ".join(children[:8])
|
|
293
|
+
if len(children) > 8:
|
|
294
|
+
child_sample += tr(", and {count} more", count=len(children) - 8)
|
|
295
|
+
detail = (
|
|
296
|
+
summary
|
|
297
|
+
+ "\n\n"
|
|
298
|
+
+ tr(
|
|
299
|
+
"Contains {count} commands under: {children}.",
|
|
300
|
+
count=bucket["total"],
|
|
301
|
+
children=child_sample,
|
|
302
|
+
)
|
|
303
|
+
)
|
|
304
|
+
help_by_path[path] = (detail, _format_short_help(summary))
|
|
305
|
+
return help_by_path
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
@lru_cache(maxsize=2)
|
|
309
|
+
def _namespace_help(locale: str):
|
|
310
|
+
with use_locale(locale):
|
|
311
|
+
return _build_namespace_help()
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
NAMESPACE_HELP = _namespace_help("en")
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _render_search_tools_text(query: str, results: list[dict[str, Any]]) -> str:
|
|
318
|
+
lines = [tr("Search results for: {value_0}", value_0=query), ""]
|
|
319
|
+
if not results:
|
|
320
|
+
lines.extend(
|
|
321
|
+
[
|
|
322
|
+
tr("No matching commands found."),
|
|
323
|
+
"",
|
|
324
|
+
tr("Try a broader query or inspect the full command list with: kaiten --help"),
|
|
325
|
+
]
|
|
326
|
+
)
|
|
327
|
+
return "\n".join(lines)
|
|
328
|
+
|
|
329
|
+
for index, item in enumerate(results, start=1):
|
|
330
|
+
canonical_name = item["canonical_name"]
|
|
331
|
+
flags = [
|
|
332
|
+
str(item.get("method", "GET")),
|
|
333
|
+
tr("mutation") if item.get("mutation") else tr("read"),
|
|
334
|
+
"read-only=allowed" if item.get("read_only_allowed") else "read-only=blocked",
|
|
335
|
+
"remote-effects=yes" if item.get("remote_side_effects") else "remote-effects=no",
|
|
336
|
+
str(item.get("execution_mode", "direct_http")),
|
|
337
|
+
f"cache={item.get('cache_policy', 'unknown')}",
|
|
338
|
+
]
|
|
339
|
+
if item.get("heavy"):
|
|
340
|
+
flags.append(tr("heavy"))
|
|
341
|
+
|
|
342
|
+
lines.append(tr("{value_0}. {value_1}", value_0=index, value_1=canonical_name))
|
|
343
|
+
lines.append(tr(" CLI: {value_0}", value_0=_cli_command_from_canonical(canonical_name)))
|
|
344
|
+
lines.append(tr(" {value_0}", value_0=item.get("description", "").strip()))
|
|
345
|
+
lines.append(tr(" {value_0}", value_0=" | ".join(flags)))
|
|
346
|
+
if item.get("bulk_alternative"):
|
|
347
|
+
lines.append(tr(" Bulk alternative: {value_0}", value_0=item["bulk_alternative"]))
|
|
348
|
+
notes = item.get("usage_notes") or []
|
|
349
|
+
if notes:
|
|
350
|
+
lines.append(tr(" Note: {value_0}", value_0=notes[0]))
|
|
351
|
+
lines.append(
|
|
352
|
+
tr(
|
|
353
|
+
" Next: kaiten describe {value_0}; kaiten examples {value_1}",
|
|
354
|
+
value_0=canonical_name,
|
|
355
|
+
value_1=canonical_name,
|
|
356
|
+
)
|
|
357
|
+
)
|
|
358
|
+
lines.append("")
|
|
359
|
+
|
|
360
|
+
lines.append(tr("Use --json before the command for machine-readable output."))
|
|
361
|
+
return "\n".join(lines).rstrip()
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _render_describe_text(description: dict[str, Any]) -> str:
|
|
365
|
+
canonical_name = description["canonical_name"]
|
|
366
|
+
lines = [
|
|
367
|
+
canonical_name,
|
|
368
|
+
"",
|
|
369
|
+
tr("Description: {value_0}", value_0=description.get("description", "")),
|
|
370
|
+
tr("CLI: {value_0}", value_0=_cli_command_from_canonical(canonical_name)),
|
|
371
|
+
tr("MCP alias: {value_0}", value_0=description.get("mcp_alias", "")),
|
|
372
|
+
(
|
|
373
|
+
tr(
|
|
374
|
+
"API: {value_0} {value_1} | mutation={value_2} | read-only={value_3} | remote-effects={value_4} | mode={value_5}",
|
|
375
|
+
value_0=description.get("method", ""),
|
|
376
|
+
value_1=description.get("path_template", ""),
|
|
377
|
+
value_2=_yes_no(description.get("mutation")),
|
|
378
|
+
value_3=tr("allowed") if description.get("read_only_allowed") else tr("blocked"),
|
|
379
|
+
value_4=_yes_no(description.get("remote_side_effects")),
|
|
380
|
+
value_5=description.get("execution_mode", ""),
|
|
381
|
+
)
|
|
382
|
+
),
|
|
383
|
+
(
|
|
384
|
+
tr(
|
|
385
|
+
"Cache: {value_0} ({value_1})",
|
|
386
|
+
value_0=description.get("cache_policy", ""),
|
|
387
|
+
value_1=description.get("cache_guidance", {}).get("strategy", "unknown"),
|
|
388
|
+
)
|
|
389
|
+
),
|
|
390
|
+
(
|
|
391
|
+
tr("Cache modes: ")
|
|
392
|
+
+ ", ".join(description.get("cache_guidance", {}).get("available_modes", []))
|
|
393
|
+
+ tr(" | default=")
|
|
394
|
+
+ str(description.get("cache_guidance", {}).get("default_mode", "auto"))
|
|
395
|
+
+ tr(" | recommended=")
|
|
396
|
+
+ str(description.get("cache_guidance", {}).get("recommended_mode", "auto"))
|
|
397
|
+
),
|
|
398
|
+
]
|
|
399
|
+
|
|
400
|
+
response_policy = description.get("response_policy", {})
|
|
401
|
+
lines.append(
|
|
402
|
+
tr(
|
|
403
|
+
"Response: kind={value_0} | compact={value_1} | fields={value_2} | heavy={value_3}",
|
|
404
|
+
value_0=response_policy.get("result_kind", "unknown"),
|
|
405
|
+
value_1=_yes_no(response_policy.get("compact_supported")),
|
|
406
|
+
value_2=_yes_no(response_policy.get("fields_supported")),
|
|
407
|
+
value_3=_yes_no(response_policy.get("heavy")),
|
|
408
|
+
)
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
if description.get("bulk_alternative"):
|
|
412
|
+
lines.append(tr("Bulk alternative: {value_0}", value_0=description["bulk_alternative"]))
|
|
413
|
+
if live_contract := description.get("live_contract"):
|
|
414
|
+
statuses = ", ".join(str(status) for status in live_contract.get("expected_statuses", []))
|
|
415
|
+
lines.append(
|
|
416
|
+
tr(
|
|
417
|
+
"Live contract: {value_0} ({value_1})",
|
|
418
|
+
value_0=live_contract.get("status"),
|
|
419
|
+
value_1=statuses or tr("no statuses"),
|
|
420
|
+
)
|
|
421
|
+
)
|
|
422
|
+
lines.append(tr("Live note: {value_0}", value_0=live_contract.get("note")))
|
|
423
|
+
|
|
424
|
+
arguments = description.get("arguments") or []
|
|
425
|
+
lines.extend(["", tr("Arguments:")])
|
|
426
|
+
if arguments:
|
|
427
|
+
for argument in arguments:
|
|
428
|
+
required = tr("required") if argument.get("required") else tr("optional")
|
|
429
|
+
type_display = argument.get("type_display") or argument.get("type") or "unknown"
|
|
430
|
+
option_name = _cli_option_name(str(argument.get("name")))
|
|
431
|
+
enum_display = _format_enum(argument.get("enum"))
|
|
432
|
+
minimum = argument.get("minimum")
|
|
433
|
+
maximum = argument.get("maximum")
|
|
434
|
+
if minimum is not None and maximum is not None:
|
|
435
|
+
bounds_display = tr(
|
|
436
|
+
", range={value_0}..{value_1}", value_0=minimum, value_1=maximum
|
|
437
|
+
)
|
|
438
|
+
elif minimum is not None:
|
|
439
|
+
bounds_display = tr(", minimum={value_0}", value_0=minimum)
|
|
440
|
+
elif maximum is not None:
|
|
441
|
+
bounds_display = tr(", maximum={value_0}", value_0=maximum)
|
|
442
|
+
else:
|
|
443
|
+
bounds_display = ""
|
|
444
|
+
arg_description = argument.get("description") or tr("No description.")
|
|
445
|
+
lines.append(
|
|
446
|
+
tr(
|
|
447
|
+
" {value_0} ({value_1}, {value_2}{value_3}{value_4}): {value_5}",
|
|
448
|
+
value_0=option_name,
|
|
449
|
+
value_1=type_display,
|
|
450
|
+
value_2=required,
|
|
451
|
+
value_3=enum_display,
|
|
452
|
+
value_4=bounds_display,
|
|
453
|
+
value_5=arg_description,
|
|
454
|
+
)
|
|
455
|
+
)
|
|
456
|
+
else:
|
|
457
|
+
lines.append(tr(" No tool-specific arguments."))
|
|
458
|
+
|
|
459
|
+
from kaiten_cli.schema_docs import schema_rows
|
|
460
|
+
|
|
461
|
+
nested = [
|
|
462
|
+
(path, definition, required)
|
|
463
|
+
for path, definition, required in schema_rows(description.get("input_schema", {}))
|
|
464
|
+
if "." in path or "<" in path or "[]" in path
|
|
465
|
+
]
|
|
466
|
+
if nested:
|
|
467
|
+
lines.extend(["", tr("Nested schemas (unknown extension fields are preserved):")])
|
|
468
|
+
for path, definition, required in nested:
|
|
469
|
+
kinds = definition.get("type", "any")
|
|
470
|
+
kinds = "|".join(kinds) if isinstance(kinds, list) else kinds
|
|
471
|
+
enum = _format_enum(definition.get("enum"))
|
|
472
|
+
limits = ", ".join(
|
|
473
|
+
f"{key}={definition[key]}"
|
|
474
|
+
for key in ("minItems", "maxItems", "minLength", "maxLength")
|
|
475
|
+
if key in definition
|
|
476
|
+
)
|
|
477
|
+
lines.append(
|
|
478
|
+
tr(
|
|
479
|
+
" {value_0} ({value_1}, {value_2}{value_3}{value_4}): {value_5}",
|
|
480
|
+
value_0=path,
|
|
481
|
+
value_1=kinds,
|
|
482
|
+
value_2=tr("required") if required else tr("optional"),
|
|
483
|
+
value_3=enum,
|
|
484
|
+
value_4=", " + limits if limits else "",
|
|
485
|
+
value_5=definition.get("description", ""),
|
|
486
|
+
)
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
examples = description.get("examples") or []
|
|
490
|
+
if examples:
|
|
491
|
+
lines.extend(["", tr("Examples:")])
|
|
492
|
+
for example in examples:
|
|
493
|
+
lines.append(tr(" {value_0}", value_0=example))
|
|
494
|
+
|
|
495
|
+
notes = description.get("usage_notes") or []
|
|
496
|
+
cache_guidance = description.get("cache_guidance") or {}
|
|
497
|
+
rendered_notes = [
|
|
498
|
+
cache_guidance.get("guidance"),
|
|
499
|
+
cache_guidance.get("refresh_hint"),
|
|
500
|
+
cache_guidance.get("off_hint"),
|
|
501
|
+
cache_guidance.get("readwrite_hint"),
|
|
502
|
+
*notes,
|
|
503
|
+
]
|
|
504
|
+
rendered_notes = [note for note in rendered_notes if note]
|
|
505
|
+
if rendered_notes:
|
|
506
|
+
lines.extend(["", tr("Notes:")])
|
|
507
|
+
for note in rendered_notes:
|
|
508
|
+
lines.append(tr(" - {value_0}", value_0=note))
|
|
509
|
+
|
|
510
|
+
lines.extend(
|
|
511
|
+
[
|
|
512
|
+
"",
|
|
513
|
+
tr("Next: kaiten examples {value_0}", value_0=canonical_name),
|
|
514
|
+
tr("Use --json before the command for machine-readable output."),
|
|
515
|
+
]
|
|
516
|
+
)
|
|
517
|
+
return "\n".join(lines)
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def _render_examples_text(identifier: str, examples: list[str]) -> str:
|
|
521
|
+
lines = [tr("Examples for: {value_0}", value_0=identifier), ""]
|
|
522
|
+
if not examples:
|
|
523
|
+
lines.append(tr("No examples registered for this command."))
|
|
524
|
+
else:
|
|
525
|
+
for index, example in enumerate(examples, start=1):
|
|
526
|
+
lines.append(tr("{value_0}. {value_1}", value_0=index, value_1=example))
|
|
527
|
+
lines.extend(["", tr("Next: kaiten describe {value_0}", value_0=identifier)])
|
|
528
|
+
return "\n".join(lines)
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def _agent_help_payload() -> dict[str, Any]:
|
|
532
|
+
return {
|
|
533
|
+
"summary": tr("Kaiten API CLI optimized for humans and agents."),
|
|
534
|
+
"llm_bootstrap": [
|
|
535
|
+
tr(
|
|
536
|
+
"Use --locale ru or --locale en before every command to match the current conversation language; explicit --locale overrides KAITEN_CLI_LOCALE. Other languages use en."
|
|
537
|
+
),
|
|
538
|
+
tr('Discover once per unfamiliar family: kaiten search-tools "wip cards"'),
|
|
539
|
+
tr("Inspect one tool: kaiten describe cards.list-all"),
|
|
540
|
+
tr("Check examples: kaiten examples cards.list-all"),
|
|
541
|
+
tr("For repeated analytics or report runs, build a local snapshot first."),
|
|
542
|
+
tr(
|
|
543
|
+
"Use query cards --view summary by default; switch to detail/evidence only for narrowed candidates."
|
|
544
|
+
),
|
|
545
|
+
tr("Use --json for automation and LLM workflows."),
|
|
546
|
+
tr(
|
|
547
|
+
"Read top-level JSON stats to understand API calls, wait time, cache hits, and grouped path families."
|
|
548
|
+
),
|
|
549
|
+
tr(
|
|
550
|
+
"Omit --cache-mode for normal workflows: auto reuses cacheable safe reads and heavy analytics."
|
|
551
|
+
),
|
|
552
|
+
tr("Use refresh once at a freshness boundary; never put refresh in an entity loop."),
|
|
553
|
+
tr(
|
|
554
|
+
"Use off only for cache diagnostics, privacy requirements, or high-frequency polling."
|
|
555
|
+
),
|
|
556
|
+
tr("Use readwrite only with a meaningful --cache-ttl-seconds."),
|
|
557
|
+
tr("Use a bulk_alternative for two or more IDs."),
|
|
558
|
+
tr("Shrink payloads with --compact and --fields."),
|
|
559
|
+
tr(
|
|
560
|
+
"Use --trace-file for wrappers with 3+ CLI commands, >10 expected HTTP requests, or an unavoidable loop."
|
|
561
|
+
),
|
|
562
|
+
tr(
|
|
563
|
+
"Summarize a trace locally with kaiten --json trace summarize --file <trace.jsonl>."
|
|
564
|
+
),
|
|
565
|
+
tr(
|
|
566
|
+
"Before mutation, run kaiten --json --profile <name> --read-only profile probe and follow the mutation skill."
|
|
567
|
+
),
|
|
568
|
+
tr(
|
|
569
|
+
"Treat dashboards as experimental and iterations/Restricted Access Files as beta; run discovery first and expect feature/version gates."
|
|
570
|
+
),
|
|
571
|
+
],
|
|
572
|
+
"quickstart": [
|
|
573
|
+
tr('Discover commands: kaiten search-tools "wip cards"'),
|
|
574
|
+
tr("Inspect one tool: kaiten describe cards.list-all"),
|
|
575
|
+
tr("See examples: kaiten examples cards.list-all"),
|
|
576
|
+
tr(
|
|
577
|
+
"Build a local read snapshot: kaiten snapshot build --name team-basic --space-id 10 --preset basic"
|
|
578
|
+
),
|
|
579
|
+
tr(
|
|
580
|
+
"Query locally after build: kaiten query cards --snapshot team-basic --view summary --fields id,title,state"
|
|
581
|
+
),
|
|
582
|
+
tr("Prefer machine-safe output: kaiten --json spaces list --compact --fields id,title"),
|
|
583
|
+
tr(
|
|
584
|
+
"Configure credentials: kaiten profile add main --domain <company-subdomain-or-url> --token <api-token> --set-active"
|
|
585
|
+
),
|
|
586
|
+
tr(
|
|
587
|
+
"Probe authentication safely: kaiten --json --profile main --read-only profile probe"
|
|
588
|
+
),
|
|
589
|
+
tr("Summarize a trace locally: kaiten --json trace summarize --file <trace.jsonl>"),
|
|
590
|
+
tr(
|
|
591
|
+
"Explore dashboards safely: kaiten --json dashboards list --fields id,title,is_public,role --compact"
|
|
592
|
+
),
|
|
593
|
+
tr(
|
|
594
|
+
"Explore iterations safely: kaiten --json iterations list --space-uid <space_uuid> --status planned,active --compact"
|
|
595
|
+
),
|
|
596
|
+
],
|
|
597
|
+
"principles": [
|
|
598
|
+
tr("Use --json for automation and LLM workflows."),
|
|
599
|
+
tr("Read top-level JSON stats before repeating or widening expensive workflows."),
|
|
600
|
+
tr("Omit --cache-mode for normal workflows: auto is the recommended default."),
|
|
601
|
+
tr(
|
|
602
|
+
"Use refresh once before a freshness-critical result or use snapshot refresh; never put refresh in a loop."
|
|
603
|
+
),
|
|
604
|
+
tr(
|
|
605
|
+
"Use off only for cache diagnostics, privacy requirements, or high-frequency polling."
|
|
606
|
+
),
|
|
607
|
+
tr("Use readwrite only with a meaningful fixed --cache-ttl-seconds."),
|
|
608
|
+
tr(
|
|
609
|
+
"Run search-tools -> describe -> examples once per unfamiliar command family, mutation, or heavy command."
|
|
610
|
+
),
|
|
611
|
+
tr(
|
|
612
|
+
"For a population used more than once, snapshot once and query locally before touching the API again."
|
|
613
|
+
),
|
|
614
|
+
tr(
|
|
615
|
+
"For two or more IDs, use bulk_alternative when available instead of a per-entity loop."
|
|
616
|
+
),
|
|
617
|
+
tr(
|
|
618
|
+
"Prefer bulk tools like cards.list-all, cards.batch-get, time-logs.batch-list, space-activity-all.get, card-children.batch-list, comments.batch-list, and card-location-history.batch-get."
|
|
619
|
+
),
|
|
620
|
+
tr(
|
|
621
|
+
"Keep query cards summary-first; use detail/evidence only after local candidate reduction."
|
|
622
|
+
),
|
|
623
|
+
tr("Live validation runs only when KAITEN_LIVE=1|true for the current process."),
|
|
624
|
+
tr("Use --compact and --fields to reduce payload and token cost."),
|
|
625
|
+
tr(
|
|
626
|
+
"Use --trace-file for wrappers with 3+ CLI commands, >10 expected HTTP requests, or an unavoidable loop."
|
|
627
|
+
),
|
|
628
|
+
tr("Inspect trace cost with kaiten --json trace summarize --file <trace.jsonl>."),
|
|
629
|
+
tr(
|
|
630
|
+
"Before mutations: profile probe, read-only investigation, exact preview, authorization, resumable manifest, small batches, and field-scoped readback."
|
|
631
|
+
),
|
|
632
|
+
tr(
|
|
633
|
+
"Dashboards are experimental; iterations and Restricted Access Files are beta and may be unavailable on older installations or tariffs."
|
|
634
|
+
),
|
|
635
|
+
],
|
|
636
|
+
"docs": {
|
|
637
|
+
"repository": REPOSITORY_URL,
|
|
638
|
+
"readme": README_URL,
|
|
639
|
+
"command_reference": COMMAND_REFERENCE_URL,
|
|
640
|
+
"architecture": ARCHITECTURE_URL,
|
|
641
|
+
"agent_guide": AGENTS_URL,
|
|
642
|
+
"skills": {
|
|
643
|
+
"heavy_data": HEAVY_DATA_SKILL_URL,
|
|
644
|
+
"metrics": METRICS_SKILL_URL,
|
|
645
|
+
"mutations": MUTATIONS_SKILL_URL,
|
|
646
|
+
},
|
|
647
|
+
},
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def _agent_help_text() -> str:
|
|
652
|
+
return "\n".join(
|
|
653
|
+
[
|
|
654
|
+
tr("Kaiten agent bootstrap"),
|
|
655
|
+
"",
|
|
656
|
+
tr("LLM bootstrap:"),
|
|
657
|
+
tr(
|
|
658
|
+
"Use --locale ru or --locale en before every command to match the current conversation language; explicit --locale overrides KAITEN_CLI_LOCALE. Other languages use en."
|
|
659
|
+
),
|
|
660
|
+
tr('1. discover: kaiten search-tools "wip cards"'),
|
|
661
|
+
tr("2. inspect: kaiten describe cards.list-all"),
|
|
662
|
+
tr("3. examples: kaiten examples cards.list-all"),
|
|
663
|
+
tr("4. use --json for automation and LLM workflows"),
|
|
664
|
+
tr(
|
|
665
|
+
"5. inspect JSON stats for API count, wait time, cache hits, and grouped path families"
|
|
666
|
+
),
|
|
667
|
+
tr("6. omit --cache-mode for normal workflows: auto is the recommended default"),
|
|
668
|
+
tr(
|
|
669
|
+
"7. use refresh once at a freshness boundary, off for diagnostics/privacy/polling, and readwrite only with --cache-ttl-seconds"
|
|
670
|
+
),
|
|
671
|
+
tr("8. use a bulk alternative for 2+ IDs; never put refresh in an entity loop"),
|
|
672
|
+
tr("9. snapshot a population before its second use"),
|
|
673
|
+
tr(
|
|
674
|
+
"10. snapshot once: kaiten snapshot build --name team-basic --space-id 10 --preset basic"
|
|
675
|
+
),
|
|
676
|
+
tr(
|
|
677
|
+
"11. query locally: kaiten query cards --snapshot team-basic --view summary --fields id,title,state"
|
|
678
|
+
),
|
|
679
|
+
tr("12. only escalate to --view detail or --view evidence after local narrowing"),
|
|
680
|
+
tr("13. shrink payloads with --compact and --fields"),
|
|
681
|
+
tr("14. trace wrappers with 3+ CLI commands, >10 expected HTTP requests, or a loop"),
|
|
682
|
+
tr("15. summarize: kaiten --json trace summarize --file <trace.jsonl>"),
|
|
683
|
+
tr("16. before mutations: kaiten --json --profile <name> --read-only profile probe"),
|
|
684
|
+
tr("17. live validation only runs when KAITEN_LIVE=1|true"),
|
|
685
|
+
tr(
|
|
686
|
+
"18. dashboards are experimental; iterations/Restricted Access Files are beta, so discover and probe before use"
|
|
687
|
+
),
|
|
688
|
+
"",
|
|
689
|
+
tr("Good bulk defaults:"),
|
|
690
|
+
" kaiten --json cards list-all --board-id 10 --selection active_only --fields id,title,state --compact",
|
|
691
|
+
" kaiten --json cards batch-get --card-ids '[101,102,103]' --workers 2 --fields id,title,description",
|
|
692
|
+
" kaiten --json time-logs batch-list --card-ids '[101,102,103]' --workers 2 --fields id,time_spent,for_date",
|
|
693
|
+
" kaiten --json card-children batch-list --card-ids '[101,102,103]' --workers 2 --compact --fields id,title",
|
|
694
|
+
" kaiten --json comments batch-list --card-ids '[101,102,103]' --workers 2 --compact --fields id,text",
|
|
695
|
+
" kaiten --json card-location-history batch-get --card-ids '[101,102,103]' --workers 2 --fields changed,column_id",
|
|
696
|
+
" kaiten --json snapshot build --name team-basic --space-id 10 --preset basic",
|
|
697
|
+
" kaiten --json query metrics --snapshot team-basic --metric count --group-by board_id",
|
|
698
|
+
" kaiten --json dashboards list --fields id,title,is_public,role --compact",
|
|
699
|
+
" kaiten --json iterations list --space-uid <space_uuid> --status planned,active --compact",
|
|
700
|
+
"",
|
|
701
|
+
tr("Docs:"),
|
|
702
|
+
tr(" repo: {value_0}", value_0=REPOSITORY_URL),
|
|
703
|
+
tr(" readme: {value_0}", value_0=README_URL),
|
|
704
|
+
tr(" command reference: {value_0}", value_0=COMMAND_REFERENCE_URL),
|
|
705
|
+
tr(" architecture: {value_0}", value_0=ARCHITECTURE_URL),
|
|
706
|
+
tr(" agents: {value_0}", value_0=AGENTS_URL),
|
|
707
|
+
tr(" skills heavy-data: {value_0}", value_0=HEAVY_DATA_SKILL_URL),
|
|
708
|
+
tr(" skills metrics: {value_0}", value_0=METRICS_SKILL_URL),
|
|
709
|
+
tr(" skills mutations: {value_0}", value_0=MUTATIONS_SKILL_URL),
|
|
710
|
+
]
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
def _run_traced(ctx: click.Context, command: str, execution_mode: str, callback):
|
|
715
|
+
recorder = _trace_recorder(ctx)
|
|
716
|
+
start = time.perf_counter()
|
|
717
|
+
try:
|
|
718
|
+
result, stats = callback()
|
|
719
|
+
duration_ms = (time.perf_counter() - start) * 1000.0
|
|
720
|
+
stats_payload = _stats_payload(stats, duration_ms=duration_ms)
|
|
721
|
+
ctx.meta["last_stats_payload"] = stats_payload
|
|
722
|
+
_emit_stats_summary(ctx, stats_payload)
|
|
723
|
+
if recorder is not None:
|
|
724
|
+
_write_trace_safely(
|
|
725
|
+
recorder,
|
|
726
|
+
canonical_name=command,
|
|
727
|
+
execution_mode=execution_mode,
|
|
728
|
+
argv=_current_argv(ctx),
|
|
729
|
+
exit_code=0,
|
|
730
|
+
duration_ms=duration_ms,
|
|
731
|
+
stats=stats,
|
|
732
|
+
bulk_meta=_trace_bulk_meta(result),
|
|
733
|
+
)
|
|
734
|
+
return result
|
|
735
|
+
except CliError as error:
|
|
736
|
+
duration_ms = (time.perf_counter() - start) * 1000.0
|
|
737
|
+
stats = getattr(error, "_kaiten_trace_stats", None)
|
|
738
|
+
ctx.meta["last_stats_payload"] = _stats_payload(stats, duration_ms=duration_ms)
|
|
739
|
+
if recorder is not None:
|
|
740
|
+
_write_trace_safely(
|
|
741
|
+
recorder,
|
|
742
|
+
canonical_name=command,
|
|
743
|
+
execution_mode=execution_mode,
|
|
744
|
+
argv=_current_argv(ctx),
|
|
745
|
+
exit_code=error.exit_code,
|
|
746
|
+
duration_ms=duration_ms,
|
|
747
|
+
stats=stats,
|
|
748
|
+
bulk_meta=_trace_bulk_meta(error),
|
|
749
|
+
)
|
|
750
|
+
raise
|
|
751
|
+
except Exception as exc:
|
|
752
|
+
duration_ms = (time.perf_counter() - start) * 1000.0
|
|
753
|
+
stats = getattr(exc, "_kaiten_trace_stats", None)
|
|
754
|
+
ctx.meta["last_stats_payload"] = _stats_payload(stats, duration_ms=duration_ms)
|
|
755
|
+
if recorder is not None:
|
|
756
|
+
_write_trace_safely(
|
|
757
|
+
recorder,
|
|
758
|
+
canonical_name=command,
|
|
759
|
+
execution_mode=execution_mode,
|
|
760
|
+
argv=_current_argv(ctx),
|
|
761
|
+
exit_code=70,
|
|
762
|
+
duration_ms=duration_ms,
|
|
763
|
+
stats=stats,
|
|
764
|
+
bulk_meta={},
|
|
765
|
+
)
|
|
766
|
+
raise
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
def _write_trace_safely(recorder: TraceRecorder, **kwargs: Any) -> None:
|
|
770
|
+
"""Keep observability failures from changing the primary command outcome."""
|
|
771
|
+
|
|
772
|
+
try:
|
|
773
|
+
recorder.write(**kwargs)
|
|
774
|
+
except Exception as error:
|
|
775
|
+
click.echo(
|
|
776
|
+
tr("Warning: trace record was not written ({value_0}).", value_0=type(error).__name__),
|
|
777
|
+
err=True,
|
|
778
|
+
)
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
def _dynamic_callback(tool: ToolSpec):
|
|
782
|
+
@click.pass_context
|
|
783
|
+
def callback(ctx: click.Context, **kwargs: Any) -> None:
|
|
784
|
+
options = _ctx_options(ctx)
|
|
785
|
+
stdin_text = click.get_text_stream("stdin").read() if options.stdin_json else None
|
|
786
|
+
reporter = _make_debug_reporter(ctx)
|
|
787
|
+
try:
|
|
788
|
+
result = _run_traced(
|
|
789
|
+
ctx,
|
|
790
|
+
tool.canonical_name,
|
|
791
|
+
tool.execution_mode,
|
|
792
|
+
lambda: execute_tool_sync_with_diagnostics(
|
|
793
|
+
tool,
|
|
794
|
+
merge_inputs(
|
|
795
|
+
tool,
|
|
796
|
+
kwargs,
|
|
797
|
+
from_file=options.from_file,
|
|
798
|
+
stdin_json=options.stdin_json,
|
|
799
|
+
stdin_text=stdin_text,
|
|
800
|
+
),
|
|
801
|
+
profile_name=options.profile_name,
|
|
802
|
+
cache_mode=options.cache_mode,
|
|
803
|
+
cache_ttl_seconds=options.cache_ttl_seconds,
|
|
804
|
+
reporter=reporter,
|
|
805
|
+
read_only=options.read_only,
|
|
806
|
+
),
|
|
807
|
+
)
|
|
808
|
+
_echo_result(ctx, tool.canonical_name, result)
|
|
809
|
+
except CliError as error:
|
|
810
|
+
_fail(ctx, tool.canonical_name, error)
|
|
811
|
+
except Exception as exc: # pragma: no cover - safety net
|
|
812
|
+
_emit_internal(ctx, tool.canonical_name, exc)
|
|
813
|
+
|
|
814
|
+
return callback
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
def _click_type_for(schema: dict[str, Any]) -> click.ParamType | None:
|
|
818
|
+
schema_type = schema.get("type")
|
|
819
|
+
allowed = schema_type if isinstance(schema_type, list) else [schema_type]
|
|
820
|
+
if len(allowed) > 1:
|
|
821
|
+
return click.STRING
|
|
822
|
+
if "integer" in allowed and "string" not in allowed:
|
|
823
|
+
return click.IntRange(min=schema.get("minimum"), max=schema.get("maximum"))
|
|
824
|
+
if "number" in allowed:
|
|
825
|
+
return click.FloatRange(min=schema.get("minimum"), max=schema.get("maximum"))
|
|
826
|
+
if "boolean" in allowed:
|
|
827
|
+
return None
|
|
828
|
+
return click.STRING
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
def _command_params(tool: ToolSpec) -> list[click.Parameter]:
|
|
832
|
+
params: list[click.Parameter] = []
|
|
833
|
+
for field_name, schema in tool.input_schema.get("properties", {}).items():
|
|
834
|
+
option_name = f"--{field_name.replace('_', '-')}"
|
|
835
|
+
description = schema.get("description", "")
|
|
836
|
+
allowed = schema.get("type")
|
|
837
|
+
allowed_types = allowed if isinstance(allowed, list) else [allowed]
|
|
838
|
+
if "boolean" in allowed_types and schema.get("enum") is None:
|
|
839
|
+
params.append(
|
|
840
|
+
click.Option(
|
|
841
|
+
[f"{option_name}/--no-{field_name.replace('_', '-')}", field_name],
|
|
842
|
+
default=None,
|
|
843
|
+
help=description,
|
|
844
|
+
)
|
|
845
|
+
)
|
|
846
|
+
continue
|
|
847
|
+
option = click.Option(
|
|
848
|
+
[option_name, field_name],
|
|
849
|
+
type=_click_type_for(schema),
|
|
850
|
+
default=None,
|
|
851
|
+
required=False,
|
|
852
|
+
help=description,
|
|
853
|
+
)
|
|
854
|
+
params.append(option)
|
|
855
|
+
return params
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
def _make_command(tool: ToolSpec, *, hidden: bool = False) -> click.Command:
|
|
859
|
+
return LocalizedCommand(
|
|
860
|
+
name=tool.action if not hidden else tool.mcp_alias,
|
|
861
|
+
help=tool.description,
|
|
862
|
+
short_help=tool.description,
|
|
863
|
+
context_settings=CLICK_CONTEXT_SETTINGS,
|
|
864
|
+
params=_command_params(tool),
|
|
865
|
+
callback=_dynamic_callback(tool),
|
|
866
|
+
hidden=hidden,
|
|
867
|
+
)
|
|
868
|
+
|
|
869
|
+
|
|
870
|
+
def _ensure_group(root: click.Group, segments: tuple[str, ...]) -> click.Group:
|
|
871
|
+
group = root
|
|
872
|
+
current_path: tuple[str, ...] = ()
|
|
873
|
+
for segment in segments:
|
|
874
|
+
current_path = current_path + (segment,)
|
|
875
|
+
existing = group.commands.get(segment)
|
|
876
|
+
if existing is None:
|
|
877
|
+
group_help, short_help = NAMESPACE_HELP.get(
|
|
878
|
+
current_path,
|
|
879
|
+
(
|
|
880
|
+
f"Kaiten command group for {'.'.join(current_path)}.",
|
|
881
|
+
f"Kaiten command group for {'.'.join(current_path)}.",
|
|
882
|
+
),
|
|
883
|
+
)
|
|
884
|
+
nested = LocalizedGroup(
|
|
885
|
+
name=segment,
|
|
886
|
+
no_args_is_help=True,
|
|
887
|
+
help=group_help,
|
|
888
|
+
short_help=short_help,
|
|
889
|
+
context_settings=CLICK_CONTEXT_SETTINGS,
|
|
890
|
+
)
|
|
891
|
+
nested.help_factory = lambda path=current_path: _namespace_help(get_locale())[path][0]
|
|
892
|
+
group.add_command(nested)
|
|
893
|
+
group = nested
|
|
894
|
+
continue
|
|
895
|
+
if not isinstance(existing, click.Group): # pragma: no cover - defensive
|
|
896
|
+
raise RuntimeError(f"Command path collision at {segment}")
|
|
897
|
+
group = existing
|
|
898
|
+
return group
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
@click.group(
|
|
902
|
+
cls=LocalizedGroup,
|
|
903
|
+
context_settings=CLICK_CONTEXT_SETTINGS,
|
|
904
|
+
no_args_is_help=True,
|
|
905
|
+
help=CLI_HELP,
|
|
906
|
+
epilog=CLI_EPILOG,
|
|
907
|
+
)
|
|
908
|
+
@click.option(
|
|
909
|
+
"--locale", type=click.Choice(["en", "ru"]), help="Language for CLI messages (default: en)."
|
|
910
|
+
)
|
|
911
|
+
@click.version_option(version=__version__, prog_name="kaiten")
|
|
912
|
+
@click.option(
|
|
913
|
+
"--json", "json_mode", is_flag=True, default=False, help="Emit machine-readable JSON output."
|
|
914
|
+
)
|
|
915
|
+
@click.option(
|
|
916
|
+
"--profile",
|
|
917
|
+
"profile_name",
|
|
918
|
+
type=click.STRING,
|
|
919
|
+
default=None,
|
|
920
|
+
help="Configuration profile to use.",
|
|
921
|
+
)
|
|
922
|
+
@click.option(
|
|
923
|
+
"--from-file",
|
|
924
|
+
"--input-file",
|
|
925
|
+
type=click.Path(exists=True, dir_okay=False, path_type=str),
|
|
926
|
+
default=None,
|
|
927
|
+
help="Load the full JSON payload from a file.",
|
|
928
|
+
)
|
|
929
|
+
@click.option(
|
|
930
|
+
"--stdin-json", is_flag=True, default=False, help="Read the full JSON payload from stdin."
|
|
931
|
+
)
|
|
932
|
+
@click.option("--verbose", is_flag=True, default=False, help="Enable verbose diagnostics.")
|
|
933
|
+
@click.option(
|
|
934
|
+
"--cache-mode",
|
|
935
|
+
type=click.Choice(["auto", "off", "readwrite", "refresh"]),
|
|
936
|
+
default=None,
|
|
937
|
+
help="Persistent cache mode. Default auto adapts TTL by tool cost; request-scoped cache stays enabled for safe GETs.",
|
|
938
|
+
)
|
|
939
|
+
@click.option(
|
|
940
|
+
"--cache-ttl-seconds",
|
|
941
|
+
type=click.INT,
|
|
942
|
+
default=None,
|
|
943
|
+
help="TTL for persistent cache entries in seconds.",
|
|
944
|
+
)
|
|
945
|
+
@click.option(
|
|
946
|
+
"--trace-file",
|
|
947
|
+
type=click.Path(dir_okay=False, path_type=str),
|
|
948
|
+
default=None,
|
|
949
|
+
help="Append compact execution traces as JSONL.",
|
|
950
|
+
)
|
|
951
|
+
@click.option(
|
|
952
|
+
"--read-only",
|
|
953
|
+
is_flag=True,
|
|
954
|
+
default=False,
|
|
955
|
+
help="Block commands that mutate Kaiten; KAITEN_CLI_READ_ONLY=1 enables the same policy.",
|
|
956
|
+
)
|
|
957
|
+
@click.option(
|
|
958
|
+
"--no-update-check",
|
|
959
|
+
is_flag=True,
|
|
960
|
+
default=False,
|
|
961
|
+
help="Skip the post-command check for a newer kaiten-cli release.",
|
|
962
|
+
)
|
|
963
|
+
@click.option("--no-color", is_flag=True, default=False, help="Disable colorized output.")
|
|
964
|
+
@click.pass_context
|
|
965
|
+
def cli(
|
|
966
|
+
ctx: click.Context,
|
|
967
|
+
locale: str | None,
|
|
968
|
+
json_mode: bool,
|
|
969
|
+
profile_name: str | None,
|
|
970
|
+
from_file: str | None,
|
|
971
|
+
stdin_json: bool,
|
|
972
|
+
verbose: bool,
|
|
973
|
+
cache_mode: str | None,
|
|
974
|
+
cache_ttl_seconds: int | None,
|
|
975
|
+
trace_file: str | None,
|
|
976
|
+
read_only: bool,
|
|
977
|
+
no_update_check: bool,
|
|
978
|
+
no_color: bool,
|
|
979
|
+
) -> None:
|
|
980
|
+
if no_color:
|
|
981
|
+
ctx.color = False
|
|
982
|
+
ctx.meta["argv"] = list(_ARGV.get() or sys.argv[1:])
|
|
983
|
+
ctx.obj = GlobalOptions(
|
|
984
|
+
locale=get_locale(),
|
|
985
|
+
json_mode=json_mode,
|
|
986
|
+
profile_name=profile_name,
|
|
987
|
+
from_file=from_file,
|
|
988
|
+
stdin_json=stdin_json,
|
|
989
|
+
verbose=verbose,
|
|
990
|
+
no_color=no_color,
|
|
991
|
+
cache_mode=cache_mode,
|
|
992
|
+
cache_ttl_seconds=cache_ttl_seconds,
|
|
993
|
+
trace_file=trace_file or os.environ.get("KAITEN_TRACE_FILE"),
|
|
994
|
+
read_only=read_only or read_only_enabled(),
|
|
995
|
+
update_check=not no_update_check,
|
|
996
|
+
)
|
|
997
|
+
|
|
998
|
+
|
|
999
|
+
@cli.command(
|
|
1000
|
+
"search-tools",
|
|
1001
|
+
help="Search the command registry and show ranked commands with usage guidance.",
|
|
1002
|
+
short_help="Search commands with usage guidance.",
|
|
1003
|
+
)
|
|
1004
|
+
@click.argument("query", type=click.STRING, metavar="QUERY")
|
|
1005
|
+
@click.pass_context
|
|
1006
|
+
def search_tools_command(ctx: click.Context, query: str) -> None:
|
|
1007
|
+
try:
|
|
1008
|
+
result = _run_traced(ctx, "search-tools", "meta", lambda: (search_tools(query), None))
|
|
1009
|
+
if _ctx_options(ctx).json_mode:
|
|
1010
|
+
_echo_result(ctx, "search-tools", result)
|
|
1011
|
+
else:
|
|
1012
|
+
_echo_human_result(ctx, _render_search_tools_text(query, result))
|
|
1013
|
+
except CliError as error:
|
|
1014
|
+
_fail(ctx, "search-tools", error)
|
|
1015
|
+
except Exception as exc: # pragma: no cover - safety net
|
|
1016
|
+
_emit_internal(ctx, "search-tools", exc)
|
|
1017
|
+
|
|
1018
|
+
|
|
1019
|
+
@cli.command(
|
|
1020
|
+
"describe",
|
|
1021
|
+
help="Describe one command: API path, arguments, cache behavior, examples and notes.",
|
|
1022
|
+
short_help="Describe one command.",
|
|
1023
|
+
)
|
|
1024
|
+
@click.argument("identifier", type=click.STRING, metavar="IDENTIFIER")
|
|
1025
|
+
@click.pass_context
|
|
1026
|
+
def describe_command(ctx: click.Context, identifier: str) -> None:
|
|
1027
|
+
try:
|
|
1028
|
+
result = _run_traced(ctx, "describe", "meta", lambda: (describe_tool(identifier), None))
|
|
1029
|
+
if _ctx_options(ctx).json_mode:
|
|
1030
|
+
_echo_result(ctx, "describe", result)
|
|
1031
|
+
else:
|
|
1032
|
+
_echo_human_result(ctx, _render_describe_text(result))
|
|
1033
|
+
except KeyError:
|
|
1034
|
+
_fail(ctx, "describe", ConfigError(tr("Unknown command: {value_0}", value_0=identifier)))
|
|
1035
|
+
except CliError as error:
|
|
1036
|
+
_fail(ctx, "describe", error)
|
|
1037
|
+
except Exception as exc: # pragma: no cover
|
|
1038
|
+
_emit_internal(ctx, "describe", exc)
|
|
1039
|
+
|
|
1040
|
+
|
|
1041
|
+
@cli.command(
|
|
1042
|
+
"examples",
|
|
1043
|
+
help="Show runnable examples for one command.",
|
|
1044
|
+
short_help="Show command examples.",
|
|
1045
|
+
)
|
|
1046
|
+
@click.argument("identifier", type=click.STRING, metavar="IDENTIFIER")
|
|
1047
|
+
@click.pass_context
|
|
1048
|
+
def examples_command(ctx: click.Context, identifier: str) -> None:
|
|
1049
|
+
try:
|
|
1050
|
+
result = _run_traced(
|
|
1051
|
+
ctx, "examples", "meta", lambda: ({"examples": tool_examples(identifier)}, None)
|
|
1052
|
+
)
|
|
1053
|
+
if _ctx_options(ctx).json_mode:
|
|
1054
|
+
_echo_result(ctx, "examples", result)
|
|
1055
|
+
else:
|
|
1056
|
+
_echo_human_result(ctx, _render_examples_text(identifier, result["examples"]))
|
|
1057
|
+
except KeyError:
|
|
1058
|
+
_fail(ctx, "examples", ConfigError(tr("Unknown command: {value_0}", value_0=identifier)))
|
|
1059
|
+
except CliError as error:
|
|
1060
|
+
_fail(ctx, "examples", error)
|
|
1061
|
+
except Exception as exc: # pragma: no cover
|
|
1062
|
+
_emit_internal(ctx, "examples", exc)
|
|
1063
|
+
|
|
1064
|
+
|
|
1065
|
+
@cli.command(
|
|
1066
|
+
"agent-help",
|
|
1067
|
+
help="Show an agent-oriented bootstrap with discovery, bulk-read and snapshot guidance.",
|
|
1068
|
+
short_help="Show agent-oriented bootstrap guidance.",
|
|
1069
|
+
)
|
|
1070
|
+
@click.pass_context
|
|
1071
|
+
def agent_help_command(ctx: click.Context) -> None:
|
|
1072
|
+
try:
|
|
1073
|
+
result = _run_traced(ctx, "agent-help", "meta", lambda: (_agent_help_payload(), None))
|
|
1074
|
+
options = _ctx_options(ctx)
|
|
1075
|
+
if options.json_mode:
|
|
1076
|
+
_echo_result(ctx, "agent-help", result)
|
|
1077
|
+
else:
|
|
1078
|
+
click.echo(_agent_help_text())
|
|
1079
|
+
except CliError as error:
|
|
1080
|
+
_fail(ctx, "agent-help", error)
|
|
1081
|
+
except Exception as exc: # pragma: no cover
|
|
1082
|
+
_emit_internal(ctx, "agent-help", exc)
|
|
1083
|
+
|
|
1084
|
+
|
|
1085
|
+
@cli.group(
|
|
1086
|
+
"trace",
|
|
1087
|
+
no_args_is_help=True,
|
|
1088
|
+
help="Inspect compact local execution traces without calling Kaiten.",
|
|
1089
|
+
short_help="Inspect local execution traces.",
|
|
1090
|
+
context_settings=CLICK_CONTEXT_SETTINGS,
|
|
1091
|
+
)
|
|
1092
|
+
def trace_group() -> None:
|
|
1093
|
+
"""Inspect local traces."""
|
|
1094
|
+
|
|
1095
|
+
|
|
1096
|
+
@trace_group.command(
|
|
1097
|
+
"summarize",
|
|
1098
|
+
help="Stream a JSONL trace and report aggregate cost and workflow recommendations.",
|
|
1099
|
+
short_help="Summarize a local JSONL trace.",
|
|
1100
|
+
)
|
|
1101
|
+
@click.option(
|
|
1102
|
+
"--file",
|
|
1103
|
+
"trace_path",
|
|
1104
|
+
required=True,
|
|
1105
|
+
type=click.Path(exists=True, dir_okay=False, path_type=str),
|
|
1106
|
+
help="Trace JSONL file created with --trace-file.",
|
|
1107
|
+
)
|
|
1108
|
+
@click.pass_context
|
|
1109
|
+
def trace_summarize_command(ctx: click.Context, trace_path: str) -> None:
|
|
1110
|
+
try:
|
|
1111
|
+
_echo_result(ctx, "trace.summarize", summarize_trace(trace_path))
|
|
1112
|
+
except CliError as error:
|
|
1113
|
+
_fail(ctx, "trace.summarize", error)
|
|
1114
|
+
except Exception as exc: # pragma: no cover - safety net
|
|
1115
|
+
_emit_internal(ctx, "trace.summarize", exc)
|
|
1116
|
+
|
|
1117
|
+
|
|
1118
|
+
@cli.group(
|
|
1119
|
+
"completion",
|
|
1120
|
+
no_args_is_help=True,
|
|
1121
|
+
help="Install, inspect, generate or remove shell completion for Kaiten CLI.",
|
|
1122
|
+
short_help="Manage Bash and Zsh completion.",
|
|
1123
|
+
context_settings=CLICK_CONTEXT_SETTINGS,
|
|
1124
|
+
)
|
|
1125
|
+
def completion_group() -> None:
|
|
1126
|
+
"""Manage shell completion."""
|
|
1127
|
+
|
|
1128
|
+
|
|
1129
|
+
def _completion_config_option(function):
|
|
1130
|
+
return click.option(
|
|
1131
|
+
"--config",
|
|
1132
|
+
"config_path",
|
|
1133
|
+
type=click.Path(dir_okay=False, path_type=Path),
|
|
1134
|
+
default=None,
|
|
1135
|
+
help="Override the shell startup file to update or inspect.",
|
|
1136
|
+
)(function)
|
|
1137
|
+
|
|
1138
|
+
|
|
1139
|
+
def _completion_shell_option(function):
|
|
1140
|
+
return click.option(
|
|
1141
|
+
"--shell",
|
|
1142
|
+
type=click.Choice(SUPPORTED_SHELLS),
|
|
1143
|
+
default=None,
|
|
1144
|
+
help="Target shell. Defaults to the basename of SHELL.",
|
|
1145
|
+
)(function)
|
|
1146
|
+
|
|
1147
|
+
|
|
1148
|
+
@completion_group.command(
|
|
1149
|
+
"install",
|
|
1150
|
+
help="Generate a static completion script and register it in the shell startup file.",
|
|
1151
|
+
short_help="Install shell completion safely.",
|
|
1152
|
+
)
|
|
1153
|
+
@_completion_shell_option
|
|
1154
|
+
@_completion_config_option
|
|
1155
|
+
@click.option("--dry-run", is_flag=True, help="Show intended changes without writing files.")
|
|
1156
|
+
@click.pass_context
|
|
1157
|
+
def completion_install_command(
|
|
1158
|
+
ctx: click.Context,
|
|
1159
|
+
shell: str | None,
|
|
1160
|
+
config_path: Path | None,
|
|
1161
|
+
dry_run: bool,
|
|
1162
|
+
) -> None:
|
|
1163
|
+
try:
|
|
1164
|
+
result = install_completion(
|
|
1165
|
+
ctx.find_root().command,
|
|
1166
|
+
shell=shell,
|
|
1167
|
+
config_path=config_path,
|
|
1168
|
+
dry_run=dry_run,
|
|
1169
|
+
)
|
|
1170
|
+
_echo_completion_result(ctx, "completion.install", "installed", result)
|
|
1171
|
+
except CliError as error:
|
|
1172
|
+
_fail(ctx, "completion.install", error)
|
|
1173
|
+
except Exception as exc: # pragma: no cover - safety net
|
|
1174
|
+
_emit_internal(ctx, "completion.install", exc)
|
|
1175
|
+
|
|
1176
|
+
|
|
1177
|
+
@completion_group.command(
|
|
1178
|
+
"status",
|
|
1179
|
+
help="Inspect whether the current completion script and shell registration are ready.",
|
|
1180
|
+
short_help="Check shell completion status.",
|
|
1181
|
+
)
|
|
1182
|
+
@_completion_shell_option
|
|
1183
|
+
@_completion_config_option
|
|
1184
|
+
@click.pass_context
|
|
1185
|
+
def completion_status_command(
|
|
1186
|
+
ctx: click.Context,
|
|
1187
|
+
shell: str | None,
|
|
1188
|
+
config_path: Path | None,
|
|
1189
|
+
) -> None:
|
|
1190
|
+
try:
|
|
1191
|
+
result = completion_status(
|
|
1192
|
+
ctx.find_root().command,
|
|
1193
|
+
shell=shell,
|
|
1194
|
+
config_path=config_path,
|
|
1195
|
+
)
|
|
1196
|
+
_echo_completion_result(ctx, "completion.status", "status", result)
|
|
1197
|
+
except CliError as error:
|
|
1198
|
+
_fail(ctx, "completion.status", error)
|
|
1199
|
+
except Exception as exc: # pragma: no cover - safety net
|
|
1200
|
+
_emit_internal(ctx, "completion.status", exc)
|
|
1201
|
+
|
|
1202
|
+
|
|
1203
|
+
@completion_group.command(
|
|
1204
|
+
"source",
|
|
1205
|
+
help="Print the Click completion source script without modifying shell files.",
|
|
1206
|
+
short_help="Print a completion script.",
|
|
1207
|
+
)
|
|
1208
|
+
@click.argument("shell", type=click.Choice(SUPPORTED_SHELLS), metavar="SHELL")
|
|
1209
|
+
@click.pass_context
|
|
1210
|
+
def completion_source_command(ctx: click.Context, shell: str) -> None:
|
|
1211
|
+
try:
|
|
1212
|
+
result = generate_completion_source(ctx.find_root().command, shell)
|
|
1213
|
+
if _ctx_options(ctx).json_mode:
|
|
1214
|
+
_echo_result(ctx, "completion.source", {"shell": shell, "source": result})
|
|
1215
|
+
else:
|
|
1216
|
+
_discard_result_stats(ctx)
|
|
1217
|
+
click.echo(result, nl=False)
|
|
1218
|
+
except CliError as error:
|
|
1219
|
+
_fail(ctx, "completion.source", error)
|
|
1220
|
+
except Exception as exc: # pragma: no cover - safety net
|
|
1221
|
+
_emit_internal(ctx, "completion.source", exc)
|
|
1222
|
+
|
|
1223
|
+
|
|
1224
|
+
@completion_group.command(
|
|
1225
|
+
"uninstall",
|
|
1226
|
+
help="Remove only the managed shell registration and generated completion script.",
|
|
1227
|
+
short_help="Uninstall managed shell completion.",
|
|
1228
|
+
)
|
|
1229
|
+
@_completion_shell_option
|
|
1230
|
+
@_completion_config_option
|
|
1231
|
+
@click.option("--dry-run", is_flag=True, help="Show intended changes without writing files.")
|
|
1232
|
+
@click.pass_context
|
|
1233
|
+
def completion_uninstall_command(
|
|
1234
|
+
ctx: click.Context,
|
|
1235
|
+
shell: str | None,
|
|
1236
|
+
config_path: Path | None,
|
|
1237
|
+
dry_run: bool,
|
|
1238
|
+
) -> None:
|
|
1239
|
+
try:
|
|
1240
|
+
result = uninstall_completion(
|
|
1241
|
+
ctx.find_root().command,
|
|
1242
|
+
shell=shell,
|
|
1243
|
+
config_path=config_path,
|
|
1244
|
+
dry_run=dry_run,
|
|
1245
|
+
)
|
|
1246
|
+
_echo_completion_result(ctx, "completion.uninstall", "uninstalled", result)
|
|
1247
|
+
except CliError as error:
|
|
1248
|
+
_fail(ctx, "completion.uninstall", error)
|
|
1249
|
+
except Exception as exc: # pragma: no cover - safety net
|
|
1250
|
+
_emit_internal(ctx, "completion.uninstall", exc)
|
|
1251
|
+
|
|
1252
|
+
|
|
1253
|
+
@cli.group(
|
|
1254
|
+
"profile",
|
|
1255
|
+
no_args_is_help=True,
|
|
1256
|
+
help="Manage named Kaiten credential profiles and per-profile cache defaults.",
|
|
1257
|
+
short_help="Manage Kaiten profiles.",
|
|
1258
|
+
context_settings=CLICK_CONTEXT_SETTINGS,
|
|
1259
|
+
)
|
|
1260
|
+
def profile_group() -> None:
|
|
1261
|
+
"""Manage profiles."""
|
|
1262
|
+
|
|
1263
|
+
|
|
1264
|
+
@profile_group.command(
|
|
1265
|
+
"add",
|
|
1266
|
+
help="Create or update a named profile with Kaiten domain, API token and cache defaults.",
|
|
1267
|
+
short_help="Create or update a profile.",
|
|
1268
|
+
)
|
|
1269
|
+
@click.argument("name", type=click.STRING, metavar="NAME")
|
|
1270
|
+
@click.option(
|
|
1271
|
+
"--domain",
|
|
1272
|
+
required=True,
|
|
1273
|
+
type=click.STRING,
|
|
1274
|
+
help="Kaiten tenant subdomain or full base URL, for example acme or https://acme.kaiten.ru.",
|
|
1275
|
+
)
|
|
1276
|
+
@click.option(
|
|
1277
|
+
"--token", required=True, type=click.STRING, help="Kaiten API token for this profile."
|
|
1278
|
+
)
|
|
1279
|
+
@click.option(
|
|
1280
|
+
"--cache-mode",
|
|
1281
|
+
type=click.Choice(["auto", "off", "readwrite", "refresh"]),
|
|
1282
|
+
default=None,
|
|
1283
|
+
help="Default persistent cache mode to store with this profile.",
|
|
1284
|
+
)
|
|
1285
|
+
@click.option(
|
|
1286
|
+
"--cache-ttl-seconds",
|
|
1287
|
+
type=click.INT,
|
|
1288
|
+
default=None,
|
|
1289
|
+
help="Default persistent cache TTL in seconds for this profile.",
|
|
1290
|
+
)
|
|
1291
|
+
@click.option(
|
|
1292
|
+
"--set-active/--no-set-active",
|
|
1293
|
+
default=False,
|
|
1294
|
+
help="Make this profile the active default immediately after saving it.",
|
|
1295
|
+
)
|
|
1296
|
+
@click.pass_context
|
|
1297
|
+
def profile_add_command(
|
|
1298
|
+
ctx: click.Context,
|
|
1299
|
+
name: str,
|
|
1300
|
+
domain: str,
|
|
1301
|
+
token: str,
|
|
1302
|
+
cache_mode: str | None,
|
|
1303
|
+
cache_ttl_seconds: int | None,
|
|
1304
|
+
set_active: bool,
|
|
1305
|
+
) -> None:
|
|
1306
|
+
try:
|
|
1307
|
+
result = _run_traced(
|
|
1308
|
+
ctx,
|
|
1309
|
+
"profile.add",
|
|
1310
|
+
"meta",
|
|
1311
|
+
lambda: (
|
|
1312
|
+
add_profile(
|
|
1313
|
+
name,
|
|
1314
|
+
domain=domain,
|
|
1315
|
+
token=token,
|
|
1316
|
+
cache_mode=cache_mode,
|
|
1317
|
+
cache_ttl_seconds=cache_ttl_seconds,
|
|
1318
|
+
set_active=set_active,
|
|
1319
|
+
),
|
|
1320
|
+
None,
|
|
1321
|
+
),
|
|
1322
|
+
)
|
|
1323
|
+
_echo_result(ctx, "profile.add", result)
|
|
1324
|
+
except CliError as error:
|
|
1325
|
+
_fail(ctx, "profile.add", error)
|
|
1326
|
+
except Exception as exc: # pragma: no cover
|
|
1327
|
+
_emit_internal(ctx, "profile.add", exc)
|
|
1328
|
+
|
|
1329
|
+
|
|
1330
|
+
@profile_group.command(
|
|
1331
|
+
"use",
|
|
1332
|
+
help="Set an existing profile as the active default for future commands.",
|
|
1333
|
+
short_help="Set the active profile.",
|
|
1334
|
+
)
|
|
1335
|
+
@click.argument("name", type=click.STRING, metavar="NAME")
|
|
1336
|
+
@click.pass_context
|
|
1337
|
+
def profile_use_command(ctx: click.Context, name: str) -> None:
|
|
1338
|
+
try:
|
|
1339
|
+
_echo_result(
|
|
1340
|
+
ctx,
|
|
1341
|
+
"profile.use",
|
|
1342
|
+
_run_traced(ctx, "profile.use", "meta", lambda: (use_profile(name), None)),
|
|
1343
|
+
)
|
|
1344
|
+
except CliError as error:
|
|
1345
|
+
_fail(ctx, "profile.use", error)
|
|
1346
|
+
except Exception as exc: # pragma: no cover
|
|
1347
|
+
_emit_internal(ctx, "profile.use", exc)
|
|
1348
|
+
|
|
1349
|
+
|
|
1350
|
+
@profile_group.command(
|
|
1351
|
+
"list",
|
|
1352
|
+
help="List configured profiles and show which one is active.",
|
|
1353
|
+
short_help="List configured profiles.",
|
|
1354
|
+
)
|
|
1355
|
+
@click.pass_context
|
|
1356
|
+
def profile_list_command(ctx: click.Context) -> None:
|
|
1357
|
+
try:
|
|
1358
|
+
_echo_result(
|
|
1359
|
+
ctx,
|
|
1360
|
+
"profile.list",
|
|
1361
|
+
_run_traced(ctx, "profile.list", "meta", lambda: (list_profiles(), None)),
|
|
1362
|
+
)
|
|
1363
|
+
except CliError as error:
|
|
1364
|
+
_fail(ctx, "profile.list", error)
|
|
1365
|
+
except Exception as exc: # pragma: no cover
|
|
1366
|
+
_emit_internal(ctx, "profile.list", exc)
|
|
1367
|
+
|
|
1368
|
+
|
|
1369
|
+
@profile_group.command(
|
|
1370
|
+
"show",
|
|
1371
|
+
help="Show one profile, or the active profile when NAME is omitted.",
|
|
1372
|
+
short_help="Show profile details.",
|
|
1373
|
+
)
|
|
1374
|
+
@click.argument("name", required=False, type=click.STRING, metavar="NAME")
|
|
1375
|
+
@click.pass_context
|
|
1376
|
+
def profile_show_command(ctx: click.Context, name: str | None) -> None:
|
|
1377
|
+
try:
|
|
1378
|
+
_echo_result(
|
|
1379
|
+
ctx,
|
|
1380
|
+
"profile.show",
|
|
1381
|
+
_run_traced(ctx, "profile.show", "meta", lambda: (show_profile(name), None)),
|
|
1382
|
+
)
|
|
1383
|
+
except CliError as error:
|
|
1384
|
+
_fail(ctx, "profile.show", error)
|
|
1385
|
+
except Exception as exc: # pragma: no cover
|
|
1386
|
+
_emit_internal(ctx, "profile.show", exc)
|
|
1387
|
+
|
|
1388
|
+
|
|
1389
|
+
@profile_group.command(
|
|
1390
|
+
"probe",
|
|
1391
|
+
help=(
|
|
1392
|
+
"Verify the selected profile with a fresh read-only GET /users/current. "
|
|
1393
|
+
"This checks authentication, not arbitrary write permissions."
|
|
1394
|
+
),
|
|
1395
|
+
short_help="Probe profile authentication safely.",
|
|
1396
|
+
)
|
|
1397
|
+
@click.pass_context
|
|
1398
|
+
def profile_probe_command(ctx: click.Context) -> None:
|
|
1399
|
+
options = _ctx_options(ctx)
|
|
1400
|
+
try:
|
|
1401
|
+
|
|
1402
|
+
def probe():
|
|
1403
|
+
resolved = resolve_profile(
|
|
1404
|
+
options.profile_name,
|
|
1405
|
+
cache_mode_override=options.cache_mode,
|
|
1406
|
+
cache_ttl_seconds_override=options.cache_ttl_seconds,
|
|
1407
|
+
)
|
|
1408
|
+
tool = resolve_tool("users.current")
|
|
1409
|
+
_, stats = execute_tool_sync_with_diagnostics(
|
|
1410
|
+
tool,
|
|
1411
|
+
{},
|
|
1412
|
+
profile_name=options.profile_name,
|
|
1413
|
+
cache_mode="off",
|
|
1414
|
+
reporter=_make_debug_reporter(ctx),
|
|
1415
|
+
read_only=True,
|
|
1416
|
+
)
|
|
1417
|
+
return (
|
|
1418
|
+
{
|
|
1419
|
+
"profile": {
|
|
1420
|
+
"name": resolved.name,
|
|
1421
|
+
"source": resolved.source,
|
|
1422
|
+
"domain_configured": bool(resolved.domain),
|
|
1423
|
+
"cache_mode": resolved.cache_mode,
|
|
1424
|
+
"cache_ttl_seconds": resolved.cache_ttl_seconds,
|
|
1425
|
+
},
|
|
1426
|
+
"authentication": {
|
|
1427
|
+
"ok": True,
|
|
1428
|
+
"probe_cache_mode": "off",
|
|
1429
|
+
},
|
|
1430
|
+
"capability_scope": "authentication_only",
|
|
1431
|
+
"write_permissions": "not_checked",
|
|
1432
|
+
},
|
|
1433
|
+
stats,
|
|
1434
|
+
)
|
|
1435
|
+
|
|
1436
|
+
result = _run_traced(ctx, "profile.probe", "direct_http", probe)
|
|
1437
|
+
_echo_result(ctx, "profile.probe", result)
|
|
1438
|
+
except CliError as error:
|
|
1439
|
+
_fail(ctx, "profile.probe", error)
|
|
1440
|
+
except Exception as exc: # pragma: no cover
|
|
1441
|
+
_emit_internal(ctx, "profile.probe", exc)
|
|
1442
|
+
|
|
1443
|
+
|
|
1444
|
+
@profile_group.command(
|
|
1445
|
+
"remove",
|
|
1446
|
+
help="Remove a saved profile by name.",
|
|
1447
|
+
short_help="Remove a profile.",
|
|
1448
|
+
)
|
|
1449
|
+
@click.argument("name", type=click.STRING, metavar="NAME")
|
|
1450
|
+
@click.pass_context
|
|
1451
|
+
def profile_remove_command(ctx: click.Context, name: str) -> None:
|
|
1452
|
+
try:
|
|
1453
|
+
_echo_result(
|
|
1454
|
+
ctx,
|
|
1455
|
+
"profile.remove",
|
|
1456
|
+
_run_traced(ctx, "profile.remove", "meta", lambda: (remove_profile(name), None)),
|
|
1457
|
+
)
|
|
1458
|
+
except CliError as error:
|
|
1459
|
+
_fail(ctx, "profile.remove", error)
|
|
1460
|
+
except Exception as exc: # pragma: no cover
|
|
1461
|
+
_emit_internal(ctx, "profile.remove", exc)
|
|
1462
|
+
|
|
1463
|
+
|
|
1464
|
+
for tool in iter_tools():
|
|
1465
|
+
group = _ensure_group(cli, tool.namespace_segments)
|
|
1466
|
+
group.add_command(_make_command(tool))
|
|
1467
|
+
cli.add_command(_make_command(tool, hidden=True), name=tool.canonical_name)
|
|
1468
|
+
cli.add_command(_make_command(tool, hidden=True), name=tool.mcp_alias)
|
|
1469
|
+
|
|
1470
|
+
|
|
1471
|
+
_ROOT_OPTION_USAGE = {
|
|
1472
|
+
"--locale": "--locale <ru|en>",
|
|
1473
|
+
"--json": "--json",
|
|
1474
|
+
"--profile": "--profile <name>",
|
|
1475
|
+
"--from-file": "--from-file <path>",
|
|
1476
|
+
"--stdin-json": "--stdin-json",
|
|
1477
|
+
"--verbose": "--verbose",
|
|
1478
|
+
"--cache-mode": "--cache-mode <auto|off|readwrite|refresh>",
|
|
1479
|
+
"--cache-ttl-seconds": "--cache-ttl-seconds <seconds>",
|
|
1480
|
+
"--trace-file": "--trace-file <path>",
|
|
1481
|
+
"--read-only": "--read-only",
|
|
1482
|
+
"--no-update-check": "--no-update-check",
|
|
1483
|
+
"--no-color": "--no-color",
|
|
1484
|
+
}
|
|
1485
|
+
_SHAPING_OPTIONS = {"--compact", "--fields"}
|
|
1486
|
+
|
|
1487
|
+
|
|
1488
|
+
def _usage_error_command(error: click.UsageError) -> tuple[str | None, ToolSpec | None]:
|
|
1489
|
+
context = error.ctx
|
|
1490
|
+
if context is None:
|
|
1491
|
+
return None, None
|
|
1492
|
+
path = context.command_path.split()
|
|
1493
|
+
if path and path[0] == "kaiten":
|
|
1494
|
+
path = path[1:]
|
|
1495
|
+
if not path:
|
|
1496
|
+
return None, None
|
|
1497
|
+
canonical_name = path[0] if len(path) == 1 and "." in path[0] else ".".join(path)
|
|
1498
|
+
try:
|
|
1499
|
+
return canonical_name, next(
|
|
1500
|
+
tool for tool in iter_tools() if tool.canonical_name == canonical_name
|
|
1501
|
+
)
|
|
1502
|
+
except StopIteration:
|
|
1503
|
+
return canonical_name, None
|
|
1504
|
+
|
|
1505
|
+
|
|
1506
|
+
def _supported_context_options(error: click.UsageError) -> list[str]:
|
|
1507
|
+
context = error.ctx
|
|
1508
|
+
if context is None:
|
|
1509
|
+
return []
|
|
1510
|
+
return sorted(
|
|
1511
|
+
{
|
|
1512
|
+
option
|
|
1513
|
+
for parameter in context.command.params
|
|
1514
|
+
if isinstance(parameter, click.Option)
|
|
1515
|
+
for option in parameter.opts
|
|
1516
|
+
if option.startswith("--")
|
|
1517
|
+
}
|
|
1518
|
+
)
|
|
1519
|
+
|
|
1520
|
+
|
|
1521
|
+
def _suggested_root_options(*, offending_option: str, supported_options: list[str]) -> list[str]:
|
|
1522
|
+
suggested: list[str] = []
|
|
1523
|
+
seen: set[str] = set()
|
|
1524
|
+
for token in _ARGV.get() or []:
|
|
1525
|
+
option = token.split("=", 1)[0]
|
|
1526
|
+
if option not in _ROOT_OPTION_USAGE or option in seen:
|
|
1527
|
+
continue
|
|
1528
|
+
if option in supported_options and option != offending_option:
|
|
1529
|
+
continue
|
|
1530
|
+
seen.add(option)
|
|
1531
|
+
suggested.append(_ROOT_OPTION_USAGE[option])
|
|
1532
|
+
if not suggested:
|
|
1533
|
+
suggested.append(_ROOT_OPTION_USAGE[offending_option])
|
|
1534
|
+
return suggested
|
|
1535
|
+
|
|
1536
|
+
|
|
1537
|
+
def _validation_details(error: click.UsageError) -> dict[str, Any] | None:
|
|
1538
|
+
if not isinstance(error, click.NoSuchOption):
|
|
1539
|
+
return None
|
|
1540
|
+
option = error.option_name
|
|
1541
|
+
canonical_name, tool = _usage_error_command(error)
|
|
1542
|
+
supported_options = _supported_context_options(error)
|
|
1543
|
+
command = (
|
|
1544
|
+
" ".join(tool.command_segments)
|
|
1545
|
+
if tool is not None
|
|
1546
|
+
else (canonical_name or "<command>").replace(".", " ")
|
|
1547
|
+
)
|
|
1548
|
+
if option in _ROOT_OPTION_USAGE:
|
|
1549
|
+
root_options = " ".join(
|
|
1550
|
+
_suggested_root_options(
|
|
1551
|
+
offending_option=option,
|
|
1552
|
+
supported_options=supported_options,
|
|
1553
|
+
)
|
|
1554
|
+
)
|
|
1555
|
+
details: dict[str, Any] = {
|
|
1556
|
+
"code": "global_option_position",
|
|
1557
|
+
"option": option,
|
|
1558
|
+
"canonical_name": canonical_name,
|
|
1559
|
+
"suggested_usage": (f"kaiten {root_options} {command} [command options]"),
|
|
1560
|
+
"supported_options": supported_options,
|
|
1561
|
+
}
|
|
1562
|
+
else:
|
|
1563
|
+
details = {
|
|
1564
|
+
"code": (
|
|
1565
|
+
"unsupported_shaping_option"
|
|
1566
|
+
if option in _SHAPING_OPTIONS
|
|
1567
|
+
else "unsupported_tool_option"
|
|
1568
|
+
),
|
|
1569
|
+
"option": option,
|
|
1570
|
+
"canonical_name": canonical_name,
|
|
1571
|
+
"suggested_usage": f"kaiten {command} [supported options]",
|
|
1572
|
+
"supported_options": supported_options,
|
|
1573
|
+
}
|
|
1574
|
+
if tool is not None:
|
|
1575
|
+
details["next"] = f"kaiten describe {tool.canonical_name}"
|
|
1576
|
+
if tool.bulk_alternative:
|
|
1577
|
+
details["bulk_alternative"] = tool.bulk_alternative
|
|
1578
|
+
elif canonical_name:
|
|
1579
|
+
details["next"] = f"kaiten {command} --help"
|
|
1580
|
+
return details
|
|
1581
|
+
|
|
1582
|
+
|
|
1583
|
+
def _main(argv: list[str] | None = None) -> int:
|
|
1584
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
1585
|
+
json_mode = "--json" in args
|
|
1586
|
+
try:
|
|
1587
|
+
argv_token = _ARGV.set(list(args))
|
|
1588
|
+
click_result = cli.main(args=args, prog_name="kaiten", standalone_mode=False)
|
|
1589
|
+
if isinstance(click_result, int) and click_result != 0:
|
|
1590
|
+
return click_result
|
|
1591
|
+
except NoArgsIsHelpError as error:
|
|
1592
|
+
sys.stdout.write(error.format_message() + "\n")
|
|
1593
|
+
return 0
|
|
1594
|
+
except click.UsageError as error:
|
|
1595
|
+
cli_error = ValidationError(
|
|
1596
|
+
error.format_message(),
|
|
1597
|
+
details=_validation_details(error),
|
|
1598
|
+
)
|
|
1599
|
+
stream = sys.stdout if json_mode else sys.stderr
|
|
1600
|
+
stream.write(render_error(None, cli_error, json_mode) + "\n")
|
|
1601
|
+
return cli_error.exit_code
|
|
1602
|
+
except CliError as error:
|
|
1603
|
+
stream = sys.stdout if json_mode else sys.stderr
|
|
1604
|
+
stream.write(render_error(None, error, json_mode) + "\n")
|
|
1605
|
+
return error.exit_code
|
|
1606
|
+
except click.ClickException as error:
|
|
1607
|
+
cli_error = ConfigError(error.format_message())
|
|
1608
|
+
stream = sys.stdout if json_mode else sys.stderr
|
|
1609
|
+
stream.write(render_error(None, cli_error, json_mode) + "\n")
|
|
1610
|
+
return cli_error.exit_code
|
|
1611
|
+
finally:
|
|
1612
|
+
_ARGV.reset(argv_token)
|
|
1613
|
+
try:
|
|
1614
|
+
return maybe_offer_update(args)
|
|
1615
|
+
except Exception: # pragma: no cover - update checks must never break the primary command
|
|
1616
|
+
return 0
|
|
1617
|
+
|
|
1618
|
+
|
|
1619
|
+
def main(argv: list[str] | None = None) -> int:
|
|
1620
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
1621
|
+
try:
|
|
1622
|
+
locale = resolve_locale(args)
|
|
1623
|
+
except click.UsageError as error:
|
|
1624
|
+
with use_locale("en"):
|
|
1625
|
+
json_mode = "--json" in args
|
|
1626
|
+
stream = sys.stdout if json_mode else sys.stderr
|
|
1627
|
+
stream.write(
|
|
1628
|
+
render_error(None, ValidationError(error.format_message()), json_mode) + "\n"
|
|
1629
|
+
)
|
|
1630
|
+
return 2
|
|
1631
|
+
with use_locale(locale):
|
|
1632
|
+
return _main(args)
|
|
1633
|
+
|
|
1634
|
+
|
|
1635
|
+
if __name__ == "__main__": # pragma: no cover
|
|
1636
|
+
raise SystemExit(main())
|