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.
Files changed (99) hide show
  1. kaiten_cli/__init__.py +5 -0
  2. kaiten_cli/__main__.py +9 -0
  3. kaiten_cli/agent_gateway.py +577 -0
  4. kaiten_cli/app.py +1636 -0
  5. kaiten_cli/completion.py +570 -0
  6. kaiten_cli/discovery.py +39 -0
  7. kaiten_cli/errors.py +79 -0
  8. kaiten_cli/i18n.py +92 -0
  9. kaiten_cli/locales/ru.json +2967 -0
  10. kaiten_cli/localized_click.py +160 -0
  11. kaiten_cli/models.py +202 -0
  12. kaiten_cli/profiles.py +316 -0
  13. kaiten_cli/registry/__init__.py +260 -0
  14. kaiten_cli/registry/addons.py +392 -0
  15. kaiten_cli/registry/audit_and_analytics.py +451 -0
  16. kaiten_cli/registry/automation_schema.py +44 -0
  17. kaiten_cli/registry/automation_schemas.json +7417 -0
  18. kaiten_cli/registry/automations.py +342 -0
  19. kaiten_cli/registry/base.py +62 -0
  20. kaiten_cli/registry/blockers.py +320 -0
  21. kaiten_cli/registry/boards.py +520 -0
  22. kaiten_cli/registry/card_relations.py +376 -0
  23. kaiten_cli/registry/card_types.py +363 -0
  24. kaiten_cli/registry/cards.py +1329 -0
  25. kaiten_cli/registry/charts.py +539 -0
  26. kaiten_cli/registry/checklists.py +600 -0
  27. kaiten_cli/registry/columns.py +558 -0
  28. kaiten_cli/registry/comments.py +224 -0
  29. kaiten_cli/registry/custom_directories.py +980 -0
  30. kaiten_cli/registry/custom_properties.py +1460 -0
  31. kaiten_cli/registry/dashboards.py +564 -0
  32. kaiten_cli/registry/documents.py +761 -0
  33. kaiten_cli/registry/external_links.py +128 -0
  34. kaiten_cli/registry/files.py +738 -0
  35. kaiten_cli/registry/github_addon.py +613 -0
  36. kaiten_cli/registry/iterations.py +397 -0
  37. kaiten_cli/registry/lanes.py +205 -0
  38. kaiten_cli/registry/live_contracts.py +361 -0
  39. kaiten_cli/registry/members.py +336 -0
  40. kaiten_cli/registry/module_docs.py +108 -0
  41. kaiten_cli/registry/projects.py +369 -0
  42. kaiten_cli/registry/query.py +129 -0
  43. kaiten_cli/registry/restrictions.py +249 -0
  44. kaiten_cli/registry/roles_and_groups.py +1102 -0
  45. kaiten_cli/registry/scim.py +263 -0
  46. kaiten_cli/registry/service_desk.py +1270 -0
  47. kaiten_cli/registry/snapshot.py +187 -0
  48. kaiten_cli/registry/spaces.py +272 -0
  49. kaiten_cli/registry/subscribers.py +168 -0
  50. kaiten_cli/registry/tags.py +193 -0
  51. kaiten_cli/registry/time_logs.py +353 -0
  52. kaiten_cli/registry/tree.py +352 -0
  53. kaiten_cli/registry/utilities.py +287 -0
  54. kaiten_cli/registry/webhooks.py +265 -0
  55. kaiten_cli/runtime/__init__.py +1 -0
  56. kaiten_cli/runtime/automation_validation.py +115 -0
  57. kaiten_cli/runtime/behaviors.py +787 -0
  58. kaiten_cli/runtime/cache.py +733 -0
  59. kaiten_cli/runtime/client.py +387 -0
  60. kaiten_cli/runtime/endpoints.py +59 -0
  61. kaiten_cli/runtime/executor.py +269 -0
  62. kaiten_cli/runtime/fs_security.py +168 -0
  63. kaiten_cli/runtime/input.py +310 -0
  64. kaiten_cli/runtime/output.py +59 -0
  65. kaiten_cli/runtime/public_validation.py +35 -0
  66. kaiten_cli/runtime/snapshot_build.py +527 -0
  67. kaiten_cli/runtime/snapshot_common.py +363 -0
  68. kaiten_cli/runtime/snapshot_query.py +306 -0
  69. kaiten_cli/runtime/snapshot_store.py +1192 -0
  70. kaiten_cli/runtime/snapshots.py +62 -0
  71. kaiten_cli/runtime/sqlite_errors.py +23 -0
  72. kaiten_cli/runtime/support/__init__.py +1 -0
  73. kaiten_cli/runtime/support/addons.py +953 -0
  74. kaiten_cli/runtime/support/audit.py +70 -0
  75. kaiten_cli/runtime/support/batch.py +234 -0
  76. kaiten_cli/runtime/support/card_move_url.py +213 -0
  77. kaiten_cli/runtime/support/cards.py +174 -0
  78. kaiten_cli/runtime/support/checklists.py +21 -0
  79. kaiten_cli/runtime/support/company_users.py +117 -0
  80. kaiten_cli/runtime/support/dashboards.py +35 -0
  81. kaiten_cli/runtime/support/documents.py +175 -0
  82. kaiten_cli/runtime/support/files.py +561 -0
  83. kaiten_cli/runtime/support/markdown_export.py +525 -0
  84. kaiten_cli/runtime/support/pagination.py +167 -0
  85. kaiten_cli/runtime/support/projects.py +66 -0
  86. kaiten_cli/runtime/support/relations.py +86 -0
  87. kaiten_cli/runtime/support/restrictions.py +62 -0
  88. kaiten_cli/runtime/support/spaces.py +20 -0
  89. kaiten_cli/runtime/support/time_logs.py +61 -0
  90. kaiten_cli/runtime/support/tree.py +140 -0
  91. kaiten_cli/runtime/support/tree_sharing.py +447 -0
  92. kaiten_cli/runtime/trace.py +500 -0
  93. kaiten_cli/runtime/transforms.py +128 -0
  94. kaiten_cli/schema_docs.py +22 -0
  95. kaiten_cli/update_check.py +519 -0
  96. kaiten_cli-0.3.3.dist-info/METADATA +1110 -0
  97. kaiten_cli-0.3.3.dist-info/RECORD +99 -0
  98. kaiten_cli-0.3.3.dist-info/WHEEL +4 -0
  99. 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())