File size: 10,969 Bytes
d5dc912
 
 
 
 
 
 
 
 
 
 
 
 
 
bb6606d
 
 
 
 
 
d5dc912
 
 
 
 
 
 
 
 
 
 
 
 
 
bb6606d
 
 
d5dc912
 
bb6606d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d5dc912
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bb6606d
 
 
 
d5dc912
 
 
bb6606d
d5dc912
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
# SPDX-License-Identifier: MIT
"""Byte Agent CLI — Main dispatcher.

Registers all command groups and dispatches to their handlers.
Global flags (--json, --url, --token, --channel) are parsed here
and injected into every command via the resolved CLIConfig.
"""

from __future__ import annotations

import argparse
import sys
from typing import Final

from rich.columns import Columns
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from rich.text import Text

from cli import __version__
from cli.client import (
    AuthenticationError,
    ByteClient,
    CLIClientError,
    ConnectionError_,
    NotFoundError,
    ServerError,
)
from cli.config import CLIConfig, load_config
from cli.formatters import print_error

PROG_NAME: Final[str] = "bytecli"

DESCRIPTION: Final[str] = """
[bold magenta]Byte Agent CLI[/bold magenta] [bold cyan]v{version}[/bold cyan] 🤖
[italic white]Controle total sobre o seu Agente Byte no Twitch.[/italic white]
"""

# ---------------------------------------------------------------------------
# Rich Help Formatter
# ---------------------------------------------------------------------------


def print_rich_help(parser: argparse.ArgumentParser) -> None:
    """Print a beautiful help screen using Rich."""
    console = Console()

    # Header / Description
    console.print(
        Panel(DESCRIPTION.format(version=__version__), border_style="magenta", expand=False)
    )

    # Commands Section
    console.print("\n[bold yellow]🚀 COMANDOS PRINCIPAIS[/bold yellow]")

    # Categorized Command Table
    def create_cmd_table(title: str, color: str) -> Table:
        table = Table(
            title=f"[bold {color}]{title}[/bold {color}]",
            show_header=True,
            header_style=f"bold {color}",
            box=None,
            padding=(0, 2),
        )
        table.add_column("Comando", style="cyan", width=25)
        table.add_column("Descrição", style="white")
        return table

    # Lifecycle & Status
    t_life = create_cmd_table("🛸 CICLO DE VIDA & STATUS", "green")
    t_life.add_row("status", "Saúde do agente, modo e métricas")
    t_life.add_row("agent suspend", "Suspende o agente (ex: --reason maintenance)")
    t_life.add_row("agent resume", "Retoma as atividades")
    t_life.add_row("agent tick", "Força um ciclo de autonomia")
    t_life.add_row("agent config", "Ver/Alterar configurações do Control Plane")

    # Observability & Goals
    t_obs = create_cmd_table("📊 MONITORAMENTO & METAS", "blue")
    t_obs.add_row("observe", "Snapshot de telemetria e custos")
    t_obs.add_row("observe sentiment", "Vibe e sentimentos do chat")
    t_obs.add_row("goals list", "Listar todas as metas ativas")
    t_obs.add_row("actions list", "Ver fila de ações pendentes")

    # Channel & Memory
    t_chan = create_cmd_table("🧠 CANAL & MEMÓRIA", "magenta")
    t_chan.add_row("channel context", "Contexto atual do canal (jogo, vibe)")
    t_chan.add_row("channel notes", "Notas persistentes do agente")
    t_chan.add_row("memory search", "Busca na memória semântica")
    t_chan.add_row("persona show", "Perfil e tom de voz da persona")

    # Others (Clips, Playbooks, Webhooks)
    t_others = create_cmd_table("🛠️ FERRAMENTAS & OPS", "yellow")
    t_others.add_row("clips jobs", "Status do pipeline de clips")
    t_others.add_row("playbooks list", "Lista playbooks de operação")
    t_others.add_row("webhooks list", "Gerenciar webhooks de saída")
    t_others.add_row("report generate", "Gerar relatório pós-stream")

    # Render Tables in Columns
    console.print(Columns([t_life, t_obs, t_chan, t_others], equal=True, expand=True))

    # Global Options Section
    console.print("\n[bold white]⚙️ OPÇÕES GLOBAIS[/bold white]")
    opt_table = Table(box=None, show_header=False, padding=(0, 2))
    opt_table.add_column("Flag", style="bold cyan")
    opt_table.add_column("Descrição", style="dim")
    opt_table.add_row("--json", "Saída em JSON bruto para automação")
    opt_table.add_row("--url URL", "Sobrescreve a URL da API (ou BYTE_API_URL)")
    opt_table.add_row("--token TK", "Token de admin (ou BYTE_ADMIN_TOKEN)")
    opt_table.add_row("--channel CH", "Nome do canal alvo (default: 'default')")
    console.print(opt_table)

    # Usage Example
    console.print(
        Panel(
            "[bold cyan]Exemplo rápido:[/bold cyan]\n"
            "[white]python -m cli [bold green]status[/bold green][/white]\n"
            '[white]python -m cli [bold magenta]chat[/bold magenta] [italic]"Oi Byte, faz um ASCII do Goku"[/italic][/white]',
            title="[bold yellow]💡 DICA[/bold yellow]",
            border_style="yellow",
            expand=False,
        )
    )

    console.print(
        f"\n[dim white]Use 'bytecli <comando> --help' para detalhes. [bold magenta]Byte Agent v{__version__}[/bold magenta][/dim white]"
    )


