10xgraph-api 0.7.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.
- 10xgraph_api-0.7.0.dist-info/METADATA +618 -0
- 10xgraph_api-0.7.0.dist-info/RECORD +203 -0
- 10xgraph_api-0.7.0.dist-info/WHEEL +5 -0
- 10xgraph_api-0.7.0.dist-info/entry_points.txt +3 -0
- 10xgraph_api-0.7.0.dist-info/licenses/LICENSE +21 -0
- 10xgraph_api-0.7.0.dist-info/top_level.txt +2 -0
- agentflow_cli/__init__.py +69 -0
- tenxgraph_api/__init__.py +23 -0
- tenxgraph_api/cli/__init__.py +6 -0
- tenxgraph_api/cli/capabilities.py +127 -0
- tenxgraph_api/cli/commands/__init__.py +49 -0
- tenxgraph_api/cli/commands/api.py +262 -0
- tenxgraph_api/cli/commands/audit.py +213 -0
- tenxgraph_api/cli/commands/build.py +305 -0
- tenxgraph_api/cli/commands/config.py +71 -0
- tenxgraph_api/cli/commands/demo.py +93 -0
- tenxgraph_api/cli/commands/eval.py +909 -0
- tenxgraph_api/cli/commands/init.py +531 -0
- tenxgraph_api/cli/commands/skills.py +532 -0
- tenxgraph_api/cli/commands/test.py +113 -0
- tenxgraph_api/cli/commands/version.py +47 -0
- tenxgraph_api/cli/config_editor/__init__.py +15 -0
- tenxgraph_api/cli/config_editor/schema.py +566 -0
- tenxgraph_api/cli/config_editor/server.py +240 -0
- tenxgraph_api/cli/config_editor/static/app.css +2 -0
- tenxgraph_api/cli/config_editor/static/app.js +3 -0
- tenxgraph_api/cli/config_editor/static/favicon.svg +11 -0
- tenxgraph_api/cli/config_editor/static/index.html +20 -0
- tenxgraph_api/cli/config_editor/store.py +120 -0
- tenxgraph_api/cli/config_editor/validation.py +377 -0
- tenxgraph_api/cli/constants.py +112 -0
- tenxgraph_api/cli/context.py +46 -0
- tenxgraph_api/cli/core/__init__.py +0 -0
- tenxgraph_api/cli/core/animations.py +550 -0
- tenxgraph_api/cli/core/config.py +279 -0
- tenxgraph_api/cli/core/output.py +625 -0
- tenxgraph_api/cli/core/prompts.py +220 -0
- tenxgraph_api/cli/core/screen.py +287 -0
- tenxgraph_api/cli/core/steps.py +621 -0
- tenxgraph_api/cli/core/theme.py +242 -0
- tenxgraph_api/cli/core/validation.py +251 -0
- tenxgraph_api/cli/exceptions.py +129 -0
- tenxgraph_api/cli/logger.py +84 -0
- tenxgraph_api/cli/main.py +936 -0
- tenxgraph_api/cli/templates/__init__.py +0 -0
- tenxgraph_api/cli/templates/defaults.py +844 -0
- tenxgraph_api/cli/templates/dev/.env.example +1 -0
- tenxgraph_api/cli/templates/dev/10xgraph.json +16 -0
- tenxgraph_api/cli/templates/dev/graph/__init__.py +0 -0
- tenxgraph_api/cli/templates/dev/graph/agent.py +63 -0
- tenxgraph_api/cli/templates/prod/.env.example +75 -0
- tenxgraph_api/cli/templates/prod/.pre-commit-config.yaml +50 -0
- tenxgraph_api/cli/templates/prod/.python-version +1 -0
- tenxgraph_api/cli/templates/prod/10xgraph.json +42 -0
- tenxgraph_api/cli/templates/prod/auth/__init__.py +0 -0
- tenxgraph_api/cli/templates/prod/auth/agent_auth.py +46 -0
- tenxgraph_api/cli/templates/prod/evals/__init__.py +0 -0
- tenxgraph_api/cli/templates/prod/evals/user_simulator_eval.py +76 -0
- tenxgraph_api/cli/templates/prod/evals/weather_agents_eval.py +53 -0
- tenxgraph_api/cli/templates/prod/graph/__init__.py +0 -0
- tenxgraph_api/cli/templates/prod/graph/agent.py +50 -0
- tenxgraph_api/cli/templates/prod/graph/state.py +5 -0
- tenxgraph_api/cli/templates/prod/graph/thread_name_generator.py +7 -0
- tenxgraph_api/cli/templates/prod/graph/tools/__init__.py +0 -0
- tenxgraph_api/cli/templates/prod/graph/tools/weather_tool.py +43 -0
- tenxgraph_api/cli/templates/prod/graph/validators/__init__.py +0 -0
- tenxgraph_api/cli/templates/prod/graph/validators/lifecyle.py +107 -0
- tenxgraph_api/cli/templates/prod/graph/validators/manager.py +9 -0
- tenxgraph_api/cli/templates/prod/graph/validators/validators.py +21 -0
- tenxgraph_api/cli/templates/prod/pyproject.toml +79 -0
- tenxgraph_api/cli/templates/prod/tests/__init__.py +0 -0
- tenxgraph_api/cli/templates/prod/tests/conftest.py +6 -0
- tenxgraph_api/cli/templates/prod/tests/test_agent_eval.py +78 -0
- tenxgraph_api/cli/templates/prod/tests/test_catalog_tools.py +47 -0
- tenxgraph_api/cli/templates/prod/tests/test_graph_nodes.py +57 -0
- tenxgraph_api/cli/templates/skills/10xgraph/SKILL.md +92 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/agents-and-tools.md +389 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/api-client.md +61 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/api-configuration.md +75 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/api-settings-and-middleware.md +89 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/architecture.md +91 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/auth-and-authorization.md +149 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/callbacks-and-command.md +283 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/checkpointing-and-threads.md +63 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/cli-commands.md +73 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/client-auth-and-errors.md +94 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/client-messages-invoke-stream.md +115 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/client-threads-memory-files.md +80 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/context-id-background.md +80 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/dependency-injection.md +58 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/evaluation.md +500 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/id-and-thread-name-generators.md +69 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/media-and-files.md +66 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/memory-and-store.md +70 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/prebuilt-agents-and-tools.md +394 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/production-runtime.md +56 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/providers-and-adapters.md +89 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/publishers-and-runtime-protocols.md +84 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/rate-limiting.md +194 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/realtime.md +172 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/remote-tools.md +55 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/rest-api-and-errors.md +97 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/security-and-validators.md +86 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/state-and-messages.md +68 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/state-graph.md +232 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/stream-emitter.md +144 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/streaming.md +53 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/testing-and-evaluation.md +64 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/unit-testing.md +202 -0
- tenxgraph_api/cli/templates/skills/copilot/10xgraph.instructions.md +108 -0
- tenxgraph_api/py.typed +0 -0
- tenxgraph_api/src/__init__.py +0 -0
- tenxgraph_api/src/app/__init__.py +13 -0
- tenxgraph_api/src/app/core/__init__.py +15 -0
- tenxgraph_api/src/app/core/auth/__init__.py +0 -0
- tenxgraph_api/src/app/core/auth/auth_backend.py +38 -0
- tenxgraph_api/src/app/core/auth/authorization.py +379 -0
- tenxgraph_api/src/app/core/auth/base_auth.py +27 -0
- tenxgraph_api/src/app/core/auth/jwt_auth.py +122 -0
- tenxgraph_api/src/app/core/auth/ownership_resolver.py +170 -0
- tenxgraph_api/src/app/core/auth/permissions.py +354 -0
- tenxgraph_api/src/app/core/auth/request_config.py +130 -0
- tenxgraph_api/src/app/core/auth/route_guard.py +105 -0
- tenxgraph_api/src/app/core/config/__init__.py +0 -0
- tenxgraph_api/src/app/core/config/graph_config.py +532 -0
- tenxgraph_api/src/app/core/config/media_settings.py +72 -0
- tenxgraph_api/src/app/core/config/sentry_config.py +60 -0
- tenxgraph_api/src/app/core/config/settings.py +202 -0
- tenxgraph_api/src/app/core/config/setup_logs.py +65 -0
- tenxgraph_api/src/app/core/config/setup_middleware.py +483 -0
- tenxgraph_api/src/app/core/config/worker_middleware.py +99 -0
- tenxgraph_api/src/app/core/exceptions/__init__.py +17 -0
- tenxgraph_api/src/app/core/exceptions/general_exception.py +58 -0
- tenxgraph_api/src/app/core/exceptions/handle_errors.py +363 -0
- tenxgraph_api/src/app/core/exceptions/resources_exceptions.py +72 -0
- tenxgraph_api/src/app/core/exceptions/user_exception.py +58 -0
- tenxgraph_api/src/app/core/middleware/__init__.py +25 -0
- tenxgraph_api/src/app/core/middleware/rate_limit/__init__.py +15 -0
- tenxgraph_api/src/app/core/middleware/rate_limit/base.py +27 -0
- tenxgraph_api/src/app/core/middleware/rate_limit/factory.py +98 -0
- tenxgraph_api/src/app/core/middleware/rate_limit/keying.py +133 -0
- tenxgraph_api/src/app/core/middleware/rate_limit/memory.py +98 -0
- tenxgraph_api/src/app/core/middleware/rate_limit/middleware.py +90 -0
- tenxgraph_api/src/app/core/middleware/rate_limit/redis.py +115 -0
- tenxgraph_api/src/app/core/middleware/request_limits.py +154 -0
- tenxgraph_api/src/app/core/middleware/security_headers.py +239 -0
- tenxgraph_api/src/app/core/utils/__init__.py +14 -0
- tenxgraph_api/src/app/core/utils/log_sanitizer.py +216 -0
- tenxgraph_api/src/app/loader.py +516 -0
- tenxgraph_api/src/app/main.py +180 -0
- tenxgraph_api/src/app/routers/__init__.py +4 -0
- tenxgraph_api/src/app/routers/ag_ui/__init__.py +10 -0
- tenxgraph_api/src/app/routers/ag_ui/converter.py +195 -0
- tenxgraph_api/src/app/routers/ag_ui/event_mapper.py +353 -0
- tenxgraph_api/src/app/routers/ag_ui/router.py +90 -0
- tenxgraph_api/src/app/routers/ag_ui/service.py +193 -0
- tenxgraph_api/src/app/routers/checkpointer/__init__.py +1 -0
- tenxgraph_api/src/app/routers/checkpointer/router.py +482 -0
- tenxgraph_api/src/app/routers/checkpointer/schemas/__init__.py +1 -0
- tenxgraph_api/src/app/routers/checkpointer/schemas/checkpointer_schemas.py +121 -0
- tenxgraph_api/src/app/routers/checkpointer/services/__init__.py +1 -0
- tenxgraph_api/src/app/routers/checkpointer/services/checkpointer_service.py +300 -0
- tenxgraph_api/src/app/routers/evals/__init__.py +6 -0
- tenxgraph_api/src/app/routers/evals/router.py +38 -0
- tenxgraph_api/src/app/routers/evals/services/__init__.py +1 -0
- tenxgraph_api/src/app/routers/evals/services/eval_report_service.py +354 -0
- tenxgraph_api/src/app/routers/graph/__init__.py +6 -0
- tenxgraph_api/src/app/routers/graph/realtime_guard.py +161 -0
- tenxgraph_api/src/app/routers/graph/router.py +821 -0
- tenxgraph_api/src/app/routers/graph/schemas/__init__.py +1 -0
- tenxgraph_api/src/app/routers/graph/schemas/graph_schemas.py +386 -0
- tenxgraph_api/src/app/routers/graph/services/__init__.py +1 -0
- tenxgraph_api/src/app/routers/graph/services/graph_service.py +1219 -0
- tenxgraph_api/src/app/routers/graph/services/multimodal_preprocessor.py +164 -0
- tenxgraph_api/src/app/routers/media/__init__.py +506 -0
- tenxgraph_api/src/app/routers/media/router.py +242 -0
- tenxgraph_api/src/app/routers/media/schemas.py +39 -0
- tenxgraph_api/src/app/routers/ping/__init__.py +4 -0
- tenxgraph_api/src/app/routers/ping/router.py +28 -0
- tenxgraph_api/src/app/routers/setup_router.py +60 -0
- tenxgraph_api/src/app/routers/store/__init__.py +4 -0
- tenxgraph_api/src/app/routers/store/router.py +200 -0
- tenxgraph_api/src/app/routers/store/schemas/__init__.py +28 -0
- tenxgraph_api/src/app/routers/store/schemas/store_schemas.py +200 -0
- tenxgraph_api/src/app/routers/store/services/__init__.py +4 -0
- tenxgraph_api/src/app/routers/store/services/store_service.py +203 -0
- tenxgraph_api/src/app/tasks/__init__.py +0 -0
- tenxgraph_api/src/app/tasks/user_tasks.py +38 -0
- tenxgraph_api/src/app/utils/__init__.py +12 -0
- tenxgraph_api/src/app/utils/callable_helper.py +26 -0
- tenxgraph_api/src/app/utils/media/__init__.py +15 -0
- tenxgraph_api/src/app/utils/media/extractor.py +172 -0
- tenxgraph_api/src/app/utils/media/pipeline.py +83 -0
- tenxgraph_api/src/app/utils/parse_output.py +17 -0
- tenxgraph_api/src/app/utils/response_helper.py +129 -0
- tenxgraph_api/src/app/utils/schemas/__init__.py +11 -0
- tenxgraph_api/src/app/utils/schemas/output_schemas.py +28 -0
- tenxgraph_api/src/app/utils/schemas/user_schemas.py +13 -0
- tenxgraph_api/src/app/utils/snowflake_id_generator.py +90 -0
- tenxgraph_api/src/app/utils/swagger_helper.py +160 -0
- tenxgraph_api/src/app/utils/telemetry_store.py +123 -0
- tenxgraph_api/src/app/utils/thread_name_generator.py +304 -0
- tenxgraph_api/src/app/worker.py +91 -0
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
"""Adaptive terminal output utilities for the CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
from collections.abc import Iterator, Sequence
|
|
9
|
+
from contextlib import contextmanager
|
|
10
|
+
from typing import Any, TextIO
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
from rich import box
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
from rich.live import Live
|
|
16
|
+
from rich.panel import Panel
|
|
17
|
+
from rich.status import Status
|
|
18
|
+
from rich.table import Table
|
|
19
|
+
from rich.text import Text
|
|
20
|
+
|
|
21
|
+
from tenxgraph_api.cli.capabilities import (
|
|
22
|
+
ColorMode,
|
|
23
|
+
OutputFormat,
|
|
24
|
+
ProgressMode,
|
|
25
|
+
TerminalCapabilities,
|
|
26
|
+
)
|
|
27
|
+
from tenxgraph_api.cli.constants import CLI_VERSION
|
|
28
|
+
from tenxgraph_api.cli.core.animations import render_command_intro, render_session_header
|
|
29
|
+
from tenxgraph_api.cli.core.prompts import PromptService
|
|
30
|
+
from tenxgraph_api.cli.core.screen import AppFrame
|
|
31
|
+
from tenxgraph_api.cli.core.steps import (
|
|
32
|
+
LiveProgressRun,
|
|
33
|
+
LiveTimeline,
|
|
34
|
+
ProgressRun,
|
|
35
|
+
QuietProgressRun,
|
|
36
|
+
QuietTimeline,
|
|
37
|
+
StaticProgressRun,
|
|
38
|
+
StaticTimeline,
|
|
39
|
+
StructuredProgressRun,
|
|
40
|
+
StructuredTimeline,
|
|
41
|
+
Timeline,
|
|
42
|
+
)
|
|
43
|
+
from tenxgraph_api.cli.core.theme import (
|
|
44
|
+
ACCENT,
|
|
45
|
+
LINE,
|
|
46
|
+
TENXGRAPH_THEME,
|
|
47
|
+
Glyphs,
|
|
48
|
+
glyphs_for,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class OutputFormatter:
|
|
53
|
+
"""Render semantic CLI output for terminals, pipes, and automation."""
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
stream: TextIO | None = None,
|
|
58
|
+
*,
|
|
59
|
+
error_stream: TextIO | None = None,
|
|
60
|
+
output_format: OutputFormat = OutputFormat.HUMAN,
|
|
61
|
+
color_mode: ColorMode = ColorMode.AUTO,
|
|
62
|
+
progress_mode: ProgressMode = ProgressMode.AUTO,
|
|
63
|
+
quiet: bool = False,
|
|
64
|
+
) -> None:
|
|
65
|
+
self._stream = stream
|
|
66
|
+
self._error_stream = error_stream
|
|
67
|
+
self.output_format = output_format
|
|
68
|
+
self.color_mode = color_mode
|
|
69
|
+
self.progress_mode = progress_mode
|
|
70
|
+
self.quiet = quiet
|
|
71
|
+
self._session_console: Console | None = None
|
|
72
|
+
self._frame: AppFrame | None = None
|
|
73
|
+
self._fullscreen_requested = False
|
|
74
|
+
self._consoles: dict[bool, Console] = {}
|
|
75
|
+
self.capabilities = TerminalCapabilities.detect(
|
|
76
|
+
stream=self.stream,
|
|
77
|
+
output_format=output_format,
|
|
78
|
+
color_mode=color_mode,
|
|
79
|
+
progress_mode=progress_mode,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def stream(self) -> TextIO:
|
|
84
|
+
"""Current stdout stream, remaining compatible with CliRunner capture."""
|
|
85
|
+
return self._stream or sys.stdout
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def error_stream(self) -> TextIO:
|
|
89
|
+
"""Current stderr stream, or the explicit test stream when supplied."""
|
|
90
|
+
if self._error_stream is not None:
|
|
91
|
+
return self._error_stream
|
|
92
|
+
if self._stream is not None:
|
|
93
|
+
return self._stream
|
|
94
|
+
return sys.stderr
|
|
95
|
+
|
|
96
|
+
def configure(
|
|
97
|
+
self,
|
|
98
|
+
*,
|
|
99
|
+
output_format: OutputFormat | None = None,
|
|
100
|
+
color_mode: ColorMode | None = None,
|
|
101
|
+
progress_mode: ProgressMode | None = None,
|
|
102
|
+
quiet: bool | None = None,
|
|
103
|
+
) -> None:
|
|
104
|
+
"""Apply invocation-level rendering policy."""
|
|
105
|
+
if output_format is not None:
|
|
106
|
+
self.output_format = output_format
|
|
107
|
+
if color_mode is not None:
|
|
108
|
+
self.color_mode = color_mode
|
|
109
|
+
if progress_mode is not None:
|
|
110
|
+
self.progress_mode = progress_mode
|
|
111
|
+
if quiet is not None:
|
|
112
|
+
self.quiet = quiet
|
|
113
|
+
# Color and terminal policy are baked into a Console at construction.
|
|
114
|
+
self._consoles.clear()
|
|
115
|
+
self.capabilities = TerminalCapabilities.detect(
|
|
116
|
+
stream=self.stream,
|
|
117
|
+
output_format=self.output_format,
|
|
118
|
+
color_mode=self.color_mode,
|
|
119
|
+
progress_mode=self.progress_mode,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
def _console(self, *, error: bool = False) -> Console:
|
|
123
|
+
"""Return the one console bound to this stream.
|
|
124
|
+
|
|
125
|
+
Consoles are cached rather than rebuilt per call because a Rich live
|
|
126
|
+
display only knows to render ordinary prints *above* itself when those
|
|
127
|
+
prints go through the same console instance.
|
|
128
|
+
"""
|
|
129
|
+
if self._session_console is not None:
|
|
130
|
+
return self._session_console
|
|
131
|
+
cached = self._consoles.get(error)
|
|
132
|
+
if cached is None:
|
|
133
|
+
cached = Console(
|
|
134
|
+
file=self.error_stream if error else self.stream,
|
|
135
|
+
theme=TENXGRAPH_THEME,
|
|
136
|
+
no_color=not self.capabilities.color,
|
|
137
|
+
force_terminal=self.capabilities.color,
|
|
138
|
+
highlight=False,
|
|
139
|
+
soft_wrap=False,
|
|
140
|
+
)
|
|
141
|
+
self._consoles[error] = cached
|
|
142
|
+
return cached
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def glyphs(self) -> Glyphs:
|
|
146
|
+
"""Symbol set matching this terminal's encoding support."""
|
|
147
|
+
return glyphs_for(self.capabilities.unicode)
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def fullscreen_active(self) -> bool:
|
|
151
|
+
"""Whether this invocation currently owns the alternate screen."""
|
|
152
|
+
return self._frame is not None and self._frame.active
|
|
153
|
+
|
|
154
|
+
def request_fullscreen(self, enabled: bool) -> None:
|
|
155
|
+
"""Record whether this invocation may claim a full-screen surface.
|
|
156
|
+
|
|
157
|
+
The screen is not taken here. It is claimed lazily by the first
|
|
158
|
+
``command_header`` call, so script-shaped commands — ``--version``,
|
|
159
|
+
``config get`` — never take over the terminal or pause on exit.
|
|
160
|
+
"""
|
|
161
|
+
self._fullscreen_requested = enabled
|
|
162
|
+
|
|
163
|
+
def start_fullscreen_session(self) -> bool:
|
|
164
|
+
"""Claim the alternate screen for the whole command."""
|
|
165
|
+
if self.fullscreen_active or self.quiet or not self.capabilities.animation:
|
|
166
|
+
return False
|
|
167
|
+
|
|
168
|
+
console = self._console()
|
|
169
|
+
frame = AppFrame(console, glyphs=self.glyphs, color=self.capabilities.color)
|
|
170
|
+
if not frame.open():
|
|
171
|
+
return False
|
|
172
|
+
|
|
173
|
+
# Bind every later render to this console so chrome, live displays, and
|
|
174
|
+
# ordinary prints all agree on where the cursor is.
|
|
175
|
+
self._session_console = console
|
|
176
|
+
self._frame = frame
|
|
177
|
+
return True
|
|
178
|
+
|
|
179
|
+
def end_fullscreen_session(self) -> None:
|
|
180
|
+
"""Restore the user's terminal, pausing on the closing hint after a normal finish.
|
|
181
|
+
|
|
182
|
+
Called from the CLI context's close callback, which runs while an exception is
|
|
183
|
+
still propagating. Ctrl+C (``KeyboardInterrupt``, or Click's ``Abort`` wrapping
|
|
184
|
+
it) releases the screen immediately instead of waiting for Enter; a failure
|
|
185
|
+
still pauses so its message can be read before the screen is discarded.
|
|
186
|
+
"""
|
|
187
|
+
frame = self._frame
|
|
188
|
+
self._frame = None
|
|
189
|
+
self._session_console = None
|
|
190
|
+
if frame is not None:
|
|
191
|
+
in_flight = sys.exc_info()[1]
|
|
192
|
+
interrupted = isinstance(in_flight, KeyboardInterrupt | click.exceptions.Abort)
|
|
193
|
+
frame.close(hold=not interrupted)
|
|
194
|
+
|
|
195
|
+
def refresh_chrome(self) -> None:
|
|
196
|
+
"""Repaint pinned chrome that something else may have drawn over."""
|
|
197
|
+
if self._frame is not None:
|
|
198
|
+
self._frame.redraw_chrome()
|
|
199
|
+
|
|
200
|
+
def prompts(self, *, interactive: bool | None = None) -> PromptService:
|
|
201
|
+
"""Build a prompt service bound to this invocation's terminal state."""
|
|
202
|
+
return PromptService(
|
|
203
|
+
interactive=interactive,
|
|
204
|
+
on_answered=self.refresh_chrome,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
@property
|
|
208
|
+
def _structured(self) -> bool:
|
|
209
|
+
return self.capabilities.output_format in {OutputFormat.JSON, OutputFormat.JSONL}
|
|
210
|
+
|
|
211
|
+
def _emit_event(self, event: str, message: str, **data: Any) -> None:
|
|
212
|
+
payload = {
|
|
213
|
+
"schema": "agentflow.cli/v1",
|
|
214
|
+
"type": event,
|
|
215
|
+
"message": message,
|
|
216
|
+
**data,
|
|
217
|
+
}
|
|
218
|
+
print(json.dumps(payload, ensure_ascii=False, default=str), file=self.stream, flush=True)
|
|
219
|
+
|
|
220
|
+
def print_banner(
|
|
221
|
+
self,
|
|
222
|
+
title: str,
|
|
223
|
+
subtitle: str | None = None,
|
|
224
|
+
color: str = LINE,
|
|
225
|
+
width: int = 50,
|
|
226
|
+
) -> None:
|
|
227
|
+
"""Render a compact section heading or a panel on an interactive terminal."""
|
|
228
|
+
if self.quiet:
|
|
229
|
+
return
|
|
230
|
+
if self._structured:
|
|
231
|
+
self._emit_event("section", title, subtitle=subtitle)
|
|
232
|
+
return
|
|
233
|
+
if self.capabilities.output_format == OutputFormat.PLAIN:
|
|
234
|
+
print(f"\n== {title} ==", file=self.stream)
|
|
235
|
+
if subtitle:
|
|
236
|
+
print(subtitle, file=self.stream)
|
|
237
|
+
print("", file=self.stream)
|
|
238
|
+
return
|
|
239
|
+
|
|
240
|
+
body = f"[tenxgraph.title]{title}[/tenxgraph.title]"
|
|
241
|
+
if subtitle:
|
|
242
|
+
body += f"\n[tenxgraph.muted]{subtitle}[/tenxgraph.muted]"
|
|
243
|
+
self._console().print(Panel.fit(body, border_style=color, padding=(0, 1), width=width))
|
|
244
|
+
|
|
245
|
+
def command_header(
|
|
246
|
+
self,
|
|
247
|
+
command: str,
|
|
248
|
+
subtitle: str | None = None,
|
|
249
|
+
*,
|
|
250
|
+
color: str = LINE,
|
|
251
|
+
hint: str | None = None,
|
|
252
|
+
) -> None:
|
|
253
|
+
"""Render an animated command identity when the terminal supports it.
|
|
254
|
+
|
|
255
|
+
``hint`` is the closing line pinned in the full-screen footer; it should
|
|
256
|
+
say how to leave whatever the command is about to do.
|
|
257
|
+
"""
|
|
258
|
+
if self.quiet:
|
|
259
|
+
return
|
|
260
|
+
if self._structured:
|
|
261
|
+
self._emit_event("section", command, subtitle=subtitle)
|
|
262
|
+
return
|
|
263
|
+
if self.capabilities.output_format == OutputFormat.PLAIN:
|
|
264
|
+
self.print_banner(command.title(), subtitle, color=color)
|
|
265
|
+
return
|
|
266
|
+
if not self.capabilities.animation:
|
|
267
|
+
# Still branded, just motionless — reduced motion should not mean
|
|
268
|
+
# a downgrade in the information the header carries.
|
|
269
|
+
render_session_header(
|
|
270
|
+
self._console(),
|
|
271
|
+
command=command,
|
|
272
|
+
subtitle=subtitle,
|
|
273
|
+
glyphs=self.glyphs,
|
|
274
|
+
version=CLI_VERSION,
|
|
275
|
+
)
|
|
276
|
+
return
|
|
277
|
+
if self._fullscreen_requested:
|
|
278
|
+
self.start_fullscreen_session()
|
|
279
|
+
# Without the opt-in frame the intro borrows a temporary screen, then prints a
|
|
280
|
+
# durable header into the normal scrollback: nothing to dismiss on exit.
|
|
281
|
+
render_command_intro(
|
|
282
|
+
self._console(),
|
|
283
|
+
command=command,
|
|
284
|
+
subtitle=subtitle,
|
|
285
|
+
unicode=self.capabilities.unicode,
|
|
286
|
+
persistent_screen=self.fullscreen_active,
|
|
287
|
+
)
|
|
288
|
+
if self._frame is not None:
|
|
289
|
+
# The intro owned the whole canvas; pin the chrome it collapses into
|
|
290
|
+
# and hand the body region over to the command's output.
|
|
291
|
+
self._frame.install_chrome(
|
|
292
|
+
command=command,
|
|
293
|
+
subtitle=subtitle,
|
|
294
|
+
version=CLI_VERSION,
|
|
295
|
+
hint=hint,
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
def timeline(
|
|
299
|
+
self,
|
|
300
|
+
title: str | None = None,
|
|
301
|
+
*,
|
|
302
|
+
steps: Sequence[tuple[str, str]] = (),
|
|
303
|
+
) -> Timeline:
|
|
304
|
+
"""Create a multi-stage progress timeline for the current output mode.
|
|
305
|
+
|
|
306
|
+
Declaring ``steps`` up front lets the user see the whole plan the moment
|
|
307
|
+
work begins, with pending stages dimmed and the running one animated.
|
|
308
|
+
"""
|
|
309
|
+
if self.quiet:
|
|
310
|
+
return QuietTimeline(steps)
|
|
311
|
+
if self._structured:
|
|
312
|
+
return StructuredTimeline(emit=self._emit_event_data, title=title, steps=steps)
|
|
313
|
+
if not self.capabilities.animation:
|
|
314
|
+
return StaticTimeline(
|
|
315
|
+
glyphs=self.glyphs,
|
|
316
|
+
emit=self._write_line,
|
|
317
|
+
title=title,
|
|
318
|
+
steps=steps,
|
|
319
|
+
)
|
|
320
|
+
return LiveTimeline(self._console(), glyphs=self.glyphs, title=title, steps=steps)
|
|
321
|
+
|
|
322
|
+
def progress_run(self, title: str, *, total: int) -> ProgressRun:
|
|
323
|
+
"""Create a determinate progress display with a running pass/fail tally."""
|
|
324
|
+
if self.quiet:
|
|
325
|
+
return QuietProgressRun(total=total, title=title)
|
|
326
|
+
if self._structured:
|
|
327
|
+
return StructuredProgressRun(total=total, title=title, emit=self._emit_event_data)
|
|
328
|
+
if not self.capabilities.animation:
|
|
329
|
+
return StaticProgressRun(
|
|
330
|
+
total=total,
|
|
331
|
+
title=title,
|
|
332
|
+
glyphs=self.glyphs,
|
|
333
|
+
emit=self._write_line,
|
|
334
|
+
)
|
|
335
|
+
return LiveProgressRun(self._console(), total=total, title=title, glyphs=self.glyphs)
|
|
336
|
+
|
|
337
|
+
def _emit_event_data(self, event: str, message: str, data: dict[str, Any]) -> None:
|
|
338
|
+
self._emit_event(event, message, **data)
|
|
339
|
+
|
|
340
|
+
def _write_line(self, message: str) -> None:
|
|
341
|
+
"""Write one pre-formatted line without interpreting console markup."""
|
|
342
|
+
if self.capabilities.output_format == OutputFormat.PLAIN:
|
|
343
|
+
print(message, file=self.stream)
|
|
344
|
+
return
|
|
345
|
+
self._console().print(Text(message))
|
|
346
|
+
|
|
347
|
+
def success(self, message: str, emoji: bool = True) -> None:
|
|
348
|
+
if self.quiet:
|
|
349
|
+
return
|
|
350
|
+
symbol = "✓" if self.capabilities.unicode else "OK"
|
|
351
|
+
self._message("success", message, symbol if emoji else "", error=False)
|
|
352
|
+
|
|
353
|
+
def error(self, message: str, emoji: bool = True) -> None:
|
|
354
|
+
symbol = "✗" if self.capabilities.unicode else "X"
|
|
355
|
+
self._message("error", message, symbol if emoji else "", error=True)
|
|
356
|
+
|
|
357
|
+
def info(self, message: str, emoji: bool = True) -> None:
|
|
358
|
+
if self.quiet:
|
|
359
|
+
return
|
|
360
|
+
self._message("info", message, "i" if emoji else "", error=False)
|
|
361
|
+
|
|
362
|
+
def warning(self, message: str, emoji: bool = True) -> None:
|
|
363
|
+
if self.quiet:
|
|
364
|
+
return
|
|
365
|
+
self._message("warning", message, "!" if emoji else "", error=False)
|
|
366
|
+
|
|
367
|
+
def _message(
|
|
368
|
+
self,
|
|
369
|
+
level: str,
|
|
370
|
+
message: str,
|
|
371
|
+
symbol: str,
|
|
372
|
+
*,
|
|
373
|
+
error: bool,
|
|
374
|
+
) -> None:
|
|
375
|
+
if self._structured:
|
|
376
|
+
self._emit_event(level, message)
|
|
377
|
+
return
|
|
378
|
+
prefix = f"{symbol} " if symbol else ""
|
|
379
|
+
if self.capabilities.output_format == OutputFormat.PLAIN:
|
|
380
|
+
print(f"{prefix}{message}", file=self.error_stream if error else self.stream)
|
|
381
|
+
return
|
|
382
|
+
self._console(error=error).print(
|
|
383
|
+
f"[tenxgraph.{level}]{prefix}{message}[/tenxgraph.{level}]"
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
def emphasize(self, message: str) -> None:
|
|
387
|
+
if self.quiet:
|
|
388
|
+
return
|
|
389
|
+
self._message(
|
|
390
|
+
"info",
|
|
391
|
+
message,
|
|
392
|
+
"•" if self.capabilities.unicode else "*",
|
|
393
|
+
error=False,
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
def print_list(
|
|
397
|
+
self,
|
|
398
|
+
items: list[str],
|
|
399
|
+
title: str | None = None,
|
|
400
|
+
bullet: str = "•",
|
|
401
|
+
) -> None:
|
|
402
|
+
if self.quiet:
|
|
403
|
+
return
|
|
404
|
+
if self._structured:
|
|
405
|
+
self._emit_event("list", title or "", items=items)
|
|
406
|
+
return
|
|
407
|
+
console = self._console()
|
|
408
|
+
if title:
|
|
409
|
+
console.print(f"\n[bold]{title}[/bold]")
|
|
410
|
+
for item in items:
|
|
411
|
+
console.print(f" {bullet} {item}")
|
|
412
|
+
|
|
413
|
+
def print_key_value_pairs(
|
|
414
|
+
self,
|
|
415
|
+
pairs: dict[str, Any],
|
|
416
|
+
title: str | None = None,
|
|
417
|
+
indent: int = 2,
|
|
418
|
+
) -> None:
|
|
419
|
+
if self.quiet:
|
|
420
|
+
return
|
|
421
|
+
if self._structured:
|
|
422
|
+
self._emit_event("data", title or "", data=pairs)
|
|
423
|
+
return
|
|
424
|
+
console = self._console()
|
|
425
|
+
if title:
|
|
426
|
+
console.print(f"\n[bold]{title}[/bold]")
|
|
427
|
+
pad = " " * indent
|
|
428
|
+
for key, value in pairs.items():
|
|
429
|
+
console.print(f"{pad}[tenxgraph.muted]{key}[/tenxgraph.muted] {value}")
|
|
430
|
+
|
|
431
|
+
def print_table(
|
|
432
|
+
self,
|
|
433
|
+
headers: list[str],
|
|
434
|
+
rows: list[list[str]],
|
|
435
|
+
title: str | None = None,
|
|
436
|
+
) -> None:
|
|
437
|
+
if self.quiet:
|
|
438
|
+
return
|
|
439
|
+
if self._structured:
|
|
440
|
+
records = [
|
|
441
|
+
{
|
|
442
|
+
header: row[index] if index < len(row) else ""
|
|
443
|
+
for index, header in enumerate(headers)
|
|
444
|
+
}
|
|
445
|
+
for row in rows
|
|
446
|
+
]
|
|
447
|
+
self._emit_event("table", title or "", columns=headers, rows=records)
|
|
448
|
+
return
|
|
449
|
+
|
|
450
|
+
table = Table(title=title, header_style="bold cyan", show_lines=False)
|
|
451
|
+
for header in headers:
|
|
452
|
+
table.add_column(str(header), overflow="fold")
|
|
453
|
+
for row in rows:
|
|
454
|
+
table.add_row(*(str(row[i]) if i < len(row) else "" for i in range(len(headers))))
|
|
455
|
+
self._console().print(table)
|
|
456
|
+
|
|
457
|
+
@contextmanager
|
|
458
|
+
def status(self, message: str, *, spinner: str = "dots12") -> Iterator[Status | None]:
|
|
459
|
+
"""Show an indeterminate status when animation is safe."""
|
|
460
|
+
if self._structured:
|
|
461
|
+
self._emit_event("progress_start", message)
|
|
462
|
+
try:
|
|
463
|
+
yield None
|
|
464
|
+
except Exception:
|
|
465
|
+
self._emit_event("progress_end", message, status="failed")
|
|
466
|
+
raise
|
|
467
|
+
else:
|
|
468
|
+
self._emit_event("progress_end", message, status="completed")
|
|
469
|
+
return
|
|
470
|
+
if self.quiet or not self.capabilities.animation:
|
|
471
|
+
if not self.quiet and self.capabilities.progress_mode == ProgressMode.PLAIN:
|
|
472
|
+
self.info(message, emoji=False)
|
|
473
|
+
yield None
|
|
474
|
+
return
|
|
475
|
+
with self._console().status(
|
|
476
|
+
f"[tenxgraph.progress]{message}[/tenxgraph.progress]",
|
|
477
|
+
spinner=spinner,
|
|
478
|
+
spinner_style="tenxgraph.brand",
|
|
479
|
+
) as status:
|
|
480
|
+
yield status
|
|
481
|
+
|
|
482
|
+
@contextmanager
|
|
483
|
+
def activity(
|
|
484
|
+
self,
|
|
485
|
+
message: str,
|
|
486
|
+
*,
|
|
487
|
+
done: str | None = None,
|
|
488
|
+
spinner: str = "dots12",
|
|
489
|
+
) -> Iterator[Status | None]:
|
|
490
|
+
"""Animate a bounded task and persist a timed completion state."""
|
|
491
|
+
started = time.monotonic()
|
|
492
|
+
try:
|
|
493
|
+
with self.status(message, spinner=spinner) as status:
|
|
494
|
+
yield status
|
|
495
|
+
except Exception:
|
|
496
|
+
elapsed = time.monotonic() - started
|
|
497
|
+
self.error(f"{message} failed after {elapsed:.1f}s")
|
|
498
|
+
raise
|
|
499
|
+
else:
|
|
500
|
+
elapsed = time.monotonic() - started
|
|
501
|
+
self.success(f"{done or message} ({elapsed:.1f}s)")
|
|
502
|
+
|
|
503
|
+
def completion_screen(
|
|
504
|
+
self,
|
|
505
|
+
title: str,
|
|
506
|
+
message: str,
|
|
507
|
+
*,
|
|
508
|
+
details: dict[str, Any] | None = None,
|
|
509
|
+
next_steps: list[str] | None = None,
|
|
510
|
+
) -> None:
|
|
511
|
+
"""Render a polished final state for a completed workflow."""
|
|
512
|
+
if self.quiet:
|
|
513
|
+
return
|
|
514
|
+
if self._structured:
|
|
515
|
+
self._emit_event(
|
|
516
|
+
"completion",
|
|
517
|
+
message,
|
|
518
|
+
title=title,
|
|
519
|
+
details=details or {},
|
|
520
|
+
next_steps=next_steps or [],
|
|
521
|
+
)
|
|
522
|
+
return
|
|
523
|
+
if self.capabilities.output_format == OutputFormat.PLAIN:
|
|
524
|
+
self.success(f"{title}: {message}")
|
|
525
|
+
if details:
|
|
526
|
+
self.print_key_value_pairs(details)
|
|
527
|
+
if next_steps:
|
|
528
|
+
self.print_list(next_steps, title="Next steps", bullet="->")
|
|
529
|
+
return
|
|
530
|
+
|
|
531
|
+
console = self._console()
|
|
532
|
+
rows = list((details or {}).items())
|
|
533
|
+
steps = list(next_steps or [])
|
|
534
|
+
total = len(rows) + len(steps)
|
|
535
|
+
|
|
536
|
+
if not self.capabilities.animation or not total:
|
|
537
|
+
console.print(self._completion_panel(title, message, rows, steps, total))
|
|
538
|
+
return
|
|
539
|
+
|
|
540
|
+
# Reveal one row at a time so the result reads as an outcome landing
|
|
541
|
+
# rather than a wall of text appearing at once.
|
|
542
|
+
with Live(
|
|
543
|
+
console=console,
|
|
544
|
+
auto_refresh=False,
|
|
545
|
+
transient=False,
|
|
546
|
+
redirect_stdout=False,
|
|
547
|
+
redirect_stderr=False,
|
|
548
|
+
) as live:
|
|
549
|
+
for visible in range(total + 1):
|
|
550
|
+
live.update(
|
|
551
|
+
self._completion_panel(title, message, rows, steps, visible),
|
|
552
|
+
refresh=True,
|
|
553
|
+
)
|
|
554
|
+
time.sleep(0.05)
|
|
555
|
+
|
|
556
|
+
def _completion_panel(
|
|
557
|
+
self,
|
|
558
|
+
title: str,
|
|
559
|
+
message: str,
|
|
560
|
+
rows: list[tuple[str, Any]],
|
|
561
|
+
steps: list[str],
|
|
562
|
+
visible: int,
|
|
563
|
+
) -> Panel:
|
|
564
|
+
"""Build the completion panel with only its first ``visible`` rows shown."""
|
|
565
|
+
glyphs = self.glyphs
|
|
566
|
+
body = Text()
|
|
567
|
+
body.append(f"{glyphs.check} ", style="tenxgraph.success")
|
|
568
|
+
body.append(message, style="tenxgraph.command")
|
|
569
|
+
|
|
570
|
+
shown = 0
|
|
571
|
+
if rows:
|
|
572
|
+
label_width = max(len(str(key)) for key, _ in rows) + 2
|
|
573
|
+
body.append("\n")
|
|
574
|
+
for key, value in rows:
|
|
575
|
+
if shown >= visible:
|
|
576
|
+
break
|
|
577
|
+
body.append(f"\n {key!s:<{label_width}}", style="tenxgraph.muted")
|
|
578
|
+
body.append(str(value), style="tenxgraph.command")
|
|
579
|
+
shown += 1
|
|
580
|
+
|
|
581
|
+
if steps and visible > len(rows):
|
|
582
|
+
body.append("\n\n")
|
|
583
|
+
body.append("Next", style=f"bold {ACCENT}")
|
|
584
|
+
for step in steps:
|
|
585
|
+
if shown >= visible:
|
|
586
|
+
break
|
|
587
|
+
body.append(f"\n {glyphs.arrow} ", style="tenxgraph.accent")
|
|
588
|
+
body.append(step, style="tenxgraph.command")
|
|
589
|
+
shown += 1
|
|
590
|
+
|
|
591
|
+
return Panel(
|
|
592
|
+
body,
|
|
593
|
+
title=Text(f" {title} ", style="tenxgraph.title"),
|
|
594
|
+
title_align="left",
|
|
595
|
+
box=box.ROUNDED,
|
|
596
|
+
border_style=LINE,
|
|
597
|
+
padding=(1, 2),
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
output = OutputFormatter()
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def print_banner(title: str, subtitle: str | None = None, color: str = LINE) -> None:
|
|
605
|
+
output.print_banner(title, subtitle, color)
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def success(message: str, emoji: bool = True) -> None:
|
|
609
|
+
output.success(message, emoji)
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def error(message: str, emoji: bool = True) -> None:
|
|
613
|
+
output.error(message, emoji)
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def info(message: str, emoji: bool = True) -> None:
|
|
617
|
+
output.info(message, emoji)
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def warning(message: str, emoji: bool = True) -> None:
|
|
621
|
+
output.warning(message, emoji)
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def emphasize(message: str) -> None:
|
|
625
|
+
output.emphasize(message)
|