humancli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentcli/__init__.py +20 -0
- agentcli/_agents.py +93 -0
- agentcli/_app.py +432 -0
- agentcli/_context.py +39 -0
- agentcli/_errors.py +140 -0
- agentcli/_help.py +65 -0
- agentcli/_output.py +282 -0
- agentcli/_parser.py +417 -0
- agentcli/_schema.py +142 -0
- agentcli/_types.py +142 -0
- agentcli/_wizard.py +83 -0
- agentcli/prompt.py +71 -0
- agentcli/py.typed +0 -0
- agentcli/testing.py +55 -0
- humancli-0.1.0.dist-info/METADATA +178 -0
- humancli-0.1.0.dist-info/RECORD +18 -0
- humancli-0.1.0.dist-info/WHEEL +4 -0
- humancli-0.1.0.dist-info/licenses/LICENSE +21 -0
agentcli/_errors.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING, Iterable
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from ._output import ErrorInfo
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class FieldError:
|
|
12
|
+
field: str
|
|
13
|
+
message: str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AgentCliError(Exception):
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
*,
|
|
20
|
+
code: str,
|
|
21
|
+
message: str,
|
|
22
|
+
retryable: bool = False,
|
|
23
|
+
cta: list[str] | None = None,
|
|
24
|
+
exit_code: int = 1,
|
|
25
|
+
) -> None:
|
|
26
|
+
super().__init__(message)
|
|
27
|
+
self.code = code
|
|
28
|
+
self.message = message
|
|
29
|
+
self.retryable = retryable
|
|
30
|
+
self.cta = cta
|
|
31
|
+
self.exit_code = exit_code
|
|
32
|
+
|
|
33
|
+
def to_error_info(self) -> ErrorInfo:
|
|
34
|
+
from ._output import ErrorInfo
|
|
35
|
+
|
|
36
|
+
return ErrorInfo(
|
|
37
|
+
code=self.code,
|
|
38
|
+
message=self.message,
|
|
39
|
+
retryable=self.retryable,
|
|
40
|
+
cta=self.cta,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ParseError(AgentCliError):
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
message: str,
|
|
48
|
+
*,
|
|
49
|
+
code: str = "PARSE",
|
|
50
|
+
retryable: bool = True,
|
|
51
|
+
cta: list[str] | None = None,
|
|
52
|
+
) -> None:
|
|
53
|
+
super().__init__(
|
|
54
|
+
code=code,
|
|
55
|
+
message=message,
|
|
56
|
+
retryable=retryable,
|
|
57
|
+
cta=cta,
|
|
58
|
+
exit_code=1,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class ValidationError(AgentCliError):
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
*,
|
|
66
|
+
message: str,
|
|
67
|
+
field_errors: list[FieldError] | None = None,
|
|
68
|
+
cta: list[str] | None = None,
|
|
69
|
+
) -> None:
|
|
70
|
+
super().__init__(
|
|
71
|
+
code="VALIDATION",
|
|
72
|
+
message=message,
|
|
73
|
+
retryable=True,
|
|
74
|
+
cta=cta,
|
|
75
|
+
exit_code=1,
|
|
76
|
+
)
|
|
77
|
+
self.field_errors = field_errors or []
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class ConfigError(AgentCliError):
|
|
81
|
+
def __init__(self, message: str) -> None:
|
|
82
|
+
super().__init__(
|
|
83
|
+
code="CONFIG",
|
|
84
|
+
message=message,
|
|
85
|
+
retryable=False,
|
|
86
|
+
exit_code=1,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def levenshtein(left: str, right: str) -> int:
|
|
91
|
+
if left == right:
|
|
92
|
+
return 0
|
|
93
|
+
if not left:
|
|
94
|
+
return len(right)
|
|
95
|
+
if not right:
|
|
96
|
+
return len(left)
|
|
97
|
+
previous = list(range(len(right) + 1))
|
|
98
|
+
for i, left_char in enumerate(left, start=1):
|
|
99
|
+
current = [i]
|
|
100
|
+
for j, right_char in enumerate(right, start=1):
|
|
101
|
+
current.append(
|
|
102
|
+
min(
|
|
103
|
+
current[j - 1] + 1,
|
|
104
|
+
previous[j] + 1,
|
|
105
|
+
previous[j - 1] + (left_char != right_char),
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
previous = current
|
|
109
|
+
return previous[-1]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def suggest_matches(
|
|
113
|
+
value: str,
|
|
114
|
+
choices: Iterable[str],
|
|
115
|
+
*,
|
|
116
|
+
threshold: int = 2,
|
|
117
|
+
limit: int = 3,
|
|
118
|
+
) -> list[str]:
|
|
119
|
+
scored: list[tuple[int, str]] = []
|
|
120
|
+
lowered = value.lower()
|
|
121
|
+
for choice in choices:
|
|
122
|
+
distance = levenshtein(lowered, choice.lower())
|
|
123
|
+
if distance <= threshold:
|
|
124
|
+
scored.append((distance, choice))
|
|
125
|
+
scored.sort(key=lambda item: (item[0], item[1]))
|
|
126
|
+
return [choice for _, choice in scored[:limit]]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def unknown_name_error(*, kind: str, value: str, choices: Iterable[str]) -> ParseError:
|
|
130
|
+
suggestions = suggest_matches(value, choices)
|
|
131
|
+
hint = f" Did you mean: {', '.join(suggestions)}?" if suggestions else ""
|
|
132
|
+
return ParseError(
|
|
133
|
+
f'Unknown {kind} "{value}".{hint}', code=f"UNKNOWN_{kind.upper()}"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def normalize_exception(error: Exception) -> AgentCliError:
|
|
138
|
+
if isinstance(error, AgentCliError):
|
|
139
|
+
return error
|
|
140
|
+
return AgentCliError(code="UNKNOWN", message=str(error) or error.__class__.__name__)
|
agentcli/_help.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ._types import CommandSchema, ParameterSpec
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def render_app_help(app, *, path: tuple[str, ...] = ()) -> str:
|
|
7
|
+
title = " ".join(path or (app.name,))
|
|
8
|
+
lines = [title]
|
|
9
|
+
if app.description:
|
|
10
|
+
lines.extend(["", app.description])
|
|
11
|
+
lines.extend(["", f"Usage: {title} <command>"])
|
|
12
|
+
if app.subapps:
|
|
13
|
+
lines.extend(["", "Groups:"])
|
|
14
|
+
for name, subapp in sorted(app.subapps.items()):
|
|
15
|
+
lines.append(f" {name:<16} {(subapp.description or '')}".rstrip())
|
|
16
|
+
if app.commands:
|
|
17
|
+
lines.extend(["", "Commands:"])
|
|
18
|
+
for name, command in sorted(app.commands.items()):
|
|
19
|
+
lines.append(f" {name:<16} {(command.description or '')}".rstrip())
|
|
20
|
+
return "\n".join(lines)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def render_command_help(command: CommandSchema) -> str:
|
|
24
|
+
path = " ".join(command.path)
|
|
25
|
+
lines = [path]
|
|
26
|
+
if command.description:
|
|
27
|
+
lines.extend(["", command.description])
|
|
28
|
+
lines.extend(["", f"Usage: {path}{_usage(command)}"])
|
|
29
|
+
if command.positionals:
|
|
30
|
+
lines.extend(["", "Arguments:"])
|
|
31
|
+
lines.extend(_parameters(command.positionals))
|
|
32
|
+
visible_options = [
|
|
33
|
+
parameter for parameter in command.options if not parameter.hidden
|
|
34
|
+
]
|
|
35
|
+
if visible_options:
|
|
36
|
+
lines.extend(["", "Options:"])
|
|
37
|
+
lines.extend(_parameters(visible_options))
|
|
38
|
+
return "\n".join(lines)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _usage(command: CommandSchema) -> str:
|
|
42
|
+
parts = []
|
|
43
|
+
for parameter in command.positionals:
|
|
44
|
+
parts.append(
|
|
45
|
+
f"<{parameter.name}>" if parameter.required else f"[{parameter.name}]"
|
|
46
|
+
)
|
|
47
|
+
for parameter in command.options:
|
|
48
|
+
if parameter.hidden:
|
|
49
|
+
continue
|
|
50
|
+
parts.append(
|
|
51
|
+
f"[--{parameter.cli_name}]"
|
|
52
|
+
if parameter.is_bool
|
|
53
|
+
else f"[--{parameter.cli_name} VALUE]"
|
|
54
|
+
)
|
|
55
|
+
return (" " + " ".join(parts)) if parts else ""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _parameters(parameters: list[ParameterSpec]) -> list[str]:
|
|
59
|
+
lines = []
|
|
60
|
+
for parameter in parameters:
|
|
61
|
+
label = parameter.display_name
|
|
62
|
+
if parameter.alias:
|
|
63
|
+
label = f"{label}, -{parameter.alias}"
|
|
64
|
+
lines.append(f" {label:<24} {(parameter.description or '')}".rstrip())
|
|
65
|
+
return lines
|
agentcli/_output.py
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
import json
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ._errors import AgentCliError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class ErrorInfo:
|
|
13
|
+
code: str
|
|
14
|
+
message: str
|
|
15
|
+
retryable: bool = False
|
|
16
|
+
cta: list[str] | None = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class Meta:
|
|
21
|
+
command: str
|
|
22
|
+
cta: list[str] | None = None
|
|
23
|
+
duration_ms: float | None = None
|
|
24
|
+
streamed: bool = False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class Envelope:
|
|
29
|
+
ok: bool
|
|
30
|
+
data: Any | None = None
|
|
31
|
+
error: ErrorInfo | None = None
|
|
32
|
+
meta: Meta | None = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def make_success_envelope(
|
|
36
|
+
data: Any,
|
|
37
|
+
*,
|
|
38
|
+
command: str,
|
|
39
|
+
cta: list[str] | None = None,
|
|
40
|
+
) -> Envelope:
|
|
41
|
+
return Envelope(ok=True, data=data, meta=Meta(command=command, cta=cta))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def make_error_envelope(error: Exception, *, command: str) -> Envelope:
|
|
45
|
+
if isinstance(error, AgentCliError):
|
|
46
|
+
info = ErrorInfo(
|
|
47
|
+
code=error.code,
|
|
48
|
+
message=error.message,
|
|
49
|
+
retryable=error.retryable,
|
|
50
|
+
cta=error.cta,
|
|
51
|
+
)
|
|
52
|
+
else:
|
|
53
|
+
info = ErrorInfo(code="UNKNOWN", message=str(error), retryable=False)
|
|
54
|
+
return Envelope(ok=False, error=info, meta=Meta(command=command, cta=info.cta))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def make_envelope(
|
|
58
|
+
*,
|
|
59
|
+
ok: bool,
|
|
60
|
+
command: str,
|
|
61
|
+
format: str,
|
|
62
|
+
data: Any = None,
|
|
63
|
+
error: ErrorInfo | None = None,
|
|
64
|
+
duration_ms: float | None = None,
|
|
65
|
+
cta: list[str] | None = None,
|
|
66
|
+
streamed: bool = False,
|
|
67
|
+
) -> Envelope:
|
|
68
|
+
if ok:
|
|
69
|
+
return Envelope(
|
|
70
|
+
ok=True,
|
|
71
|
+
data=data,
|
|
72
|
+
meta=Meta(
|
|
73
|
+
command=command, cta=cta, duration_ms=duration_ms, streamed=streamed
|
|
74
|
+
),
|
|
75
|
+
)
|
|
76
|
+
return Envelope(
|
|
77
|
+
ok=False,
|
|
78
|
+
error=error,
|
|
79
|
+
meta=Meta(command=command, cta=cta, duration_ms=duration_ms, streamed=streamed),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def normalize_value(value: Any) -> Any:
|
|
84
|
+
if dataclasses.is_dataclass(value):
|
|
85
|
+
return normalize_value(dataclasses.asdict(value))
|
|
86
|
+
if hasattr(value, "to_dict") and callable(value.to_dict):
|
|
87
|
+
return normalize_value(value.to_dict())
|
|
88
|
+
if hasattr(value, "model_dump") and callable(value.model_dump):
|
|
89
|
+
return normalize_value(value.model_dump())
|
|
90
|
+
if isinstance(value, dict):
|
|
91
|
+
return {
|
|
92
|
+
str(key): normalize_value(item)
|
|
93
|
+
for key, item in value.items()
|
|
94
|
+
if item is not None
|
|
95
|
+
}
|
|
96
|
+
if isinstance(value, (list, tuple)):
|
|
97
|
+
return [normalize_value(item) for item in value]
|
|
98
|
+
return value
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def envelope_payload(envelope: Envelope, *, verbose: bool) -> Any:
|
|
102
|
+
if envelope.ok and not verbose:
|
|
103
|
+
return normalize_value(envelope.data)
|
|
104
|
+
return normalize_value(dataclasses.asdict(envelope))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def render_output(
|
|
108
|
+
envelope: Envelope,
|
|
109
|
+
*,
|
|
110
|
+
format_name: str | None,
|
|
111
|
+
is_tty: bool,
|
|
112
|
+
verbose: bool = False,
|
|
113
|
+
) -> str:
|
|
114
|
+
actual_format = format_name or ("pretty" if is_tty else "toon")
|
|
115
|
+
payload = envelope_payload(envelope, verbose=verbose)
|
|
116
|
+
if actual_format == "json":
|
|
117
|
+
return json.dumps(payload, indent=2, default=str)
|
|
118
|
+
if actual_format == "jsonl":
|
|
119
|
+
return json.dumps(payload, default=str)
|
|
120
|
+
if actual_format == "md":
|
|
121
|
+
return render_markdown(payload)
|
|
122
|
+
if actual_format == "yaml":
|
|
123
|
+
return render_yaml(payload)
|
|
124
|
+
if actual_format == "pretty":
|
|
125
|
+
return render_plain(payload)
|
|
126
|
+
return render_toon(payload)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def render_stream_item(
|
|
130
|
+
item: Any,
|
|
131
|
+
*,
|
|
132
|
+
format_name: str | None = None,
|
|
133
|
+
output_format: str | None = None,
|
|
134
|
+
is_tty: bool | None = None,
|
|
135
|
+
) -> str:
|
|
136
|
+
format_name = output_format or format_name
|
|
137
|
+
payload = normalize_value(item)
|
|
138
|
+
if format_name == "jsonl":
|
|
139
|
+
return json.dumps(payload, default=str)
|
|
140
|
+
if format_name == "json":
|
|
141
|
+
return json.dumps(payload, indent=2, default=str)
|
|
142
|
+
if format_name == "md":
|
|
143
|
+
return render_markdown(payload)
|
|
144
|
+
return render_toon(payload)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def render_envelope(envelope: Envelope, *, format: str, verbose: bool = False) -> str:
|
|
148
|
+
return render_output(
|
|
149
|
+
envelope, format_name=format, is_tty=(format == "human"), verbose=verbose
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def render_plain(value: Any) -> str:
|
|
154
|
+
value = normalize_value(value)
|
|
155
|
+
if isinstance(value, dict):
|
|
156
|
+
return "\n".join(_plain_lines(value))
|
|
157
|
+
if isinstance(value, list):
|
|
158
|
+
return "\n".join(f"- {item}" for item in value)
|
|
159
|
+
return str(value)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _plain_lines(value: dict[str, Any], indent: int = 0) -> list[str]:
|
|
163
|
+
lines: list[str] = []
|
|
164
|
+
prefix = " " * indent
|
|
165
|
+
for key, item in value.items():
|
|
166
|
+
if isinstance(item, dict):
|
|
167
|
+
lines.append(f"{prefix}{key}:")
|
|
168
|
+
lines.extend(_plain_lines(item, indent + 2))
|
|
169
|
+
continue
|
|
170
|
+
if isinstance(item, list):
|
|
171
|
+
lines.append(f"{prefix}{key}:")
|
|
172
|
+
lines.extend(f"{prefix} - {entry}" for entry in item)
|
|
173
|
+
continue
|
|
174
|
+
lines.append(f"{prefix}{key}: {item}")
|
|
175
|
+
return lines
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def render_toon(value: Any) -> str:
|
|
179
|
+
value = normalize_value(value)
|
|
180
|
+
if isinstance(value, dict):
|
|
181
|
+
return "\n".join(_toon_dict(value))
|
|
182
|
+
if isinstance(value, list):
|
|
183
|
+
return _toon_list("items", value)
|
|
184
|
+
return _toon_scalar(value)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _toon_dict(value: dict[str, Any], indent: int = 0) -> list[str]:
|
|
188
|
+
lines: list[str] = []
|
|
189
|
+
prefix = " " * indent
|
|
190
|
+
for key, item in value.items():
|
|
191
|
+
if item is None:
|
|
192
|
+
continue
|
|
193
|
+
if isinstance(item, dict):
|
|
194
|
+
if not item:
|
|
195
|
+
lines.append(f"{prefix}{key}: (empty)")
|
|
196
|
+
continue
|
|
197
|
+
lines.append(f"{prefix}{key}:")
|
|
198
|
+
lines.extend(_toon_dict(item, indent + 2))
|
|
199
|
+
continue
|
|
200
|
+
if isinstance(item, list):
|
|
201
|
+
lines.extend(_toon_list_lines(key, item, indent))
|
|
202
|
+
continue
|
|
203
|
+
lines.append(f"{prefix}{key}: {_toon_scalar(item)}")
|
|
204
|
+
return lines
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _toon_list(key: str, value: list[Any]) -> str:
|
|
208
|
+
return "\n".join(_toon_list_lines(key, value, 0))
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _toon_list_lines(key: str, value: list[Any], indent: int) -> list[str]:
|
|
212
|
+
prefix = " " * indent
|
|
213
|
+
if not value:
|
|
214
|
+
return [f"{prefix}{key}: (empty)"]
|
|
215
|
+
if all(not isinstance(item, (dict, list)) for item in value):
|
|
216
|
+
joined = ",".join(_toon_scalar(item) for item in value)
|
|
217
|
+
return [f"{prefix}{key}[{len(value)}]: {joined}"]
|
|
218
|
+
if all(isinstance(item, dict) and item for item in value):
|
|
219
|
+
headers = list(value[0].keys())
|
|
220
|
+
if all(list(item.keys()) == headers for item in value):
|
|
221
|
+
lines = [f"{prefix}{key}[{len(value)}]{{{','.join(headers)}}}:"]
|
|
222
|
+
for item in value:
|
|
223
|
+
row = ",".join(_toon_scalar(item[column]) for column in headers)
|
|
224
|
+
lines.append(f"{prefix} {row}")
|
|
225
|
+
return lines
|
|
226
|
+
lines = [f"{prefix}{key}:"]
|
|
227
|
+
for item in value:
|
|
228
|
+
lines.append(f"{prefix} - {_toon_scalar(item)}")
|
|
229
|
+
return lines
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _toon_scalar(value: Any) -> str:
|
|
233
|
+
if value is True:
|
|
234
|
+
return "true"
|
|
235
|
+
if value is False:
|
|
236
|
+
return "false"
|
|
237
|
+
if value is None:
|
|
238
|
+
return "(empty)"
|
|
239
|
+
if isinstance(value, (int, float)):
|
|
240
|
+
return str(value)
|
|
241
|
+
text = str(value)
|
|
242
|
+
if not text:
|
|
243
|
+
return '""'
|
|
244
|
+
if any(char in text for char in [":", ",", "\n", '"']):
|
|
245
|
+
return json.dumps(text)
|
|
246
|
+
return text
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def render_markdown(value: Any) -> str:
|
|
250
|
+
value = normalize_value(value)
|
|
251
|
+
if (
|
|
252
|
+
isinstance(value, list)
|
|
253
|
+
and value
|
|
254
|
+
and all(isinstance(item, dict) for item in value)
|
|
255
|
+
):
|
|
256
|
+
headers = list(value[0].keys())
|
|
257
|
+
lines = [
|
|
258
|
+
f"| {' | '.join(headers)} |",
|
|
259
|
+
f"| {' | '.join('---' for _ in headers)} |",
|
|
260
|
+
]
|
|
261
|
+
for item in value:
|
|
262
|
+
lines.append(
|
|
263
|
+
f"| {' | '.join(str(item.get(header, '')) for header in headers)} |"
|
|
264
|
+
)
|
|
265
|
+
return "\n".join(lines)
|
|
266
|
+
if isinstance(value, dict):
|
|
267
|
+
return "\n".join(f"- **{key}**: {item}" for key, item in value.items())
|
|
268
|
+
if isinstance(value, list):
|
|
269
|
+
return "\n".join(f"- {item}" for item in value)
|
|
270
|
+
return str(value)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def render_yaml(value: Any) -> str:
|
|
274
|
+
try:
|
|
275
|
+
import yaml
|
|
276
|
+
except ImportError as exc:
|
|
277
|
+
raise AgentCliError(
|
|
278
|
+
code="MISSING_DEP",
|
|
279
|
+
message="YAML output requires agentcli[yaml]",
|
|
280
|
+
retryable=False,
|
|
281
|
+
) from exc
|
|
282
|
+
return yaml.safe_dump(normalize_value(value), sort_keys=False)
|