# ---------------------------------------------------------------------------
# Command Registration
# ---------------------------------------------------------------------------


def _register_all_commands(subparsers: argparse._SubParsersAction) -> None:
    """Register every command group."""
    # Imports are deferred to avoid circular deps and keep startup fast
    from cli.commands.actions import register as reg_actions
    from cli.commands.channel import register as reg_channel
    from cli.commands.chat import register as reg_chat
    from cli.commands.clips import register as reg_clips
    from cli.commands.control import register as reg_control
    from cli.commands.conversions import register as reg_conversions
    from cli.commands.goals import register as reg_goals
    from cli.commands.memory import register as reg_memory
    from cli.commands.observe import register as reg_observe
    from cli.commands.persona import register as reg_persona
    from cli.commands.playbooks import register as reg_playbooks
    from cli.commands.report import register as reg_report
    from cli.commands.status import register as reg_status
    from cli.commands.webhooks import register as reg_webhooks

    reg_status(subparsers)
    reg_control(subparsers)
    reg_observe(subparsers)
    reg_goals(subparsers)
    reg_actions(subparsers)
    reg_channel(subparsers)
    reg_memory(subparsers)
    reg_persona(subparsers)
    reg_clips(subparsers)
    reg_playbooks(subparsers)
    reg_webhooks(subparsers)
    reg_report(subparsers)
    reg_chat(subparsers)
    reg_conversions(subparsers)


# ---------------------------------------------------------------------------
# Parser Builder
# ---------------------------------------------------------------------------


def build_parser() -> argparse.ArgumentParser:
    """Build the top-level argument parser with all subcommands."""
    parser = argparse.ArgumentParser(
        prog=PROG_NAME,
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )

    parser.add_argument(
        "--version",
        action="version",
        version=f"%(prog)s {__version__}",
    )
    parser.add_argument(
        "--json",
        dest="json_output",
        action="store_true",
        default=False,
        help="Output raw JSON instead of human-readable tables. Useful for piping to jq or other tools.",
    )
    parser.add_argument(
        "--url",
        dest="api_url",
        default=None,
        metavar="URL",
        help="Override the agent API URL. Default: http://localhost:7860. Can also be set via BYTE_API_URL env var or ~/.byterc.",
    )
    parser.add_argument(
        "--token",
        dest="api_token",
        default=None,
        metavar="TOKEN",
        help="Override the admin authentication token. Can also be set via BYTE_ADMIN_TOKEN or BYTE_DASHBOARD_ADMIN_TOKEN env var or ~/.byterc.",
    )
    parser.add_argument(
        "--hf-token",
        dest="hf_token",
        default=None,
        metavar="HF_TOKEN",
        help="Hugging Face Bearer token for accessing Private Spaces. Can also be set via HF_TOKEN env var or ~/.byterc.",
    )
    parser.add_argument(
        "--channel",
        dest="channel",
        default=None,
        metavar="CHANNEL",
        help="Target channel name for multi-channel operations. Default: 'default'. Most commands accept this to scope data to a specific Twitch channel.",
    )

    subparsers = parser.add_subparsers(
        title="commands",
        dest="command",
        metavar="<command>",
        description="Use 'bytecli <command> --help' for detailed help on each command.",
    )

    _register_all_commands(subparsers)

    return parser


# ---------------------------------------------------------------------------
# Execution Core
# ---------------------------------------------------------------------------


def _resolve_config(args: argparse.Namespace) -> CLIConfig:
    """Build CLIConfig from parsed CLI args + file + env."""
    cfg = load_config(
        cli_url=args.api_url,
        cli_token=args.api_token,
        cli_channel=args.channel,
        cli_json=args.json_output,
    )
    # Apply hf_token if present in args
    hf_tok = getattr(args, "hf_token", None)
    if hf_tok is not None:
        cfg = cfg.with_overrides(hf_token=hf_tok)
    return cfg


def _run(args: argparse.Namespace, config: CLIConfig) -> int:
    """Execute the dispatched command handler. Returns exit code."""
    handler = getattr(args, "handler", None)
    if handler is None:
        return 1  # no command selected

    client = ByteClient.from_config(config)

    try:
        handler(args, client, config)
        return 0
    except AuthenticationError as exc:
        print_error(f"Authentication failed: {exc}")
        print_error("Check your --token or BYTE_ADMIN_TOKEN environment variable.")
        return 2
    except ConnectionError_ as exc:
        print_error(f"Cannot reach agent: {exc}")
        print_error(f"Is the agent running at {config.url}?")
        return 3
    except NotFoundError as exc:
        print_error(f"Endpoint not found: {exc}")
        return 4
    except ServerError as exc:
        print_error(f"Server error: {exc}")
        return 5
    except CLIClientError as exc:
        print_error(f"API error: {exc}")
        return 6
    except KeyboardInterrupt:
        print_error("Interrupted.")
        return 130


# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------


def main(argv: list[str] | None = None) -> None:
    """CLI entry point."""
    parser = build_parser()

    # Override standard help to use our colorful Rich version
    parser.print_help = lambda file=None: print_rich_help(parser)

    args = parser.parse_args(argv)

    if args.command is None:
        print_rich_help(parser)
        sys.exit(0)

    config = _resolve_config(args)
    exit_code = _run(args, config)
    sys.exit(exit_code)