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/__init__.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""agentcli: Python CLIs for agents and humans."""
|
|
2
|
+
|
|
3
|
+
from ._app import App, run
|
|
4
|
+
from ._context import Context
|
|
5
|
+
from ._errors import AgentCliError, ConfigError, ParseError, ValidationError
|
|
6
|
+
from ._types import Param, Result
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"AgentCliError",
|
|
12
|
+
"App",
|
|
13
|
+
"ConfigError",
|
|
14
|
+
"Context",
|
|
15
|
+
"Param",
|
|
16
|
+
"ParseError",
|
|
17
|
+
"Result",
|
|
18
|
+
"ValidationError",
|
|
19
|
+
"run",
|
|
20
|
+
]
|
agentcli/_agents.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from ._errors import AgentCliError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def iter_commands(app, prefix: tuple[str, ...] | None = None):
|
|
10
|
+
current = prefix or (app.name,)
|
|
11
|
+
if app._default_command is not None:
|
|
12
|
+
command = app._default_command
|
|
13
|
+
command.full_path = current
|
|
14
|
+
yield current, command
|
|
15
|
+
for name, command in sorted(app.commands.items()):
|
|
16
|
+
path = (*current, name)
|
|
17
|
+
command.full_path = path
|
|
18
|
+
yield path, command
|
|
19
|
+
for name, subapp in sorted(app.subapps.items()):
|
|
20
|
+
yield from iter_commands(subapp, (*current, name))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def render_llms_index(app) -> str:
|
|
24
|
+
lines = [f"# {app.name}" + (f" v{app.version}" if app.version else "")]
|
|
25
|
+
if app.description:
|
|
26
|
+
lines.extend(["", app.description])
|
|
27
|
+
lines.extend(["", "| Command | Description |", "|---------|-------------|"])
|
|
28
|
+
for path, command in iter_commands(app):
|
|
29
|
+
lines.append(f"| `{' '.join(path)}` | {command.description or ''} |")
|
|
30
|
+
lines.extend(["", f"Run `{app.name} <command> --help` for details."])
|
|
31
|
+
return "\n".join(lines)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def render_llms_full(app) -> str:
|
|
35
|
+
commands = []
|
|
36
|
+
for path, command in iter_commands(app):
|
|
37
|
+
commands.append(
|
|
38
|
+
{
|
|
39
|
+
"command": " ".join(path),
|
|
40
|
+
"description": command.description,
|
|
41
|
+
"arguments": [parameter.name for parameter in command.positionals],
|
|
42
|
+
"options": [
|
|
43
|
+
parameter.cli_name
|
|
44
|
+
for parameter in command.options
|
|
45
|
+
if not parameter.hidden
|
|
46
|
+
],
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
return json.dumps(
|
|
50
|
+
{
|
|
51
|
+
"name": app.name,
|
|
52
|
+
"version": app.version,
|
|
53
|
+
"description": app.description,
|
|
54
|
+
"commands": commands,
|
|
55
|
+
},
|
|
56
|
+
indent=2,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def start_mcp(app) -> None:
|
|
61
|
+
try:
|
|
62
|
+
from mcp.server.fastmcp import FastMCP
|
|
63
|
+
except ImportError as error:
|
|
64
|
+
raise AgentCliError(
|
|
65
|
+
"MISSING_DEPENDENCY",
|
|
66
|
+
"MCP mode requires agentcli[mcp]",
|
|
67
|
+
cta=["pip install agentcli[mcp]"],
|
|
68
|
+
) from error
|
|
69
|
+
|
|
70
|
+
server = FastMCP(app.name)
|
|
71
|
+
for path, command in iter_commands(app):
|
|
72
|
+
tool_name = "_".join(path[1:] if path and path[0] == app.name else path)
|
|
73
|
+
|
|
74
|
+
@server.tool(name=tool_name, description=command.description or tool_name)
|
|
75
|
+
async def _tool(**kwargs: Any) -> str:
|
|
76
|
+
argv = list(path[1:] if path and path[0] == app.name else path)
|
|
77
|
+
for key, value in kwargs.items():
|
|
78
|
+
flag = f"--{key.replace('_', '-')}"
|
|
79
|
+
if value is True:
|
|
80
|
+
argv.append(flag)
|
|
81
|
+
elif value not in (False, None):
|
|
82
|
+
argv.extend([flag, str(value)])
|
|
83
|
+
result = app.run(argv, tty=False)
|
|
84
|
+
return json.dumps(
|
|
85
|
+
{
|
|
86
|
+
"ok": result.envelope.ok,
|
|
87
|
+
"data": result.envelope.data,
|
|
88
|
+
"error": result.envelope.error,
|
|
89
|
+
},
|
|
90
|
+
default=str,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
server.run()
|
agentcli/_app.py
ADDED
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import inspect
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
from collections.abc import Callable, Iterable
|
|
9
|
+
from contextlib import AbstractContextManager
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from ._agents import iter_commands, render_llms_full, render_llms_index, start_mcp
|
|
13
|
+
from ._context import Context, detect_agent_mode
|
|
14
|
+
from ._errors import normalize_exception, unknown_name_error
|
|
15
|
+
from ._help import render_app_help, render_command_help
|
|
16
|
+
from ._output import make_envelope, render_envelope, render_stream_item
|
|
17
|
+
from ._parser import normalize_format, parse_values, strip_builtin_flags
|
|
18
|
+
from ._schema import build_command_schema, command_name_for
|
|
19
|
+
from ._types import CommandSchema, ExecutionResult, MISSING, OutputFormat, Result
|
|
20
|
+
from ._wizard import prompt_for_parameter, wizard_schema
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
Middleware = Callable[[Context, Callable[[], Any]], Any]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class App:
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
name: str,
|
|
30
|
+
*,
|
|
31
|
+
version: str | None = None,
|
|
32
|
+
description: str | None = None,
|
|
33
|
+
help_header: str | None = None,
|
|
34
|
+
help_footer: str | None = None,
|
|
35
|
+
autocorrect_threshold: int = 2,
|
|
36
|
+
) -> None:
|
|
37
|
+
self.name = name
|
|
38
|
+
self.version = version
|
|
39
|
+
self.description = description
|
|
40
|
+
self.help_header = help_header
|
|
41
|
+
self.help_footer = help_footer
|
|
42
|
+
self.autocorrect_threshold = autocorrect_threshold
|
|
43
|
+
self.commands: dict[str, CommandSchema] = {}
|
|
44
|
+
self.subapps: dict[str, App] = {}
|
|
45
|
+
self.middleware: list[Middleware] = []
|
|
46
|
+
self._default_command: CommandSchema | None = None
|
|
47
|
+
|
|
48
|
+
def command(
|
|
49
|
+
self,
|
|
50
|
+
func: Callable[..., Any] | None = None,
|
|
51
|
+
*,
|
|
52
|
+
name: str | None = None,
|
|
53
|
+
output: type[Any] | None = None,
|
|
54
|
+
):
|
|
55
|
+
def decorator(callback: Callable[..., Any]) -> Callable[..., Any]:
|
|
56
|
+
command_name = name or command_name_for(callback.__name__)
|
|
57
|
+
self.commands[command_name] = build_command_schema(
|
|
58
|
+
callback,
|
|
59
|
+
name=command_name,
|
|
60
|
+
full_path=(self.name, command_name),
|
|
61
|
+
output_type=output,
|
|
62
|
+
)
|
|
63
|
+
return callback
|
|
64
|
+
|
|
65
|
+
return decorator if func is None else decorator(func)
|
|
66
|
+
|
|
67
|
+
def use(self, middleware: Middleware) -> Middleware:
|
|
68
|
+
self.middleware.append(middleware)
|
|
69
|
+
return middleware
|
|
70
|
+
|
|
71
|
+
def mount(self, sub_app: App, name: str | None = None) -> None:
|
|
72
|
+
self.subapps[name or sub_app.name] = sub_app
|
|
73
|
+
|
|
74
|
+
def default(
|
|
75
|
+
self, func: Callable[..., Any] | None = None, *, output: type[Any] | None = None
|
|
76
|
+
):
|
|
77
|
+
"""Register the default command — runs when no sub-command is given.
|
|
78
|
+
|
|
79
|
+
Can be used as a bare decorator (``@app.default``) or with options
|
|
80
|
+
(``@app.default(output=MyType)``).
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def decorator(callback: Callable[..., Any]) -> Callable[..., Any]:
|
|
84
|
+
self._default_command = build_command_schema(
|
|
85
|
+
callback, name=self.name, full_path=(self.name,), output_type=output
|
|
86
|
+
)
|
|
87
|
+
return callback
|
|
88
|
+
|
|
89
|
+
return decorator if func is None else decorator(func)
|
|
90
|
+
|
|
91
|
+
def _set_default(
|
|
92
|
+
self, callback: Callable[..., Any], *, output: type[Any] | None = None
|
|
93
|
+
) -> None:
|
|
94
|
+
self.default(callback, output=output)
|
|
95
|
+
|
|
96
|
+
def invoke(
|
|
97
|
+
self,
|
|
98
|
+
*,
|
|
99
|
+
argv: Iterable[str] | None = None,
|
|
100
|
+
env: dict[str, str] | None = None,
|
|
101
|
+
is_tty: bool | None = None,
|
|
102
|
+
stdin: Any = None,
|
|
103
|
+
stdout: Any = None,
|
|
104
|
+
stderr: Any = None,
|
|
105
|
+
) -> ExecutionResult:
|
|
106
|
+
return self.run(
|
|
107
|
+
argv, env=env, tty=is_tty, stdin=stdin, stdout=stdout, stderr=stderr
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def run(
|
|
111
|
+
self,
|
|
112
|
+
argv: Iterable[str] | None = None,
|
|
113
|
+
*,
|
|
114
|
+
env: dict[str, str] | None = None,
|
|
115
|
+
tty: bool | None = None,
|
|
116
|
+
stdin: Any = None,
|
|
117
|
+
stdout: Any = None,
|
|
118
|
+
stderr: Any = None,
|
|
119
|
+
) -> ExecutionResult:
|
|
120
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
121
|
+
env_map = {**os.environ, **(env or {})}
|
|
122
|
+
stdin = stdin or sys.stdin
|
|
123
|
+
stdout = stdout or sys.stdout
|
|
124
|
+
stderr = stderr or sys.stderr
|
|
125
|
+
is_tty = bool(tty) if tty is not None else _isatty(stdout)
|
|
126
|
+
builtins, remaining = strip_builtin_flags(args)
|
|
127
|
+
output_format = normalize_format(builtins.format, tty=is_tty)
|
|
128
|
+
started = time.perf_counter()
|
|
129
|
+
try:
|
|
130
|
+
app, command, remainder, middlewares = self._resolve(remaining)
|
|
131
|
+
if builtins.version:
|
|
132
|
+
text = self.version or ""
|
|
133
|
+
_write(stdout, text)
|
|
134
|
+
return _finish(
|
|
135
|
+
make_envelope(
|
|
136
|
+
ok=True, command=self.name, format=output_format, data=text
|
|
137
|
+
),
|
|
138
|
+
stdout,
|
|
139
|
+
output_format,
|
|
140
|
+
)
|
|
141
|
+
if builtins.llms:
|
|
142
|
+
text = render_llms_index(self)
|
|
143
|
+
_write(stdout, text)
|
|
144
|
+
return _finish(
|
|
145
|
+
make_envelope(
|
|
146
|
+
ok=True, command=self.name, format=output_format, data=text
|
|
147
|
+
),
|
|
148
|
+
stdout,
|
|
149
|
+
output_format,
|
|
150
|
+
)
|
|
151
|
+
if builtins.llms_full:
|
|
152
|
+
text = render_llms_full(self)
|
|
153
|
+
_write(stdout, text)
|
|
154
|
+
return _finish(
|
|
155
|
+
make_envelope(
|
|
156
|
+
ok=True, command=self.name, format=output_format, data=text
|
|
157
|
+
),
|
|
158
|
+
stdout,
|
|
159
|
+
output_format,
|
|
160
|
+
)
|
|
161
|
+
if builtins.mcp:
|
|
162
|
+
start_mcp(self)
|
|
163
|
+
return _finish(
|
|
164
|
+
make_envelope(
|
|
165
|
+
ok=True, command=self.name, format=output_format, data=None
|
|
166
|
+
),
|
|
167
|
+
stdout,
|
|
168
|
+
output_format,
|
|
169
|
+
)
|
|
170
|
+
if builtins.help or command is None:
|
|
171
|
+
text = (
|
|
172
|
+
render_command_help(command)
|
|
173
|
+
if command
|
|
174
|
+
else render_app_help(app, path=_app_path(self, app))
|
|
175
|
+
)
|
|
176
|
+
_write(stdout, text)
|
|
177
|
+
return _finish(
|
|
178
|
+
make_envelope(
|
|
179
|
+
ok=True,
|
|
180
|
+
command=" ".join(_app_path(self, app)),
|
|
181
|
+
format=output_format,
|
|
182
|
+
data=text,
|
|
183
|
+
),
|
|
184
|
+
stdout,
|
|
185
|
+
output_format,
|
|
186
|
+
)
|
|
187
|
+
if builtins.wizard and not is_tty:
|
|
188
|
+
envelope = make_envelope(
|
|
189
|
+
ok=True,
|
|
190
|
+
command=command.command,
|
|
191
|
+
format=output_format,
|
|
192
|
+
data=wizard_schema(command),
|
|
193
|
+
)
|
|
194
|
+
_write(
|
|
195
|
+
stdout,
|
|
196
|
+
render_envelope(envelope, format=output_format, verbose=True),
|
|
197
|
+
)
|
|
198
|
+
return _finish(envelope, stdout, output_format)
|
|
199
|
+
values = parse_values(
|
|
200
|
+
remainder,
|
|
201
|
+
command,
|
|
202
|
+
env=env_map,
|
|
203
|
+
tty=is_tty,
|
|
204
|
+
prompt_value=lambda spec, current_value=None, is_tty=False, **_kwargs: (
|
|
205
|
+
prompt_for_parameter(spec, stdin=stdin, stdout=stdout)
|
|
206
|
+
if spec.prompt and is_tty
|
|
207
|
+
else current_value
|
|
208
|
+
),
|
|
209
|
+
)
|
|
210
|
+
if builtins.wizard and is_tty:
|
|
211
|
+
for parameter in command.all_parameters:
|
|
212
|
+
if values.get(parameter.name, MISSING) is MISSING:
|
|
213
|
+
values[parameter.name] = prompt_for_parameter(
|
|
214
|
+
parameter, stdin=stdin, stdout=stdout
|
|
215
|
+
)
|
|
216
|
+
context = Context(
|
|
217
|
+
self.name,
|
|
218
|
+
command.path,
|
|
219
|
+
output_format,
|
|
220
|
+
detect_agent_mode(stdin=stdin, stdout=stdout, env=env_map),
|
|
221
|
+
args,
|
|
222
|
+
env_map,
|
|
223
|
+
{},
|
|
224
|
+
{},
|
|
225
|
+
{},
|
|
226
|
+
)
|
|
227
|
+
if command.context_parameter:
|
|
228
|
+
values[command.context_parameter] = context
|
|
229
|
+
envelope, streamed = asyncio.run(
|
|
230
|
+
self._execute(
|
|
231
|
+
command,
|
|
232
|
+
values,
|
|
233
|
+
context=context,
|
|
234
|
+
middlewares=middlewares,
|
|
235
|
+
stdout=stdout,
|
|
236
|
+
format=output_format,
|
|
237
|
+
)
|
|
238
|
+
)
|
|
239
|
+
if envelope.meta:
|
|
240
|
+
envelope.meta.duration_ms = (time.perf_counter() - started) * 1000
|
|
241
|
+
rendered = render_envelope(
|
|
242
|
+
envelope,
|
|
243
|
+
format=output_format,
|
|
244
|
+
verbose=builtins.verbose or not envelope.ok,
|
|
245
|
+
)
|
|
246
|
+
if not streamed or builtins.verbose:
|
|
247
|
+
_write(stdout, rendered)
|
|
248
|
+
return _finish(envelope, stdout, output_format, streamed)
|
|
249
|
+
except Exception as error: # noqa: BLE001
|
|
250
|
+
agent_error = normalize_exception(error)
|
|
251
|
+
envelope = make_envelope(
|
|
252
|
+
ok=False,
|
|
253
|
+
command=self.name,
|
|
254
|
+
format=output_format,
|
|
255
|
+
error=agent_error.to_error_info(),
|
|
256
|
+
duration_ms=(time.perf_counter() - started) * 1000,
|
|
257
|
+
)
|
|
258
|
+
target = stderr if stderr is not None else stdout
|
|
259
|
+
_write(
|
|
260
|
+
target, render_envelope(envelope, format=output_format, verbose=True)
|
|
261
|
+
)
|
|
262
|
+
return ExecutionResult(
|
|
263
|
+
agent_error.exit_code, envelope, _buffer(stdout, stderr), output_format
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
def __call__(self, argv: Iterable[str] | None = None) -> None:
|
|
267
|
+
result = self.run(argv)
|
|
268
|
+
if result.exit_code:
|
|
269
|
+
raise SystemExit(result.exit_code)
|
|
270
|
+
|
|
271
|
+
def test(self) -> AbstractContextManager[Any]:
|
|
272
|
+
from .testing import _TestClient
|
|
273
|
+
|
|
274
|
+
return _TestClient(self)
|
|
275
|
+
|
|
276
|
+
def _resolve(
|
|
277
|
+
self, argv: list[str]
|
|
278
|
+
) -> tuple[App, CommandSchema | None, list[str], list[Middleware]]:
|
|
279
|
+
current = self
|
|
280
|
+
index = 0
|
|
281
|
+
middlewares = list(self.middleware)
|
|
282
|
+
while index < len(argv):
|
|
283
|
+
token = argv[index]
|
|
284
|
+
if token.startswith("-"):
|
|
285
|
+
break
|
|
286
|
+
if token in current.subapps:
|
|
287
|
+
current = current.subapps[token]
|
|
288
|
+
middlewares.extend(current.middleware)
|
|
289
|
+
index += 1
|
|
290
|
+
continue
|
|
291
|
+
if token in current.commands:
|
|
292
|
+
command = current.commands[token]
|
|
293
|
+
command.full_path = (*_app_path(self, current), token)
|
|
294
|
+
return current, command, argv[index + 1 :], middlewares
|
|
295
|
+
if current._default_command is not None:
|
|
296
|
+
break
|
|
297
|
+
raise unknown_name_error(
|
|
298
|
+
kind="command",
|
|
299
|
+
value=token,
|
|
300
|
+
choices=[*current.subapps.keys(), *current.commands.keys()],
|
|
301
|
+
)
|
|
302
|
+
if current._default_command is not None:
|
|
303
|
+
current._default_command.full_path = _app_path(self, current)
|
|
304
|
+
return current, current._default_command, argv[index:], middlewares
|
|
305
|
+
return current, None, argv[index:], middlewares
|
|
306
|
+
|
|
307
|
+
async def _execute(
|
|
308
|
+
self,
|
|
309
|
+
command: CommandSchema,
|
|
310
|
+
values: dict[str, Any],
|
|
311
|
+
*,
|
|
312
|
+
context: Context,
|
|
313
|
+
middlewares: list[Middleware],
|
|
314
|
+
stdout: Any,
|
|
315
|
+
format: OutputFormat,
|
|
316
|
+
) -> tuple[Any, list[Any]]:
|
|
317
|
+
streamed: list[Any] = []
|
|
318
|
+
cta: list[str] | None = None
|
|
319
|
+
|
|
320
|
+
async def invoke_callback() -> Any:
|
|
321
|
+
nonlocal cta
|
|
322
|
+
outcome = command.callback(**values)
|
|
323
|
+
if inspect.isawaitable(outcome):
|
|
324
|
+
outcome = await outcome
|
|
325
|
+
if inspect.isasyncgen(outcome):
|
|
326
|
+
async for item in outcome:
|
|
327
|
+
streamed.append(item)
|
|
328
|
+
_write(
|
|
329
|
+
stdout,
|
|
330
|
+
render_stream_item(item, output_format=format, is_tty=False),
|
|
331
|
+
)
|
|
332
|
+
return None
|
|
333
|
+
if inspect.isgenerator(outcome):
|
|
334
|
+
for item in outcome:
|
|
335
|
+
streamed.append(item)
|
|
336
|
+
_write(
|
|
337
|
+
stdout,
|
|
338
|
+
render_stream_item(item, output_format=format, is_tty=False),
|
|
339
|
+
)
|
|
340
|
+
return None
|
|
341
|
+
if isinstance(outcome, Result):
|
|
342
|
+
cta = [
|
|
343
|
+
item.command if hasattr(item, "command") else str(item)
|
|
344
|
+
for item in outcome.cta
|
|
345
|
+
]
|
|
346
|
+
return outcome.data
|
|
347
|
+
return outcome
|
|
348
|
+
|
|
349
|
+
last_result: Any = None
|
|
350
|
+
|
|
351
|
+
async def dispatch(index: int) -> Any:
|
|
352
|
+
nonlocal last_result
|
|
353
|
+
if index == len(middlewares):
|
|
354
|
+
last_result = await invoke_callback()
|
|
355
|
+
return last_result
|
|
356
|
+
middleware = middlewares[index]
|
|
357
|
+
|
|
358
|
+
async def next_call() -> Any:
|
|
359
|
+
return await dispatch(index + 1)
|
|
360
|
+
|
|
361
|
+
result = middleware(context, next_call)
|
|
362
|
+
if inspect.isawaitable(result):
|
|
363
|
+
result = await result
|
|
364
|
+
if result is not None:
|
|
365
|
+
last_result = result
|
|
366
|
+
return last_result
|
|
367
|
+
|
|
368
|
+
data = await dispatch(0)
|
|
369
|
+
if streamed and data is None:
|
|
370
|
+
data = streamed
|
|
371
|
+
envelope = make_envelope(
|
|
372
|
+
ok=True,
|
|
373
|
+
command=command.command,
|
|
374
|
+
format=format,
|
|
375
|
+
data=data,
|
|
376
|
+
cta=cta,
|
|
377
|
+
streamed=bool(streamed),
|
|
378
|
+
)
|
|
379
|
+
return envelope, streamed
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def run(
|
|
383
|
+
func: Callable[..., Any],
|
|
384
|
+
*,
|
|
385
|
+
name: str | None = None,
|
|
386
|
+
version: str | None = None,
|
|
387
|
+
argv: list[str] | None = None,
|
|
388
|
+
) -> None:
|
|
389
|
+
app = App(
|
|
390
|
+
name or command_name_for(func.__name__),
|
|
391
|
+
version=version,
|
|
392
|
+
description=inspect.getdoc(func),
|
|
393
|
+
)
|
|
394
|
+
app._set_default(func)
|
|
395
|
+
app(argv)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _app_path(root: App, target: App) -> tuple[str, ...]:
|
|
399
|
+
if root is target:
|
|
400
|
+
return (root.name,)
|
|
401
|
+
for path, _command in iter_commands(root):
|
|
402
|
+
if path[:-1] and path[-2] == target.name:
|
|
403
|
+
return path[:-1]
|
|
404
|
+
return (root.name, target.name)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _isatty(stream: Any) -> bool:
|
|
408
|
+
isatty = getattr(stream, "isatty", None)
|
|
409
|
+
return bool(isatty()) if callable(isatty) else False
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _write(stream: Any, text: str) -> None:
|
|
413
|
+
if not text:
|
|
414
|
+
return
|
|
415
|
+
stream.write(text)
|
|
416
|
+
if not text.endswith("\n"):
|
|
417
|
+
stream.write("\n")
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _buffer(stdout: Any, stderr: Any | None = None) -> str:
|
|
421
|
+
for stream in (stdout, stderr):
|
|
422
|
+
if hasattr(stream, "getvalue"):
|
|
423
|
+
return stream.getvalue()
|
|
424
|
+
return ""
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _finish(
|
|
428
|
+
envelope, stdout: Any, format: OutputFormat, streamed: list[Any] | None = None
|
|
429
|
+
) -> ExecutionResult:
|
|
430
|
+
return ExecutionResult(
|
|
431
|
+
0 if envelope.ok else 1, envelope, _buffer(stdout), format, streamed or []
|
|
432
|
+
)
|
agentcli/_context.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any, Mapping
|
|
7
|
+
|
|
8
|
+
from ._types import OutputFormat
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class Context:
|
|
13
|
+
app_name: str = ""
|
|
14
|
+
command_path: tuple[str, ...] = ()
|
|
15
|
+
format: OutputFormat = "toon"
|
|
16
|
+
agent: bool = False
|
|
17
|
+
argv: list[str] = field(default_factory=list)
|
|
18
|
+
env: Mapping[str, str] = field(default_factory=dict)
|
|
19
|
+
state: dict[str, Any] = field(default_factory=dict)
|
|
20
|
+
meta: dict[str, Any] = field(default_factory=dict)
|
|
21
|
+
parent_state: Mapping[str, Any] = field(default_factory=dict)
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def command(self) -> str:
|
|
25
|
+
return " ".join(self.command_path)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def detect_agent_mode(
|
|
29
|
+
*, stdin: Any = None, stdout: Any = None, env: Mapping[str, str] | None = None
|
|
30
|
+
) -> bool:
|
|
31
|
+
environment = env or os.environ
|
|
32
|
+
if environment.get("AGENTCLI_MODE"):
|
|
33
|
+
return environment["AGENTCLI_MODE"].lower() == "agent"
|
|
34
|
+
return not _isatty(stdout or sys.stdout) or not _isatty(stdin or sys.stdin)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _isatty(stream: Any) -> bool:
|
|
38
|
+
isatty = getattr(stream, "isatty", None)
|
|
39
|
+
return bool(isatty()) if callable(isatty) else False
|