Skip to main content
CLI commands print human-readable output to the terminal via Rich. cli.log is the machine-readable counterpart: every significant CLI invocation writes a structured CSV row to <workspace>/logs/cli.log using the same hiro_commons.log.Logger infrastructure as server.log.

System overview

hirocli command (@app.command)
  └─ Logger.get("CLI.<MODULE>").info("hirocli <cmd>", key=val)

       └─ _CsvRenderer  ──────────→  <workspace>/logs/cli.log  (CSV file)

                                              ├─ LogTailTool   ──→  admin UI live tail
                                              ├─ LogSearchTool ──→  admin UI search / CLI / AI agent
                                              └─ hirocli logs search --source cli
Logging lives in the commands layer (commands/*.py), not the tools layer. This is critical because Tool classes are shared between CLI processes and the in-server AI agent. Logging inside tools would cause double-writes and log contamination when the agent invokes the same tools.

Module-prefix routing

Log events are routed to the correct file by module prefix, configured centrally in Logger.open_log_dir():
Module prefixDestination fileRule
CLI.*cli.loginclude_prefix="CLI."
Everything elseserver.logexclude_prefix=("CLI.",)
This means Logger.get("CLI.SERVER").info(...) automatically lands in cli.log — no per-call sink setup needed. In foreground mode (where CLI and server share a process), the routing prevents CLI events from leaking into server.log and vice versa.

Source files

LayerFile
Logger API + routinghiro-commons/src/hiro_commons/log.py
App-level callback (opens sinks)hirocli/src/hirocli/commands/app.py
Command wrappers (log here)hirocli/src/hirocli/commands/root.py, channel.py, device.py, workspace.py
Log reading toolshirocli/src/hirocli/tools/logs.py
Admin UI pagehirocli/src/hirocli/ui/pages/logs.py

Relationship to server-logging

cli.log uses the exact same CSV format as server.log:
timestamp,level,module,message,extra
Both are created by Logger.open_log_dir(log_dir) and read by LogTailTool / LogSearchTool. The only difference is the source process: server.log is written by the long-running server process; cli.log is written by short-lived CLI invocations. See Server logging for the full CSV format spec and Logger API.

Module names reference

Each command group uses a fixed CLI.* module identifier. The CLI. prefix lets you filter all CLI activity in one query, or narrow to a specific group.
ModuleCoversSource file
CLI.SETUPsetup commandcommands/root.py
CLI.SERVERstart, stop, restart, teardown, uninstall commandscommands/root.py
CLI.CHANNELAll channel subcommandscommands/channel.py
CLI.DEVICEdevice add, device revokecommands/device.py
CLI.WORKSPACEworkspace create, workspace remove, workspace updatecommands/workspace.py

Log events reference

The message field is the CLI command path (e.g. hirocli start). The extra field contains the actual flags and parameters, plus key outcome fields.

Server lifecycle

MessageModuleKey extras
hirocli setupCLI.SETUPworkspace, gateway_url, http_port, skip_autostart, elevated_task, start_server, device_id, autostart
hirocli startCLI.SERVERworkspace, foreground, admin, already_running, pid, http_port
hirocli stopCLI.SERVERworkspace, was_running, pid
hirocli restartCLI.SERVERworkspace, foreground, admin, was_running, old_pid, new_pid, http_port
hirocli teardownCLI.SERVERworkspace, purge, autostart_removed
hirocli uninstallCLI.SERVERworkspace, purge, autostart_removed

Channel management

MessageModuleKey extras
hirocli channel installCLI.CHANNELchannel, package, editable
hirocli channel setupCLI.CHANNELchannel, enabled, command
hirocli channel enableCLI.CHANNELchannel
hirocli channel disableCLI.CHANNELchannel
hirocli channel removeCLI.CHANNELchannel, found

Device management

MessageModuleKey extras
hirocli device addCLI.DEVICEttl_seconds, code_length, expires_at
hirocli device revokeCLI.DEVICEdevice_id, found
The pairing code itself is not logged — it is a short-lived secret used for device onboarding.

Workspace management

MessageModuleKey extras
hirocli workspace createCLI.WORKSPACEname, path, set_default
hirocli workspace removeCLI.WORKSPACEworkspace, purge
hirocli workspace updateCLI.WORKSPACEworkspace, name, set_default, gateway_url

Viewing CLI logs

Admin UI

The admin UI log viewer at /logs shows a CLI source chip alongside Server, Channels, and Gateway. The chip appears only when cli.log exists. Toggle it on or off just like any other source.

CLI

# All CLI activity
hirocli logs search --source cli

# Only errors from CLI commands
hirocli logs search --source cli --level ERROR

# Find all setup operations
hirocli logs search --source cli --module CLI.SETUP

# Find when the server was last started
hirocli logs search --source cli "hirocli start"

AI agent

The LogSearchTool and LogTailTool both accept source="cli" (or source="all" to include CLI alongside the other sources). The agent can answer questions like:
"When was the server last restarted?"
"Has the telegram channel been set up?"
"What device IDs have been revoked?"

How to add new log points

Add logging in the command function (@app.command), before calling Tool.execute():
@channel_app.command("my-command")
def my_command(workspace: Optional[str] = ..., flag: bool = False) -> None:
    """My new command."""
    Logger.get("CLI.MY_MODULE").info("hirocli my-command", flag=flag)
    result = MyTool().execute(workspace=workspace, ...)
    console.print(f"[green]Done.[/green]")
Sink setup is handled automatically by Logger.open_log_dir() in the _cli_init callback — no manual sink initialisation needed.

Why log before execute?

  1. Correct chronological order — the CLI entry appears before any server-side logs the tool may trigger (e.g. server startup).
  2. No foreground double-logging — prefix routing ensures CLI.* events go only to cli.log, even when the server adds server.log in the same process.

Two rules

  1. Never log inside Tool.execute() — tools are shared between the CLI process and the AI agent running inside the server.
  2. Always log before the tool call — this ensures correct timestamp ordering and clean separation of log files.
The only exception is commands that create the workspace (e.g. setup, workspace create), where the path doesn’t exist yet — those call Logger.open_log_dir() after the tool, then log. Follow the module names reference conventions: use CLI.<COMPONENT> in ALL-CAPS, ≤ 12 characters total.